jim800121chen ff9bbc81ed feat(local-tool): M9-4.5 — server SIGTERM + Wails OnBeforeClose firmware-aware shutdown
A 階段尾端 milestone、雙層防護避免使用者在 firmware 升級進行中關閉 app 造成 dongle brick。

Server 端 (3 改):
- main.go: SIGTERM/SIGINT goroutine 加 firmware-aware preamble
- server/internal/firmware/shutdown.go: 新 211 行(AwaitActiveTasksOrTimeout + 3 interfaces + shutdownBroadcastTask minimal struct + toBroadcastTasks helper)
- server/internal/firmware/shutdown_test.go: 新 384 行、8 tests

Wails 端 (3 新 + 2 改):
- visiona-local/main.go: OnBeforeClose 從 inline → app.OnBeforeClose
- visiona-local/app.go: App struct 加 firmwareCloseGuard
- visiona-local/firmware_close_guard.go: 新 244 行(CloseGuard + OnBeforeClose + ConfirmForceClose)
- visiona-local/firmware_close_guard_test.go: 新 280 行、8 tests
- visiona-local/query_firmware_active_tasks.go: 新 111 行(HTTP helper、fail-open、1s timeout)
- visiona-local/query_firmware_active_tasks_test.go: 新 250 行、7 tests

行為:
- Server SIGTERM 有 active task → broadcast `server:shutdown-pending` to "system" room → RequestShutdown + WaitForActiveTasks(220s) → 走原本 shutdownFn
- Wails OnBeforeClose 有 active task → emit Wails event `app:firmware-in-progress` + return true 擋住關閉
- ConfirmForceClose binding 給 frontend 第二層 FORCE 確認用、走 graceful 7+1s shutdown(不是 SIGKILL bypass、雙層防護)

Reviewer 兩輪審查:
- Round 1: 0 Critical / 1 Major / 3 Minor / 4 Suggestion
- 第 2 輪修法(3 sub-agent 平行):
  - Architect: TDD §8.6 改 event 名 `firmware:shutdown-rejected` → `server:shutdown-pending`、標題「拒絕」→「延遲」、補 payload schema 註明 tasks 不含 startTs
  - Design: control-panel.md §6a 改「SIGKILL bypass」→「graceful 7+1s 雙層防護」、補「為何不採 SIGKILL」5 點設計理由、§6a.11 IPC 規格對齊
  - Backend: MaxShutdownWait 180s → 220s(KL720 200s upgrade + 20s buffer)+ broadcast 過濾 startTs(shutdownBroadcastTask minimal struct + toBroadcastTasks helper)

測試:
- server: go test ./... -race 全綠(firmware 2.7s + api/ws/handlers)
- wails: go test ./... -race 全綠(visiona-local 11.2s、21 tests)
- 合計新增 23 unit tests race-clean、0 regression

下一步: M9-5 三平台實機驗證 + 順手修 MJ3(backend smoke test schema phase→stage / firmware:progress→firmware_progress)

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

55 KiB
Raw Permalink Blame History

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 subsetscan_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 APIlibkplus.{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 使用者):

// 偽碼、不出 code
type DeviceDriver interface {
    // ... 既有 methods ...
    UpgradeFirmware(progressCh chan<- FirmwareProgress) error
    DowngradeFirmware(version string, progressCh chan<- FirmwareProgress) error
    ListFirmwareVersions() ([]FirmwareVersion, error)
}

新增 statusStatusUpgrading DeviceStatus = "upgrading"StatusDowngrading DeviceStatus = "downgrading"、跟既有 StatusConnecting / StatusFlashing / StatusInferencing 並列。

2.3 bridge.py 新增 handlerserver/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 找不到裝置 preparingscan 階段) FW_UPGRADE_FAILED scan_not_found settings.firmware.error.scan
2. connect 失敗 preparingconnect 階段) 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_FAILEDFW_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 mappingstage=verifyingsettings.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

// 偽碼、給 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 / EtaMsReason 等失敗欄位為空字串
  • 失敗路徑:Percent = -1Stage = "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_loaderSDK loader mode 載入、僅 legacy → modern 走此階段
flashing 50% 寫入 KDP2 firmwareupdate_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_fileupdate_* 中段拋 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

# 偽碼
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 驗證決定):

  • 策略 ZSDK 支援 load_firmware_from_tar() → 直接餵 .tar、format="tar"
  • 策略 YSDK 不支援 → build time 解壓進 extracted/format="bin"、安裝包 ship 解壓後 .bin
  • 不選策略 Xruntime 解壓):每次 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 statev2/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。注意:本設計採「延遲關閉、等 task 完成」、不是「拒絕關閉」——server 收到 SIGTERM 後仍會關、只是延後到 firmware task 跑完或 180s timeout 後才真正走 shutdown。

8.6.1 Server 端延遲關閉邏輯

當 server 收到 SIGTERMWails close handler、systemd、kill -TERM 等):

  1. 檢查 firmware.Service.HasActiveTask() 是否回 true(任一 device 在 StatusUpgrading / StatusDowngrading
  2. 若有 active task — 延遲關閉、不立即終止
    • 透過 WebSocket server:shutdown-pending event 廣播到 "system" room payload schema{"type": "server:shutdown-pending", "tasks": [...ActiveTaskInfo]} tasks 內欄位由 firmware.Service.GetActiveTaskInfo() 提供、不含 startTs 避免時間戳洩漏 — startTs 由 Frontend 自行依 elapsedMs 推算、見 Reviewer Minor-3
    • 呼叫 firmware.Service.RequestShutdown() 設旗標、拒絕 firmware task 開始(既有 task 讓它自然跑完、不主動 cancel
    • 呼叫 firmware.Service.WaitForActiveTasks(180s) 等所有既有 task 結束或 180s timeoutKL720 升級的硬上界)
    • FW task 完成 → 走原本 7+1s graceful shutdown 流程(呼叫 shutdownFn → 收 inferenceSvc / httpServer → os.Exit(0)
    • 180s timeout 後仍未完成(罕見、可能 device 已 brick→ 仍然強制走 shutdown、log warning(不會無限期卡住)
  3. 若無 active task立刻走正常 graceful shutdown既有 7+1s pattern

為什麼用 system room 而非 firmware:<device> room:對齊既有 R5-2 server:shutdown-imminent 命名 pattern兩者都是 server lifecycle event、不限定特定 device。Frontend 端 useShutdownWatcher hook 已訂閱 system room、可同時處理 pending + imminent 兩個 event、不需額外訂閱所有 firmware:<device> room。server:shutdown-pending(延後關)跟 server:shutdown-imminent(真的要關了)各司其職、語意清楚。

event 命名取捨:原 spec 草案曾考慮 firmware:shutdown-rejected、但實作落地後改 server:shutdown-pending(1) server-prefix 符合既有 lifecycle event 命名慣例;(2) 「pending」語意更準是延遲、不是拒絕(3) firmware: prefix 應留給 per-device progress eventfirmware:<deviceId> room。本 TDD 以實作為準、event 名統一 server:shutdown-pending

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 命名 server:shutdown-pending 對齊既有 system room既有 useShutdownWatcher hook 已訂閱、不增加噪音);tasks payload 過濾掉 startTs 避免時間戳洩漏Reviewer Minor-3

8.6.5 工時影響

併入 M9-11B2.1 多版本 firmware 並存)+0.5 人天、不另列 milestone。Design 端對應補 force-quit modal UI 在 control-panel.mdDesign 後續修)、工時包進 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-checkPM 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/verifyingDesign 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-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 升級 regressionArchitect 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