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>
53 KiB
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 階段不開。理由:
- warrenchen 完全沒實作 KL630/KL730 升降版(reference 實作為零、設計風險高)
update_kdp_firmware_from_files對 KL630/KL730 是否走同一條 flash 寫入路徑、必須實機 confirm- 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 強驗證階段):
- backend agent 在 macOS / Linux 跑
python3 -c "import kp; print(kp.ProductId.KP_DEVICE_KL630)"確認 2.0.0 是否含 enum - 若 2.0.0 已含 enum → B 階段可選擇不升 wheel(風險最低)
- 若 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 使用者):
// 偽碼、不出 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 升降版用。理由:
- 命名語意分離(flash = 燒 model、firmware = 升降版 FW)
- progress event schema 不同(flash 是「載入模型」、firmware 是 chip-reset → loader → flash write → verify)
- 失敗復原策略不同(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:<deviceId> |
— | — | 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 處理邏輯:
- 收到 WebSocket
progress event含{percent: -1, stage: "error", error: "...", reason: "..."} - 用
reason欄位對應 i18n key(lookup 本表第 4 欄) - 找不到
reason→ fallback 到 stage-only mapping(如stage=verifying→settings.firmware.error.verify) 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
// 偽碼、給 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
// 偽碼
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.6settings.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:
- 把 A 階段檔案搬進
firmware/<chip>/v2.2.0/ - 加
CURRENT_VERSION單行檔 - 把
_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:<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
↓
[使用者] 選版本 → 點「降版」按鈕
↓
二次確認 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_DIRECTION400)、不會走到 progress event;本欄位保留為 bridge.py 防禦性編碼用
6.2 chip 判斷擴展(M9-7 改)
handle_connect() L732-741 從 fall-through 路徑(KL630/KL730 誤判 KL520)改為明確 dispatch:
# 偽碼
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:
# 偽碼
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 流程:
update_kdp_firmware_from_files(loader, None, auto_reboot=True)後 device 自動 reboot- USB stack 觀察到 disconnect、
disconnect_devices()預期回非零(容忍、用 try/except 包) time.sleep(2)等 USB 穩定(不是 1s、不是 5s、實測 warrenchen 用 2s 已穩)- rescan → 找回 target by usb_port_id(不靠 chip / kn_number、re-enumerate 後可能變)
- reconnect with magic
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:<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 收到 SIGTERM(Wails close handler、systemd、kill -TERM 等):
- server 進
shutting_downstate、停止 accept 新 HTTP 連線 - 檢查
firmware.Service.HasActiveTask()是否回 true(任一 device 在StatusUpgrading/StatusDowngrading) - 若有 active task:
- server 延遲 graceful shutdown、不殺 Python sidecar
- 透過 WebSocket
firmware:shutdown-rejectedevent broadcast 給所有訂閱者 - 持續等待 FW task 完成(success / error 任一終態)或最多 180s timeout(KL720 升級的硬上界)
- FW task 完成後 → 走原本 7+1s graceful shutdown 流程
- 180s timeout 後 → 仍未完成(罕見、可能 device 已 brick)→ 強制走 shutdown、log 警告
- 若無 active task:正常 graceful shutdown(既有 7+1s pattern)
8.6.2 Wails 控制台端攔截
Wails app 的 OnBeforeClose handler 改造:
- 偵測 server 是否有 active firmware task(query 內部 API
/api/firmware/active-tasks或讀firmware.Service.HasActiveTask()Wails binding) - 若有 → return false 阻擋關閉、推送 Wails event
app:firmware-in-progress給控制台 UI - 控制台 UI 顯示 modal:「韌體切換進行中(device {name})、為避免裝置損毀、無法關閉應用程式。請等待約 {ETA 秒}。」
- modal 不可關閉、不可 dismiss、只能等 firmware task 完成
- firmware task 完成後 → Wails 自動推送
app:firmware-completed、modal 消失、使用者可正常關閉 - 強制 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 階段檔案搬到 <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 | 降版 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.mdforce-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 |