Alexa で ESP8266 を制御するプログラムの解説です。
目次
今回は【Alexa × Arduino】第3弾ということでやっていきます。前々回と前回の記事では、Alexa Smart Home Skill と AWS IoT を組み合わせることで Alexa から ESP8266 に接続した LED を操作する方法を具体的に説明しました。
今回はこれらで用いたプログラムの簡単な解説をしたいと思います。今回解説するのは、
- ESP8266 のプログラム(Arduino スケッチ)
- AWS Lambda のプログラム
です。
プログラムは、以下のレポジトリにアップロードしてあります。
ESP8266 のプログラム
Arduino スケッチを読んでいただければ大体の流れが分かると思いますが、AWS IoT Core とのやりとりに関わる部分だけ抜粋します。
まずは初期処理の部分です。このライブラリでは IAM ユーザー認証方式での接続を試みています。connectAWSIoTCore
が AWS IoT Core サーバーへの接続をする関数です。起動してから接続まで5秒程度以上待ってから接続を開始しないと、Exception (9) が発生するという原因がよく分からないトラブルに見舞われたので、ここでは 10秒待ってから接続しています。接続が完了したら、$aws/things/ESP8266/shadow/update/delta
を Subscribe して、mqttCallback
をコールバック関数として登録しています。
C++💩/*** AWS IoT Core Initialization ***/
awsWSclient.setAWSRegion(aws_region);
awsWSclient.setAWSDomain(aws_endpoint);
awsWSclient.setAWSKeyID(aws_iam_key);
awsWSclient.setAWSSecretKey(aws_iam_secret_key);
awsWSclient.setUseSSL(true);
awsWSclient.setCA(ca);
awsWSclient.setUseAmazonTimestamp(false);
connectAWSIoTCore();
void connectAWSIoTCore() {
if ( mqttClient.connected() ) {
mqttClient.disconnect();
}
Serial.print("Waiting for AWS IoT Core connection.");
delay(10000); // Important ! To Avoid Exception (9)
mqttClient.setServer(aws_endpoint, aws_port);
if ( ! mqttClient.connect("HogeHoge") ){
Serial.println(" Failed !");
return;
}
Serial.println(" Finished !");
mqttClient.setCallback(mqttCallback);
mqttClient.subscribe(aws_topic_shadow_delta);
Serial.println("Subscribed !");
}
$aws/things/ESP8266/shadow/update/delta
が変化すると、ただちに mqttCallback
が呼ばれます。mqttCallback
では、メッセージの解読がおこなわれ、LED の ON/OFF をします。メッセージは JSON 形式で送信されるので、それをパースして情報を抽出します。
void mqttCallback(char *topic, byte *payload, unsigned int length) {
if (strcmp(topic, aws_topic_shadow_delta) == 0) {
// Parse received message (JSON Format)
JSONVar json = JSON.parse( (char *)payload );
Serial.println( JSON.stringify(json) );
JSONVar state = json["state"];
if ( state.hasOwnProperty("led") ){
// Obtain desired LED state
bool led = state["led"];
// LED ON/OFF
digitalWrite(LED, led ? HIGH : LOW);
// Report current LED state
reportLedState(led);
}
}
}
受信される JSON の形式は次のようになっています。
json💩{
"state" : {
"led" : true
}
}
LED の ON/OFF を切り替えたら、それを Thing Shadow に反映させるために $aws/things/ESP8266/shadow/update
に Publish します。
C++💩void reportLedState(bool led) {
char buf[100];
sprintf(buf, "{\"state\":{\"reported\":{\"led\": %s}}}", led ? "true" : "false");
mqttClient.publish(aws_topic_shadow, buf);
}
送信する JSON の形式は次のようになっています。
json💩{
"state" : {
"reported" : {
"led" : true
}
}
}
Thing Shadow の復讐
Thing Shadow は、JSON で表すと以下のような構造です。
{
"desired": {
"led": false
},
"reported": {
"led": false
}
}
Thing Shadow の更新と受信は、 $aws/things/.../shadow/update
という名前の Topic でおこないます。例えば、$aws/things/.../shadow/update
に、
{
"state" : {
"desired" : {
"led" : true
}
}
}
を Publish すると、Thing Shadow は
{
"desired": {
"led": true
},
"reported": {
"led": false
}
}
に変化します。$aws/things/.../shadow/update
を購読していた場合、Publish した内容と全く同じものが受信されます。
さて、$aws/things/.../shadow/update/delta
を購読していたとします。.../delta
は、 desired
と reported
に差分が生じた時に通知がいきます。先ほどの場合、"desired" : { "led" : true }
で "reported" : { "led" : false }
なので差分があり、
{
"state" : {
"led" : true
}
}
が受信されます。この差分が生じた状態を解消するには、reported
を変化させます。今回の場合、$aws/things/.../shadow/update
に
{
"state" : {
"reported" : {
"led" : true
}
}
}
を Publish することで、Thing Shadow が
{
"desired": {
"led": true
},
"reported": {
"led": true
}
}
となり、desired
と reported
の差がなくなってめでたしめでたしです。
AWS Lambda のプログラム
AWS Lambda では Alexa Smart Home Skill の実装と、Thing Shadow の desired
の更新をおこなっています。
まずは、Thing Shadow の更新部分です。
python💩client = boto3.client('iot-data', region_name='us-west-2')
def update_thing_shadow(name, value) :
obj = {
"state": {
"desired": {
name : value
}
}
}
topic_shadow_name = "$aws/things/ESP8266/shadow/update"
client.publish(
topic=topic_shadow_name,
qos=0,
payload=json.dumps(obj)
)
ここでは、$aws/things/ESP8266/shadow/update
に
json💩{
"state" : {
"desired" : {
"led" : true
}
}
}
を Publish することで、Thing Shadow の desired
の更新をしています。
次に、Alexa Smart Home Skill です。雑に説明します。
まずは、Alexa がスマートホームデバイスを探す「Discovery」中の処理です。
python💩if namespace == 'Alexa.Discovery':
if name == 'Discover':
adr = AlexaResponse(namespace='Alexa.Discovery', name='Discover.Response')
capability_alexa = adr.create_payload_endpoint_capability()
capability_alexa_powercontroller = adr.create_payload_endpoint_capability(
interface='Alexa.PowerController',
supported=[{'name': 'powerState'}])
adr.add_payload_endpoint(
endpoint_id='LED',
friendly_name='LED',
description='これはただのLEDです。',
display_categories=['LIGHT'],
capabilities=[capability_alexa, capability_alexa_powercontroller])
return send_response(adr.get())
Capability は、そのスマートホームデバイスが持つ機能です。例えば、スマートプラグやライトは ON/OFF ができるので、下に示すように「PowerController」という属性を付与しています。
capability_alexa_powercontroller = adr.create_payload_endpoint_capability(
interface='Alexa.PowerController',
supported=[{'name': 'powerState'}])
friendly_name
は Alexa アプリに表示されるデバイス名です。日本語でもかまいません。display_categories
はここの一覧表の中から適切に設定する必要があります。
adr.add_payload_endpoint(
endpoint_id='LED',
friendly_name='LED',
description='これはただのLEDです。',
display_categories=['LIGHT'],
capabilities=[capability_alexa, capability_alexa_powercontroller])
次に、「アレクサ、〇〇を消して!」と言った時の処理の部分です。
if namespace == 'Alexa.PowerController':
# Note: This sample always returns a success response for either a request to TurnOff or TurnOn
endpoint_id = request['directive']['endpoint']['endpointId']
power_state_value = 'OFF' if name == 'TurnOff' else 'ON'
correlation_token = request['directive']['header']['correlationToken']
apcr = AlexaResponse(correlation_token=correlation_token)
apcr.add_context_property(namespace='Alexa.PowerController', name='powerState', value=power_state_value)
# Update Thing Shadow Desired Value
update_thing_shadow("led", False if power_state_value == 'OFF' else True)
return send_response(apcr.get())
「アレクサ、〇〇つけて!」と言うと、name が TurnOn になるようです。
power_state_value = 'OFF' if name == 'TurnOff' else 'ON'
update_thing_shadow 関数で Thing Shadow の desired の値を更新します。
update_thing_shadow("led", False if power_state_value == 'OFF' else True)
最後に、アレクサに「〇〇を ON/OFF にした」という返信をします。
apcr = AlexaResponse(correlation_token=correlation_token)
apcr.add_context_property(namespace='Alexa.PowerController', name='powerState', value=power_state_value)
return send_response(apcr.get())
雑に説明するとこんな感じです。
今度は、Raspberry Pi を AWS IoT Core に接続してみたいと思います。ESP8266 のときより簡単だと思います。