ESP32でAWS IoTに繋いでThing Shadowを弄る


ESP32は便利だ。WiFiが使える。Arduino IDEでそのまんま開発できるのでコーディングもまあまあ簡単。Bluetooth Low Energyにも対応している……が、今のところライブラリが整備されていないのでそっちはあまり使っていない。ESP8266より性能はいいがまだ少し情報が少ない。このESP32を使ってAWS IoTに繋いでMQTTでPublish/Subscribeし、Thing Shadow情報のUpdateや更新の受取を行うことができたので手順を共有する。

ESP32ではAWS IoTの公式SDKをそのまま使うわけにはいかないのでWiFiClientSecureとMQTTライブラリ (色々ある) を使うことになる。

前提

AWS IoTについては資料が色々あるので今回は詳しく書かず、以下の作業は済んでいる前提で話を進める。

  • AWS IoTの登録
  • Thingの登録
  • X.509証明書の生成
  • 証明書へのPolicyとThingのアタッチ
  • AWS IoT Consoleの使い方を把握

証明書の準備

公開鍵・秘密鍵は作成した直後しかダウンロードできないので、作成したらすぐローカルに保存しておく。うっかり保存し忘れたら新しいのを作るしかない。ルートCAも必要になるので、一緒にダウンロードしておく。

実装準備

今回使うのは

ESP32用のWiFiClientSecureは、ちょっと古いのを使うとRoot証明書をサポートしていない。WiFiClientSecure::setCACert()函数が見当たらなかったら要アップデート。

通信プロトコルはMQTTを使う。SSLで接続するので、WiFiClientSecureを使えばいい。証明書・秘密鍵・ルート証明書の3点セットを使ってAWS IoTのMQTTブローカに接続する。

MQTTクライアントは探すと色々あるが、PubSubClientがかなり使いやすかった。PubSubClientはArduino IDEのLibrary Managerで入れられる。ちなみにMITライセンス。インストールしたてホヤホヤの状態だと、一度にやりとりできるメッセージの長さ制限がヘッダ込みで128バイト。これだとshadow/update/delta、shadow/get/acceptedなど大事なメッセージが大概受け取れなくて困ることになるので、PubSubClient.h を探して

#define MQTT_MAX_PACKET_SIZE 128

の部分を

#define MQTT_MAX_PACKET_SIZE 1024

など大きめの値に変えておく。Thing Shadowのデータ形式と相談して十分なサイズを確保しておくこと。REAEMEにもこう書かれている:

The maximum message size, including header, is 128 bytes by default. This is configurable via MQTT_MAX_PACKET_SIZE in PubSubClient.h.

実装

手順は

  • WiFi接続
  •  証明書一式をWiFiClientSecureにセット
    • WiFiClientSecure::setCACert()
    • WiFiClientSecure::setCertificate()
  • MQTTクライアントでAWS IoTのMQTTブローカに接続
    • ポートは8883
    • エンドポイントはAWS IoTコンソールで確認 (xxxxxxxxxx.iot.ap-northeast-1.amazonaws.com のような形式)
  • Pub & Sub

コードはこんな感じになる。

  • WiFi接続情報
  • 証明書と秘密鍵
  • デバイス名
  • AWS IoTエンドポイント

の箇所を書き換えればとりあえず動く。

#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>

char *ssid = "<YOUR_SSID>";
char *password = "<YOUR_WIFI_PASSWORD>";

const char *endpoint = "<AWS_IOT_ENDPOINT>";
// Example: xxxxxxxxxxxxxx.iot.ap-northeast-1.amazonaws.com
const int port = 8883;
char *pubTopic = "$aws/things/<DEVICE_NAME>/shadow/update";
char *subTopic = "$aws/things/<DEVICE_NAME>/shadow/update/delta";

const char* rootCA = "-----BEGIN CERTIFICATE-----\n" \
"......" \
"-----END CERTIFICATE-----\n";

const char* certificate = "-----BEGIN CERTIFICATE-----\n" \
"......" \
"-----END CERTIFICATE-----\n";

const char* privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" \
"......" \
"-----END RSA PRIVATE KEY-----\n";

WiFiClientSecure httpsClient;
PubSubClient mqttClient(httpsClient);

void setup() {
    Serial.begin(115200);

    // Start WiFi
    Serial.println("Connecting to ");
    Serial.print(ssid);
    WiFi.begin(ssid, password);

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

    // Configure MQTT Client
    httpsClient.setCACert(rootCA);
    httpsClient.setCertificate(certificate);
    httpsClient.setPrivateKey(privateKey);
    mqttClient.setServer(endpoint, port);
    mqttClient.setCallback(mqttCallback);

    connectAWSIoT();
}

