M5StickC with ENV HatでAWS IoT Device Shadowを利用してみる

はじめに

前回の続きとなります。
https://www.skyarch.net/blog/?p=18393

前回までで、AWS IoTサービスへセンサ値をPublish出来たため
今回はShadowを利用して、LED状態の管理をしてみます。

出来たもの

Shadowドキュメントを更新する事でDeltaを発生させデバイス側LEDを制御

左側のウィンドウ

AWS IoT ⇢ モノ ⇢ シャドウ
にて下記シャドウステータスの desired 内、led_status を 1 に更新すると、deltaが発生しデバイスが受信してLEDを付ける
led_statusを 0 に更新すると、LEDが消えます。

{
  "desired": {
    "welcome": "aws-iot",
    "led_status": 0
  },
  "reported": {
    "welcome": "aws-iot",
    "led_status": 0
  }
}

右側のウィンドウ

テスト ⇢ MQTTクライアントにて下記トピックをサブスクライブしています。
このため delta が発生した際に、デバイス側が受信(Subscribeしている)するトピック内容となります。

$aws/things/M5Stick01/shadow/update/delta

デバイス側でLEDステータスを変化させてもDeltaが発生しShadowドキュメント状態に収束する

デバイス側では、自身のLED点灯/消灯情報をFlagのような形で持っており
およそ5秒に1回、その状態を Shadowに reportedとして通知しています。

このため、Shadowドキュメント内では desired が 消灯 にも関わらず
デバイス側で点灯されると、desired と reportedに deltaが発生
受信したデバイスが desiredの状態にLEDを点灯/消灯し合わせるという形です。

このあたりの挙動はdesiredをnullとして送信する事で変更できそうですね。
https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/device-shadow-document.html

左側のウィンドウ

AWS IoT ⇢ モノ ⇢ シャドウ
にてシャドウステータスを確認していますが、シャドウドキュメントの更新は行っていません。

右側のウィンドウ

テスト ⇢ MQTTクライアントにて下記トピックをサブスクライブしています。
手動でLED状態を変更すると、deltaが発生します。

$aws/things/M5Stick01/shadow/update/delta

目次

手順の概要

  1. AWS IoTで モノ/証明書/ポリシーを作成し適切な関連にAttach (AWS CLIにて実施) 前回までに実施
  2. AWS IoTで Shadowドキュメントを変更
  3. PubSubClientヘッダファイル変更
  4. コード書く/転送

M5StickCで利用するライブラリ等

前回と同様

参考にさせて頂いた記事

ESP32でAWS IoTに繋いでThing Shadowを弄る
https://blog.maripo.org/2017/07/esp32-aws-iot/

ESP32 Arduino: Timer interrupts
https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/

実行環境

MacOS上で実施しました。

手順

2. AWS IoTで Shadowドキュメントを変更

作成したモノのシャドウドキュメントで初期値は色々入っておりますが、シンプルにled_statusのみとします。
“welcome”: “aws-iot” は消せなかったため。

{
  "desired": {
    "welcome": "aws-iot",
    "led_status": 0
  },
  "reported": {
    "welcome": "aws-iot",
    "led_status": 0
  }
}

3. PubSubClientヘッダファイル変更

私のPCでは下記場所にありました。
/Users/[自分のユーザ名]/Documents/Arduino/libraries/PubSubClient/src/PubSubClient.h

Mac/Linuxでしたら下記にて検索出来ると思います。

$ cd
$ sudo find . -name 'PubSubClient.h'

ファイル内の下記を書き換えます。
サイズオーバすると、Callback関数内も実行されずDebugが難しいためお気をつけ下さい。

#define MQTT_MAX_PACKET_SIZE 128
↓
#define MQTT_MAX_PACKET_SIZE 1024

4. コード書く/転送

下記の [] 括弧部分をご自身の物に書き換える必要があります。
詳細は前回の記事をご覧下さい。
https://www.skyarch.net/blog/?p=18393

