Step6 M5側:UDP受信 (step06_led_udp.ino)

WiFiUDP を使って、飛んでくるパケットをキャッチします。
※SSIDとパスワードは変更すること

step06_led_udp.ino
/**
 * M5StickC Plus2 UDP Receiver
 * 解説:
 * - MQTTの代わりに UDP (ポート8000) を監視します。
 * - PCから送られてくるブロードキャストパケット("r,g,b")を受け取ります。
 * - UDPは接続手続きが不要なため、ループ処理が高速です。
 */

// --- 必要なライブラリの読み込み ---
#include <M5Unified.h>          // M5StickC Plus2の画面や電源、ボタン管理用ライブラリ
#include <WiFi.h>               // ESP32でWi-Fi接続を行うための標準ライブラリ
#include <WiFiUdp.h>            // UDP通信(高速な投げっぱなし通信)を行うためのライブラリ
#include <Adafruit_NeoPixel.h>  // LEDテープ(NeoPixel/WS2812B)を制御するライブラリ

// ======= Wi-Fi設定 =======
// 接続するWi-FiのSSID(名前)を設定します
const char* WIFI_SSID = "YOUR_SSID";
// 接続するWi-Fiのパスワードを設定します
const char* WIFI_PASS = "YOUR_PASS";

// ======= UDP設定 =======
// UDP通信を管理するオブジェクトを作成します
WiFiUDP udp;
// 待ち受けるポート番号(送信側のbridge.jsで設定した8000と合わせる必要があります)
const int LOCAL_PORT = 8000;
// 受信したデータ(文字列)を一時的に保存しておくための配列(バッファ)
char packetBuffer[255];

// ======= NeoPixel (LED) 設定 =======
// LEDテープを接続しているピン番号(M5StickC Plus2のG32端子などを想定)
#define LED_PIN 32
// 接続しているLEDの個数(使用するLEDテープに合わせて変更してください)
#define LED_COUNT 15

// NeoPixelオブジェクトを作成(個数, ピン番号, 色の並び順+通信速度設定)
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

// ======= UI更新用変数 =======
// 画面の文字情報を更新した最後の時間を記録する変数(処理落ちを防ぐため)
uint32_t lastUiMs = 0;

// -------- 初期設定 (一度だけ実行) --------
void setup() {
  // ---- M5本体の初期化 ----
  auto cfg = M5.config();    // M5の設定情報を取得
  M5.begin(cfg);             // M5デバイスを開始(電源管理などが動きます)
  M5.Display.setRotation(1); // 画面の向きを横長(1)に設定
  M5.Display.setTextSize(1); // 文字サイズを標準に設定

  // ---- LEDの初期化 ----
  strip.begin();            // LED制御を開始
  strip.setBrightness(30);  // 明るさを設定(0〜255)。30くらいが適当です
  strip.show();             // 一旦、設定(初期状態)を反映させて消灯

  // ---- Wi-Fi接続 ----
  M5.Display.print("WiFi Connecting"); // 画面に接続中と表示
  WiFi.mode(WIFI_STA);                 // 子機(ステーション)モードに設定
  WiFi.begin(WIFI_SSID, WIFI_PASS);    // Wi-Fi接続を開始

  // 接続が完了するまでループして待機
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);              // 0.5秒待つ
    M5.Display.print(".");   // 進捗を示すドットを表示
  }
  
  // 接続完了後の表示
  M5.Display.println("\nConnected!");
  // 自分のIPアドレスを表示(確認用)
  M5.Display.printf("IP: %s\n", WiFi.localIP().toString().c_str());

  // ======= UDP通信の開始 =======
  // 指定したポート番号で、データの待ち受け(リッスン)を開始します
  udp.begin(LOCAL_PORT);
  // 画面に待ち受け開始メッセージを表示
  M5.Display.printf("Listening UDP: %d\n", LOCAL_PORT);
}

