前々回の「Nest MiniでLocal Home SDKを使ってお家のIoTロボットを操作する 第2回」と、前回の「Nest MiniでLocal Home SDKを使ってお家のIoTロボットを操作する 第3回」では、コードラボの「Smart Home Local Execution」を一通りして、Nest Miniでローカル実行ができるようにしました。
今回は、仮想デバイス(仮想スマートウォッシャー)を、ピッコロボIoTに置き換えるように実装していきます。

ピッコロボIoTについては、「Nest MiniでLocal Home SDKを使ってお家のIoTロボットを操作する 第1回」をご覧ください。

ピッコロボIoTを検出できるようにする

仕様

Google Homeデバイス(Nest Mini)がローカルネットワーク上のデバイスを検知するために、UDPで通信(ブロードキャスト)を行っていました。
そのため、ピッコロボIoTはUDPで通信できるようにする必要があります。
Google Homeデバイス(Nest Mini)がUDPブロードキャストを送信するポートは3311でした。
また、DiscoveryPacketとして、「HelloLocalHomeSDK」という文字列を送ってくるので、その文字列をチェックする必要もあります。
Google Homeデバイス(Nest Mini)に、otherDeviceIdsで設定しているものと一致するDeviceIDを返す必要もあります。

  • UDPで通信できるようにする (ポートは3311)
  • DiscoveryPacketとして、「HelloLocalHomeSDK」という文字列を チェックする
  • otherDeviceIdsで設定しているものと一致するDeviceIDを返す

実装

Arduino IDEを起動し、新しくスケッチを作りましょう。
スケッチ名は何でも構わないですが、「LocalHomeSDK_PiccoroboIoT」と名前を付けました。(保存もしておいてください)

「udp.h」という名前のファイルを追加し、以下のように実装しました。

#include <WiFiUdp.h>
#include <Arduino.h>

#define DISCOVERY_PACKET "HelloLocalHomeSDK"  // DiscoveryPacketで設定した文字列
#define DEVICEID "deviceid123"  // otherDeviceIdsで設定しているものと一致するDeviceID

class LocalHomeUDP {
    public:
        WiFiUDP Udp;
        unsigned int localUdpPort = 3311;

        char incomingPacket[255];

        /*
         UDP通信開始
        */
        void begin() {
            Udp.begin(localUdpPort);
            Serial.printf("Now listening at UDP port %d\n", localUdpPort);
        }

        /*
         UDP通信処理
        */
        void task(){
            int packetSize = Udp.parsePacket();
            if (!packetSize) return;
            
            int len = Udp.read(incomingPacket, 255);
            Serial.printf("UDP packet contents: %s\n", incomingPacket);
            if (len > 0) {
                incomingPacket[len] = 0;
            }

            // 文字列部分(NULL終端まで)をコピーする
            // 最後にNULL終端を入れるので、コピー先の配列はコピー元文字列+1の要素数にする
            int messLen = strlen(incomingPacket) + 1;
            char mess[messLen];
            strncpy(mess, incomingPacket, messLen - 1);
            mess[messLen - 1] = 0;
            Serial.println(mess);

            // DiscoveryPacketと異なる場合は以降処理しない
            if(strcmp(mess, DISCOVERY_PACKET) != 0) {
                Serial.printf("The received message is not '%s'\n", DISCOVERY_PACKET);
                return;
            }
            Serial.println("The received message is ok");

            // DeviceIDを返す
            Udp.beginPacket(Udp.remoteIP(), Udp.remotePort());
            Udp.write(DEVICEID);
            Udp.endPacket();
        }
};

LocalHomeSDK_PiccoroboIoTに、udp.hをインクルードし、Wifi接続と、UDP通信できるように実装します。
WifiのSSIDとPASSWORDは適宜変更してください。

#include <vs-rc202.h>
#include <ESP8266WiFi.h>
#include "udp.h"

#define SSID "********"
#define PASSWORD "********"

LocalHomeUDP udp;

void setup() {
    Serial.begin(115200);
    // V-duino(VS-RC202)のライブラリを使用するための初期化処理
    initLib();
    
    // Wifi接続
    Serial.printf("Connecting to %s ", SSID);
    WiFi.begin(SSID, PASSWORD);
    while (WiFi.status() != WL_CONNECTED)
    {
      delay(500);
      Serial.print(".");
    }
    Serial.println(" connected");
    Serial.printf("IP : %s\n", WiFi.localIP().toString().c_str());

    // UDP通信開始
    udp.begin();
}

void loop() {
    // UDP通信処理
    udp.task();
}

動作確認

ピッコロボIoTにアップロードしたら、ncコマンドで動作確認してみます。
なお、Google Homeデバイス(Nest Mini)の電源が入っていると、Google Homeデバイス(Nest Mini)から通信してくるので、一旦電源を抜いておいた方が確認しやすいと思います。

まずはピッコロボIoTのIPアドレスを確認します。
Arduino IDEのシリアルモニタを開いて、ピッコロボIoTのリセットボタン(「V-duino 取扱説明書.pdf」の3ページ参照)を押してください。
「IP : 」と出力した後にIPアドレスが表示されていると思います。

