LINE CEK with Arduino ESP32

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個を用意しておく。

LED_ON_SLOT

LED_OFF_SLOT

何故にオン・オフで分けたか…というと、言葉の分岐と後で処理を行う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」とか、見ている方も何気に面白かったりする。
何にしても、色んな面白いアプリが出てくればなー...という期待な感じ。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください