Claude Codeの状態を自作キーボードで物理表示させた話 (TinyGo + RP2040 + Gopher)

に公開

はじめに

Claude Code をバックグラウンドで動かしていると、応答中なのか完了したのかがよく分からなくなります。通知バナーは見落とすし、ターミナルのタブも裏に隠れがち。

そこで、手元にあった自作キーボード zero-kb02(Waveshare RP2040-Zero ベース)に TinyGo でファームを書いて、「Claude Code の状態を物理的に表示する装置」にしてみました。

完成形はこんな感じ

Claude Code の状態 LED OLED
プロンプト送信中 🔵 外周ぐるぐる 各文字の Gopher アニメ
質問 UI 表示時 🔴 赤 Gopher C "Choose"
応答完了 🟢 緑 Gopher D "Done"

小さく作って試行錯誤しながら積み上げていったので、その過程ごと記事にしてみます。

環境

  • マイコン: Waveshare RP2040-Zero
  • キーボード: tinygo-keeb/workshop で配布されている zero-kb02
    • 12 個 WS2812 LED + SSD1306 OLED + 4×3 キーマトリクス
  • 言語: TinyGo 0.40.1
  • Mac: macOS (Apple Silicon)

ハードウェアのピン構成

キーマトリクス Cols : GPIO5, 6, 7, 8 (Output)
キーマトリクス Rows : GPIO9, 10, 11 (InputPulldown)
キーボード WS2812 ×12: GPIO1
オンボード WS2812 ×1 : GPIO16
SSD1306 OLED         : I2C0 (SDA=GPIO12, SCL=GPIO13, 0x3C)

ステップ 1: まずは Claude Code フックで通知を出す

最初は「Claude が応答を返し終わった瞬間に何かさせる」ところから始めました。Claude Code には Stop というイベントがあって、応答完了時にコマンドを実行できます。

~/.claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"応答が完了しました\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

これで応答完了時に macOS の通知バナーが出ます。設定変更後は /hooks を一度開いて閉じればリロードされます。

「これは便利だ」となって、次にもっと派手な通知を作りたくなりました。

ステップ 2: TinyGo でオンボード LED を光らせる

RP2040-Zero にはオンボード WS2812 LED が GPIO16 に付いています。まずはこれを光らせます。

package main

import (
    "image/color"
    "machine"
    "time"

    "tinygo.org/x/drivers/ws2812"
)

func main() {
    pin := machine.GPIO16
    pin.Configure(machine.PinConfig{Mode: machine.PinOutput})
    ws := ws2812.New(pin)

    leds := []color.RGBA{{}}
    colors := []color.RGBA{
        {R: 30}, {G: 30}, {B: 30},
    }
    for i := 0; ; i++ {
        leds[0] = colors[i%len(colors)]
        ws.WriteColors(leds)
        time.Sleep(500 * time.Millisecond)
    }
}

ビルドして書き込み:

tinygo build -target=waveshare-rp2040-zero -o /tmp/main.uf2 main.go
# BOOTSEL ボタン押しながら USB 挿す
cp /tmp/main.uf2 /Volumes/RPI-RP2/

赤緑青で 0.5 秒ごとに点滅。あっさり光りました。

ステップ 3: USB シリアルで色を制御する

次に「Mac から動的に色を変えたい」となり、USB CDC(シリアル)経由でコマンドを受け取る形に。machine.Serial から行単位で読み取って、red / green / blue などをパースします。

var buf []byte
for {
    if machine.Serial.Buffered() > 0 {
        b, _ := machine.Serial.ReadByte()
        if b == '\n' || b == '\r' {
            handleCommand(string(buf))
            buf = buf[:0]
        } else {
            buf = append(buf, b)
        }
    }
    time.Sleep(5 * time.Millisecond)
}

Mac 側はこんな感じで送る:

stty -f /dev/cu.usbmodem2101 115200 raw -echo
printf 'red\n' >| /dev/cu.usbmodem2101

これで Mac からシリアルコマンド送信 → LED 色変更ができるように。

ステップ 4: Stop フックから LED を光らせる

ステップ 1 の通知バナーに加えて、Stop フックから LED を緑にするスクリプトを呼びます。

~/.claude/hooks/keyboard-led.sh:

#!/bin/bash
PORT=$(ls /dev/cu.usbmodem* 2>/dev/null | head -1)
[ -z "$PORT" ] && exit 0
stty -f "$PORT" 115200 raw -echo 2>/dev/null
for line in "$@"; do
    printf '%s\n' "$line" >| "$PORT" 2>/dev/null
