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」とか、見ている方も何気に面白かったりする。
何にしても、色んな面白いアプリが出てくればなー...という期待な感じ。
