LINE CEKを使って色々と遊べそうだったから、ちょっと遊んでみた。
■ 何にするか…
ESP32があるから、「照明をつけて・消して」というと、LEDの点灯制御をClovaから行えるようにしてみる。いわゆる…IoT的な連携の基本動作といった感じ。
LINE BOTと連携して、ESP32側からLINEに向けてデータを送信する事も出来て、スピーカ・端末・アプリの3者連携は何気に面白いこともできる。
■ 全体構成
全体的な接続の流れはこんな感じ。
LINE Clova Wave <=> (LINE社のサーバ) <=> golang(ヲイラのサーバ) <=(MQTT)=> ESP32
LINE CEKと自分のサーバ側(golang)と接続して、端末とのインタフェイスはMQTTで。IFFFTとかでも良いだろうけど、MQTTにしておくと後々で便利というかIoT的(?)な感じ。あとgolangの部分は自分のサーバを使ったけど、nodejs(firebase function)を使うとサーバーレスでサラッと無料で作ることもできる。
いまふと思ったら、firebase function NodeJSの方がクールだったかな…けど、この手のサラッとしたWeb APIというかエンドポイントはgolangで作る方が自分は軽い。まぁ、なんでもいいけど。
■ モデルのビルド
まずは他のスピーカー類と同じく、言語モデルを作っていく。他で作ったことがある人なら、めっちゃ簡単にサクサク作れると思われ。このLEDのオン・オフのモデルを作る時の自分の考え方としては…
カスタムスロットには、「人が言うオン・オフの言葉」を別々に用意しておく。
インテントは一つだけ用意して、オン・オフ部分の言葉をスロットに割り当てる。
といった感じ。まずはスロットタイプを作る。LED_ON_SLOT/LED_OFF_SLOTの2個を用意しておく。
何故にオン・オフで分けたか…というと、言葉の分岐と後で処理を行うgolang部分を楽にするため。
例えば明るくする場合は、照明を付ける、照明をオンにする、照明を点灯する…etc といった色んな表現があって、どの言葉でも「LEDが点灯する」という現象としては同じになる。そこで、LED_ON_SLOTというスロットにひとまとめにしておくことにした。
次はインテント、これは簡単、”照明をつけて・けして”の部分にスロット(LED_ON, LED_OFF)を割り当てるだけ。
ここに2個のスロットをそれぞれ割り当てておくと、”つけて・けして”という現象に対する言葉を、スロットタイプに言葉を増やすことで対応することができる。出来たらビルドをして、あとはテスト。
と…その前に、golang部分のコードはこんな感じ。Json to Goのコンバータで、structはゴソッと作った。フロントは何かWebサーバで終端させて、裏側のgolangにReverse ProxyしてAPとして動作。この手のAPIをサラッと作って遊ぶ時はgolangを使う感じが自分は好き。
あとMQTTサーバは、オープンブローカーで誰でも使える test.mosquitto.org を利用。ここではテストだからOKだけど、マジでやる時はAWS IoT/Google IoT CoreとかMQTTをTLSで終端できるサーバーを利用した方が良い。
package main import ( "fmt" "log" "encoding/json" "net/http" MQTT "git.eclipse.org/gitroot/paho/org.eclipse.paho.mqtt.golang.git" ) type ClovaRequest struct { Version string `json:"version"` Session struct { SessionID string `json:"sessionId"` SessionAttributes struct { } `json:"sessionAttributes"` User struct { UserID string `json:"userId"` AccessToken string `json:"accessToken"` } `json:"user"` New bool `json:"new"` } `json:"session"` Context struct { System struct { Application struct { ApplicationID string `json:"applicationId"` } `json:"application"` User struct { UserID string `json:"userId"` AccessToken string `json:"accessToken"` } `json:"user"` Device struct { DeviceID string `json:"deviceId"` Display struct { Size string `json:"size"` Orientation string `json:"orientation"` Dpi int `json:"dpi"` ContentLayer struct { Width int `json:"width"` Height int `json:"height"` } `json:"contentLayer"` } `json:"display"` } `json:"device"` } `json:"System"` } `json:"context"` Request struct { Type string `json:"type"` Intent struct { Name string `json:"name"` Slots interface{} `json:"slots"` } `json:"intent"` } `json:"request"` } type ClovaResponse struct { Version string `json:"version"` SessionAttributes interface{} `json:"sessionAttributes"` Response struct { OutputSpeech struct { Type string `json:"type"` Values struct { Lang string `json:"lang"` Type string `json:"type"` Value string `json:"value"` } `json:"values"` } `json:"outputSpeech"` Directives interface{} `json:"directives"` ShouldEndSession bool `json:"shouldEndSession"` } `json:"response"` } func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { var clovaRequest ClovaRequest var clovaResponse ClovaResponse if err := json.NewDecoder(r.Body).Decode(&clovaRequest); err != nil { panic(err) } // create clova response var onoff bool clovaResponse.Version = "1.0" clovaResponse.SessionAttributes = nil clovaResponse.Response.OutputSpeech.Type = "SimpleSpeech" clovaResponse.Response.OutputSpeech.Values.Lang = "ja" clovaResponse.Response.OutputSpeech.Values.Type = "PlainText" // この部分でスロット名で処理を分岐させる if clovaRequest.Request.Intent.Slots.(map[string]interface{})["LED_ON"] != nil { clovaResponse.Response.OutputSpeech.Values.Value = "つけたよ" onoff = true } else if clovaRequest.Request.Intent.Slots.(map[string]interface{})["LED_OFF"] != nil { clovaResponse.Response.OutputSpeech.Values.Value = "けしたよ" onoff = false } clovaResponse.Response.Directives = nil clovaResponse.Response.ShouldEndSession = false outputJson, err := json.Marshal(&clovaResponse) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, string(outputJson)) // sendto MQTT go func() { ops := MQTT.NewClientOptions().SetClientID("clovagw").AddBroker("tcp://test.mosquitto.org:1883") client := MQTT.NewClient(ops) if token := client.Connect(); token.Wait() && token.Error() != nil { log.Println("Error %s\n", token.Error()) } if onoff { client.Publish("clova/led", 0, true, "1") } else { client.Publish("clova/led", 0, true, "0") } client.Disconnect(250) }() }) log.Fatal(http.ListenAndServe(":8080", nil)) }
そしてESP32側。こっちは clova/led でsubscribeして終端している。ここにClova -> golangを経由してデータが流れてくるといった感じ。これも簡単、pubsubclientのサンプルをそのまま使えば一瞬で出来る。
#include "WiFi.h" #include "PubSubClient.h" const char* ssid = "...."; const char* password = "....."; const char* mqtt_server = "test.mosquitto.org"; #define LED_PIN 12 WiFiClient espClient; PubSubClient client(espClient); long lastMsg = 0; char msg[50]; int value = 0; void setup_wifi() { WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } randomSeed(micros()); Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); } void callback(char* topic, byte* payload, unsigned int length) { Serial.print("Message arrived ["); Serial.print(topic); Serial.print("] "); for (int i = 0; i < length; i++) { Serial.print((char)payload[i]); } Serial.println(); if ((char)payload[0] == '1') { digitalWrite(LED_PIN, HIGH); } else { digitalWrite(LED_PIN, LOW); } } void reconnect() { while (!client.connected()) { String clientId = "ESP32Client"; // Attempt to connect if (client.connect(clientId.c_str())) { Serial.println("connected"); client.subscribe("clova/led"); } else { Serial.print("failed, rc="); Serial.print(client.state()); Serial.println(" try again in 5 seconds"); // Wait 5 seconds before retrying delay(5000); } } } void setup() { pinMode(LED_PIN, OUTPUT); Serial.begin(115200); setup_wifi(); client.setServer(mqtt_server, 1883); client.setCallback(callback); } void loop() { if (!client.connected()) { reconnect(); } client.loop(); }
そんで、ESP32側はこんな感じでLEDが光ってくれる。端末側に距離センサーをつけて、Clova側から距離いくら〜?で身長を測定するような"ものさし"をMQTT経由で送ったり、まぁ...色々となんでも使えると思われ。
Clovaに話しかけなくても、端末側でなにか温度・気圧とか環境状態が変わったら、MQTTでLINEアプリ側にBOT連携して送信したりも。楽しそうかなーと思うのが、LINEアプリ/Clova/端末の3者連携して、LINEのGWをプラットフォームにした空間(部屋)を使って遊べそうな。
LINEのコミュニケーションツールとしての側面はもちろんだけど、ユーザーとモノ・家をつなぐそれぞれプラットフォーム、そしてルーム=部屋=仮想空間として考えると、また違った風景が見えてくる。IoTやM2Mのような、モノとモノや人が介在するという以上に、アプリや仮想空間(ルーム)も関与して、それぞれがプラットフォームを介して相互にやりとり出来るといった感じ。
例えば、振動モーターが付いた端末で、ルーム同士で気に入って仲間になったら、遠隔でブルブルさせてその反応が音としてスピーカーから聞こえるとか。まぁこれはエロい用途にしか使え無さそうだけど。
ちょっと気になるのは、スマートスピーカーを使った目隠し将棋だ。やった事がある人なら分かるけど、駒盤も何も見ないで音(23歩と言って駒を動かしていく)だけで対戦する。観戦者だけはLINEアプリ上で、実際の戦況が見えるといった感じ。「あぁ...完全に位置関係見失っちゃってるよwww」とか、見ている方も何気に面白かったりする。
何にしても、色んな面白いアプリが出てくればなー...という期待な感じ。