カメラとM5StickC、BLE、インターネットにも接続して自動判別機の完成!
第1回:M5Stackとカメラを使ってできること、必要なもの
第2回:M5StickVカメラとM5StickCをセットアップして写真を撮る
第3回:M5StickVカメラでAI機械学習
こんにちは、ヨシケンです!
今回の連載では、M5Stickとカメラをつなげて、AI機械学習からそこに映っている人を自動判別できる機能を搭載したカメラを作っていきたいと思います。最終回の今回は、M5StickVカメラとM5StickCをつなげて、別の部屋に居ても家に誰が来たか通知し、応答が出来るデバイスを完成させます。
今回の記事の流れ
このデバイスを作るのに必要なもの:
名前、説明 | デバイス |
---|---|
M5StickC Plus
ESP32を搭載したArduino互換機。ディスプレイ、BLE、Wifiおよびモーションセンサなどが始めから入っている。 |
|
M5StickV
カメラとAI機械学習が可能なチップをセットにしたデバイス(ESP32は内蔵しない) |
|
マイクロSDカード | 使用可能かどうかをこちらのサイトを参考に確認 |
SwitchBot BOT | |
GROVEケーブル、USB Type-Cケーブルなど | 必要に応じて適宜。 |
1. M5StickCのSwitchBotとの連動
前回記事ではM5StickVで画像判別ができるようになったので、もし誰かが来たらそれに応じてインターフォンの通話スイッチを押して、話せる状態にしようと思います。
外部から自動でスイッチを押せるようにするには、reTerminalの記事にも使ったSwitchBotというIoTデバイスが手軽に実現してくれます。SwitchBot製品には、電源タップや赤外線リモコンを使えるものなどがありますが、今回はスイッチを押せるボットというものを使用。これをインターホンの通話ボタンのところにセットすることにします。
このSwitchBotを操作するためにアプリをスマホにインストールして、外部から接続する為の設定を行います。
[ iPhoneアプリ https://apps.apple.com/jp/app/switchbot/id1087374760 ]
[Androidアプリ https://play.google.com/store/apps/details?id=com.theswitchbot.switchbot&hl=ja&gl=US&pli=1]
アプリをインストール後に、ボットを登録します(画面ショット中で赤枠のA1がボット)。
設定画面中の「デバイス情報」から、このボットのMAC情報を取得して、コピーしておきます。
では、このデバイス情報を使って、M5StickCからボットに接続してみましょう。
ここでM5StickC Plusのライブラリを追加しておきます。このGithubにアクセスして、ZIPのダウンロードを行い、Aruduino IDEでライブラリを追加してください。
https://github.com/m5stack/M5StickC-Plus
M5StickCのAボタン(画面下のM5と書かれたボタン)を押すと、BLE経由でボットにつながるようにします。その後ボットを動かして、ボタンを押すバーを一回前に出し、また戻るような信号を送ります。
以下のM5_SwitchBotスケッチ中の黄色い部分に、先ほど確認したMACアドレスをペーストしてください。またボットのバーを押して戻す場合は、コマンドを以下のように指定します。
static uint8_t cmdPress[3] = {0x57, 0x01, 0x00};
[M5_SwitchBot.ino]
#include <M5StickCPlus.h>
#include "BLEDevice.h"
#define BTN_A_PIN 37
#define BTN_ON LOW
#define BTN_OFF HIGH
uint8_t prev_btn_a = BTN_OFF;
uint8_t btn_a = BTN_OFF;
// SwitchBot MAC Address
static String MAC_SWITCHBOT = "MMMM";
// SwitchBot BLE
static BLEUUID SERV_SWITCHBOT("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
static BLEUUID CHAR_SWITCHBOT("cba20002-224d-11e6-9fb8-0002a5d5c51b");
static uint8_t cmdPress[3] = {0x57, 0x01, 0x00};
bool doScan = true;
BLEScan* pBLEScan;
static BLEAddress *pGattServerAddress;
static BLEAdvertisedDevice* myDevice;
static BLERemoteCharacteristic* pRemoteCharacteristic;
static BLEClient* pClient = NULL;
unsigned int sampling_period_us;
float dmax = 10000.0;
bool doDetection = false;
bool sendFlg = false;
bool cas = false;
// BLE connection
class MyClientCallback : public BLEClientCallbacks {
void onConnect(BLEClient* pclient) {
Serial.println("onConnect");
}
void onDisconnect(BLEClient* pclient) {
Serial.println("onDisconnect");
if (cas) {
esp_restart();
}
}
};
// Callback for advertising
class advdCallback: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
if (advertisedDevice.haveServiceUUID()) {
String addr = advertisedDevice.getAddress().toString().c_str();
Serial.printf("It have service addr: %s \n", advertisedDevice.getAddress().toString().c_str());
if (addr.equalsIgnoreCase(MAC_SWITCHBOT)) {
Serial.printf("found");
advertisedDevice.getScan()->stop();
pGattServerAddress = new BLEAddress(advertisedDevice.getAddress());
myDevice = new BLEAdvertisedDevice(advertisedDevice);
doScan = false;
doDetection = true;
}
}
}
};
void setup() {
Serial.begin(115200);
M5.begin();
pinMode(BTN_A_PIN, INPUT_PULLUP);
M5.Lcd.setTextSize(2);
BLEDevice::init("");
pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new advdCallback());
pBLEScan->setActiveScan(true);
pBLEScan->setInterval(100);
pBLEScan->setWindow(99); // less or equal setInterval value
}
void loop() {
if (doScan) {
Serial.printf(“Scan");
pBLEScan->start(5, false);
}
btn_a = digitalRead(BTN_A_PIN);
if(prev_btn_a == BTN_OFF && btn_a == BTN_ON && doScan == false){
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 0);
Serial.printf("Button!");
if (connectAndSendCommand(*pGattServerAddress)) {
Serial.printf("Done!");
sendFlg = false;
doScan = false;
} else {
Serial.printf("Failed.");
doScan = true;
delay(3000);
}
}
vTaskDelay(500 / portTICK_RATE_MS);
}
// Connect to SwitchBot Server
static bool connectAndSendCommand(BLEAddress pAddress) {
cas = true;
try {
pClient = BLEDevice::createClient();
pClient->setClientCallbacks(new MyClientCallback());
Serial.printf("Connect");
while (!pClient->connect(myDevice)) {
Serial.printf("Reconnect");
delay(1000);
}
BLERemoteService* pRemoteService = pClient->getService(SERV_SWITCHBOT);
if (pRemoteService == nullptr) {
Serial.printf("e:service not found");
return false;
}
pRemoteCharacteristic = pRemoteService->getCharacteristic(CHAR_SWITCHBOT);
if (pRemoteCharacteristic == nullptr) {
Serial.printf("e:characteristic not found");
return false;
}
pRemoteCharacteristic->writeValue(cmdPress, sizeof(cmdPress), false);
Serial.printf("Send");
cas = false;
delay(3000);
pClient->disconnect();
pClient = NULL;
delay(1000);
}
catch (...) {
Serial.printf("error");
if (pClient) {
pClient->disconnect();
pClient = NULL;
}
return false;
}
return true;
}
M5StickCにこのスケッチを書き込んで、BLE経由でSwitchBotに接続されるか確認してください。Aボタンを押したら、見事SwitchBotが動き、バーを一回動かして、また戻る動作をしたでしょうか?
2. M5StickCとIFTTT、LINEとの連動
それでは、M5StickVで判別した画像をM5StickCで受け取って、SwitchBotを動かし、更にLINEなどにその通知を送ることにします。
ここではBrownie Learn内にある、M5StickCでIFTTT(外部アプリと接続するWebサービス)に転送するスケッチがあるので、それを使用させてもらいます。
こちらにあるIFTTTConnector.inoスケッチをコピーして、M5_IFTTT_SwitchBot.inoとします。https://github.com/ksasao/brownie/blob/master/src/brownie_learn/M5StickC/IFTTTConnector/IFTTTConnector.ino
元のIFTTTConnectorプログラム中に、以下黄色の部分として先ほどのSwtichBotの記述を追加します。また、水色の部分はIFTTTにつなぐために変更する部分です。
[ M5_IFTTT_SwitchBot.ino ]
#include <M5StickCPlus.h>
#include <WiFi.h>
…
#include "BLEDevice.h"
static String MAC_SWITCHBOT = "MMMM";
static BLEUUID SERV_SWITCHBOT("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
static BLEUUID CHAR_SWITCHBOT("cba20002-224d-11e6-9fb8-0002a5d5c51b");
static uint8_t cmdPress[3] = {0x57, 0x01, 0x00};
bool doScan = true;
BLEScan* pBLEScan;
static BLEAddress *pGattServerAddress;
static BLEAdvertisedDevice* myDevice;
static BLERemoteCharacteristic* pRemoteCharacteristic;
static BLEClient* pClient = NULL;
unsigned int sampling_period_us;
float dmax = 10000.0;
bool doDetection = false;
bool sendFlg = false;
bool cas = false;
void log(String s) {
Serial.println(s);
M5.Lcd.println(s);
}
class MyClientCallback : public BLEClientCallbacks {
void onConnect(BLEClient* pclient) {
Serial.println("onConnect");
}
void onDisconnect(BLEClient* pclient) {
Serial.println("onDisconnect");
if (cas) {
esp_restart();
}
}
};
class advdCallback: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
if (advertisedDevice.haveServiceUUID()) {
String addr = advertisedDevice.getAddress().toString().c_str();
Serial.printf("It have service addr: %s \n", advertisedDevice.getAddress().toString().c_str());
if (addr.equalsIgnoreCase(MAC_SWITCHBOT)) {
log("found");
advertisedDevice.getScan()->stop();
pGattServerAddress = new BLEAddress(advertisedDevice.getAddress());
myDevice = new BLEAdvertisedDevice(advertisedDevice);
doScan = false;
doDetection = true;
}
}
}
};
static char* ssid = "XXXX"; // SSID
static char* pass = "YYYY"; // password
static char* ifttt = "ZZZZ"; // IFTTT key
…
void setup() {
M5.begin();
M5.Lcd.setRotation(1);
M5.Lcd.setTextSize(3);
initialize_wifi();
M5.Lcd.println("M5Cam Check");
serial_ext.begin(115200, SERIAL_8N1, 32, 33);
BLEDevice::init("");
pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new advdCallback());
pBLEScan->setActiveScan(true);
pBLEScan->setInterval(100);
pBLEScan->setWindow(99); // less or equal setInterval value
}
void loop() {
M5.update();
if (doScan) {
log("Scan");
pBLEScan->start(5, false);
}
if (serial_ext.available()) {
char d = serial_ext.read();
rx_buffer[rx_buffer_pointer] = d;
if(d == 0){
…
// Send to IFTTT
String rx_string = String(rx_buffer);
if(rx_buffer_pointer > 0){
if (rx_string == "okaeri" or rx_string == "haitatsu") {
if (rx_string == "okaeri") { M5.Lcd.fillScreen(GREEN);
} else if (rx_string == "haitatsu") { M5.Lcd.fillScreen(ORANGE);
} else { M5.Lcd.fillScreen(RED); }
M5.Lcd.setCursor(10, 10);
M5.Lcd.setTextColor(BLACK);
M5.Lcd.println(rx_string);
if (connectAndSendCommand(*pGattServerAddress)) {
log("Done!");
sendFlg = false;
doScan = false;
} else {
log("Failed.");
doScan = true;
delay(3000);
}
String url = url_pre + "M5Check" + url_post + String(ifttt) + "?value1=" + String(rx_buffer);
Serial.println(url);
http_get(url);
delay(3000);
M5.Lcd.fillScreen(TFT_BLACK);
M5.Lcd.setCursor(10, 10);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.println("M5 Camera is checking ...");
}
}
rx_buffer_pointer = 0;
}else{
if(rx_buffer_pointer < MAX_RX_BUFFER_SIZE-2){
rx_buffer_pointer++;
}
}
}
}
…
// Connect to SwitchBot Server
static bool connectAndSendCommand(BLEAddress pAddress) {
cas = true;
try {
pClient = BLEDevice::createClient();
pClient->setClientCallbacks(new MyClientCallback());
log("SwitchBot...");
while (!pClient->connect(myDevice)) {
log("reconnect");
delay(1000);
}
BLERemoteService* pRemoteService = pClient->getService(SERV_SWITCHBOT);
if (pRemoteService == nullptr) {
log("e:service not found");
return false;
}
pRemoteCharacteristic = pRemoteService->getCharacteristic(CHAR_SWITCHBOT);
if (pRemoteCharacteristic == nullptr) {
log("e:characteristic not found");
return false;
}
pRemoteCharacteristic->writeValue(cmdPress, sizeof(cmdPress), false);
log("Send");
cas = false;
delay(3000);
pClient->disconnect();
pClient = NULL;
delay(1000);
}
catch (...) {
log("error");
if (pClient) {
pClient->disconnect();
pClient = NULL;
}
return false;
}
return true;
}
IFTTTはLINEやTwitterなどと簡単に接続できるWebサービスです。今回はインターフォンに誰か来たら、その通知をIFTTT経由でLINEに送るようします。
IFTTTに接続するために、プログラム中のZZZZでIFTTT Keyが必要になるので、まず https://ifttt.com にアクセスしアカウントを取りましょう。そこから”Create”を押して新たなAppletを作成します。ここではM5CheckというEventNameで作っています。
まず、最初のIFとなるトリガーとして、Webhooksというものを選び、適当な”M5Check”というEvent nameを付けます。
次にその結果起こるTHENとして、LINEを選択。そしてLINE上のルーム(ここでは自分のM5Room)にメッセージを送るようにします。送信する文言としてEventNameとValue1を指定してください。これで判別したEventNameとValue1に応じたメッセージが、LINEに送られることになります。
また、ここでWebhooksのボタンを押して、右上の”Settings”から、IFTTTのKeyを取得します。
このURLにある/use/より後ろの黒くマスクした部分がKeyで、これをコピーしておいてください。以下のようにURL中のEventNameのところに先ほど設定した”M5Check”、ZZZZの部分にKey、Value1の後ろに判別した文字列を指定して使用します。
https://maker.ifttt.com/trigger/EventName/with/key/ZZZZ?value1=okaeri
“Settings”の中にURLのポストの仕方が書いてあります。
このURLをWebブラウザに貼り付けるだけで、外部からIFTTTを起動することができます。以下のように実行して、“Congratulations!”とブラウザ上にメッセージが出たら、正常に実行されています。LINEにちゃんとメッセージが送られているか確認してみてください。
Value1の後ろに付けた文字列がLINEに送られ、定義したメッセージが以下のように表示されるはずです。
この確認ができたら、M5_IFTTT_SwitchBot.inoスケッチの中にあるIFTTTKeyのZZZZのところに、IFTTTのKeyを設定しておいてください。
これでM5StickC側のプログラムは完成です。M5StickVとつないでインターホンの前にセットする準備をしておいてください。より小型のM5Atomでもほぼ同じプログラムで動かすことができます。
3. M5StickVから画像判別結果を送信、全てを連動させる
それではM5StickVからの画像読み取り、M5StickCへのデータ送信、インターフォンのスイッチを押す、LINE送付の一連の動作を全て連動させます。
M5StickVでの画像判別とデータ転送は、第3回で使ったBrownie Learnプログラムを使っています。
https://github.com/ksasao/brownie/blob/master/src/brownie_learn/M5StickV/boot.py
boot.pyプログラムの紫でハイライトした108-110行目のsend_packetファンクションで、M5StickCに対してUARTというシリアル信号を送れるようになっています。196行目で画像が判別できて物体からnameが取得できたら、このsend_packetファンクションをキックします。
# Brownie Learn for M5StickV / UnitV
…
def send_packet(message):
data_packet = bytearray(message+'\x00')
uart_Port.write(data_packet)
…
# get nearest target
name,dist,_ = get_nearest(feature_list,p)
if dist < 200 and name != "*exclude":
img.draw_rectangle(1,46,222,132,color=br.get_color(0,255,0),thickness=3)
img.draw_string(2, 47 +30, "%s"%(name), scale=3)
br.set_led(0,1,0)
if old_name != name:
print("[DETECTED]: " + name)
br.show_image(img)
send_packet(name)
time.sleep(5.0)
br.play_sound("/sd/voice/"+name+".wav")
old_name = name
else:
br.set_led(0,0,0)
if old_name != '':
send_packet('')
old_name = ''
…
これで、事前に登録した画像、判別結果から、以下のような信号が送られることになります。
写っているのが家族だったら:
name: “okaeri”
okaeri.wav: 「おかえり いま行きます」
写っているのがそれ以外(配達など)だったら:
name: “haitatsu”
haitatsu.wav: 「配達なら置き配お願いします」
ここで、196行目でsend_packetした後、M5StickCにデータが送られ、SwitchBotが起動するまでの間5秒ほど待つように、197行目にtime.sleep(5.0)を入れておきます。これによりMStickC側からボタンが押されて通話状態になった後に、wav音声が再生されて応答できるようになります。
それでは、M5StickVとM5SickCを一緒にインターホン前にセットして使ってみましょう。
さあ、これで動かしてみましょう。判別した物体のNameを読み上げるのですが、これはインターフォンに対する応答メッセージになっているので、一度判別してその結果をM5StickCに転送、SwitchBotでボタンを押す、という動作を待つために、5秒ほどSleepの処理を入れています。
帽子を被った青の服を着た人が映ると、配達員となるように画像を学習させています。その結果、このように動いています。
またそれ以外の家族なら、「おかえり!」と言ってくれると思います。
4. まとめ
今回の連載ではAI画像判別ができるM5StickVカメラを使って電子工作をおこないました。
このM5StickVは、インターネット接続などはありませんが、画像を取得し解析する、エッジAIをすることが可能です。この結果をM5StickCに送って、インターネットやBLEに接続できるようにしています。BLEを使って、SwitchBotというIoTデバイスを動かす事ができるので、外部の機器を操作できるようになります。
また、インターネットからIFTTTというWebサービスを使って、LINEなどに簡単に判別結果を送ることも可能。これらにより、インターフォンに画像が映ったら、それを解析し、その人に応じた応答やそれを外部に転送できるようになっています。
このように、カメラとM5Stackシリーズを使うと、さまざまなことができるようになります。アイデア次第でおもしろい電子工作ができるはずです。ぜひ、他にもいろいろ試してみてください!
今回の連載の流れ
第1回: M5Stackとカメラを使ってできること、必要なもの
第2回: M5StickVカメラとM5StickCをセットアップして写真を撮る
第3回: M5StickVカメラでAI機械学習
第4回: カメラとM5StickCを連動させて、インターネットにも接続して完成!(今回)