茱萸note

電子工作の備忘録と旅行の記録

Alexa Arduino AWS ESP8266 IoT マイコン 電子工作

Alexa で ESP8266 を制御する ―― プログラムの解説編【Alexa×Arduino その3】

投稿日:2020年12月23日 更新日:

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 は、 desiredreported に差分が生じた時に通知がいきます。先ほどの場合、"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
  }
}

となり、desiredreported の差がなくなってめでたしめでたしです。

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 のときより簡単だと思います。

-電子工作
-, , , , , ,


comment

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

関連記事

Raspberry Pi と AWS IoT を連携させて Alexa で LED や スマートプラグを操作する【Alexa×Raspberry Pi その1】

AWS IoT Core と Raspberry Pi を連携させて、Alexa を搭載した Amazon Echo 等を使って Raspberry Pi につないだ LED を制御します。ついでに、Tp-Link のスマートプラグ『Tapo P105』も操作させてみます。

権限のない環境でvirtualenvを入れる

sudoが使えない、root権限のない環境でvirtualenvを入れます。

【ふざけるな Anaconda】Windows で Python 環境を整える

Anaconda なんか使わずに Windows に Python をインストールします。

【Ubuntu】【上級者向け】Grubブートローダを削除する

「デュアルブートしてたけどUbuntuを削除することにした。」という人などに向けて、Grubブートローダ(Ubuntuのブートローダ)を削除する方法を説明します。

C言語でのマイコンプログラミングのお作法

マイコンのプログラムをC言語で書く際に心がけるべきこと、知っておくべき知識について述べます。