Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
82965f623This file is pinned to the selected release. Download the original to open it in your design tool.
# USB Dock整合性レビュー
## 対象
このレビューは、Firmwareの`feat/android-usb-audio`とDockの追跡済みHEADを比較し、両リポジトリの修正責務を整理した記録である。
以下の`e12542a0`は比較を実施した時点の履歴であり、現在vendorしている正本revisionではない。
- **Firmware**:`stack-chan/stack-chan`の`feat/android-usb-audio`(同期前HEAD `403c36e7`と作業中の修正)
- **Dock(比較時点)**:`meganetaaan/stack-chan-dock`の`agent/restructure-stack-chan-dock`(HEAD `e12542a0cf351b3944736b3a6416e6f680d36f21`)
- **Dock(現在のvendor元)**:revision `ab30e10aec7ddc2531f9a860ce0accfd5cd0497a`
Dock側の未コミット変更は比較対象に含めていない。
実機USB試験も、このレビューの比較時点では実行していない。
## 合意した判定
Firmware側レビューの主要指摘は妥当であり、比較時点のDock HEAD `831cb8f`ではAndroidのEVENT能力交渉が成立していなかった。
レビュー記載から漏れていた冪等化、送信抑止、不正JSONの継続処理もDock側の修正対象とする。
Dock HEAD `e12542a`ではこれらの修正と共有fixtureの追加が取り込まれている。
| 項目 | 判定 | 担当 |
| --- | --- | --- |
| AndroidでEVENT bit 10を広告 | 修正必須 | Dock |
| application eventとraw Realtime eventの分離 | 修正必須 | Dock |
| `conversation.start/stop/result`対応 | 修正必須 | Dock |
| `requestId`による直近64件の冪等化 | 修正必須 | Dock |
| EVENT非対応Firmwareへの送信抑止 | 修正必須 | Dock |
| 不正JSONで受信collectorを終了させない | 修正必須 | Dock |
| HELLOとEVENTの統合テスト | 修正必須 | Dock |
| 共有fixture拡張 | 推奨 | Dock |
| 接続とEVENT対応状態の三値公開 | 修正必須 | Firmware |
| `sendEvent()`の黙示的な破棄を廃止 | 修正必須 | Firmware |
| Dock正本vectorの固定と同期 | 修正必須 | Firmware |
| `android-usb-audio`の改名 | 今回は見送り | Firmware |
## 確認した不整合
比較時点のDock HEAD `831cb8f`のAndroid実装は、HELLOでEVENT bit 10を広告していなかった。
一方、Realtime sessionは接続準備後に`session.created`をEVENTとして送信していた。
FirmwareはpeerがEVENTを広告していない場合、受信EVENTを`INVALID_REQUEST`として拒否する。
FirmwareからAndroidへ送るEVENTも利用不可になるため、次の経路で`session.update`まで到達しない。
```text
Android HELLO: EVENTなし
→ Firmware HELLO_ACK: EVENTあり
→ Android: session.createdを送信
→ Firmware: INVALID_REQUEST
→ Firmware: session.updateは送信しない
```
EVENTの利用可否は、双方のcapabilityをANDで評価する。
ローカルとpeerのEVENT有無を全列挙すると、利用可能になるのは双方がEVENTを広告した1通りだけである。
| Firmware EVENT | Dock EVENT | Firmwareの公開状態 |
| --- | --- | --- |
| なし | なし | `unsupported` |
| なし | あり | `unsupported` |
| あり | なし | `unsupported` |
| あり | あり | `ready` |
物理的にUSBが接続されていない場合は、capabilityの組み合わせにかかわらず`disconnected`とする。
## Dock側の修正
Dock側はapplication eventをCodex Dock実装と同じ規則で処理する。
- `schema`がないJSONはraw Realtime eventとして処理する。
- `schema == "stackchan.event.v1"`はapplication eventとして処理する。
- 共通schemaを持つ未知または不正なeventは拒否し、raw Realtime eventへフォールバックしない。
- 同一`requestId`には直近64件の保存済み`conversation.result`を返し、操作を再実行しない。
- EVENT非対応peerには`session.created`を送らず、音声機能だけを継続する。
- 壊れたJSONまたは未知eventを受信してもcollectorを継続する。
Android上の`conversation.start`は、モデルとPiperの準備が完了していればautomatic conversationを開始する。
開始済みなら成功として扱い、push-to-talk設定は変更しない。
準備不足の場合は`success=false`、`state="blocked"`と理由を返す。
`conversation.stop`は会話モードにかかわらず停止し、未起動でも成功とする。
Dock側では、HELLOの能力交渉、EVENT非対応peerへの送信抑止、raw eventとapplication eventの分岐、64件の冪等化、不正JSON後の継続処理を統合テストで固定する。
## Firmware側の修正
Firmwareは接続状態を`disconnected`、`unsupported`、`ready`の三値で公開する。
ESP-IDFの`usb_serial_jtag_is_connected()`でUSB SOFを検出するため、USB Serial/JTAGのCDCポートを開いていないホストでも物理接続を判定できる。
このAPIにはFreeRTOSの各tickへ小さな追加負荷がある。
HELLO前またはEVENT非対応peerとの接続は`unsupported`である。
双方がEVENT bit 10を広告した場合だけ`ready`へ遷移する。
USB切断時はpeer capability、EVENT codec、parser、送信queue、マイクsession、スピーカーsessionを初期化する。
Worker境界の`sendEvent()`は、request IDで応答を対応付け、`queued`、`disconnected`、`unsupported`のいずれかをPromiseで返す。
EVENTのencode失敗またはWorker内部エラーはPromiseをrejectする。
bridgeを閉じた場合は保留中の送信を`disconnected`で解決し、未解決Promiseを残さない。
`robot.conversation.remoteSession`は`transportState`と`subscribeTransport()`を公開する。
EVENT非対応時の`requestStart()`と`requestStop()`はrequest IDを返し、frameを送信せず、timerも作らずに会話状態を`blocked`へ遷移させる。
USB未接続時は同じrequest IDを最大10秒間保持し、`ready`へ遷移した時点で即時送信する。
10秒以内に利用可能にならない場合は従来どおり`blocked`へ遷移する。
raw Realtime eventは送信Promiseを直列化し、function outputと`response.create`の順序を維持する。
transportが`ready`以外へ遷移した場合はAndroidの`session.created`受信済み状態を破棄し、再接続後の新しい`session.created`を待つ。
`session.update`の`event_id`をAndroidの`session.updated`と照合し、反映確認済みのprovider世代に対するfunction callだけを実行する。
`session.update`が送信queueのoverflowなどで未配信の場合は、同じprovider・transport leaseが有効な間、同一eventを非ブロッキングで再送する。
function callの`stackchan_session_update_id`も同じIDと照合し、provider更新前に開始したAndroid応答が更新後に生成した遅延callを破棄する。
承認応答の送信失敗は明示的に処理し、未処理のPromise rejectionを発生させない。
## Dock正本vector
FirmwareはDockの追跡済み正本を`vendor/stack-chan-dock`へ内容を変えずに取り込む。
出典とSHA-256は`vendor/stack-chan-dock/VENDOR_SOURCE.json`に記録する。
- **Dock revision**:`ab30e10aec7ddc2531f9a860ce0accfd5cd0497a`
- **LICENSE SHA-256**:`1d291f59c29098471171a78eaba979c38fa58b8bf60d5b330b862f289ecfd8c2`
- **test-vectors.json SHA-256**:`1e2c31c5981e1a963a1c85b89241da1a11e8a6e1b8316ea644f237f8ca0506c0`
- **negotiation-vectors.json SHA-256**:`63a70846df8a5d8651c9c3ee01e96064aef1749563c9abbf01d2767466eb7da7`
- **application-event-vectors.json SHA-256**:`1b4ff73439d6bab2288e5d2efda15196f889f894c4c4a7e55f83de4dbb3234d3`
同期スクリプトはDockリポジトリのHEADを取得し、`LICENSE`または3つのfixtureに未コミット変更があれば停止する。
対象外の未追跡ファイルは同期を妨げない。
Dock正本のvalid frame 3件とCRC不正frame 1件は、TypeScript codecとPython diagnostics codecの双方へ直接入力する。
Firmware固有のfragmentation、coalescing、再同期、EVENT sequence mismatchは`parser-scenarios-v2.json`へ分離する。
これにより、byte単位のgolden値をFirmware側の別fixtureやunit testへ複製しない。
`negotiation-vectors.json`と`application-event-vectors.json`はDockで正本としてcommitされたため、Firmwareへ無改変でvendorする。
Firmwareの能力交渉と会話EVENTのunit testは、この共有fixtureを直接入力する。
## 今回の対象外
`android-usb-audio`の改名は、既存manifestとnpm scriptの互換性へ影響するため今回実施しない。
Dock側のAndroid実装も、このFirmwareセッションでは変更しない。
## 検証
能力交渉は2つのEVENT bitの全4組を列挙し、AND条件だけが1組を`ready`にすることを検査する。
negative controlとしてOR条件を使うと、片側だけがEVENTを広告する2組が反例になることも固定する。
Firmwareのunit testは、transport状態遷移、未接続時のrequest ID保持、非対応時の即時`blocked`、10秒timeout、送信Promiseの対応付けとclose時の解決、Dock正本vectorを検査する。
実機USB試験を実行していない場合は、ビルド結果と分けて報告する。