IDチームの前田です。
先日PaperMono https://docs.m5stack.com/ja/core/PaperMono というデバイスを購入し、届いたので自作アプリを開発して動かすためにまずClaude Codeでエミュレーターを作成しました。
3行まとめ
- 電子ペーパー開発ボード PaperMono のファームウェアを、ソースを1行も変えずに macOS 上でそのまま動かすエミュレーターを Claude Code で作りました
- 画面・ボタン・タッチなどはブラウザの操作パネルから扱え、電子ペーパー特有の「
display()で初めて焼き付く」作法も再現しています - ロジックとレイアウトはエミュレーターで詰め、実機への書き込みは最後の1回だけになり、開発が大幅に快適になるはずです
PaperMonoとは
PaperMono は、低消費電力ディスプレイ、近接認識、遠距離通信機能を中心に設計された電子ペーパー開発ボードです。バッテリーを内蔵し、2.4GHz Wi-FiやNFCなどを搭載しています。
公式サイトでは下記のような自作アプリケーション開発が例として挙げられています。
- 電子書籍リーダー
- 電子サイネージ
- 入退室管理端末
- 本人認証デバイス
- スマート交通端末
サイズはiPhone17やPixel11の2/3程度で画面も小さいため、電子書籍リーダーとしては不向きな印象です。
なぜエミュレーターが必要なのか
M5Stackの他のデバイスもそうですが、
- ケーブルを抜く
- 電源ボタン長押しでUSBケーブルを挿し直し、書き込みモードで動作させる
- 開発したアプリを書き込む
- USBケーブルを挿し直す
- アプリの動作を確認する
という形で動作確認をするときにデバイスの操作が必要になり面倒でした。
とくに電子ペーパーのレイアウト調整のように「1文字ずらして確認」を何十回も繰り返す作業では、この手順がそのまま開発速度の上限になってしまいます。
そこで、ファームウェアを macOS 上でそのまま動かしてしまうエミュレーターを作りました。
従来の開発
エミュレーターを利用した開発
作ったもの
M5Unified / M5GFX / Arduino core をシム(同じシグネチャの差し替え実装)で肩代わりし、ファームウェアの setup() / loop() を macOS のネイティブバイナリとして実行するエミュレーターです。
一番のポイントは ファームウェアのソースを1行も変更せずにビルドする ことです。
実機用のコードとエミュレーター用のコードを #ifdef で分岐させてしまうと、「エミュレーターでは動くのに実機では動かない」という一番避けたい状況が生まれます。差異はすべてシム側で吸収する方針にしました。
画面・ボタン・タッチ・LED・ブザー・Wi-Fi・シリアルは、ブラウザで開く操作パネルから扱います。パネルの筐体は公式の外形図(C153_PaperMono_model_size.pdf)の寸法をそのまま起こしているので、実機を持っていない状態でもボタン位置の感覚が狂いません。
左が実機を模した筐体、右が操作用のパネルです。左側面のボタンをクリックすれば BtnA / BtnB / 電源ボタンが押され、画面をクリックすればタッチが入ります。下段には refresh 回数・EPD モード・ボタンやタッチの状態が並び、右下には Serial.print の出力がそのまま流れます。
配色はライトとダークの2種類を用意し、既定は OS の設定に追従するようにしました。
使い方
あくまで自分用のツールで汎用性が全くないため、GitHubなどには公開していませんが、下記のような実装にしました。
Claude Codeに依頼すれば同等のものを作ってくれるかと思います。
make run # ビルドしてフォアグラウンド起動- 操作パネル: http://127.0.0.1:8091/
- ファームウェア側 HTTP: http://127.0.0.1:8090/
ファームウェアが WebServer に登録したエンドポイントはそのまま生きているので、curl でも叩けます。
curl -X POST <http://127.0.0.1:8090/next> # 画面を送る
curl -X POST <http://127.0.0.1:8090/refresh> # 全画面フルリフレッシュボタンやタッチも HTTP で注入できるようにしてあるので、操作を含めた疎通確認をシェルスクリプトで自動化できます。
| エンドポイント | 用途 |
|---|---|
POST /api/gpio/<pin>/<down|up> | ボタン(2 = BtnA / 3 = BtnB / 0 = 電源) |
POST /api/touch/down/<x>/<y> | タッチ(FT6336G) |
POST /api/wifi/<up|down> | Wi-Fi リンク断 / 復帰 |
GET /api/state | 状態 JSON |
自分のファームウェアは、別リポジトリのまま場所を教えるだけで取り込めます。
cmake -B build -S . -DFIRMWARE_DIR=/path/to/papermono-firmware電子ペーパーの作法も再現する
電子ペーパーは、描画命令を出しただけでは画面が変わりません。display() を呼んでリフレッシュした瞬間に初めて焼き付きます。
エミュレーターもこの2段構えをそのまま実装しました。
M5.Display.setEpdMode(epd_fast); // モードを決めてから
M5.Display.fillScreen(TFT_WHITE); // 描く(この時点では画面は変わらない)
M5.Display.drawString("hello", 16, 16);
M5.Display.display(); // ここで焼き付く「描いたら見える」実装にしてしまうと display() の呼び忘れが実機まで残ってしまうため、あえて同じ不便さを持たせています。
リフレッシュのモードは4種類あり、実機での実測時間は下記のとおりでした。
| モード | 実機実測 | 使いどころ |
|---|---|---|
epd_quality | 4.71s | 全画面の作り直し・残像リセット |
epd_text | 0.45s | 文字主体の書き換え |
epd_fast | 0.34s | 画面遷移 |
epd_fastest | 0.07s | 部分更新(タップの反応など) |
エミュレーターはこの時間を待ちませんが、[emu] display() epd_fast (実機 約0.34s) のようにログへ残すので、実機での体感を見積もりながら開発できます。
画面をクリックしたところにマークが描かれ、左側面の LED も緑に光っています。画面は 480x800 の4階調で、バッファの値も display() のタイミングで 0 / 85 / 170 / 255 に量子化しています。中間調が実機より1段多く出てしまわないようにするためです。
エミュレートしないもの
当然ですが、ソフトウェアで再現できない部分があります。下記は最後に実機で確認する必要があります。
- リフレッシュの所要時間と残像(ゴースト)、部分更新を繰り返したときの劣化
- フロントライトの実際の見え方
- Wi-Fi の無線 / NFC / LoRa / microSD / マイク
- ブザーの音そのもの(鳴らす指示が出たことだけを記録します)
- 電源制御(長押し・スリープ復帰)
- 内蔵フォントの字形(シムは ASCII の 5x7 ビットマップ1書体のみ)
この線引きを README にも書いておき、動作確認の報告時には「エミュレーターで確認済み」と「実機で未検証」を必ず分けて書くようにしています。
まとめ
ロジックとレイアウトはエミュレーターで詰めて、実機での確認は最後の1回だけ。この形にできたおかげで、これからPaperMono のアプリ開発がかなり快適になるかと思っています。
筆者は同じ仕組みを AtomS3 Lite 向けにも作っており、シムを足せば他の M5Stack デバイスにも展開できそうです。手元のデバイスで開発が面倒だと感じている方は、エミュレーターを先に作ってしまうのも一つの手だと思います。
参考リンク
- PaperMono 製品ドキュメント(M5Stack 公式)
https://docs.m5stack.com/ja/core/PaperMono - PaperMono 外形図 PDF(
C153_PaperMono_model_size.pdf)
https://m5stack-doc.oss-cn-shenzhen.aliyuncs.com/1267/C153_PaperMono_model_size.pdf - M5Unified(シムのシグネチャを合わせる際に参照)
https://github.com/m5stack/M5Unified - M5GFX(描画 API と EPD モードの実装を参照)
https://github.com/m5stack/M5GFX