Step5-1 PC側:中継サーバーの準備 (Node.js)

フォルダとライブラリの準備

ターミナル(WindowsならPowerShellやコマンドプロンプト、macOSならターミナル)を開きます。
以下のコマンドを順番に実行してください。

mkdir step05_video_color_udp_sync
cd step05_video_color_udp_sync
npm init -y
npm i ws

bridge.js の作成

VSCodeを使って step05_video_color_udp_sync フォルダ内に bridge.js というファイルを作成し、以下のコードをコピー&ペーストします。

bridge.js
/**
 * UDP Bridge Server (Node.js)
 * 役割: p5.js (WebSocket) と M5StickC (UDP) の間の通訳
 * 理由: ブラウザは直接UDP通信ができないため、このNode.jsアプリが仲介します。
 */

// --- 1. 必要なライブラリの読み込み ---
const WebSocket = require('ws');   // WebSocket通信用
const dgram = require('dgram');    // UDP通信用

// --- 2. 通信設定(定数) ---

const WS_PORT = 8080;       // p5.js (ブラウザ) からの接続を受け付けるポート
const UDP_PORT = 8000;      // M5StickC (マイコン) へデータを送信する先のポート

// ========== 送信先の設定(どちらか片方を選んでください) ==========

// 【パターンA】 全員に送る(ブロードキャスト) ※現在はこっちが有効
// '255.255.255.255' を指定すると、同じWi-Fi内の全デバイスに届きます
const UDP_HOST = '255.255.255.255';

// 【パターンB】 特定の1台だけに送る(ユニキャスト)
// ※ 特定の相手に送る場合は、上のパターンAをコメントアウト(//)し、
//    下のコメント(//)を外して、M5の画面に表示されたIPアドレスを書いてください。
// const UDP_HOST = '192.168.11.5';  // 例: M5の画面に出ているIP

// =============================================================


// --- 3. UDPクライアント(送信機)の準備 ---

// IPv4形式のUDPソケット(通信の出入り口)を作成します
const udpClient = dgram.createSocket('udp4');

// ソケットをシステムにバインド(紐付け)して準備完了状態にします
udpClient.bind(() => {
    // ブロードキャスト(一斉送信)機能を有効にします
    // ※パターンB(特定送信)の場合でも、この行はそのままで問題ありません
    udpClient.setBroadcast(true);
});


// --- 4. WebSocketサーバー(受信機)の準備 ---

// 指定したポートでWebSocketサーバーを立ち上げます
const wss = new WebSocket.Server({ port: WS_PORT });

// サーバーが起動したことをターミナルに表示します
console.log(`Bridge running... WS:${WS_PORT} -> UDP:${UDP_HOST}:${UDP_PORT}`);


// --- 5. 通信のメイン処理 ---

// ブラウザ(p5.js)から接続があったときに実行されるイベント
wss.on('connection', ws => {
    console.log('p5.js connected!');

    // ブラウザからメッセージ(色データなど)が届いたときに実行されるイベント
    ws.on('message', message => {
        // ★デバッグ用:届いたデータをターミナルに表示!
        // これが表示されなければ、p5.js側がおかしいです
        console.log(`RX: ${message}`);
        
        // 受け取ったメッセージをUDPで送れる「Buffer(バイト列)」に変換します
        const msgBuffer = Buffer.from(message.toString());

        // 変換したデータをUDPを使って送信します
        udpClient.send(msgBuffer, 0, msgBuffer.length, UDP_PORT, UDP_HOST, (err) => {
            if (err) console.error(err);
        });
    });
});


// --- 6. 終了処理(お行儀よく終わる設定) ---

// ターミナルで「Ctrl + C」が押されたときを検知します
process.on('SIGINT', () => {
    console.log('\n[SYS] SIGINT: closing...');
    udpClient.close();                 // UDPソケットを閉じる
    wss.close(() => process.exit(0));  // WebSocketサーバーを閉じて終了
});

コード解説

  • require('ws'):
    • インストールしたWebSocketライブラリを使えるように読み込んでいます。

  • dgram.createSocket('udp4'):
    • UDP通信を行うための「ソケット(差込口)」を作成します。

  • udpClient.setBroadcast(true):
    • これが重要です。特定のIPアドレス(相手)を指定するのではなく、ネットワーク内の全員にデータをばら撒く「ブロードキャスト送信」を許可しています。

  • ws.on('message', ...):
    • ブラウザ(p5.js)からデータが届くたびに実行されます。ここで「受け取ったメッセージ」を即座に「UDPパケット」として転送しています。

  • process.on('SIGINT', ...):
    • プログラムを終了する際(Ctrl+Cを押した際)に、通信ポートを開きっぱなしにしないよう、行儀よく閉じてから終了する処理です。

起動方法

ターミナルで以下のコマンドを実行します。

node bridge.js

以下のように表示されれば準備完了です。

Bridge running... WS:8080 -> UDP:255.255.255.255:8000

※ このターミナルは開いたままにしておいてください。
※ 停止したいときは、ターミナルをクリックしてから Ctrl + C を押します。

Step5-2 PC側:p5.js (sketch.js)

変更のポイント

  • ライブラリ削除: WebSocket は現代のWebブラウザに標準搭載されている機能なので、MQTTの時のような外部ライブラリの読み込みは不要です。
  • 宛先の変更: インターネット上の test.mosquitto.org ではなく、自分のPC内で動いている localhost(中継サーバー)に接続します。

index.html の修正

まず、不要になったMQTTライブラリを削除します。
index.html を開き、MQTTライブラリを読み込む以下の一行を削除します。