done

settings.json に追加:

"Stop": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "$HOME/.claude/hooks/keyboard-led.sh green"
      }
    ]
  }
]

Claude が応答完了するたびにオンボード LED が緑になりました 🟢

ステップ 5: 12 個の LED で外周くるくるアニメ

オンボード 1 個だけだと寂しいので、キーボードの 12 個の LED も使います。最初は同じ GPIO16 に書き込んでみたところ、オンボードしか光りませんでした。

workshop の 02_blinky2 を見たら GPIO1 に繋がっていました(オンボードと別ピン)。ws2812.Device を 2 つ持つように修正:

pinBoard := machine.GPIO16
pinBoard.Configure(machine.PinConfig{Mode: machine.PinOutput})
wsBoard := ws2812.New(pinBoard)

pinKeys := machine.GPIO1
pinKeys.Configure(machine.PinConfig{Mode: machine.PinOutput})
wsKeys := ws2812.New(pinKeys)

「考え中」状態(プロンプト送信時)には外周をくるくる回したいので、3×4 グリッドの外周巡回順序を定義してチェイサーアニメに:

// 3×4 グリッド:
//   0  3  6  9
//   1  4  7 10
//   2  5  8 11
var perimeter = []int{0, 3, 6, 9, 10, 11, 8, 5, 2, 1}

func tickSpin() {
    // 全消灯
    for i := range keys {
        keys[i] = color.RGBA{}
    }
    head := perimeter[spinIdx%len(perimeter)]
    tail := perimeter[(spinIdx-1+len(perimeter))%len(perimeter)]
    keys[head] = color.RGBA{B: 60}
    keys[tail] = color.RGBA{B: 15} // 尻尾フェード
    wsKeys.WriteColors(keys)
    spinIdx++
}

メインループで 70ms ごとに tickSpin() を呼ぶ。シリアル読み取りと並行動作させるために、ブロッキングしないよう自前ティック方式に。

settings.json に UserPromptSubmit フック追加:

"UserPromptSubmit": [
  {
    "hooks": [
      { "type": "command", "command": "$HOME/.claude/hooks/keyboard-led.sh spin" }
    ]
  }
]

これで「プロンプト送信中=青くるくる」「応答完了=緑」が実現できました。視界の端で状態がわかるようになります。

ステップ 6: SSD1306 OLED にテキスト表示

LED だけだと色しかわからないので、OLED に状態テキストも表示します。zero-kb02 には SSD1306 128×64 OLED が I2C0 に繋がっています。

machine.I2C0.Configure(machine.I2CConfig{
    Frequency: 400000,
    SDA:       machine.GPIO12,
    SCL:       machine.GPIO13,
})
display := ssd1306.NewI2C(machine.I2C0)
display.Configure(ssd1306.Config{
    Address: 0x3C,
    Width:   128,
    Height:  64,
})

テキストは tinygo.org/x/tinyfontfreesans.Regular12pt7b で描画:

import "tinygo.org/x/tinyfont/freesans"

func showText(s string) {
    display.ClearBuffer()
    tinyfont.WriteLine(display, &freesans.Regular12pt7b, 0, 22, s, white)
    display.Display()
}

text Thinking... のようなコマンドも追加して、フックから一緒に送るようにしました:

$HOME/.claude/hooks/keyboard-led.sh spin "text Thinking..."

ステップ 7: Gopher フォントで状態アイコン

ここで「OLED に Gopher を出したい」欲が高まり、tinygo.org/x/tinyfont/gophers パッケージを利用。gophers.Regular32pt という、A〜Z の各文字に異なるポーズの Gopher イラストが割り当てられた特殊フォントです。

import "tinygo.org/x/tinyfont/gophers"

func showGopher(letter, label string) {
    display.ClearBuffer()
    if label != "" {
        tinyfont.WriteLine(display, &freesans.Regular9pt7b, 30, 14, label, white)
    }
    tinyfont.WriteLine(display, &gophers.Regular32pt, 50, 60, letter, white)
    display.Display()
}

各状態に文字を割り当てて:

  • Thinking → Gopher T
  • Choose → Gopher C
  • Waiting → Gopher W
  • Done → Gopher D

26 種類の Gopher が見られて完全に Gopher 。かわよ❤️

ステップ 8: Gopher を順番に切り替えるアニメーション

「Thinking 状態の間は Gopher が次々変わったら楽しいよね」となり、seq <word> コマンドを追加。

func tickSeq() {
    letter := strings.ToUpper(string(seqWord[seqIdx%len(seqWord)]))
    renderGopher(letter, seqWord) // 大きな Gopher + ラベル
    seqIdx++
}

