
【夏休みの自由研究リレー】ESP32でClaude Codeの処理状況を可視化する
はじめに
皆様こんにちは、あかいけです。
当記事はクラスメソッドの有志による『夏休みの自由研究リレー』第28回目のエントリです。
このブログリレーの企画は、普段からクラウドやAIを追いかけているメンバーによって、「やってみた」だけではなく「作ってみた」や「調査/研究してみた」もアウトプットしてみようという企画です。
新たな知見になることは勿論、アイデアを自社やコミュニティの発展に寄与できればと考えておりますので、お付き合いいただけますと幸いです。
モチベーション
Claude Codeの処理状況を可視化するために今まで色々してきました。
具体的にはミラーボールを回しましたが、以下のようなデメリットがあり実業務での活用には至りませんでした。
- どこにでもミラーボールがあるわけではない
- TPOを考慮するとミラーボールを使えない場面がある
- 会社にミラーボールを持参する必要がある
というわけで、今回はESP32を使ってよりキュートに、よりコンパクトに、より便利に、
Claude Codeの処理状況を可視化してみました。
なおESP32の基本的なセットアップや使い方は以下ブログを書いているので、気になる方はご参照ください。
作ったもの
こんな感じでClaude Codeの処理状況と連動して動きます。
- セッション起動時モーション
- Claude Codeのセッション開始とともに起きます

- 処理中モーション
- プロンプトを投げると Thinking time が始まります

- 承認待ちモーション
- ユーザーの承認が必要な場面では助けを求めます

- セッション終了時モーション
- 一仕事終えたら寝ます、お疲れ様でした

かわいいね。
やりたいことの定義
ミラーボールでの反省を踏まえて、今回は次の3つを満たすことを目指しました。
- 可愛く可視化したい
- せっかく毎日眺めるものなので、無機質なミラーボールよりマスコットが動くほうが楽しいため
- 視認性をいい感じにしたい
- 離れた席や横目からでも今どの状態かひと目で分かるようにしたい
- 低予算でやりたい
- 全国のClaudeユーザーへの普及を視野に入れ、周辺部品を含めても数千円程度に収めたい
アーキテクチャ
全体感はこんな感じです。
Claude Codeのhookが状態をファイルに書き込み、そのファイルを常駐プロセスが監視してESP32へシリアル通信で送り、ESP32側がアニメーションとLEDに変換するという流れです。
なんのこっちゃだと思うので、これから詳細を説明します。
実装してみる
コードの全量は以下においているので、ご自由にお使いください。
本ブログでは大事なポイントを抜粋して解説します。
用意するもの
私は以下を使いました。
なおハードウェア周りは同じ製品じゃなくてもなんとかなると思うので、あくまで一例となります。
- ソフト
- Claudeのアカウント(プラン問わず)
- ハード
- ESP32(FNK0090B)
- 1,390円
- USBケーブル(ESP32についてたやつ)
- 0円
- ESP32向けのブレイクアウトボード)
- 700円
- ブレッドボード(LEDに使う)
- 200円
- ジャンパーワイヤセット(arduino用ワイヤ—ゲ—ジ28AWG)
- 120本で800円、使ったのは17本 = 約113円
- 単色LED × 3(緑 / 橙 / 赤)
- 200個で800円、使ったのは3個 = 12円
- 抵抗 × 3(LEDに合わせたいい感じの抵抗を使ってください)
- 1,280本で1000円、使ったのは3個 = 約2円
- 1.3インチ IPS液晶(ST7789VW搭載、SPI接続、240×240px)
- 1,200円
- ESP32(FNK0090B)
というわけで、合計金額は3,617円ぐらいでした。
ほとんどAmazonで買ったので、専門店とかで買ったらもう少し安くなると思います。
ハードウェア
配線について
ESP32へ直接ジャンパーワイヤを挿すのではなく、ブレイクアウトボードを使いました。
また今回使ったESP32のピン配置は以下の通りです。

ブレイクアウトボードのピンヘッダーからジャンパーワイヤを伸ばし、LEDと抵抗は別のブレッドボードへ、液晶モジュールは専用のケーブルでそれぞれ接続しました。
液晶と3個のLEDの対応は、以下のとおりです。
| 用途 | ESP32側 | 接続先 |
|---|---|---|
| 液晶 電源 | 3.3V | VIN |
| 液晶 GND | GND | GND |
| 液晶 MOSI | GPIO23 | SDA |
| 液晶 SCK | GPIO18 | SCL |
| 液晶 RES | GPIO17 | RES |
| 液晶 DC | GPIO16 | DC |
| 液晶 バックライト | GPIO4 | BLK |
| LED0 | GPIO25 | 緑(抵抗を介してGND) |
| LED1 | GPIO26 | 橙(抵抗を介してGND) |
| LED2 | GPIO27 | 赤(抵抗を介してGND) |
実際の配線を上から見るとこんな感じです。

