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