// -------- メインループ (繰り返し実行) --------
void loop() {
  M5.update(); // ボタンなどのハードウェア状態を更新

  // ======= UDP受信処理 =======
  
  // パケット(データ)が届いているかポストを確認します
  // 戻り値は「届いたデータのサイズ(バイト数)」。0なら届いていない。
  int packetSize = udp.parsePacket();
  
  // もしデータが届いていたら(サイズが0より大きければ)
  if (packetSize) {
    // データを読み込んで packetBuffer に格納します
    // readの戻り値は実際に読み込めた文字数
    int len = udp.read(packetBuffer, 255);
    
    // データが正しく読み込めた場合
    if (len > 0) {
      // 文字列の最後には必ず「終端文字(\0)」を付けるルールがあります
      // これをしないと、前の古いデータがゴミとして残ることがあります
      packetBuffer[len] = 0;
    }

    // 受信した文字列 "r,g,b" を解析して、数値の r, g, b に変換します
    int r = 0, g = 0, b = 0;
    
    // sscanf は文字列から指定したフォーマットでデータを取り出す関数です
    // 戻り値が3なら、3つの変数が正しく読み取れたことを意味します
    if (sscanf(packetBuffer, "%d,%d,%d", &r, &g, &b) == 3) {
      // 数値が0〜255の範囲に収まるように強制します(エラー防止)
      r = constrain(r, 0, 255);
      g = constrain(g, 0, 255);
      b = constrain(b, 0, 255);

      // 人間の目にとって自然な色になるよう、ガンマ補正をかけます
      uint32_t color = strip.gamma32(strip.Color(r, g, b));
      
      // 全てのLEDに対して同じ色を設定します
      for (int i = 0; i < LED_COUNT; i++) {
        strip.setPixelColor(i, color);
      }
      
      // 設定した色データを送信し、実際にLEDを光らせます
      strip.show();
    }
  }

  // ======= 画面情報の更新 (1秒に1回) =======
  // 毎回画面を描画すると処理が重くなるので、間引いて実行します
  uint32_t now = millis(); // 現在時刻を取得
  
  // 前回の更新から1000ミリ秒(1秒)以上経過していたら
  if (now - lastUiMs >= 1000) {
    lastUiMs = now; // 最終更新時刻を更新

    // カーソル位置を指定(上から100ピクセル目あたり)
    M5.Display.setCursor(0, 100);
    
    // 受信した最後の文字列データを画面に表示(デバッグ用)
    // 末尾の空白は、前の文字が長かった場合に消すためのパディング
    M5.Display.printf("UDP Packet: %s   \n", packetBuffer);
    
    // Wi-Fiの電波強度(RSSI)を表示
    M5.Display.printf("RSSI: %d dBm", WiFi.RSSI());
  }
}

このコードは、PC(中継サーバー)からブロードキャストされたUDPパケットをキャッチし、LEDを光らせる役割を担います。 MQTTのときとは異なり、「接続維持」の概念がなく、非常にシンプルなループ処理で構成されているのが特徴です。

1. UDP通信の開始 (setup)

Wi-Fi接続が完了した後、以下の命令で「受信の待ち受け」を開始します。

// ポート8000番を開けて、データが飛んでくるのを待ちます
udp.begin(LOCAL_PORT);

これだけで準備は完了です。MQTTのようなサーバーへのログイン認証や、トピックの購読手続きは一切不要です。

2. データの受信確認 (loop)

MQTTでは「データが届いたら勝手に関数が呼ばれる(コールバック方式)」でしたが、UDPのこのライブラリでは「ポストに手紙が届いているか、自分で見に行く(ポーリング方式)」という書き方をします。

// ポストを確認する命令。届いていればデータのサイズ(バイト数)が返ってきます。
int packetSize = udp.parsePacket();

if (packetSize) {
  // 届いていた場合だけ、ここが実行されます
  udp.read(packetBuffer, 255); // データを読み出す
  // ...
}

loop() 関数は超高速で繰り返されているため、人間から見ると遅延なくリアルタイムに反応しているように見えます。

3. 文字列の解析 (sscanf)

PCからは "255,128,0" のような文字データが届きます。これをC言語のプログラムで扱える数値に変換するために sscanf という関数を使っています。

// "文字列" から "%d,%d,%d" (整数,整数,整数) のパターンを探して取り出す
if (sscanf(packetBuffer, "%d,%d,%d", &r, &g, &b) == 3) {
  // 成功したら LED に色をセット
}

4. 画面更新の工夫(非同期処理)

高速な反応を実現するため、loop() の中に delay(1000) のような「処理を止める命令」を書くのは厳禁です。止まっている間に飛んできたUDPパケットを取りこぼしてしまうからです。 そのため、millis() (起動してからの経過時間)を使って、「1秒経った時だけ画面を更新する」という書き方をしています。これにより、LEDの制御を止めることなく、画面情報の更新を行っています。

動作確認

MQTTと比べると同期ズレがほとんどなくなったと思います。

ちなみにUDP通信は複数の球体型自走ロボット制御の際に使用していました。
日本語スライドに詳細が記載されています。