visionA/local-tool/.autoflow/04-architecture/v2/firmware-management.md
jim800121chen 5e281ed449 feat(local-tool): M9-3 — firmware API handlers + WebSocket progress room
A 階段第三個 milestone、暴露 firmware service 給 Frontend / Wails control panel。

New / modified:
- server/internal/api/handlers/firmware_handler.go: 新檔 465 行(upgrade + active-tasks endpoint + WS broadcast goroutine)
- server/internal/api/handlers/firmware_handler_test.go: 新檔 938 行、26+ subtests
- server/internal/api/handlers/device_handler.go: +47 行(3 個 firmware 衍生欄位)
- server/internal/api/router.go: +23 行
- server/main.go: +10 行(wire firmware service + handler)

4 endpoints 全到位(對齊 TDD §3.1):
- GET /api/devices: 加 firmwareIsLegacy / firmwareCanUpgrade / bundledFirmwareVersion(firmwareVersion 沿用既有 DeviceInfo 鍵)
- POST /api/devices/scan: 同步走 enrichDevices
- POST /api/devices/:id/firmware/upgrade: 202 + {taskId}
- GET /api/firmware/active-tasks: HasActiveTask + GetActiveTaskInfo
- WebSocket room firmware:<deviceID> broadcast 對齊 §4.2

關鍵設計:
- 3 層 interface(firmwareBroadcaster / firmwareService / deviceLookupSource)+ DeviceManagerAdapter 解 import cycle
- bundledVersion cache(只 cache success、避免 thundering herd / poison)
- isLegacyFirmware 對齊 bridge.py 規則(legacy_exact set + KDP1.x prefix + KDP2-9 forward-compat)+ parity 真值表測試
- 5 個錯誤碼齊全(DEVICE_NOT_FOUND / FW_UNSUPPORTED_CHIP / FW_DEVICE_BUSY / FW_UPGRADE_FAILED / FW_UPGRADE_BRICK_RISK)

Reviewer 兩輪審查:
- Round 1: 0 Critical / 1 Major / 3 Minor / 5 Suggestion
- Round 2: 0 Critical / 0 Major / 0 Minor / 3 極小 Suggestion(全部 backend 不需處理、純評估)
- Major 1(JSON 雙鍵衝突 firmwareVer vs firmwareVersion)方案 A 完全到位、3 個 test 鎖定 regression

TDD 同步:firmware-management.md §3.1 line 131 firmwareVer → firmwareVersion 對齊實作。

測試:go test ./... -race -count=1 全綠(handlers 2.489s / api 3.522s / ws 4.623s / device 1.931s / firmware 2.695s / driver/kneron 5.583s / model 5.022s)

SIGTERM main.go 整合留 M9-4.5(與 Wails OnBeforeClose 一起做)。

下一步:M9-4 Frontend Devices 頁 FW badge + 升級 modal + i18n(1.5 人天)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:05:42 +08:00

