(この記事は M5Stack の Advent Calendar 2025 の記事です)
はじめに
この記事で書いている内容は、「ブラウザのキャンバスに描画した画像を、M5Stack のデバイスに送って画面上に表示させる」というものです。
p5.js でブラウザのキャンバスに画像を描画し、それを M5Stack Core2 にシリアル通信(USB による有線接続)で送る仕組みで実装しています(以下の動画のようなことを試しました)。
この後、さっそく実装した内容などを書いていきます。
試した内容
コードの実装
実装したコードと簡単な補足を書いていきます。
M5Stack側(画像受信側)
以下は、M5Stack Core2側の実装です。
#include <M5Unified.h>
#include <M5GFX.h>
#define MAX_JPEG_SIZE 40000
uint8_t jpegBuffer[MAX_JPEG_SIZE];
// 受信待ち画面表示
void drawWaitingScreen()
{
M5.Display.fillScreen(TFT_BLACK);
M5.Display.setTextColor(TFT_WHITE, TFT_BLACK);
M5.Display.setTextSize(2);
M5.Display.setCursor(10, 10);
M5.Display.print("Waiting for Image...");
}
void setup()
{
auto cfg = M5.config();
M5.begin(cfg);
M5.Display.begin();
M5.Display.setRotation(1);
// バッファ拡張
Serial.setRxBufferSize(50000);
Serial.begin(1500000);
// 待機画面
drawWaitingScreen();
}
void loop()
{
M5.update();
// Aボタン押下:画面クリアして待機表示
if (M5.BtnA.wasPressed())
{
drawWaitingScreen();
while (Serial.available())
Serial.read();
}
// データ受信部分
if (Serial.available() >= 2)
{
if (Serial.read() == 0xFF && Serial.read() == 0xAA)
{
// サイズ待ちの処理
while (Serial.available() < 4)
;
uint32_t len = 0;
Serial.readBytes((char *)&len, 4);
if (len > MAX_JPEG_SIZE)
return;
// 表示するデータ本体の受信
uint32_t received = 0;
while (received < len)
{
if (Serial.available() > 0)
{
int toRead = min((uint32_t)Serial.available(), len - received);
Serial.readBytes(jpegBuffer + received, toRead);
received += toRead;
}
}
M5.Display.drawJpg(jpegBuffer, len);
}
}
}
以下のような前提/挙動の処理にしています。
- 受信待ち状態関連
- 最初は受信待ち状態
- 画像を受信したら表示処理を行う
- Aボタン押下で画面表示をクリアして、受信待ち状態に
- 画像受信関連
- 画像はシリアル通信で送られてくる JPEG画像
- JPEG画像がヘッダ情報付きで送られてくる
- 画像は 1枚が単独でくる前提(連続的な送信は行われない)
- 画像はシリアル通信で送られてくる JPEG画像
それと iniファイルは、以下としています。
[env:m5stack-core2]
platform = espressif32
board = m5stack-core2
framework = arduino
lib_deps =
M5GFX
M5Unified
p5.js側(画像送信側)
以下は、p5.js Web Editor用の p5.js の実装で、送信側の処理です。
let port;
let writer;
// M5Stack Core2 の解像度に合わせた設定
const SEND_W = 320;
const SEND_H = 240;
const JPEG_QUALITY = 0.3;
function setup() {
createCanvas(SEND_W, SEND_H);
pixelDensity(1);
background(0);
fill(255);
textAlign(CENTER, CENTER);
textSize(16);
text("キャンバスをクリックして接続", width / 2, height / 2);
noLoop();
}
function keyPressed() {
// スペースキーか9: ノイズでカラフルな描画
if (key === " " || key === "9") {
generateNoiseArt();
sendIfConnected();
}
// bキーか0: 水色で塗りつぶし
else if (key === "b" || key === "0") {
background(135, 206, 250);
fill(255);
noStroke();
textAlign(LEFT, TOP);
textSize(14);
text("水色の描画を送信", 10, 10);
sendIfConnected();
}
}
function sendIfConnected() {
if (writer) {
sendJpegSerial();
} else {
console.log("Not connected yet.");
}
}
function generateNoiseArt() {
background(30);
noStroke();
for (let i = 0; i < 50; i++) {
fill(random(255), random(255), random(255), 200);
let size = random(20, 80);
rect(random(width), random(height), size, size);
fill(random(255), random(255), random(255), 150);
ellipse(random(width), random(height), size, size);
}
fill(255);
textAlign(LEFT, TOP);
textSize(14);
text("カラフルな描画の送信", 10, 10);
}
async function mousePressed() {
if (!port) {
try {
port = await navigator.serial.requestPort();
await port.open({ baudRate: 1500000 });
writer = port.writable.getWriter();
console.log("Connected!");
background(50);
fill(255);
textAlign(CENTER, CENTER);
textSize(16);
text("Space: Noise / 'b': Blue", width / 2, height / 2);
} catch (e) {
console.error("Connection failed", e);
}
}
}
async function sendJpegSerial() {
let dataUrl = document
.getElementById("defaultCanvas0")
.toDataURL("image/jpeg", JPEG_QUALITY);
const base64Str = dataUrl.split(",")[1];
const binaryStr = atob(base64Str);
const len = binaryStr.length;
const totalSize = 2 + 4 + len;
let buffer = new Uint8Array(totalSize);
let idx = 0;
// ヘッダ
buffer[idx++] = 0xff;
buffer[idx++] = 0xaa;
// サイズ
buffer[idx++] = len & 0xff;
buffer[idx++] = (len >> 8) & 0xff;
buffer[idx++] = (len >> 16) & 0xff;
buffer[idx++] = (len >> 24) & 0xff;
// データ
for (let i = 0; i < len; i++) {
buffer[idx++] = binaryStr.charCodeAt(i);
}
try {
await writer.write(buffer);
console.log(`Sent: ${len} bytes`);
} catch (e) {
console.error("Write error:", e);
}
}
送信側は以下のような前提/挙動の処理にしています。
- 送信する画像の描画関連
- キャンバスのサイズは M5Stack Core2 の画面サイズに合わせたもの
- キー押下でキャンバス描画と画像送信を行う
- 動作1: 画面上にカラフルな図形描画をしたもの
- 動作2: 画面を水色で塗りつぶしたもの
- 画像データ関連
- JPEG は、サイズ削減のための圧縮用パラメータを適当に設定
- ヘッダ情報を付与した JPEG画像データを送信
動作確認
上記の実装内容でシリアル通信による画像送信を試した結果、ブラウザ側に描画された内容が M5Stack側の画面上で表示されることが確認できました。
その他
【追記】 動画対応
その後、送信側と受信側の実装に手を加えて、画像を連続送信・受信する仕組みも試しました。