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のデータサイズは非常に小さい。

[わかりやすいサイト](http://appirio.co.jp/blog-category/tech-blog/device-management-through-aws-iot-shadow/)

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

#AWS Lambdaで使用したコード

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

```Python: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上のコード

```c++: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/

#シャドウステータス
「管理」->「モノ」->「シャドウ」で確認できます。

```json: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データは送られてこない。

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

- [ArduinoのPubsubClientでSubscribeするデータ量が小さい。(128KBまでとか)](https://github.com/knolleary/pubsubclient/blob/master/src/PubSubClient.cpp)
- [Shadowではどうも送受信できるデータ通信量に制限がある。](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/diagnosing-shadows.html)

##考えてた案
- 実験として$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はされなくなった。


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

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

#次にやること
- どうにかして制御
- ルールの設定がやっぱり難しい・・・

#参考文献
- https://pubsubclient.knolleary.net/api.html#subscribe
- https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/pub-sub-policy.html
- https://recipe.kc-cloud.jp/archives/10747
- https://techtutorialsx.com/2017/04/24/esp32-subscribing-to-mqtt-topic/
- https://blog.maripo.org/2017/07/esp32-aws-iot-troubleshooting/