前々回の「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を、適切なデバイス名で呼ぶように実装していこうと思います。