【ESP32入門】AWS IoT Core へ MQTT 接続しメッセージ送信してみた

記事タイトルとURLをコピーする

はじめに

前回、以下の記事で開発環境を作成しました。
【ESP32入門】開発環境をセットアップしてLチカしてみた

今回は、AWS IoT Core を使って、ESP32 から送信されたメッセージを受信するところをやってみようと思います。

前提

Apple シリコン搭載の Mac で動作確認しています。 Python 3系が動作する前提で進めます。

開発環境のセットアップに関しては、前回の記事を参照してください。

ESP32 から AWS IoT Core へメッセージ送信する際に、 Wi-Fi を使うので、 SSID と パスワードが必要になります。

必要なもの

前回同様、以下のボードを使用します。

Freenove ESP32-WROVER CAM

選定理由は公式のリポジトリサンプルプロジェクトが公開されており、初心者でも手軽に試せる点が決め手となりました。
安価なのに、 Wi-Fi や Bluetooth に加え、カメラモジュールが標準で搭載されています。

AWS IoT Policy の作成

CloudFormation で Policy の作成を行います。 後述する証明書にこのポリシーを紐づけることで、機器の権限を制御することが可能です。

Description: "esp32 iot-core"

Resources:
  # ポリシー
  Policy:
    Type: AWS::IoT::Policy
    Properties:
      PolicyName: "esp32-iot-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - iot:*
            Resource: "*"

今回は、 Action や Resource を全許可していますが、実運用する場合は必要に応じて制限してください。

AWS IoT Thing(モノ) の作成

モノと証明書を作成します。 モノに証明書を、証明書にポリシーをアタッチしていきます。

モノを作成

AWS IoT Thing(モノ) の作成します。

aws iot  create-thing --thing-name ESP32

証明書を作成

証明書を作成します。 出力される certificateArn は控えておいてください。次のコマンド実行時にしようします。

aws iot create-keys-and-certificate --set-as-active --certificate-pem-outfile "certificate.pem" --public-key-outfile "publicKey.pem" --private-key-outfile "privateKey.pem"

証明書にポリシーをアタッチ

前項で作成した証明書にポリシーをアタッチします。 certificateArn は先ほど控えた値に置き換えてください。

aws iot attach-policy --policy-name esp32-iot-policy --target {certificateArn}

モノに証明書をアタッチ

AWS IoT Thing(モノ) に証明書をアタッチします。 certificateArn は先ほど控えた値に置き換えてください。

aws iot attach-thing-principal --thing-name ESP32 --principal {certificateArn}

これで、 AWS 側の受信準備は完了です。

ESP32 からメッセージ送信

サンプルリポジトリの取得

こちらのリポジトリに AWS IoT Core へメッセージを送信するサンプルプログラムが格納されているので、 Clone してください。 https://github.com/espressif/esp-aws-iot

cd ~
mkdir esp
cd esp
git clone git@github.com:espressif/esp-aws-iot.git

証明書の設置

ESP32 が AWS IoT Core へメッセージを送信する際の認証として、 ダウンロードしていた証明書を特定のディレクトリに設置します。

esp-aws-iot/examples/mqtt/tls_mutual_auth/main/certs
に以下のルールに沿ってファイルを配置してください。

格納するファイル名 元となるファイル
client.crt certificate.pem
privateKey.pem privateKey.pem
root_cert_auth.crt AmazonRootCA1.pem

設定の書き込み

Wi-Fi や MQTT のエンドポイントの設定を行います。

cd ~/esp/esp-aws-iot/examples/mqtt/tls_mutual_auth/
idf.py menuconfig

もしパスが通らない場合、前回の記事の通り、
. $HOME/esp-idf/export.sh を実行してください。

menuconfig が実行できたら、 以下の設定を行なってください。

Example Configuration

AWS IoT Core との接続設定を行います。

項目名 備考
The MQTT client identifier used in this example ESP32 自由な機器名で大丈夫です
Endpoint of the MQTT broker to connect to {ランダム文字列}-ats.iot.ap-northeast-1.amazonaws.com AWS マネコン-> IoT Core -> 接続 ->ドメイン設定

Example Connection Configuration

ネットワークの接続設定を行います。

項目名
WiFi SSID 接続先の SSID
WiFi Password 接続先のパスワード

プログラムの実行

cd ~/esp/esp-aws-iot/examples/mqtt/tls_mutual_auth/

# パスはお使いの環境に合わせて適宜変更してください
idf.py -p /dev/cu.usbserial-1110 build flash monitor

受信確認

AWS マネコン-> IoT Core -> テスト -> MQTT テストクライアント のページに遷移します。 トピックのフィルターに ESP32/example/topic を設定すると、以下のようにメッセージが受信できていることが確認できます。 ※ ESP32 の箇所は、 menuconfig で設定した機器名になります。

