(この記事は スタックチャン(Stack-chan) の Advent Calendar 2025 の記事です)
はじめに
今回の内容
今回の内容は、「スタックチャン の顔の表示を PC側で変える」ということができそうな何らかの仕組みを試そうとしてやったものです。
試したことは以下の動画のとおりで、「PC(ブラウザ)から M5Stack Core2 に、描画を変化させる情報を送信 ⇒ M5Stack Core2上の描画が受信した情報に合わせて変化」というものです(M5Stack Core2上の描画は、自前のシンプルな図形描画)。
データのやりとりは、シリアル通信(USB での有線通信)を使っています。
仕組みの部分
PC(ブラウザ)から M5Stack Core2 に送るデータについて、「M5Stack Core2側の図形描画を変化させるための情報」としています。
試す内容として、画面に描画する内容を丸ごと PC から送るという方向性も考えたのですが、それは別途試しつつ、この記事では「M5Stack Core2側で図形描画をする + その図形描画を外部から受信したデータによって変化させる」というシンプルなものにしています。
なお、「画面に描画させる内容を丸ごと送る」という方向のことは、以下の内容を試しました。
静止画の送信(記事)
●p5.js での描画を M5Stack Core2 にシリアル通信で送って表示【M5Stack】 - Qiita
https://qiita.com/youtoy/items/2f4e30fe5b19207bfe9f
静止画の連続送信:PC のカメラ映像の送信(以下で引用しているポストは、上記の静止画の送信を試した時のもの)
試した内容
この後は、今回試した内容についてざっくり書いていきます。
実装内容
【受信側】 M5Stack Core2 での処理
以下は、M5Stack Core2側の実装です。
図形描画部分はデフォルトでは円を描画し、外部から情報を受け取った時には、一定時間、形状が矩形になるというものにしています。また、外部から受け取る情報は JSON で、その中身は「isLeftEyeClosed・isRightEyeClosed・isMouthClosed」の 3つです(値はブール値)。
通信には、シリアル通信を使っています。
#include <Arduino.h>
#include <M5Unified.h>
#include <ArduinoJson.h>
M5Canvas canvas(&M5.Display);
// 表情の状態
bool isLeftEyeClosed = false;
bool isRightEyeClosed = false;
bool isMouthClosed = false;
// 時間経過で自動リセット
const unsigned long RESET_DURATION = 150; // もとに戻るまでの時間(単位ミリ秒)
unsigned long lastExpressionTime = 0; // 最後に表情が変わった時刻
bool isExpressionActive = false;
JsonDocument doc;
void drawFace()
{
canvas.fillScreen(BLACK);
if (isRightEyeClosed)
{
canvas.fillRect(60, 90, 60, 10, WHITE);
}
else
{
canvas.fillCircle(90, 95, 30, WHITE);
}
if (isLeftEyeClosed)
{
canvas.fillRect(200, 90, 60, 10, WHITE);
}
else
{
canvas.fillCircle(230, 95, 30, WHITE);
}
if (isMouthClosed)
{
canvas.fillRect(130, 180, 60, 5, WHITE);
}
else
{
canvas.fillEllipse(160, 180, 20, 30, WHITE);
}
canvas.pushSprite(0, 0);
}
void setup()
{
auto cfg = M5.config();
M5.begin(cfg);
Serial.begin(115200);
canvas.createSprite(320, 240);
drawFace();
}
void loop()
{
M5.update();
if (Serial.available() > 0)
{
String input = Serial.readStringUntil('\n');
input.trim();
if (input.length() > 0)
{
DeserializationError error = deserializeJson(doc, input);
if (!error)
{
// JSONの内容を反映
isLeftEyeClosed = doc["left"];
isRightEyeClosed = doc["right"];
isMouthClosed = doc["mouth"];
// どれか1つでも閉じている(true)なら、タイマーを作動させる
if (isLeftEyeClosed || isRightEyeClosed || isMouthClosed)
{
drawFace();
lastExpressionTime = millis();
isExpressionActive = true;
}
}
}
}
// 自動で元に戻す処理
if (isExpressionActive)
{
if (millis() - lastExpressionTime > RESET_DURATION)
{
isLeftEyeClosed = false;
isRightEyeClosed = false;
isMouthClosed = false;
drawFace();
isExpressionActive = false;
}
}
delay(10);
}
それと、iniファイルの内容は、以下のとおりです。M5GFX・M5Unified の他に、JSON を処理する部分のために「bblanchon/ArduinoJson」も使えるようにしています。
[env:m5stack-core2]
platform = espressif32
board = m5stack-core2
framework = arduino
lib_deps =
M5GFX
M5Unified
bblanchon/ArduinoJson@^7.4.2
【送信側】 p5.js での処理
以下は送信側です。ブラウザで、p5.js Web Editor のページを開いて処理を行っています。
実装した内容は、ブラウザでシリアル通信を扱う「Web Serial API」の処理や、ボタン押下で JSON を送る処理などです。
let port;
let isConnected = false;
function setup() {
createCanvas(400, 200);
}
function draw() {
background(220);
textAlign(LEFT, TOP);
textSize(16);
fill(0);
if (!isConnected) {
fill(0);
text("【未接続】", 20, 20);
text("キャンバスをクリックして\nM5Stackと接続してください", 20, 50);
} else {
fill(0, 150, 0);
text("【接続済み】", 20, 20);
fill(0);
text("キーを押して送信:", 20, 60);
text("[9] : 右目を閉じる ( { right: true } )", 40, 90);
text("[0] : 左目を閉じる ( { left: true } )", 40, 115);
text("[o] : 口を閉じる ( { mouth: true } )", 40, 140);
}
}
async function mousePressed() {
if (isConnected) return;
try {
port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });
isConnected = true;
console.log("Connected!");
} catch (err) {
console.error("接続エラー:", err);
}
}
// キー押下時でJSONを送信
async function keyPressed() {
if (!isConnected || !port || !port.writable) return;
let data = null;
if (key === "9") {
data = { right: true };
} else if (key === "0") {
data = { left: true };
} else if (key === "o") {
data = { mouth: true };
}
if (data) {
const jsonString = JSON.stringify(data) + "\n";
const encoder = new TextEncoder();
const writer = port.writable.getWriter();
try {
await writer.write(encoder.encode(jsonString));
console.log("Sent:", jsonString);
} catch (err) {
console.error("送信エラー:", err);
} finally {
writer.releaseLock();
}
}
}
上記の送信側・受信側の仕組みを組み合わせることで、以下を実現できました。
余談
スタックチャン入門
以下は余談になるのですが、自分の スタックチャン入門に関するものです(第n回秋葉原スタックチャンオンラインもくもくオフラインハンズオンinロボスタディオン での動画)。