通信プロトコル仕様
対象 Issue: #5 ステータス: Draft v0.1 (実装前の設計合意用)
スタックちゃん本体(M5Stack CoreS3)と Mac Studio 上の管理画面サーバーの間で交わされる通信仕様。 音声対話・状態同期・通知・MCP ツール呼び出しすべての土台となる。
1. 設計方針
| 観点 | 採用 | 理由 |
|---|---|---|
| 物理層 | Wi-Fi (2.4GHz) | CoreS3 内蔵、家庭内 LAN 前提 |
| トランスポート | WebSocket over TLS | 双方向ストリーミング、CoreS3 ライブラリ豊富 |
| アプリ層 | MCP (Model Context Protocol) をスタックちゃん→Mac Studio に。状態通知は独自イベント | 管理画面の機能を Claude にも直接渡せるため、二度書きしない |
| エンコード | JSON (UTF-8) | デバッグ容易、音声バイナリだけ別経路でバイナリフレーム |
| 検出 | mDNS (_stackchan._tcp.local) | 固定 IP 不要、ユーザー設定なしで自動接続 |
| 認証 | 共有 PSK(事前共有鍵)+ デバイス ID | 家庭内 LAN 前提、複雑化を避ける |
将来 BLE / iPhone リレーを追加する可能性は v2 で別途検討(v0.1 は Wi-Fi 直結のみ)。
2. 接続シーケンス
スタックちゃん Mac Studio
│ │
│ ① mDNS Query: _stackchan._tcp │
│ ──────────────────────────────────▶ │
│ │
│ ② mDNS Reply: 192.168.x.x:7780 │
│ ◀────────────────────────────────── │
│ │
│ ③ WSS Upgrade: /ws │
│ + Header: X-Device-Id │
│ + Header: X-Auth (HMAC-SHA256) │
│ ──────────────────────────────────▶ │
│ │
│ ④ 101 Switching Protocols │
│ ◀────────────────────────────────── │
│ │
│ ⑤ {"type":"hello", ...} │
│ ──────────────────────────────────▶ │
│ │
│ ⑥ {"type":"welcome", "session": ...} │
│ ◀────────────────────────────────── │
│ │
│ ⑦ MCP セッション開始 + 状態同期 │
│ ◀──────────────────────────────────▶ │mDNS サービス情報
| フィールド | 値 |
|---|---|
| サービス名 | _stackchan._tcp.local |
| ポート | 7780 |
| TXT レコード | version=0.1, tls=1, mcp=1 |
認証
- セットアップ時に 管理画面で1回 PSK(32バイト)を生成 → 本体に NFC or QR で転送(v0.1 はシリアル経由で焼き込み)
- 接続時のヘッダ:
X-Device-Id: ESP32 の MAC アドレスから生成した固定値X-Auth:HMAC-SHA256(PSK, device_id + ":" + timestamp)X-Timestamp: 接続試行時の Unix ミリ秒(±60s のずれは許容、リプレイ攻撃対策)
3. メッセージ・スキーマ
3.1 共通エンベロープ
jsonc
{
"id": "uuid-v4", // リクエスト/レスポンス対応用
"type": "voice.chunk", // ドット区切りで階層化
"ts": 1731988800123, // 送信側のミリ秒タイムスタンプ
"payload": { ... } // type 固有のデータ
}3.2 ハンドシェイク
hello (スタックちゃん → Mac Studio)
jsonc
{
"type": "hello",
"payload": {
"fw_version": "0.1.0",
"device_id": "stackchan-xxxx",
"capabilities": ["voice.in", "voice.out", "expression", "servo", "led"]
}
}welcome (Mac Studio → スタックちゃん)
jsonc
{
"type": "welcome",
"payload": {
"session_id": "sess_xxxx",
"server_version": "0.1.0",
"mcp_tools": ["task.list", "mail.unread", "news.latest", ...]
}
}3.3 状態・通知
state (双方向 / pub-sub)
スタックちゃん本体の状態(表情・処理中フラグ)を同期。
jsonc
{
"type": "state",
"payload": {
"expression": "thinking", // idle | thinking | talking | happy | sad | sleep
"battery": 0.87, // 0.0 - 1.0
"wifi_rssi": -52,
"busy": true
}
}notify (Mac Studio → スタックちゃん)
管理画面側で発生したイベント通知(メール・タスク完了)。
jsonc
{
"type": "notify",
"payload": {
"kind": "mail.important", // mail.important | task.done | task.overdue | agent.session_end
"summary": "山田さんから返信",
"speak": true, // true なら TTS で読み上げ
"led": {"color": "blue", "pattern": "blink_2"}
}
}3.4 音声経路
音声は別チャンネル(WebSocket バイナリフレーム)で送り、メタ情報だけ JSON で送る。
スタックちゃん Mac Studio
│ │
│ {"type":"voice.start", id:"v1", │
│ "format":"pcm_s16le_16khz_mono"} │
│ ──────────────────────────────────────▶│
│ │
│ [binary frame: 16KB PCM chunk] │
│ ──────────────────────────────────────▶│
│ [binary frame: 16KB PCM chunk] │
│ ──────────────────────────────────────▶│
│ ... │
│ │
│ {"type":"voice.end", id:"v1"} │
│ ──────────────────────────────────────▶│
│ │
│ {"type":"voice.transcript", │
│ "id":"v1", "text":"今日の予定は?"} │
│ ◀──────────────────────────────────────│
│ │
│ {"type":"tts.start", "id":"r1", │
│ "format":"pcm_s16le_24khz_mono"} │
│ ◀──────────────────────────────────────│
│ [binary frame: TTS PCM] │
│ ◀──────────────────────────────────────│
│ {"type":"tts.end", "id":"r1"} │
│ ◀──────────────────────────────────────│バイナリフレームは「直前の voice.start / tts.start で宣言された id」に紐付ける。 並走する音声を分離するため、混在しない設計(送信側で逐次化)。
3.5 MCP ツール呼び出し
スタックちゃんが「タスクを読み上げて」と認識した場合、ローカル LLM が MCP ツール呼び出し に変換 → Mac Studio に投げる。
jsonc
{
"type": "mcp.call",
"id": "req-001",
"payload": {
"tool": "task.list",
"args": {"date": "today"}
}
}レスポンス:
jsonc
{
"type": "mcp.result",
"id": "req-001",
"payload": {
"ok": true,
"data": [
{"id": "t1", "title": "ナレッジページを書く", "due": "2026-05-19T18:00:00+09:00"}
]
}
}エラー時:
jsonc
{
"type": "mcp.result",
"id": "req-001",
"payload": {
"ok": false,
"error": {"code": "tool_not_found", "message": "..."}
}
}3.6 深い思考の委譲 (Claude Agent SDK)
jsonc
{
"type": "agent.run",
"id": "ag-01",
"payload": {
"prompt": "今週の売上トレンドをまとめて、改善案を3つ",
"stream": true
}
}ストリーミング応答:
jsonc
{"type":"agent.delta", "id":"ag-01", "payload":{"text":"先週比 +12% …"}}
{"type":"agent.delta", "id":"ag-01", "payload":{"text":"の伸びが見られ …"}}
{"type":"agent.done", "id":"ag-01", "payload":{"usage":{"input":1024,"output":2048,"cost_usd":0.018}}}4. 接続管理
ハートビート
- 間隔: 15秒
- スタックちゃんから
{"type":"ping"}を送る - Mac Studio は
{"type":"pong"}で返す - 30秒応答が無ければスタックちゃん側で切断 → 再接続
再接続戦略
| 状況 | 動作 |
|---|---|
| 初回接続失敗 | 1秒 → 2秒 → 4秒 → 8秒 ... の指数バックオフ(上限60秒) |
| 接続中の切断 | 即時 1回トライ → 失敗したら指数バックオフ |
| Wi-Fi 自体が切れた | Wi-Fi 再接続が成立するまで mDNS は走らせない |
接続状態 UI
スタックちゃんの LCD 右上に小さく:
- 🟢 接続中
- 🟡 再接続中
- 🔴 オフライン
5. エラーコード(参考)
| code | 意味 |
|---|---|
auth_failed | PSK ミスマッチ、Timestamp ズレすぎ |
unsupported_version | バージョン不一致 |
tool_not_found | MCP ツール未登録 |
internal_error | サーバー側未捕捉例外 |
rate_limited | 過剰呼び出し(v1 以降) |
6. 未決事項 (v0.2 以降)
- [ ] PSK のローテーション手順
- [ ] BLE フォールバック(外出時、Wi-Fi 圏外時の最小通信)
- [ ] 複数スタックちゃん時のサーバー側の名前空間
- [ ] 音声を Opus 圧縮にして帯域削減
- [ ] iPhone リレー対応(モバイル時の安定化案、議論中)
7. 実装メモ(Phase ごとの最小実装)
| Phase | 実装範囲 |
|---|---|
| P1-a | WSS 接続 + hello/welcome + ping/pong だけ |
| P1-b | mDNS 検出 + 認証ヘッダ |
| P2-a | voice.start/end + バイナリ PCM 送受信 |
| P2-b | tts 再生(I2S 出力)と state 同期 |
| P3 | MCP ツール呼び出し(task / mail / news) |
| P3 | agent.run と Claude Agent SDK バックエンド |
| P4 | notify と LED/サーボ連動 |