M5Stick-VとM5Stick-Cを繋ぎ、Alexaスマートホームスキルとして使用できる顔認識デバイスを作ってみた

# はじめに

M5Stick-Vを使おうと思ったキッカケは、Alexaスマートホームスキルの起動トリガーとして、顔認識でAlexaスキルが起動できたら面白いと思ったから。センサー用のスマートホームスキルを作成することで、外部のデバイスやアプリをセンサーに見立ててAlexaを操作する事ができるとわかったのだけど、そもそもセンサーも作ったことなければ、顔認識もやったことなかったので、何から手をつければ良いかわからなかった。なので、こういったデバイスというか機械に詳しい人に相談したところ、M5Stick-Vだったら簡単にできると教えてもらったので、これを使うことにした。

M5StickとGroveケーブルは、[マルツ秋葉原本店](https://www.marutsu.co.jp/pc/static/shop/akihabara)で購入。やっぱり実物を見て購入できる方が好きかな。この店は、20:00まで営業しているので、会社帰りに立ち寄れるので嬉しい。※営業時間は確認してください。

# 前提

- [Alexaスマートホームスキルが作れる](https://qiita.com/takara1121/items/712d0db50be903c43c73)
- Arduino Software (IDE)
- Python

# 使用機器

- M5Stick-V
- M5Stick-C
- Groveケーブル

# 全体像

全体のシステムイメージはこんな感じ。

![全体図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/75176961-b0c3-5977-b6f2-c268d67ea716.png)


# 流れ

1. [M5Stick-Vで顔認識するためのモデル作成](#M5Stick-Vで顔認識するためのモデル作成)
2. [M5Stick-Vプログラム作成](#M5Stick-Vプログラム作成)
3. [M5Stick-Cプログラム作成](#M5Stick-Cプログラム作成)
4. [M5Stick-VとM5Stick-Cを接続](#M5Stick-VとM5Stick-Cを接続)
5. [テスト](#テスト)


# M5Stick-Vで顔認識するためのモデル作成

M5Stic-Vの物体認識モデルを作成する。手順は下図の通りで、簡単に書くと次のようになる。

1. `Material Training`する（`Shoot...`の部分）
2. Zipで固め[V-Trainingサイト](http://v-training.m5stack.com/)にアップする（`ZIP & Upload`の部分）
3. モデルができたらダウンロードする（`Download Model`の部分）

![モデル作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/87b70c04-c88f-21ff-2785-db4c5a1d0075.png)

### ※Zipの注意点
このように`train`と`vaild`を選択してZIPする。はじめはこれがわからず`train`と`vaild`の上位フォルダをZIPしてアップロードしたのだが、それではモデルを作成してくれずにエラーとなってしまった。
　<img width="50%" alt="Zip" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/e9e68945-77c9-7cdc-c241-2bc04a2224db.png">

# M5Stick-Vプログラム作成

M5Stick-VはWi-Fi機能を持っていないため、外部送信できない。ではどうするか？ネットを漁っていたところ、M5Stick-VとM5Stick-Cを繋いでM5Stick-Cが通信機能を担うといった記事があったため、さっそく参考にした。

今回は、僕と子供の２人分の顔情報を登録して判別できるようにした。念のためもう１人分、まったく関係ない人物を登録し、elseとして判別するようにしたのでプログラムは`labels=["1","2","3"]`となっている。

プログラムの大部分は`Download Model`でダウンロードした時の`boot.py`のままで、変更したところはM5Stick-Cとの通信部分の追加と、閾値の変更`pmax > 0.98:`ぐらい。マイコン初心者なのでこの程度しかできない。

```Python:M5Stick-Cへ送信
data_packet = bytearray([0xFF,0xD8,0xEA,0x01,0x00])
uart_Port.write(data_packet)
```

`0xFF,0xD8,0xEA`はパケットの識別をするためのもの。`0x01`は判別した人の番号（僕＝１、子供＝２）。`0x00`は予備。といっても使うことはないと思われる。と書いたものの、以下のサイトの情報をただパクっただけ・・・
- [M5StickVとM5StickCでお知らせ機能付きエッジAIカメラを作った](https://qiita.com/MikH/items/4da4d9237b79a7a201be)
- [Wi-FiがないM5StickVを、M5StickCと繋ぎLINEに投稿してみるまでの手順](https://qiita.com/nnn112358/items/5efd926fea20cd6c2c43)



```Python:boot.py
import network
import socket
import image
import lcd
import sensor
import sys
import time
import KPU as kpu
from Maix import GPIO
from fpioa_manager import fm, board_info
from machine import UART

lcd.init()
lcd.rotation(2)

try:
    from pmu import axp192
    pmu = axp192()
    pmu.enablePMICSleepMode(True)
except:
    pass

try:
    img = image.Image("/sd/startup.jpg")
    lcd.display(img)
except:
    lcd.draw_string(lcd.width()//2-100,lcd.height()//2-4, "Error: Cannot find start.jpg", lcd.WHITE, lcd.RED)

task = kpu.load("/sd/3ca317adcac44597_mbnet10_quant.kmodel")
labels=["1","2","3"] #You can check the numbers here to real names.

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((224, 224))
sensor.run(1)

#M5StickV GPIO_UART
fm.register(35, fm.fpioa.UART2_TX, force=True)
fm.register(34, fm.fpioa.UART2_RX, force=True)
uart_Port = UART(UART.UART2, 115200,8,0,0, timeout=1000, read_buf_len= 4096)

lcd.clear()

while(True):
    img = sensor.snapshot()
    fmap = kpu.forward(task, img)
    plist=fmap[:]
    pmax=max(plist)
    max_index=plist.index(pmax)
    a = lcd.display(img)

    if pmax > 0.98:
        if labels[max_index].strip() == '1' :
            lcd.draw_string(40, 60, "Accu:%.2f Type:%s"%(pmax, labels[max_index].strip()))
            lcd.draw_string(40, 70, "Person A")
            data_packet = bytearray([0xFF,0xD8,0xEA,0x01,0x00])
            uart_Port.write(data_packet)
            time.sleep(5)
        elif labels[max_index].strip() == '2' :
            lcd.draw_string(40, 60, "Accu:%.2f Type:%s"%(pmax, labels[max_index].strip()))
            lcd.draw_string(40, 70, "Person B")
            data_packet = bytearray([0xFF,0xD8,0xEA,0x02,0x00])
            uart_Port.write(data_packet)
            time.sleep(5)
a = kpu.deinit(task)

#   Send UART End
uart_Port.deinit()
del uart_Port
print("finish")
```

# M5Stick-Cプログラム作成

#### 処理の流れ
1. M5Stick-Vからパケットが来たら
2. [LWAから新しいTOKENを取得して](https://qiita.com/takara1121/items/e2bc22f0f51068c40d25)
3. [AlexaイベントゲートウェイへChangeReportを送信する](https://qiita.com/takara1121/items/e2bc22f0f51068c40d25)

```C++:パケット判別
    uint8_t rx_buffer[5];
    int rx_size = serial_ext.readBytes(rx_buffer, 4);
    if (rx_size == 4) {
      if ((rx_buffer[0] == packet_begin[0]) && (rx_buffer[1] == packet_begin[1]) && (rx_buffer[2] == packet_begin[2]))
```

対象のパケット（先頭3バイトが`0xFF,0xD8,0xEA`）を受信したら、`Login with Amazon`から新しい`TOKEN`を取得して、`Alexa Event Gataway`に`ChangeReport`を[送信](https://qiita.com/takara1121/items/e2bc22f0f51068c40d25)する。

```C++:4バイト目の数値で処理を判別
        if ((int)rx_buffer[3] == 1) {
          ：
        } else if ((int)rx_buffer[3] == 2) {
```

4バイト目が１か２で処理を分ける。下のコードは、１でも２でも処理内容は同じにしている。それ以外の場合は処理をしない。

```C++:SendToAlexaEventGataway.ino
#include <M5StickC.h>
#include <WiFi.h>
#include <ssl_client.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <time.h>

// Wi-Fi
const char* ssid = "<YOUR SSID>";
const char* passwd = "<YOUR SSID PASSWORD>";
struct tm timeInfo;//時刻オブジェクト
char ISO8601[22];  //ISO8601形式の日付文字列

// Amazon API
const size_t capacity = 1024;
const char* host_amazon_api = "api.amazon.com";
String access_token = "";
String refresh_token = "<LWA REFRESH TOKEN>";
String alexa_client_id = "<ALEXA SMARTHOME SKILL CLIENT ID>";
String alexa_client_secret = "<ALEXA SMARTHOME SKILL CLIENT SECRET>";

// Amazon SmartHome EventGateway
const char* host_eventgateway = "api.fe.amazonalexa.com";

// Packet marker
static const uint8_t packet_begin[3] = { 0xFF, 0xD8, 0xEA };

HardwareSerial serial_ext(2);

// the setup routine runs once when M5StickC starts up
void setup() {
  M5.begin();  // initialize the M5StickC object
  M5.Lcd.setRotation(1);
  M5.Lcd.setCursor(10, 0, 2);
  M5.Lcd.println("Send 2 Alexa EventGW");
  
  setup_wifi();  // Wi-Fi
  configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");  // NTP
  serial_ext.begin(115200, SERIAL_8N1, 32, 33);
}

// the loop routine runs over and over again forever
void loop(){
  M5.update();
  if (serial_ext.available()>0) {
    uint8_t rx_buffer[5];
    int rx_size = serial_ext.readBytes(rx_buffer, 4);
    if (rx_size == 4) {
      if ((rx_buffer[0] == packet_begin[0]) && (rx_buffer[1] == packet_begin[1]) && (rx_buffer[2] == packet_begin[2])) {
        if ((int)rx_buffer[3] == 1) {
          access_token = getAccessToken();
          if (access_token.length() > 0) {
            sendToEventgateway(access_token);
            Serial.println(" ChangeReport sent to Alexa Event Gateway Server");
          }
        } else if ((int)rx_buffer[3] == 2) {
          access_token = getAccessToken();
          if (access_token.length() > 0) {
            sendToEventgateway(access_token);
            Serial.println(" ChangeReport sent to Alexa Event Gateway Server");
          }
        } else {
          Serial.println(" TARGET OTHER");
        }
      }
    }
  }
  vTaskDelay(2000  / portTICK_RATE_MS);  
}

void setup_wifi() {
  Serial.println();  // We start by connecting to a WiFi network
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, passwd);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

String getAccessToken() {
  WiFiClientSecure client;
  Serial.println("Try");
  if (!client.connect(host_amazon_api, 443)) {
    Serial.println("Connection failed");
    return "";
  }
  Serial.println("Connected");

  String query_amazon_api = String("");
         query_amazon_api += "grant_type=refresh_token";
         query_amazon_api += "&refresh_token=" + refresh_token;
         query_amazon_api += "&client_id=" + alexa_client_id;
         query_amazon_api += "&client_secret=" + alexa_client_secret;
  String request_amazon_api = String("");
         request_amazon_api += "POST /auth/o2/token HTTP/1.1\r\n";
         request_amazon_api += "Host: " + String(host_amazon_api) + "\r\n";
         request_amazon_api += "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n";
         request_amazon_api += "Content-Length: " + String(query_amazon_api.length()) +  "\r\n";
         request_amazon_api += "\r\n";
         request_amazon_api += query_amazon_api + "\r\n";
  client.print(request_amazon_api);
  Serial.println(request_amazon_api);

  // Skip header
  while (client.connected()) {
    String line = client.readStringUntil('\n');
    Serial.println(line);
    line.trim();
    if (line.length() == 0) {
      break;
    }
  }
  String buffer="";
  while (client.connected()) {
    String line = client.readStringUntil('\n');
    Serial.println(line);
    buffer.concat(line);
    line.trim();
    if (line.length() == 0) {
      break;
    }
  }
  String line = client.readStringUntil('\n');
  Serial.println(line);
  client.stop();  // Connection terminated

  DynamicJsonDocument doc(capacity);
  DeserializationError err = deserializeJson(doc, buffer.c_str());
  Serial.printf("DeserializationError:%s\n",err.c_str());
  const char* new_token = doc["access_token"];
  return String(new_token);
}

void sendToEventgateway(String token) {
  WiFiClientSecure client2;
  Serial.println("Try");
  if (!client2.connect(host_eventgateway, 443)) {
    Serial.println("Connection failed");
    return;
  }
  Serial.println("Connected");
  
  getLocalTime(&timeInfo); 
  sprintf(ISO8601, "%04d-%02d-%02dT%02d:%02d:%02d.00Z",
          timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
          timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);  // ISO8601形式にする  "2020-04-11T23:20:50.52Z"

  String query_eventgateway = String("");
         query_eventgateway += "{";
         query_eventgateway += "  \"context\": {},";
         query_eventgateway += "  \"event\": {";
         query_eventgateway += "    \"header\": {";
         query_eventgateway += "      \"messageId\": \"TS-000-000-328\",";
         query_eventgateway += "      \"namespace\": \"Alexa\",";
         query_eventgateway += "      \"name\": \"ChangeReport\",";
         query_eventgateway += "      \"payloadVersion\": \"3\"";
         query_eventgateway += "    },";
         query_eventgateway += "    \"endpoint\": {";
         query_eventgateway += "      \"scope\": {";
         query_eventgateway += "        \"type\":\"BearerToken\",";
         query_eventgateway += "        \"token\":\"" + token + "\"";
         query_eventgateway += "       },";
         query_eventgateway += "       \"endpointId\" :  \"sensor-001\"";
         query_eventgateway += "    },";
         query_eventgateway += "    \"payload\": {";
         query_eventgateway += "      \"change\": {";
         query_eventgateway += "        \"cause\": {";
         query_eventgateway += "          \"type\": \"PHYSICAL_INTERACTION\"";
         query_eventgateway += "        },";
         query_eventgateway += "        \"properties\": [";
         query_eventgateway += "          {";
         query_eventgateway += "            \"namespace\": \"Alexa.MotionSensor\",";
         query_eventgateway += "            \"name\": \"detectionState\",";
         query_eventgateway += "            \"value\": \"DETECTED\",";
         query_eventgateway += "            \"timeOfSample\": \"" + String(ISO8601) + "\",";
         query_eventgateway += "            \"uncertaintyInMilliseconds\": 0";
         query_eventgateway += "          }";
         query_eventgateway += "        ]";
         query_eventgateway += "      }";
         query_eventgateway += "    }";
         query_eventgateway += "  }";
         query_eventgateway += "}";
  String request_eventgateway = String("") +
         request_eventgateway += "POST /v3/events HTTP/1.1\r\n";
         request_eventgateway += "Host: " + String(host_eventgateway) + "\r\n";
         request_eventgateway += "Authorization: Bearer " + token + "\r\n";
         request_eventgateway += "Content-Length: " + String(query_eventgateway.length()) +  "\r\n";
         request_eventgateway += "Content-Type: application/json; charset=UTF-8\r\n";
         request_eventgateway += "\r\n";
         request_eventgateway += query_eventgateway + "\r\n";
  client2.print(request_eventgateway);
  Serial.println(request_eventgateway);

  while (client2.connected()) {
    String line = client2.readStringUntil('\n');
    Serial.println(line);
    if (line == "\r") {
      break;
    }
  }
  String line = client2.readStringUntil('\n');
  Serial.println(line);
  client2.stop();  // Connection terminated
}
```

# M5Stick-VとM5Stick-Cを接続

M5Stick-VとM5Stick-CをGrove用ケーブルで繋ぐだけで、特に設定等の作業は必要ないみたい。コンパクトにしたかったので、今回は5cmのケーブルを使用した。M5Stick-Vの方はコネクタが入りにくい？仕様みたいなのでコネクタのツメの部分をハサミで切り落として指すだけにした。


<img width="50%" alt="M5Stick-VとM5Stick-C" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/b85d0175-ed46-6c3f-aaea-5df80ca8fd3d.png">


# テスト

Alexaの[センサー用のスマートホームスキル](https://qiita.com/takara1121/items/712d0db50be903c43c73)と連動できるかテストした。

[![デモ](http://img.youtube.com/vi/rlXi_jlGO5s/0.jpg)](https://www.youtube.com/watch?v=rlXi_jlGO5s)


# おわりに

素人でも簡単に顔認識をすることができた。認識の精度うんぬんはあるかもしれないけど、手軽に短時間でこれだけのモノができるのだから素晴らしいと思った。
結果、時刻起動でもなく、モーションセンサー起動でもなく、人の顔をトリガーにしてスキルを起動する事ができた。最終的には以下のような感じにしようかな・・・と思っている。

<img width="50%" alt="ダンボー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/34991f99-d787-9c14-93cd-38e60f4c339f.png">


# 参考

- [M5StickVとM5StickCでお知らせ機能付きエッジAIカメラを作った](https://qiita.com/MikH/items/4da4d9237b79a7a201be)
- [Wi-FiがないM5StickVを、M5StickCと繋ぎLINEに投稿してみるまでの手順](https://qiita.com/nnn112358/items/5efd926fea20cd6c2c43)
- [M5Stack Docs - V-Training](https://docs.m5stack.com/#/en/related_documents/v-training)
- [マルツエレック株式会社](https://www.marutsu.co.jp/)

# 関連

- [Alexa センサー用のスマートホームスキルを作成し、定型アクションのトリガーとして利用する](https://qiita.com/takara1121/items/712d0db50be903c43c73)
- [Alexa スマートホームスキル イベントゲートウェイにChangeReportを送信する](https://qiita.com/takara1121/items/e2bc22f0f51068c40d25)
- [くすりの時間（Alexaスキル）](https://amzn.to/34JaQRS)