PC(WSLを使用しています)からncコマンドを実行します。
「<IPアドレス>」は適宜置き換えてください。
まずは「test」という文字列を送ってみます。
echoコマンドでenオプションを使用することで、改行コードを送らない(\nと書かないと送られない)ようにしています。

echo -en "test" | nc -u <IPアドレス> 3311

シリアルモニタを確認すると、以下のように出力され、「HelloLocalHomeSDK」でないと判断されていることが分かります。

UDP packet contents: test
test
The received message is not 'HelloLocalHomeSDK'

確認ができたら、PC側でCtrl+Cでncコマンドから抜けます。

次に「HelloLocalHomeSDK」という文字列を送ってみます。

echo -en "HelloLocalHomeSDK" | nc -u <IPアドレス> 3311

シリアルモニタを確認すると、以下のように出力され、「HelloLocalHomeSDK」と判断されていることが分かります。

UDP packet contents: HelloLocalHomeSDK
HelloLocalHomeSDK
The received message is ok

また、PC側では、DeviceIDの「deviceid123」が返ってきているのが確認できます。

deviceid123

確認ができたら、PC側でCtrl+Cでncコマンドから抜けてください。

これで検出処理は良さそうです。

ピッコロボIoTがコマンドを受け取れるようにする

仕様

Google Homeデバイス(Nest Mini)からピッコロボIoTにコマンドを送るために、ピッコロボIoTはHttpサーバとして待受ける必要があります。
Google Homeデバイス(Nest Mini)は、ポートは3388で通信するように設定しているので、3388で待受けるようにします。

Google Homeデバイス(Nest Mini)からは、以下のようなJSONデータが送られてきます。
なお、すべての項目は送られてこず、変更があった(コマンド実行された)ものだけ送られてきます。

{
    "on": [bool値],
    "isRunning": [bool値],
    "isPaused": [bool値]
}

また、状態が変わったことをクラウド側に伝えるために、「https://<project-id>.firebaseapp.com/updateState」に状態をポストする必要があります。
ポストするデータは、上記のJSONデータと同じですが、すべての項目を送る必要があります。

  • Httpサーバとして待受ける (ポートは3388)
  • 変更があった項目がJSON形式で送られてくる
  • クラウド側に現在の状態をポストする

実装

ピッコロボIoTでJSONデータを扱うには、Arduinojsonというライブラリを使用します。まずはArduinojsonを追加しましょう。
「スケッチ > ライブラリをインクルード > ライブラリを管理」を選択してください。

ライブラリマネージャが表示されると思います。
「検索をフィルタ」と書かれたテキストボックスに「Arduinojson」を入力すると、一番上にArduinojsonが出てくると思います。
「インストール」ボタンを押下してください。これでインストールされ、Arduinojsonを使用できるようになります。

「httpsrv.h」という名前のファイルを追加し、以下のように実装しました。
REPORT_STATE_ENDPOINT_URLの「<project-id>」は自分のプロジェクトIDに置き換えてください。
FINGERPRINTの「<fingerprint>」はSHA1のフィンガープリントを記載します。
取得方法等は次に記載しています。

#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>
#include <ArduinoJson.h>
#include <Arduino.h>

#define LOCAL_HOME_SERVER_PORT 3388
#define REPORT_STATE_ENDPOINT_URL "https://<project-id>.firebaseapp.com/updateState"
// SHA1のフィンガープリント(「XX XX ... XX XX」といった形式になる)
#define FINGERPRINT "<fingerprint>"

class LocalHomeServer {
    public:
        ESP8266WebServer *server;
        void begin();
        void task();
        void reportState();
};

class Status {
    public:
        bool on;
        bool isRunning;
        bool isPaused;
};

LocalHomeServer localHomeSrv;
Status status;

/*
 Httpサーバ開始
*/
void LocalHomeServer::begin() {
    server = new ESP8266WebServer(LOCAL_HOME_SERVER_PORT);
    server->on("/", HTTP_POST,  [](){
        StaticJsonDocument<200> doc;
        JsonObject object = doc.as<JsonObject>();
        String json = localHomeSrv.server->arg("plain");
        deserializeJson(doc, json);
        Serial.println(json);

        // 変更があった項目のみ送られてくるので、存在するものだけ状態に反映する
        if(doc.containsKey("on")) {
            status.on = doc["on"];
        }
        if(doc.containsKey("isRunning")) {
            status.isRunning = doc["isRunning"];
        }
        if(doc.containsKey("isPaused")) {
            status.isPaused = doc["isPaused"];
        }
        Serial.printf("on:%d, isRunning:%d, isPaused:%d\n", status.on, status.isRunning, status.isPaused);

        localHomeSrv.reportState();

        localHomeSrv.server->send(200);
    });
    server->begin();
    Serial.printf("Http Server at port %d\n", LOCAL_HOME_SERVER_PORT);

    status.on = false;
    status.isRunning = false;
    status.isPaused = false;
}

/*
 待受け処理
*/
void LocalHomeServer::task() {
    server->handleClient();
}