メインループで 400ms ごとに tickSeq() を呼ぶ。seq THINKING を送ると T → H → I → N → K → I → N → G とポーズが切り替わるアニメーションに。

$HOME/.claude/hooks/keyboard-led.sh spin "seq THINKING"

これがめちゃくちゃ可愛い。

ステップ 9: 「選択肢が出た時」を検知する

Claude Code が AskUserQuestion で質問 UI を出した時にも反応させたい。最初は Notification イベントを使ってみたものの、AskUserQuestion では発火しないことが判明。

そこで PreToolUsematcher を使って、ツール呼び出しの直前にフックを発火させる方法に変更:

"PreToolUse": [
  {
    "matcher": "AskUserQuestion",
    "hooks": [
      {
        "type": "command",
        "command": "$HOME/.claude/hooks/keyboard-led.sh red 'gopher C Choose'"
      }
    ]
  }
]

これで質問 UI が出る瞬間に 赤 + Gopher C に切り替わるようになりました 🔴

ステップ 10: HID キーボード機能を追加

ここまで来たら「物理キーで質問 UI を操作したい」となります。TinyGo は machine/usb/hid/keyboard パッケージで USB HID キーボードを提供しています。USB CDC と HID は composite device として共存できます。

import "machine/usb/hid/keyboard"

var kb = keyboard.New()

// マトリクス index → HID キーコード
var keymap = [12]keyboard.Keycode{
    keyboard.KeyUpArrow, keyboard.KeyDownArrow, keyboard.KeyEsc, keyboard.KeyReturn,
    0, 0, 0, 0,
    0, 0, 0, 0,
}

func scanMatrix() {
    for c, colPin := range colPins {
        colPin.High()
        time.Sleep(1 * time.Millisecond) // ★ プルダウン安定待ち
        for r, rowPin := range rowPins {
            pressed := rowPin.Get()
            idx := r*4 + c
            if pressed != keyState[idx] {
                keyState[idx] = pressed
                if k := keymap[idx]; k != 0 {
                    if pressed {
                        kb.Down(k)
                    } else {
                        kb.Up(k)
                    }
                }
            }
        }
        colPin.Low()
    }
}

これで物理キーボードの ↑↓ + Enter で Claude Code の質問 UI を選択 → 決定できるようになりました。

完成

https://youtube.com/shorts/tH7Jl2R-sr0

状態 LED OLED
プロンプト送信中 🔵 spin seq THINKING (アニメ)
質問 UI 表示時 🔴 red gopher C "Choose"
通知 (権限/アイドル) 🔴 red gopher W "Waiting"
応答完了 🟢 green + macOS 通知 gopher D "Done"

Claude Code の状態がキーボード上で物理的に見えるので、別ウィンドウで作業していても視界の端で気づけます。物理キーで質問 UI の操作もできるので、ラップトップのキーボードに手を伸ばさずに済むのも快適です。

やってみての所感

Claude Code フックは想像以上に使える

Stop / UserPromptSubmit / PreToolUse / Notification などのイベントに任意のシェルコマンドを紐付けられるので、外部ハードウェアとの連携が驚くほど簡単。設定ファイル数行で物理デバイスが Claude の状態を反映するようになります。

TinyGo + RP2040 はもう普通に書ける

ws2812 / ssd1306 / tinyfont / HID キーボードまで、必要なものがほぼ揃っています。Go の文法のままマイコンのコードが書けるので便利です。

Gopher フォントは隠れた名作

tinygo.org/x/tinyfont/gophersRegular32pt は知名度が低い気がしますが、26 文字に違うポーズの Gopher が割り当てられた完全に遊びのフォントで、見ていて飽きません。OLED 付きマイコンを持っているなら使わない手はないです。

TinyGo Keeb ツアーについて

今回使った zero-kb02TinyGo Keeb ツアー で扱われている自作キーボードです。TinyGo を使ってマイコンプログラミングや自作キーボードを学べる勉強会で、初心者でも気軽に参加できます。

次回は 2026 年 5 月 20 日にシンガポールの GopherCon SG で開催されます!

🔗 GopherCon SG 2026 - TinyGo Keeb Workshop

ハードウェアを触りながら Go でファームを書ける機会は意外と少ないので、興味があればぜひチェックしてみてください。今回の記事みたいな「思いつきで機能を足していく」遊び方を体験するには最高の題材です。

ソースコード

リポジトリは
https://github.com/Senoue/claude-status-kb
にあります。

参考

Discussion