<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
index.html
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>UDP</title>
    <script src="https://cdn.jsdelivr.net/npm/p5@1.10.0/lib/p5.min.js"></script>
    <style>
        body {
            margin: 0;
            font: 14px/1.4 system-ui;
            overflow: hidden;
        }

        #ui {
            position: fixed;
            left: 8px;
            top: 8px;
            background: #0008;
            color: #fff;
            padding: 8px;
            border-radius: 8px;
        }
    </style>
</head>

<body>
    <div id="ui">
        status: <span id="st">connecting...</span><br>
        RGB: <span id="rgb">-</span>
    </div>
    <script src="./sketch.js"></script>
</body>

</html>

sketch.js

sketch.js
// グローバル変数
let vid;
let socket; // WebSocketクライアント
let statusText;
let rgbText;

// 中継サーバーのURL(自分自身のPC上の8080ポート)
const wsUrl = 'ws://localhost:8080';

function preload(){
  // 動画ファイルの読み込み
  vid = createVideo("clip.mp4");
}

function setup(){
  // ウィンドウ全体のサイズでキャンバスを作成
  createCanvas(windowWidth, windowHeight);
  
  // 動画のHTML要素は隠す
  vid.hide();
  // 繰り返し再生
  vid.loop();

  statusText = document.getElementById("st");
  rgbText = document.getElementById("rgb");

  // === WebSocket接続 ===
  // ブラウザ標準の機能で接続します
  socket = new WebSocket(wsUrl);

  // 接続が開いたとき
  socket.onopen = () => {
    console.log("WS Connected");
    statusText.innerHTML = "WS Connected";
  };

  // エラー時
  socket.onerror = (error) => {
    console.log("WS Error", error);
    statusText.innerHTML = "WS Error (Run bridge.js!)";
  };
}

// ウィンドウサイズが変更されたときにキャンバスもリサイズする
function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

function draw(){
  background(20);

  // === 画面いっぱいに動画を表示する計算 (Coverモード) ===
  let vidRatio = vid.width / vid.height;
  let canvasRatio = width / height;
  let w, h;

  if (canvasRatio > vidRatio) {
    // 画面の方が横長なら、幅を合わせる(高さがはみ出る)
    w = width;
    h = w / vidRatio;
  } else {
    // 画面の方が縦長なら、高さを合わせる(幅がはみ出る)
    h = height;
    w = h * vidRatio;
  }

  // 中心に配置
  let x = (width - w) / 2;
  let y = (height - h) / 2;

  // 動画を描画
  image(vid, x, y, w, h);

  // === 色情報の取得と送信 ===
  
  // 画面中央の色を取得
  let c = get(width / 2, height / 2);
  rgbText.innerHTML = `${c[0]}, ${c[1]}, ${c[2]}`;

  // プレビュー描画(右下に固定)
  noStroke();
  fill(c[0], c[1], c[2]);
  rect(width - 60, height - 60, 50, 50);

  // WebSocketがつながっているなら送信
  if (socket.readyState === WebSocket.OPEN) {
    // RGB値をカンマ区切りの文字列に変換
    const message = `${c[0]},${c[1]},${c[2]}`;
    socket.send(message);
  }
}

コード解説:p5.js (Full Screen + WebSocket)

このコードは、これまでの「固定サイズのキャンバス × MQTT」から、「全画面表示のキャンバス × WebSocket」 へと進化しています。主な変更点と仕組みを解説します。

1. 画面いっぱいに映像を広げる(Coverモード)

これまでのコードでは、画面サイズと動画サイズが合わないと「黒い余白(黒帯)」ができていました。 今回は、「画面を隙間なく埋め尽くす(Cover)」 計算式を導入しています。

  • 仕組み: 動画と画面、それぞれの「縦横比(アスペクト比)」を計算します。
  • 画面の方が「横長」なら: 動画のを画面に合わせます(動画の上下が少しはみ出します)。
  • 画面の方が「縦長」なら: 動画の高さを画面に合わせます(動画の左右が少しはみ出します)。

これにより、ブラウザのサイズを自由に変えても、大きな映像で体験できます。

2. 通信方式の変更(MQTT → WebSocket)

この sketch.js は、M5StickC Plus2へ直接データを送るのではなく、自分のパソコン内で動いている「中継サーバー (bridge.js)」にデータを渡す役割を持っています。
PC内部での通信なので、インターネットを経由するMQTTではなく、ブラウザ標準の「WebSocket」 を使用しています。

  • new WebSocket('ws://localhost:8080'):
    • localhost は「自分自身のPC」を指します。
    • 8080 は中継サーバーが待ち構えているポート番号です。

  • socket.send(message):
    • トピック(宛先名)の指定などは不要です。パイプで繋がった中継サーバーへ、データを流し込むだけでOKです。

コードの各部詳細

// ウィンドウサイズが変わった時に自動で実行される関数
function windowResized() {
  // キャンバスの大きさを新しいウィンドウサイズに合わせる
  resizeCanvas(windowWidth, windowHeight);
}

function draw() {
  // ...(中略)...

  // === 画面埋め尽くし計算 ===
  // vidRatio: 動画の比率, canvasRatio: 画面の比率
  if (canvasRatio > vidRatio) {
    // 画面の方が横長 → 幅を基準にする
    w = width;
    h = w / vidRatio;
  } else {
    // 画面の方が縦長 → 高さを基準にする
    h = height;
    w = h * vidRatio;
  }
  
  // ...(中略)...

  // WebSocketが開通している時だけデータを送信
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(message);
  }
}