Step4 M5側:受信してLEDを点灯させる

前回PC側で、動画のピクセル色をMQTTで送信する準備ができました。
このステップでは、M5StickC Plus2を「受信機」として設定し、その情報を受け取ってLEDを光らせます。

プログラムの全体像

このコードは、大きく分けて4つの役割を担っています。

  1. Wi-Fi接続
    • インターネットに接続して、MQTTサーバーと通信できるようにします。
  2. MQTT接続
    • MQTTサーバーに接続し、PCが送ってくる情報を受け取れるようにします。
  3. LED制御
    • 受け取った情報(RGB値)をもとに、LEDを光らせます。
  4. 画面表示
    • M5の画面に、現在の接続状態や受信した情報などを表示して、状況をわかりやすくします。

MQTT通信のルール

PC側とM5側で、同じ「トピック」という住所を使って情報をやり取りします。

  • PC側
    • m5class/roomA/任意ID/ledというトピックに「255,0,0」のような色情報を送信(publish)します。
  • M5側
    • m5class/roomA/任意ID/ledというトピックを購読(subscribe)します。
      • 任意IDの箇所を「+」にするとなんでも受け入れるようになります。
      • +(プラス)は「ワイルドカード」と言って、「この部分はどんな名前でもOK」という意味です。
      • これにより、誰が送った情報でも受け取ることができます。

データの受け渡し

PCから送られる色情報は「255,0,0」のような文字列です。
M5側では、この文字列をsscanf()という関数を使って、それぞれの数字(255、0、0)に分解し、RGBそれぞれの値として扱えるようにします。

LEDの制御

コード内のAdafruit_NeoPixelというライブラリを使って、受け取ったRGB値でLEDの色を設定し、strip.show()という命令で実際に光らせます。

M5側:コード

※ひとまずトピックはtsuchidaのままでいいです。

※SSIDとPASSの設定忘れずに!

step04_led_subscriber_mqtt.ino(M5・受信側)
/**
 * M5StickC Plus2 → test.mosquitto.org (MQTT over TLS) + NeoPixel
 * 解説:
 * - 公開ブローカー "test.mosquitto.org" のポート8883 (MQTTS) に接続します。
 * - WiFiClientSecure を使い、TLS (SSL) で通信を暗号化します。
 * - 証明書検証は setInsecure() でスキップし、手軽に接続できるようにしています。
 * - 受信した "R,G,B" 文字列を解析し、NeoPixel LEDテープを光らせます。
 */

// 必要なライブラリを読み込みます
#include <M5Unified.h>          // M5StickC Plus2の画面や電源管理などをまとめたライブラリ
#include <WiFi.h>               // ESP32でWi-Fi接続を行うための標準ライブラリ
#include <WiFiClientSecure.h>   // 安全な通信(TLS/SSL)を行うためのライブラリ(ポート8883用)
#include <PubSubClient.h>       // MQTT通信を行うためのライブラリ
#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";

// ======= MQTTサーバー設定 =======
// 1. test.mosquitto.org (公開サーバー・認証なし・TLSあり)
const char* MQTT_HOST = "test.mosquitto.org";
// ポート番号。8883は「暗号化あり(MQTTS)」の標準ポートです。
const int MQTT_PORT = 8883;

// ユーザー名とパスワード(公開サーバーなのでNULL=なし)
const char* MQTT_USER = NULL;
const char* MQTT_PASS = NULL;

/* // (参考) HiveMQ Cloud を使う場合
// const char* MQTT_HOST = "xxxxxxxx.s1.eu.hivemq.cloud";
// const int MQTT_PORT = 8883;
// const char* MQTT_USER = "your_username";
// const char* MQTT_PASS = "your_password";
*/

// ======= トピック設定 =======
// 購読(Subscribe)するトピック。PCから送られてくるデータを受け取る宛先です。
// "m5class/roomA/+/led" の "+" はワイルドカードで、誰宛のデータでも受け取る設定です。
// 特定の人(自分など)だけ受信したい場合は "m5class/roomA/tsuchida/led" のように指定します。
// const char* MQTT_SUB = "m5class/roomA/+/led";
const char* MQTT_SUB = "m5class/roomA/tsuchida/led";

// 自分の接続状態を通知するためのトピック(生存確認用)
// "status" というサブトピックに "online" や "offline" を書き込みます。
const char* TOPIC_STAT = "m5class/roomA/tsuchida/status";