/*
 クラウドへ状態を送信
*/
void LocalHomeServer::reportState() {
    StaticJsonDocument<200> doc;
    doc["on"] = status.on;
    doc["isRunning"] = status.isRunning;
    doc["isPaused"] = status.isPaused;
    String payload = "";
    serializeJson(doc, payload);
    Serial.println(payload);

    HTTPClient http;
    http.begin(REPORT_STATE_ENDPOINT_URL, FINGERPRINT);
    http.addHeader("Content-Type", "application/json");
    int respCode = http.POST(payload);
    if(0 < respCode) {
        Serial.printf("report state response code : %d\n", respCode);
    } else {
        Serial.printf("report state failed, error: %s\n", http.errorToString(respCode).c_str());
    }

    http.end();
}

httpsでリクエストを投げるには、SHA1のフィンガープリントが必要になります。
よくあるライブラリだと勝手にやってくれるんですが、Arduinoのライブラリには無いので、自前で用意する必要があります。
とりあえずフィンガープリントを取得し、ソースコードに直書きをします。
下記コマンドでフィンガープリントが取得できます。
「<project-id>」は自分のプロジェクトIDに置き換えてください。

openssl s_client -no_ssl3 -connect <project-id>.firebaseapp.com:443 < /dev/null 2>&1 \
 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' \
 | openssl x509 -noout -fingerprint -sha1 \
 | sed -e 's/SHA1 Fingerprint=//g' \
 | sed -e 's/:/ /g'

やっている流れは以下になります。以下で取得しても構わないです。

1. 下記コマンドを実行します。
「<project-id>」は自分のプロジェクトIDに置き換えてください。

openssl s_client -no_ssl3 -connect <project-id>.firebaseapp.com:443 < /dev/null 2>&1

2. 出力結果の中から、「—–BEGIN CERTIFICATE—–」から「—–END CERTIFICATE—–」までをコピーして、「cert.perm」というファイル名で保存します。
下記コマンドを実行します。

openssl x509 -noout -in ./cert.perm -fingerprint -sha1

3. 出力結果の中から、「SHA1 Fingerprint=」以降の文字列で、「:」を半角空白に置き換えたもの(「XX XX … XX XX」といった形式になる)を、ソースコードに直書きします。

httpsrv.hの「<fingerprint>」を取得したフィンガープリントに書き替えたら、LocalHomeSDK_PiccoroboIoTに、httpsrv.hをインクルードし、Http通信できるように、setup関数とloop関数に追加で以下のように実装します。

#include "httpsrv.h"
・・・

void setup() {
    ・・・
    // Httpサーバ開始
    localHomeSrv.begin();
}

void loop() {
    ・・・
    // Httpサーバ待受け処理
    localHomeSrv.task();
}

動作確認

ピッコロボIoTにアップロードしたら、curlコマンドで動作確認してみます。
シリアルモニタでピッコロボIoTのIPアドレスを確認してください。

PC(WSLを使用しています)からcurlコマンドを実行します。
「<IPアドレス>」は適宜置き換えてください。

curl -f -X POST \
    -H 'Content-Type:application/json' \
    -d '{"on":true, "isRunning":false, "isPaused":false}' \
    http://<IPアドレス>:3388/

シリアルモニタを確認すると、以下のように出力され、受信した値で状態を変更し、クラウドにその状態を送信できているのが分かります。
※ ついでに、Web UI側も同期していることが確認できると思います。

{"on":true, "isRunning":false, "isPaused":false}
on:1, isRunning:0, isPaused:0
{"on":true,"isRunning":false,"isPaused":false}
report state response code : 200

Nest Miniから操作できるか確認

ここまで出来たら、Google Homeデバイス(Nest Mini)から操作できるか確認しましょう。
Google Homeデバイス(Nest Mini)の電源を抜いている場合は、電源を入れます。
シリアルモニタを確認し、以下のように出力されるのを待ちます。

UDP packet contents: HelloLocalHomeSDK
HelloLocalHomeSDK
The received message is ok

Google Homeデバイス(Nest Mini)へ下記のように話し、音声コマンドを介してコマンドをピッコロボIoTに送信します。
※ 「ウォッシャー」じゃなく「洗濯機」でも操作できます。

  • ウォッシャーをオンにして
  • ウォッシャーをスタートして
  • ウォッシャーを一時停止
  • ウォッシャーを再開して
  • ウォッシャーの動作を止めて
  • ウォッシャーをオフにして

シリアルモニタを確認すると、以下のように出力されていると思います。

{"on":true}
on:1, isRunning:0, isPaused:0
{"on":true,"isRunning":false,"isPaused":false}
report state response code : 200
...
{"on":false}
on:0, isRunning:0, isPaused:0
{"on":false,"isRunning":false,"isPaused":false}
report state response code : 200

これで仮想デバイス(仮想スマートウォッシャー)を、ピッコロボIoTに置き換えるように実装することができました!
なお、今回のソースコードはGithubの「vstoneofficial/LocalHomeSDK_PiccoroboIoT」に上げています。
次回は、「洗濯機」として扱っていたピッコロボIoTを、適切なデバイス名で呼ぶように実装していこうと思います。