Seeed Studio XIAO ESP32C3のMQTT送信

Seeed K.K.の松岡です。
Seeed Studio XIAO ESP32C3で、MQTTサーバーへパブリッシュ(送信)するコードを作ってみて、どういうところに気をつけなければいけないか確認したいと思います。

作るもの

IoTで定番中の定番、一定時間間隔にMQTTサーバーにJSONメッセージをパブリッシュ(送信)するコードを作ります。

ほぼ動くだけの試験的なものではなく、実運用にも耐えられるものを作りたいと思います。ただし、過度な仕組みを実装するとコードが複雑になってしまい読みにくくなるので、必ず必要ではないと感じたものは実装せずに説明でとどめておきます。

  • MQTT over TLS
    インターネット上にデータを流すなら暗号化は必須ですね。AWS IoT CoreやAzure IoT Hubなどへの接続も想定して、TLSで暗号化したMQTTで通信します。サーバー認証のみで実装しますが、相互認証も出来るように考慮しておきます。
  • MQTTサーバー接続の維持
    MQTTサーバーの接続を維持していると実装が極端に複雑になるので、今回はパブリッシュの都度、接続/切断することにします。
  • 時刻同期
    サーバー証明書の有効期限チェックや、デバイスの時計が合っていないと接続できないサーバーがあるので、SNTPサーバーと時刻同期を実装します。
  • Wi-Fi/サーバー接続不可時の振る舞い
    実運用を意識するなら、Wi-FiアクセスポイントやMQTTサーバーと通信できなくなったときの対処は必須です。分かりやすさを重視して、最初はWi-Fiと時刻同期ができるまで待機(ブロッキング)、定期的なパブリッシュのときは、Wi-FiアクセスポイントやMQTTサーバーと通信できるときにだけパブリッシュすることにします。

そして、C/C++ちょっと苦手な方でも追いかけやすいように、できるだけ独自に関数を用意せず、setup()loop()にダラダラ~と書こうと思います。(再利用しやすいライブラリ化したものは、今後に。)

作ったもの

作ったものがこちらです。

github.com

WiFiでWiFiアクセスポイントに接続して、configTzTime()でSNTPサーバーと時刻同期、WiFiClientSecure+MQTTClientでMQTTサーバーに接続してパプリッシュしています。

ArduinoでMQTTするときはPubSubClientライブラリを使うことが多いのですが、これはQoS0でしかパブリッシュできないようです。今回はWiFiClientSecureと組み合わせて使えて、QoSを指定できるarduino-mqttライブラリを使うことにしました。
また、JSON文字列の構築にArduinoJsonライブラリを使いました。Stringで文字列を組み立てる方法でも問題ありませんが、この先、センサーデータなどからJSON文字列を作ることを想定して、あえてこのライブラリを使いました。

実装のポイント

大まかな構造は下図のとおりです。

WiFi.enableSTA(true)WiFi.begin()でWi-Fiクライアントを有効にします。(WiFi.mode(WIFI_STA)としているサンプルコードが多いようですが、Wi-FiクライアントとWi-Fiアクセスポイントを独立して有効/無効するときのことを想定すると、WiFi.enableSTA(true)の方が良い気がします。) WiFi.begin()は引数にWi-FiのSSIDとパスフレーズを指定します。前回接続時のSSIDとパスフレーズを使うときは引数無しにすればOKです。

WiFi.enableSTA(true);
if (WIFI_SSID[0] != '\0')
{
    WiFi.begin(WIFI_SSID, WIFI_PASSPHRASE);
}
else
{
    WiFi.begin();
}

次に、SNTPサーバーと時刻同期ですが、WiFi.enableSTA(true)した直後はまだWi-Fiアクセスポイントに接続が完了していないので、WiFi.status()がWL_CONNECTEDになるまで待ちます。

while (WiFi.status() != WL_CONNECTED)
{
    Serial.print('.');
    delay(200);
}

SNTPサーバーと時刻同期はconfigTime()もしくはconfigTzTime()で開始します。これもWi-Fiと同様に、時刻同期が完了する前に関数呼び出しが返ってきます。sntp_set_time_sync_notification_cb()でコールバック関数を登録しておき、コールバック関数が呼ばれるまで待機します。

sntp_set_time_sync_notification_cb(TimeSyncNotificationCallback);
TimeSyncCompleted = false;
configTzTime(getenv("TZ"), SNTP_SERVER);

while (!TimeSyncCompleted)
{
    Serial.print('.');
    delay(200);
}

getenv("TZ")の戻り値(ポインタ)を直接configTzTime()に渡すのは若干危険ではある。安全に実装するなら、getenv("TZ")の戻り値を別途確保したメモリにコピーしてからconfigTzTime()に渡す。

あとは、一定時間間隔のパブリッシュです。
WiFi.status()でWi-Fiの接続状態を確認して、接続しているときにパブリッシュします。

if (WiFi.status() != WL_CONNECTED)
{
    ...
}
else
{
    ...

    if (!MqttClient.connect(DEVICE_NAME))
    {
        ...
    }
    else
    {
        ...

        MqttClient.publish(topic, payload, false, LWMQTT_QOS0);
        MqttClient.disconnect();
    }
}

一定時間間隔は、単純にdelay()する方法と、送信にかかった時間を考慮してdelay()する方法があります。たとえば、60秒間隔としていて送信に5秒かかったときに、delay(60)なのかdelay(55)なのかの違いです。今回は、あとからデータが確認しやすい後者で実装しました。

const int sleepSec = StartTime == 0 ? 0 : INTERVAL - (time(nullptr) - StartTime) % INTERVAL;
delay(sleepSec * 1000);

実行結果

1分間隔のパブリッシュを丸一日動かして、MQTTサーバーにパブリッシュしたデータを確認しました。

Wi-FiのRSSIは-60~-50と少し変動していました。部屋の中をウロウロしていたのが影響していたのかもしれません。興味深い。

データ欠損は1440件のうち1件でした。QoS0にしては良好ですね。

ついでに、5V電源供給の消費電流も確認しました。
平均30mAで、ピークは300mA弱でした。delay()で待機しているだけでも30mA程度流れていました。(CPUは停止していないので)

心配なこと

今回の実装で「本当に大丈夫か?」と心配になったことも書いておきます。

  • 稼働中にWi-Fiが切断すると、自動的に再接続するのか?
    自動的に再接続します。
  • 長期間、連続稼働しているとSNTPサーバーと時刻がズレるのでは?
    定期的にSNTPサーバーと時刻同期するので(SNTPサーバーと通信できれば)ズレは一定範囲内に収まります。

まとめ

  • Seeed Studio XIAO ESP32C3のMQTT+TLS パブリッシュは安定動作
  • 消費電流は平均30mA、ピーク300mA弱。(5V電源供給)
  • Wi-Fi再接続と時刻同期は自動

変更履歴

日付 変更者 変更内容
2022/9/12 松岡 作成