AWS IoT Core サブスクライブした様子

プログラムの削除

monitor を終了しても、ESP32のフラッシュメモリにはLチカプログラムが書き込まれたままになるため、電源を再投入すると再び AWS IoT Core へ送信し始めます。

予期せぬ動作を防ぐためにも、書き込んだプログラムを消去しておくことをお勧めします。

erase_flash でフラッシュメモリを消去することができます。

# パスはお使いの環境に合わせて適宜変更してください
idf.py -p /dev/cu.usbserial-1110 erase_flash

プログラム確認

サンプルプログラムを見ていきます。

Main 関数(app_main)

該当プログラム
https://github.com/espressif/esp-aws-iot/blob/master/examples/mqtt/tls_mutual_auth/main/app_main.c

デバイスをネットワークに接続し、 AWS IoT Core へ送信しています。

void app_main()
{
    // アプリケーションの起動ログを出力
    ESP_LOGI(TAG, "[APP] Startup..");
    // 利用可能なヒープメモリサイズをログ出力
    ESP_LOGI(TAG, "[APP] Free memory: %"PRIu32" bytes", esp_get_free_heap_size());
    // 使用しているIDFのバージョンをログ出力
    ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());

    // 全てのログレベルをINFOに設定
    esp_log_level_set("*", ESP_LOG_INFO);
    
    /* NVSパーティションを初期化 */
    esp_err_t ret = nvs_flash_init();
    // NVSが初期化できなかった場合 (NVS_NO_FREE_PAGES: 空きページがない、NVS_NEW_VERSION_FOUND: 新しいバージョンが見つかった)
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        /* NVSパーティションが切り詰められたか、
         * 消去する必要がある */
        ESP_ERROR_CHECK(nvs_flash_erase()); // NVSフラッシュを消去
        /* nvs_flash_init を再試行 */
        ESP_ERROR_CHECK(nvs_flash_init());  // 再度NVSを初期化
    }
    
    // ネットワークインターフェースを初期化
    ESP_ERROR_CHECK(esp_netif_init());
    // デフォルトのイベントループを作成
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    /* menuconfig で選択された Wi-Fi またはイーサネットを設定します。*/
    ESP_ERROR_CHECK(example_connect());

    // AWS IoT デモのメイン関数を呼び出し
    // ネットワーク接続が確立された後に実行されます。
    aws_iot_demo_main(0,NULL);
}

NVS(Non-Volatile Storage 不揮発性ストレージ) は、機器に搭載されているフラッシュメモリの一部を利用し、永続的なデータを保存するメカニズムです。 処理の中で初期化しているのは、書き込む容量を確保するためだと思います。

AWS IoT Core へ送信している箇所

該当プログラムを見てみます。長いので、削っています。
https://github.com/espressif/esp-aws-iot/blob/master/examples/mqtt/tls_mutual_auth/main/mqtt_demo_mutual_auth.c

MQTT で接続し、セッションが残っていればそれを使用、なければ再接続実施する。 接続が確立されると、メッセージがパブリッシュされるといった流れのようです。

概ね想定通りですが、毎回接続しにいくのではなく、セッションを利用することで接続処理コストを抑えられるんだなと思いました。

int aws_iot_demo_main( int argc,char ** argv )
{
    /* MQTTライブラリを初期化します。*/
    returnStatus = initializeMqtt( &mqttContext, &xNetworkContext );

    // MQTT初期化が成功した場合
    if( returnStatus == EXIT_SUCCESS )
    {
        // 無限ループ。MQTTブローカーへの接続と、サブスクライブ/パブリッシュのサイクルを継続的に行う。
        for( ; ; )
        {
            /* MQTTブローカーへの接続を試みます。接続が失敗した場合、タイムアウト後に再試行します。*/
            returnStatus = connectToServerWithBackoffRetries( &xNetworkContext, &mqttContext, &clientSessionPresent, &brokerSessionPresent );

            // 接続に失敗した場合
            if( returnStatus == EXIT_FAILURE )
            {
                /* 接続失敗を示すエラーをログに出力します。 */
                LogError( ( "Failed to connect to MQTT broker %.*s.",AWS_IOT_ENDPOINT_LENGTH,AWS_IOT_ENDPOINT ) );
            }
            // 接続に成功した場合
            else
            {
                /* MQTTクライアントセッションが保存されたことを示すフラグを更新します。*/
                clientSessionPresent = true;

                /* セッションが存在するか、再送信する必要がある送信中のパブリッシュがあるかを確認します。*/
                if( brokerSessionPresent == true )
                {
                    // ブローカー側でもセッションが再確立された場合
                    LogInfo( ( "An MQTT session with broker is re-established. "
                               "Resending unacked publishes." ) );

                    /* パブリッシュメッセージの再送信を全て処理します。 */
                    // 前回のセッションで未確認 (unacked) のパブリッシュメッセージがあれば再送信する
                    returnStatus = handlePublishResend( &mqttContext );
                }
                else
                {
                    // ブローカー側でセッションが新しく開始された場合 (クリーンセッション)
                    LogInfo( ( "A clean MQTT connection is established."
                               " Cleaning up all the stored outgoing publishes.\n\n" ) );

                    /* この新しい接続は既存のセッションを再確立しないため、
                     * ACKを待っている送信中のパブリッシュをクリーンアップします。 */
                    // 未確認のパブリッシュメッセージを破棄する (新しいセッションのため)
                    cleanupOutgoingPublishes();
                }

                /* TLSセッションが確立された場合、サブスクライブ/パブリッシュループを実行します。 */
                // 接続が確立されたら、実際のMQTT通信 (購読と発行) を行うメインループに入る
                returnStatus = subscribePublishLoop( &mqttContext );
            }

        }
    }
}