コード

およそ5秒毎にデータをAWSIoTへ送信しております
タイマーを割り込みにて処理しています。

/*
    note: need add library Adafruit_BMP280 from library manage
    Github: https://github.com/adafruit/Adafruit_BMP280_Library
*/

#include <M5StickC.h>
#include "DHT12.h"
#include <Wire.h>
#include "Adafruit_Sensor.h"
#include <Adafruit_BMP280.h>
#include <ArduinoJson.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>

// PubSubClient.h 実ファイルのMQTT_MAX_PACKET_SIZEを1024等に設定する必要があります
// https://github.com/knolleary/pubsubclient

// Wifi Settings
const char* ssid     = "[ご自身の環境のWifiSSID]";
const char* password = "[ご自身の環境のWifiPassowrd]";

// AWS IoT Settings
const char* endpoint = "[ご自身のAWSアカウントのIoTエンドポイント]"; // 東京リージョンなら右記のような形 xxxx-ats.iot.ap-northeast-1.amazonaws.com
const int   port     = 8883;
const char* pubTopic = "[適当なTopic名 - 記事中では envTopic としていました]";
const char* clientId = "[適当なデバイス名 - 記事中では M5Stick01 としていました]"; // 
char pubTopicShadow[128];
char subTopicShadow[128];
#define QOS_SUB 0

const char* rootCA = \
"-----BEGIN CERTIFICATE-----\n" \
"...\n" \ // [ご自身で取得したRootCA]
"-----END CERTIFICATE-----\n";

const char* clientCert = \
"-----BEGIN CERTIFICATE-----\n" \
"...\n" \ // [上記CLIコマンドで生成したデバイス用証明書]
"-----END CERTIFICATE-----\n";

const char* clientPrivateKey = \
"-----BEGIN RSA PRIVATE KEY-----\n" \
"...\n" \ // [上記CLIコマンドで生成したデバイス用秘密鍵]
"-----END RSA PRIVATE KEY-----\n";

// Wifi/Network
WiFiClientSecure httpsClient;
PubSubClient mqttClient(httpsClient);
char buffer[1024];

// Timer interrupts
// https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/
volatile int interruptCounter;
 
hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

void IRAM_ATTR onTimer() {
  portENTER_CRITICAL_ISR(&timerMux);
  interruptCounter++;
  portEXIT_CRITICAL_ISR(&timerMux);
}

// LED_PIN
#define M5_STICK_PIN_LED 10
boolean ledState = true; // HIGH: LED Off/Low: LED On

// ENV HAT
DHT12 dht12; 
Adafruit_BMP280 bme;

void checkWiring() {
  while (!bme.begin(0x76)) {
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0, 2);
    M5.Lcd.println("Could not find a valid BMP280 sensor, check wiring!");
    delay(1000);
  }
}