void connectAWSIoT() {
    while (!mqttClient.connected()) {
        if (mqttClient.connect("ESP32_device")) {
            Serial.println("Connected.");
            int qos = 0;
            mqttClient.subscribe(subTopic, qos);
            Serial.println("Subscribed.");
        } else {
            Serial.print("Failed. Error state=");
            Serial.print(mqttClient.state());
            // Wait 5 seconds before retrying
            delay(5000);
        }
    }
}

long messageSentAt = 0;
int dummyValue = 0;
char pubMessage[128];

void mqttCallback (char* topic, byte* payload, unsigned int length) {
    Serial.print("Received. topic=");
    Serial.println(topic);
    for (int i = 0; i < length; i++) {
        Serial.print((char)payload[i]);
    }
    Serial.print("\n");
}

void mqttLoop() {
    if (!mqttClient.connected()) {
        connectAWSIoT();
    }
    mqttClient.loop();

    long now = millis();
    if (now - messageSentAt > 5000) {
        messageSentAt = now;
        sprintf(pubMessage, "{\"state\": {\"desired\":{\"foo\":\"%d\"}}}", dummyValue++);
        Serial.print("Publishing message to topic ");
        Serial.println(pubTopic);
        Serial.println(pubMessage);
        mqttClient.publish(pubTopic, pubMessage);
        Serial.println("Published.");
    }
}

void loop() {
  mqttLoop();
}

ちょっと解説

    WiFi.begin(ssid, password);

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

ここのところはWiFi接続。

SSLの設定はこんな感じでルートCA・証明書・秘密鍵をWiFiClientSecureにセットする。今回はダウンロードした証明書と鍵をベタ書きしたが、実際はEEPROMに保存したのを読み出したりすることになると思う。各行の末尾の改行も忘れずに!

    httpsClient.setCACert(rootCA);
    httpsClient.setCertificate(certificate);
    httpsClient.setPrivateKey(privateKey);

次はMQTTの設定。ホスト情報としてエンドポイントとポート (8883) を設定。

    mqttClient.setServer(endpoint, port);

そしてコールバック。コールバック函数をセットしておくと、受け取ったメッセージをハンドルできる。

    mqttClient.setCallback(mqttCallback);

今回、コールバック函数は非常にシンプルにトピック名とメッセージ本体をシリアル出力するだけの内容になっている。

void mqttCallback (char* topic, byte* payload, unsigned int length) {
    Serial.print("Received. topic=");
    Serial.println(topic);
    for (int i = 0; i < length; i++) {
        Serial.print((char)payload[i]);
    }
    Serial.print("\n");
}

実際の接続はこんな感じ。

void connectAWSIoT() {
    while (!mqttClient.connected()) {
        if (mqttClient.connect("ESP32_device")) {
            Serial.println("Connected.");
            int qos = 0;
            mqttClient.subscribe(subTopic, qos);
            Serial.println("Subscribed.");
        } else {
            Serial.print("Failed. Error state=");
            Serial.print(mqttClient.state());
            // Wait 5 seconds before retrying
            delay(5000);
        }
    }
}

PubSubClient::connectで実際にブローカに接続。引数として渡しているのはClient IDだが、これは識別できればなんでもいい。失敗したら5秒待つことにしておいた。

Subscribeするのはこんな具合。

            mqttClient.subscribe(subTopic, qos);

メッセージを受け取ったときはPubSubClient::setCallbackでセットしたコールバック函数が呼ばれる。

接続したらPubSubClient::loopを呼び続ける。

    mqttClient.loop();

Publishも簡単。

        mqttClient.publish(pubTopic, pubMessage);

今回のサンプルコードは、5秒ごとにインクリメントした値を送るようにしてみた。これを実際動かしてみると、自分でShadowを更新した内容が $aws/things/<Your_device_name>/shadow/update/delta にpublishされたのを受け取れる。もっとじっくり試したい場合は、mqttClient.publish(pubTopic, pubMessage); の行をコメントにして5秒ごとのPublishを止め、AWS IoT Consoleから手動でトピック $aws/things/<Your_device_name>/shadow/update にメッセージをpublishしてみると良い。

うまくいったらおめでとう。うまくいかなかったら、次のエントリでチェックポイントを列挙するのでそちらを読むと解決するかも。