プログラムを改修しランダムな値を送るようにする

固定のメッセージだと味気ないため、ランダムな値を送るように変えてみます。
(温度センサーを用意していたのですが、うまく取り付けられなかったので、仕方がなくこの形にします・・。)

publishToTopic でデータを送信しているので、そこを以下のように変えてみます。

static int publishToTopic( MQTTContext_t * pMqttContext )
{
    if( returnStatus == EXIT_FAILURE )
    {
        LogError( ( "Unable to find a free spot for outgoing PUBLISH message.\n\n" ) );
    }
    else
    {
        /* 変更ここから */

        int32_t randomNumber = rand() % 1000; // 0から999までのランダムな整数を生成

        // ランダムな数値を格納するためのバッファを定義
        // {"random_value": XXXXXX} 程度の文字列が入れば十分なので、余裕をもって64バイトとします。
        static char publishPayloadBuffer[64];

        // JSON形式の文字列を作成(例: {"random_value": 123})
        int payloadLength = snprintf(publishPayloadBuffer, sizeof(publishPayloadBuffer),
                                     "{\"random_value\": %ld}", (long)randomNumber);



        /* This example publishes to only one topic and uses QOS1. */
        outgoingPublishPackets[ publishIndex ].pubInfo.qos = MQTTQoS1;
        outgoingPublishPackets[ publishIndex ].pubInfo.pTopicName = MQTT_EXAMPLE_TOPIC;
        outgoingPublishPackets[ publishIndex ].pubInfo.topicNameLength = MQTT_EXAMPLE_TOPIC_LENGTH;
        // outgoingPublishPackets[ publishIndex ].≈ = MQTT_EXAMPLE_MESSAGE;
        // outgoingPublishPackets[ publishIndex ].pubInfo.payloadLength = MQTT_EXAMPLE_MESSAGE_LENGTH;
        outgoingPublishPackets[ publishIndex ].pubInfo.pPayload = publishPayloadBuffer; // ランダムな数値が入ったバッファをセット
        outgoingPublishPackets[ publishIndex ].pubInfo.payloadLength = (uint16_t)payloadLength; // 生成されたペイロードの長さをセット

        /* 変更ここまで */
}

無事 0から999までのランダムな整数が MQTT で送信されていることを確認できした!

0から999までのランダムな整数が送信されていることを確認

詰まった点

ビルド失敗

ビルドした際に以下のようなエラーに遭遇しました。

CMake Error at run_serial_tool.cmake:58 (message):
  
  /Users/sample/.espressif/python_env/idf5.0_py3.8_env/bin/python;;/Users/sample/esp-idf/components/esptool_py/esptool/esptool.py;--chip;esp32
  failed


FAILED: CMakeFiles/flash /Users/sample/esp/esp-aws-iot/examples/mqtt/tls_mutual_auth/build/CMakeFiles/flash 

どうやら、過去にビルドした成果物が影響してしまっているようです。 以下を実行し、再ビルドすると成功しました。

idf.py fullclean

温度センサー

ジャンパー線と温度センサーを用意して接続しようとしたのですが、
おそらく用意した温度センサーは半田付け前提だったため、すぐに折れてしまいました・・。

温度センサー選びは慎重にしたいです。

折れた温度センサー(ごめんなさい)

まとめ

AWS IoT Core へ送信できるところまで実現できました。
擬似データで検証していたときは、awsiotsdkを使っていたので、 比較的シンプルなコードで接続できていましたが、サンプルプログラムの方は見慣れない関数も多く、やや難しいなと思いました。

普段使っている Python に近い MicroPython でも開発できるようなので、今度試してみます。