void reconnect() {
  while (!mqttClient.connected()) {
    if (mqttClient.connect(clientId)) {
      M5.Lcd.setCursor(0, 40, 2);
      M5.Lcd.println("AWSIoTConnected");
      mqttClient.subscribe(subTopicShadow, QOS_SUB);
    } else {
      M5.Lcd.setCursor(0, 40, 2);
      M5.Lcd.printf("AWSIoTConnect Failed. state=%d", mqttClient.state());
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void mqttCallback(char* topic, byte* payload, unsigned int length) {
    // JSON Parse
    payload[length] = '\0';
    String json = String((char*) payload);
    // Debug JSON in SerialConsole
    Serial.println(json);

    StaticJsonDocument<1024> docShadowDesired;
    DeserializationError error = deserializeJson(docShadowDesired, json);
    
    M5.Lcd.setCursor(0, 60, 2);
    if (error) {
      M5.Lcd.printf("deserializeJson Failed");
    } else {      
      if (sizeof(docShadowDesired["state"]["led_status"]) > 0) {
        M5.Lcd.printf("Desired: %d", int(docShadowDesired["state"]["led_status"]));
        ledState = !boolean(docShadowDesired["state"]["led_status"]);
        digitalWrite(M5_STICK_PIN_LED, ledState);
      } else {
        M5.Lcd.printf("Desired: corrupted value");
      }
    }
}

void setup() {  
  // put your setup code here, to run once:
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  // Timer
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 5000000, true);
  timerAlarmEnable(timer);
  // I2C
  Wire.begin(0,26);
  // GPIOSetup
  pinMode(M5_STICK_PIN_LED, OUTPUT);
  digitalWrite(M5_STICK_PIN_LED, ledState);
  // check ENVHat
  checkWiring();
  // WifiSetup
  //// Avoid Connect Error (JIC)
  WiFi.disconnect(true);
  delay(1000);
  //// Connect
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  M5.Lcd.setCursor(0, 40, 2);
  M5.Lcd.printf("Wifi Connected");
  // Configure MQTT Client
  httpsClient.setCACert(rootCA);
  httpsClient.setCertificate(clientCert);
  httpsClient.setPrivateKey(clientPrivateKey);
  mqttClient.setServer(endpoint, port);
  mqttClient.setCallback(mqttCallback);
  // pubTopic/subTopic
  sprintf(pubTopicShadow, "$aws/things/%s/shadow/update", clientId);
  sprintf(subTopicShadow, "$aws/things/%s/shadow/update/delta", clientId);
  // Debug topic name in SerialConsole
  Serial.printf("pubTopicShadow=%s\n", pubTopicShadow);
  Serial.printf("subTopicShadow=%s\n", subTopicShadow);
}

void loop() {  
  // update button state
  M5.update();

  // Button Pressed
  if (M5.BtnA.wasReleased()) {
    // turnOn/Off LED
    ledState = !ledState;
    digitalWrite(M5_STICK_PIN_LED, ledState);
  }

  // check ENVHat
  checkWiring();

  // reconnect
  if (!mqttClient.connected()) {
    reconnect();
  }
  mqttClient.loop();
  
  // Timer Interrupt execute Every 5sec
  if (interruptCounter > 0) {
    Serial.println("Timer interrupted");
    // CounterReset
    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux);
 
    // display Temp/Humidity/Pressure
    float tmp = dht12.readTemperature();
    float hum = dht12.readHumidity();
    M5.Lcd.setCursor(0, 0, 2);
    M5.Lcd.printf("Temp: %2.1f Humi: %2.0f%%", tmp, hum);
    
    float pressure = bme.readPressure();
    M5.Lcd.setCursor(0, 20, 2);
    M5.Lcd.printf("pressure: %2.1f", pressure);
  
    // create JSON
    DynamicJsonDocument docEnv(1024);
    JsonObject data = docEnv.createNestedObject("env");
    data["temperture"] = tmp;
    data["humidity"] = hum;
    data["pressure"] = pressure;
    serializeJson(docEnv, buffer, sizeof(buffer));
    // Debug JSON in SerialConsole
    Serial.println(buffer);
        
    // Publish - The client only supports publishing at QoS 0
    mqttClient.publish(pubTopic, buffer);

    // report current LED status to ShadowTopic
    // create JSON
    DynamicJsonDocument docShadowReport(1024);
    JsonObject state = docShadowReport.createNestedObject("state");
    JsonObject reported = state.createNestedObject("reported");
    reported["led_status"] = int(!ledState);
    serializeJson(docShadowReport, buffer, sizeof(buffer));
    // Debug JSON in SerialConsole
    Serial.println(buffer);

    // Publish - The client only supports publishing at QoS 0
    mqttClient.publish(pubTopicShadow, buffer);
  }
  
  // wait
  delay(500);
}

コード上のポイントとなる処理

  • センサ値取得は、ENV HAT用のサンプルスケッチからほぼ変えていません。
  • タイマーに関しては、参考ブログから拝借した内容となります。

Shadow Topicのパブリッシュ/サブスクライブ

clientIdを元にTopicエンドポイント名を生成しています。

void setup() {
  // pubTopic/subTopic
  sprintf(pubTopicShadow, "$aws/things/%s/shadow/update", clientId);
  sprintf(subTopicShadow, "$aws/things/%s/shadow/update/delta", clientId);
  ...

パブリッシュ

現在のデバイス状態(LED点灯状況)をShadowにてレポートするため5秒毎に
LEDの点灯/消灯状態変数を元にJSONドキュメントを生成し、Publishしています。

生成されるJSONは下記のようになります。
{"state": {"reported" : {"led_status" : 0}}}

void loop() {
  ...
  // Timer Interrupt execute Every 5sec
  if (interruptCounter > 0) {
    ..
    // report current LED status to ShadowTopic
    // create JSON
    DynamicJsonDocument docShadowReport(1024);
    JsonObject state = docShadowReport.createNestedObject("state");
    JsonObject reported = state.createNestedObject("reported");
    reported["led_status"] = int(!ledState);
    serializeJson(docShadowReport, buffer, sizeof(buffer));
    // Debug JSON in SerialConsole
    Serial.println(buffer);

    // Publish - The client only supports publishing at QoS 0
    mqttClient.publish(pubTopicShadow, buffer);
...

サブスクライブ

サブスクライブ設定を行うと、コールバック関数にトピック名、メッセージ、メッセージ長が通知されます。
通知されたメッセージを、char型からString型にキャストし、deserializeを実施しています。

取得したJSON内の数値を、booleanに変換し出力しています。
※M5StickのLED接続I/OピンがHighだと消灯/Lowだと点灯のため反転する必要あり

ledState = !boolean(docShadowDesired["state"]["led_status"]);
digitalWrite(M5_STICK_PIN_LED, ledState);
関連コード部分
void mqttCallback(char* topic, byte* payload, unsigned int length) {
    // JSON Parse
    payload[length] = '\0';
    String json = String((char*) payload);
    // Debug JSON in SerialConsole
    Serial.println(json);

    StaticJsonDocument<1024> docShadowDesired;
    DeserializationError error = deserializeJson(docShadowDesired, json);
    
    M5.Lcd.setCursor(0, 60, 2);
    if (error) {
      M5.Lcd.printf("deserializeJson Failed");
    } else {      
      if (sizeof(docShadowDesired["state"]["led_status"]) > 0) {
        M5.Lcd.printf("Desired: %d", int(docShadowDesired["state"]["led_status"]));
        ledState = !boolean(docShadowDesired["state"]["led_status"]);
        digitalWrite(M5_STICK_PIN_LED, ledState);
      } else {
        M5.Lcd.printf("Desired: corrupted value");
      }
    }
}

void setup() {
  ...
  mqttClient.setCallback(mqttCallback);
...

void reconnect() {
  while (!mqttClient.connected()) {
    if (mqttClient.connect(clientId)) {
      ...
      mqttClient.subscribe(subTopicShadow, QOS_SUB);

投稿者プロフィール

takashi
開発会社での ASP型WEBサービス企画 / 開発 / サーバ運用 を経て
2010年よりスカイアーチネットワークスに在籍しております

機械化/効率化/システム構築を軸に人に喜んで頂ける物作りが大好きです。
個人ブログではRaspberryPiを利用したシステムやロボット作成も
実施しております。

スカイアーチネットワークスで一緒に働きましょう!

ABOUTこの記事をかいた人

開発会社での ASP型WEBサービス企画 / 開発 / サーバ運用 を経て 2010年よりスカイアーチネットワークスに在籍しております 機械化/効率化/システム構築を軸に人に喜んで頂ける物作りが大好きです。 個人ブログではRaspberryPiを利用したシステムやロボット作成も 実施しております。 スカイアーチネットワークスで一緒に働きましょう!