824 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# v2/firmware-management.md — Kneron Dongle FW 偵測 + 升降版
> 所屬TDD v2 §2.10v2.2 新增)
> 版本v2.22026-05-24 初版 / 2026-05-25 三方互審後修)
> 決策依據:使用者拍板方案 A + B 一次做完progress.md 2026-05-24 M9 啟動)+ ADR-001-firmware-management
> 對應 milestoneM9-1 ~ M9-13A 階段 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 legacypid=0x0200、插上完全不能 inference | 中 | 使用者卡住、退裝 visionA-local |
| KL520 firmware 殘留導致 Error 15 SEND_DATA_TOO_LARGE2026-04-21 已修但 root cause 仍在 FW 層)| 高 | inference 隨機失敗 |
| KL630 / KL730 dongle 偵測不到、被誤路由到 KL520 → 連不上 | 低(裝置出貨少) | 新世代 dongle 完全不可用 |
| 舊 model 在新 FW 上跑不出結果NEF 版本與 FW 不相容)| 低 | 進階使用者卡住 |
### 1.2 範圍邊界
| 在範圍 | 不在範圍 |
|--------|---------|
| KL520 / KL720 自動升級 KDP1 → KDP2A 階段) | 使用者燒任意 model 到 device flashR5-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-checkPM 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` | AM9-1|
| `handle_firmware_downgrade` | `firmware_downgrade` | B2M9-12 之前)|
| `handle_firmware_list_versions` | `firmware_list_versions` | B2M9-11|
| 既有 `handle_connect` 擴展KL630/KL730 chip 判斷 + .tar 路徑)| — | B0/B1M9-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 modelfirmware 失敗可能 brick
---
## 3. API 設計
### 3.1 端點清單
| Endpoint | Method | Request Body | Success Response | 階段 |
|----------|--------|-------------|------------------|-----|
| `GET /api/devices` | GET | — | `data[].firmwareVersion / firmwareIsLegacy / firmwareCanUpgrade / bundledFirmwareVersion` | AM9-3|
| `POST /api/devices/:id/firmware/upgrade` | POST | `{}` | `202 {success:true, data:{taskId:"..."}}` | AM9-3|
| `GET /api/devices/:id/firmware/versions` | GET | — | `{success:true, data:{versions:[...], current:"v2.2.0"}}` | B2M9-11|
| `POST /api/devices/:id/firmware/downgrade` | POST | `{version:"v2.1.0", confirmToken:"DOWNGRADE"}` | `202 {success:true, data:{taskId:"..."}}` | B2M9-11/12|
| WebSocket room `firmware:<deviceId>` | — | — | progress eventsschema 見 §4.2| AM9-3|
### 3.2 認證 / Rate Limit
- 與既有 device API 一致loopback 127.0.0.1 only + CORS whitelist、見 `v2/cors-security.md`
- 同一 device 同時只允許一個 firmware taskservice 層 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 keyDesign §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 keylookup 本表第 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/<chip>/`(舊扁平結構)、不啟用 `CURRENT_VERSION` 機制。
B2 階段M9-11才做 migration
1. 把 A 階段檔案搬進 `firmware/<chip>/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 偵測到 KDPKDP1 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)
│ elseKDP2 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:<id>" broadcast 所有 progress events
Frontend modal subscribe、progress bar 更新、完成 toast + 自動 rescan
```
### 5.2 B2 階段:手動降版(面向一般使用者)
```
[使用者] Settings → 韌體管理 → 選 dongle → 點「切換 FW 版本」
Frontend GET /api/devices/:id/firmware/versions → 顯示 dropdown
[使用者] 選版本 → 點「降版」按鈕
二次確認 Modaldesign 領域、必須有「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` |
| TimeoutKL520 >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 已 workhardened runtime + entitlements| 無 |
| Windows | WinUSB driver 必須先綁定(既有 M1+ TODO| 升級 handler 偵測到 driver 未綁、明確錯誤訊息引導 re-run installer |
| Linux | 既有 udev rulesinstaller 已處理)| 無 |
### 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 → 不升
- 不支援 → 升 wheelwarrenchen 用 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 bug2026-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:<id>`、跟既有 `flash:<id>` / `inference:<id>` 並列 |
### 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 收到 SIGTERMWails 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 timeoutKL720 升級的硬上界)
- 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 taskquery 內部 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 UIi18n 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-11B2.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 階段檔案搬到 `<chip>/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 | 降版 UISettings 韌體管理面板 + 二次確認 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 testUR-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.5KL630/KL730 升降版落點M9-10B 階段、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-4TDD-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 設 200sPRD AC-FW-1.7 + §7.2timeout 後 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 validationwarrenchen 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 不載」但需實機 confirmB-2 強驗證項)|
| R-FW-11 | 一般使用者誤觸降版 brick 風險 | **高** | UI 多層 safety net4 條警告語 + 二次確認字串「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-132026-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-circuitdetect 後直接 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 |
| 升級 timeoutmock 180s 不回)| UI 顯示 timeout 訊息(`Reason="timeout"`、device 仍可 rescan、無 server crash |
| **KL720 升級實測時長 ≤ 200sPRD §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 完整 E2EM9-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 schemaPM 互審 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-checkPM MJ-A2改描述式定位(2) §3.4 新增 stage 錯誤碼 Reason 對應表Design A-MISMATCH-2PM M-A3(3) §4.2 `FirmwareProgress` Reason / ElapsedMs / EtaMs / 失敗 context 6 欄位Design A-OK-2 / A-MID-3Architect F3(4) §4.3 stage Design 命名 `preparing/loading/flashing/verifying`使用者拍板裁決 1Design A-MISMATCH-1Architect F1(5) §5.1 流程內 stage 命名同步 + §5.3 失敗復原表對應 Design §7.1 8 Architect F7Design A-MID-3(6) §6.1 handler 回傳格式對齊新 stage + reason 欄位Architect F2(7) §7.3 wheel 一致性說明 + §8.6 新增 graceful shutdown 拒絕設計Design A-MID-1Architect F4(8) §9 工時表採 PM 拆法使用者拍板裁決 2PM MJ-A1Architect F5(9) §10 R-FW 編號採 PM PRD 編號 + R-FW-13 wheel 升級 regressionArchitect F6M9-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 |