// ======= NeoPixel (LED) 設定 =======
// LEDテープを接続しているピン番号(G32など、配線に合わせて変更してください)
#define LED_PIN 32
// 接続しているLEDの個数
#define LED_COUNT 15

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

// ======= MQTTクライアント準備 =======
// TLS(暗号化)通信を行うためのクライアント機能を作成
WiFiClientSecure tlsClient;
// そのクライアントを使ってMQTT通信を行う機能を作成
PubSubClient mqtt(tlsClient);

// 画面の表示更新タイミングを管理するための変数
uint32_t lastUiMs = 0;

// -------- データ受信時の処理 --------
// MQTTでメッセージが届いた時に自動的に呼び出される関数(コールバック関数)
// topic: 届いたトピック名, payload: データの中身(バイト列), len: データの長さ
void onMqtt(char* topic, byte* payload, unsigned int len) {
  // 受信したデータを扱いやすい「文字列」に変換するための準備
  static char buf[64];
  
  // データが長すぎてバッファから溢れないように長さを制限します
  len = (len < sizeof(buf) - 1) ? len : sizeof(buf) - 1;
  
  // payloadの中身をbufにコピーします
  memcpy(buf, payload, len);
  
  // 文字列の最後には必ず「終端文字(\0)」を付けるルールがあります
  buf[len] = '\0';

  // 受信した内容をM5の画面下部に表示して確認できるようにします
  M5.Display.setTextColor(TFT_WHITE, TFT_BLACK); // 白文字、背景黒
  M5.Display.setCursor(8, 200);                  // 表示位置を指定
  M5.Display.printf("RX %s : %s        \n", topic, buf); // トピックとデータを表示

  // 期待しているデータの形式は "R,G,B"(例: "255,0,100")です
  int r = 0, g = 0, b = 0;
  
  // sscanf関数を使って、文字列から3つの数字を取り出します
  // 戻り値が3なら、正しく3つの数字が読み取れたということです
  if (sscanf(buf, "%d,%d,%d", &r, &g, &b) == 3) {
    // 数字が0〜255の範囲に収まるように調整します(異常値対策)
    r = constrain(r, 0, 255);
    g = constrain(g, 0, 255);
    b = constrain(b, 0, 255);

    // NeoPixel用に色のデータを作成します
    uint32_t color = strip.Color(r, g, b);
    
    // ★ガンマ補正:人間の目にとって自然な明るさに変換します
    color = strip.gamma32(color);

    // 全てのLEDに対して同じ色を設定します
    for (int i = 0; i < LED_COUNT; ++i) {
      strip.setPixelColor(i, color);
    }
    
    // 設定した色を反映させて実際に光らせます
    strip.show();
  }
}

// -------- MQTT接続確認・再接続処理 --------
// 定期的に呼び出し、接続が切れていたら再接続を試みます
void ensureMqtt() {
  // すでに接続されているなら何もしないで戻ります
  if (mqtt.connected()) return;

  // 画面に「接続中…」と表示します
  M5.Display.setTextColor(TFT_YELLOW, TFT_BLACK); // 黄色文字
  M5.Display.setCursor(8, 100);
  M5.Display.println("MQTT: connecting...");

  // クライアントIDをランダムに生成します
  // 同じIDを持つ機器が複数あると、接続が弾かれてしまうためです
  String cid = String("m5-") + String((uint32_t)esp_random(), HEX);

  // LWT(遺言)機能の設定:
  // もし電源が急に切れるなどして通信が途絶えたら、サーバーが自動でこのメッセージを書き込みます
  const char* willTopic = TOPIC_STAT;   // 書き込む場所
  const char* willMessage = "offline";  // 書き込む内容
  const bool willRetain = true;         // 後から来た人にも知らせるか
  const int willQos = 0;                // 通信品質設定

  // サーバーへの接続を試みます
  // ユーザー名(MQTT_USER)とパスワード(MQTT_PASS)はNULLの場合、認証なしで接続します
  bool ok = mqtt.connect(
    cid.c_str(),           // クライアントID
    MQTT_USER, MQTT_PASS,  // ユーザー名とパスワード(今回はNULL)
    willTopic, willQos, willRetain, willMessage // LWT設定
  );

  if (ok) {
    // 接続に成功した場合
    
    // 指定したトピックを購読(受信待ち)開始します
    mqtt.subscribe(MQTT_SUB);
    
    // 自分のステータスを「online」にして送信します
    mqtt.publish(TOPIC_STAT, "online", true);

    // 画面に「接続成功」と緑色で表示します
    M5.Display.setTextColor(TFT_GREEN, TFT_BLACK);
    M5.Display.setCursor(8, 120);
    M5.Display.println("MQTT: connected");
  } else {
    // 接続に失敗した場合
    
    // 画面に「失敗」と赤色で表示し、エラーコード(rc)も出します
    M5.Display.setTextColor(TFT_RED, TFT_BLACK);
    M5.Display.setCursor(8, 120);
    M5.Display.printf("MQTT NG, rc=%d\n", mqtt.state());
    
    // すぐ再試行すると負荷がかかるので、1秒待ちます
    delay(1000);
  }
}

