# v2/firmware-management.md — Kneron Dongle FW 偵測 + 升降版 > 所屬:TDD v2 §2.10(v2.2 新增) > 版本:v2.2(2026-05-24 初版 / 2026-05-25 三方互審後修) > 決策依據:使用者拍板方案 A + B 一次做完(progress.md 2026-05-24 M9 啟動)+ ADR-001-firmware-management > 對應 milestone:M9-1 ~ M9-13(A 階段 5 人天 + B 階段 10.5 人天 = 15.5 人天) > 關聯研究檔(保留為附錄參考、不再回讀):`research-kl520-fw-management/00..55` > 翻案紀錄:R5-Q9「韌體燒錄 flash → B 砍掉」(progress.md 重要決策紀錄 §「第二輪使用者決策 Q9」)→ 範圍切割後翻案,詳 ADR-001 --- ## 1. 目的與範圍 ### 1.1 解決什麼問題 | 真實痛點 | 出現頻率 | 影響 | |---------|---------|------| | 拿到舊 dongle 是 KDP1 legacy(pid=0x0200)、插上完全不能 inference | 中 | 使用者卡住、退裝 visionA-local | | KL520 firmware 殘留導致 Error 15 SEND_DATA_TOO_LARGE(2026-04-21 已修但 root cause 仍在 FW 層)| 高 | inference 隨機失敗 | | KL630 / KL730 dongle 偵測不到、被誤路由到 KL520 → 連不上 | 低(裝置出貨少) | 新世代 dongle 完全不可用 | | 舊 model 在新 FW 上跑不出結果(NEF 版本與 FW 不相容)| 低 | 進階使用者卡住 | ### 1.2 範圍邊界 | 在範圍 | 不在範圍 | |--------|---------| | KL520 / KL720 自動升級 KDP1 → KDP2(A 階段) | 使用者燒任意 model 到 device flash(R5-Q9 原本範圍、繼續砍)| | KL520 手動降版(KDP2 → KDP1 / 跨次版本切換、面向一般使用者)| 線上 OTA firmware 更新通道(使用者已決策不做) | | KL630 / KL730 driver 擴展(偵測 + connect + inference、B 階段 M9-7~M9-9) | KL530 / KL830 支援(warrenchen 雖列出、本期不做) | | **KL630 / KL730 升降版(B 階段 M9-10、AC-FW-3.5)** | KL530 / KL830 / 其他新 chip 升降版(本期外) | | 多版本 firmware 並存(CURRENT_VERSION metadata 結構) | DFUT.exe 救磚工具打包(Windows-only / 30MB、僅內部 SOP) | | 安裝包內嵌所有 firmware(保守 +7MB) | Kneron firmware redistribution 授權(先不管、發佈前評估)| **AC-FW-3.5 階段歸屬說明(M9-6 弱驗證後修)**: KL630/KL730 升降版(PRD AC-FW-3.5)依 M9-6 弱驗證結論(見 `research-kl520-fw-management/55-m9-6-weak-validation-result.md`)**延後至 B 階段 M9-10**、A 階段不開。理由: 1. warrenchen 完全沒實作 KL630/KL730 升降版(reference 實作為零、設計風險高) 2. `update_kdp_firmware_from_files` 對 KL630/KL730 是否走同一條 flash 寫入路徑、必須實機 confirm 3. macOS/Linux wheel 為 2.0.0、Windows 為 3.1.2(見 §1.3)、KL630/KL730 enum 在 2.0.0 是否存在無法靜態確認、A 階段做 KL630/KL730 必須先升 wheel + 跑 KL520/KL720 三平台回歸 ### 1.3 KneronPLUS wheel 三平台版本不一致(2026-05-25 M9-6 弱驗證新增) **現況**: | 平台 | wheel 版本 | 路徑 | |------|----------|------| | macOS | KneronPLUS 2.0.0 | `visiona-local/wheels/macos/KneronPLUS-2.0.0-py3-none-any.whl` | | Linux | KneronPLUS 2.0.0 | `visiona-local/wheels/linux/KneronPLUS-2.0.0-py3-none-any.whl` | | Windows | KneronPLUS 3.1.2 | `visiona-local/wheels/windows/KneronPLUS-3.1.2-py3-none-any.whl` | **影響分析**: | 階段 | 是否阻塞 | 處理方式 | |------|---------|---------| | A 階段(KL520/KL720 升級) | **不阻塞** | 既有 bridge.py 用的 API subset(`scan_devices` / `connect_devices` / `load_firmware_from_file` / `update_kdp_firmware_from_files` 等)在 2.0.0 和 3.1.2 都存在;A 階段繼續用既有 wheel、不升 | | B 階段(KL630/KL730 driver + 升降版) | **阻塞** | B 階段啟動前(M9-7 之前)必須統一三平台 wheel 到 3.1.2+、並完成 KL520/KL720 三平台 E2E 回歸(M9-13 範圍) | **M9-7 wheel 升級決策的執行步驟**(M9-6 強驗證階段): 1. backend agent 在 macOS / Linux 跑 `python3 -c "import kp; print(kp.ProductId.KP_DEVICE_KL630)"` 確認 2.0.0 是否含 enum 2. 若 2.0.0 已含 enum → B 階段可選擇不升 wheel(風險最低) 3. 若 2.0.0 缺 enum → 必須升 wheel 到 3.1.2 才能做 B 階段 **R-FW-13(新增風險)**:wheel 2.0.0 → 3.1.2 跨主版本升級可能 breaking change 三平台 KL520/KL720 既有行為。緩解:M9-7 前 30 分鐘弱驗證 + M9-13 三平台完整 KL520/KL720 + KL630/KL730 E2E 跑過。 ### 1.4 ADR-001 重點摘要 - **Status**: Accepted(使用者 2026-05-24 拍板、2026-05-25 update 補 M9-6 弱驗證新事實) - **核心翻案**: R5-Q9 砍的是「使用者按按鈕燒任意 model 到 device flash」、不是「升級到 Kneron 官方 KDP2 標準版本」、範圍切割後翻案合理 - **技術路線**: 跨平台用 KneronPLUS Python C API(`libkplus.{dll,so,dylib}`、三平台都有 wheel)、不打包 DFUT.exe - **模組分離**: 既有 `server/internal/flash/`(load model 到 RAM)不動、新建 `server/internal/firmware/`(升降版) ### 1.5 R5-Q9 行號 cross-check(PM MJ-A2 對應) PM PRD §2.1 / Architect research summary 早期版本引用 `progress.md L776`、PM 在後續讀取時 progress.md 第二輪 Q9 條目位於 L854 (2026-05-24)。 **Architect 結論**:行號是動態值(progress.md 持續更新導致行號漂移)、決策本身的「R5 第二輪 Q9 砍 flash」**內容明確**、不依賴行號定位。**本 TDD 與 ADR-001 之後一律以「progress.md 重要決策紀錄 §『第二輪使用者決策 Q9』」描述式定位、不寫具體 L 行號**、避免日後再次失準。 --- ## 2. 模組職責 ### 2.1 新模組 `server/internal/firmware/` | 檔 | 職責 | |----|------| | `firmware/service.go` | FW 升降版 service、仿 `flash/service.go` 的 goroutine + progressCh + ProgressTracker pattern | | `firmware/progress.go` | `ProgressTracker` struct、追蹤每個 task 狀態、清理超時 task | | `firmware/versions.go` | bundled firmware 版本列舉、`CURRENT_VERSION` 解析、版本比較 helper | | `firmware/guards.go` | safety guards(不能跨晶片、不能升版偽裝降版、不能 no-op)| ### 2.2 driver interface 擴展(`server/internal/driver/interface.go`) 擴展三個 method(不破壞既有 interface 使用者): ```go // 偽碼、不出 code type DeviceDriver interface { // ... 既有 methods ... UpgradeFirmware(progressCh chan<- FirmwareProgress) error DowngradeFirmware(version string, progressCh chan<- FirmwareProgress) error ListFirmwareVersions() ([]FirmwareVersion, error) } ``` 新增 status:`StatusUpgrading DeviceStatus = "upgrading"`、`StatusDowngrading DeviceStatus = "downgrading"`、跟既有 `StatusConnecting / StatusFlashing / StatusInferencing` 並列。 ### 2.3 bridge.py 新增 handler(`server/scripts/kneron_bridge.py`) | Handler | 對應 cmd | 出現於階段 | |---------|---------|----------| | `handle_firmware_upgrade` | `firmware_upgrade` | A(M9-1)| | `handle_firmware_downgrade` | `firmware_downgrade` | B2(M9-12 之前)| | `handle_firmware_list_versions` | `firmware_list_versions` | B2(M9-11)| | 既有 `handle_connect` 擴展(KL630/KL730 chip 判斷 + .tar 路徑)| — | B0/B1(M9-7/M9-8/M9-9)| ### 2.4 邊界:不重用 `flash/` 模組 `flash/service.go:StartFlash()` 語意是「load model 到 device RAM」、本期**不**改寫成 FW 升降版用。理由: 1. 命名語意分離(flash = 燒 model、firmware = 升降版 FW) 2. progress event schema 不同(flash 是「載入模型」、firmware 是 chip-reset → loader → flash write → verify) 3. 失敗復原策略不同(flash 失敗只需 re-load model;firmware 失敗可能 brick) --- ## 3. API 設計 ### 3.1 端點清單 | Endpoint | Method | Request Body | Success Response | 階段 | |----------|--------|-------------|------------------|-----| | `GET /api/devices` | GET | — | `data[].firmwareVersion / firmwareIsLegacy / firmwareCanUpgrade / bundledFirmwareVersion` | A(M9-3)| | `POST /api/devices/:id/firmware/upgrade` | POST | `{}` | `202 {success:true, data:{taskId:"..."}}` | A(M9-3)| | `GET /api/devices/:id/firmware/versions` | GET | — | `{success:true, data:{versions:[...], current:"v2.2.0"}}` | B2(M9-11)| | `POST /api/devices/:id/firmware/downgrade` | POST | `{version:"v2.1.0", confirmToken:"DOWNGRADE"}` | `202 {success:true, data:{taskId:"..."}}` | B2(M9-11/12)| | WebSocket room `firmware:` | — | — | progress events(schema 見 §4.2)| A(M9-3)| ### 3.2 認證 / Rate Limit - 與既有 device API 一致(loopback 127.0.0.1 only + CORS whitelist、見 `v2/cors-security.md`) - 同一 device 同時只允許一個 firmware task(service 層 mutex) - `confirmToken` 必須字面字串 `"DOWNGRADE"`(防 CSRF + 防前端 bug 誤觸) ### 3.3 錯誤碼 | Code | HTTP | 觸發 | |------|------|------| | `FW_DEVICE_BUSY` | 409 | device 已在 `StatusInferencing` / `StatusFlashing` / `StatusUpgrading` / `StatusDowngrading` | | `FW_VERSION_NOT_FOUND` | 404 | 降版目標版本不在 bundled list | | `FW_INVALID_DIRECTION` | 400 | 降版目標 ≥ current(要走升版 API)| | `FW_NO_CONFIRM_TOKEN` | 400 | downgrade request 缺 `confirmToken` 或值錯 | | `FW_UPGRADE_FAILED` | 500 | bridge.py 回 error 但 device 仍可用 | | `FW_UPGRADE_BRICK_RISK` | 500 | 升級期間 device disconnect 且未 verify、可能損壞 | ### 3.4 stage → 錯誤碼 → 失敗類型對應表(給 Frontend / Testing 用) Design Spec §7.1 列了 8 種使用者面失敗情境、本 TDD §3.3 列 6 個 API 層錯誤碼、bridge.py 內部還有 stage 細分。三層 granularity 不同、本表提供完整對應、供 Frontend 拿到 `FirmwareProgress.Error` + `FirmwareProgress.Reason` 後對應 UI 文案: | Design §7.1 使用者面失敗情境 | bridge.py 觸發 stage | API 錯誤碼 | `FirmwareProgress.Reason` 值 | UI i18n key(Design §9.8)| |---------------------------|---------------------|------------|-----------------------------|--------------------------| | 1. scan 找不到裝置 | `preparing`(scan 階段)| `FW_UPGRADE_FAILED` | `scan_not_found` | `settings.firmware.error.scan` | | 2. connect 失敗 | `preparing`(connect 階段)| `FW_UPGRADE_FAILED` | `connect_failed` | `settings.firmware.error.connect` | | 3. loader 寫入失敗 | `loading` | `FW_UPGRADE_FAILED` | `loader_write_failed` | `settings.firmware.error.loader` | | 4. upgrade 中段失敗 | `flashing` | `FW_UPGRADE_FAILED` | `upgrade_mid_failed` | `settings.firmware.error.upgrade` | | 5. verify 失敗(升級已寫但驗證對不上)| `verifying` | `FW_UPGRADE_BRICK_RISK` | `verify_mismatch` | `settings.firmware.error.verify` | | 6. Timeout (>60s KL520 / >180s KL720) | 任一階段 | `FW_UPGRADE_FAILED` 或 `FW_UPGRADE_BRICK_RISK`(>180s)| `timeout` | `settings.firmware.error.timeout` | | 7. Disconnect during operation | 任一階段(不限)| `FW_UPGRADE_BRICK_RISK` | `disconnect_during_op` | `settings.firmware.error.disconnect` | | 8. 部分成功(升級已寫但 verify 找不到、提示拔插)| `verifying` | `FW_UPGRADE_FAILED` | `verify_not_found` | `settings.firmware.error.partial` | **Frontend 處理邏輯**: 1. 收到 WebSocket `progress event` 含 `{percent: -1, stage: "error", error: "...", reason: "..."}` 2. 用 `reason` 欄位對應 i18n key(lookup 本表第 4 欄) 3. 找不到 `reason` → fallback 到 stage-only mapping(如 `stage=verifying` → `settings.firmware.error.verify`) 4. `reason` 是 string、Frontend 可安全字串比對、不需 enum 同步 **`FirmwareProgress.Reason` 欄位**:Backend 在失敗 progress event 中 push 此欄位(成功時為空字串)。schema 詳見 §4.2。 **Pre-upgrade 驗證的錯誤碼**(API 層、不走 progress event): | API 錯誤碼 | HTTP | 對應 UI | |----------|------|--------| | `FW_DEVICE_BUSY` | 409 | toast「裝置正在進行其他作業、請稍後再試」 | | `FW_VERSION_NOT_FOUND` | 404 | toast「找不到指定版本」+ 重新整理版本清單 | | `FW_INVALID_DIRECTION` | 400 | toast「請從正確的 API 升級(不要用降版 API 升版)」 | | `FW_NO_CONFIRM_TOKEN` | 400 | 二次確認 modal disable 按鈕(前端應該攔住、不應該打到後端) | --- ## 4. 資料模型 ### 4.1 `FirmwareVersion` struct ```go // 偽碼、給 backend 實作參考 type FirmwareVersion struct { Version string `json:"version"` // "v2.2.0" / "v2.1.0" / "kdp1" / "SDK-v2.5.7" DisplayName string `json:"displayName"` // "v2.2.0 (current)" / "v2.1.0 (older)" / "KDP1 (legacy)" IsCurrent bool `json:"isCurrent"` // 是否為當前 bundled current IsBundled bool `json:"isBundled"` // 永遠 true(不做線上更新) ReleaseDate string `json:"releaseDate,omitempty"` // ISO 8601、optional Notes string `json:"notes,omitempty"` // 「KDP1:限制 NPU 功能」等說明 } ``` ### 4.2 `FirmwareProgress` struct ```go // 偽碼 type FirmwareProgress struct { Percent int `json:"percent"` // 0-100、-1 表示 error Stage string `json:"stage"` // see §4.3、採 Design 命名:preparing/loading/flashing/verifying/done/error Direction string `json:"direction"` // "upgrade" / "downgrade" Message string `json:"message,omitempty"` Error string `json:"error,omitempty"` // 進度時序(2026-05-25 互審後新增、給 Frontend 算 ETA UI) ElapsedMs int64 `json:"elapsed_ms,omitempty"` // service goroutine 啟動到本 event 的毫秒數 EtaMs int64 `json:"eta_ms,omitempty"` // 預估剩餘毫秒(依 stage hardcode 對照表估算、非精確) // 失敗細節(2026-05-25 互審後新增、給 Frontend 對應 i18n + 「複製錯誤訊息」UI) Reason string `json:"reason,omitempty"` // 細分 reason(見 §3.4 對應表、如 scan_not_found / verify_mismatch) DeviceID string `json:"device_id,omitempty"` // 哪台 device 出事 BeforeVer string `json:"before_version,omitempty"` // 升降版前的 firmware 字串 RawError string `json:"raw_error,omitempty"` // bridge.py 拋的原始 exception text、給「複製錯誤訊息」用 ErrorCode string `json:"error_code,omitempty"` // 內部追蹤碼(如 fw_upgrade_stage_loader_E102、供 client-server 對盤) } ``` **欄位填寫規則**: - 成功路徑:`Percent / Stage / Direction / ElapsedMs / EtaMs`、`Reason` 等失敗欄位為空字串 - 失敗路徑:`Percent = -1`、`Stage = "error"`、`Error` 含使用者可讀訊息、`Reason / DeviceID / BeforeVer / RawError / ErrorCode` 全部填齊(給 Frontend 複製錯誤 + 對應 i18n + 客服診斷用) - `EtaMs` 不精確(KneronPLUS C API 無精確進度)、UI 應顯示「~X 秒」(Design §9.6 `settings.firmware.progress.estimatedRemaining` 已用 `~{seconds}s remaining` 文案 OK) ### 4.3 Stage 列舉(採 Design 命名) 依使用者 2026-05-24 拍板裁決:採 Design Spec §8 命名 `preparing/loading/flashing/verifying`(不採原 TDD `connecting/loading_loader/loading_firmware/verifying`)。理由:UI 視角的命名語意對使用者更自然、且 Frontend i18n key lookup 與 backend stage event 一致、減少 mapping 心智負擔。 | Stage | 進度 | 觸發 | 對應原 TDD 語意(保留參考)| |-------|------|------|--------------------------| | `preparing` | 5% | bridge.py scan + connect 階段(含 USB 連線、device handle 建立) | 原 `connecting`:包含 scan_devices + connect_devices_with_magic_pass | | `loading` | 20% | KL520 KDP1 → KDP2 走 SDK loader 階段(`load_firmware_from_file` for loader.bin、僅 KDP1 → KDP2 路徑) | 原 `loading_loader`:SDK loader mode 載入、僅 legacy → modern 走此階段 | | `flashing` | 50% | 寫入 KDP2 firmware(`update_kdp_firmware_from_files` 對 KL720 = 寫 flash 永久 / 對 KL520 = load to RAM)| 原 `loading_firmware`:正式寫入 firmware | | `verifying` | 90% | disconnect → sleep 3s → rescan → 驗證版本字串符合目標 | 原 `verifying`:版本字串驗證 | | `done` | 100% | 完成、`needsReset=true` 已設、API 端 task cleanup | 同 | | `error` | -1 | 失敗(含 `Reason` 細分、見 §3.4)| 同 | **Backend 端對應的命名**: - `firmware/service.go` 內 const enum 同步用 `StagePreparing / StageLoading / StageFlashing / StageVerifying / StageDone / StageError` - bridge.py handler 回傳的 `stage` 字串值用同一組(見 §6.1) - Frontend i18n key 也對齊:`settings.firmware.progress.stage.preparing` 等 ### 4.4 多版本目錄結構(選項 C — CURRENT_VERSION metadata) 依使用者決策(progress.md 2026-05-24 B 階段 4 個決策第 1 條): ``` server/scripts/firmware/ ├── KL520/ │ ├── CURRENT_VERSION ← 單行檔:"v2.2.0" │ ├── v2.2.0/ │ │ ├── fw_scpu.bin │ │ ├── fw_ncpu.bin │ │ ├── fw_loader.bin ← A 階段補進來(KDP1→KDP2 升級用) │ │ └── VERSION ← "v2.2.0" │ ├── v2.1.0/ ← 保守策略 bundle 的舊版 │ │ └── ... 同結構 ... │ └── kdp1/ ← KDP1 降版用 │ ├── fw_scpu.bin │ ├── fw_ncpu.bin │ └── VERSION ← "KDP1" ├── KL720/ │ ├── CURRENT_VERSION │ ├── v2.2.0/ + v2.1.0/(如有) ├── KL630/ │ ├── CURRENT_VERSION │ ├── SDK-v2.5.7/ │ │ ├── kp_firmware.tar │ │ ├── kp_loader.tar │ │ ├── extracted/ ← build time 解壓(若 SDK 不接受 .tar 直接餵) │ │ │ ├── fw_scpu.bin │ │ │ └── fw_ncpu.bin │ │ └── VERSION └── KL730/ └── ... 同 KL630 結構 ... ``` **.gitignore 規則**: ``` server/scripts/firmware/*/*/extracted/ ``` 解壓後 `.bin` 不進 git、靠 build script 即時產生。`.tar` 進 git。 ### 4.5 A 階段相容性 migration A 階段(M9-1)只動 `KL520/` `KL720/` 下的 `current` firmware、檔案直接放在 `firmware//`(舊扁平結構)、不啟用 `CURRENT_VERSION` 機制。 B2 階段(M9-11)才做 migration: 1. 把 A 階段檔案搬進 `firmware//v2.2.0/` 2. 加 `CURRENT_VERSION` 單行檔 3. 把 `_resolve_firmware_paths(chip)` 升級成 `_resolve_firmware_paths_versioned(chip, version=None)`、`version=None` → 讀 `CURRENT_VERSION` A 階段 caller 不變、B2 階段一次性升級、不破壞 A 階段已 commit 的 path。 --- ## 5. 流程設計 ### 5.1 A 階段:自動升級 KDP1 → KDP2 ``` [使用者] 點 Devices 卡片「升級韌體」按鈕 ↓ Frontend POST /api/devices/:id/firmware/upgrade ↓ API handler → firmware.Service.StartUpgrade(deviceID) ↓ Service spawn goroutine: ├── 立即回 202 + taskID └── driver.UpgradeFirmware(progressCh) → ├── Stage 1: progressCh ← {percent:5, stage:"preparing"} // scan + connect 階段 ├── driver disconnect 舊連線 → restart Python bridge ├── bridge.py handle_firmware_upgrade(chip, port): │ 1. scan_devices → 找 target by usb_port_id │ 2. connect_devices_with_magic_pass(magic=KDP_MAGIC_CONNECTION_PASS) │ 3. set_timeout(60000) │ 4. _resolve_firmware_paths(chip) → (scpu, ncpu, loader) │ 5. if 偵測到 KDP(KDP1 legacy): │ // progressCh ← {percent:20, stage:"loading"} // SDK loader mode 階段 │ update_kdp_firmware_from_files(loader, None, auto_reboot=True) │ sleep 2s → rescan → reconnect with magic │ // progressCh ← {percent:50, stage:"flashing"} // 正式寫入 firmware │ load_firmware_from_file(scpu, ncpu) │ else(KDP2 short-circuit): │ // progressCh ← {percent:50, stage:"flashing"} // KDP2 直接 load 到 RAM │ load_firmware_from_file(scpu, ncpu) │ 6. disconnect (auto_reboot 後 disconnect 失敗預期、容忍) │ 7. sleep 3s → rescan → 確認 firmware 字串已變 │ 8. return {status:"upgraded", before, after, duration_ms} ├── Stage 5: progressCh ← {percent:90, stage:"verifying"} ├── driver 設定 needsReset=true(下次 connect 走完整 reset、避開 KL520 Error 15) └── Stage 6: progressCh ← {percent:100, stage:"done"} ↓ WS room "firmware:" broadcast 所有 progress events ↓ Frontend modal subscribe、progress bar 更新、完成 toast + 自動 rescan ``` ### 5.2 B2 階段:手動降版(面向一般使用者) ``` [使用者] Settings → 韌體管理 → 選 dongle → 點「切換 FW 版本」 ↓ Frontend GET /api/devices/:id/firmware/versions → 顯示 dropdown ↓ [使用者] 選版本 → 點「降版」按鈕 ↓ 二次確認 Modal(design 領域、必須有「DOWNGRADE」字面輸入框) ↓ [使用者] 輸入「DOWNGRADE」→ 點確認 button ↓ Frontend POST /api/devices/:id/firmware/downgrade body: {version:"v2.1.0", confirmToken:"DOWNGRADE"} ↓ API handler 驗 confirmToken → firmware.Service.StartDowngrade(deviceID, version) ↓ Service spawn goroutine: ├── 立即回 202 + taskID └── driver.DowngradeFirmware(version, progressCh) → ├── safety guards(§6.2) ├── bridge.py handle_firmware_downgrade(chip, port, version): │ 1. _validate_downgrade_request → 拒絕 no-op / 升版偽裝 / 跨晶片 │ 2. _resolve_firmware_paths_versioned(chip, version) → 拿目標版本檔 │ 3. connect_devices_with_magic_pass + set_timeout │ 4. update_kdp_firmware_from_files(target_scpu, target_ncpu, auto_reboot=True) │ (KL520 需先 loader、跟升級流程對稱) │ 5. sleep 3s → rescan → 驗證 firmware 字串符合目標 │ 6. return {status:"downgraded", before, after, duration_ms} ├── 期間 progressCh 推送 stages(同升級) └── 完成 → driver 設 needsReset=true ↓ WS broadcast、Frontend 「進行中」UI(不可關 modal、persistent banner) ↓ 完成 → toast「已降版到 vX.X.X」+ rescan + 卡片更新 ``` ### 5.3 失敗復原(對應 Design §7.1 8 種、§3.4 完整對應表) 本表為 service / driver 層的失敗分類、與 §3.4「stage → 錯誤碼 → Reason → i18n」對應表互補(§3.4 偏 Frontend 處理視角、本表偏服務內部處理視角)。 | 失敗類型 | 觸發 stage | `Reason` 值 | 偵測 | UI 訊息(i18n key 見 §3.4)| 後續 | |---------|----------|-------------|------|----------------------------|------| | scan 找不到裝置 | `preparing` | `scan_not_found` | bridge.py scan_devices 回空、目標 port_id 不在清單 | 「找不到裝置、請拔插後重新掃描」 | 自動 rescan 提示 | | connect 失敗 | `preparing` | `connect_failed` | `connect_devices_with_magic_pass` 回非零 | 「連線失敗、請拔插後重試」 | re-plug 後重試 | | loader 寫入失敗(KDP1→KDP2 pre-flash) | `loading` | `loader_write_failed` | bridge.py `update_kdp_firmware_from_files(loader, ...)` 拋 exception | 「韌體寫入未開始失敗、可重試」 | re-plug 後重試、`needsReset=true` | | upgrade 中段失敗 | `flashing` | `upgrade_mid_failed` | bridge.py `load_firmware_from_file` 或 `update_*` 中段拋 exception | 「韌體寫入未完成、可能損壞、請拔插裝置再 scan」 | 提示聯絡技術支援 + 「複製錯誤訊息」按鈕 | | device disconnect during op | 任一階段 | `disconnect_during_op` | bridge.py `disconnect_devices` 回非零、或 verify rescan 找不到 | 「裝置已斷開、請重新插入後重試」 | 自動 rescan、`needsReset=true` | | Timeout(KL520 >60s / KL720 >180s)| 任一階段 | `timeout` | service goroutine timeout watcher 觸發 | 「升級時間過長、可能損壞、請拔插裝置再 scan」| 提示「複製錯誤訊息」+ 內部 SOP | | verify 失敗(升級已寫但版本字串對不上)| `verifying` | `verify_mismatch` | rescan 後 firmware 字串 ≠ 預期 | 「升級疑似未生效、請拔插裝置後再 scan」 | rescan + 不阻塞 device 繼續用 | | verify 找不到 device(部分成功)| `verifying` | `verify_not_found` | rescan 後 device 不在清單(多半是 USB 仍未穩定) | 「升級可能完成但裝置暫時找不到、請拔插後重新掃描」 | rescan + 不阻塞 device 繼續用 | **重要原則**:FW 升降版失敗是 device-level 失敗、**不**升級為 server Error state(不觸發 watchServer 機制、見 `v2/server-lifecycle.md`)。 --- ## 6. bridge.py 介面 ### 6.1 Handler 參數與回傳格式 stage 命名與 §4.3 對齊(`preparing/loading/flashing/verifying/done/error`)、`reason` 欄位對齊 §3.4 對應表。 | Handler | 參數 | 回傳(成功) | 回傳(失敗) | |---------|------|-----------|------------| | `firmware_upgrade` | `{port:str, chip:"KL520"\|"KL720"\|"KL630"\|"KL730"}` | `{status:"upgraded", before_firmware, after_firmware, method, duration_ms}` | `{error:str, stage:"preparing\|loading\|flashing\|verifying", reason:"scan_not_found\|connect_failed\|loader_write_failed\|upgrade_mid_failed\|disconnect_during_op\|timeout\|verify_mismatch\|verify_not_found", raw_error:str}` | | `firmware_downgrade` | `{port:str, chip, version:str}` | `{status:"downgraded", before_version, after_version, duration_ms, stage:"done"}` | `{error:str, stage:"preparing\|loading\|flashing\|verifying", reason:"validate_failed\|<同 upgrade>", raw_error:str}` | | `firmware_list_versions` | `{chip}` | `{versions:[{version, displayName, isCurrent, isBundled, directory}, ...], current:str}` | `{error:str}` | **Driver / Service 層轉換**: - bridge.py 失敗回傳的 `{error, stage, reason, raw_error}` → driver 包成 `FirmwareProgress{Percent:-1, Stage:"error", Error: error, Reason: reason, RawError: raw_error, ...}` push 到 progressCh - API handler 收到 service 層的失敗 progress event → 同時:(1) WebSocket broadcast 給訂閱者;(2) 若是 pre-upgrade 驗證失敗(如 confirmToken 錯)走 HTTP 4xx 回應 §3.3 錯誤碼 **downgrade 額外 reason**: - `validate_failed`:`_validate_downgrade_request` 拒絕(跨晶片 / 升版偽裝 / no-op)、應該在 API 層攔截(`FW_INVALID_DIRECTION` 400)、不會走到 progress event;本欄位保留為 bridge.py 防禦性編碼用 ### 6.2 chip 判斷擴展(M9-7 改) `handle_connect()` L732-741 從 fall-through 路徑(KL630/KL730 誤判 KL520)改為明確 dispatch: ```python # 偽碼 pid = target_dev.product_id device_type_lower = device_type.lower() if "kl720" in device_type_lower or pid in (0x0200, 0x0720): _device_chip = "KL720" elif "kl730" in device_type_lower or pid == 0x0730: _device_chip = "KL730" elif "kl630" in device_type_lower or pid == 0x0630: _device_chip = "KL630" elif "kl520" in device_type_lower or pid == 0x0100: _device_chip = "KL520" else: _device_chip = "KL520" # fallback、保留既有行為 ``` Go 端 `detector.go:chipFromProductID()` 同步擴 case。 ### 6.3 .tar firmware 處理(M9-8) `_resolve_firmware_paths(chip)` 擴成 dict/union return: ```python # 偽碼 def _resolve_firmware_paths(chip="KL520", version=None): """Return dict with format-specific keys、None if not found. For KL520/KL720: {"format":"bin", "scpu":..., "ncpu":..., "loader":...} For KL630/KL730: {"format":"tar", "firmware":..., "loader":...} or {"format":"bin", "scpu":..., "ncpu":...} 解壓後路徑(策略 Y) """ ``` 策略選擇(待 M9-6 SDK 驗證決定): - **策略 Z**:SDK 支援 `load_firmware_from_tar()` → 直接餵 .tar、`format="tar"` - **策略 Y**:SDK 不支援 → build time 解壓進 `extracted/`、`format="bin"`、安裝包 ship 解壓後 .bin - **不選策略 X**(runtime 解壓):每次 connect 50-200ms 浪費 + temp file 管理複雜 ### 6.4 既有 handler 改動清單 | Handler | 改動 | milestone | |---------|------|----------| | `handle_connect` | chip 判斷加 KL630/KL730 case + .tar firmware 路徑分支 | M9-7 / M9-8 / M9-9 | | `_resolve_firmware_paths` | 加 version 參數 + dict return + .tar 支援 | M9-8 / M9-11 | | 新增 `_validate_downgrade_request` | 多版本 + chip + 方向驗證 | M9-11 | | 新增 `_read_version_file` / `_format_display_name` | helper、讀 CURRENT_VERSION + 顯示文案 | M9-11 | --- ## 7. 跨平台考量 ### 7.1 USB 權限 | 平台 | 既有狀態 | FW 升降版額外需求 | |------|---------|----------------| | macOS | 既有 KneronPLUS 已 work(hardened runtime + entitlements)| 無 | | Windows | WinUSB driver 必須先綁定(既有 M1+ TODO)| 升級 handler 偵測到 driver 未綁、明確錯誤訊息引導 re-run installer | | Linux | 既有 udev rules(installer 已處理)| 無 | ### 7.2 Loader mode + re-enumerate 升級 KL520 KDP1 → KDP2 流程: 1. `update_kdp_firmware_from_files(loader, None, auto_reboot=True)` 後 device 自動 reboot 2. USB stack 觀察到 disconnect、`disconnect_devices()` 預期回非零(容忍、用 try/except 包) 3. **`time.sleep(2)`** 等 USB 穩定(不是 1s、不是 5s、實測 warrenchen 用 2s 已穩) 4. rescan → 找回 target by usb_port_id(不靠 chip / kn_number、re-enumerate 後可能變) 5. reconnect with magic 6. `load_firmware_from_file(scpu, ncpu)` **為什麼不在 handler 內 reconnect 給 Go**:避免 race。Go 端負責後續 rescan + GetDevice、bridge.py handler 回傳成功後就讓 Go 重新建立 session。 ### 7.3 KneronPLUS wheel 版本 A 階段:用既有 wheel 版本(不升級、避免 regression)。 **重要事實**:visionA-local 既有 wheel 三平台版本不一致(macOS/Linux = 2.0.0、Windows = 3.1.2、詳見 §1.3)。對 A 階段不阻塞、但 B 階段啟動前必須統一。 B 階段 M9-6 強驗證後決定: - 既有 wheel 2.0.0 已支援 KL630/KL730 enum + .tar API → 不升 - 不支援 → 升 wheel(warrenchen 用 3.1.2)+ 三平台 KL520/KL720 回歸驗證(併入 M9-13) 升 wheel 的 regression 風險見 R-FW-2(採 PM 編號)。 ### 7.4 macOS notarization 新增 firmware bundle 檔(.bin / .tar)不是 executable、預估不需 codesign。 但 build time 解壓出的 `.bin` 進 dmg 時、走既有 `wails-macos` target 的 `codesign --force --deep --sign -` 一併覆蓋、不需額外設定。M9-13 三平台驗收時跑 `spctl --assess` 確認 Gatekeeper 不擋。 --- ## 8. 與既有架構的銜接 ### 8.1 watchServer Error state(`v2/server-lifecycle.md`) FW 升降版失敗是 device-level、**不**升級為 server Error state。具體: | 失敗 | 處理 | |------|------| | bridge.py 回 `{error:...}` | driver 設 `StatusError`、推 WS 失敗訊息、server 繼續運行 | | `firmware_upgrade` 超過 180s | timeout、同上、不殺 server | | 升級期間 device disconnect | 同上、自動 rescan | watchServer goroutine 不會把 FW timeout 誤判為 server 死掉(FW 流程在 service goroutine 內、不阻塞 HTTP server)。 ### 8.2 KL520 reset bug(2026-04-21) 2026-04-21 fix 已恢復「KL520 connect 走完整 reset + restartBridge」。FW 升級流程必須維持這個假設: - 升級成功後 driver 設 `needsReset=true` - 下次使用者點 connect 走完整 reset flow - 避開 Error 15 SEND_DATA_TOO_LARGE bridge.py handler 內**不**自己 reconnect、不**繞過**既有 reset 邏輯。 ### 8.3 R5-E 60s 啟動上限 FW 升降版是**使用者主動觸發**、不在 server 啟動 pipeline、跟 R5-E 60s hard timeout 完全無關。 但升級本身需 30-180s、UI 必須給 progress bar、不能 block 任何 HTTP 請求。API 設計已採 202 + WebSocket pattern。 ### 8.4 R5-B4 授權 R5-B4 已有「Kneron 預置模型 re-distribution 授權」未解決問題。Firmware 同性質: - 我們已 bundle KDP2 firmware 4 個月(Q9 砍的是「使用者主動燒」、不是「打包 firmware」) - B 階段多版本 + KDP1 / 舊 SDK 版本 bundle 範圍擴大 - **發佈前必須與 Kneron 取得書面 redistribution 授權**(含 current + 舊版 + KDP1) - 使用者決策(2026-05-24):先不管、發佈前評估、不阻塞開發 ### 8.5 既有 `Flash()` method 與 `restartBridge()` | 既有機制 | 本期不動 | |---------|---------| | `flash/service.go:StartFlash()` | 不擴 firmware 用、保持 load model 語意 | | `restartBridge()` | 不擴 firmware 用、firmware handler 自己控制 bridge 生命週期 | | `needsReset` flag | 本期**重用**:FW 升降版完成後設 `true`、下次 connect 走完整 reset | | WS room 命名規範 | 新 `firmware:`、跟既有 `flash:` / `inference:` 並列 | ### 8.6 降版/升級進行中的 graceful shutdown 拒絕(Design A-MID-1 / §14.4 第 6 點) **問題背景**(Design 自提):依 R5-2 規則「關閉 Wails 控制台視窗 = 結束 server」。若升降版進行中(特別是 KL720 KDP1→KDP2 寫 flash 階段、或 B2 階段使用者降版到 KDP1)使用者關閉控制台 → server 收 SIGTERM → SIGTERM 中斷 Python sidecar → bridge.py handler 被中斷 → device flash 寫到一半就停 → **brick 風險**。Design Spec 的「不可關 modal」「persistent banner」是瀏覽器 tab 內的防護、**擋不住關 Wails 視窗**。 **設計方案**:在 server / Wails 兩層加 lock + 強制 force-quit modal。 #### 8.6.1 Server 端拒絕邏輯 當 server 收到 SIGTERM(Wails close handler、systemd、`kill -TERM` 等): 1. server 進 `shutting_down` state、停止 accept 新 HTTP 連線 2. **檢查 `firmware.Service.HasActiveTask()` 是否回 true**(任一 device 在 `StatusUpgrading` / `StatusDowngrading`) 3. 若有 active task: - server **延遲 graceful shutdown**、不殺 Python sidecar - 透過 WebSocket `firmware:shutdown-rejected` event broadcast 給所有訂閱者 - 持續等待 FW task 完成(success / error 任一終態)或最多 180s timeout(KL720 升級的硬上界) - FW task 完成後 → 走原本 7+1s graceful shutdown 流程 - 180s timeout 後 → 仍未完成(罕見、可能 device 已 brick)→ 強制走 shutdown、log 警告 4. 若無 active task:正常 graceful shutdown(既有 7+1s pattern) #### 8.6.2 Wails 控制台端攔截 Wails app 的 `OnBeforeClose` handler 改造: 1. 偵測 server 是否有 active firmware task(query 內部 API `/api/firmware/active-tasks` 或讀 `firmware.Service.HasActiveTask()` Wails binding) 2. 若有 → return false 阻擋關閉、推送 Wails event `app:firmware-in-progress` 給控制台 UI 3. 控制台 UI 顯示 modal:「韌體切換進行中(device {name})、為避免裝置損毀、無法關閉應用程式。請等待約 {ETA 秒}。」 4. modal 不可關閉、不可 dismiss、只能等 firmware task 完成 5. firmware task 完成後 → Wails 自動推送 `app:firmware-completed`、modal 消失、使用者可正常關閉 6. **強制 force-quit 路徑**:若使用者堅持要關(按 `Cmd+Q` / `Alt+F4` 多次、或工作管理員強殺)→ Wails handler 阻擋不了 `kill -9`、無法防範、屬接受的取捨(Design Spec 已聲明 R-FW-11 brick 風險未完全消除) #### 8.6.3 實作 hook 點 | 檔 | 改動 | |----|------| | `server/internal/firmware/service.go` | 新增 `HasActiveTask() bool` method、查 progress tracker 是否有 task 在 active 狀態 | | `server/internal/firmware/service.go` | shutdown signal handler 註冊:收 SIGTERM → 等 FW task → 才放行 | | `server/cmd/server/main.go` | signal handler 整合 firmware.Service.HasActiveTask 檢查 | | Wails `app.go`(或 OnBeforeClose handler) | close 前 query `/api/firmware/active-tasks`、有 task → 顯示 force-quit modal | | `frontend/control-panel/*` | 新增 force-quit modal UI(i18n key 由 Design 補在 `control-panel.md`)| | bridge.py firmware handler | 進入 critical section 前 register SIGTERM handler(拒絕 SIGTERM、log 警告)、出 critical section 後 unregister | #### 8.6.4 風險與接受的取捨 | 風險 | 緩解 | |------|------| | 使用者強制 `kill -9` / 工作管理員強殺 → 仍可能 brick | 屬接受的取捨、Design Spec R-FW-11 已聲明 | | 180s timeout 內 FW task 真的卡死 → server 永遠不關 | 180s hard timeout 後強制走 shutdown | | modal 阻擋使用者關 app 體驗困擾 | 接受、brick 風險 > 體驗困擾、且 modal 顯示 ETA 給使用者預期 | | WebSocket broadcast 對非 firmware modal 的 tab 噪音 | event 命名 `firmware:shutdown-rejected` 限定 firmware: room、其他 room 不收 | #### 8.6.5 工時影響 併入 M9-11(B2.1 多版本 firmware 並存)+0.5 人天、不另列 milestone。Design 端對應補 force-quit modal UI 在 `control-panel.md`(Design 後續修)、工時包進 M9-12 Frontend 範圍。 --- ## 9. 工時拆解(M9-1 ~ M9-13,採 PM 拆法) 依使用者 2026-05-24 拍板裁決:採 PM PRD §10 拆法作為三方共用的工時表(Architect 自審亦建議採此拆法、見 architect-review §1.4-1.5)。本表為 source of truth、PRD §10 同步、Design 範圍對齊。 ### 9.1 A 階段(M9-1 ~ M9-5) | # | Milestone | 負責 | 工時 | 依賴 | 平行性 | |---|-----------|------|------|------|-------| | M9-0 | 文件先行(PRD / TDD / Design Spec + ADR-001 三方互審)| Orchestrator + PM + Design + Architect | 0.5 人天 | — | 啟動 | | M9-1 | bridge.py `handle_firmware_upgrade` + 補 `fw_loader.bin` | Backend | 1.0 人天 | M9-0 | — | | M9-2 | Go driver `UpgradeFirmware()` + `firmware/service.go` + new status | Backend | 1.0 人天 | M9-1 | — | | M9-3 | API handler + WebSocket progress room + DeviceInfo 衍生欄位 | Backend | 0.5 人天 | M9-2 | — | | M9-4 | Frontend Devices 頁 FW badge + 升級按鈕 + progress modal + i18n | Frontend | 1.5 人天 | M9-3 | 與 M9-1/M9-2/M9-3 部分平行(mock API)| | M9-5 | 三平台實機驗證(macOS / Windows / Linux)| Testing | 1.0 人天 | 全部前置完成 | — | | **A 合計** | — | — | **5 人天** | — | — | ### 9.2 B 階段(M9-6 ~ M9-13) | # | Milestone | 負責 | 工時 | 依賴 | 平行性 | |---|-----------|------|------|------|-------| | M9-6 | KneronPLUS SDK + KL630/KL730 API + .tar firmware Python API 驗證(含 30 分鐘弱驗證 + 強驗證)| Architect | 1.0 人天 | — | **與 A 階段平行**(2026-05-24 使用者決策)| | M9-7 | Driver 擴展處理 product_id 0x0630/0x0730 + chip-aware connect 分流 | Backend | 1.5 人天 | M9-6 | — | | M9-8 | bridge.py 處理 .tar firmware(解壓策略 Y 落地、M9-6 已確認策略 Z 不可行)| Backend | 1.0 人天 | M9-7 | — | | M9-9 | 多版本目錄結構重整(A 階段檔案搬到 `/v2.2.0/` + 加 `CURRENT_VERSION`)+ bridge.py 升級 `_resolve_firmware_paths_versioned()` | Backend | 1.0 人天 | A 階段完成 | — | | M9-10 | KL630/KL730 升級 / 降版 driver method 實作(**AC-FW-3.5 落地點**、含 §8.6 graceful shutdown 拒絕的 backend 部分 +0.5 並入)| Backend | 1.5 人天 | M9-8 + M9-9 | — | | M9-11 | 多版本降版後端(API + bridge.py + driver guards)| Backend | 1.5 人天 | M9-9 | — | | M9-12 | 降版 UI(Settings 韌體管理面板 + 二次確認 modal + 進行中 UI + force-quit modal in control panel)| Design + Frontend | 2.0 人天(Frontend 1 + Design 1)| M9-11 | Design + Frontend 平行 | | M9-13 | B 階段三平台實機驗證 + Beta usability test(UR-1/UR-2/UR-3)+ wheel 升級回歸測試 | Testing | 1.0 人天 | M9-7 ~ M9-12 全部 | — | | **B 合計** | — | — | **10.5 人天** | — | — | ### 9.3 合計 - A + B = **15.5 人天** - 與 PM PRD §10.3 完全對齊 - AC-FW-3.5(KL630/KL730 升降版)落點:M9-10(B 階段)、A 階段不開 ### 9.4 平行性 - M9-6 與 M9-1 ~ M9-5 平行(純研究 + 弱驗證、不阻塞 A 階段) - M9-9 與 M9-7 / M9-8 序列(多版本目錄是 B 階段 schema 基礎) - M9-11 / M9-12 部分平行(Backend M9-11 完成 API mock + Frontend M9-12 可開工) - 其他序列依賴 ### 9.5 Reviewer 切點 - M9-1 ~ M9-5 每個 milestone 結束過 Reviewer - M9-6 純研究 + 強驗證、Architect 自身產出、不過 Reviewer(研究結論需 Orchestrator 確認;強驗證結果若觸發 AC-FW-3.5 降級條件、回 Orchestrator 重派 PM 微調 PRD 後再啟動 M9-7) - M9-7 ~ M9-12 每個 milestone 結束過 Reviewer - M9-13 testing report 過 Reviewer --- ## 10. 風險清單(採 PM PRD §8 編號) 依使用者 2026-05-24 拍板裁決:採 PM PRD §8 的 R-FW-1 ~ R-FW-12 編號(PRD 已是對外的風險清單、跨多個下游 agent 引用)、本 TDD 自帶 4 條技術細節風險用 R-TAR-1 ~ R-TAR-4(TDD-only、PRD 不列)。R-FW-13 為 2026-05-25 M9-6 弱驗證新增。 ### 10.1 A 階段風險(R-FW-1 ~ R-FW-7、採 PM 編號) | # | 風險 | 等級 | 緩解 | |---|------|------|------| | R-FW-1 | 升級後 device re-enumerate 不穩定(reconnect 拿到舊 handle)+ KL520 reset bug 再現 | 中 | bridge.py `time.sleep(3)` 等 USB 穩定 + bridge handler 內不 reconnect、由 Go 端 rescan + `needsReset=true` 機制(§8.2)| | R-FW-2 | KneronPLUS Python wheel `update_kdp_firmware_from_files` API 支援度 + wheel 三平台版本不一致(macOS/Linux 2.0.0、Windows 3.1.2)| 中 | M9-6 弱驗證已確認 3.1.2 支援、2.0.0 待 30 分鐘強驗證;A 階段不升 wheel、B 階段啟動前統一(詳 §1.3) | | R-FW-3 | bridge.py `update_kdp_firmware_from_files` 在 macOS x86_64 / Linux x86_64 未驗證(warrenchen 雲端版只跑 Windows)| 中 | M9-5 三平台實機驗證 + M9-13 完整回歸 | | R-FW-4 | timeout 設定不合理(升級實際時長分布未知、可能 ≥ 60s)| 低 | KL520 設 60s upper bound / KL720 設 200s(PRD AC-FW-1.7 + §7.2);timeout 後 brick 風險視 stage(§5.3)| | R-FW-5 | Kneron 預置 firmware redistribution 法律 / 簽章授權(含 KDP1 / 舊 SDK 版本)| **P0 release gate** | 與 R5-B4 同性質、發佈前統一處理(不阻塞開發、見 §8.4)| | R-FW-6 | 既有 `flash/` 模組命名混淆(flash = load model vs firmware = 升降版)| 低 | 模組分離:`server/internal/firmware/` 新建(§2.1)、`flash/` 不擴;文件 + code 同時用 `firmware` 詞 | | R-FW-7 | 升級失敗 device unknown state(沒 verify 也沒 brick、ambiguous)| 中 | §3.4 `reason: verify_mismatch` / `verify_not_found` 區分;rescan + 不阻塞 device 繼續用;UI 提示「複製錯誤訊息」+ 內部 SOP | ### 10.2 B 階段風險(R-FW-8 ~ R-FW-12、採 PM 編號) | # | 風險 | 等級 | 緩解 | |---|------|------|------| | R-FW-8 | KneronPLUS SDK 對 KL630/KL730 API 不可預測(含 `update_kdp_firmware_from_files` 是否走同條 flash 寫入路徑) | **高** | M9-6 強驗證 + M9-9 實機驗 + M9-10 啟動前 0.5 天 strong validation;warrenchen reference 實作為零、設計工時併入 M9-10 已加 buffer | | R-FW-9 | .tar firmware 解包對安裝包大小衝擊(+7MB 保守估)| 低 | build script 解壓後刪 .tar(只 ship 解壓後 .bin、淨增可控)| | R-FW-10 | KL630/KL730 沒有 Loader mode 概念(單階段 flash 不需 SDK loader)| 中 | M9-6 驗證 firmware 字串可能值 + chip-specific 分支邏輯(§6.2);推測「flash-based 不載」但需實機 confirm(B-2 強驗證項)| | R-FW-11 | 一般使用者誤觸降版 brick 風險 | **高** | UI 多層 safety net(4 條警告語 + 二次確認字串「DOWNGRADE」+ 不可關 modal + persistent banner + force-quit modal §8.6)+ driver 層 safety guards(不能跨晶片 / 不能 no-op / 不能升版偽裝)+ Frontend 嚴格 `===` 比對(Design §6.1) | | R-FW-12 | 多版本管理 UX 複雜度(dropdown 容易誤選舊版、使用者看不懂 v2.2.0 vs v2.1.0 vs KDP1)| 中 | Design §3.3 accordion + radio list + 版本說明文字 + KDP1 額外紅色警告 + §9 i18n 已覆蓋;落地由 Design v2.2 §3.3 + §9 完成 | ### 10.3 R-FW-13(2026-05-25 M9-6 弱驗證新增) | # | 風險 | 等級 | 緩解 | |---|------|------|------| | R-FW-13 | wheel 2.0.0 → 3.1.2 跨主版本升級可能 breaking change 三平台 KL520/KL720 既有行為 | 中 | M9-7 前 30 分鐘弱驗證 + M9-13 三平台完整 KL520/KL720 + KL630/KL730 E2E 跑過;既有 visionA-local 用的 API subset 在 warrenchen 3.1.2 程式碼也有使用、相容性看起來高、但仍需實測 | ### 10.4 TDD-only 技術細節風險(R-TAR-1 ~ R-TAR-4、不進 PRD) 依 architect-review §1.6 結論:這 4 條偏技術細節、PRD 是 PM 視角不列、保留在 TDD §10 即可。 | # | 風險 | 等級 | 緩解 | |---|------|------|------| | R-TAR-1 | SDK 不接受 .tar 直接餵(策略 Z 退回 Y)| 已確認 | M9-6 弱驗證確認策略 Z 不可行(FirmwareLoadRequest schema 強證據)、走策略 Y 唯一方案 | | R-TAR-2 | build time 解壓步驟漏跑(CI 沒加 step)| 中 | installer build script mandatory step + build-time check「extracted/fw_scpu.bin 不存在 → build fail」+ 安裝包 smoke test | | R-TAR-3 | macOS notarization 對解壓 .bin 影響 | 低 | M9-13 跑 notarized dmg 確認沒被砍 + 既有 codesign 路徑覆蓋 | | R-TAR-4 | .tar 解壓跨平台問題(Python 3.12+ 拒絕 `..` path)| 低 | 用 `tarfile.data_filter` 過濾 + 預先驗證 .tar 內容(C-1 強驗證 5 分鐘可解)| --- ## 11. 測試策略 ### 11.1 單元測試 | 模組 | 測什麼 | Mock 對象 | |------|-------|----------| | `firmware/service.go` | StartUpgrade / StartDowngrade 的 goroutine 行為、cleanup task 機制、同 device mutex | mock `DeviceDriver` | | `firmware/guards.go` | 拒絕跨晶片 / 拒絕 no-op / 拒絕升版偽裝降版 | — | | `firmware/versions.go` | CURRENT_VERSION 解析、版本順序比較(包含 KDP1 特殊版本)、display name 格式 | — | | `kl720_driver.go:UpgradeFirmware` | progressCh 推送順序、needsReset flag 設定、status 轉換 | mock bridge subprocess | | `kneron_bridge.py:_resolve_firmware_paths` | KL520/KL720/KL630/KL730 各 chip 路徑解析、缺檔 fallback、.tar vs .bin format dispatch | tmp_path fixtures | | `kneron_bridge.py:_validate_downgrade_request` | 各種無效輸入(不存在版本 / 跨晶片 / 升版偽裝)| — | ### 11.2 整合測試(需實機) | 場景 | 平台 | 設備 | |------|------|------| | KL520 KDP1 → KDP2 完整升級 | macOS / Windows / Linux | 1× KL520 KDP1 legacy dongle(如有)| | KL520 KDP2 short-circuit(detect 後直接 load_firmware to RAM)| macOS / Windows / Linux | 1× KL520 KDP2 dongle | | KL720 升級(如果有 legacy KL720)| macOS / Windows / Linux | 1× KL720 dongle | | KL630 scan + connect + inference | macOS / Windows / Linux | 1× KL630 dongle(如有)| | KL730 scan + connect + inference | macOS / Windows / Linux | 1× KL730 dongle(如有)| | 降版 KL520 v2.2.0 → v2.1.0 / kdp1 | macOS / Windows / Linux | 同 KL520 dongle | | 升降版來回切換(v2.1.0 → v2.2.0 → kdp1 → v2.2.0)| macOS | KL520 | ### 11.3 異常路徑測試 | 場景 | 驗收 | |------|------| | 升級中拔除 device | UI 顯示「裝置已斷開」、自動 rescan、無 server crash、`Reason="disconnect_during_op"` 正確 push | | **升級中關 Wails 控制台視窗(§8.6 graceful shutdown 拒絕)**| force-quit modal 出現 + server 延遲 shutdown 直到 FW task 完成、升級不中斷不 brick | | 升級 timeout(mock 180s 不回)| UI 顯示 timeout 訊息(`Reason="timeout"`)、device 仍可 rescan、無 server crash | | **KL720 升級實測時長 ≤ 200s(PRD §7.2 護欄上界、AC-FW-1.7 預估 180s)**| 升級在 200s 內完成(含 stage 切換時間),超過 200s 觸發 timeout 並標 `FW_UPGRADE_BRICK_RISK` | | 跨晶片誤匹配(測試手動 hack request body 改 chip)| API 回 400 + driver 層雙重 guard 拒絕 + `_validate_downgrade_request` 拒絕 | | 一般使用者誤觸降版(沒輸入 DOWNGRADE)| 二次確認 modal 阻擋、按鈕 disabled、API 收到無 `confirmToken` request 回 400 | | wheel 升級回歸(M9-13)| 三平台 KL520+KL720 既有 E2E 全部 pass + KL630/KL730 connect/inference pass | ### 11.4 回歸測試 - 既有 KL520 / KL720 connect + inference 完整 E2E(M9-5 A 階段 + M9-13 B 階段) - M7-B Wails 控制台 UI 不受影響(FW 升級 modal 在瀏覽器 tab、不在 Wails 視窗) - watchServer 機制不會把 FW timeout 誤判為 server 死亡(升級期間 server 持續 alive、HTTP 200 OK 回 health check) - §8.6 graceful shutdown 拒絕:升級期間關閉 Wails 視窗 → force-quit modal 阻擋、確認 server 不會在 firmware task active 期間退出 --- ## 12. 與其他子檔關係 | 關聯子檔 | 銜接點 | |---------|--------| | `v2/server-lifecycle.md` | watchServer Error state 不被 FW 升降版觸發(§8.1)| | `v2/control-panel.md` | Wails 控制台不顯示 FW 管理 UI(業務 UI 在瀏覽器 tab、§1.2 範圍邊界)| | `v2/cors-security.md` | FW API loopback only + CORS whitelist + WS origin check | | `v2/deletions.md` | 不衝突(FW 是新增、不在 deletion 清單)| | `v2/milestone-plan.md` | 加 M9-1 ~ M9-13(既有檔不動、本檔自帶 milestone 表)| --- ## 13. 給 PM / Design 互審的注意點 ### 13.1 PM 互審注意 - **§1.2 範圍邊界**:確認 R5-Q9 翻案範圍切割對齊使用者期待(升級 vs 燒任意 model) - **§1.5 R5-Q9 行號 cross-check**(PM MJ-A2):本 TDD 不寫具體 L 行號、以描述式定位、PM PRD 同步修改避免行號漂移失準 - **§8.4 R5-B4 授權**:確認「發佈前評估」是 PRD 該記為 P0 懸念 / 不阻塞開發 - **§9 工時 15.5 人天 + M9-7~M9-10 拆法**:跟 PM 的盈虧分析對齊、採 PM 拆法(PM 互審 MJ-A1 + architect-review §1.5 共識) - **§10 R-FW-1~7 編號**:採 PM PRD §8 編號(PM 互審 MJ-A1 + architect-review §1.6 共識) - **§11.4 回歸測試**:確認測試覆蓋面對齊 PRD AC #### 13.1.1 PM PRD §14.2 待回覆項對應(A-FW-1 ~ A-FW-6) | PM A-FW 編號 | 內容 | 本 TDD 對應位置 | 狀態 | |------------|------|---------------|------| | A-FW-1 | R5-Q9 行號 cross-check + ADR-009 vs ADR-001 統一 | §1.5 + ADR-001(已修為 ADR-001、PRD 引用待 Orchestrator 同步改) | **已回覆** | | A-FW-2 | driver safety guards 對齊 AC-FW-2.10 | §2.1 `firmware/guards.go` + §5.2 `_validate_downgrade_request` + §3.3 `FW_INVALID_DIRECTION` / `FW_DEVICE_BUSY` | **已回覆** | | A-FW-3 | 升降版 progress event schema(PM 互審 M-A3)| §3.4 stage 對應表 + §4.2 `FirmwareProgress` schema(補 Reason / Elapsed / ETA / 失敗 context) | **已回覆** | | A-FW-4 | M9-6 SDK 驗證若不支援 → AC-FW-3.5 降級條件 | §9.5 Reviewer 切點補「M9-6 強驗證結論若觸發 AC-FW-3.5 降級條件、回 Orchestrator 重派 PM 微調 PRD」+ §1.2 AC-FW-3.5 階段歸屬說明 | **已回覆** | | A-FW-5 | 模組路徑 `server/internal/firmware/` | §2.1 模組職責 | **已回覆** | | A-FW-6 | R-FW-5 P0 release gate 與 R5-B4 合併處理 | §8.4 + §10.1 R-FW-5 標 P0 release gate | **已回覆** | ### 13.2 Design 互審注意 - **§5.2 降版流程**:確認二次確認 modal 的「DOWNGRADE」字面輸入框設計是否可實現(不是只用 OK / Yes 按鈕) - **§3.4 stage → 錯誤碼 → Reason 對應表**:給 Frontend / Testing 用、Design §7.1 8 種失敗情境 / §9.8 i18n keys 對應 - **§4.3 Stage 列舉採 Design 命名**(`preparing/loading/flashing/verifying`):Design Spec §8 狀態機與 §5.3 階段對應表需內部對齊(Design 自承內部矛盾、待 Design 修);Design §9.6 i18n key 改為 `progress.stage.preparing` 等 - **§8.6 graceful shutdown 拒絕**:Design 補 `control-panel.md` force-quit modal 規格(Design A-MID-1);工時併入 M9-12 - **§10 R-FW-11 / R-FW-12**:確認 UI 多層 safety net 落地(不只是技術層、design 必須對應) - **Design A-MID-2 token 對比**:信任 Design 推算 4.7-5.2:1、M9-12 Frontend 實作時用 axe-core 驗證、實測 < 4.5:1 才調整 token ### 13.3 共同確認 - §4.4 多版本目錄結構選 C 後、A 階段 → B2 階段 migration(§4.5)對既有 KL520/KL720 使用者透明 - §9 平行性(M9-6 與 M9-1 ~ M9-5 平行)是否衝擊 Reviewer 排程 - **§1.3 wheel 三平台版本不一致**(2026-05-25 新增):A 階段不阻塞、B 階段 M9-7 啟動前 30 分鐘弱驗證 + M9-13 三平台回歸 --- ## 14. 變更記錄 | 日期 | 版本 | 變更 | 作者 | |------|------|------|------| | 2026-05-24 | v2.2 草稿 | 初版產出、依使用者拍板方案 A + B 落實研究計畫 | Architect Agent | | 2026-05-25 | v2.2 互審後修 | 三方互審吸收 + M9-6 弱驗證新事實 + 使用者裁決點:(1) §1.2 補 AC-FW-3.5 延後 B 階段 M9-10 說明 + §1.3 新增 wheel 三平台版本不一致章節 + §1.5 R5-Q9 行號 cross-check(PM MJ-A2、改描述式定位);(2) §3.4 新增 stage → 錯誤碼 → Reason 對應表(Design A-MISMATCH-2、PM M-A3);(3) §4.2 `FirmwareProgress` 補 Reason / ElapsedMs / EtaMs / 失敗 context 6 欄位(Design A-OK-2 / A-MID-3、Architect F3);(4) §4.3 stage 採 Design 命名 `preparing/loading/flashing/verifying`(使用者拍板裁決 1、Design A-MISMATCH-1、Architect F1);(5) §5.1 流程內 stage 命名同步 + §5.3 失敗復原表對應 Design §7.1 8 種(Architect F7、Design A-MID-3);(6) §6.1 handler 回傳格式對齊新 stage + reason 欄位(Architect F2);(7) §7.3 補 wheel 一致性說明 + §8.6 新增 graceful shutdown 拒絕設計(Design A-MID-1、Architect F4);(8) §9 工時表採 PM 拆法(使用者拍板裁決 2、PM MJ-A1、Architect F5);(9) §10 R-FW 編號採 PM PRD 編號 + 補 R-FW-13 wheel 升級 regression(Architect F6、M9-6 弱驗證);(10) §11 補 KL720 ≤ 200s 上界測試(PM M-A2)+ §8.6 graceful shutdown 拒絕測試(Architect O3);(11) §13 PM/Design 互審注意點補 A-FW-1~6 對應狀態(PM M-A6) | Architect Agent |