メインコンテンツへスキップ
見出し画像

【IoT】LTE-M Shield for ArduinoでUDP送信し、消費電力量と通信量を低減する方法

    LTE-M Shield for Arduino(BG96)で使用されるTinyGSM(通信用ライブラリ)は、HTTPとTCPには対応しているものの、UDPには対応していません。消費電力量と通信量の低減のため、UDPで送信したい!ということで、実装しました。本記事では、UDP送信実装までの思考プロセスと、HTTP/TCP/UDPの比較試験結果について整理します。LTE-M Shield for Arduinoを用いたIoTシステムを自作される方の参考になれば幸いです。

    背景と目的

    電池駆動型のIoTデバイスにおいて、1回の通信あたりの消費電力量と通信量を低減させることは重要な課題の一つです。消費電力量と通信量に影響を与える主要な因子は「通信プロトコル」です。主な通信プロトコルとして、HTTP、TCP、UDPがありますが、UDPが最も低消費電力かつ低通信量であるため、UDP送信する方法を考えます。

    本記事執筆時点で、LTE-M Shield for ArduinoでTCP送信した事例と、M5Stack用 3G 拡張ボードでUDP送信した事例は見つかりますが、LTE-M Shield for ArduinoでUDP送信した事例が見つかりません。既存記事を参考にしつつ、UDP送信の実装を目指します。

    画像
    通信プロトコルの比較
    出典:SORACOM公式ブログ

    詳細

    ベースとなるコード

    SORACOM公式サイトを見ると、LTE-M Shield for Arduinoでデバイス稼働時間をHTTP送信するコード(send_uptime_with_soracom.ino)が公開されています。このコードをベースとし、UDP送信を実装していきます。下記がloop()関数部コードです。RESET_DURATIONの部分は、使わないので削除しています。

    void loop() {
      long uptime_sec = millis() / 1000;
    
      char payload[120];
      sprintf_P(payload, PSTR("{\"uptime\":%lu}"), uptime_sec);
      CONSOLE.println(payload);
    
      /* connect */
      if (!ctx.connect(ENDPOINT, 80)) {
        CONSOLE.println(F("failed."));
        delay(3000);
        return;
      }
      /* send request */
      char hdr_buf[40];
      ctx.println(F("POST / HTTP/1.1"));
      sprintf_P(hdr_buf, PSTR("Host: %s"), ENDPOINT);
      ctx.println(hdr_buf);
      ctx.println(F("Content-Type: application/json"));
      sprintf_P(hdr_buf, PSTR("Content-Length: %d"), strlen(payload));
      ctx.println(hdr_buf);
      ctx.println();
      ctx.println(payload);
      /* receive response */
      while (ctx.connected()) {
        String line = ctx.readStringUntil('\n');
        CONSOLE.println(line);
        if (line == "\r") {
          CONSOLE.println(F("Response header received."));
          break;
        }
      }
      // NOTE: response body is ignore
      ctx.stop();
    
      delay(INTERVAL_MS);
    }

    主な変更点

    今回、HTTPからUDPへの変更に加え、通信量削減の観点から、送信文字列をJSONからバイナリに変更します。上記コードからの主な変更点は4つです。

    1. int型(float型)の数値を%04X(%08lX)フォーマット指定子をつかって16進数に置き換え

    2. ATコマンドの+QISENDEXを使って16進数の送信用データを作成

    3. 送信先をHTTP 80番ポートからTCP/UDP 23080番ポートに変更

    4. データ送信方法を、ライブラリによる送信からATコマンドによる送信に変更

    5. ATコマンドによる送信の場合、ctxを使っていないため、ctx.stop();不要

    1〜3については参考にした記事に詳しく書かれているため、4,5を深掘りします。

    ATコマンドによるUDP送信

    LTE-M Shield for Arduino(BG96)での通信には、TinyGSMというライブラリが使用されていますが、このライブラリはHTTPとTCPに対応しているものの、UDPに対応していません(ver0.11.7時点で)。ctx.connect(ENDPOINT, 80)を、ctx.connect(ENDPOINT, 23080)に書き換えただけではTCP送信となり、UDP送信されません。。

    UDP送信の実装のためには、ctx.connect()の動きを知る必要がありそうです。ライブラリの中身を覗いてみると、TinyGsmClientBG96.h内の下記コードが実行されているようです。

      bool modemConnect(const char *host, uint16_t port, uint8_t mux,
                        bool ssl = false, int timeout_s = 150)
      {
        if (ssl)
        {
          DBG("SSL not yet supported on this module!");
        }
    
        uint32_t timeout_ms = ((uint32_t)timeout_s) * 1000;
    
        // <PDPcontextID>(1-16), <connectID>(0-11),
        // "TCP/UDP/TCP LISTENER/UDPSERVICE", "<IP_address>/<domain_name>",
        // <remote_port>,<local_port>,<access_mode>(0-2; 0=buffer)
        sendAT(GF("+QIOPEN=1,"), mux, GF(",\""), GF("TCP"), GF("\",\""), host,
               GF("\","), port, GF(",0,0"));
        waitResponse();
    
        if (waitResponse(timeout_ms, GF(GSM_NL "+QIOPEN:")) != 1)
        {
          return false;
        }
    
        if (streamGetIntBefore(',') != mux)
        {
          return false;
        }
        // Read status
        return (0 == streamGetIntBefore('\n'));
      }

    上記コードを見ると、ATコマンドの+QIOPENでデータ送信されています。+QIOPENを深掘りすべく、ATコマンドマニュアルで該当部を見てみました。

    画像
    画像
    出典:BG96_TCP/IP_AT_Commands_Manual_V1.1

    これを見ると、+QIOPENの3つ目の引数である<service_type>を、"UDP"にすると良さそうです。TinyGsmClientBG96.hの該当部を、"TCP"から"UDP"に手動で書き換える手もありますが、ライブラリがアップデートされた時に上書きされてしまいます(これでミスりました)。そこで、Arduinoコード内に記述することとしました。

    が、どう書き換えたらいいかようわからん。ということで、ChatGPTにライブラリを読み込ませ、UDP送信のコードを書いてもらいました。便利〜

    UDP送信に対応したコード

    以下が、HTTP送信をUDP送信に変更したコードです。UDP送信の場合、ctx.stop();は不要と判断し、コメントアウトしています。

    #define CONSOLE Serial
    #define INTERVAL_MS (60000)
    #define ENDPOINT "uni.soracom.io"
    #define SKETCH_NAME "send_uptime_with_soracom_UDP"
    #define VERSION "1.0"
    
    /* for LTE-M Shield for Arduino */
    #define RX 10
    #define TX 11
    #define BAUDRATE 9600
    #define BG96_RESET 15
    
    #define TINY_GSM_MODEM_BG96
    #include <TinyGsmClient.h>
    
    #include <SoftwareSerial.h>
    SoftwareSerial LTE_M_shieldUART(RX, TX);
    TinyGsm modem(LTE_M_shieldUART);
    TinyGsmClient ctx(modem);
    
    void setup() {
      CONSOLE.begin(115200);
    
      CONSOLE.println();
      CONSOLE.print(F("Welcome to ")); CONSOLE.print(SKETCH_NAME); CONSOLE.print(F(" ")); CONSOLE.println(VERSION);
    
      CONSOLE.print(F("resetting module "));
      pinMode(BG96_RESET,OUTPUT);
      digitalWrite(BG96_RESET,LOW);
      delay(300);
      digitalWrite(BG96_RESET,HIGH);
      delay(300);
      digitalWrite(BG96_RESET,LOW);
      CONSOLE.println(F(" done."));
    
      LTE_M_shieldUART.begin(BAUDRATE);
    
      CONSOLE.print(F("modem.restart()"));
      modem.restart();
      CONSOLE.println(F(" done."));
    
      CONSOLE.print(F("modem.getModemInfo(): "));
      String modemInfo = modem.getModemInfo();
      CONSOLE.println(modemInfo);
    
      CONSOLE.print(F("waitForNetwork()"));
      while (!modem.waitForNetwork()) CONSOLE.print(".");
      CONSOLE.println(F(" Ok."));
    
      CONSOLE.print(F("gprsConnect(soracom.io)"));
      modem.gprsConnect("soracom.io", "sora", "sora");
      CONSOLE.println(F(" done."));
    
      CONSOLE.print(F("isNetworkConnected()"));
      while (!modem.isNetworkConnected()) CONSOLE.print(".");
      CONSOLE.println(F(" Ok."));
    
      CONSOLE.print(F("My IP addr: "));
      IPAddress ipaddr = modem.localIP();
      CONSOLE.println(ipaddr);
    }
    
    void loop() {
      long uptime_sec = millis() / 1000;
    
      // バイナリ形式の送信データ作成
      // バイナリパーサーフォーマット: uptime:0:int:16
      char payload[120];
      sprintf_P(payload, PSTR("+QISENDEX=0,\"%04X\""), uptime_sec);  
      CONSOLE.println(payload);
    
      // SORACOMに接続
      int mux = 0;
      modem.sendAT(GF("+QIOPEN=1,"), mux, GF(",\"UDP\",\""), ENDPOINT, GF("\","), 23080, GF(",0,0"));
      modem.waitResponse();
    
      // 接続の応答を待つ
      uint32_t timeout_ms = 15 * 1000; // ライブラリでは150秒だが、長いため15秒とする
      if (modem.waitResponse(timeout_ms, GF(GSM_NL "+QIOPEN:")) != 1) {
        CONSOLE.println(F("failed."));
        delay(3000);
        return;
      }
      
      // 送信
      modem.sendAT(payload);  // send AT command
      // ctx.stop(); // UDP送信の場合不要
    
      delay(INTERVAL_MS);
    }

    HTTP/TCP/UDP比較試験結果

    HTTP/TCP/UDPで、消費電力量と通信量がどの程度変わるのか試験してみました。プログラム開始から終了までの時間(uptimeではない)も記録しました。下記が試験結果です。

    画像

    開始〜終了の時間、消費電力量、通信量とも5回平均の値です。
    HTTP(xJSON)を、UDP(xバイナリー)と比較すると、
    ・開始〜終了の時間:20.2→16.0秒(▲4.2秒)
    ・消費電力量:3.1→2.2mWh(▲0.9mWh)
    ・通信量:7029→346bytes(▲6683bytes、▲95%)
    と、消費電力量、通信量とも削減されていることが確認できました。こうして比較すると、UDPの優位性がよくわかります。

    まとめと今後の課題

    LTE-M Shield for ArduinoでUDP送信するコードを作成できました。また、消費電力量と通信量削減に効果があることがわかりました。ただ、UDP送信に16秒もかかっています。これは時間がかかりすぎです。。次の記事では、setup()関数内の通信モジュール関連のコードを見直すことで、1回の通信あたりの起動時間を高速化し、消費電力量と通信量を低減させることを目指します。

    参考

    ・通信プロトコルの違いについて

    ・LTE-M Shield for Arduino x UDPという気づきを与えてくれたスライド

    ・LTE-M Shield for Arduino x TCPでの実装事例。非常に参考になる。

    ・M5Stack用 3G 拡張ボード x UDPでの実装事例。非常に参考になる。

    ・SORACOMのエントリポイントについて

     
     
    IoTデバイス開発、住宅修繕、家電修理、エアコン設置、電気工事など、できることを増やし続けるモノづくりエンジニア!できること(=生活自給力)を増やし、高知の課題を解きたい。興味はIoT・AI・クラウド、機械・電気・ソフト、空き家再生など。好きな県は高知県、好きな国はインド。

    あなたへのおすすめ