// -------- 初期設定 (一度だけ実行) --------
void setup() {
  // ---- M5本体の初期化 ----
  auto cfg = M5.config();            // 設定情報を取得
  M5.begin(cfg);                     // M5デバイスを開始
  M5.Display.setRotation(1);         // 画面の向きを横長(1)に設定
  M5.Display.fillScreen(TFT_BLACK);  // 画面全体を黒で塗りつぶし
  M5.Display.setTextSize(1);         // 文字サイズを標準に設定
  M5.Display.setTextColor(TFT_CYAN, TFT_BLACK); // 水色文字
  M5.Display.setCursor(8, 8);        // 左上にカーソル移動
  M5.Display.println("MQTT TLS + NeoPixel");  // タイトル表示
  M5.Display.setTextColor(TFT_WHITE, TFT_BLACK); // 白文字に戻す

  // ---- NeoPixel(LED)の初期化 ----
  strip.begin();            // LED制御を開始
  strip.setBrightness(30);  // 明るさを設定 (0〜255)。30くらいが眩しすぎず適当です
  strip.clear();            // 色データをリセット
  strip.show();             // 一旦消灯させる

  // ---- Wi-Fi接続 ----
  M5.Display.setCursor(8, 40);
  M5.Display.printf("Wi-Fi: %s\n", WIFI_SSID); // SSIDを表示
  WiFi.mode(WIFI_STA);               // 子機モードに設定
  WiFi.begin(WIFI_SSID, WIFI_PASS);  // 接続開始
  
  // 接続待ちのタイムアウト計測用
  uint32_t t0 = millis();
  
  // つながるまでループして待機
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);                   // 0.3秒待つ
    M5.Display.print(".");        // 進捗を示すドットを表示
    
    // もし20秒経っても繋がらなければ
    if (millis() - t0 > 20000) {
      M5.Display.println("\nWi-Fi timeout. Reboot.");
      delay(800);
      esp_restart();  // 強制再起動(授業などでの復帰用)
    }
  }
  // 接続成功したらIPアドレスを表示
  M5.Display.setCursor(8, 70);
  M5.Display.printf("IP: %s\n", WiFi.localIP().toString().c_str());

  // ---- TLS (SSL) 設定 ----
  // test.mosquitto.org の 8883番ポートは暗号化されていますが、
  // 簡単のため証明書の検証をスキップする設定にします
  tlsClient.setInsecure();

  // ---- MQTTクライアント設定 ----
  mqtt.setServer(MQTT_HOST, MQTT_PORT);  // 接続先サーバーとポートを設定
  mqtt.setCallback(onMqtt);              // 受信時に呼ぶ関数を登録
  mqtt.setBufferSize(512);               // 通信バッファサイズを確保
  mqtt.setKeepAlive(30);                 // 接続維持確認の間隔(秒)
  mqtt.setSocketTimeout(10);             // 通信タイムアウト設定(秒)

  // 最初の接続を試みます
  ensureMqtt();
}

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

  ensureMqtt();  // MQTT接続が切れていないか確認し、切れていれば再接続
  mqtt.loop();   // MQTTの送受信処理を実行(これを呼ばないと通信できません)

  // 画面情報の更新(1秒に1回だけ行う)
  uint32_t now = millis();
  if (now - lastUiMs >= 1000) {
    lastUiMs = now;

    // Wi-Fiの電波強度(RSSI)を表示
    M5.Display.setCursor(8, 130);
    M5.Display.printf("WiFi %s RSSI %d dBm   \n",
                      (WiFi.status() == WL_CONNECTED ? "OK" : "NG"), WiFi.RSSI());

    // MQTTの接続状態とエラーコードを表示
    M5.Display.setCursor(8, 150);
    M5.Display.printf("MQTT %s (state %d)    \n",
                      (mqtt.connected() ? "OK" : "NG"), mqtt.state());
  }

  // CPUを休ませるためのごく短い待機(省電力・安定動作のため)
  delay(1);
}

