AWS
IoT
ESP-WROOM-32

AWS Lambda~(IoT)~ESP32の双方向通信

前説

  • AWS IoTとAWS Lambdaの連携
  • AWS IoTとESP32の連携

目的

AWS Lambda->ESP32へメッセージを送るために、AWS IoTのShadow機能を用いて実装してみました。
Lambdaのプログラムを実行すると、LEDが点灯するだけのプログラム。

環境

  • Arduino IDE
  • ESP-WROOM-32
  • AWS IoT
  • AWS Lambda
  • MQTT
  • Pubsubclient

相互通信

デバイス -> AWS IoT -> AWS他サービス
この流れならルールを用いたりして、結構充実している感じがした。しかし、その逆=アプリケーション->デバイス方面の実装にAWS IoT初心者としては少し戸惑った。
ネット情報も思っていたより、少なかったのでドキュメントを読み漁ったところ・・・

Shadowとは

AWS IoTからデバイスに向けて、デバイスの設定を指示できる機能。アプリケーションとデバイス間の相互通信が可能であり、デバイスの現在の設定とアプリケーションが期待する設定の差分をとって、デバイスに設定変更を指示してくれる。
ただし、一度に送信できるJsonのデータサイズは非常に小さい。

わかりやすいサイト

今回の実装
デバイス「LED OFFだよ!」
Lambda「ONにしてこい」
IoT Shadow「あいよ」
デバイス「ONになった!」

AWS Lambdaで使用したコード

AWS IoT上のトピックにメッセージを投げています。

lambda_function.py
# coding: utf-8

import json
import boto3


iot = boto3.client('iot-data')

#関数を定義
def lambda_handler(event, context):

    #トピックを指定
    topic = '$aws/things/ESP32/shadow/update'
    #メッセージの内容
    #形式通りのJsonでお願いします
    payload = {"state": {"desired": {"led": "on"}}}
    try:
        #メッセージをPublish
        iot.publish(
            topic=topic,
            qos=0,
            payload=json.dumps(payload, ensure_ascii=False)
        )

        return "Succeeeded."

    except Exception:
        return "Failed."

Arduino上のコード

AWS_iot.ino
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include "ArduinoJson.h" 

#define ID "ESP32" 
#define OUT_TOPIC "$aws/things/[device]/shadow/update"
#define IN_TOPIC "$aws/things/[device]/shadow/update/delta"

#OUT_TOIC => Publishする先のトピック
#IN_TOIC => Publishする先のトピック

const char* ssid     = "[ssid]";
const char* password = "[password]";

const char* server = "[end point].amazonaws.com";

const char* json = "{\"state\":{\"reported\":{\"led\":\"off\"}}}";

StaticJsonDocument<1024> doc;
const int port = 8883;
const int LED_pin = 15;

const char* Root_CA = \
"-----BEGIN CERTIFICATE-----\n" \

"-----END CERTIFICATE-----\n";

const char* Client_cert = \
"-----BEGIN CERTIFICATE-----\n" \

"-----END CERTIFICATE-----\n";

const char* Client_private = \
"-----BEGIN RSA PRIVATE KEY-----\n" \

"-----END RSA PRIVATE KEY-----\n";

WiFiClientSecure client;
PubSubClient mqttClient(client);

#AWS IoTとの接続
void connectAWSIoT() {
    while (!mqttClient.connected()) {
        if (mqttClient.connect("ESP32")) {
            Serial.println("Connected.");
        } else {
            Serial.print("Failed. Error state=");
            Serial.print(mqttClient.state());
            //5秒待ち
            delay(5000);
        }
    }
}


void setup()
{
  Serial.begin(115200);
  pinMode(LED_pin, OUTPUT);

  delay(10);

  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);
  delay(1000);

  Serial.println(Root_CA);
  Serial.println(Client_cert);
  Serial.println(Client_private);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.println(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  client.setCACert(Root_CA);
  client.setCertificate(Client_cert);
  client.setPrivateKey(Client_private);
  mqttClient.setServer(server, port);  
  mqttClient.setCallback(callback);

  connectAWSIoT();

  int qos = 0;
  //通信できるデータの最大サイズ
  Serial.println(MQTT_MAX_PACKET_SIZE);
  if(mqttClient.subscribe(IN_TOPIC, qos)){
    Serial.println("Subscribed.");
    Serial.println("Success!!");
  }

    deserializeJson(doc, json);
    JsonObject& obj = doc.as<JsonObject>();

   if(mqttClient.publish(OUT_TOPIC, json)){
    Serial.println("Published!!");
   }
}

#subscribeした時に呼び出すコールバック関数
void callback (char* topic, byte* payload, unsigned int length) {
    Serial.println("Received. topic=");
    Serial.println(topic);
    char subsc[length];
    #文字の出力
    for (int i = 0; i < length; i++) {
        subsc[i] = (char)payload[i];
        subsc[length]='\0';
        Serial.print(subsc);
    }
    Serial.print("\n");
    #Subscribeした際にLチカ
    digitalWrite(LED_pin, HIGH);
}

void mqttLoop() {
    #PubsubClientのmainLoop->データの取得を待っている
    mqttClient.loop();
    delay(100);
    digitalWrite(LED_pin, LOW);
    Serial.print(".");

}

void loop() {
  mqttLoop();
  delay(1000);
}

arduinoJson変換サイト -> https://arduinojson.org/v5/assistant/

シャドウステータス

「管理」->「モノ」->「シャドウ」で確認できます。

shadow_status
{
  "desired": {
    "led": "on"
  },
  "reported": {
    "led": "off"
  },
  "delta": {
    "led": "on"
  }
}

大事なこと

qos -> 0か1。2はAWS IoTでは使えません!
Jsonデータは必ず形式通りに!

ここまではスムーズに行った

しかし・・・Arduino上でJsonがSubscibeされないではないか!
Rejectされてた。
よってLチカもされない!

原因

Jsonのbyte数が小さい時は、問題なく送受信できる。
しかし、/update/deltaで受信するJsonデータは送られてこない。

$aws/things/ESP32/shadow/update/delta
{
  "version": 212,
  "timestamp": 1531017344,
  "state": {
    "led": "on"
  },
  "metadata": {
    "led": {
      "timestamp": 1531017344
    }
  }
}

考えてた案

  • 実験として$aws/things/ESP32/shadow/update/deltaに送信される中身からmetadataを削除すると送れる -> 設定できない。
  • ルールから送信するデータのカラムを設定する -> やり方がよくわかんない
  • なんとかしてJsonデータを小さくしたい。

良い改善方法があればご教授お願いいたします。

妥協案

とりあえず、IN_TOPICを$aws/things/ESP32/shadow/update/deltaとした差分を発見した時に、データを受信するようにしていた。
これを変更して$aws/things/ESP32/shadow/updateにして、トピックにLambdaからメッセージが来た時に、LEDが点灯するようにした。この場合、metadataが含まれず、送受信データ量も比較的小さく収まるので、データ量の制約によりRejectはされなくなった。

$aws/things/ESP32/shadow/update
{
  "state": {
    "desired": {
      "led": "on"
    }
  }
}

なんだかモヤモヤする・・・

次にやること

  • どうにかして制御
  • ルールの設定がやっぱり難しい・・・

参考文献