通信プロトコル仕様
対象 Issue: #5 ステータス: Draft v0.4 (Tailscale Funnel + Backend Router + 多 Wi-Fi + Degraded Mode 版)
スタックちゃん本体(M5Stack CoreS3 + Module-LLM)と Mac Studio 上の管理画面サーバーの間で交わされる通信仕様。 音声・軽い対話は本体内で完結し、Mac Studio とは「深い思考の委譲」と「管理画面の状態同期・通知」だけをやり取りする。
遠隔地でもフルスペック動作を目標とし、家・iPhone テザリング・カフェ Wi-Fi など どこで電源を入れても同じ動作になることを保証する。Mac Studio への到達は Tailscale Funnel による固定エンドポイント経由。深い思考の バックエンド(Claude / Codex / Local LLM)は切替可能。
0. 本体内 (CoreS3 ↔ Module-LLM)
役割分担(ハイブリッド構成)
| 担当 | CoreS3 (ESP32-S3) | Module-LLM (AX630C) |
|---|---|---|
| マイク | ❌(封止 or 補助) | ✅ DSP 入りマイクで収音 |
| ウェイクワード検出 | ❌ | ✅ 常時 KWS(NPU) |
| STT (音声→テキスト) | ❌ | ✅ Whisper を NPU で |
| 軽い LLM 推論 | ❌ | ✅ Qwen 等を NPU で |
| TTS 生成(テキスト→PCM) | ❌ | ✅ NPU で |
| スピーカー出力(PCM→音) | ✅ I2S → 顔のスピーカー | ❌(背中側のスピーカーは予備) |
| LCD / ドットキャラ描画 | ✅ | ❌ |
| サーボ・LED 制御 | ✅ | ❌ |
| センサ Unit (PIR/ToF/IR) 読取 | ✅ | ❌ |
| Unit IR 送信(家電操作) | ✅ | ❌ |
| 状態機械の指揮 | ✅(統合点) | ❌ |
| Wi-Fi / WSS クライアント | ✅ | ❌ |
| 深い思考の判定→ Mac Studio 委譲 | ✅ | ❌ |
設計意図: 「声は顔から出る」を実現するため スピーカー出力だけは CoreS3 側 に寄せる。 その他の音声処理は Module-LLM の NPU と DSP に任せる。
物理接続
| 経路 | バス | 用途 | 帯域要件 |
|---|---|---|---|
| Module-LLM → CoreS3 | UART (M5Module バス) | TTS PCM + 認識結果イベント | 921,600 bps 以上(PCM 16kHz mono を流すため) |
| CoreS3 → Module-LLM | UART (同上) | プロンプト・話者切替・割り込み | 標準で十分 |
UART ボーレート
標準の 115,200 bps では 16kHz/16bit PCM (256 kbps) を流せない。921,600 bps 以上での運用が前提。 帯域が足りない場合は Opus 圧縮(16〜32 kbps)に切替できる経路を v1 で用意する。USB-CDC 接続にすれば 1.5 Mbps 以上出るのでさらに余裕。
イベント・スキーマ(本体内)
Module-LLM → CoreS3 は JSON テキストフレーム + バイナリ PCM フレーム の2チャンネル。
| イベント | 方向 | ペイロード | 説明 |
|---|---|---|---|
wake_detected | LLM → CoreS3 | {confidence} | ウェイクワード検出 |
asr_partial | LLM → CoreS3 | {text} | 認識中の途中結果(口パク同期用) |
asr_final | LLM → CoreS3 | {text} | 認識確定 |
llm_thinking | LLM → CoreS3 | {} | 推論開始(思考表情に切替) |
llm_response | LLM → CoreS3 | {text} | LLM の応答テキスト |
tts_start | LLM → CoreS3 | {id, sample_rate, channels} | TTS PCM 送信開始の宣言 |
| (binary frame) | LLM → CoreS3 | PCM チャンク | バイナリで PCM データ |
tts_end | LLM → CoreS3 | {id} | TTS PCM 送信終了 |
prompt | CoreS3 → LLM | {text, system?, history?} | CoreS3 から推論を依頼 |
say | CoreS3 → LLM | {text, voice?} | TTS だけ生成依頼(LLM スキップ) |
cancel | CoreS3 → LLM | {id?} | 処理キャンセル |
mode | CoreS3 → LLM | `{mode: wake | always |
JSON のエンベロープは Mac Studio との通信(§3)と同じ形式を流用する。
公式の詳細仕様は M5Module-LLM ドキュメント に準拠する。本仕様書 §1 以降は 本体⇔Mac Studio のネットワーク経路 を定義する。
1. 設計方針
| 観点 | 採用 | 理由 |
|---|---|---|
| 物理層 | Wi-Fi (2.4GHz) | CoreS3 内蔵、家庭内 LAN 前提 |
| トランスポート | WebSocket over TLS | 双方向ストリーミング、CoreS3 ライブラリ豊富 |
| アプリ層 | MCP (Model Context Protocol) をスタックちゃん→Mac Studio に。状態通知は独自イベント | 管理画面の機能を Claude にも直接渡せるため、二度書きしない |
| エンコード | JSON (UTF-8) | デバッグ容易、音声バイナリだけ別経路でバイナリフレーム |
| エンドポイント | Tailscale Funnel の固定 URL | 家・外出・iPhone テザリング問わず同じ URL でアクセス可能 |
| 認証 | 共有 PSK(事前共有鍵)+ デバイス ID | Funnel は誰でも HTTPS で繋がるため、アプリ層で防御 |
| バックエンド | Backend Router(Claude / Codex / Local LLM 切替) | モデルロックインを避ける、用途別ルーティング |
| Wi-Fi 戦略 | 複数 SSID プロファイル + 優先順位フォールバック | 家・iPhone テザリング・カフェなどを順に試す |
| 動作モード | Full / Degraded / Offline の 3 段階 | Mac Studio 不到達時も Module-LLM だけで雑談・家電操作が成立 |
2. 接続シーケンス
Tailscale Funnel エンドポイント
| フィールド | 値(例) |
|---|---|
| URL | wss://stackchan-XXX.<tailnet>.ts.net/ws |
| パス | /ws |
| TLS | Tailscale が自動付与(Let's Encrypt 由来) |
| 公開範囲 | Funnel = 公開(誰でも HTTPS で到達可) |
エンドポイント URL は本体側に 環境変数 STACKCHAN_SERVER として焼き込む(NVS に保存)。 管理画面から OTA で更新可能。
Wi-Fi プロファイル(本体側)
スタックちゃんに 複数の Wi-Fi 候補 を登録しておく。電源 ON / 切断時に priority 順で接続を試す。
[
{ "ssid": "home-2.4G", "psk": "xxxx", "priority": 1 },
{ "ssid": "stackchan-tether", "psk": "yyyy", "priority": 2 }, // iPhone テザリング
{ "ssid": "office-guest", "psk": "zzzz", "priority": 3 }
]接続戦略:
- 起動時に priority 順に 1 つずつ試す
- 接続成功した時点でその SSID を採用、次の優先候補へは行かない
- 切断時はリスト先頭から再試行
- 全 SSID 失敗 → Degraded Mode(§4.3)に移行
iPhone テザリングについて
iPhone の Personal Hotspot は 普通の WPA2 Wi-Fi として 登録するだけで動く。専用統合は不要。
| 項目 | 推奨 |
|---|---|
| SSID | iPhone 設定 → 一般 → 情報 → 名前 を 半角英数 に(例: stackchan-tether) |
| 互換性モード | 「互換性を優先」を ON(2.4GHz 強制、CoreS3 の Wi-Fi が安定) |
| 自動 OFF 対策 | スタックちゃん起動後、最初の接続を成立させておく(90秒の電池節約タイマーをリセット) |
| 充電 | iPhone を充電中にすると Personal Hotspot が落ちにくい |
認証
- セットアップ時に 管理画面で1回 PSK(32バイト)を生成 → 本体に焼き込む(v0.1 はシリアル経由、将来 BLE プロビジョニング)
- 接続時のヘッダ:
X-Device-Id: ESP32 の MAC アドレスから生成した固定値X-Auth:HMAC-SHA256(PSK, device_id + ":" + timestamp)X-Timestamp: 接続試行時の Unix ミリ秒(±60s のずれは許容、リプレイ攻撃対策)
Funnel は公開なので、TLS は外側にあるが「正当な本体だけが繋げる」保証はアプリ層 PSK で担保する。
3. メッセージ・スキーマ
3.1 共通エンベロープ
{
"id": "uuid-v4", // リクエスト/レスポンス対応用
"type": "voice.chunk", // ドット区切りで階層化
"ts": 1731988800123, // 送信側のミリ秒タイムスタンプ
"payload": { ... } // type 固有のデータ
}3.2 ハンドシェイク
hello (スタックちゃん → Mac Studio)
{
"type": "hello",
"payload": {
"fw_version": "0.1.0",
"device_id": "stackchan-xxxx",
"capabilities": ["voice.in", "voice.out", "expression", "servo", "led"]
}
}welcome (Mac Studio → スタックちゃん)
{
"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)
スタックちゃん本体の状態(表情・処理中フラグ)を同期。
{
"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 → スタックちゃん)
管理画面側で発生したイベント通知(メール・タスク完了)。
{
"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"}
}
}session (Mac Studio → スタックちゃん) — #106 連続会話セッション
「ねえドナック」で開いた会話セッションの状態を本体に同期する。active=true の間は wake ワード不要で 発話が継続転送され、本体は左上に緑インジケータを表示する。終了ワード/タイムアウト/手動終了で active=false。
{
"type": "session",
"payload": {
"active": true, // true=会話継続中(wake不要) / false=終了
"conversation_id": "uuid", // active 時。会話文脈のキー (#191)
"reason": "end_phrase" // false 時: end_phrase | idle_timeout | max_duration | manual
}
}session.close (スタックちゃん → Mac Studio) — #106
本体の画面左上タップ等でユーザーが会話セッションを明示終了する。bridge が権威でセッションを閉じ、 締めの一言を発話させたうえで session{active:false, reason:"manual"} を返す。
{ "type": "session.close" }3.4 音声経路
v0.2 での変更
Module-LLM 導入により、通常の音声は本体内 (CoreS3 ↔ Module-LLM) で完結する。 下記のバイナリフレーム経路は 「Mac Studio 側にも音声を渡す必要が出た場合の予備設計」 として残してある(例: 録音アーカイブ、Claude に音声をそのまま渡すなど)。 v0.2 の通常運用では、Mac Studio に投げるのは voice.transcript 相当のテキストのみ。
音声は別チャンネル(WebSocket バイナリフレーム)で送り、メタ情報だけ JSON で送る。
バイナリフレームは「直前の voice.start / tts.start で宣言された id」に紐付ける。 並走する音声を分離するため、混在しない設計(送信側で逐次化)。
3.5 MCP ツール呼び出し
スタックちゃんが「タスクを読み上げて」と認識した場合、ローカル LLM が MCP ツール呼び出し に変換 → Mac Studio に投げる。
{
"type": "mcp.call",
"id": "req-001",
"payload": {
"tool": "task.list",
"args": {"date": "today"}
}
}レスポンス:
{
"type": "mcp.result",
"id": "req-001",
"payload": {
"ok": true,
"data": [
{"id": "t1", "title": "ナレッジページを書く", "due": "2026-05-19T18:00:00+09:00"}
]
}
}エラー時:
{
"type": "mcp.result",
"id": "req-001",
"payload": {
"ok": false,
"error": {"code": "tool_not_found", "message": "..."}
}
}3.6 深い思考の委譲(Backend Router 経由)
本体から agent.run を投げると、Mac Studio 側の Backend Router が Claude / Codex / Local LLM のいずれかにルーティングしてストリーミング応答を返す。本体側はバックエンドの違いを意識しない。
{
"type": "agent.run",
"id": "ag-01",
"payload": {
"prompt": "今週の売上トレンドをまとめて、改善案を3つ",
"backend": "claude", // optional. 省略時は Mac Studio のデフォルト or ルール適用
"stream": true,
"context": { "tags": ["work"] } // optional. タグ別ルール適用に使う
}
}ストリーミング応答:
{"type":"agent.delta", "id":"ag-01", "payload":{"text":"先週比 +12% …"}}
{"type":"agent.delta", "id":"ag-01", "payload":{"text":"の伸びが見られ …"}}
{"type":"agent.done", "id":"ag-01", "payload":{"backend":"claude","usage":{"input":1024,"output":2048,"cost_usd":0.018}}}Backend Router の選択ロジック
優先順位(先に当てはまったものを採用):
- リクエストの
payload.backendで明示指定があればそれ - タグ別ルール(管理画面で定義)— 例:
code → codex,private → local,default → claude - 管理画面のデフォルト — 1 つを選んで運用
サポートする Backend
| backend | 実装 | 特徴 |
|---|---|---|
claude | @anthropic-ai/claude-agent-sdk | MCP ツールネイティブ、サブスク/API キー |
codex | OpenAI Responses / Codex SDK | 関数呼び出しで MCP 相当を再現、Function calling |
local | Ollama HTTP API / node-llama-cpp / MLX | オフライン可、プライベート、ツール対応はモデル次第 |
各 Backend は内部で以下の共通 IF を実装する:
interface AgentBackend {
name: 'claude' | 'codex' | 'local'
run(req: AgentRequest): AsyncIterable<AgentDelta>
capabilities: { tools: boolean; vision: boolean; streaming: boolean }
}4. 接続管理 + 動作モード
4.1 ハートビート
- 間隔: 15秒
- スタックちゃんから
{"type":"ping"}を送る - Mac Studio は
{"type":"pong"}で返す - 30秒応答が無ければスタックちゃん側で切断 → 再接続
4.2 再接続戦略
| 状況 | 動作 |
|---|---|
| Wi-Fi 接続失敗 | Wi-Fi プロファイル §2 の優先順位リストを順に試す |
| 全 Wi-Fi 失敗 | Degraded Mode へ(オフラインだが Module-LLM で雑談・IR は動く) |
| Wi-Fi OK、WSS 接続失敗 | 1秒 → 2秒 → 4秒 → 8秒 ... の指数バックオフ(上限60秒) |
| 接続中の切断 | 即時 1回トライ → 失敗したら指数バックオフ |
4.3 動作モード
スタックちゃんは 3 段階の動作モード を持つ。Wi-Fi と Mac Studio の到達状況で自動遷移する。
| モード | 条件 | 出来ること |
|---|---|---|
| 🟢 Full Mode | Wi-Fi + Mac Studio (Tailscale Funnel) 到達可 | 本体内軽い応答 + 深い思考 (Backend選択可) + MCP ツール + 通知 |
| 🟡 Degraded Mode | Wi-Fi OK だが Mac Studio 不到達 / または Wi-Fi NG | Module-LLM 内蔵モデルだけ、雑談・時計・基本操作・IR家電操作(家にいれば) |
| 🔴 Offline Mode | バッテリー低下や復旧不能エラー | キャラ表示のみ、PIR で起こされたら最低限のリアクション |
Degraded Mode 中の発話例(ユーザーが Mac Studio 依存の依頼をしたとき):
「ごめん、いまネットの向こうに繋がってないから、簡単な話しかできないよ」
通知系(メール着信等)は Mac Studio が再接続を検出してからまとめて送る。
4.4 ロケーション通知(LCD ステータスアイコン)
LCD 右上に小さく現在の接続元を示す:
| アイコン | 意味 |
|---|---|
| 🏠 | 家 Wi-Fi 接続中(Full Mode) |
| 📱 | iPhone テザリング接続中(Full Mode) |
| ☁️ | 他の Wi-Fi 接続中(Full Mode) |
| 🟡 | Wi-Fi 接続中だが Mac Studio 不到達(Degraded) |
| 🔴 | Wi-Fi 接続なし(Degraded / Offline) |
判定ルール: 接続中の SSID と Wi-Fi プロファイル登録時のタグ (home / tether / other) を見て切り替える。
5. エラーコード(参考)
| code | 意味 |
|---|---|
auth_failed | PSK ミスマッチ、Timestamp ズレすぎ |
unsupported_version | バージョン不一致 |
tool_not_found | MCP ツール未登録 |
internal_error | サーバー側未捕捉例外 |
rate_limited | 過剰呼び出し(v1 以降) |
6. 未決事項
- [ ] PSK のローテーション手順
- [ ] 複数スタックちゃん時のサーバー側の名前空間
- [ ] Module-LLM の判定で「深い思考が必要」と判断する閾値設計
- [ ] Backend Router のタグ別ルールの DSL 設計
- [ ] iOS リレーアプリ(Option B) — テザリング設定不要で BLE 経由で繋がる究極形態。詳細別 issue
7. 実装メモ(Phase ごとの最小実装)
Module-LLM が音声・軽い対話を引き受けるため、Phase 2 は本体側完結が中心になり、ネットワークプロトコル経路の重みは Phase 3 以降に寄る。
| Phase | 実装範囲 |
|---|---|
| P1-a | CoreS3 ↔ Module-LLM の UART 疎通(公式サンプル) |
| P1-b | 複数 Wi-Fi プロファイル + 優先順位フォールバック |
| P1-c | Tailscale Funnel セットアップ(Mac Studio 側)+ WSS 接続 + hello/welcome + ping/pong + 認証ヘッダ |
| P2-a | Module-LLM 内で wake word → STT → 軽い LLM 応答 → TTS の本体内ループ |
| P2-b | 状態 (state) を Mac Studio に同期 |
| P2-c | Degraded Mode の挙動定義 + LCD ステータスアイコン |
| P3-a | 「深い思考」判定 → agent.run を Mac Studio へ投げる |
| P3-b | Backend Router 抽象層 + Claude アダプタ |
| P3-c | Backend: Codex アダプタ |
| P3-d | Backend: Local LLM (Ollama) アダプタ |
| P3-e | MCP ツール呼び出し (task / mail / news) |
| P4 | notify と LED/サーボ/Unit IR 連動 |
| P5 | iOS リレーアプリ(Option B) — BLE 経由でテザリング設定不要に |