※LEDのDINピンはG32に刺さるように設定しています。

// M5StickC Plus2 の Grove 端子に接続した想定の信号ピン(手元の配線に合わせて変更可)
#define LED_PIN 32

※ tsuchidaのところは自分の名前に。一斉に光らせる場合にはワイルドカード「+」を選択。

コード解説

setup() 関数

M5が起動したときに一度だけ実行されます。

  • M5.begin():
    • M5本体の初期設定をします。画面表示やボタンなどを使えるようにします。

  • WiFi.begin():
    • Wi-Fiに接続します。
  • tlsClient.setInsecure():
    • 今回接続する test.mosquitto.org のポート8883は暗号化通信(TLS)を行いますが、証明書の検証を簡略化するために使用します。

  • mqtt.setCallback(onMqtt):
    • onMqttという名前の関数を、メッセージが届くたびに呼び出してね」とMQTTクライアントに教えています。

tlsClient.setInsecure() の役割と注意点(時間がある際に読んでみてください)

このプログラムでは、MQTTサーバーとの安全な通信のために「TLS(Transport Layer Security)」という仕組みを使っています。 このTLS通信を正しく行うには、本来、接続先のサーバーが「信頼できる」ことを証明するための「証明書」というものが必要になります。しかし、この確認作業は少し複雑なため、授業でスムーズに通信を試すにはハードルとなります。

  • この命令がやっていること
    • tlsClient.setInsecure()は、「証明書の検証を一時的にスキップする」という命令です。 例えるなら、「この荷物は〇〇さんからのものだと、身分証明書で確認する必要があるけど、今回は時間が無いから、身分証明書を見なくても受け取ってしまおう!」という状態です。これを使うことで、複雑な設定なしに暗号化通信(MQTTS)を確立できます。

  • なぜ「製品開発では使わない」のか
    • この命令は、あくまで実験やデモのために用意されています。証明書の検証をスキップすると、悪意のあるサーバーが本物のサーバーになりすましていた場合に気づけないリスクがあります。 今回の授業では公開データ(RGB値)をやり取りするだけなので問題ありませんが、将来パスワードや個人情報を扱う作品を作る場合は、この命令は使わず、正しく証明書を設定(setCACert等を使用)する必要があります。

onMqtt() 関数

この関数は、MQTTでメッセージが届くと自動的に呼び出されます。

  • sscanf(buf, "%d,%d,%d", &r, &g, &b):
    • 届いた文字列(buf)から、RGBの3つの数字を読み取ります。

  • r = constrain(r, 0, 255):
    • RGBの値は0から255の範囲でなければならないので、もし範囲外の値が届いたら、自動的に正しい範囲に収めるようにしています。

  • strip.setPixelColor(i, color):
    • NeoPixelに「何番目のLEDを、何色で光らせるか」を指示します。

  • strip.show():
    • 設定した色で、実際にLEDを光らせます。

loop() 関数

この関数は、M5が動いている間、ずっと繰り返し実行されます。

  • ensureMqtt():
    • MQTTの接続が切れていないか常に確認し、もし切れていたら自動的につなぎ直します。

  • mqtt.loop():
    • MQTT通信をスムーズに行うための、とても大事な処理です。この命令を常に実行することで、メッセージの受信や接続の維持が行われます。

動作確認

このコードをM5StickC Plus2に書き込むと、PCから送られてくる動画の色情報に合わせて、LEDが光り始めるはずです。動画とシンクロしているはずです。

全員がトピック名をワイルドカード「+」に設定した場合、全員のLEDの明滅がシンクロするはずです。サーバーの性能や通信環境、通信量や通信頻度にもよりますが、数百台から数千台、それ以上のデバイスの同期が可能なはずです(試したことないので試したいです)。