LEDについて
LEDは抵抗を挟んでGPIOにつなぎ、色は信号機と同じ緑・黄・赤にしました。
待機中は緑、処理中は黄色、承認待ちは赤、といった感じで関連づけています。
また点滅やフェードはさせず、常時点灯と消灯だけで表現しています。
static const int LED_PINS[] = {25, 26, 27}; // 緑 / 橙 / 赤
void updateLeds(State s) {
bool green = false, yellow = false, red = false;
switch (s) {
case ST_IDLE: green = true; break;
case ST_PROCESSING: yellow = true; break;
case ST_APPROVAL: red = true; break;
}
setLed(0, green ? 255 : 0);
setLed(1, yellow ? 255 : 0);
setLed(2, red ? 255 : 0);
}
モニターについて
今回使った液晶モジュールはピンヘッダーが付属していないタイプだったため、先にはんだ付けが必要でした。
配線自体は前述の配線表のとおりです。
このクローン品はST7789の初期化時にSPI_MODE3を指定しないと映らない個体差がありました。
tft.init(TFT_WIDTH, TFT_HEIGHT, SPI_MODE3);
画面が真っ暗なまま何も表示されない場合は、SPI_MODE0に変えて試してみてください。
ソフトウェア
通信方式について
ESP32のような開発ボードは、USBシリアルポートを開く動作そのものがボードを自動リセットする仕組みになっています。
hookが発火するたびにポートを開閉すると、そのたびにESP32が再起動してアニメーションが止まってしまいます。
そこで、シリアルポートを開いたまま保持し続ける常駐プロセス(ブリッジ)をPythonで用意しました。
hook側がやることは、状態を1単語ファイルに書くだけです。
# set-state.sh(hookから呼ばれる)
echo "$1" > "$RUN/led.state"
ブリッジ側は、このファイルをポーリングして変化があればシリアルに流すだけです。
# bridge.py(常駐プロセス、抜粋)
while True:
led = read_text(LED_FILE)
if led and led != last_led:
send(f"STATE {led}")
last_led = led
time.sleep(POLL)
hookをファイル書き込みだけにしているのは、シリアル通信のような時間のかかる処理をhook自身にやらせるとClaude Codeの応答が遅くなるためです。
実際のbridge.pyには、ESP32の抜き差しでシリアルが切れても自動で再接続する処理や、セッション終了時に最後の状態(OFF)を送り切ってから終了する処理も入っていますが、ここでは説明のため単純化しています。
ESP32側は、受け取った単語をこの対応表から探して状態を切り替えるだけです。
static const CommandAlias COMMAND_ALIASES[] = {
{"START", ST_START},
{"IDLE", ST_IDLE},
{"BUSY", ST_PROCESSING},
{"WAIT", ST_APPROVAL},
{"OFF", ST_END},
};
Clawdの描画方法について
描画処理の度に液晶へ直接転送すると、転送中の途中経過がチラついて見えてしまいます。
そこでAdafruit GFXライブラリのオフスクリーンキャンバス(GFXcanvas16)に1フレーム分をすべて描いてから、完成した絵を液晶へ一括転送しています。
void drawClawd(const Pose &p) {
canvas.fillScreen(COLOR_BG);
fillO(O_TORSO_X, O_TORSO_Y, O_TORSO_W, O_TORSO_H, 0, p.body); // 胴体
// ...脚や目も同様にcanvasへ描く
tft.drawRGBBitmap(CANVAS_X, CANVAS_Y, canvas.getBuffer(), CANVAS_W, CANVAS_H); // ここで一括転送
}
姿勢は、状態に入ってからの経過時間をサイン波に入れて計算しています。
// IDLE: ゆっくり呼吸するように上下させる
p.jumpY = -(int)(2 * (1 + sinf(t * 2 * M_PI / 3.0f)));
待機中は呼吸、処理中は歩行、承認待ちは震え、周期や振幅を変えるだけでそれぞれの動きを表現しています。
Claude Codeの設定について
Claude Codeのhookは、決められたタイミングでコマンドを呼び出すだけの仕組みです。
たとえば承認待ちになったタイミングでは、こう登録しています。
"PermissionRequest": [
{
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/esp32/set-state.sh WAIT" }
]
}
]
.claude/settings.jsonに登録したhookの全体像は以下のとおりです。
| タイミング(hook) | 書き込む状態 |
|---|---|
| セッション開始(SessionStart) | 起床 |
| プロンプト送信(UserPromptSubmit) | 処理中 |
| ツールが承認を必要とした瞬間(PermissionRequest) | 承認待ち |
| ツール実行後(PostToolUse) | 処理中 |
| 応答完了(Stop) | 待機中 |
| セッション終了(SessionEnd) | 終了 |
なお、SessionStartとSessionEndだけは単純な状態書き込みに加えて、ブリッジ自体の起動と停止も兼ねています。
今後の課題
とりあえず動く状態にはなりましたが、普段使いするためには以下のような課題が残っています。
これらは今後Claudeと一緒に改善していきたいところです。
- 承認ダイアログで「拒否」を選んだ場合を検知するhookが無く、拒否した直後は承認待ちの画面が残る
- 次のプロンプトを送信すれば通常の処理中の表示に戻るのでよしとする(妥協)
- 基盤が剥き出し
- ブレッドボードからプリント基板にする
- 3Dプリンターとかでボディーを作る
- Claude Codeの1セッションしか対応できない
- デカいモニターにして画面分割を制御できるようにする
- シリアルポートからHTTP経由にする
さいごに
以上、『夏休みの自由研究リレー』の第28回のエントリでした。
ミラーボールの時と同様に勢いで始めてしまいましたが、結果的にかわゆい感じに仕上がって満足しました。
皆さんもClaude Codeの処理状況を可愛く可視化したくなったらやってみてください。
Claudeと夏の終わり 将来の夢 大きな希望 忘れない
来年の8月また出会えるのを 信じて
最高の思い出を…










