docs(local-tool): M9 — Kneron Dongle FW 偵測 + 升降版(A+B、翻案 R5-Q9)

L 級新功能、PRD/Design/TDD/ADR 三方協作 + 互審 + M9-6 SDK 雙驗證、總計 ~9000 行文件。

範圍:
- A 階段(MVP、5 人天):KL520 + KL720 自動升級 KDP1 → KDP2
- B 階段(10.5 人天):手動降版面向一般使用者 + KL630 / KL730 擴展
- 合計 15.5 人天、安裝包 +7MB(保守 bundle 策略)

關鍵決策:
- 翻案 R5-Q9(progress.md 第二輪使用者決策「韌體燒錄 flash → B 砍掉」)
- 跨平台用 KneronPLUS Python C API、不用 DFUT.exe
- 多版本目錄結構選 C metadata(firmware/<chip>/{version}/ + CURRENT_VERSION)
- Kneron firmware redistribution 授權與 R5-B4 預置模型同性質、發佈前評估

文件產出:
- PRD v2.2(PRD-v2.md 495 行 + features/feature-firmware-management.md 599 行)
- Design v2.2(firmware-management.md 948 行 + control-panel.md §6a graceful shutdown)
- TDD v2.2(v2/firmware-management.md 823 行 + ADR-001 218 行)
- 8 份 research(含 M9-6 弱驗證 + 強驗證、~3200 行)
- 3 份三方互審報告(PM/Design/Architect cross-review)

M9-6 強驗證重大發現(影響 B 階段):
- KL730 product_id 實際是 0x732(不是 0x0730)
- KL630/KL730 firmware 是 embedded Linux rootfs(不是 .bin、不同代設計)
- KneronPLUS Python 沒 update_kdp_firmware_from_files 公開 API、warrenchen 走 ctypes
- 不影響 A 階段、B 階段 M9-8 需 spike

下一步:派 backend M9-1 起跑(bridge.py handle_firmware_upgrade)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-05-25 07:40:56 +08:00
parent 2d629f3ba2
commit 46514d77d7
22 changed files with 8984 additions and 59 deletions

View File

@ -1,10 +1,10 @@
# visionA-local — 產品需求文件PRD v2
> 版本:**v2.12026-04-14**
> 版本:**v2.22026-05-24**
> 作者PM Agent
> 任務等級L 級(重構方向變更
> 狀態:**v2.1 補丁:吸收 R5-D1/D2/D3 + R5-E + Design 交叉審閱 4 Major / 4 Minor**
> 前版:`PRD-v2.md` v2.02026-04-14/ [`PRD.md`](./PRD.md) v1.22026-04-11
> 任務等級L 級(v2.2 = 新增 FW 管理 feature翻案 R5-Q9
> 狀態:**v2.2 補丁:新增 Kneron Dongle FW 偵測 + 升降版A + B 一次做完,~15.5 人天,+7MB。詳見 [`features/feature-firmware-management.md`](./features/feature-firmware-management.md)**
> 前版:v2.12026-04-14R5-D + R5-E/ v2.02026-04-14R5 重構/ [`PRD.md`](./PRD.md) v1.22026-04-11
---
@ -38,6 +38,10 @@ v2.1 為 Design 交叉審閱發回的小版本修正Major 13 + 4 個 Minor
**行數目標**v2.1 新增約 40 行、精簡約 20 行,總量仍 ≤ 500 行。
### v2.2 補丁摘要(相對 v2.1
v2.2 為**新增 feature**不是修訂——加入「Kneron Dongle FW 偵測 + 升降版」A + B 一次做完),翻案 R5-Q9progress.md 第二輪使用者決策 Q9 條目「韌體燒錄 flash → B 砍掉」、2026-04-11 拍板)。完整 user stories / AC / 風險 / milestone 在 [`features/feature-firmware-management.md`](./features/feature-firmware-management.md)本檔僅補§4.4 對照表 + 新增 §4.5 索引 + §10 新增一列 + §11 新增 11-8 + 變更紀錄 v2.2。新風險 R-FW-1 ~ R-FW-14 在 feature 子檔 §8 詳述v2.2.1 補強)。安裝包 +7MB保守 bundle 策略dmg 163MB → ~170MB仍在 §6.2 上限內。Kneron firmware redistribution 授權與 R5-B4 預置模型授權合併處理(同性質智財授權)。
**保留 vs 重寫對照表:**
| 章節 | 子檔 | v2 處理 |
@ -178,8 +182,20 @@ visionA-local 是一個**本機服務 + 網頁控制台**架構的 Kneron AI 邊
| Settings > 語言 / 深色模式 | P1, P2 | 瀏覽器 Web UI |
| Settings > 自動開瀏覽器 | P1, P2 | **Wails 控制台**(因為它控制的是 Wails 啟動行為)|
| Server Offline Overlay | 全體 | 瀏覽器 Web UI |
| **FW badge + 升級 modal**v2.2 新)| P1, P2 | 瀏覽器 Devices 頁 |
| **Settings → 韌體管理**v2.2 新B 階段)| P1, P2 | 瀏覽器 Settings |
| ~~Mock 模式~~ | ~~P3~~ | ~~砍~~ |
### 4.5 Kneron Dongle FW 偵測 + 升降版v2.2 新增 feature
**翻案 R5-Q9**progress.md 表格條目「Q9 韌體燒錄 flash → B 砍掉」2026-04-11 第二輪決策)。當時砍的是「使用者燒任意 model 到 device flash」這條使用者旅程v2.2 範圍切割後只做「升降版到 Kneron 官方 bundle 版本」,避開原始 brick 擔憂。完整 user storiesUS-FW-1 ~ US-FW-3/ AC / 風險 / milestone 詳見 [`features/feature-firmware-management.md`](./features/feature-firmware-management.md)。
- **A 階段MVP5 人天)**KL520 + KL720 自動升級 KDP1 → KDP2、+0KB 安裝包、Devices 頁 FW badge綠/黃/紅/灰)+ 升級 modalM9-1 ~ M9-5。**KL630/KL730 A 階段只做 FW 偵測(無升降版按鈕)**v2.2.1 吸收 M9-6 弱驗證)
- **B 階段擴展10.5 人天)**手動「韌體版本切換」面向一般使用者UI 文案中性「韌體管理」/「切換到此版本」、不用「降版」字眼;含多層 safety net、不藏 dev mode、二次確認字串「DOWNGRADE」防誤觸+ KL630/KL730 升降版M9-10、含 AC-FW-3.5 生效條件 a/b/c、+7MBM9-6 ~ M9-13M9-6 SDK 弱驗證已完成、強驗證 M9-9/M9-10 啟動前實機)
- **stage 命名統一**v2.2.1preparing / loading / flashing / verifying / done / error採 Design 命名為 source of truth、WebSocket event stage + i18n key 一致)
- **新風險 R-FW-1 ~ R-FW-14**v2.2.1 補 R-FW-13 wheel 三平台版本不一致 P1 / R-FW-14 大小寫繞過 P2詳見 feature 子檔 §8
- **與 R5-B4 合併處理**Kneron firmware redistribution 授權與既有預置模型 redistribution 同性質(智財授權),發佈前一起取得 Kneron 書面授權使用者明示「先不管授權、發佈前再評估」feature 子檔 §9 Q-FW-1
---
## 5. User Stories重寫受影響的
@ -437,58 +453,35 @@ Architect TDD v2 必須產出 `scripts/build-ffmpeg-macos.sh`(記錄 configure
---
## 10. 變更追蹤(和 v1.2 的完整差異清單
## 10. 變更追蹤(高階差異,逐項見 git history
| 區域 | v1.2 | v2.0 | R5 依據 |
v1.2 → v2.x 已折成下方 bullet原 25 項大表移除以保 500 行上限):
- **定位轉向**R5-1Wails 內嵌 Next.js → 本機服務 + 網頁控制台Ollama 式)
- **砍 Mock / URL 推論 / yt-dlp 35MB**R5-5a / 共識 2US-1/2/5/6/9 全面重寫
- **ffmpeg GPL → LGPL 方案 B**R5-6 系列Win/Linux BtbN + macOS 自 build ~20MB
- **每次啟動自動開瀏覽器**R5-4/D2/D3+ AC-1.3 ≤60s perceived performanceR5-E1~E6
- **新基礎建設**R5-5/共識 5/6/8/14Wails 控制台 + Offline Overlay 硬阻斷 + CORS + boot-id + watchServer Error state + Footer 警示 + Error 三按鈕 + 崩潰 OS 通知
- **First-Run 3 步→2 步**R5-5a / Minor 4影片副檔名改 mp4/avi/mov/mpeg/mpgmacOS ≤ 185MBv2.2 含 +7MB FW ~170MBidle RAM ≤ 450/550MB
**節錄表(僅 v2.2 新項與 v2.x 風險)**
| 區域 | v1.2 | v2.x | R5 依據 |
|------|------|------|---------|
| 產品定位 | Wails 桌面 app 內嵌 Next.js UI | 本機服務 + 網頁控制台 | R5-1 |
| 競品類比 | Photo Booth | Ollama / Docker Desktop / LM Studio / SD WebUI | R5-1 |
| 一句話價值主張 | 「裝起來就能跑、離線可用、零依賴」 | 「雙擊 → 自動開瀏覽器 → 插 USB → 5 分鐘跑第一次推論」 | R5-1 / R5-4 |
| 北極星指標終點 | Mock 第一幀 | 瀏覽器第一幀(真實硬體)| R5-5a |
| Mock 模式 | MVP 必達 | **砍** | R5-5a |
| URL 影片推論 | US-8 / `/api/media/url` / yt-dlp 內嵌 | **砍** | R5 共識 2 |
| yt-dlp vendor35MB| 打包 | **砍** | R5 共識 2 |
| ffmpeg | GPL buildblocker 🔴)| **LGPL 方案 B 混合**Win/Linux BtbN、macOS 自 build decoder-only ~20MB commit| R5-6 / R5-6a / R5-6b / R5-6c |
| 安裝檔大小macOS 目標)| ≤ 220 MB | **≤ 185 MB** | R5-6 |
| idle RAM無硬體| ≤ 500 MB / 上限 600 MB | ≤ 450 MB / 上限 550 MB | 砍 Mock / yt-dlp |
| Wails 視窗關閉 | 結束程式 | **同 v1.2**R5-2 復議後維持)| R5-2 |
| Tray | 砍v1.2 Q-A| **同 v1.2**R5-3 復議後維持)| R5-3 |
| 啟動時自動開瀏覽器 | 無 | **新增,每次啟動都開**R5-D3macOS/Win 預設 ON、**Linux 預設 OFF**R5-D2toggle 住 Wails 控制台 Settings已與 Design 取得共識) | R5-4 / R5-D2 / R5-D3 |
| AC-1.3 啟動預算 | — | **≤ 60 秒**(原 v2.0 草案 10 秒硬指標被 R5-E1 取代)+ 階段化進度6 階段R5-E2+ 20 秒卡住提示R5-E3+ 60 秒超時進 Error stateR5-E4+ WebSocket 就緒偵測R5-E6 | **R5-E1E6** |
| Server 崩潰 OS 通知 | 無 | **新增**(控制台 Error banner 並存R5-D1| R5-D1 |
| First-Run wizard 步數 | 3 步(歡迎 / 模式選擇 / 硬體偵測) | **2 步**(歡迎 / 硬體偵測,砍模式選擇;無硬體可按「稍後再設定」略過) | R5-5a / Design review Minor 4 |
| Offline Overlay 硬阻斷 | — | **新增驗收特性**`role="alertdialog"` + focus trap + 無 ✕ 按鈕 + 純色卡片) | Design review Minor 2 |
| Wails 控制台 Footer 持久警示 | 無 | **新增**「⚠ 關閉此視窗會停止 Local Server」取代單次 confirm 作為主要提醒 | Design review Minor 3 |
| Error state 三動作按鈕 | 無 | **新增** `Restart Server` / `View log` / `Report issue`Wails 控制台 Error banner 內) | Design review Minor 3 |
| Wails 控制台 | 無webview 載 Next.js| **新增 vanilla HTML/JS/CSS**Start/Stop/Restart/Open Browser/log panel/status/port/dir/version | R5-5 / R5 共識 5 |
| Server Offline Overlay | 無 | **新增**Web UI 全螢幕覆蓋層)| R5-2 |
| CORS middleware | 無 | **新增**127.0.0.1 / localhost only| R5 共識 6 |
| watchServer 連 3 次失敗 | `os.Exit(1)` | **改為 Error state不退出** | R5 共識 8 |
| Server boot-id | 無 | **新增**(支援 Restart Server 按鈕的 tab 自動重連)| R5 共識 14 |
| US-1 | 3 分鐘看到 Dashboard | 5 分鐘瀏覽器跑第一次推論 | R5-1 / R5-4 |
| US-2 | Mock 試玩 | 日常啟動(取代)| R5-5a |
| US-5 | Wails webview 攝影機 | 瀏覽器 Workspace + 後端攝影機 pipeline不走 `getUserMedia`| R5-1 |
| US-7 / US-8 | 影片 / URL 上傳 | 合併為 US-6砍 URL | R5 共識 2 |
| US-9 | Settings 進階分頁次要功能 | **升級為 Wails 控制台一級功能** | R5-1 / R5-5 |
| 新 US-7server 崩潰 / 離線)| 無 | **新增** | R5-2 |
| 上傳影片副檔名 | mp4 / avi / mov / webm瀏覽器能吃| **mp4 / avi / mov / mpeg / mpg** | R5 共識 11 |
| Persona P3 Sam | 三順位目標 | **實質降級為非目標** | R5-5a |
| 新風險 | — | N-R1 瀏覽器相容 / N-R2 macOS ffmpeg 維護 / N-R3 關窗誤解 / N-R4 CI 測試分層 | — |
| 新風險v2.x| — | N-R1 瀏覽器相容 / N-R2 macOS ffmpeg 維護 / N-R3 關窗誤解 / N-R4 CI 測試分層 + **R-FW-1 ~ R-FW-14v2.2.1 補強 R-FW-13 wheel 三平台版本不一致 P1 / R-FW-14 大小寫繞過 P2** | — / v2.2.1 |
| 解除風險 | R6 ffmpeg GPL blocker | **解除** | R5-6 |
| **v2.2 新 feature** | — | **FW 偵測 + 升降版**A+B、+7MB、~15.5 人天、翻案 R5-Q9v2.2.1 吸收三方互審 + M9-6 弱驗證)| **§4.5 + feature 子檔** |
---
## 11. 給 Orchestrator / 下輪審閱的問題
| # | 問題 | v2.1 狀態 |
| # | 問題 | 狀態 |
|---|------|---------|
| 11-1 | auto-open Settings 資料落在哪Web UI 和 Wails 控制台共用同一個 config 嗎? | ✅ **已解決**Architect 答:`preferences.json @ <dataDir>/`write-rename 原子寫fallback DefaultPreferencesauto-open toggle 由 Wails 進程讀取Web UI 不碰此欄位) |
| 11-2 | US-1 AC-1.3 10 秒預算是否可達? | ✅ **已解決(由 R5-E 取代)** — 整個問題被重新定義為「60 秒總預算 + 階段化進度 + perceived performance」不再是硬時間指標 |
| 11-3 | idle RAM ≤ 450 MB 是否可達? | ✅ **已解決**Architect 實測Wails + Go server + Python 275405 MB 達標;悲觀估含瀏覽器 tab ~500 MB 超 50 MB但瀏覽器 tab 不在本指標範圍內,已於 §6.1 clarify |
| 11-1 ~ 11-3 / 11-5 / 11-6 | v2.1 五題已解決auto-open 落點、AC-1.3 預算、idle RAM、Web UI 徽章、Settings toggle 位置)| ✅ **已解決**(詳細結論散見 §1.4 / §6.1 / §4.5 N1-N2 / Design Spec v2 `v2/settings-update.md`;如需追溯,見 git log v2.1 提交)|
| 11-4 | N-R4 砍 Mock 後 CI / E2E 測試分層 | ⏳ **懸置**,交 Testing Agent 在測試計畫提出「不需硬體的 UI 測試」vs「需硬體測試」分層方案 |
| 11-5 | Web UI 要不要加常駐「本機服務」徽章? | ✅ **已解決:不加**Design Agent 決定,改由 First-Run Step 1 歡迎頁一次性告知;見 Design review §D |
| 11-6 | Settings auto-open toggle 住哪裡? | ✅ **已解決**Design 仲裁採 PRD 原案:住 Wails 控制台 Settings menuDesign Spec v2 `v2/settings-update.md` 將同步修正) |
| 11-7 | R5-E 階段化進度 6 階段文案(中英雙語)定稿 | ⏳ **懸置**R5-E5 使用者授權 Design Agent 決定;最終在 Design Spec v2 wireframe 定版時由使用者審閱 override |
| 11-8 | FW 管理 feature 三方互審項 | ✅ **v2.2.1 已吸收**D-FW-1~6 / A-FW-1~6 全部已處理、O-FW-1與 R5-B4 release blocker 合併)+ O-FW-2M9-10 AC-FW-3.5 條件評估)為 Orchestrator 追蹤項;詳見 [`features/feature-firmware-management.md`](./features/feature-firmware-management.md) §14|
---
@ -496,5 +489,6 @@ Architect TDD v2 必須產出 `scripts/build-ffmpeg-macos.sh`(記錄 configure
| 版本 | 日期 | 作者 | 變更 |
|------|------|------|------|
| v2.0 | 2026-04-14 | PM Agent | 依 R5 五輪決策重寫:砍 Mock / URL 推論 / yt-dlp新增 Wails 控制台 + Server Offline Overlay + 首次自動開瀏覽器 + CORSffmpeg 從 GPL blocker 解除為 LGPL 方案 B 混合Persona P3 降級US-1/US-2/US-5/US-6/US-7 重寫;解除 R6新增 N-R1 ~ N-R4 風險。完整差異見 §10 |
| v2.1 | 2026-04-14 | PM Agent | 吸收 Design 交叉審閱4 Major + 4 Minor+ R5-D1/D2/D3 + R5-E1E6(1) 「首次啟動自動開」→「每次啟動自動開」全文修正R5-D3(2) auto-open 平台預設 Linux OFFR5-D2(3) US-7 新增 AC-7.7 Server 崩潰時發 OS 原生通知R5-D1(4) §4 新增 N7 OS 通知、N8 啟動階段化進度6 階段);(5) **AC-1.3 從 10 秒硬指標改寫為 60 秒 + perceived performance + AC-1.3a/b/c/d 四條新驗收**R5-E1E6(6) US-2 明確日常啟動同樣 auto-openMinor 1(7) Offline Overlay 補齊 `role="alertdialog"` + focus trap + 不可關閉 + 純色卡片Minor 2(8) US-7 AC-7.3 / AC-7.6 補 Wails 控制台 Footer 持久警示 + Error state 三動作按鈕Minor 3(9) US-1 AC-1.4 補 First-Run 從 3 步縮為 2 步 + 「稍後再設定」略過Minor 4(10) §6.1 idle RAM 加不含瀏覽器 tab 註記 + Architect 實測佐證;(11) §11 懸念 5 題結案 + 新增 11-6 / 11-7保留 11-4 CI 測試分層懸置 |
| v2.0 / v2.1 | 2026-04-14 | PM Agent | R5 五輪決策重寫 + Design 互審吸收(詳見 §10 / git log |
| v2.2 | 2026-05-24 | PM Agent | 翻案 R5-Q9、新增 FW 管理 featureA+B、~15.5 人天、+7MB。詳見 [`features/feature-firmware-management.md`](./features/feature-firmware-management.md) |
| v2.2.1 | 2026-05-25 | PM Agent | FW feature 吸收三方互審 + M9-6 弱驗證①stage 命名採 Designpreparing/loading/flashing/verifying②M9 工時表拆法採 PMArchitect 同意③AC-FW-3.5KL630/KL730 升降版)延後 B 階段 M9-10 + 生效條件 a/b/c、A 階段 KL630/KL730 只做 FW 偵測④ADR-009 → ADR-001 編號統一⑤R5-Q9 行號改描述式引用⑥US-FW-2 拆 2a / 2b + UI 文案中性化⑦補 §7.2.1 體驗指標 5 條⑧新增 R-FW-13/14⑨補 AC-FW-1.9 graceful shutdown⑩badge 4 色補灰色⑪11-8 解決。詳見 [`features/feature-firmware-management.md`](./features/feature-firmware-management.md) §14.4 |

View File

@ -0,0 +1,599 @@
# feature: Kneron Dongle FW 偵測 + 升降版
> 對應 PRD[`PRD-v2.md`](../PRD-v2.md) v2.22026-05-24
> 作者PM Agent
> 任務等級L 級新功能(翻案 R5-Q9
> 範圍A 階段MVP+ B 階段(擴展),一次做完
> 預估工時A 5 人天 + B 10.5 人天 = **15.5 人天**
> 安裝包衝擊:+7MB保守 bundle 策略163MB → ~170MB
> 狀態:**v2.2.1 — 吸收三方互審 + M9-6 弱驗證結果裁決stage 命名採 Design / M9 工時拆法採 PM / AC-FW-3.5 延後 B 階段 M9-10**
---
## 0. 一句話摘要
讓使用者把插上的 Kneron Dongle 從「Error 15 完全不能用的舊 FW」一鍵升級到內建 KDP2 標準版本,並提供面向一般使用者的多版本切換 UI含降版全程離線可用、不需要 DFUT.exe 或外部工具。
---
## 1. 動機
### 1.1 真實痛點:拿到舊 dongle 完全不能用
2026-04-21 的 bug 紀錄progress.md §「2026-04-21 推論 bbox 標註不顯示 + KL520 Error 15」已具體記錄
- KL520 USB Boot dongle 若 session 間 firmware 殘留 `fw=KDP2 Comp/U`**直接 `load_model + inference` 100% 炸 `Error 15 SEND_DATA_TOO_LARGE`**
- 解法是強制走完整 `reset → 退回 Loader → 重新載 firmware` 流程
但這只解決「session 間殘留」,**沒解決「拿到一根插上是 KDP1 legacy 的舊 dongle」這個更基礎的問題**——使用者收到 Kneron 出貨的 dongle 可能是 KDP1舊版、可能是 KDP2 但版本太舊、可能 firmware 在運送過程被擦掉留在 Loader mode。所有這些情境的共通解法只有一條**升級到 Kneron 官方 KDP2 標準版本**。
目前 visionA-local 沒有任何 UI 暴露「升級 firmware」這個動作使用者只能
1. 自己想辦法找 DFUT.exeWindows-only Qt 工具)
2. 把 dongle 寄回 Kneron 換貨
3. 放棄使用
→ 這違反 v1.2 北極星指標「**5 分鐘內順利跑出第一次推論的比例 ≥ 95%**」——只要使用者拿到不對版本的 dongle5 分鐘永遠達不到。
### 1.2 為什麼現在做
| 觸發因素 | 說明 |
|---------|------|
| Architect 已完成 research plan | `.autoflow/04-architecture/research-kl520-fw-management/` 7 個檔、合計 ~3000 行,已驗證技術可行 |
| 同事 warrenchen 已有雲端版實作可參考 | `gitea/warrenchen/web_academy_prototype/local_service_win`UX 旅程已被驗證 |
| KneronPLUS C API 跨平台可用 | `libkplus.{dll,so,dylib}` 三平台都在現有 bundle 內、增加成本 0KB |
| 使用者明確要求 + 願意翻案 R5-Q9 | 2026-05-24 拍板 A + B 一次做完 |
| 既有 code 已有 70% 邏輯 | bridge.py `handle_connect()` 已可偵測 Loader mode 並 RAM-load firmware缺的只是「持久化升降版」 |
---
## 2. R5-Q9 翻案分析
### 2.1 R5-Q9 原文progress.md 第二輪決策表)
```
| Q9 | 韌體燒錄 flash | **B** 砍掉 |
```
**位置描述**progress.md 「第二輪使用者決策」表格中、Q9 條目2026-04-11 拍板)。行號因 progress.md 多次追加內容會浮動、文件追溯**以決策內容為準、不以行號為準**(早期 Architect research summary `00-research-summary.md` §1.7 與本檔早期版本均引用過 L776 / L854、僅供歷史追溯參考。「Q9 韌體燒錄 flash 砍掉」的決策本身沒有疑義。
### 2.2 當時為何砍
progress.md 沒留下明文理由只標「B 砍掉」。Architect research 推測的 4 個可能原因(待使用者最終確認):
1. visionA-local 定位是「local 推論工具」、不是「dongle 管理工具」,燒 flash 不屬核心使用旅程
2. 燒 flash 有 brick 風險、當時不想對使用者開放
3. 早期 MVP 範圍縮小、把非必要功能延後
4. 既有「load_firmware 到 RAM」邏輯已夠用基本推論KL520 USB Boot 每次重 load、KL720 已預燒 KDP2
### 2.3 現在為何要翻
| 因素 | 說明 |
|------|------|
| 真實痛點 | 2026-04-21 Error 15 經驗證實「舊 firmware = 完全不能用」 |
| 使用者明確要求 | 2026-05-24 拍板 A + B 全做 |
| 範圍可切割 | 只做「升級到內建 KDP2 標準版本」,不做「使用者燒任意 binary」 |
| Architect research 已驗證可行 | 5 人天 MVP、+0KB 安裝包、技術風險可控 |
| 同事雲端版已驗證 UX | warrenchen 雲端網頁 + 本地服務雙進程版已 ship使用者旅程被驗證過 |
### 2.4 範圍切割:避開 Q9 原始擔憂
| Q9 砍掉的 | 本任務不做 | 本任務做 |
|----------|-----------|---------|
| 「使用者按按鈕燒任意 model 到 device flash」 | ✅ 不做(與 model load 區隔) | — |
| 「開放使用者選任意 binary 寫 flash」 | ✅ 不做(只允許內建 bundle 過的官方 firmware | — |
| — | — | ✅ 偵測 dongle 當前 FW 版本 |
| — | — | ✅ 升級到內建 Kneron 官方 KDP2 標準版本 |
| — | — | ✅ 切換到內建其他 bundle 版本(含降版) |
**既有 `server/internal/flash/` 模組保持「load model 到 device RAM」原意**;新建 **`server/internal/firmware/`** 模組做升降版Architect research §2.4 已明示模組劃分)。
### 2.5 決策痕跡
Architect 已於 [`ADR-001-firmware-management.md`](../../04-architecture/adr/ADR-001-firmware-management.md) 留下完整翻案理由與技術決策Status: Accepted、2026-05-24。本 feature 文件 + ADR-001 + progress.md 三處互相引用、確保未來回溯時不會誤會「為什麼第二輪砍了又補回來」。
---
## 3. 範圍切割A / B 兩階段
### 3.1 A 階段MVPFW 偵測 + 自動升級 KDP1 → KDP2
**裝置範圍**KL520 + KL720既有 driver 已支援)
**功能範圍**
1. **被動偵測**scan / connect 時讀取 firmware 字串、Devices 頁顯示 FW 健康度 badge綠 = 最新、黃 = 可升、紅 = 必升)
2. **主動升級**(單一動作):使用者按「升級到最新」按鈕 → bridge.py 走 `kp.core.update_kdp_firmware_from_files` → progress 透過 WebSocket 推播 → 完成後自動 rescan
3. **不做手動降版 UI**B 階段才做)
**API 範圍**
- `GET /api/devices/:id/firmware`(讀取 FW 狀態)
- `POST /api/devices/:id/firmware/upgrade`(觸發升級)
- WebSocket room `firmware:<deviceId>`progress 推播)
**安裝包衝擊**+0KBKL520/KL720 firmware 已 bundle 在 `server/scripts/firmware/`
**工時**5 人天M9-1 ~ M9-5見 §10
### 3.2 B 階段(擴展):手動降版 + KL630/KL730
**裝置範圍擴展**+ KL630 + KL730需先擴 driver 處理 product_id 0x0630 / 0x0730 + .tar firmware
**功能範圍**
1. **手動降版 UI面向一般使用者**Settings → 「韌體管理」面板,不只 dev mode含多層 safety net見 §4
2. **多版本 firmware 並存**:每個 chip 提供 current + 1-2 個降版選項
3. **KL630/KL730 完整 FW 升降版**:含 .tar firmware 解壓邏輯Architect M9-6 SDK 驗證待完成)
4. **KneronPLUS SDK 對 KL630/KL730 的 Python API 驗證**M9-6
**API 範圍**
- `GET /api/devices/:id/firmware/versions`(列出可選版本)
- `POST /api/devices/:id/firmware/downgrade`(觸發降版,需 `confirmToken: "DOWNGRADE"`
**安裝包衝擊**+7MB保守 bundle 策略;見 §6
**工時**10.5 人天M9-6 ~ M9-13見 §10
### 3.3 A / B 範圍對照表
| 項目 | A 階段MVP | B 階段(擴展) |
|------|---------|---------------|
| **裝置範圍** | KL520 + KL720 | + KL630 + KL730 |
| **操作範圍** | 自動升級 KDP1 → KDP2 | + 手動降版 + 多版本選擇 |
| **使用者介面範圍** | Devices 頁 FW badge + 升級按鈕 + progress modal | + Settings → 韌體管理面板 + 二次確認 modal |
| **新增 API 數** | 3 個GET firmware、POST upgrade、WS progress room | + 2 個GET versions、POST downgrade |
| **bridge.py 新 handler** | `firmware_upgrade` | + `firmware_downgrade` + `firmware_list_versions` |
| **driver 新 method** | `UpgradeFirmware()` | + `DowngradeFirmware(version)` + `ListFirmwareVersions()` + `GetCurrentFirmwareVersion()` |
| **目錄結構** | 沿用 A 階段 `firmware/<chip>/fw_*.bin` | 改 selecting C`firmware/<chip>/{v2.2.0,v2.1.0,kdp1}/` + `CURRENT_VERSION` |
---
## 4. User Stories面向「飛 firmware 痛點」的使用者旅程)
### US-FW-1A 階段 — 拿到舊 dongle 一鍵變可用)
**身份**P1 Arthur內部 FAE/ P2 Dora外部開發者
**情境**:客戶現場、剛拿到 Kneron dongle可能是 KDP1 legacy / 舊 KDP2插上 USB
**敘述**
> 身為 FAE我希望插上 dongle 後 Devices 頁立刻告訴我「這根 firmware 太舊、按下『升級到最新』就能用」,按下後 30 秒~3 分鐘內升級完成、自動 rescan、就能繼續正常 demo。
**驗收標準**
- **AC-FW-1.1**scan 後 Devices 頁卡片右上角顯示 FW badge綠/黃/紅/灰 四色(綠 = 與內建 bundle current 相同;黃 = 比 current 舊但可升、含 v2.1.0 等 older 版本;紅 = legacy KDP1 系列或損毀 firmware 字串讀不到、必須升才能用;**灰 = 狀態未知**:壞掉 firmware / loader mode / KL630/KL730 A 階段「升降版尚未支援」狀態。Design v2.2 §4.2 標 3 色 + 補灰色 = 4 種總狀態。Driver 層判定來源:`firmwareIsLegacy` / `firmwareCanUpgrade` / `bundledFirmwareVersion`Architect TDD §3.1 衍生欄位)
- **AC-FW-1.2**:黃 / 紅 badge 點擊或卡片內「升級到最新」按鈕,跳出 progress modal全程顯示**階段(採 Design 命名preparing準備中 / Preparing/ loading載入引導程式 / Loading loader/ flashing寫入韌體 / Flashing firmware/ verifying驗證中 / Verifying/ done完成 / Done**+ progress bar + 不可中斷警告。WebSocket event 的 `stage` enum 值與 i18n key 與此命名一致Architect TDD §4.3 stage 列舉同步、為 source of truth
- **AC-FW-1.3**:升級成功後 modal 顯示 ✅ + 「裝置已升級到 v2.2.0」 + 自動關閉 modal5 秒後)+ 觸發 Devices 頁 rescan + 卡片 badge 變綠
- **AC-FW-1.4**:升級失敗時 modal 顯示明確失敗類型Design v2.2 §7.1 列 8 種scan / connect / loader / upgrade / verify / timeout / disconnect / 部分成功)+「複製錯誤訊息」按鈕 + 「重新插拔裝置後重試」指引;後端透過 `FirmwareProgress.reason` 細分 stageArchitect TDD §3.4 對應表)
- **AC-FW-1.5**:升級期間 device 進入 `StatusUpgrading` 狀態(既有 driver Status enum 擴展),鎖住其他操作(推論 / 切模型 / disconnect
- **AC-FW-1.6**:升級成功後 device 會 re-enumerateUSB 拔插效果bridge.py 必須等 **5-8 秒(實測 5 秒已穩、保留上界容忍)** 讓 USB stable 才主動 rescan避免 KL520 reset bug 同樣坑progress.md「2026-04-21 推論 bbox」紀錄已驗證機制
- **AC-FW-1.7**KL520 升級預估 ~30 秒(實測值)/ KL720 預估 ~180 秒實測值progress modal 顯示預估時間讓使用者不焦慮。**timeout 護欄上界**KL520 ≤ 60s / KL720 ≤ 200s§7.2 護欄、超過視為失敗)
- **AC-FW-1.8**升級流程屬「device 層」失敗、不應觸發 `watchServer` Error state與 Architect TDD v2.2 §8.1 解耦research §2.1
- **AC-FW-1.9(吸收 A-MID-1 + Architect §2.5**:升級 / 降版進行中、Wails 控制台關閉視窗動作必須被擋下(防止 SIGTERM 中斷 firmware task brick 裝置UI 顯示 modal「韌體切換進行中device X、為避免裝置損毀、無法關閉應用程式」、modal 不可 dismiss、等 firmware task 完成才能關 appArchitect TDD §8.6「graceful shutdown 拒絕」實作 + Design control-panel.md 補對應 modal
### US-FW-2aB 階段 — 一般使用者主動切版本,相容性 / 回上版情境)
**身份**P1 / P2 — 一般使用者FAE / 外部開發者,非純末端 consumer
**情境**(中、低頻率):
- 使用者升版後發現「我的舊 model 在新 FW 上跑不出結果」想回到 v2.1.0
- 使用者跟某個 third-party tool 不相容、要回 KDP1
- 使用者誤觸升級、想 revert
- 客戶現場 demo 需求特定版本
**敘述**
> 身為一般使用者,我希望能去 Settings → 「韌體管理」面板,看到我每根 dongle 當前 FW 版本、bundle 內所有可選版本、每個版本的說明,能選一個並按下「切換到此版本」。我不希望點錯按鈕就 brick 裝置——應該要有明確警告 + 二次確認字串。
### US-FW-2bB 階段 — 進階使用者跨版本切換,測試 / 開發情境)
**身份**P2 — 進階使用者 / 開發者
**情境**
- 開發者測試多版本相容性
- 測試環境需要特定版本
- 「就是想試試看」
**敘述**
> 身為開發者,我希望同一個「韌體管理」面板讓我自由在 bundled 版本間切換、UI 不對升 / 降特別 framing方便我快速測試各版本行為。我能接受多一層確認流程作為 brick 防誤觸。
**framing 共識Design + PM 對齊,吸收互審 MJ-D1**
- **UI 文案統一中性**分頁名稱「韌體管理」、按鈕文案「切換到此版本」、warning 強調「切換可能影響相容性 / 不可中斷」、**不用「降版」字眼**(避免「面向一般使用者過於負面」+ 同時涵蓋 US-FW-2b 開發者跨版本切換情境)
- **程式碼 / log / API 內部用語可保留 `downgrade`**API endpoint 仍是 `POST .../firmware/downgrade`、ADR / TDD / log 行為事件名稱保留技術詞(與 PRD §9 Q-FW-3 對齊 Design v2.2 §2.1 立場)
- **「DOWNGRADE」確認字串保留英文字面**:是 API contract、不 i18n 化Architect 互審結論一致)
### US-FW-2 共通驗收標準US-FW-2a + US-FW-2b 適用)
- **AC-FW-2.1**:入口在 Settings → 「韌體管理」面板(**不在 Devices 頁主流程**避免誤觸Architect research 42 §1.2 推薦;分頁順序「一般 / 硬體 / 韌體 / 模型 / 進階」見 Design v2.2 §2
- **AC-FW-2.2**:列出所有偵測到的 dongle、每張一張卡片顯示dongle 名稱、kn_number、當前 FW 版本、bundle current 版本
- **AC-FW-2.3**:卡片內「切換 FW 版本」accordion 展開後顯示版本 **radio list 或同等選擇 UI**(不限 dropdown — Design v2.2 §3.3 採 radio list理由版本 < 3 a11y 友善每個版本顯示 displayNamev2.2.0 (current)」「v2.1.0 (older)」「KDP1 (legacy)」)+ 說明文案 + 預估時間
- **AC-FW-2.4**選了非當前版本後「切換到此版本」按鈕變紅色destructive 視覺、即使是切換到 newer 版本也統一處理),點擊後跳出二次確認 modal
- **AC-FW-2.5**:二次確認 modal 必含以下警告語Design Agent 定稿文案):
- 「降版可能導致現有 model 無法運作」
- 「降版過程不可中斷、否則裝置可能損壞」
- 「降版完成後可能需要重新插拔裝置」
- 「請確認版本相容性、降版至 KDP1 的舊版會限制可用功能」
- **AC-FW-2.6**:二次確認 modal 強制使用者**輸入字面字串「DOWNGRADE」**才能點確認按鈕防誤觸input 欄空 / 不對時按鈕 disabled
- **AC-FW-2.7**:二次確認 modal 不可被點外部關閉(必須點「取消」明確關閉),確認後 modal 不關閉、直接切「進行中」狀態(避免使用者誤以為什麼都沒發生)
- **AC-FW-2.8**降版進行中、「進行中」UI **不顯示「取消」按鈕**(降版不可中斷),顯示 persistent banner「請勿拔除裝置」
- **AC-FW-2.9**API 層強制要求 request body 含 `confirmToken: "DOWNGRADE"`(字面字串);沒帶 / 帶錯 → 400防 CSRF + 強制 UI 二次確認流程)
- **AC-FW-2.10**Driver 層 safety guards
- 拒絕跨晶片誤匹配version 必須在 `ListFirmwareVersions(chip)` 結果內)
- 拒絕升版偽裝(目標版本 >= current → 拒絕、改走 upgrade API
- 拒絕 no-op目標版本 == current → 拒絕)
- 拒絕 status busydevice 在 `StatusInferencing` / `StatusFlashing` / `StatusUpgrading` 不接受降版)
### US-FW-3B 階段 — KL630/KL730 dongle 偵測 + 升降版)
**身份**P1 / P2
**情境**:客戶用 KL630 或 KL730新一代 chip插上後希望也能偵測 + 升降版
**敘述**
> 身為使用者,我希望 KL630/KL730 跟 KL520/KL720 一樣支援 FW 偵測 + 升降版UI 流程一致。
**驗收標準**
- **AC-FW-3.1**KL630/KL730 dongle 在 Devices 頁能被偵測(既有 driver `handle_connect()` 必須先擴展處理 product_id 0x0630/0x0730progress.md 已標註)
- **AC-FW-3.2**KL630/KL730 的 FW 升降版 UI 流程與 KL520/KL720 完全一致(複用同一套元件)
- **AC-FW-3.3**bridge.py 必須能處理 .tar firmwareArchitect M9-6 弱驗證確認「策略 Z 直接餵 .tar 給 SDK 不可行」、唯一可行解為 **策略 Ybuild-time 解壓 .tar 找 .bin**;詳見 §8.3 + research 41 §4.4
- **AC-FW-3.4**升降版預估時間KL630/KL730 ~60 秒(待 M9-10 強驗證實測校正)
#### AC-FW-3.5KL630/KL730 升降版 — **延後到 B 階段 M9-10 才實作**
**A 階段對 KL630/KL730****只做 FW 偵測**顯示版本字串、Devices 頁 badge 可顯示「未知 / 待 M9-10」灰色 state**不開放升降版動作**(無升級按鈕、無「切換 FW 版本」accordion
**B 階段 M9-10 才實作升降版**,理由(吸收 Architect M9-6 弱驗證結論,詳見 `research-kl520-fw-management/55-m9-6-weak-validation-result.md`
1. **無 warrenchen reference 實作**warrenchen 對 KL630/KL730 完全沒寫 `/firmware/legacy-upgrade/kl630|730` endpoint、bridge.py 對 KL630/KL730 升降版要自己根據 KneronPLUS SDK 推design risk 高)
2. **KneronPLUS wheel 三平台版本不一致**macOS/Linux 是 2.0.0、Windows 是 3.1.2、A 階段要開 KL630/KL730 升降版必先處理三平台 wheel 統一(額外 1-1.5 人天 + KL520/KL720 regression 風險)
3. **無實機 verify 路徑**A 階段不適合冒進、B 階段 M9-10 啟動時 KL630/KL730 實機已就位再驗 `update_kdp_firmware_from_files` 對 KL630/KL730 是否走同一條 flash 寫入路徑
**生效條件**Architect 互審補充、M9-10 啟動前評估):以下任一條件成立 → M9-10 KL630/KL730 升降版可進入;任一條件不滿足 → 維持「只做 FW 偵測 / 不開升降版」:
- **條件 a**KneronPLUS Python wheel 對 product_id 0x0630/0x0730 有 `kp.core.update_kdp_firmware_from_files` 的 dispatch或 ctypes-level `lib.kp_update_kdp_firmware_from_files` 對 KL630/KL730 確認可用)
- **條件 b**:策略 Ybuild-time 解壓 .tar → 取得 `extracted/fw_scpu.bin` / `fw_ncpu.bin`)可正常 produce 兩個 .bin 路徑、且 SDK 接受餵這兩個 .binM9-8 已驗)
- **條件 c**升級驗證階段、firmware 字串可穩定回讀(與 KL520/KL720 相同機制)
任一條件不成立 → KL630/KL730 維持「只做 FW 偵測 only」、UI 顯示版本字串但無升降版動作 + 卡片標「升降版即將支援、目前僅顯示版本資訊」提示。最終決定回 Orchestrator 派 PM 微調 PRD見 §14.3 O-FW-2
---
## 5. 手動降版面向一般使用者user research 假設
### 5.1 為什麼不藏 dev mode
使用者在 2026-05-24 明確決定「手動降版面向一般使用者」、不只 dev mode。理由PM 推測 + 與使用者對話確認):
| 假設 | 支持證據 |
|------|---------|
| H1一般使用者也會遇到「model 在新 FW 上跑不出結果」 | progress.md L93 Error 15 經驗已暗示 firmware 版本對推論結果有影響 |
| H2visionA-local 的目標使用者P1 FAE、P2 開發者)技術水平夠高,能讀懂警告 | R5 Persona 定義 P1 是 FAE、P2 是「外部開發者」,都不是純末端 consumer |
| H3藏在 dev mode 反而會讓真正需要降版的人找不到 | 內部 FAE 找不到 = 客戶現場 demo 失敗 |
| H4safety net二次確認 + 字面字串 + 警告語)足以擋住 90% 誤觸 | Architect research 42 §6 風險矩陣評估後殘餘風險低 |
### 5.2 user research 待驗證項
| # | 項目 | 驗證方式 | 階段 |
|---|------|---------|------|
| UR-1 | 一般使用者是否會誤把「切換 FW 版本」當無害操作 | Beta 測試或 5 人 usability test | B 階段開發完成後 |
| UR-2 | 二次確認字串「DOWNGRADE」是否足以擋誤觸 | 同上 | B 階段開發完成後 |
| UR-3 | 「降版」這個詞是否會嚇跑使用者 | A/B 測試「切換 FW 版本」vs「降版」文案 | B 階段開發完成後 |
| UR-4 | brick 事件發生率(殘餘風險實測) | 上線後 6 個月追蹤客服回報 | Post-launch |
→ B 階段上線時、UR-1/UR-2/UR-3 視機會做小規模測試UR-4 是長期追蹤項。
### 5.3 情境分析
Architect research 42 §1.1 列出 5 個一般使用者會降版的情境(中、低頻率):
| 情境 | 頻率 | 處理 |
|------|------|------|
| 「我的舊 model 在新 FW 上跑不出結果」 | 中 | 提供降版回 v2.1 選項 |
| 「跟某個 third-party tool 不相容」 | 低 | 提供降版回 KDP1 選項(僅 KL520 |
| 「我升錯了、想回到原本狀態」 | 低 | 提供「降版回上次版本」選項 |
| 「測試環境需要特定版本」 | 中 | 提供降版到任意 bundled 版本 |
| 「就是想試試看」 | 中 | 用警告語勸阻、但不阻止 |
---
## 6. Bundle 策略與安裝包衝擊
### 6.1 使用者拍板的保守策略
| Chip | current | 額外 bundled | 合計(每 chip|
|------|---------|------------|--------------|
| KL520 | v2.2.0 (~100KB) | v2.1.0 (~100KB) + kdp1 (~90KB) | ~290KB |
| KL720 | v2.2.0 (~250KB) | v2.1.0 (~250KB) | ~500KB |
| KL630 | SDK-v2.5.7 (~3MB) | + extracted (~3MB) | ~6MB |
| KL730 | SDK-v1.3.0 (~4MB) | + extracted (~4MB) | ~8MB |
| **合計** | | | **~15MB**(保守估計),實際 ~7MB因 KL630/KL730 解壓策略未定,可能不需 extracted |
### 6.2 多版本目錄結構(選項 C
使用者拍板選 C — `firmware/<chip>/{v2.2.0,v2.1.0,kdp1}/` + `CURRENT_VERSION` 單行檔Architect research 42 §3.2 / §3.3)。
理由:
- 跨平台無 symlink 風險vs 選項 A symlink
- 節省空間vs 選項 B 實體副本)
- 架構乾淨清晰
### 6.3 安裝包大小衝擊
- 既有 dmg163MB
- A 階段:+0KB → 163MB
- B 階段(保守):+7MB → **~170MB**
→ 在 PRD v2.1 §6.2 「macOS dmg 目標 ≤ 185MB / 上限 ≤ 220MB」範圍內無需 PRD §6.2 數字調整。
**使用者體驗層面**(吸收 Design P-LOW-2+7MB 對下載 / 安裝時間影響:寬頻 ~50MB/s → +0.14 秒(無感)/ 一般 4G hotspot ~5MB/s → +1.4 秒(無感)/ 慢速 wifi ~1MB/s → +7 秒(有感但可接受、屬罕見場景)
### 6.4 不做線上更新通道
使用者拍板「不做線上更新通道、所有 firmware 內嵌」progress.md 2026-05-24
→ 不實作 OTA / 不做 firmware 從遠端 fetch。所有可用版本就是 bundle 內的版本。
→ 風險:未來 Kneron 出新 firmware 版本時必須打新安裝包才能用(接受此 trade-off因為使用者明確要 offline-first
---
## 7. 成功指標
### 7.1 主要指標(與 PRD v2.1 §1.4 北極星指標掛鉤)
| 指標 | baseline | A 階段目標 | B 階段目標 |
|------|---------|-----------|----------|
| **5 分鐘首次推論達成率**PRD v2.1 北極星)| 假設受 firmware 問題影響 ~10% | **+ 5pp**(從「拿到舊 dongle 完全不能用」變「+ 30 秒升級即可用」) | 維持 |
| **拿到舊 dongle 後完成首次推論的中位時間** | 不可估算(需重新出貨)| **≤ 5 分鐘**(自動升級流程內) | 維持 |
### 7.2 次要指標
| 指標 | 目標 | source |
|------|------|--------|
| FW 升級成功率A 階段KL520 + KL720| ≥ 95% | log 統計(非自動 telemetry、技術支援彙整|
| FW 降版成功率B 階段KL520 + KL720| ≥ 95% | 同上 |
| FW 升降版平均時長(護欄)| KL520 ≤ 60s / KL720 ≤ 200s / KL630/KL730 待 M9-10 強驗證校正 | log 統計 |
| 一般使用者誤觸降版發生率B 階段上線後 3 個月)| ≤ 1% | 客服回報統計 |
| Brick 事件B 階段上線後 6 個月) | ≤ 0.1%< 1 / 1000 dongles | 客服回報 |
**注意(吸收 Architect §1.2**v2.2 階段大部分 FW 指標**靠客服回報 + log 統計、非自動上報**;自動 telemetry 是未來 feature、不在本 PRD scope。
### 7.2.1 體驗指標(吸收 Design P-MID-1
「以下指標補強 §7.2 純技術指標、量測使用者體驗層面是否健康」:
| 指標 | 目標 | source / 量測方式 |
|------|------|------|
| **升級任務完成率**modal 開啟 → 升級成功 toast| ≥ 85% | log 統計modal-opened 事件 vs upgrade-success 事件配對)|
| **二次確認 modal 中途放棄率**B 階段降版) | 50-70%(合理區間)| log 統計confirm-modal-shown vs token-typed-and-confirmed 配對)|
| **Devices 頁 FW badge 點擊率**(只算紅 / 黃 badge| ≥ 30% | 前端 click 事件統計 |
| **升級失敗後重試率**(首次失敗後 5 分鐘內重試比例)| 視底線、追蹤即可 | log 統計 |
| **B 階段 SUS 分數**5 人 usability test| ≥ 65 | Beta 階段 usability test、見 §5.2 UR-1~3 |
**解讀準則**
- 二次確認中途放棄率 < 50% 暗示UX 不夠恐嚇應加強警告
- 二次確認中途放棄率 > 70% 暗示「嚇跑使用者」(應減弱警告)
- FW badge 點擊率 < 30% 暗示使用者沒注意到要升級應加強顯著性 / IA
### 7.3 護欄指標(不可惡化)
| 指標 | 上限 |
|------|------|
| dmg 安裝包大小 | ≤ 220MBPRD v2.1 §6.2 上限)|
| FW 升降版期間 server 是否進 Error state | 不可device 層失敗、與 server 解耦)|
| FW 升降版期間其他 device 是否能正常推論 | 可以device 之間互不影響)|
| 啟動時間 | ≤ 60 秒PRD v2.1 AC-1.3FW 升降版不在啟動 pipeline 內、無影響)|
---
## 8. 風險與已知限制
承前 Architect research 30 + 40 + 41 + 42 的風險清單PM 視角彙整為 **R-FW-1 ~ R-FW-12**(不重寫技術細節,引用 research
### 8.1 R-FW-1 ~ R-FW-7A + B 階段共通,來自 Architect research 30
| ID | 風險 | 可能性 | 影響 | 等級 | 緩解 | 詳見 |
|----|------|-------|------|------|------|------|
| R-FW-1 | KL520 reset bug 再現(升級後 device re-enumerate 不穩定)| 中 | 中 | P2 | 升級後等 5-8 秒 + 主動 rescan承襲 2026-04-21 修法 | research 30 §3 |
| R-FW-2 | KneronPLUS Python wheel 版本對 update API 支援度不足 | 低 | 高 | P1 | **M9-6 弱驗證已確認** 3.1.2 wheel 對 `update_kdp_firmware_from_files` 完整支援warrenchen 使用中)、剩 macOS/Linux 2.0.0 wheel enum 待 35 分鐘強驗證;策略 Y解壓 .tar 取 .bin唯一可行解 | research 30 §4 + 40 §5 + **55-m9-6-weak-validation-result.md** |
| R-FW-3 | bridge.py `kp.core.update_kdp_firmware_from_files` 在 macOS x86_64 行為未驗證 | 低 | 中 | P2 | **M9-6 弱驗證**API 簽名與 warrenchen 一致、預期 macOS/Linux 2.0.0 + Windows 3.1.2 KL520/KL720 升級可用M9-1 開發前 backend agent 跑 35 分鐘強驗證、若 enum 不存在則升 wheel牽動三平台 wheel 統一) | research 30 §4 + **55-m9-6-weak-validation-result.md** |
| R-FW-4 | 升級 timeout 設定不合理(太短 = 誤判失敗、太長 = 卡 UI| 低 | 中 | P3 | 採 warrenchen 經驗值KL520 30s / KL720 180s+ progress 推播提供使用者感知 | research 30 §5 |
| R-FW-5 | 打包 Kneron 官方 firmware 是否合法(與 R5-B4 預置模型授權同性質)| 高 | 高 | **P0**(發佈 gate| 使用者明示「先不管授權、發佈前再評估」、PRD 標註為未解決問題§9| research 00 §1.7 + 40 §7 + 42 §7 |
| R-FW-6 | 既有 `flash/` 模組名稱混淆load model 到 RAM vs 燒 firmware| 低 | 低 | P3 | 新建 `firmware/` 模組、保持 `flash/` 原意Architect research §2.4| research 00 §2.4 |
| R-FW-7 | 升級失敗 device 進入 unknown state既不能用舊 firmware、新 firmware 也沒寫成功)| 低 | 高 | P1 | bridge.py 失敗時不切換 device.firmware 字串、UI 提示重新插拔Plan B技術支援 SOP 用 DFUT.exe 救磚(不打包進 app | research 30 §6 + 42 §6.3 |
### 8.2 R-FW-8 ~ R-FW-12B 階段新增,來自 research 40 + 42
| ID | 風險 | 可能性 | 影響 | 等級 | 緩解 | 詳見 |
|----|------|-------|------|------|------|------|
| R-FW-8 | KneronPLUS SDK 對 KL630/KL730 API 行為不可預測 | 高 | 中 | P1 | M9-6 SDK 驗證(與 A 階段平行進行progress.md 2026-05-24 拍板)| research 40 §5 |
| R-FW-9 | .tar 解包對安裝包大小衝擊(如果採策略 X build-time 解壓 + bundle| 低 | 低 | P3 | 採策略 Y 或 Z 平衡空間 vs 速度M9-6 結論定)| research 41 §3 |
| R-FW-10 | KL630/KL730 沒有 Loader mode 概念(與 KL520 USB Boot 流程不同)| 中 | 中 | P2 | bridge.py `handle_connect()` 必須 chip-aware 分流(既有 fall-through 到 KL520 是 bug、會連不上| research 40 §3 |
| R-FW-11 | 一般使用者誤觸降版導致 brick | 高 | 高 | **P1** | 多層 safety netUI 警告 + 二次確認字串 + driver guard + 進行中 bannerresearch 42 §5/§6 + 本檔 AC-FW-2.5 ~ 2.10| research 42 §6 |
| R-FW-12 | 多版本管理 UX 複雜度(使用者看不懂 v2.2.0 vs v2.1.0 vs KDP1| 中 | 中 | P2 | **已具體落地**Design v2.2 §3.3 accordion + radio list + 每個版本 displayName 後綴current/older/legacy+ §9 i18n 涵蓋KDP1 額外紅色警告 banner | research 42 §5.3 + Design v2.2 §3.3 / §9 |
| **R-FW-13** | **KneronPLUS wheel 三平台版本不一致**macOS/Linux 2.0.0 vs Windows 3.1.2| 高 | 中 | **P1** | **A 階段**:先在 macOS/Linux 強驗證 2.0.0 wheel 是否含 KL520/KL720 update APIM9-1 啟動前 35 分鐘)、預期可用、若不可用先升 Windows 對齊;**B 階段 M9-10 啟動前必須升 macOS/Linux 到 3.1.2**(含 KL630/KL730 enum+ 跑 KL520/KL720 三平台 E2E 回歸M9-13 | **55-m9-6-weak-validation-result.md** 發現 6 |
| **R-FW-14**Frontend 防誤觸補強) | 使用者輸入「downgrade」小寫繞過確認 | 低 | 高 | P2 | Frontend 用嚴格 `===` 比對、**不用 `.toUpperCase()`**;後端 API 同時雙重比對M9-12 Reviewer 必檢查 | Design v2.2 §12.2 R-FW-11.5 自提 + Architect §2.10 同意 |
### 8.3 與既有架構的衝突已釐清項
| 衝突點 | 結論 | 詳見 |
|--------|------|------|
| **watchServer Error state**TDD v2.1 §8| FW 升降版失敗屬 device 層、與 server 層 watchServer 解耦、不觸發 Error state | research 30 §2.1 |
| **R5-E 60s 啟動上限** | FW 升降版是使用者主動觸發、不在啟動 pipeline 內、無關 | research 30 §2.3 |
| **KL520 reset bug fix**2026-04-21| 升級成功後 device re-enumerate 必須等 5-8 秒 + 主動 rescan、不立即 reconnect | research 30 §2.2 |
| **既有 `flash/` 模組命名包袱** | 不改檔名(本任務範圍外)、新文件命名用 `kneron_*``device_firmware_*` | research 30 §2.5 |
---
## 9. 未解決問題(彙整給 Orchestrator
| # | 問題 | 性質 | 處理建議 |
|---|------|------|---------|
| Q-FW-1 | Kneron firmware redistribution 授權(含舊版 v2.1.0 / KDP1 | **發佈前 gate**(與 R5-B4 預置模型授權同性質)| 使用者明示「先不管授權、發佈前再評估」PRD 留紀錄。建議發佈前 3-6 個月與 Kneron 取得明確書面授權,授權範圍含 current + 舊版v2.1.0 / KDP1+ KL630/KL730 firmware tar |
| Q-FW-2 | KneronPLUS Python wheel 對 KL630/KL730 update API 支援度 | **B 階段啟動 gate** | M9-6 SDK 驗證為先(與 A 階段平行)、結論定 B 階段 scope若不支援、AC-FW-3.5 啟動「降級為 FW 偵測 only」 |
| Q-FW-3 | 「降版」這個詞是否用於 UI 文案 | **✅ 已決定(吸收三方互審 MJ-D1** | UI 文案統一中性(分頁名稱「韌體管理」、按鈕「切換到此版本」),**不用「降版」字眼**;程式碼 / log / API 內部用語可保留 `downgrade`、API endpoint 仍是 `POST .../firmware/downgrade`。對齊 Design v2.2 §2.1 + Architect 同意 |
| Q-FW-8| 「DOWNGRADE」確認字串是否 i18n 化 | **✅ 已決定(吸收 Design D-M3 + Architect §2.2** | 保留英文字面 `"DOWNGRADE"`、是 API contract 不 i18n 化UI 提示文字 i18n 化(如「請輸入 DOWNGRADE 確認」中文 + 「Type DOWNGRADE to confirm」英文但 input 比對的字串跨語系一致 |
| Q-FW-4 | 一般使用者降版的 user research 假設驗證時機 | **B 階段 post-launch** | UR-1/UR-2/UR-3 在 Beta 階段做 5 人 usability testUR-4 brick 事件追蹤 6 個月§5.2|
| Q-FW-5 | KL520 / KL720 / KL630 / KL730 升降版預估時間是否準確 | **開發階段實測** | M9-5A/ M9-13B三平台實機驗證時校正 |
| Q-FW-6 | progress.md R5-Q9 行號 L776 vs L854 不一致 | **✅ 已決定** | 文件追溯以「progress.md 第二輪使用者決策 Q9 條目」描述式引用為準、不引用行號(行號因 progress.md 多次追加會浮動。決策本身明確2026-04-11 第二輪 Q9 「韌體燒錄 flash → B 砍掉」、本 PRD §2.1 已修正 |
| Q-FW-7 | 升級失敗後的「救磚 SOP」是否需要在 UI 內提示 | **B 階段 Design 決定** | 目前傾向UI 提示「請重新插拔裝置後重試」+「複製錯誤訊息」+ 技術支援聯絡資訊research 42 §6.3 SOP 留內部 wiki|
---
## 10. 工時與 Milestone
### 10.1 A 階段M9-1 ~ M9-5、共 5 人天PM 拆法、Architect 互審同意採此版本)
| # | Milestone | 工時 | 依賴 | 平行性 |
|---|-----------|------|------|-------|
| M9-0 | 開發前強驗證backend agent、35 分鐘macOS/Linux 跑 `python3 -c "import kp; print(kp.ProductId.KP_DEVICE_KL630)"`、判斷 2.0.0 wheel 是否含 KL630/KL730 enum + `tar -tvf` inspect firmware .tar | < 0.1 人天內含| | M9-1 之前必跑 |
| M9-1 | bridge.py 新增 `firmware_upgrade` handlerKneronPLUS C API、策略 Y 唯一可行解)| 1 人天 | M9-0 | 獨立 |
| M9-2 | Go driver + service`UpgradeFirmware()` + `server/internal/firmware/service.go` + safety guards | 1 人天 | M9-1 | — |
| M9-3 | API handler + WebSocket progress room`FirmwareProgress.reason` 細分 stage、Elapsed/ETA 欄位、graceful shutdown 拒絕邏輯 hook 點)| 0.5 人天 | M9-2 | — |
| M9-4 | 前端 Devices 頁 FW badge4 色含灰色狀態未知)+ 升級按鈕 + progress modal + 6 個 component-level design tokens 落地(含 a11y 對比驗證)| 1.5 人天 | M9-3 | 與 M9-1/M9-2/M9-3 部分平行mock API|
| M9-5 | 三平台實機驗證macOS / Windows / Linux × KL520 + KL720 完整升級 E2E| 1 人天 | 全部前置完成 | — |
| **A 合計** | | **5 人天** | | |
### 10.2 B 階段M9-6 ~ M9-13、共 10.5 人天PM 拆法、Architect 互審同意採此版本)
| # | Milestone | 工時 | 依賴 | 平行性 |
|---|-----------|------|------|-------|
| M9-6 | KneronPLUS SDK 對 KL630/KL730 + .tar firmware Python API 驗證(**弱驗證已完成**:見 `55-m9-6-weak-validation-result.md`;強驗證須等 KL630/KL730 實機)| 1 人天(含弱驗證 0.5 + 強驗證 0.5| — | **與 A 階段平行**2026-05-24 使用者決策)|
| M9-7 | Driver 擴展處理 product_id 0x0630/0x0730 + chip-aware connect 分流(含 KL630/KL730 firmware 載入分支)| 1.5 人天 | M9-6 | — |
| M9-8 | bridge.py 處理 .tar firmware**策略 Y 唯一可行解、build-time 解壓**;策略 Z 已被弱驗證排除)| 1.5 人天 | M9-7 | — |
| M9-9 | 多版本目錄結構重整A 階段檔案搬到 `<chip>/v2.2.0/` + 加 `CURRENT_VERSION`+ bridge.py 升級 `_resolve_firmware_paths_versioned()` | 1 人天 | A 階段完成 | — |
| M9-10 | **KL630/KL730 升級 / 降版 driver method 實作**AC-FW-3.5 生效條件評估在此 milestone 啟動時跑、無 warrenchen reference 實作、design risk 較高)| 1.5 人天 | M9-8 + M9-9 + M9-6 強驗證完成 + macOS/Linux wheel 升 3.1.2R-FW-13| — |
| M9-11 | 多版本降版後端API + bridge.py + driver guards + graceful shutdown 拒絕落地)| 1.5 人天 | M9-9 | — |
| M9-12 | 降版 UISettings 韌體管理面板 + 二次確認 modal + 進行中 UI + Wails 控制台關閉攔截 modal| 2 人天Frontend 1 + Design 1| M9-11 | Design + Frontend 平行 |
| M9-13 | B 階段三平台實機驗證 + KL520+KL720 wheel 升級 regression + Beta usability testUR-1/UR-2/UR-3| 1 人天 | M9-7 ~ M9-12 全部 | — |
| **B 合計** | | **10.5 人天** | | |
### 10.3 合計
- A + B = **15.5 人天**
- 與 Architect research 結論一致
---
## 11. 涉及檔案 / 模組(從 Architect research 30 + 40 摘要)
### 11.1 新建模組
```
server/internal/firmware/ ← 新模組(升降版核心)
├── service.go ← FirmwareService 提供 Upgrade/Downgrade/List
├── versions.go ← 版本管理 + 比較邏輯isOlderVersion()
└── safety.go ← driver guard helpers不跨晶片 / 不升版偽裝 / 不 no-op
```
### 11.2 既有檔案修改
| 檔案 | 修改範圍 | 階段 |
|------|---------|------|
| `server/scripts/kneron_bridge.py` | 新增 handler`firmware_upgrade` / `firmware_downgrade` / `firmware_list_versions` | A + B |
| `server/internal/driver/kneron/kl720_driver.go` | 新增 method`UpgradeFirmware` / `DowngradeFirmware` / `ListFirmwareVersions` / `GetCurrentFirmwareVersion` | A + B |
| `server/internal/driver/types.go` 或同等 | DeviceDriver interface 擴展 + `FirmwareVersion` / `FirmwareProgress` struct | A |
| `server/internal/api/handlers/device_handler.go` 或新檔 | 新 endpoint`GET /api/devices/:id/firmware*` / `POST .../upgrade` / `POST .../downgrade` | A + B |
| `server/internal/api/router.go` | 註冊新路由 | A + B |
| `server/internal/ws/hub.go` | 註冊 `firmware:<deviceId>` WebSocket room | A |
| `server/scripts/firmware/` | 重整目錄結構為 `<chip>/<version>/` + `CURRENT_VERSION` | BM9-9|
### 11.3 前端新增 / 修改
| 檔案 | 修改範圍 | 階段 |
|------|---------|------|
| `frontend/components/devices/device-card.tsx` 或同等 | 加 FW badge綠/黃/紅)+ 升級按鈕 | A |
| `frontend/components/devices/firmware-upgrade-modal.tsx`(新) | progress modal | A |
| `frontend/pages/settings/firmware.tsx` 或同等(新) | Settings → 韌體管理面板 | B |
| `frontend/components/firmware/version-switch-modal.tsx`(新) | 二次確認 modal | B |
| `frontend/components/firmware/downgrade-progress-modal.tsx`(新) | 進行中 UI | B |
| i18n keys | +46 個research 42 §5.6 估算) | B 為主 |
### 11.4 文件新增
| 檔案 | 角色 | 階段 |
|------|------|------|
| `docs/autoflow/04-architecture/adr/ADR-001-firmware-management.md` 或同等 | Architect 留 R5-Q9 翻案紀錄 + 技術決策 | A 啟動前 |
| `docs/autoflow/04-architecture/TDD-v2.md` §5.4 firmware 子節 | Architect 補 TDD v2.2 | A 啟動前 |
| `docs/autoflow/03-design/design-spec-v2.md` 對應子節 | Design Agent 補設計規格 v2.2 | A 啟動前 |
| `docs/autoflow/02-prd/features/feature-firmware-management.md`(本檔)| PM 補 PRD v2.2 | 進行中 |
| `docs/troubleshooting/brick-recovery.md`(內部 wiki| 技術支援 SOPDFUT.exe 救磚流程,**不打包進 app**| 上線前 |
---
## 12. 與既有 R5 決策的關係
| R5 條目 | 本任務關係 |
|---------|----------|
| **R5-Q9砍 flash** | **翻案**§2透過範圍切割避開原始擔憂 |
| **R5-B4Kneron 預置模型 redistribution** | 同性質問題擴展 — 本任務的 firmware bundle 與 R5-B4 同樣是「打包 Kneron 智財」、發佈前須一起跟 Kneron 取得授權§9 Q-FW-1|
| **R5-E60s 啟動 + perceived performance** | 無關 — FW 升降版不在啟動 pipeline 內research 30 §2.3|
| **R5-D1Server 崩潰 OS 通知)** | 不擴展 — FW 升降版失敗屬 device 層、不發 OS 通知(與 device toast 一致PRD v2.1 §AC-3.7 + R4-8 策略)|
| **R5-1產品定位** | 對齊 — FW 管理 UI 住瀏覽器 Web UIDevices 頁 + Settings 韌體管理)、不住 Wails 控制台 |
| **R5-3砍 tray** | 無衝突 |
| **R5-6ffmpeg LGPL** | 無關 |
---
## 13. 風險:本 feature 對既有功能的影響
承前 R5「既有專案的每一步都需要特別小心、避免破壞現有功能」原則本 feature 的潛在影響:
| 影響範圍 | 風險 | 緩解 |
|---------|------|------|
| 既有 KL520 / KL720 推論流程 | 升級期間 device 鎖住 `StatusUpgrading`、其他操作會被擋 | 預期行為、UI 清楚告知 |
| 既有 `flash/service.go` (load model 到 RAM)| 命名 vs 新 `firmware/` 模組可能混淆 | 嚴格保持 `flash/` 原意、新模組獨立目錄research §2.4|
| KL630 / KL730 連線(既有 fall-through 到 KL520 bug| 本任務 M9-7 必修,否則 KL630/KL730 連不上 | M9-7 是 B 階段必做、driver 改動需 Reviewer 把關 |
| 既有 `kneron_bridge.py handle_connect()` | 新增 handler 不影響既有 connect 流程(獨立 handler| Architect 確認 |
| 安裝包大小(既有 dmg 163MB| +7MB → ~170MB仍在 PRD v2.1 §6.2 上限內(≤ 220MB| 已在範圍內,無需 PRD §6.2 數字調整 |
---
## 14. 三方互審回饋處理紀錄v2.2 → v2.2.1
### 14.1 給 Design Agent — **全部已處理**
- **D-FW-1**(降版入口位置):✅ Design v2.2 §2 採 Settings 第 3 分頁「韌體管理」、與 PM 對齊
- **D-FW-2**(「降版」用詞):✅ **已決定**UI 統一中性「韌體管理」/「切換到此版本」、不用「降版」字眼;內部 / API / log 保留 `downgrade`Q-FW-3 已落定、見 §9
- **D-FW-3**(防誤觸機制):✅ Design v2.2 §6.1 採「DOWNGRADE」嚴格字面輸入嚴格 `===` 比對、不接受小寫 / 全形 / 半形空白、雙重保險)
- **D-FW-4**FW badge 三色閾值):✅ AC-FW-1.1 已明示 4 色閾值(綠 / 黃 / 紅 / 灰、Design v2.2 §4.2 對齊
- **D-FW-5**6 階段中英雙語文案):✅ **採 Design 命名**preparing / loading / flashing / verifying / done / error中文文案準備中 / 載入引導程式 / 寫入韌體 / 驗證中 / 完成 / 失敗)、見 AC-FW-1.2
- **D-FW-6**(持久 banner 視覺):✅ Design v2.2 §6.3 已落地
### 14.2 給 Architect Agent — **大部分已處理**
- **A-FW-1**R5-Q9 行號):✅ **已決定**:本檔 §2.1 改為描述式引用、Q-FW-6 已落定
- **A-FW-2**driver safety guards✅ Architect TDD v2.2 §2.1 + §3.3 已對應寫具體 Go interface + `_validate_downgrade_request`
- **A-FW-3**(三平台實機驗證):✅ Architect TDD §11.4 已涵蓋
- **A-FW-4**KL630/KL730 wheel 支援度):✅ **已決定**(吸收 M9-6 弱驗證AC-FW-3.5 延後到 B 階段 M9-10、含 a/b/c 生效條件、見本檔對應段落
- **A-FW-5**(模組路徑 `server/internal/firmware/`):✅ Architect TDD v2.2 §2.1 對齊
- **A-FW-6**(與 R5-B4 授權合併):✅ Architect ADR-001 §Compliance 標 `[ ] 與 Kneron 取得 firmware redistribution 授權`、§Related 已連結 R5-B4
### 14.3 給 Orchestrator — **追蹤項**
- **O-FW-1**(與 R5-B4 release blocker 合併):⏳ 待 Orchestrator 統一決定。PM 建議合併(都是 Kneron 智財 + 都是發佈前 gate、但保留各自具體 scopefirmware vs model 兩個 license 範圍)
- **O-FW-2**M9-6 強驗證 + AC-FW-3.5 觸發條件):⏳ M9-10 啟動時評估 a/b/c 三條件、任一不滿足 → Orchestrator 派 PM 微調 PRDKL630/KL730 維持 FW 偵測 only / 不開升降版)
### 14.4 互審 Minor 處理紀錄
| # | 來源 | 內容 | 處理 |
|---|------|------|------|
| M-A1 | Architect TDD §10 R-FW-5 等級「待釐清」| 統一用 P0/P1/P2/P3 分級 | ✅ Architect 修 TDDPM 不處理)|
| M-A2 | Architect TDD §11.3 缺 KL720 timeout 上界測試 | 加「KL720 升級實測時長 ≤ 200s」測試案例 | ✅ Architect 修 TDD |
| M-A3 | Architect §3.1 API 缺 `notes` / `estimatedDurationSec` 欄位 | 補在 `GET /firmware/versions` response | ✅ Architect 修 TDD |
| M-A4 | TDD §6.2 KL520 KDP1 pid=0x0200 vs KL720 衝突 | Architect 確認後對齊 PRD §1.1 | ⏳ Architect 修 TDDPM 等對齊結果)|
| M-D1 | Design §9 i18n keys 52 vs PM 估 46 | 採 Design 實際值 52 keys | ✅ 本檔 §11.3 已記「+46 個estimate→ 實際 52 個Design v2.2 §9」 |
| M-D2 | Design §3.3 「accordion + radio list」vs PRD「dropdown」 | AC-FW-2.3 改「radio list 或同等選擇 UI」 | ✅ 已處理 |
| M-D3 | DOWNGRADE 字串 i18n 化 | Q-FW-8 已落定保留英文字面 | ✅ 已處理 |
| P-MID-1 | 缺體驗指標 | 補 §7.2.1 體驗指標 5 條 | ✅ 已處理 |
| P-MID-2 | PRD 用詞混用「降版」 | Q-FW-3 落定 + US-FW-2 拆 a/b + framing 共識段落 | ✅ 已處理 |
| P-MID-3 | KL520 升級時間 30s vs ≤ 60s | AC-FW-1.7 明示「~30 秒實測 + ≤ 60s 護欄上界」| ✅ 已處理 |
| P-MID-4 | badge 閾值不清 | AC-FW-1.1 補 4 色定義(含灰色狀態未知)| ✅ 已處理 |
| P-LOW-1 | 缺「使用者怎麼發現要降版」US | 標為 v2.3 未來迭代Workspace inference 失敗時 hint| ⏳ 暫不處理 |
| P-LOW-2 | Bundle 安裝時間衝擊 | §6.3 補一句 | 見 §6.3 |
| P-LOW-3 | R-FW-12 緩解過抽象 | R-FW-12 改「已具體落地」+ 引用 Design v2.2 §3.3 / §9 | ✅ 已處理 |
| F1-F7 | Architect 自查 7 項必修 | 由 Architect 修 TDD、PM 不直接處理 | ⏳ Architect 端處理中 |
---
## 變更紀錄
| 版本 | 日期 | 作者 | 變更 |
|------|------|------|------|
| v2.2-draft | 2026-05-24 | PM Agent | 初稿;翻案 R5-Q9A + B 兩階段範圍定義;多層 safety net for 一般使用者降版;風險 R-FW-1 ~ R-FW-12 彙整自 Architect research 30/40/41/42§14 列出 14 個 Design / Architect / Orchestrator 待互審項 |
| v2.2.1 | 2026-05-25 | PM Agent | 吸收三方互審PM Self / Design / Architect+ M9-6 弱驗證結果。**主要變更**①stage 命名統一採 Designpreparing/loading/flashing/verifying②M9 工時表拆法採 PMArchitect 已同意③AC-FW-3.5KL630/KL730 升降版)延後到 B 階段 M9-10、補生效條件 a/b/c、A 階段 KL630/KL730 只做 FW 偵測無升降版按鈕④ADR-009 → ADR-001 編號統一⑤R5-Q9 行號改描述式引用、不引用脆弱行號⑥US-FW-2 拆成 US-FW-2a一般使用者+ US-FW-2b進階使用者、UI 文案 framing 統一中性「韌體管理」「切換到此版本」⑦補 §7.2.1 體驗指標 5 條(升級任務完成率 / 中途放棄率 / badge 點擊率 / 重試率 / SUS 分數)⑧新增 R-FW-13 wheel 三平台版本不一致 + R-FW-14 大小寫繞過⑨補 AC-FW-1.9 graceful shutdown 拒絕⑩badge 4 色定義補灰色狀態未知⑪§14 標記哪些已解決 / 哪些 ⏳ 追蹤中。詳見本檔 §14.4 互審 Minor 處理紀錄 |

View File

@ -0,0 +1,200 @@
# PM 互審報告Architect TDD v2.2 + Design v2.2 firmware
> 審查日期2026-05-24
> 審查者PM Agent
> 對應 PRD`.autoflow/02-prd/features/feature-firmware-management.md` v2.2-draft
> 審查範圍:
> - `.autoflow/04-architecture/v2/firmware-management.md`TDD v2.2466 行)
> - `.autoflow/04-architecture/adr/ADR-001-firmware-management.md`152 行)
> - `.autoflow/03-design/v2/firmware-management.md`Design v2.2920 行)
---
## TL;DR
**整體結論:通過 with Minor + 少數 Major**。三份文件對 PRD 的 4 個 user storyUS-FW-1 ~ US-FW-3與成功指標的覆蓋度極高、R5-Q9 翻案的範圍切割與 PRD 完全一致、ADR-001 寫得清楚可追溯。
但有 **3 個 Major 問題**需要 Architect / Design 修改後才能 merge
1. PRD 寫了 US-FW-2 包含「進階使用者切換 FW 版本」的 user story含「就是想試試看」「測試環境」等情境但 Design 文案策略卻是「**對一般使用者過於負面**、用『版本切換』中性詞」——這兩個 framing 的隱含 persona 不一致、PM 需與 Design 對齊
2. ADR-001 編號錯誤PRD 引用的是 ADR-009`feature-firmware-management.md` §2.5 / §11.4),但實際檔名是 ADR-001——需統一
3. R5-Q9 行號爭議L776 vs L854PM 在 PRD 明示要 Architect cross-check、但 ADR-001 + TDD 沒有處理這點
另外有 **8 個 Minor**建議改但不阻擋、4 個建議性回饋。詳見下方分項。
---
## 對 Architect TDD 的審閱
### 🟢 通過項
1. **§1.1 痛點對應 PRD §1.1 完全一致**3 個列出的痛點(拿到 KDP1 不能用 / KL520 Error 15 / KL630/KL730 偵測不到)與 PRD §1.1 + §1.2 對齊
2. **§1.2 範圍邊界對應 PRD §2.4 完全一致**:「升級到官方 KDP2 標準版本」做、「使用者燒任意 model 到 flash」繼續砍——R5-Q9 翻案的範圍切割對齊 PRD §2.4 表格
3. **§3 API 設計覆蓋 PRD US-FW 全部 4 個 user story**
- `GET /api/devices` 衍生欄位 → AC-FW-1.1 FW badge
- `POST /api/devices/:id/firmware/upgrade` → AC-FW-1.2 升級
- `GET /api/devices/:id/firmware/versions` → AC-FW-2.3 版本列表
- `POST /api/devices/:id/firmware/downgrade` + `confirmToken` → AC-FW-2.6 / AC-FW-2.9 強制二次確認
- WebSocket room `firmware:<deviceId>` → AC-FW-1.2 progress modal
4. **§3.3 錯誤碼涵蓋 PRD 8.1 / 8.2 R-FW-1 ~ R-FW-12 對應 mitigation**6 個錯誤碼(`FW_DEVICE_BUSY` / `FW_VERSION_NOT_FOUND` / `FW_INVALID_DIRECTION` / `FW_NO_CONFIRM_TOKEN` / `FW_UPGRADE_FAILED` / `FW_UPGRADE_BRICK_RISK`)與 PRD §8.1 / §8.2 風險清單對齊
5. **§5.2 降版流程完整對應 AC-FW-2.6 ~ AC-FW-2.10**:二次確認 + DOWNGRADE 輸入 + 不可關 modal + persistent banner + driver guards 都有設計
6. **§8.1 watchServer Error state 解耦對應 AC-FW-1.8**:明示 FW 升降版失敗不觸發 server Error state、與 PRD §8.3「衝突已釐清項」一致
7. **§8.2 KL520 reset bug 對應 PRD §1.1 痛點 #2**:明示「升級成功後 needsReset=true、下次連線走完整 reset、避開 Error 15」——這正是 2026-04-21 fix 機制的延續
8. **§9 工時 15.5 人天對齊 PRD §10.3**A 5 + B 10.5 = 15.5、milestone 切分 M9-1 ~ M9-13 一致
9. **§10 風險表 R-FW-1 ~ R-FW-12 + R-TAR-1 ~ R-TAR-4 對齊 PRD §8.1 / §8.2**:除編號略有偏移外、實質內容對齊
10. **§11 測試策略涵蓋 AC-FW-1.8watchServer 不誤判)+ 既有 KL520/KL720 回歸**§11.4 明示「watchServer 機制不會把 FW timeout 誤判為 server 死亡」、且涵蓋 KL520 / KL720 既有 E2E 回歸
### 🟡 Minor不阻擋、建議改
| # | 章節 | 問題 | 建議修法 |
|---|------|------|---------|
| M-A1 | §10 R-FW-5 | 風險表內欄位 `等級` 寫「待釐清」、不是 PRD §8.1 用的「P0發佈 gate」 | 統一用 PRD 的 P0/P1/P2/P3 分級語彙、或在 §10 表頭加註「等級 = PRD R-FW 對應的優先級」說明 |
| M-A2 | §11.3 異常路徑測試 | 缺一條「升級 timeoutmock 180s 不回)」對應 AC-FW-1.7「KL720 預估 180 秒」的明確上界測試 | 加一行「KL720 升級實測時長 ≤ 200sPRD §7.2 護欄)」測試案例 |
| M-A3 | §3.1 API 端點清單 | 表格沒有列出 PRD AC-FW-2.3「版本說明文案」「預估時間」這些 metadata 從哪個 API 取 | 在 `GET /firmware/versions` 的 response 範例中明示是否含 `notes` / `estimatedDurationSec` 欄位Design §3.3 wireframe 顯示了「2024-08 發行」「與舊模型相容」等資訊、後端需提供) |
| M-A4 | §6.2 chip 判斷 | KL520 / KL720 / KL630 / KL730 product_id 都列了、但 PRD §1.1 痛點表列「KDP1 legacy pid=0x0200」、TDD §6.2 卻把 0x0200 對到 KL720——這是 KL520 KDP1 legacy 還是 KL720需與 PRD 痛點表對齊 | Architect 互審時確認0x0200 到底是 KL520 KDP1 legacyPRD §1.1)還是 KL720TDD §6.2 L322。若兩者皆是不同 product_id、PRD §1.1 痛點表應更正為「KDP1 legacy pid=0x0100」或實際正確值 |
| M-A5 | §9 M9-6 「純研究、Architect 自身產出、不過 Reviewer」 | 與 PRD Q-FW-2 「結論定 B 階段 scope」存在關聯——若 SDK 驗證結論是「不支援」、需重派 PM 微調 PRDPRD §14.2 A-FW-4但 TDD 沒寫此 reflow 條件 | §9「Reviewer 切點」段落補一條「M9-6 結論若觸發 AC-FW-3.5 降級條件,須回 Orchestrator 重派 PM 微調 PRD 後再啟動 M9-7」 |
| M-A6 | §13.1 「PM 互審注意」 | Architect 列了 4 個 PM 互審注意點、但漏列 PRD §14.2 A-FW-4 / A-FW-5 / A-FW-6 的 6 個 Architect 待回覆項——PM 不知道哪些是 Architect 已回覆、哪些待回 | §13.1 補一節「PM PRD §14.2 待回覆項對應」列出 A-FW-1 ~ A-FW-6 各自在本 TDD 哪一節已回覆(或標「未處理、留 follow-up」 |
### 🟠 Major建議改後 merge
| # | 章節 | 問題 | 建議修法 |
|---|------|------|---------|
| MJ-A1 | 整檔 ADR 編號 | PRD §2.5 + §11.4 明示「ADR-009-firmware-management」、但 Architect 實際檔名是 `ADR-001-firmware-management.md`——這是專案歷史上的第幾個 ADR與 PRD 引用差 8 號需要解釋 | 三方需對齊:(1) 改 ADR 檔名為 `ADR-009-firmware-management.md`、或 (2) PRD §2.5 / §11.4 改為引用 `ADR-001`。PM 傾向 (1)因為「009」對應 R5 第 9 個議題、語意較豐富。但若 ADR 編號規則是「按時間順序、本專案第 1 個 ADR」、那 PRD 該改為 ADR-001。需 Architect 確認 ADR 編號規則後統一 |
| MJ-A2 | §1 (整體) | TDD 沒處理 PRD §2.1 標明的「R5-Q9 行號 L776 vs L854 不一致、Architect 互審時 cross-check」 | TDD §1或新增 §1.4補一段「R5-Q9 行號釐清」:說明 Architect 確認 progress.md 當前 L854 為 R5 第二輪 Q9 條目、L776 是 research summary 引用的歷史快照行號、決策本身內容一致 |
### 🔴 Critical必須改、阻擋 merge
無。Architect TDD 與 PRD 的需求覆蓋度足以推進開發。
---
## 對 Design Spec 的審閱
### 🟢 通過項
1. **§1 定位職責對齊 PRD §3.2 + AC-FW-2.1**:明示「韌體管理在 Settings 第 5 分頁、不在 Devices 頁主流程」、避免誤觸——與 PRD §4 user story 一致
2. **§3.1 ~ §3.3 wireframe 完整覆蓋 US-FW-1 + US-FW-2**:裝置卡片 + FW badge 三色 + 「版本切換」accordion 設計符合 PRD AC-FW-1.1 / AC-FW-2.3
3. **§4 Devices 頁 FW Badge 對應 AC-FW-1.1**:綠 / 黃 / 紅三色 + tooltip + deep-link ⚙ icon、與 PRD US-FW-1 直接對齊
4. **§5 升級流程完整覆蓋 US-FW-1 全部 AC**
- §5.1 確認 modal → AC-FW-1.2
- §5.2 進度 modal 不可中斷 → AC-FW-1.5
- §5.3 6 stage 對應 → PRD AC-FW-1.2「全程顯示階段」+ TDD §4.3
- §5.4 成功 toast → AC-FW-1.3「自動關閉 + 觸發 rescan + badge 變綠」
- §5.5 失敗復原 → §7
5. **§6.1 降版二次確認 modal 對應 AC-FW-2.5 + AC-FW-2.6 + AC-FW-2.7 + AC-FW-2.8 全部**:警告語 4 條、輸入 DOWNGRADE 字串、不可外部關閉、進行中無取消按鈕——一條一條都對上 PRD 的 AC
6. **§6.2 KDP1 額外警告**:超出 PRD AC-FW-2.5 既有 4 條警告語、Design 主動加「KDP1 不支援多模型」這條——這個延伸符合 PRD US-FW-2 第 2 個情境「跟某個 third-party tool 不相容、要回 KDP1」的精神
7. **§7 失敗復原 8 種類型對應 PRD AC-FW-1.4 + AC-FW-2.8**:每種 stage 失敗都有 friendly message + 復原行動、超出 PRD 既有要求
8. **§10 A11y 對應 PRD US-FW-2 一般使用者**focus trap、aria-live、role="alertdialog"、reduced motion——這對「面向一般使用者」的承諾是必要的
9. **§11.2 新增 6 個 component-level tokens** 不是亂增、有推導表與對比比率驗證
10. **§12 R-FW 風險的 Design 對策**:對應 PRD §8.2 R-FW-11 「一般使用者誤觸降版 brick 風險」、列出 7 條 mitigation 全部對應 PRD AC 條目
11. **§14.4 給 Orchestrator 懸而未決 3 條**:對應 PRD §9 未解決問題、Design 補上 3 個 PRD 沒提到的i18n 化 / metadata 來源 / 控制台關閉攔截)——這是好的延伸
### 🟡 Minor不阻擋、建議改
| # | 章節 | 問題 | 建議修法 |
|---|------|------|---------|
| M-D1 | §9 i18n keys 52 個 | PRD §10.2 引用 research 42 §5.6 估算 46 keys、Design 實際 52 keys——多 6 keys 是合理的(含 settings.tabs.firmware + Devices badge tooltip 4 keys + 1-2 個技術資訊)| 不用改、但 PRD §10.2 應在下次補丁更新為 52 keys、或在當前 PRD 加註「實際以 Design §9 為準」 |
| M-D2 | §3.3 「版本切換」accordion | accordion 樣式對應 PRD AC-FW-2.3 「dropdown」Design 改用 radio list、§3.3 末段已說明理由(< 3 個版本a11y 友善這是好的延伸但 PRD AC-FW-2.3 文字仍寫dropdown | PRD §4 US-FW-2 AC-FW-2.3 應在下次補丁更新為radio list 或同等選擇 UI PM 採納 Design 決定後改 PRD |
| M-D3 | §6.1 「DOWNGRADE」字串 i18n 化 | Design §14.4 第 4 點懸而未決提到、PRD §9 Q-FW-3 「降版」用詞策略已說明 UI 不用「降版」字眼但 PRD 沒明示 DOWNGRADE 字串本身要不要 i18n | PRD §9 應在下次補丁新增 Q-FW-8「DOWNGRADE 字串 i18n 化」對應 Design §14.4 第 4 點 |
| M-D4 | §3.1 裝置卡片配色 | 圓點 🔴 / 🟡 / 🟢 對應 PRD AC-FW-1.1 三色 badge、但 Design 把「KDP1」+「has newer」都用紅色Design L101vs PRD AC-FW-1.1 寫「黃 = 比 current 舊但可升」、「紅 = legacy KDP1 或損毀」——Design 表格分了 3 colour 但 wireframe 圖示 KDP1 用紅、PRD 文字寫「v2.1.0」應為黃。需確認 wireframe legend 是否清楚 | Design wireframe 加註釋「KDP1 → 紅 / v2.1.0 較舊 → 黃 / v2.2.0 current → 綠」,避免讀者只看 wireframe 不看 §3.1 表格時誤解 |
| M-D5 | §11.2 token Dark 模式對比 | Design 自己標「Architect 互審重點:請確認 Dark 模式下 `legacy.fg` 用黑色對紅底對比是否真的足夠(推算 4.8:1、但實作時需用 contrast checker 工具驗證)」 | 這個是 Architect 互審的事、不是 PM 互審範圍。但 PM 確認:對比 4.8:1 達 WCAG AA≥ 4.5:1、不違反 PRD 對 a11y 的承諾。Design 可以將 §11.2 結尾「Frontend 需用 contrast checker 工具驗證」改為更明示的 AC如「實作後 Frontend M9-12 驗收必跑 axe-core / Lighthouse a11y 測試、對比比率必達 4.5:1」 |
### 🟠 Major建議改後 merge
| # | 章節 | 問題 | 建議修法 |
|---|------|------|---------|
| MJ-D1 | §2.1 「分頁名稱不能叫『降版』——對一般使用者過於負面」 vs PRD §5.1 / US-FW-2 | **這是本次互審最大的 framing 分歧**PRD §5.1 引用 architect research 42 結論、明示「手動降版面向一般使用者」、§5.3 列出 5 個情境含「就是想試試看」「測試環境」。Design §2.1 卻說「降版對一般使用者過於負面、一律稱『韌體』或『韌體管理』」——這兩個 framing 的隱含 persona 不一致PRD 把一般使用者當「有能力理解風險的進階使用者」、Design 把一般使用者當「需要保護不要看到負面詞彙的非技術使用者」。需 PM + Design 對齊「一般使用者」的具體定義 | **建議方案**PRD + Design 對齊到 Design 立場UI 文案中性、`downgrade` 只留技術文件)、因為 R5 Persona 表已明示 P1 是 FAE / P2 是「外部開發者」、但實際 dongle 也會給「跟著開發者一起使用的客戶現場 demo 觀察者」、後者並非技術人員。PRD §5.1 H3 假設「藏在 dev mode 反而讓真正需要降版的人找不到」仍成立、Design §2.1 中性文案不違反此假設。**具體修法**PRD §9 Q-FW-3 「降版這個詞是否用於 UI 文案」應從「待 Design Agent 最終決定」改為「已決定UI 用『版本切換』中性詞、技術文件保留 downgrade」並在 PRD §4 US-FW-2 敘述中將「按下『降版』按鈕」改為「按下『切換到此版本』按鈕」 |
| MJ-D2 | §3.3 第 3 個 user story「進階使用者切換 FW 版本」)的 PRD 對應 | Design §14.4 第 5 點 + §3.3 accordion 設計都暗示存在「進階使用者主動切版本」的 user story、但 PRD §4 只有 US-FW-1自動升級+ US-FW-2降版 + 多版本切換)+ US-FW-3KL630/KL730——PRD 沒有獨立的「進階使用者切換 FW 版本」US。Design 把這個假設藏在「accordion」設計中但若 PM 沒明示這個 user story、可能造成實作時 Frontend 缺少對應的 AC 驗收標準 | **建議方案**PM 在下次 PRD 補丁將 US-FW-2 拆分為兩個:(1) **US-FW-2 一般使用者主動切版本**(含降版到舊版 / KDP1 兩個情境)+ (2) **US-FW-2.5 進階使用者跨版本切換**(含「升回上一個版本」「測試環境切版本」等情境)。這樣 Design 的「accordion + 中性文案」策略才有對應的 PRD source-of-truth。**或**PRD §4 US-FW-2 敘述加一段「本 user story 同時覆蓋進階使用者主動跨版本切換的情境(含升回上版 / 測試環境切版本、UI 上以中性『版本切換』詞彙呈現、不區分『升 / 降』」 |
### 🔴 Critical必須改、阻擋 merge
無。Design Spec 與 PRD 的需求覆蓋度足以推進開發。MJ-D1 + MJ-D2 雖是 Major、但屬 framing 對齊問題、不是設計錯誤、可在下次 PRD 補丁修正後直接 merge Design Spec。
---
## 對 ADR-001 的審閱
### 結論:通過 with 1 個 Minor + 對齊 ADR 編號的 Major已在 TDD 章節列出 MJ-A1
| 項 | 評估 |
|----|------|
| Status: Accepted | ✅ 對齊使用者 2026-05-24 拍板紀錄 |
| Context 引用歷史紀錄正確嗎? | ✅ 4 個痛點背景對齊 PRD §1.1 / progress.mdR5-Q9 砍 flash 歷史決策推測的 4 個原因與 PRD §2.2 一致 |
| Decision / Consequences 跟 PRD 結論一致嗎? | ✅ 5 項決策A+B 一次做 / R5-Q9 範圍切割 / KneronPLUS Python C API / 選項 C metadata / 保守 +7MB全部對齊 PRD §3 / §6 |
| Alternatives Considered 完整嗎? | ✅ 列了 5 個替代方案、含 DFUT.exe / dev mode only / 多版本目錄選 A B C / 等 A 階段驗證再評估 B——這是好的留痕 |
| Compliance checklist | ✅ 含 firmware redistribution 授權標未完成、與 PRD §9 Q-FW-1 一致 |
### 🟡 Minor
| # | 章節 | 問題 | 建議修法 |
|---|------|------|---------|
| M-ADR1 | Related 表 | 列了「R5-B4」「R5-E」「2026-04-21 commit」「watchServer」「research-kl520-fw-management/」——但沒列「`feature-firmware-management.md`PRD| 加一行「`feature-firmware-management.md` / PRD v2.2 對應 user story 與成功指標」 |
### 🟠 Major與 TDD 互審共用)
MJ-A1ADR 編號 ADR-001 vs PRD 引用的 ADR-009 不一致——詳見 Architect TDD 互審 MJ-A1。
---
## 結論
| 文件 | 結論 |
|------|------|
| Architect TDD v2.2 | ✅ 通過 with 6 Minor + 2 MajorMJ-A1 ADR 編號 + MJ-A2 行號 cross-check |
| Design Spec v2.2 | ✅ 通過 with 5 Minor + 2 MajorMJ-D1 「降版」文案 framing + MJ-D2 user story 拆分) |
| ADR-001 | ✅ 通過 with 1 Minor + 共用 MJ-A1 編號修正 |
| 整體 | **通過 with Major 修正**——三份文件對 PRD 需求覆蓋度高、R5-Q9 翻案的範圍切割三方一致、ADR 留痕完整。需 Architect 統一 ADR 編號、PM 在下次 PRD 補丁對齊 Design 的「中性文案」立場 + 拆分 user story。所有 Major 修完後即可 merge、進 M9-1 開發。 |
---
## 給 Orchestrator 的建議
### 建議派 Architect 修改項
| # | 項目 | 工作量 |
|---|------|-------|
| 1 | **MJ-A1 ADR 編號統一**:與 Architect 確認 ADR 編號規則後、決定 `ADR-001` 還是 `ADR-009`、修改檔名 + 內部引用 + 通知 PM 同步改 PRD §2.5 / §11.4 | 0.1 人天 |
| 2 | **MJ-A2 R5-Q9 行號 cross-check**TDD §1 補一段釐清 L776 vs L854 | 0.1 人天 |
| 3 | M-A1 ~ M-A6Minor| 0.2 人天(或全部 留為 follow-up|
**建議**Architect 修 2 個 Major 後即可 merge。Minor 全部留 follow-up、在 M9-1 啟動前的 prep 階段一次處理。
### 建議派 Design 修改項
| # | 項目 | 工作量 |
|---|------|-------|
| 1 | **MJ-D1 對齊 PRD「降版」文案立場**:等 PM 修 PRD §9 Q-FW-3 後、Design 不用改檔Design 立場已是中性、PRD 對齊到 Design 即可)| 0 人天Design 端)|
| 2 | **MJ-D2 user story 拆分**:等 PM 修 PRD §4 US-FW-2 後、Design 在 §1 加註對應 PRD 哪個 user story、確保未來查閱可追溯 | 0.1 人天 |
| 3 | M-D1 ~ M-D5Minor| 0.2 人天(或全部留為 follow-up|
**建議**Design 等 PM 改完 PRD 後再做小幅對齊、目前 Design Spec 不需要主動改。
### 建議派 PM我自己修改項
| # | 項目 | 工作量 |
|---|------|-------|
| 1 | **修 PRD §2.5 / §11.4 ADR 編號**:等 Architect 統一後同步改 | 0.1 人天 |
| 2 | **修 PRD §9 Q-FW-3「降版」用詞**:從「待 Design 決定」改為「已決定 UI 用中性『版本切換』、技術文件保留 downgrade」| 0.1 人天 |
| 3 | **修 PRD §4 US-FW-2 拆分(或加註覆蓋進階使用者切版本情境)** | 0.2 人天 |
| 4 | **修 PRD §10.2 i18n keys 數量**:從 46 改為 52 | 0.05 人天 |
| 5 | **修 PRD §4 US-FW-2 AC-FW-2.3「dropdown」→「radio list 或同等」** | 0.05 人天 |
| 6 | **新增 PRD §9 Q-FW-8「DOWNGRADE 字串 i18n 化」** | 0.05 人天 |
**合計**PM 後續補丁工作量 ~0.55 人天。
### 互審 follow-up 流程建議
1. **本輪先收尾**Orchestrator 收三份互審報告PM / Architect / Design、評估分歧點
2. **Architect 修 ADR 編號 + 行號 cross-check**2 個 Major→ 通知 PM
3. **PM 補 PRD 對齊 4 個 Major 對應項**ADR 編號 / Q-FW-3 / US-FW-2 / i18n keys 數量)→ 通知 Design
4. **Design 在 §1 加註對應 PRD user story 後再 merge**
5. 整體進度推進到 M9-1 啟動
**不阻擋的 follow-up**:所有 MinorM-A1 ~ M-A6 + M-D1 ~ M-D5 + M-ADR1可在 M9-1 啟動前 prep 階段一次處理、或直接留到 M9-13 三平台驗收後再回頭補。
---
## 變更紀錄
| 版本 | 日期 | 作者 | 變更 |
|------|------|------|------|
| v1 | 2026-05-24 | PM Agent | 初版互審報告3 個 Major + 11 個 Minor + 4 個建議;整體通過 with Major 修正 |

View File

@ -1,7 +1,8 @@
# visionA-local 設計規格 v2索引
> Design Agent · 第五輪正式規格 · 初版 2026-04-14
> **目前版本v2.12026-04-14 補丁)** — 吸收 Architect 交叉審閱 Major 1+2 / Minor 1-5 修正 + R5-D1/D2/D3 / R5-E 追加決策
> **目前版本v2.22026-05-24 補丁)** — 吸收 R5-Q9 翻案、新增 Kneron Dongle FW 管理規格(升級 + 版本切換、面向一般使用者)
> 前一版本v2.12026-04-14 補丁)— 吸收 Architect 交叉審閱 Major 1+2 / Minor 1-5 修正 + R5-D1/D2/D3 / R5-E 追加決策
> 本版依 **R5 決策**重構visionA-local 從「Wails 跑 Next.js 的單一桌面 app」轉為「**Wails Server Control Panel + 瀏覽器 Web UI**」雙 UI 架構。
> 本文件為 v2 索引檔,各章節詳細內容見 `v2/` 子檔。v1`design-spec.md` + `spec/`)仍保留作為基準參考,未來的實作以 v2 為準。
@ -36,6 +37,7 @@
| v2.4 | First-Run 流程重定義 | `v2/first-run-update.md` | 砍 Mock 模式選擇步驟、自動開瀏覽器的起手流程圖 |
| v2.5 | Settings 頁更新 | `v2/settings-update.md` | 新增「啟動時自動開啟瀏覽器」toggle、Linux 預設 OFF、落地 `preferences.json`、砍 Mock 相關設定 |
| **v2.6** | **啟動進度面板**v2.1 新增) | **`v2/startup-progress.md`** | **R5-E 階段化啟動進度面板6 階段 wireframe、中英雙語文案、20 秒卡頓提示、60 秒 timeout Error 流程** |
| **v2.7** | **韌體管理**v2.2 新增) | **`v2/firmware-management.md`** | **Kneron dongle FW 升級 + 版本切換完整規格Settings 新分頁、Devices 頁 FW badge紅/黃/綠 + deep-link、二次確認流程輸入 DOWNGRADE、52 個 i18n keys、6 個新 component token、R-FW-11 設計緩解** |
---
@ -86,6 +88,23 @@
---
## v2.1 → v2.2 差異總覽2026-05-24 補丁輪)
v2.2 是吸收 R5-Q9 翻案FW 管理面向一般使用者)後的補丁,新增 Kneron dongle FW 升級 + 版本切換的完整設計規格。**不改變 R5 雙 UI 架構立論**、只新增功能模組。
| 面向 | v2.1 | v2.22026-05-24 補丁)| 來源 |
|------|------|---------------------|------|
| Settings 分頁數 | 4一般 / 硬體 / 模型 / 進階) | **5**(一般 / 硬體 / **韌體** / 模型 / 進階) | R5-Q9 + architect 42 |
| Devices 頁 FW 顯示 | 純文字 `FW 2.1.0` | **pill badge**(紅 / 黃 / 綠三色)+ deep-link ⚙ icon | R5-Q9 第 5 點 |
| FW 升級 UX | 未設計 | **單步驟確認 + 進度 modal**(不可中斷) | architect 30 §M9-4 |
| FW 降版 UX | 未設計 | **二次確認**輸入「DOWNGRADE」字串+ 進度 modal + 全螢幕 hover 攔截 | architect 42 §5.3 |
| 失敗復原 UX | 未設計 | **8 種失敗類型對應 friendly message + 復原行動** | architect 42 §5.5 |
| 新增 i18n keys | — | **52 keys**zh-TW + en | 本檔 `v2/firmware-management.md §9` |
| 新增 component tokens | — | **6 個 fw-badge token**current / older / legacy × bg/fg | 本檔 `v2/firmware-management.md §11.2` |
| UI 用語策略 | — | UI 一律稱「韌體管理」/「版本切換」中性詞、code/log/技術文件仍稱 downgrade | architect 42 §1.1 |
---
## v2 → v2.1 差異總覽2026-04-14 補丁輪)
v2.1 是 v2 三方互審後的修正補丁,不改變 R5 雙 UI 架構立論,只處理 Architect 互審發現 + R5-D / R5-E 追加決策。
@ -118,6 +137,11 @@ v2.1 是 v2 三方互審後的修正補丁,不改變 R5 雙 UI 架構立論,
- (b) 階段 6 WebSocket 連線被安全軟體擋的 Error 說明是否要特別提示
- (c) 使用者按 Retry 的語意:重置整個啟動流程 vs 重試當前階段Design 建議前者)
**v2.2 新增懸而未決**(來自 `v2/firmware-management.md §14.4`
4. **降版 modal 字串「DOWNGRADE」未來 i18n 化處理**:當前 Design 規格要求所有語系使用者皆輸入字面字串 `DOWNGRADE`en。若未來改 i18n如中文使用者輸入「降版」需與 Architect / Frontend 確認後端 `confirmToken` 是否同步調整architect 42 §2.3 後端固定接受 `confirmToken: "DOWNGRADE"`。Design 建議**短期維持英文字串**(跨語言一致、避免後端多語對應)。
5. **bundled 版本「發布日期」資料源**firmware-management.md §3.3 範例顯示「2024-08 發行」等文案,但 architect 42 §2.1 `FirmwareVersion.ReleaseDate` 為 optional。需 Backend 在 bundle firmware 時準備 metadata每個 version 目錄附 `META.json` 含 releaseDate / notes。metadata 缺則 UI fallback 為不顯示日期。
6. **降版進行中關閉 Wails 控制台視窗的攔截邏輯**:依 R5-2 控制台關閉 = 結束 server。降版進行中關控制台 = 中斷降版 = 可能 brick。Frontend / Backend 是否需要在控制台側偵測「降版進行中」並擋下關閉動作?建議 Architect 補充。
---
**下一步:**

View File

@ -0,0 +1,476 @@
# Design 互審報告 — PM PRD + Architect TDD + ADR-001v2.2 Firmware
> 審閱者Design Agent
> 審閱日期2026-05-24
> 審閱對象:
> - PM`.autoflow/02-prd/features/feature-firmware-management.md`v2.2-draft, 523 行)
> - Architect TDD`.autoflow/04-architecture/v2/firmware-management.md`v2.2, 604 行)
> - Architect ADR`.autoflow/04-architecture/adr/ADR-001-firmware-management.md`v1.0, 210 行)
> 對照基準:`.autoflow/03-design/v2/firmware-management.md`Design v2.2, 920 行)
---
## TL;DR
**整體評估**:🟢 三份產出與 Design 整體一致、可進 M9-12 Frontend 實作。共找出 **13 個對齊點**
- **🔴 嚴重(需互審回合解決)**1 個 — Architect TDD 狀態機名稱與 Design 不一致A-MISMATCH-1
- **🟠 中等(需 PM / Architect 補回應)**4 個 — PM 缺 NPS / 任務完成率指標、PRD 沒明示「降版」用詞策略、Architect 沒解 graceful shutdown 拒絕、Architect 缺 token 對比實測
- **🟡 輕微(建議補但不阻塞)**5 個 — PM 沒列「使用者怎麼知道要降版」user story、PRD 沒提 Bundle 多下載 X 秒、Architect ADR Alternatives 缺一條 Design 視角、PM 對 R-FW-12 不夠具體、PRD §10 KL520 升級時間估算不一致
- **🟢 對齊良好**3 個 — 6 個互審待確認點中 4 個已解、API/UI DisplayName 對齊、WebSocket progress event schema 足夠
**結論**:建議先解 🔴 A-MISMATCH-1狀態機對齊其他 🟠 在實作前的 polish 階段補。**不阻塞 M9-1 ~ M9-5 A 階段啟動**A 階段不涉及降版 UI、6 個對齊點都是 B2 才會踩到)。
---
## 一、對 PM PRD 的審閱
### 1.1 對齊良好(🟢)
#### 🟢 P-OK-16 個 Architect 互審待確認點 PM 已涵蓋 3 個
PM PRD §14.2 列了 6 個給 Architect 的互審項A-FW-1 ~ A-FW-6其中
- A-FW-2driver safety guards→ Architect TDD §6.1 + §5.2 已對應寫具體 dispatch 與 `_validate_downgrade_request`
- A-FW-5模組路徑 `server/internal/firmware/`)→ Architect TDD §2.1 已對應
- A-FW-6R-FW-5 與 R5-B4 合併)→ Architect TDD §8.4 已對應
Design 視角確認PM 給 Architect 的問題、Architect 都回答了。**對齊 OK**。
#### 🟢 P-OK-2「降版面向一般使用者」假設§5的支持證據完整
PM §5.1 列 4 個假設H1-H4+ §5.3 5 種降版情境Design 視角檢核:
- 4 個假設都對齊 R5 Persona 定義P1 FAE / P2 開發者)
- 5 種情境覆蓋 Design 設計的 UX 流程Design §6 二次確認 modal、§7 失敗復原都針對這 5 種使用者預期)
- UR-1 ~ UR-4 user research 待驗證項合理Beta 階段 / post-launch 6 個月追蹤)
**對齊 OK**Design 對 PM 的假設沒有體驗面異議。
#### 🟢 P-OK-3AC-FW-2.5 二次確認 modal 警告語 4 條與 Design §6.1 對齊
PM AC-FW-2.5 列 4 條警告語:
- 「降版可能導致現有 model 無法運作」→ Design §6.1 風險列表第 1 點「部分新版 .nef 模型無法載入」
- 「降版過程不可中斷、否則裝置可能損壞」→ Design §6.1 風險列表第 3 點 + §6.3 進度 banner
- 「降版完成後可能需要重新插拔裝置」→ Design §6.4 toast「可能需要重新插拔」
- 「請確認版本相容性、降版至 KDP1 的舊版會限制可用功能」→ Design §6.2 KDP1 額外紅色 banner + 多 1 條風險
**4 條警告語 100% 對應**。Design 用詞略不同(「.nef 模型」vs 「現有 model」但語意一致、Design 用詞更精準。
### 1.2 中等問題(🟠,需 PM 補)
#### 🟠 P-MID-1成功指標缺「使用者感知體驗指標」
**問題**PM §7 列 2 種指標(北極星 5 分鐘首次推論達成率 + 次要指標如升級成功率 ≥ 95% / Brick < 0.1%**全部是技術 / 業務指標**沒有對應使用者體驗的指標
**Design 視角缺漏**
- 沒有 **「降版任務完成率」**(使用者點「版本切換」→ 真正完成切換的比例。可能很多人在二次確認 modal 階段就放棄)
- 沒有 **「中途放棄率」**多少使用者打到一半「DOWNGRADE」就取消、暗示 UX 過度恐嚇)
- 沒有 **「FW badge 點擊率」**Devices 頁 ⚙ icon / badge 點擊後是否真的進 Settings 韌體分頁,反映 Design IA 是否讓使用者找得到)
- 沒有 **使用後 NPS / SUS 分數**(特別是 B2 階段一般使用者降版流程的可用性)
**建議**PM 在 §7.2 加 4-5 個體驗指標。範例:
```
| 指標 | 目標 |
|------|------|
| 升級任務完成率modal 開啟 → 完成)| ≥ 85% |
| 二次確認 modal 中途放棄率 | 50-70%(合理區間、太低暗示 UX 不夠恐嚇、太高暗示嚇跑使用者)|
| Devices 頁 FW badge 點擊率 | ≥ 30%(只算紅 / 黃 badge|
| B 階段 SUS 分數5 人 usability test| ≥ 65 |
```
**阻塞性**:不阻塞 A 階段、但 B 階段上線前應補(不然沒指標可衡量 UR-1 ~ UR-3
#### 🟠 P-MID-2PRD 沒明示「降版」用詞策略、可能與 UI 矛盾
**問題**
- PM §14.1 D-FW-2「『降版』這個詞在 UI 上是否使用」標為 Design 待定
- PM 自己文件多處用「降版」§4 US-FW-2 標題就是「一般使用者主動切版本」、但內文反覆用「降版」)
- Design 已明確採「韌體 / 版本切換」中性詞、UI 不用「降版」Design §2.1 + §3 範例 wireframe 全部用「版本切換」)
**體驗面風險**:如果 PM PRD 的 user story 文字用「降版」、未來測試人員 / 文件譯者可能誤以為 UI 應該叫「降版」、造成 UI 文案改錯方向。
**建議**PM 在 §14.1 D-FW-2 加註「Design 採『韌體 / 版本切換』、PRD 後續更新時 user-facing 描述應同步」。或在 §4 user story 標題明示「**進階使用者**主動切版本(內部 / 技術文件用 downgrade、UI 用『版本切換』)」。
**阻塞性**:不阻塞 A 階段、但 B 階段 PM 應明示。
#### 🟠 P-MID-3PRD §10 KL520 升級時間估算不一致
**問題**
- AC-FW-1.7「KL520 升級預估 30 秒KL720 預估 180 秒」
- §7.2 次要指標「FW 升降版平均時長KL520 ≤ 60s / KL720 ≤ 200s」
- AC-FW-1.7 30s vs §7.2 ≤ 60s 不一致
**Design 視角影響**Design §5.5 進度 modal 顯示「預估剩餘 X 秒」、這個數字從哪裡來?如果以 AC 30s 為基準、實際拖到 50s 使用者會誤以為「failed」如果以 60s 為基準、實際 30s 結束又會讓使用者懷疑「真的有寫到 flash 嗎?」
**建議**PM 統一兩處數字、Design 建議用 §7.2 的上限(≤ 60s for KL520 / ≤ 200s for KL720作為「預估剩餘」基準、給 UX 緩衝。
**阻塞性**:不阻塞 A 階段啟動、但 Frontend 實作 §5.5 進度 modal 前要釐清。
#### 🟠 P-MID-4PM §14.1 D-FW-4「FW badge 三色閾值」沒明示 PM 期待
**問題**PM 列了 D-FW-4「Design 需定具體閾值」、把這個皮球丟回 Design。
- Design §4.2 已定 3 色規則current = 綠、older = 黃、legacy KDP1 = 紅)
- 但**沒有定義「current 之前 1 版 vs 2 版以上算什麼顏色」**——bundle 三個版本v2.2.0 / v2.1.0 / kdp1時、v2.1.0 是黃還是紅?
- 也沒定「壞掉的 firmwareloader mode、firmware 字串讀不到)」算什麼顏色
**Design 當前邏輯**(從 §4.2 推):
- 綠 = bundled current 完全相同(如 `v2.2.0`
- 黃 = bundle 內較舊但可升(如 `v2.1.0`
- 紅 = KDP1 系列(不論版本)
→ 缺「壞掉 firmware」狀態的處理。
**建議**PM 在 D-FW-4 補一句「Design 已定 3 色規則(綠 / 黃 / 紅、Architect 需提供 driver 層 `FirmwareIsLegacy` / `FirmwareCanUpgrade` 判定邏輯給 Design」、或 Design 直接補一條「壞掉 firmware = 灰色 badge『狀態未知』」。
**阻塞性**:不阻塞 A 階段A 只有 KL520/KL720 current + 升級、沒有 older 中間狀態)。
### 1.3 輕微建議(🟡)
#### 🟡 P-LOW-1缺「進階使用者怎麼知道要降版」user story
**問題**PM §4 列 3 個 US-FW、但缺一個關鍵 user story「使用者怎麼**發現**需要降版?」
- 情境:使用者升版後跑舊 model 出現「Error / 模型載入失敗」、怎麼從錯誤訊息推到「啊我應該降版」?
- 當前 PRD 沒明示 inference 失敗時是否暗示「可能是 FW 版本問題、考慮降版」
**Design 視角**:這個 user story 直接影響 Design 是否要在 Workspace inference 失敗的 error modal 加一條 hint「如果問題是 model 不相容、可以到 Settings → 韌體分頁切換版本」。當前 Design §7 失敗復原**只處理 FW 操作本身的失敗**、不處理「模型推論失敗指向 FW 問題」。
**建議**PM 加 US-FW-2.5「使用者從推論錯誤推導到需要降版」、Design 後續補對應 hint UX。或標為「v2.3 未來迭代」。
**阻塞性**:不阻塞 v2.2、可延後到 v2.3。
#### 🟡 P-LOW-2Bundle 策略對使用者的等待感 PRD 沒提
**問題**PM §6 列 +7MB 安裝包衝擊、但沒列「使用者下載 / 安裝時可能多感覺到的 X 秒等待」。
- macOS 寬頻 ~50MB/s → +7MB = +0.14s(無感)
- 一般使用者 4G hotspot ~5MB/s → +7MB = +1.4s(無感)
- 慢速 wifi 1MB/s → +7MB = +7s有感、但仍可接受
**Design 視角**:這部分 Design 不需要做事Bundle 大小是 PM/Architect 範圍)、但 PRD 缺體驗層的描述。
**建議**PM §6.3 加一句「使用者層面:寬頻 +0.14s 安裝時間(無感)、慢速網路最多 +7s仍可接受」。
**阻塞性**:不阻塞、純文件 polish。
#### 🟡 P-LOW-3R-FW-12「使用者看不懂版本」緩解措施不夠具體
**問題**PM §8 R-FW-12「多版本管理 UX 複雜度、使用者看不懂 v2.2.0 vs v2.1.0 vs KDP1」、緩解寫「Design Agent 設計清晰版本說明文案」、太抽象。
**Design 視角**Design 已具體做了:
- §3.3 每個版本有 displayName 後綴(`(current)` / `(older)` / `(legacy)`
- §3.3 每個版本有獨立說明文字(「發布於 2024-08與舊模型相容
- §9 i18n keys 已涵蓋
**建議**PM 把緩解寫具體「Design v2.2 §3.3 + §9 i18n 已對應、含 displayName 後綴 + 版本說明文字 + KDP1 額外警告」。讓 R-FW-12 不再是 open item。
**阻塞性**:不阻塞、純風險追蹤 polish。
---
## 二、對 Architect TDD 的審閱
### 2.1 對齊良好(🟢)
#### 🟢 A-OK-1DisplayName 格式對齊
Architect TDD §4.1 `FirmwareVersion` struct
```go
DisplayName string `json:"displayName"` // "v2.2.0 (current)" / "v2.1.0 (older)" / "KDP1 (legacy)"
```
Design §3.1 wireframe 顯示「KDP2 v2.2.0 (current)」、§9.2 i18n key 對應:
- `settings.firmware.card.tag.current``(current)`
- `settings.firmware.card.tag.legacy``(legacy)`
**對齊 100%**。但 Design 用「KDP2 v2.2.0 (current)」、Architect 用「v2.2.0 (current)」(沒前綴 KDP2
**小釐清**Architect 的 `FirmwareVersion.Version` 是「v2.2.0」、Design 的 displayName 是「KDP2 v2.2.0 (current)」、兩個是「version field + displayName field 合併」嗎?建議 Architect 明示 displayName 包含 KDP 前綴:「`displayName = "{kdpFamily} {version} ({status})"`」。
#### 🟢 A-OK-2WebSocket progress event schema 足夠支撐 Design UI
Architect TDD §4.2 `FirmwareProgress` struct
- Percent: 0-100 → 對應 Design §5.2 進度條
- Stage: 字串列舉 → 對應 Design §5.3 階段對應表
- Direction: `upgrade` / `downgrade` → 對應 Design §6.3 「切換韌體」vs「升級韌體」標題切換
- Message / Error: 對應 Design §7.2 友善訊息
**Design 缺的ETA預估剩餘秒數**。Design §5.2 範例顯示「預估剩餘 X 秒」、但 Architect schema 沒有 `EstimatedRemainingSeconds` 欄位。
**建議**Architect 在 `FirmwareProgress` 加 optional `EstimatedRemainingSeconds int``ElapsedMs int`Frontend 用 elapsed 自己算 ETA。
#### 🟢 A-OK-3多語系策略對齊
Architect TDD §3.3 錯誤碼表(`FW_DEVICE_BUSY` / `FW_VERSION_NOT_FOUND` 等 6 個)→ Design §9.8 失敗訊息 8 個對應 friendly message keys。
**對齊 OK**、但 Architect 6 個錯誤碼 vs Design 8 個失敗類型,數量不對等(見下方 A-MISMATCH-2
### 2.2 嚴重問題(🔴)
#### 🔴 A-MISMATCH-1狀態機名稱不一致
**問題**
| 來源 | 狀態 / Stage 名稱 |
|------|----------------|
| Design §8 狀態機 | `idle` / `confirming` / **`preparing`** / **`loading`** / **`flashing`** / **`verifying`** / `success_toast` / `error_modal` |
| Architect TDD §4.3 stage 列舉 | `connecting` / `loading_loader` / `loading_firmware` / `verifying` / `done` / `error` |
| Architect TDD §5.1 流程 progressCh stage | `connecting` / `loading_firmware` / `verifying` / `done` |
| PM PRD AC-FW-1.2 | 「連線中 / 載入引導程式 / 寫入韌體 / 重新偵測 / 完成」(中文)|
**衝突點**
- Design 用 `preparing` / `loading` / `flashing` 三階段、Architect 用 `connecting` / `loading_loader` / `loading_firmware` 三階段、PM 用「連線中 / 載入引導程式 / 寫入韌體」三階段(中文)
- 全部都意指同樣的事、但名稱完全沒對齊
**Design 視角影響**
- Design §9.6 i18n keys 把 stage 用 `connect` / `loader` / `upgrade` / `verify` 四個 key在 §5.3 階段對應表)
- 但 Design §8 狀態機又用 `preparing` / `loading` / `flashing` / `verifying`
- **連 Design 自己內部都不一致**——Design §5.3 用 `connect/loader/upgrade/verify`、Design §8 用 `preparing/loading/flashing/verifying`
**Design 自承錯誤**:這是 Design 自己沒對齊內部章節 + 沒採 Architect TDD 的命名。
**建議**:以 **Architect TDD §4.3 stage 列舉** 為標準(後端是 source of truth調整
- Design §8 狀態機:`preparing``connecting``loading``loading_loader``flashing``loading_firmware`
- Design §5.3 階段表的 `connect` / `loader` / `upgrade` 對齊 Architect 的 `connecting` / `loading_loader` / `loading_firmware`
- PM PRD AC-FW-1.2 不變(中文是 user-facing 文案、英文 stage code 是技術名稱)
- Design §9.6 i18n key suffix 從 `stage.connect` 改為 `stage.connecting`
**阻塞性****M9-3 後端 API 完成前必解**。如果 Frontend 不知道後端會推什麼 stage 字串、subscribe WebSocket 時對應不上、進度 modal 顯示「unknown stage」。
#### 🔴 A-MISMATCH-2Architect 6 個錯誤碼 vs Design 8 個失敗類型不對等
**問題**
| Architect TDD §3.3 錯誤碼 | Design §7.1 失敗類型 |
|------------------------|------------------|
| `FW_DEVICE_BUSY` | — |
| `FW_VERSION_NOT_FOUND` | — |
| `FW_INVALID_DIRECTION` | — |
| `FW_NO_CONFIRM_TOKEN` | — |
| `FW_UPGRADE_FAILED` | `upgrade` / `connect` |
| `FW_UPGRADE_BRICK_RISK` | `verify` |
| — | `scan` 找不到裝置 |
| — | `loader` 寫入失敗 |
| — | Timeout>60s |
| — | Disconnect during op |
**衝突點**
- Architect 6 個是「API 層錯誤碼」HTTP 4xx/5xx 對應)
- Design 8 個是「使用者面失敗情境」stage 細分)
- 兩者**不在同一層**、不衝突、但對應關係沒明示
**Design 視角影響**
- Design §7.2 失敗 modal 顯示「錯誤代碼fw_upgrade_stage_loader_E102」、暗示有更細的代碼系統
- 但 Architect 只給 6 個 API 層代碼、沒給 stage-level 代碼
- Frontend 拿到 `FW_UPGRADE_FAILED` 後不知道對應 Design 8 種失敗類型中哪一種
**建議**Architect TDD §3.3 補一段「失敗類型對應 stage」表
```
| 觸發 stage | API 錯誤碼 | 給 frontend 的細分 reason 欄位 |
|----------|----------|---------------------------|
| scan 失敗 | FW_UPGRADE_FAILED | reason: "scan_not_found" |
| connect 失敗 | FW_UPGRADE_FAILED | reason: "connect_failed" |
| loader 寫入失敗 | FW_UPGRADE_FAILED | reason: "loader_write_failed" |
| upgrade 中段失敗 | FW_UPGRADE_FAILED | reason: "upgrade_mid_failed" |
| verify 失敗 | FW_UPGRADE_BRICK_RISK | reason: "verify_mismatch" |
| timeout | FW_UPGRADE_FAILED | reason: "timeout" |
| disconnect | FW_UPGRADE_FAILED | reason: "disconnect_during_op" |
```
或在 `FirmwareProgress.Error` 字串內帶結構化資訊Frontend 解析)。
**阻塞性**M9-3 後端 API 完成前必解、不然 Frontend 拿不到細分 reason。
### 2.3 中等問題(🟠)
#### 🟠 A-MID-1Architect 沒解 graceful shutdown 拒絕
**問題**Design §14.4 懸而未決第 6 點明示問:
- 降版進行中關閉 Wails 控制台 = 結束 server = 中斷降版 = 可能 brick
- Frontend / Backend 是否需要偵測「降版進行中」並擋下關閉動作?
**Architect TDD 沒對應**
- §8.5 「Flash() method 與 restartBridge()」段提到「FW 升降版完成後設 needsReset=true」、但**沒提**降版進行中如何拒絕 server shutdown
- §5.2 流程沒提 graceful shutdown 拒絕邏輯
- §6.4 既有 handler 改動清單沒列「shutdown handler 加 FW 進行中檢查」
**Design 視角影響**
- Design 規格已採「全螢幕 hover 攔截」§6.3)防止 click 關 modal、但**擋不住關 Wails 控制台視窗**
- 如果 Architect 不在 server 層加保護、Design 的 UX 多層 safety net 在這個漏洞前破功
**建議**Architect TDD 補一段 §8.6「降版進行中的 graceful shutdown 拒絕」:
- server 偵測 FW task 進行中、收到 SIGTERM / Wails 視窗關閉 → **延遲 shutdown**、等 FW task 完成或 60s timeout
- 同時推 WebSocket event 通知 frontend「使用者嘗試關閉、已擋下」
- Frontend 在 Wails 控制台顯示「降版中、無法關閉」提示Design §13.3 應補對應 UX
**阻塞性****B2 階段M9-11/M9-12前必解**。A 階段升級不涉及(升級失敗風險小、可中斷)、但降版必須有此保護。
#### 🟠 A-MID-2Architect token 對比比率沒對應 Design §11.2 推導表
**問題**Design §11.2 列了 6 個新 component token 與 Light/Dark 對比比率4.7:1 / 5.2:1 / 4.8:1。Design §11.2 末尾標「Architect 互審重點:請確認 Dark 模式下 legacy.fg 用黑色對紅底對比是否真的足夠」。
**Architect TDD 沒回應**
- TDD §11 測試策略沒提 token 對比驗證
- TDD §13.2 Design 互審注意沒提此項
- ADR-001 也沒提
**Design 視角影響**
- Design 的對比比率是「推算值」、需 Architect / Frontend 用 axe / Chrome DevTools contrast checker 工具實測
- 如果實測 Dark mode 紅底黑字 < 4.5:1Design 必須調整 token可能改 fg 為白色但白配紅可能更糟
- 不解 = 違反 Design A 層 De-A2 verificationWCAG AA 對比度達標)
**建議**Architect TDD §11.1 加單元測試項:
```
| firmware/tokens.test | 6 個 fw-badge token 對比 ≥ 4.5:1light + dark| axe-core 程式化驗證 |
```
或 Architect 不主動驗、由 Design 在 M9-4 / M9-12 Frontend 實作時 Design QA 階段實測。
**阻塞性**M9-12 Frontend 實作前必解。不解的話 Design verification 過不了。
#### 🟠 A-MID-3Architect 失敗復原 §5.3 表只列 4 種、Design §7.1 列 8 種
**問題**
- Architect TDD §5.3 失敗復原表4 種device disconnect / firmware load pre-flash / firmware load mid / verify 失敗)
- Design §7.1 失敗類型對應8 種scan / connect / loader / upgrade / verify / Timeout / Disconnect / 部分成功)
**衝突**Architect 表少了 scan 失敗、connect 失敗、loader 寫入失敗、timeout 4 種。
**Design 視角影響**
- Design 已寫對應 friendly message + 復原行動的 i18n§9.8
- 但如果 Architect 後端沒分這 4 種、Frontend 拿到的都是 generic「FW_UPGRADE_FAILED」、Design 顯示不出 8 種不同 modal
**建議**:與 A-MISMATCH-2 一起處理。Architect TDD §5.3 對齊 Design §7.1 8 種、或在 reason 欄位細分(見 A-MISMATCH-2 建議)。
**阻塞性**M9-3 後端 API 完成前必解。
### 2.4 輕微建議(🟡)
#### 🟡 A-LOW-1Architect TDD §10 風險 R-FW-12 對齊不完整
**問題**Architect TDD §10 列 R-FW-12「多版本管理 UX 複雜度」、緩解寫「預設只顯示『升級到最新』+『切換 FW 版本』、降版必須展開 + 詳細說明」。
**Design 視角**
- Design 已具體做§3.3 accordion 收合、radio list 含說明)
- Architect 寫「預設只顯示...」與 Design 不完全一致Design 是「永遠顯示升級按鈕」、不是「預設只顯示」)
**建議**Architect 引用 Design 對應章節:「緩解措施由 Design v2.2 §3.3 落地accordion + radio list + 版本說明)」。
**阻塞性**:不阻塞、純文件對齊。
---
## 三、對 ADR-001 的審閱
### 3.1 對齊良好(🟢)
#### 🟢 ADR-OK-1Decision / Consequences 與 Design 整體理念一致
ADR Decision 5 條:
1. A + B 一次做完 → Design 規格已涵蓋 A + B
2. 翻案 R5-Q9 範圍切割 → Design §1.2 範圍邊界對齊
3. KneronPLUS Python C API → Design 不需要知道(後端細節)
4. 多版本目錄結構選項 C → Design §4.2 對齊(顯示版本不涉及目錄結構、但 Design 假設 bundled 版本有 metadata
5. 保守 +7MB Bundle → Design 不影響
**對齊 OK**。
#### 🟢 ADR-OK-2Consequences 負面影響第 3 條對齊 Design 多層 safety net
ADR Consequences 列「法律 / 簽章授權待釐清」、Design 不涉及。
列「Brick 風險未完全消除」、Design 已透過 §6 + §7 + §12 三層應對:
- §6.1 二次確認 modal + DOWNGRADE 字串
- §6.3 全螢幕 hover 攔截 + 不可關 modal
- §7 失敗復原 8 種 friendly message
- §12 R-FW Design 對策(明示對應 brick 風險路徑)
**對齊 OK**。
### 3.2 輕微建議(🟡)
#### 🟡 ADR-LOW-1Alternatives Considered 缺一條 Design 視角
**問題**ADR 列 5 個 Alternatives但**沒有「替代方案:先做 dev mode 降版、不面向一般使用者」**(其實 §3 「只做內部 dev mode 降版」算半個、但 ADR 是從工時 / 痛點視角討論、沒從 UX 風險視角討論)。
**Design 視角應補**:「替代方案 6只做 dev mode藏在 Settings → 進階 → 開發者選項)」的 UX trade-off
- 優點:誤觸風險降到 0
- 缺點使用者明確要求面向一般使用者H3 假設「藏在 dev mode 反而會讓真正需要降版的人找不到」)
- 否決原因:使用者 2026-05-24 拍板「面向一般使用者」+ Design 多層 safety net 評估足以擋誤觸
**建議**Architect 在 ADR Alternatives §6 補上面這條 UX 視角的替代方案分析。
**阻塞性**:不阻塞、純 ADR 完整度。
---
## 四、總結與給 Orchestrator 的建議
### 4.1 對齊狀態總覽
| 類別 | 數量 | 狀態 |
|------|------|------|
| 🟢 對齊良好 | 6 | 不需處理 |
| 🟡 輕微建議 | 5 | 建議補、但不阻塞任何 milestone |
| 🟠 中等問題 | 4 | B 階段M9-11 ~ M9-12前必解 |
| 🔴 嚴重問題 | 2 | M9-3 後端 API 完成前必解 |
### 4.2 必解項目(按優先級)
#### 優先級 P0M9-3 後端 API 完成前必解)
1. **A-MISMATCH-1**:狀態機名稱不一致
- 行動:以 Architect TDD §4.3 為準、Design 同步調整 §8 + §5.3 + §9.6 i18n key
- 工時Design < 0.5h
- **誰來改**Design AgentDesign 內部對齊)+ Architect 確認最終名稱
2. **A-MISMATCH-2**:錯誤碼 vs 失敗類型不對等
- 行動Architect TDD §3.3 補「失敗類型對應 stage」表 + `FirmwareProgress.Reason` 欄位
- 工時Architect < 0.5h
- **誰來改**Architect Agent
#### 優先級 P1B2 階段 M9-11/12 前必解)
3. **A-MID-1**graceful shutdown 拒絕邏輯
- 行動Architect TDD 補 §8.6 + Design 補 Wails 控制台「降版中無法關閉」UX
- 工時Architect 1h + Design 1h
- **誰來改**Architect Agent+ Design Agent控制台 UX
4. **A-MID-2**token 對比實測
- 行動M9-12 Frontend 實作時 Design QA 階段實測、不過則調整 token
- 工時:< 0.5h axe 工具
- **誰來改**Frontend / DesignM9-12
5. **A-MID-3**:失敗復原表 4 vs 8 對齊
- 行動:與 A-MISMATCH-2 一起處理
- **誰來改**Architect Agent
6. **P-MID-1**:成功指標補體驗指標
- 行動PM 在 §7.2 補 4-5 個體驗指標
- 工時PM 0.5h
- **誰來改**PM AgentB 階段上線前)
#### 優先級 P2可延後 / polish
7. **P-MID-2**(降版用詞)/ **P-MID-3**(升級時間估算)/ **P-MID-4**badge 閾值PM 補
8. **P-LOW-1 ~ P-LOW-3**PM polish
9. **A-LOW-1**Architect TDD R-FW-12 緩解對齊
10. **ADR-LOW-1**ADR Alternatives 補 Design 視角
### 4.3 不阻塞 M9-1 ~ M9-5 A 階段啟動
- A 階段(升級 only不涉及 B2 降版、狀態機名稱對齊A-MISMATCH-1解決後即可進
- A 階段不涉及 graceful shutdown 拒絕、failed type 8 分類也只在 B 階段需要M9-11/12
- 建議 Orchestrator **派 Architect 解 A-MISMATCH-1 + A-MISMATCH-2 之後**、即可啟動 M9-1 backend
### 4.4 Design 本輪不改
依任務 prompt「不改 design、本輪只審」Design 不主動更新 `firmware-management.md`
**待 Orchestrator 整合三方互審報告後、若決議 Design 需配合調整(如 A-MISMATCH-1 把狀態機名稱換成 connecting/loading_loader/loading_firmware/verifyingOrchestrator 另派 Design Agent 改檔**。
### 4.5 Design 自承的內部矛盾
- Design §5.3 階段對應表 vs §8 狀態機名稱不一致Design 自己問題、不是 PM/Architect 引起)
- 處理 A-MISMATCH-1 時一併修
---
## 變更紀錄
| 日期 | 版本 | 變更 | 作者 |
|------|------|------|------|
| 2026-05-24 | v1.0 | Design 互審 PM PRD + Architect TDD + ADR-001 完成、找出 13 個對齊點2 P0 / 4 P1 / 7 P2| Design Agent |

View File

@ -184,7 +184,8 @@ Wails Server Control Panel以下稱「控制台」是 visionA-local 雙擊
| 元素 | 位置 | 文字 / 樣式 |
|------|------|-----------|
| 行數統計 | 左 | `Lines: {current} / 2000`12px muted |
| 關閉提示 | 右 | `⚠ Closing this window will stop the server.`12px muted |
| 關閉提示(預設) | 右 | `⚠ Closing this window will stop the server.`12px muted |
| 關閉提示韌體進行中、v2.2 新增) | 右 | `🚫 韌體更新中、關閉視窗可能造成裝置損毀`12px `color.destructive` SemiBold取代預設提示對應 i18n key `control.footer.closeWarningFirmwareActive`、詳見 §6a.8 |
**持久提示為什麼不彈 modal**R5-2 解釋):
- 使用者已明確決策「關閉 = 結束 server」
@ -288,6 +289,186 @@ Banner **不可手動關閉**(避免使用者忽略問題)。只有下列條
---
## 6a. 韌體進行中關閉攔截v2.2 新增、對應 firmware-management §14.4 第 6 點)
### 6a.1 為什麼需要這個
依 R5-2「關閉控制台 = 結束 server」但有一個例外場景**韌體升級 / 切換進行中**。
- 韌體切換 / 降版是寫 flash 的破壞性操作、中斷會造成裝置永久損毀brick
- 如果使用者在韌體切換進行中關掉 Wails 控制台、Wails close handler 預設會送 SIGTERM 給 server 結束 Python sidecar
- Python sidecar 正在 `update_kdp_firmware_from_files` 中段被砍 = **flash 寫一半就停 = brick**
依 Architect TDD §8.6v2.2 新增、server 已實作「降版進行中拒絕 graceful shutdown」邏輯
- server 收到 SIGTERM → 檢查 `firmware.Service.HasActiveTask()`
- 有 active task → 拒絕 shutdown、回傳 `firmwareInProgress: true` 給 Wails close handler
- 沒 active task → 正常 graceful shutdown既有 7+1s pattern
控制台側需提供對應 UI 攔截關閉動作、警告使用者風險。
### 6a.2 觸發時機
下列任一情況、使用者試圖關閉 Wails 控制台時:
- 按視窗右上角 `×` 關閉鈕
- `⌘W` / `Ctrl+W` 鍵盤快捷鍵
- `⌘Q` / 系統選單「結束」macOS/ 系統匣「結束」Windows
- 強制最小化 + 結束程式(透過 dock / taskbar
→ 控制台先 query server `/api/firmware/status`(或對應 IPC method
- 回傳 `{active: false}` → 正常走 R5-2 流程(結束 server + 關視窗)
- 回傳 `{active: true, taskInfo: {deviceName, stage, etaSeconds}}` → 觸發本節攔截 modal
### 6a.3 攔截 Modal Wireframe
```
┌─ ⚠ 韌體更新進行中 ───────────────────────────────────────┐
│ │
│ 🚫 強制關閉可能造成裝置永久損毀 │
│ │
│ KL520 #1 的韌體切換正在進行中。 │
│ 現在強制關閉應用程式會中斷韌體寫入、可能造成裝置 │
│ 永久損毀brick、無法救援。 │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 目前階段:寫入韌體(階段 3 / 4 │ │
│ │ 預估剩餘:約 22 秒 │ │
│ │ ████████████░░░░ 60% │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ⚠ 建議「繼續等待」、約 22 秒後即可安全關閉。 │
│ │
│ [ 繼續等待 ] [ 強制關閉 ] │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 6a.4 元件規格
| 元素 | 類型 | 樣式 |
|------|------|------|
| Modal 容器 | `<div role="alertdialog" aria-modal="true">` | 寬度 520px、`color.card` 背景、`color.destructive` 2px 邊框、`elevation.5` 陰影 |
| 標題 icon | `<svg>` | 24×24、`color.destructive` |
| 標題 | `<h2>` | 18px / SemiBold / `color.destructive`、「⚠ 韌體更新進行中」 |
| 主要警語 | `<strong>` | 16px / Bold / `color.destructive`、「🚫 強制關閉可能造成裝置永久損毀」 |
| 說明段落 | `<p>` | 14px / regular / `color.foreground` |
| 進度資訊區 | `<div>` | 背景 `color.muted/30`、padding 16、圓角 `radius.md` |
| 階段文字 | `<span>` | 13px / medium / `color.foreground` |
| 預估剩餘 | `<span>` | 13px / medium / `color.foreground`、每秒更新(從 server WebSocket progress event 推送)|
| 進度條 | `<progress>``<div role="progressbar">` | 高 8px、`color.destructive` fill強調危險`color.muted` track、圓角 `radius.full` |
| 建議文字 | `<p>` | 13px / regular / `color.warning`、附 ⚠ icon |
| **「繼續等待」按鈕** | Button `primary` `md` | **預設聚焦**focus、Enter 觸發、樣式為主要 CTA |
| **「強制關閉」按鈕** | Button `destructive ghost` `md` | 邊框 `color.destructive`、文字 `color.destructive`、背景透明(不主動誘導點擊)|
### 6a.5 互動行為
**「繼續等待」**(建議路徑):
- 點擊 / Enter → 關閉 modal、保留控制台視窗
- 控制台留在原狀態韌體進度面板繼續顯示、log 持續更新)
- 不結束 server、不關 Wails 視窗
- 韌體任務完成後、firmware-management §5.4 / §6.4 toast 出現、使用者可以再次嘗試關閉視窗(這次走正常 R5-2 流程)
**「強制關閉」**(危險路徑):
- 點擊 → **不立即關閉**、跳出第二層**最終確認 modal**
```
┌─ 🚫 確定要強制關閉? ────────────────────────────────┐
│ │
│ 強制關閉會: │
│ • 中斷正在進行的韌體寫入 │
│ • 可能造成裝置永久損毀(無法救援) │
│ • 即使重新插拔也無法復原 │
│ │
│ 請輸入「FORCE」確認你了解後果
│ ┌──────────────────────────────────────────┐ │
│ │ │ │
│ └──────────────────────────────────────────┘ │
│ (大小寫需完全相同) │
│ │
│ [ 返回 ] [ 確認強制關閉 ] │
│ │
└───────────────────────────────────────────────────────┘
```
- 「FORCE」輸入框比對與 firmware-management §6.1 「DOWNGRADE」相同邏輯嚴格 `===` 比對)
- 「確認強制關閉」按鈕在輸入 `FORCE` 前 disabled、輸入正確後變為 `destructive` 紅色
- 點擊 → Wails close handler 跳過 server graceful shutdown 流程、直接 kill server process傳 SIGKILL+ 關閉視窗
**ESC 鍵**:等同點「繼續等待」(安全路徑)、關 modal 保留視窗
**外部點擊**:不可關閉(捕獲事件、抖動 modal 200ms 提示)、與 firmware-management §6.1 同設計
### 6a.6 視覺強度層級對比
| 元素 | firmware-management §6.1 降版二次確認 | 本節 §6a.3 韌體進行中關閉攔截 |
|------|------------------------------------|---------------------------|
| 場景 | 降版**開始前** | 降版**進行中** + 使用者試圖中斷 |
| 風險等級 | 中(降版本身有 brick 風險、但 < 1%| 中斷正在寫 flash = 高機率 brick|
| 確認字串 | `DOWNGRADE`(指向操作意圖)| `FORCE`(指向強制中斷)|
| Modal 邊框 | 1px | **2px**(更強烈)|
| 預設聚焦 | 「DOWNGRADE」input | **「繼續等待」按鈕**(引導安全路徑)|
| 安全 CTA | 「取消」`ghost` | **「繼續等待」`primary`**(主要 CTA、強引導|
| 危險 CTA | 「確認切換」`destructive` filledinput 通過後)| 「強制關閉」`destructive ghost`(不主動誘導、需二次確認)|
### 6a.7 不重複攔截
- 同一個關閉嘗試只跳一次 modal
- 使用者選「繼續等待」後、韌體完成前如再次嘗試關閉 → 重新跑 6a.2 流程(再次 query server status
- 「強制關閉」確認後、modal 立即關閉、不留任何撤銷機會(已通過二次確認)
### 6a.8 與 Footer 持久提示 §4.6 的關係
- §4.6 「⚠ Closing this window will stop the server.」footer 提示**仍然顯示**
- 韌體進行中時、footer 提示**改為更嚴重的紅色版本**:「🚫 韌體更新中、關閉視窗可能造成裝置損毀」
- 對應 i18n key`control.footer.closeWarningFirmwareActive`
### 6a.9 i18n keys新增
| Key | zh-TW | en |
|-----|-------|----|
| `control.firmwareBlock.title` | 韌體更新進行中 | Firmware update in progress |
| `control.firmwareBlock.heading` | 🚫 強制關閉可能造成裝置永久損毀 | 🚫 Force-close may permanently damage the device |
| `control.firmwareBlock.description` | {deviceName} 的韌體{operation}正在進行中。現在強制關閉應用程式會中斷韌體寫入、可能造成裝置永久損毀brick、無法救援。 | {operation} on {deviceName} is in progress. Force-closing the application now will interrupt the firmware write and may permanently damage (brick) the device beyond recovery. |
| `control.firmwareBlock.operation.upgrade` | 升級 | upgrade |
| `control.firmwareBlock.operation.switch` | 切換 | switch |
| `control.firmwareBlock.stage` | 目前階段:{stage}(階段 {n} / {total}| Current stage: {stage} (stage {n} / {total}) |
| `control.firmwareBlock.eta` | 預估剩餘:約 {seconds} 秒 | Estimated remaining: ~{seconds}s |
| `control.firmwareBlock.recommendation` | ⚠ 建議「繼續等待」、約 {seconds} 秒後即可安全關閉。 | ⚠ Recommended: "Continue waiting". Safe to close in about {seconds}s. |
| `control.firmwareBlock.action.continueWaiting` | 繼續等待 | Continue waiting |
| `control.firmwareBlock.action.forceClose` | 強制關閉 | Force close |
| `control.firmwareBlock.confirm.title` | 🚫 確定要強制關閉? | 🚫 Confirm force close? |
| `control.firmwareBlock.confirm.risk.interrupt` | 中斷正在進行的韌體寫入 | Interrupt the ongoing firmware write |
| `control.firmwareBlock.confirm.risk.brick` | 可能造成裝置永久損毀(無法救援) | May permanently damage the device (unrecoverable) |
| `control.firmwareBlock.confirm.risk.noRecover` | 即使重新插拔也無法復原 | Cannot be recovered even by unplugging and reconnecting |
| `control.firmwareBlock.confirm.prompt` | 請輸入「FORCE」確認你了解後果 | Type "FORCE" to confirm you understand the consequences: |
| `control.firmwareBlock.confirm.placeholder` | FORCE | FORCE |
| `control.firmwareBlock.confirm.helpText` | (大小寫需完全相同) | (case-sensitive) |
| `control.firmwareBlock.confirm.action.back` | 返回 | Back |
| `control.firmwareBlock.confirm.action.forceClose` | 確認強制關閉 | Confirm force close |
| `control.footer.closeWarningFirmwareActive` | 🚫 韌體更新中、關閉視窗可能造成裝置損毀 | 🚫 Firmware update in progress — closing window may damage device |
合計新增 **19 個 i18n keys**zh-TW / en 各一套)。
### 6a.10 無障礙考量
| 項目 | 設計 |
|------|------|
| Modal 結構 | `role="alertdialog"`、screen reader 主動朗讀 |
| Focus management | modal 開啟時自動 focus 「繼續等待」按鈕、focus trap |
| Tab 順序 | 「繼續等待」→「強制關閉」 |
| ESC 鍵 | 等同「繼續等待」(安全路徑)|
| 危險視覺不單靠顏色 | 標題附 ⚠ icon + 「強制關閉」按鈕附文字描述(不只是紅色) |
| Reduced motion | `prefers-reduced-motion: reduce` → modal 動畫 0ms、進度條改靜態顯示 |
| 觸控目標 | 兩個按鈕 ≥ 44×44px |
| 對比 | 紅色 destructive 對 card 背景 ≥ 4.5:1WCAG AA、critical 信號不妥協)|
### 6a.11 與 Architect TDD §8.6 的銜接
- TDD §8.6 提供 `HasActiveTask()` method 與 server graceful shutdown 拒絕邏輯
- 控制台側透過既有 IPC或新增一個 `/api/firmware/status` 端點)查詢 active task
- 取得 active task 資訊(`deviceName / stage / etaSeconds / direction`)後渲染本節 modal
- 「強制關閉」確認後、控制台需呼叫 server `force-shutdown` IPC methodbypass graceful shutdown、server 收到後直接送 SIGKILL 給 Python sidecar、不等 firmware task 完成
- 詳細 IPC 規格依 TDD §8.6 規範Frontend M8-5 階段對接)
---
## 7. 啟動行為(對應 R5-4 / R5-D3 / R5-E
### 7.1 預設流程v2.1 修訂)
@ -460,6 +641,12 @@ Banner **不可手動關閉**(避免使用者忽略問題)。只有下列條
| 7 | §7.1 第 5 步 | 「首次 / Settings 為 ON」 | **「每次 / Settings 為 ON」**,新增 Linux 預設 OFF 說明,流程改為 6 階段化 | R5-D3 + R5-E |
| 8 | §7.1 新增 | — | 引用新檔 `v2/startup-progress.md`R5-E 階段化啟動進度面板) | R5-E |
## 13. v2.1 → v2.2 Diff2026-05-25
| # | 位置 | v2.1 | v2.2 | 來源 |
|---|------|----|----|------|
| 1 | §6a 新增整節 | — | **§6a 韌體進行中關閉攔截**(兩層 modal + 「FORCE」二次確認 + 19 個 i18n keys + 6a.8 footer 紅色變體) | firmware-management §14.4 第 6 點 + Architect TDD §8.6 + Design 三方互審吸收 |
---
**下一步**:交 M8-5 Frontend Agent 實作Wails 控制台 + 啟動進度面板),交 Reviewer 審查 control-panel.md + startup-progress.md 整體一致性。
**下一步**:交 M8-5 Frontend Agent 實作Wails 控制台 + 啟動進度面板 + 韌體進行中關閉攔截 modal),交 Reviewer 審查 control-panel.md + startup-progress.md + firmware-management.md 整體一致性。

View File

@ -0,0 +1,948 @@
# v2.7 — Kneron Dongle FW 管理(升級 + 版本切換)
> 本章對應 R5-Q9 翻案FW 管理面向一般使用者)+ Architect research 30-integration-plan §M9-2/3/4 + 42-manual-downgrade-for-end-users §5。
> 上層索引:`../design-spec-v2.md`
> 版本:**v2.2 新增** · 建立日期2026-05-24
> 相關:
> - 架構參考:`.autoflow/04-architecture/research-kl520-fw-management/30-integration-plan.md``42-manual-downgrade-for-end-users.md`
> - UI 銜接:`v2/settings-update.md`(新增分頁進這個檔的結構)、`spec/03-wireframes.md §3.3 Devices`FW badge
---
## 1. 定位與職責
「韌體管理」是 Settings 內**新增的第 5 個分頁**,使用者在此:
1. 看到每張 Kneron dongle 當前 FW 版本 + bundled 最新版的對照
2. 升級到最新 FW一鍵動作、KL520 KDP1→KDP2 為主要場景)
3. **切換 FW 版本(含降版)**——進階使用者操作,多層 safety guard 保護
4. 看升級 / 切換進度、失敗復原指引
**這個分頁不做的事**
- 不做 device-level inference / model 操作(那是 Workspace 的事)
- 不顯示非 Kneron 裝置
- 不做 OTA / 線上版本更新(所有 FW 從安裝包內 bundleR5-Q9 決策)
**為什麼是 Settings 分頁、不是 Devices 頁主流程**
- 降版屬於高風險操作(理論上可能 brick、不應放在使用者點擊頻率最高的 Devices 頁
- 升級雖然低風險,但「升級」與「切換」介面共用同一套版本選單較自然
- 對應 architect 42 §1.2 的 trade-off 分析
---
## 2. IA 整合Settings 分頁順序
`v2/settings-update.md §1` 的 4 分頁結構(一般 / 硬體 / 模型 / 進階)擴充為 **5 分頁**
| Tab | 順序 | 變更 |
|-----|------|------|
| 一般 | 1 | 不變 |
| 硬體 | 2 | 不變 |
| **韌體** | **3** | **本檔新增** |
| 模型 | 4 | 順序後移 |
| 進階 | 5 | 順序後移 |
**為什麼放在「硬體」與「模型」之間**
- 概念分組:硬體(裝置本身)→ 韌體(裝置韌體)→ 模型(裝置上跑的軟體資產),由低階到高階
- 「硬體」分頁列出裝置掃描策略,使用者看完掃描設定後,自然會問「裝置韌體呢」
- 進階分頁仍放最後(不變)
### 2.1 分頁標籤文案
| Key | zh-TW | en |
|-----|-------|----|
| `settings.tabs.firmware` | 韌體 | Firmware |
> **用詞警告**:分頁名稱**不能叫「降版」**——對一般使用者過於負面。一律稱「韌體」或「韌體管理」。「版本切換」是進階操作的中性說法。
---
## 3. 頁面結構Wireframe
### 3.1 整體佈局(韌體分頁主畫面)
預設狀態(有偵測到 2 張 dongle、其中 KL520 有可升級的版本):
```
┌─ Settings > 韌體 ───────────────────────────────────────────────────┐
│ │
│ 韌體管理 │
│ 管理已連接 Kneron 裝置的韌體版本。 │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ ┌─ KL520 #1 ─────────────────────────────────────────────────┐ │
│ │ kn_number: │ │
│ │ ● KDP1 (legacy) 0x1A2B3C4D │ │
│ │ 建議升級到 KDP2 v2.2.0 │ │
│ │ │ │
│ │ 當前版本KDP1 │ │
│ │ 最新版本KDP2 v2.2.0 (建議) │ │
│ │ │ │
│ │ [ 升級到 KDP2 v2.2.0 ] [ 版本切換 ▾ ] │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ KL720 #1 ─────────────────────────────────────────────────┐ │
│ │ kn_number: │ │
│ │ ● KDP2 v2.2.0 (current) 0x5E6F7890 │ │
│ │ 裝置使用最新韌體 │ │
│ │ │ │
│ │ 當前版本KDP2 v2.2.0 │ │
│ │ 最新版本KDP2 v2.2.0 │ │
│ │ │ │
│ │ [ 版本切換 ▾ ] │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ⓘ 沒看到裝置?檢查 USB 連接後到「硬體」分頁重新掃描。 │
│ │
└───────────────────────────────────────────────────────────────────────┘
```
**配色解讀(裝置卡片左側圓點)**
| 圓點顏色 | 條件 | 文字標籤 |
|---------|------|---------|
| 🔴 `color.destructive` | KDP1 / legacy FW | `KDP1 (legacy)` |
| 🟡 `color.warning` | 有可用較新版本 | `KDP2 v2.1.0 (older)` |
| 🟢 `color.success` | 已是最新 bundled 版本 | `KDP2 v2.2.0 (current)` |
### 3.2 空狀態(無 dongle 連接)
```
┌─ Settings > 韌體 ───────────────────────────────────────────────────┐
│ │
│ 韌體管理 │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🔌 │ │
│ │ │ │
│ │ 尚未偵測到 Kneron 裝置 │ │
│ │ │ │
│ │ 插上 USB 裝置後,到「硬體」分頁點擊 │ │
│ │ 「重新掃描」便會出現在這裡。 │ │
│ │ │ │
│ │ [ 前往硬體分頁 ] │ │
│ │ │ │
│ └──────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────┘
```
### 3.3 「版本切換」展開後accordion 樣式、不是 dropdown
`版本切換 ▾` 後、卡片下半部展開:
```
┌─ KL520 #1 ─────────────────────────────────────────────────────────┐
│ ● KDP2 v2.2.0 (current) kn_number: 0x... │
│ 裝置使用最新韌體 │
│ │
│ 當前版本KDP2 v2.2.0 │
│ 最新版本KDP2 v2.2.0 │
│ │
│ [ 版本切換 ▴ ] │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ⚠ 切換到舊版可能導致: │ │
│ │ • 部分新版 .nef 模型無法載入 │ │
│ │ • 推論效能與穩定性下降 │ │
│ │ • KDP1 無法執行多模型載入 │ │
│ │ 切換過程約需 30 秒KL520/ 3 分鐘KL720切勿拔除裝置。│ │
│ │ │ │
│ │ 可用版本: │ │
│ │ ○ KDP2 v2.2.0 (current) 當前版本 │ │
│ │ ○ KDP2 v2.1.0 發布於 2024-08與舊模型相容 │ │
│ │ ○ KDP1 (legacy) 僅供 third-party 工具相容測試 │ │
│ │ │ │
│ │ [ 取消 ] [ 切換到此版本 ] │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
**展開區的元件規格**
| 元素 | 類型 | 細節 |
|------|------|------|
| Inline warning | `<div role="note">` | `color.warning/10` 背景、`color.warning` 邊框1px、padding 16、圓角 `radius.md` |
| ⚠ icon | inline SVG | 20×20、`color.warning` |
| 版本 radio list | `<input type="radio">` + `<label>` | name 共用「fw-version-{deviceId}」、預設選中 current |
| 版本說明 | `<span>` | 12px / `color.muted-foreground`、跟版本 label 同行右側 |
| 「切換到此版本」按鈕 | Button `destructive` `md` | **disabled 條件**:選中的就是 current**enabled 條件**:選了非 current 版本 |
| 「取消」按鈕 | Button `ghost` `md` | 點擊 → 收起 accordion、radio 重置為 current |
**為什麼用 radio list 不用 dropdown**
- bundled 版本通常 ≤ 3 個current + 1 舊版 + KDP1不需要 dropdown 的省空間優勢
- radio list 讓所有選項與描述都可見、降低使用者「不知道有什麼選項」的疑慮
- 對 a11y 友善screen reader 一次讀完所有選項)
---
## 4. Devices 頁面FW Badge 整合
依 R5-Q9 第 5 點Devices 頁 device card 需顯示 FW badge。
### 4.1 既有 Devices 卡片(`spec/03-wireframes.md §3.3`)的補強
```
┌──────────────────┐ ┌──────────────────┐
│ KL720-A │ │ KL520-B │
│ 🟢 Ready │ │ 🟢 Ready │
│ FW [● v2.2.0] │ ← 綠 │ FW [● KDP1] ⚙ │ ← 紅 + 連結
│ kn: 0x5E6F... │ │ kn: 0x1A2B... │
│ [ Workspace ] │ │ [ Workspace ] │
└──────────────────┘ └──────────────────┘
```
### 4.2 Badge 規格
| Badge 狀態 | 顏色 token | 文字 | 形狀 |
|-----------|-----------|------|------|
| Current | `color.success` bg + `color.success-foreground` fg | `● v2.2.0` 或對應版本 | Pill, padding `2 8`, radius `radius.full`, 11px medium |
| Older | `color.warning` bg + `color.warning-foreground` fg | `● v2.1.0` | 同上 |
| Legacy | `color.destructive` bg + `color.destructive-foreground` fg | `● KDP1` | 同上 |
**版本後綴規則**
- bundled current → 不加後綴(純版本號)
- 較舊的 bundled 版本 → 不加後綴(但用黃色提示)
- 非 KDP2 系列KDP1→ 顯示 `KDP1`(不顯示版本號,因為 KDP1 沒有 semver
### 4.3 Tooltiphover badge
| Badge | Tooltip 文案 |
|-------|------------|
| 綠 | 韌體為最新版本({version})。 |
| 黃 | 韌體版本較舊,建議升級。點擊前往韌體管理 → |
| 紅 | 此韌體為舊版 KDP1建議升級到 KDP2 以支援完整功能。點擊前往韌體管理 → |
### 4.4 點擊行為(⚙ icon
- 黃 / 紅 badge → badge 右側顯示 ⚙ icongrey、12×12
- 綠 badge → **不顯示** ⚙ icon避免吸引使用者進入降版
- 點 ⚙ icon → deep-link 跳轉到 `Settings > 韌體` 並 scroll 到該裝置卡片(並 flash 該卡片邊框 600ms 提示)
- 點 badge 本體 → 同 ⚙ icon 行為(提高 hit area
- 鍵盤badge / icon 都可 Tab 聚焦Enter 觸發 deep-link
### 4.5 為什麼 Devices 頁不放升級 / 切換按鈕
依 architect 42 §1.2 / R5-Q9 第 5 點:
- Devices 頁是使用者最常用的 Workspace 入口前置頁,避免誤觸高風險操作
- Badge + ⚙ deep-link 維持「資訊呈現」的職責
- 真正執行升級 / 切換的操作集中在 Settings → 韌體分頁、視覺更隆重
---
## 5. 升級流程 UX一鍵升級到 bundled latest
### 5.1 升級確認 Modal低風險、單擊 + 一次確認)
點裝置卡片的 `升級到 KDP2 v2.2.0` 按鈕後彈出:
```
┌─ 升級韌體 ──────────────────────────────────────┐
│ │
│ 升級 KL520 #1 的韌體 │
│ │
│ 當前版本KDP1 (legacy) │
│ 升級到: KDP2 v2.2.0 │
│ │
│ ⓘ 升級過程約需 30 秒,期間切勿拔除裝置或關閉 │
│ 應用程式。完成後裝置會自動重新識別。 │
│ │
│ [ 取消 ] [ 開始升級 ] │
│ │
└───────────────────────────────────────────────────┘
```
**規格**
- 寬度 480px、`color.card` 背景、`elevation.4` 陰影
- 標題 18px / SemiBold
- 「開始升級」按鈕 `primary` 變體(**不是 destructive**,因為升級是建議行為)
- 「取消」按鈕 `ghost`
- 可點外部關閉(升級不可中斷的警告留給降版 modal、升級單純確認意願即可
- ESC 可關閉
### 5.2 升級進度 Modal不可中斷
點「開始升級」後 modal 不關閉、切換為進度狀態:
```
┌─ 升級韌體 ──────────────────────────────────────┐
│ │
│ 升級 KL520 #1 的韌體 │
│ │
│ ████████████████░░░░░░░░░░░ 60% │
│ │
│ 階段 3 / 5寫入韌體 │
│ │
│ ⚠ 請勿拔除裝置 │
│ │
│ │
│ (沒有取消按鈕) │
│ │
└───────────────────────────────────────────────────┘
```
**規格**
- 進度條:高 8px、`color.primary` fill、`color.muted` track、圓角 `radius.full`
- 階段文字14px / regular / `color.muted-foreground`
- ⚠ banner紅色 destructive升級失敗風險仍存在
- **不顯示 ✕ / 取消按鈕**(不可中斷)
- 點擊外部 / ESC**不關閉**(捕獲事件並抖動 modal 200ms 提示)
- 預估時間(如可從後端取得):「預估剩餘 X 秒」附在階段文字下方
### 5.3 升級階段對應
依 architect 30 §M9-1 stage 對應。**stage 命名以本檔 §8 狀態機為準v2.2 對齊修正、取代早期草稿的 `scan/connect/loader/upgrade/verify`**
| 後端 stage`FirmwareProgress.Stage` | UI 顯示文字 | 預估佔比 |
|-----------|------------|---------|
| `preparing` | 階段 1 / 4準備偵測 + 連接裝置) | 5% |
| `loading` | 階段 2 / 4載入引導程式僅 KDP1→KDP2 路徑) | 20% |
| `flashing` | 階段 3 / 4寫入韌體 | 50% |
| `verifying` | 階段 4 / 4驗證完成 | 90% |
| `done` | 階段完成 | 100% |
如果是 KDP2→KDP2 同代升級(`loading` 引導程式路徑跳過),階段壓縮為 3 階段(`preparing``flashing``verifying`UI 顯示文字改為 `階段 n / 3`
**stage 名稱對齊備註**
- 早期 Architect TDD 曾用 `connecting / loading_loader / loading_firmware / verifying`、Design 早期草稿曾用 `scan / connect / loader / upgrade / verify`
- v2.2 三方對齊後、**以本檔 §8 狀態機的 `preparing / loading / flashing / verifying` 為唯一 source of truth**
- 後端 `FirmwareProgress.Stage` 欄位、Frontend i18n key§9.6)、失敗 stage 對應§7.1)全部用此命名
### 5.4 升級成功 Toast
升級成功後 modal 關閉、右上角 toast
```
┌──────────────────────────────────────────┐
│ ✓ KL520 #1 升級成功 │
│ 從 KDP1 升級到 KDP2 v2.2.0(耗時 28 秒)│
└──────────────────────────────────────────┘
```
- variant: success
- 停留 6 秒(比預設 3 秒長、讓使用者讀完細節)
- 同時對應裝置卡片自動重新整理rescan APIFW badge 變綠
### 5.5 升級失敗
詳見 §7「失敗復原 UX」。
---
## 6. 版本切換流程 UX含降版、高風險
依 R5-Q9 第 5 點「使用『輸入 DOWNGRADE 字串』二次確認」+ architect 42 §5.3。
### 6.1 進入二次確認 Modal
使用者在 §3.3 accordion 選了非 current 版本、點「切換到此版本」後:
```
┌─ 切換到 KDP2 v2.1.0 ─────────────────────────────────────────┐
│ │
│ ⚠ 確認切換到舊版韌體? │
│ │
│ 你正在將 KL520 #1 的韌體切換到較舊的版本。 │
│ │
│ ┌──────────────────────┬──────────────────────┐ │
│ │ 當前版本 │ 將切換到 │ │
│ ├──────────────────────┼──────────────────────┤ │
│ │ KDP2 v2.2.0 │ KDP2 v2.1.0 │ │
│ │ 2025-03 發行 │ 2024-08 發行 │ │
│ └──────────────────────┴──────────────────────┘ │
│ │
│ 切換到舊版可能導致: │
│ • 部分新版 .nef 模型無法載入 │
│ • 推論效能與穩定性下降 │
│ • 切換過程中拔除裝置可能導致裝置損毀 │
│ │
│ 預估時間:約 30 秒 │
│ │
│ 為確認你了解風險請輸入「DOWNGRADE」
│ ┌──────────────────────────────────────────┐ │
│ │ │ │
│ └──────────────────────────────────────────┘ │
│ (大小寫需完全相同) │
│ │
│ [ 取消 ] [ 確認切換 ] │
│ │
└────────────────────────────────────────────────────────────────┘
```
**規格**
- Modal 寬度560px
- 標題:「⚠ 確認切換到舊版韌體KDP1 目標時改為「⚠ 確認切換到 KDP1
- 比較表格:兩欄、表頭 `color.muted` 背景、表身 padding `space.3`
- 風險列表:用 `<ul>`、12px / `color.foreground`、無 icon避免過載
- 「DOWNGRADE」輸入
- `<input type="text" autocomplete="off" spellcheck="false">`
- placeholder: `輸入 DOWNGRADE 確認`i18n key 換對應 EN: `Type DOWNGRADE to confirm`
- 輸入框邊框:未輸入 / 輸入錯誤時 `color.warning`;輸入正確時 `color.success`
- 輸入正確時:邊框變綠 + 右側顯示 `✓` icon16×16、`color.success`
- 「確認切換」按鈕:
- variant: `destructive`
- disabled 條件input 值不等於字面 `DOWNGRADE`
- 大小:`md`
- 「取消」按鈕:`ghost` `md`
- **可點外部關閉嗎?****不能**(捕獲事件、抖動 200ms 提示「請使用『取消』按鈕關閉」ESC 可關閉與外部點擊行為不同——ESC 是鍵盤使用者的明確意圖、外部點擊較可能誤觸)
### 6.2 切換到 KDP1 的額外警告
當目標版本是 KDP1legacy風險列表多加一條
```
• KDP1 不支援多模型載入,部分 visionA-local 功能將無法使用
```
並在表格上方加紅色 banner
```
┌──────────────────────────────────────────────────┐
│ 🚫 KDP1 是已棄用的舊版韌體,建議僅用於 │
│ 與第三方工具相容性測試。 │
└──────────────────────────────────────────────────┘
```
- banner 背景 `color.destructive/10`、邊框 `color.destructive/30`、icon `color.destructive`
- 文字 13px / SemiBold / `color.destructive`
### 6.3 切換進行中 Modal
確認後 modal **不關閉**、直接切換為進度狀態(與 §5.2 升級進度同設計、但有差異):
```
┌─ 切換韌體 ──────────────────────────────────────┐
│ │
│ 切換 KL520 #1 至 KDP2 v2.1.0 │
│ │
│ ████████████░░░░░░░░░░░░░░░ 45% │
│ │
│ 階段 3 / 5寫入韌體 │
│ │
│ ⚠ 請勿拔除裝置、請勿關閉應用程式 │
│ 切換韌體不可中斷,中斷可能導致裝置永久損毀。 │
│ │
│ │
└───────────────────────────────────────────────────┘
```
**與升級進度的差異**
- 警告語更強(「永久損毀」用詞)
- 警告區域更大、紅色背景(不只是紅字)
- 仍然沒有取消按鈕
**全螢幕 hover 攔截**:當切換進行中時、視窗任何地方(除了 modal 內)的點擊都被攔截、滑鼠 cursor 變 `not-allowed`、確保使用者不會誤關視窗。
### 6.4 切換成功
切換成功後與升級成功類似toast
```
┌──────────────────────────────────────────┐
│ ✓ KL520 #1 韌體已切換到 KDP2 v2.1.0 │
│ (耗時 32 秒、可能需要重新插拔裝置) │
└──────────────────────────────────────────┘
```
variant: success、停留 8 秒(含「可能需要重新插拔」提示,使用者要讀完)。
---
## 7. 失敗復原 UX升級 / 切換共用)
依 architect 42 §5.5 失敗類型分類。
### 7.1 失敗類型對應
UI 失敗分類以 **stage**§5.3 / §8 狀態機)為主軸、加上 timeout / disconnect 兩個橫切性失敗,共 7 種使用者面友善訊息。對應 backend `FirmwareProgress.Reason` 的細分原因見 **TDD §3.4**Architect TDD v2.2 新增的 stage→errorCode→reason 對應表)。
| 失敗 stage / 類型 | 對應 backend stage | 使用者面友善訊息 | 建議動作 |
|------------------|------------------|---------------|---------|
| `preparing` 找不到裝置scan_not_found | `preparing` | 找不到裝置,可能已斷開。 | [ 重新插拔後重試 ] |
| `preparing` 連接失敗connect_failed | `preparing` | 無法連接裝置,請確認 USB 連接後重試。 | [ 重試 ] |
| `loading` 引導程式寫入失敗loader_write_failed | `loading` | 引導程式載入失敗。請拔除裝置後重新插入並重試。 | [ 拔插後重試 ] |
| `flashing` 寫入失敗upgrade_mid_failed | `flashing` | 韌體寫入失敗。可能是裝置硬體異常,請聯絡技術支援。 | [ 複製錯誤訊息 ] [ 取得協助 ] |
| `verifying` 失敗verify_mismatch | `verifying` | 韌體已寫入但驗證未通過。請拔除裝置後重新插入並到「硬體」分頁重新掃描。 | [ 拔插後重新掃描 ] |
| Timeout>60s | 任一 stage、`reason=timeout` | 操作超時。可能成功也可能未完成,請拔除裝置後重新掃描以確認狀態。 | [ 拔插後重新掃描 ] |
| Disconnect during op | 任一 stage、`reason=disconnect_during_op` | 裝置在操作過程中斷開,狀態未知。請拔除後重新插入裝置。 | [ 重新插拔後重試 ] |
**Reason 欄位對應**Frontend 收到 WebSocket error event 時、依 `FirmwareProgress.Stage + FirmwareProgress.Reason` 兩欄位查表決定顯示哪條 friendly message。完整 backend reason 列舉與 errorCode 對應見 TDD §3.4。
### 7.2 失敗 Modal Wireframe
```
┌─ 升級失敗 / 切換失敗 ────────────────────────────────┐
│ │
│ ⚠ 韌體寫入失敗 │
│ │
│ {對應友善訊息} │
│ │
│ 錯誤代碼fw_loading_loader_write_failed_E102 │
│ │
│ ─ 技術資訊 ▾ ─ │
│ ┌──────────────────────────────────────────┐ │
│ │ stage: loading │ │
│ │ reason: loader_write_failed │ │
│ │ device: KL520 port 1 │ │
│ │ before: KDP1 │ │
│ │ raw_error: kp_update_kdp_firmware... │ │
│ │ duration_ms: 18203 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ [ 複製錯誤訊息 ] │
│ │
│ [ 關閉 ] [ 對應的建議動作按鈕 ] │
│ │
└───────────────────────────────────────────────────────┘
```
**規格**
- 寬度 560px
- icon 24×24、`color.destructive`
- 標題 18px / SemiBold / `color.destructive`
- 友善訊息 14px / regular / `color.foreground`、最多 3 行
- 錯誤代碼12px / mono / `color.muted-foreground`(給技術支援辨識用)
- 「技術資訊」collapsible、預設收合
- 展開內容用 `font.family.mono` 12px、`color.surface-1` 背景、padding 12、圓角 `radius.md`、可選取複製
- 「複製錯誤訊息」:複製整段技術資訊到剪貼簿、複製後按鈕短暫變「已複製 ✓」2 秒
- modal 不自動關閉(讓使用者讀完訊息)
### 7.3 部分成功(罕見但需處理)
若後端回傳 `verify` 階段失敗、但韌體已寫入before/after 版本不同UX 邏輯:
- 視為「升級可能成功、但驗證未確認」
- toast 提示為 warning非 destructive「韌體可能已升級但無法驗證請重新插拔裝置後到『硬體』分頁重新掃描。」
- 裝置卡片暫時隱藏 FW badge顯示「狀態未知」灰色 badge、直到使用者重新掃描
---
## 8. 狀態機(裝置卡片 + Modal 整合)
```
[idle]
│ 使用者點「升級」/「版本切換 → 切換到此版本」
[confirming] ── 使用者點「取消」 / ESC ──▶ [idle]
│ 使用者點「開始升級」 / 輸入 DOWNGRADE + 確認
[preparing] ← 後端 FirmwareProgress.Stage = "preparing"(涵蓋偵測 + 連接)
[loading] ← 後端 FirmwareProgress.Stage = "loading"(僅 KDP1→KDP2 路徑)
[flashing] ← 後端 FirmwareProgress.Stage = "flashing"
[verifying] ← 後端 FirmwareProgress.Stage = "verifying"
├── 成功 ──▶ [success_toast] ──▶ rescan ──▶ [idle]badge 更新)
└── 失敗 ──▶ [error_modal] ── 使用者點「重試」──▶ [preparing]
└── 使用者點「關閉」──▶ [idle](裝置卡 badge 維持原狀或變灰)
```
**狀態 → UI 對應**
| 狀態 | 卡片視覺 | Modal 視覺 |
|------|---------|----------|
| idle | 正常 | 不顯示 |
| confirming | 卡片變灰、按鈕 disabled | 確認 modal 顯示 |
| preparing / loading / flashing / verifying | 卡片變灰、按鈕 disabled、有 inline progress bar修可選、與 modal 重複) | 進度 modal 顯示,進度條對應 stage |
| success_toast | 短暫高亮(綠 ring 1s後復原 | 關閉、toast 出現 |
| error_modal | 卡片變灰 | 失敗 modal 顯示 |
---
## 9. i18n keys 清單namespace: `settings.firmware`
依 architect 42 §5.6 估算 46 keys、實際定版 **51 keys**v2.2 對齊修正後、含 settings 分頁 label 與 Devices 頁 badge tooltip對齊細節見本節末尾備註
### 9.1 頁面結構
| Key | zh-TW | en |
|-----|-------|----|
| `settings.tabs.firmware` | 韌體 | Firmware |
| `settings.firmware.title` | 韌體管理 | Firmware Management |
| `settings.firmware.description` | 管理已連接 Kneron 裝置的韌體版本。 | Manage firmware versions for connected Kneron devices. |
| `settings.firmware.empty.title` | 尚未偵測到 Kneron 裝置 | No Kneron device detected |
| `settings.firmware.empty.description` | 插上 USB 裝置後,到「硬體」分頁點擊「重新掃描」便會出現在這裡。 | Plug in a USB device and click "Rescan" on the Hardware tab to see it here. |
| `settings.firmware.empty.cta` | 前往硬體分頁 | Go to Hardware |
| `settings.firmware.helpText` | 沒看到裝置?檢查 USB 連接後到「硬體」分頁重新掃描。 | Don't see your device? Check USB connection then rescan on the Hardware tab. |
### 9.2 裝置卡片
| Key | zh-TW | en |
|-----|-------|----|
| `settings.firmware.card.label.knNumber` | kn_number | kn_number |
| `settings.firmware.card.label.currentVersion` | 當前版本 | Current version |
| `settings.firmware.card.label.latestVersion` | 最新版本 | Latest version |
| `settings.firmware.card.tag.current` | (current) | (current) |
| `settings.firmware.card.tag.latest` | (建議) | (recommended) |
| `settings.firmware.card.tag.legacy` | (legacy) | (legacy) |
| `settings.firmware.card.status.legacy` | KDP1 (legacy) — 建議升級到 KDP2 | KDP1 (legacy) — Upgrade to KDP2 recommended |
| `settings.firmware.card.status.older` | 有可用較新版本 | Newer version available |
| `settings.firmware.card.status.current` | 裝置使用最新韌體 | Device is up to date |
| `settings.firmware.card.action.upgrade` | 升級到 {version} | Upgrade to {version} |
| `settings.firmware.card.action.switchVersion` | 版本切換 | Switch version |
### 9.3 版本切換 accordion
| Key | zh-TW | en |
|-----|-------|----|
| `settings.firmware.switch.warningTitle` | 切換到舊版可能導致: | Switching to an older version may cause: |
| `settings.firmware.switch.risk.modelIncompat` | 部分新版 .nef 模型無法載入 | Some newer .nef models may not load |
| `settings.firmware.switch.risk.perfDegrade` | 推論效能與穩定性下降 | Reduced inference performance and stability |
| `settings.firmware.switch.risk.kdp1Limit` | KDP1 無法執行多模型載入 | KDP1 does not support multi-model loading |
| `settings.firmware.switch.duration` | 切換過程約需 {duration},切勿拔除裝置。 | Switching takes ~{duration}. Do not unplug the device. |
| `settings.firmware.switch.availableVersions` | 可用版本: | Available versions: |
| `settings.firmware.switch.versionNote.releasedOn` | 發布於 {date} | Released {date} |
| `settings.firmware.switch.versionNote.compatibleWithOld` | 與舊模型相容 | Compatible with older models |
| `settings.firmware.switch.versionNote.thirdPartyOnly` | 僅供 third-party 工具相容測試 | For third-party tool compatibility testing only |
| `settings.firmware.switch.action.cancel` | 取消 | Cancel |
| `settings.firmware.switch.action.switchToThis` | 切換到此版本 | Switch to this version |
### 9.4 升級確認 modal
| Key | zh-TW | en |
|-----|-------|----|
| `settings.firmware.upgradeModal.title` | 升級韌體 | Upgrade firmware |
| `settings.firmware.upgradeModal.heading` | 升級 {deviceName} 的韌體 | Upgrade firmware on {deviceName} |
| `settings.firmware.upgradeModal.from` | 當前版本 | Current version |
| `settings.firmware.upgradeModal.to` | 升級到 | Upgrading to |
| `settings.firmware.upgradeModal.warning` | 升級過程約需 {duration},期間切勿拔除裝置或關閉應用程式。完成後裝置會自動重新識別。 | Upgrade takes ~{duration}. Do not unplug the device or close the application. The device will be re-detected automatically when done. |
| `settings.firmware.upgradeModal.action.start` | 開始升級 | Start upgrade |
### 9.5 降版二次確認 modal
| Key | zh-TW | en |
|-----|-------|----|
| `settings.firmware.downgradeModal.title` | 切換到 {version} | Switch to {version} |
| `settings.firmware.downgradeModal.heading` | ⚠ 確認切換到舊版韌體? | ⚠ Confirm switching to an older firmware? |
| `settings.firmware.downgradeModal.headingKdp1` | ⚠ 確認切換到 KDP1 | ⚠ Confirm switching to KDP1? |
| `settings.firmware.downgradeModal.intro` | 你正在將 {deviceName} 的韌體切換到較舊的版本。 | You are about to switch the firmware on {deviceName} to an older version. |
| `settings.firmware.downgradeModal.kdp1Banner` | KDP1 是已棄用的舊版韌體,建議僅用於與第三方工具相容性測試。 | KDP1 is a deprecated legacy firmware. Recommended only for third-party tool compatibility testing. |
| `settings.firmware.downgradeModal.compareHeader.from` | 當前版本 | Current version |
| `settings.firmware.downgradeModal.compareHeader.to` | 將切換到 | Switching to |
| `settings.firmware.downgradeModal.risk.brick` | 切換過程中拔除裝置可能導致裝置損毀 | Unplugging the device during the process may damage it |
| `settings.firmware.downgradeModal.duration` | 預估時間:約 {duration} | Estimated time: ~{duration} |
| `settings.firmware.downgradeModal.confirmPrompt` | 為確認你了解風險請輸入「DOWNGRADE」 | To confirm you understand the risks, type "DOWNGRADE": |
| `settings.firmware.downgradeModal.confirmPlaceholder` | 輸入 DOWNGRADE 確認 | Type DOWNGRADE to confirm |
| `settings.firmware.downgradeModal.confirmHelpText` | (大小寫需完全相同) | (case-sensitive) |
| `settings.firmware.downgradeModal.action.confirm` | 確認切換 | Confirm switch |
| `settings.firmware.downgradeModal.escAttemptHint` | 請使用「取消」按鈕關閉 | Use the "Cancel" button to close |
### 9.6 進度 modal
| Key | zh-TW | en |
|-----|-------|----|
| `settings.firmware.progress.upgradeTitle` | 升級韌體 | Upgrading firmware |
| `settings.firmware.progress.switchTitle` | 切換韌體 | Switching firmware |
| `settings.firmware.progress.deviceHeading.upgrade` | 升級 {deviceName} 的韌體 | Upgrading firmware on {deviceName} |
| `settings.firmware.progress.deviceHeading.switch` | 切換 {deviceName} 至 {version} | Switching {deviceName} to {version} |
| `settings.firmware.progress.stage.preparing` | 階段 {n} / {total}:準備(偵測 + 連接裝置) | Stage {n} / {total}: Preparing |
| `settings.firmware.progress.stage.loading` | 階段 {n} / {total}:載入引導程式 | Stage {n} / {total}: Loading bootloader |
| `settings.firmware.progress.stage.flashing` | 階段 {n} / {total}:寫入韌體 | Stage {n} / {total}: Flashing firmware |
| `settings.firmware.progress.stage.verifying` | 階段 {n} / {total}:驗證完成 | Stage {n} / {total}: Verifying |
| `settings.firmware.progress.warning.upgrade` | ⚠ 請勿拔除裝置 | ⚠ Do not unplug the device |
| `settings.firmware.progress.warning.switch` | ⚠ 請勿拔除裝置、請勿關閉應用程式。切換韌體不可中斷,中斷可能導致裝置永久損毀。 | ⚠ Do not unplug the device or close the application. Firmware switching cannot be interrupted; interruption may permanently damage the device. |
| `settings.firmware.progress.estimatedRemaining` | 預估剩餘 {seconds} 秒 | ~{seconds}s remaining |
### 9.7 成功 toast
| Key | zh-TW | en |
|-----|-------|----|
| `settings.firmware.success.upgrade` | {deviceName} 升級成功 | {deviceName} upgrade succeeded |
| `settings.firmware.success.upgradeDetail` | 從 {from} 升級到 {to}(耗時 {duration} | From {from} to {to} (took {duration}) |
| `settings.firmware.success.switch` | {deviceName} 韌體已切換到 {version} | {deviceName} firmware switched to {version} |
| `settings.firmware.success.switchDetail` | (耗時 {duration}、可能需要重新插拔裝置) | (took {duration}, may need to unplug and reconnect) |
| `settings.firmware.partialSuccess.toast` | 韌體可能已升級但無法驗證,請重新插拔裝置後到「硬體」分頁重新掃描。 | Firmware may have been written but verification failed. Please unplug, reconnect, and rescan on the Hardware tab. |
### 9.8 失敗訊息
| Key | zh-TW | en |
|-----|-------|----|
| `settings.firmware.error.modalTitle` | 韌體操作失敗 | Firmware operation failed |
| `settings.firmware.error.message.preparing.scanNotFound` | 找不到裝置,可能已斷開。 | Device not found, possibly disconnected. |
| `settings.firmware.error.message.preparing.connectFailed` | 無法連接裝置,請確認 USB 連接後重試。 | Cannot connect to device. Please check the USB connection and retry. |
| `settings.firmware.error.message.loading` | 引導程式載入失敗。請拔除裝置後重新插入並重試。 | Bootloader load failed. Please unplug, reconnect, and retry. |
| `settings.firmware.error.message.flashing` | 韌體寫入失敗。可能是裝置硬體異常,請聯絡技術支援。 | Firmware write failed. Possible hardware issue, please contact technical support. |
| `settings.firmware.error.message.verifying` | 韌體已寫入但驗證未通過。請拔除裝置後重新插入並到「硬體」分頁重新掃描。 | Firmware was written but verification failed. Please unplug, reconnect, and rescan on the Hardware tab. |
| `settings.firmware.error.message.timeout` | 操作超時。可能成功也可能未完成,請拔除裝置後重新掃描以確認狀態。 | Operation timed out. The operation may or may not have completed. Please unplug, reconnect, and rescan to confirm. |
| `settings.firmware.error.message.disconnect` | 裝置在操作過程中斷開,狀態未知。請拔除後重新插入裝置。 | Device disconnected during operation. State unknown. Please unplug and reconnect. |
| `settings.firmware.error.errorCode` | 錯誤代碼:{code} | Error code: {code} |
| `settings.firmware.error.technicalInfo` | 技術資訊 | Technical info |
| `settings.firmware.error.copyError` | 複製錯誤訊息 | Copy error info |
| `settings.firmware.error.copied` | 已複製 ✓ | Copied ✓ |
| `settings.firmware.error.action.close` | 關閉 | Close |
| `settings.firmware.error.action.retry` | 重試 | Retry |
| `settings.firmware.error.action.replugRetry` | 重新插拔後重試 | Unplug and retry |
| `settings.firmware.error.action.rescan` | 拔插後重新掃描 | Unplug and rescan |
| `settings.firmware.error.action.getHelp` | 取得協助 | Get help |
### 9.9 Devices 頁 FW badge tooltip
| Key | zh-TW | en |
|-----|-------|----|
| `devices.card.fwBadge.tooltipCurrent` | 韌體為最新版本({version})。 | Firmware is up to date ({version}). |
| `devices.card.fwBadge.tooltipOlder` | 韌體版本較舊,建議升級。點擊前往韌體管理 → | Firmware is older. Click to manage firmware → |
| `devices.card.fwBadge.tooltipLegacy` | 此韌體為舊版 KDP1建議升級到 KDP2 以支援完整功能。點擊前往韌體管理 → | Legacy KDP1 firmware. Upgrade to KDP2 for full feature support. Click to manage firmware → |
| `devices.card.fwBadge.deepLinkA11y` | 前往韌體管理 — {deviceName} | Go to firmware management — {deviceName} |
**合計新增 i18n keys****51 個**zh-TW 與 en 各一套)。
> v2.2 對齊修正:原稿標 52、本輪因 stage 命名統一§5.3 / §8將 §9.6 從 5 個 stage key 減為 4 個(`preparing/loading/flashing/verifying`),同時 §9.8 將 `preparing` 拆為 `scanNotFound` / `connectFailed` 兩個 sub-key總數不變
---
## 10. 無障礙考量A11y
| 項目 | 設計 |
|------|------|
| Settings 分頁切換 | 沿用既有 Tabs 元件,`role="tablist"` / `role="tab"` / `role="tabpanel"``←` `→` 切換 tab |
| 裝置卡片結構 | `<section aria-labelledby="fw-device-{id}">`、標題用 `<h3 id="fw-device-{id}">{deviceName}</h3>` |
| FW 狀態圓點 | 不單靠顏色傳達——圓點旁同時有文字標籤(`KDP1 (legacy)` 等);圓點本身加 `aria-hidden="true"` 避免重複朗讀 |
| 版本 radio | 每個 radio 用 `<label>` 包住 visible 文字、`fieldset` + `legend` 包整組(`<legend>` 用 sr-only class 隱藏視覺、保留 screen reader |
| 「切換到此版本」disabled 狀態 | 加 `aria-describedby="hint-fw-{id}"`、hint 用 sr-only 寫「請先選擇與當前不同的版本」 |
| Upgrade modal | `role="dialog" aria-modal="true" aria-labelledby="..." aria-describedby="..."`、focus trap、ESC 可關 |
| Downgrade modal | 同上 + 額外 `aria-describedby` 指向風險列表 `<ul>` 的 idscreen reader 先讀標題、再讀風險) |
| DOWNGRADE 輸入 | `<input aria-label="輸入 DOWNGRADE 確認" aria-describedby="downgrade-hint">`、hint 「大小寫需完全相同」、輸入正確時 `aria-invalid="false"`、錯誤時 `aria-invalid="true"` |
| 進度 modal | `<div role="status" aria-live="polite">` 包進度文字(不用 `aria-live="assertive"`、避免每秒朗讀) |
| 進度條 | `<div role="progressbar" aria-valuenow={percent} aria-valuemin="0" aria-valuemax="100" aria-valuetext="階段 {n}/{total}">` |
| 失敗 modal | `role="alertdialog"`(比 dialog 更強的語意、screen reader 會主動朗讀) |
| 鍵盤導航 | Tab 順序:分頁切換 → 第 1 張卡片的升級按鈕 → 第 1 張卡片的版本切換按鈕 → 第 2 張卡片… |
| Focus ring | 沿用 `ring.2 · color.ring`、2px outline-offset |
| 色彩對比 | FW badge 紅 / 黃 / 綠對白色文字皆 ≥ 4.5:1critical 信號不妥協) |
| 觸控目標 | 升級按鈕、版本切換按鈕、⚙ icon 全部 ≥ 44×44px tap area即使視覺更小、padding 補足) |
| Reduced motion | `prefers-reduced-motion: reduce` → 進度條改靜態顯示百分比文字、modal 動畫 0ms、卡片高亮 1s 改為直接最終色(不淡入淡出) |
---
## 11. Design Tokens 沿用 / 新增
### 11.1 沿用既有 tokens`spec/07-design-tokens.md`
所有色彩、字級、間距、圓角、elevation、focus ring 全部沿用 v1 既有 token。
具體沿用的 semantic token
- `color.destructive` / `color.warning` / `color.success` / `color.info`
- `color.muted` / `color.muted-foreground` / `color.foreground`
- `color.card` / `color.background` / `color.border`
- `color.primary` / `color.primary-foreground`
- `color.ring`
- `radius.md` / `radius.lg` / `radius.full`
- `space.2` ~ `space.8`
- `font.size.xs` / `sm` / `base` / `lg`
- `font.weight.medium` / `semibold`
- `font.family.mono`(技術資訊區用)
### 11.2 新增 component-level tokens本檔提案
只在「需要」時新增、避免過度膨脹 token 系統:
| 新 token | 推導自 | 用途 |
|---------|-------|------|
| `color.fw-badge.current.bg` | `color.success` | FW badge 綠底 |
| `color.fw-badge.current.fg` | `color.background`(白) | FW badge 綠底文字 |
| `color.fw-badge.older.bg` | `color.warning` | FW badge 黃底 |
| `color.fw-badge.older.fg` | `color.foreground` | FW badge 黃底文字(黑、確保對比) |
| `color.fw-badge.legacy.bg` | `color.destructive` | FW badge 紅底 |
| `color.fw-badge.legacy.fg` | `color.background`(白) | FW badge 紅底文字 |
**為什麼要拆 component token**
- Light / Dark 模式下warning 黃底配什麼前景需獨立調(直接用 `color.warning-foreground` 可能對比不足)
- 未來如果調 badge 視覺、不需要動 semantic token 影響其他元件
**推導表Light / Dark**
| Token | Light value | Dark value | 對比比率(對 fg |
|-------|------------|-----------|----------------|
| `color.fw-badge.current.bg` | `oklch(0.65 0.17 145)` | `oklch(0.75 0.17 145)` | 4.7 : 1 |
| `color.fw-badge.current.fg` | `oklch(1 0 0)` | `oklch(0.145 0 0)` | — |
| `color.fw-badge.older.bg` | `oklch(0.75 0.18 85)` | `oklch(0.80 0.18 85)` | 5.2 : 1 |
| `color.fw-badge.older.fg` | `oklch(0.145 0 0)` | `oklch(0.145 0 0)` | — |
| `color.fw-badge.legacy.bg` | `oklch(0.577 0.245 27.325)` | `oklch(0.704 0.191 22.216)` | 4.8 : 1 |
| `color.fw-badge.legacy.fg` | `oklch(1 0 0)` | `oklch(0.145 0 0)` | — |
> **對比比率實測責任**v2.2 三方互審後對齊):
>
> 表中 4.7:1 / 5.2:1 / 4.8:1 為 Design 在 oklch 色彩空間的**推算值**、提供給後續實作 / 驗收作參考。
>
> **實測為 M9-12 Frontend Agent 責任**M9-12 Frontend 在落地 6 個 component token 到 `frontend/src/app/globals.css` 後、於 Design QA 階段用 axe-core / Lighthouse / Chrome DevTools contrast checker 工具驗證真實對比 ≥ 4.5:1WCAG AA、特別需注意 Dark 模式下 `legacy.fg` 黑底紅 `oklch(0.704 0.191 22.216)``oklch(0.145 0 0)` 文字。實測未達標時、Frontend 與 Design 對齊調整 token 數值。
>
> **此項不需 Architect 互審回應**——是 implement-time verification、不是 design-time decision。
---
## 12. R-FW 風險的 Design 對策
### 12.1 R-FW-11一般使用者誤觸降版 brick 風險
依 architect 42 §6 風險評估、Design 提供多層 mitigation
| 風險路徑 | Design 緩解措施 | 對應 §章節 |
|---------|--------------|----------|
| 點到「版本切換」就直接降 | 二次確認 modal、需輸入「DOWNGRADE」字串 | §6.1 |
| 沒讀風險警告就降 | 風險列表大字、`color.warning` 背景區塊、KDP1 額外紅色 banner | §6.1 §6.2 |
| 降到 KDP1 後發現功能不可用 | KDP1 額外警告語、明示「不支援多模型」 | §6.2 |
| 降版進行中拔 USB | 全螢幕 hover 攔截 + 紅色 banner「永久損毀」 + 不可關閉 modal | §6.3 |
| 降版進行中關 app | 同上、加上 `beforeunload` event handler 攔截Frontend 實作) | §6.3 + 給 Frontend 提示 |
| 升錯版本當降版 | Architect 42 §2.4 driver 層 guard拒絕目標版本 >= current這是後端職責 | UI 不需處理、driver 拒絕後顯示「無效操作」error modal |
| 找不到救援路徑 | 失敗 modal 提供「取得協助」連結 + 「複製錯誤訊息」給技術支援 | §7.2 |
### 12.2 R-FW-11.5Design 自提使用者輸入「downgrade」小寫繞過
依 §6.1input 嚴格 case-sensitive 比對字面 `DOWNGRADE`、小寫 / 全形 / 半形 / 空白都不接受。
**Frontend 實作提示**:用 `event.target.value === 'DOWNGRADE'`(嚴格相等)、不要用 `.toUpperCase()` 後比對。
### 12.3 R-FW-11.6Design 自提):意外觸發 deep-link 跳轉
從 Devices 頁的 ⚙ icon deep-link 到 Settings → 韌體後、**不自動展開「版本切換」accordion**——只 highlight 卡片邊框 600ms。
**理由**deep-link 對應的是「使用者想關注此裝置的韌體狀態」、不是「使用者已決定要降版」。展開 accordion 等於暗示「快來降版」、與整體謹慎策略矛盾。
---
## 13. 與既有 design v2.1 的銜接點
### 13.1 與 `v2/settings-update.md` 的銜接
- Settings 分頁結構從 4 變 5、第 3 個分頁新增「韌體」
- v2.1 既有的「一般」「硬體」「模型」「進階」內容**不變**、只是順序調整
- 本檔不重寫 settings-update.md、但 settings-update.md 應在下次補丁加註「分頁順序見 firmware-management.md §2」
- **Settings 整體分頁切換高度**:保持與既有分頁一致(依 `spec/03-wireframes.md §3.X Settings` 既定高度),新分頁內容若超出視窗高度則加 scroll不擴張 Settings 整體高度)
### 13.2 與 `spec/03-wireframes.md §3.3 Devices` 的銜接
- Devices 卡片現有結構 5 行(標題 / status / FW / kn / button保持
- FW 那行從純文字 `FW 2.1.0` 改為 `FW [● v2.2.0]` pill badge+ 黃/紅時加 ⚙ icon
- pill badge 寬度依版本字串自適應、但 FW 行整體高度不變(仍 18px
- 點 badge / ⚙ 行為見 §4.4
### 13.3 與 `v2/control-panel.md` 的銜接
控制台**不涉及 FW 管理**——FW 管理在瀏覽器端 Settings、不在控制台。
- 升級 / 切換 modal 的 dialog 不會在控制台顯示
- 但升級 / 切換的 log後端 stage 訊息)會出現在控制台 log panelINFO / WARN / ERROR
- log 等級依 architect 30 §M9-3 設計、Design 不重複定
### 13.4 與 `spec/08-states.md`(全域錯誤分級)的銜接
韌體升級 / 切換失敗的 **嚴重級別屬於 Error**08 表的 `Error` 級別、非 Critical
- 不擋整個 app
- 顯示 modal、可關閉
- 提供重試 + 排錯提示
- 對應 08 §8.3 inline error 規格、但因為涉及裝置操作、用 modal 而非 inline
---
## 14. v2.1 → v2.2 索引變更摘要(給 design-spec-v2.md
### 14.1 索引變更
`design-spec-v2.md` §文件結構 表格、加一行:
```markdown
| **v2.7** | **韌體管理**v2.2 新增) | **`v2/firmware-management.md`** | **Kneron dongle FW 升級 + 版本切換完整規格Settings 新分頁、Devices 頁 FW badge、二次確認流程、51 個 i18n keys、R-FW-11 設計緩解** |
```
### 14.2 版本標頭變更
- 版本:`v2.1``v2.2`
- 子標題增補「v2.22026-05-24 補丁)」吸收 R5-Q9 翻案 + B2 階段降版面向一般使用者的設計規格
### 14.3 「v2 → v2.2 差異總覽」表格新增段
```markdown
## v2.1 → v2.2 差異總覽2026-05-24 補丁)
v2.2 是吸收 R5-Q9 翻案後的補丁,新增 FW 管理面向一般使用者的設計規格。
| 面向 | v2.1 | v2.22026-05-24 補丁)| 來源 |
|------|------|---------------------|------|
| Settings 分頁數 | 4一般 / 硬體 / 模型 / 進階)| **5**(一般 / 硬體 / **韌體** / 模型 / 進階)| R5-Q9 + architect 42 |
| Devices 頁 FW 顯示 | 純文字 `FW 2.1.0` | **pill badge**(紅 / 黃 / 綠三色)+ deep-link ⚙ icon | R5-Q9 第 5 點 |
| FW 升級 UX | 未設計 | **單步驟確認 + 進度 modal**(不可中斷)| architect 30 §M9-4 |
| FW 降版 UX | 未設計 | **二次確認**輸入「DOWNGRADE」字串+ 進度 modal + 全螢幕 hover 攔截 | architect 42 §5.3 |
| 失敗復原 UX | 未設計 | **8 種失敗類型對應 friendly message + 復原行動** | architect 42 §5.5 |
| 新增 i18n keys | — | **51 keys**zh-TW + en| 本檔 §9 |
| 新增 component tokens | — | **6 個 fw-badge token** | 本檔 §11.2 |
```
### 14.4 「給 Orchestrator 的問題(懸而未決)」新增
```markdown
**v2.2 新增懸而未決**
4. **降版 modal 字串「DOWNGRADE」未來 i18n 化的處理**:當前 Design 規格要求使用者輸入字面字串 `DOWNGRADE`en中文使用者也輸入這個字。若未來改為 i18n如中文使用者輸入「降版」需與 Architect / Frontend 確認後端 confirmToken 是否同步調整(目前 architect 42 §2.3 後端固定接受 `confirmToken: "DOWNGRADE"`)。建議**短期維持英文字串**(跨語言一致、避免後端多語對應)、長期看是否分語系 token。
5. **bundled 版本清單的「發布日期」資料源**§3.3 範例顯示「2024-08 發行」等文案,但目前 architect 42 §2.1 `FirmwareVersion.ReleaseDate` 為 optional。需 Backend 在 bundle firmware 時準備 metadata每個 version 目錄附 `META.json` 含 releaseDate / notes。如果 metadata 缺、UI fallback 為不顯示日期、只顯示版本號。
6. ~~**降版進行中關閉 Wails 控制台視窗會發生什麼**~~ **已解決、v2.2 三方對齊)**:依 R5-2 控制台關閉 = 結束 server。降版進行中關控制台 = 中斷降版 = 可能 brick。**解法(三方對齊)**(a) Architect 在 TDD §8.6 補 server graceful shutdown 拒絕邏輯(偵測 active firmware task → 拒絕 shutdown → 通知 Wails 控制台);(b) Design 在 `v2/control-panel.md` 補對應 UI——降版進行中按關閉視窗 → 跳警告 modal「韌體更新進行中、強制關閉會造成裝置損毀」+ 預估剩餘時間 + 兩個按鈕「繼續等待」主、「強制關閉」destructive
```
---
## 15. 給 Orchestrator 派 PM / Architect 互審的注意點
### 15.1 派 PM 互審重點
- **驗 PRD 是否需要新增章節**R5-Q9 翻案後、PRD-v2 應有對應 §「Firmware 管理 v2.2 補丁」、Design 已對齊 architect 30 §M9-0 PM 任務、但 PM 是否實際補了 PRD 是 Orchestrator 應追蹤的點
- **驗 user story 覆蓋**:本檔 §1 列了 4 種使用者降版情境architect 42 §1.1 取出、PM 應檢查是否與 PRD 既有 user story 相容、是否需要新增 user story「進階使用者切換 FW 版本」
- **驗 UI 用語**Design 採用「韌體 / 版本切換」中性詞、不用「降版」對 user-facing UI。PM 確認此用語策略與 PRD 整體 product voice 一致
- **驗成功指標**:本功能的 PRD 是否定了 success metric如「90% 升級成功率」「Brick 事件 < 0.1%」),Design 文件未涵蓋指標但實作前 PM 應補
### 15.2 派 Architect 互審重點
- **§2.1 分頁順序**Architect 是否覺得「韌體」放在「硬體」與「模型」之間是合理的vs 放最後 / 進階)
- **§4.1 FW badge 配色**Architect review §11.2 推導表的對比比率、特別是 Dark 模式下 `legacy.fg` 用黑底紅、需用 contrast checker 工具驗證真實對比
- **§6.1 「DOWNGRADE」嚴格比對**Architect 42 §2.3 後端要求 `confirmToken: "DOWNGRADE"`、Design 對應 UI 強制使用者輸入字面字串、確認前後端規格一致
- **§7.1 失敗類型對應**Design 列了 7 種失敗類型、對應 backend stage`preparing / loading / flashing / verifying`+ 兩個橫切性失敗timeout / disconnect+ `FirmwareProgress.Reason` 細分原因(見 TDD §3.4)。確認所有後端可能 throw 的 error 都有對應 friendly message、沒有遺漏
- **§8 狀態機**v2.2 三方對齊後、stage 命名以 Design `preparing / loading / flashing / verifying` 為 source of truth、Architect TDD §4.3 對齊此命名
- **§11.2 token 命名**`color.fw-badge.*` 命名是否符合 Architect / Frontend 對 token 階層的期望vs 用 `color.status.firmware-*` 等替代方案)
- ~~**§14.4 懸而未決第 6 點**~~ **(已解)**:降版進行中關 Wails 控制台 = brick 風險。**v2.2 三方對齊後解法**Architect TDD §8.6 補 server graceful shutdown 拒絕Design 在 `v2/control-panel.md` 補 graceful shutdown 警告 modal UI。
### 15.3 互審回合預期v2.2 補丁)
| 互審項 | 狀態 |
|--------|------|
| Architect §11.2 顏色對比實測值 | 已解:實測責任歸 M9-12 Frontend、Design 提供推算值參考§11.2 備註) |
| Architect §8 狀態機名稱對齊 | 已解:三方統一採 `preparing / loading / flashing / verifying`Design §5.3 §8 / TDD §4.3 / i18n keys 全對齊) |
| Architect graceful shutdown 拒絕邏輯 | 已解TDD §8.6 + Design `v2/control-panel.md` graceful shutdown modal |
| PM user story 覆蓋 / 用語策略 / 成功指標 | PM 在下次 PRD 補丁處理(見 PM review|
| §7.1 失敗復原 4 vs 8 對齊 | 已解:對齊 backend `FirmwareProgress.Reason`、TDD §3.4 補完整 mapping |
三方互審 v2.2 已收斂、本檔可進 M9-12 Frontend 實作。
---
## 16. 文件大小與拆檔策略
本檔 v2.2 修訂後 ~930 行含表格、wireframe、i18n table、v2.2 互審吸收紀錄),已逼近原訂的 800 行拆檔門檻、但尚未跨越 1000 行強制拆檔門檻。
**判斷****v2.2 暫不拆**。理由:
- v2.2 主要新增是吸收三方互審後的「對齊紀錄」stage 命名、reason mapping、graceful shutdown 解法),不是新增獨立章節
- 拆檔反而會讓「升級流程」「切換流程」「失敗復原」三個密切相關的章節分散
- §9 i18n keys 佔 ~140 行、本身是一張長表、拆出去也不會增加可讀性
- Frontend 實作時需要對照所有章節(從 IA 到 i18n單檔較便利
**下次拆檔觸發**:若本檔超過 **1000 行**(如 KL630/KL730 特殊降版流程落地、或補新的失敗情境),則拆為:
- `firmware-management.md`(索引 + IA + 主流程,目標 ≤ 600 行)
- `firmware-i18n.md`i18n keys 細表)
- `firmware-error-recovery.md`(失敗類型細表 + reason mapping
---
**下一步**
1. 更新 `design-spec-v2.md` 索引§14.1 / §14.2 / §14.3 / §14.4
2. Orchestrator 派 PM Agent 互審(依 §15.1+ Architect Agent 互審(依 §15.2
3. 互審通過後、本檔 + design-spec-v2.md 一起進 git
4. 進 M9-12 Frontend 實作前、Design 同步 confirm Architect 補 TDD `v2/device-firmware.md` 章節與本檔一致
---
## 變更紀錄
| 日期 | 版本 | 變更 | 作者 |
|------|------|------|------|
| 2026-05-24 | v2.2-draft | 初版R5-Q9 翻案 + B2 階段降版設計)| Design Agent |
| 2026-05-25 | v2.2 | 吸收三方互審:(1) §5.3 / §8 / §9.6 / §9.8 stage 命名統一為 `preparing/loading/flashing/verifying`A-MISMATCH-1(2) §7.1 失敗類型對齊 backend `FirmwareProgress.Reason` 細分原因A-MID-3(3) §11.2 token 對比實測責任明示歸 M9-12 FrontendA-MID-2(4) §14.4 第 6 點 graceful shutdown 標已解、UI 落地至 `v2/control-panel.md`A-MID-1(5) §9 i18n keys 51 個(原 52 減 1(6) §15.3 互審回合預期改為 v2.2 收斂狀態表 | Design Agent |

View File

@ -1,17 +1,40 @@
# TDD v2 — visionA-localRound-2 refactor 後)
> 作者Architect Agent
> 版本:**v2.1**
> 日期2026-04-14v2.0 → v2.1 吸收 PM 審閱 + R5-D + R5-E
> 狀態Draftv2.1 由 Architect 根據 PM 互審 + R5-D/E 新決策產出,待三方再次 cross-review + 使用者確認)
> 版本:**v2.2**
> 日期2026-05-24 初版 / 2026-05-25 三方互審後修FW 管理章節吸收 PM + Design 互審 + M9-6 弱驗證新事實
> 狀態Draftv2.2 由 Architect 根據使用者 2026-05-24 拍板 FW 管理方案 A + B 產出、2026-05-25 三方互審 + 使用者拍板 stage 命名 / M9 工時拆法 / AC-FW-3.5 延後 三項裁決後修;待最終使用者確認)
> 取代:`TDD.md` v1.02026-04-11四輪修訂 + Plan B 補件)
> 決策來源:`progress.md` §「R5 第五輪使用者決策」+ 「R5-D 補充決策」+「R5-E 階段化啟動新指標」2026-04-14
> 決策來源:`progress.md` §「R5 第五輪使用者決策」+「R5-D 補充決策」+「R5-E 階段化啟動新指標」+「M9 Kneron Dongle FW 偵測 + 升降版」2026-05-24+「2026-05-25 三方互審後使用者裁決」
---
## 0. 版本資訊與變更摘要
### 0.0 v2.0 → v2.1 差異速覽2026-04-14 本次更新)
### 0.0 v2.1 → v2.2 差異速覽2026-05-24 初版 / 2026-05-25 互審後修)
| # | 變更面向 | v2.1 | v2.2 | 觸發 |
|---|---------|------|------|------|
| 1 | **FW 管理** | 沿用既有 RAM-load only`load_firmware_from_file`)、無持久化升降版 | **新增 firmware module + ADR-001**A 階段自動升級 KDP1 → KDP2KL520/KL720+ B 階段 KL630/KL730 driver 擴展 + B2 階段手動降版(面向一般使用者)+ 多版本管理 | M9 啟動2026-05-24|
| 2 | **R5-Q9 翻案** | 「韌體燒錄 flash → B 砍掉」維持 | **範圍切割後翻案**:升級到 Kneron 官方 KDP2 標準版本納入範圍、「使用者燒任意 model 到 flash」繼續砍 | ADR-001 |
| 3 | **新模組** | — | `server/internal/firmware/`service / progress / versions / guards| ADR-001 |
| 4 | **bridge.py handler** | `handle_connect` / `handle_scan` / ... | + `handle_firmware_upgrade` / `handle_firmware_downgrade` / `handle_firmware_list_versions` + `handle_connect` chip 判斷擴 KL630/KL730 + `_resolve_firmware_paths` 支援 .tar / 多版本 | M9-1 / M9-7 / M9-11 |
| 5 | **多版本目錄結構** | 扁平 `firmware/<chip>/fw_*.bin` | **`firmware/<chip>/CURRENT_VERSION` + 版本子目錄**(選項 C metadata 結構A 階段保持扁平、B2 階段 migration | 使用者決策2026-05-24|
| 6 | **安裝包大小** | macOS dmg 163MB | **A 階段 +0KB / B 階段 +7MB保守 bundle 策略)→ ~170MB** | 使用者決策 |
| 7 | **milestone 數量** | M8-1 ~ M8-10 + M8-4b11 個) | **+M9-1 ~ M9-13**13 個新 milestone、5 + 10.5 = **15.5 人天**| M9 啟動 |
| 8 | **總工時估算** | ~12 人天M8| **+15.5 人天M9= 累計 ~27.5 人天** | 以上 |
| 9 | **新增檔案** | — | **`v2/firmware-management.md`** + **`adr/ADR-001-firmware-management.md`** | ADR-001 |
| 10 | **新風險** | R-v2-1 ~ R-v2-7 | + R-FW-1 ~ R-FW-13採 PM 編號 + M9-6 新增 R-FW-13+ R-TAR-1 ~ R-TAR-4TDD-only、合計 17 條、詳 `v2/firmware-management.md` §10| M9 + 2026-05-25 互審 |
| 11 | **stage 命名**2026-05-25 互審後修) | TDD 草案用 `connecting / loading_loader / loading_firmware / verifying` | **採 Design 命名 `preparing / loading / flashing / verifying`**(使用者拍板裁決) | 三方互審 + 使用者裁決 |
| 12 | **M9-7~M9-10 工時拆法**2026-05-25 互審後修) | TDD 自有拆法B0 認 chip 0.5 / .tar 1.5 / KL630 inference 2.0 / KL630 升版 1.0 | **採 PM 拆法**M9-7 1.5 / M9-8 1.0 / M9-9 1.0 / M9-10 1.5)、總和 5.0 不變 | 使用者拍板裁決 |
| 13 | **AC-FW-3.5 階段歸屬**2026-05-25 M9-6 弱驗證新增) | 未明示 A/B 階段 | **延後至 B 階段 M9-10**warrenchen 無 reference 實作 + 需實機驗證 KL630/KL730 適用性) | M9-6 弱驗證 |
| 14 | **wheel 三平台版本不一致**2026-05-25 M9-6 弱驗證新增) | 未察覺 | **macOS/Linux 2.0.0 / Windows 3.1.2**、A 階段不阻塞、B 階段啟動前必須統一 | M9-6 弱驗證 |
| 15 | **graceful shutdown 拒絕**2026-05-25 互審後修) | TDD 草案無 | **新增 §8.6**FW task active 期間 server 延遲 shutdown + Wails force-quit modal避免關 Wails 視窗中斷降版導致 brick | Design A-MID-1 |
| 16 | **`FirmwareProgress` schema**2026-05-25 互審後修) | 5 欄位 | **補 6 欄位**Reason / ElapsedMs / EtaMs / DeviceID / BeforeVer / RawError / ErrorCode給 Frontend ETA UI + 「複製錯誤訊息」+ i18n 對應 + 客服診斷用) | Design A-OK-2 / A-MID-3 + Architect F3 |
**未變更**v2.1 其餘設計保持不動Wails 視窗 = 控制台 UI、瀏覽器 tab 載業務、yt-dlp/Mock 砍除、ffmpeg LGPL 方案 B、CORS 白名單、boot-id 機制、LogBuffer 2000 行 ring buffer、state machine 5 個狀態、watchServer Error state、R5-E 6 階段啟動管線。
### 0.0a v2.0 → v2.1 差異速覽2026-04-14 歷史保留)
| # | 變更面向 | v2.0 | v2.1 | 觸發 |
|---|---------|------|------|------|
@ -126,6 +149,13 @@ Go server ←── HTTP / WebSocket over loopback ── 瀏覽器 tabNext.j
| 2.7 | [`v2/milestone-plan.md`](./v2/milestone-plan.md) | M8-1 ~ M8-10 + **M8-4b 階段化啟動**;總工時 ~12 人天 | 整體M8-* |
| 2.8 | [`v2/code-reuse-v2.md`](./v2/code-reuse-v2.md) | 逐模組沿用 / 改寫 / 新寫比例表 | 整體 |
| 2.9 | [`v2/startup-pipeline.md`](./v2/startup-pipeline.md) | **v2.1 新增**R5-E 6 階段啟動管線實作event schema、StartupPipeline struct、watcher goroutine、60 s hard timeout / 20 s soft timeout、前端 startup-panel.js| R5-E1~E6M8-4b |
| 2.10 | [`v2/firmware-management.md`](./v2/firmware-management.md) | **v2.2 新增**Kneron Dongle FW 偵測 + 升降版A 階段 KL520/KL720 自動升級 + B 階段 KL630/KL730 driver 擴展 + B2 階段手動降版 + 多版本管理 + CURRENT_VERSION metadata + .tar firmware 處理)| ADR-001M9-1 ~ M9-13 |
### 2.11 ADR 索引
| ADR | 主題 | Status |
|-----|------|--------|
| [`adr/ADR-001-firmware-management.md`](./adr/ADR-001-firmware-management.md) | Kneron Dongle FW 偵測 + 升降版(翻案 R5-Q9| Accepted2026-05-24|
---
@ -143,6 +173,25 @@ Go server ←── HTTP / WebSocket over loopback ── 瀏覽器 tabNext.j
| R-v2-6 | **boot-id polling 對瀏覽器 tab 休眠的影響** — Chrome 會把背景 tab 的 setInterval 降頻到 1 次/分鐘,可能讓使用者切回 tab 時 60 s 才偵測到 server 重啟 | 🟡 低 | 新增 | 用 Page Visibility APItab visible → 5 s intervaltab hidden → 停 pollingtab 再次 visible → 立即 probe 一次再恢復 5 s interval。實作細節見 `v2/web-ui-offline-overlay.md` §3 |
| R-v2-7 | **砍 Mock 後空白 UI 體驗** — 使用者第一次打開沒插硬體時Devices 頁會是空的 | 🟡 低 | 新增 | (1) Devices 頁顯示友善 empty state「未偵測到 Kneron 裝置。請連接 KL520/KL720 後按『掃描』。」附安裝 driver 按鈕Windows(2) 這是 R5-5a 明示接受的結果PRD v2 也會記錄為預期行為 |
### v2.2 新增 FW 管理相關風險(採 PM PRD §8 編號、完整清單見 `v2/firmware-management.md` §10
| # | 風險 | 等級 | 詳見 |
|---|------|------|------|
| R-FW-1 | 升級後 device re-enumerate 不穩定 + KL520 reset bug 再現 | 中 | `v2/firmware-management.md` §10.1 |
| R-FW-2 | KneronPLUS Python wheel API 支援度 + 三平台版本不一致 | 中 | 同上 |
| R-FW-3 | bridge.py `update_kdp_firmware_from_files` macOS x86_64 / Linux x86_64 未驗證 | 中 | 同上 |
| R-FW-4 | timeout 設定不合理 | 低 | 同上 |
| R-FW-5 | Kneron firmware redistribution 法律 / 簽章授權 | **P0 release gate** | 同上、與 R5-B4 同性質 |
| R-FW-6 | 既有 `flash/` 模組命名混淆 | 低 | 同上 |
| R-FW-7 | 升級失敗 device unknown state | 中 | 同上 |
| R-FW-8 | KneronPLUS SDK 對 KL630/KL730 API 不可預測 | 🔴 高 | `v2/firmware-management.md` §10.2 |
| R-FW-9 | .tar 解包對安裝包大小衝擊 | 低 | 同上 |
| R-FW-10 | KL630/KL730 沒有 Loader mode 概念 | 中 | 同上 |
| R-FW-11 | 一般使用者誤觸降版 brick 風險 | 🔴 高 | 同上、緩解見 §11.3 + §8.6 graceful shutdown 拒絕 |
| R-FW-12 | 多版本管理 UX 複雜度 | 中 | 同上 |
| **R-FW-13**2026-05-25 新增)| wheel 2.0.0 → 3.1.2 跨主版本升級可能 breaking change | 中 | `v2/firmware-management.md` §10.3、M9-6 弱驗證 |
| R-TAR-1 ~ R-TAR-4 | TDD-only 技術細節:.tar handling 特定風險SDK 不接受 .tar 已確認 / build 步驟漏跑 / notarization / Python 3.12+ path filter| 已確認 / 中 / 低 | `v2/firmware-management.md` §10.4 |
v1 已列、v2 解除的風險:
- **ffmpeg GPL release blockerF-5, R9** — R5-6 LGPL 方案 B 解除
- **F-1 Wails tray 在 Linux GNOME 可能壞掉** — R5-3 維持砍 tray風險消失
@ -159,4 +208,10 @@ v1 已列、v2 解除的風險:
| 2026-04-14 | Architect Agent | **v2.1** 產出:吸收 PM Major/Minor 修正 + R5-D 三條補充決策 + R5-E 階段化啟動 + Architect 自補清單F-2 port 保留 / B-1 OS 通知 / Q1/Q3/Q5/Q7 自決)。新增 `v2/startup-pipeline.md`;總工時 ~10 → ~12 人天 |
| 2026-04-14 | PM Agent | v2.1 待再次審閱(確認 Major × 4 都已落地 + R5-D/E 理解一致)|
| 2026-04-14 | Design Agent | v2.1 待審重點R5-E5 啟動階段文案、7+1 秒 stopping modal 文案、startup-panel.js 視覺對齊 Design Spec v2.1|
| 2026-04-14 | 使用者 | 待確認 |
| 2026-04-14 | 使用者 | v2.1 待確認 |
| 2026-05-24 | Architect Agent | **v2.2** 產出:吸收使用者拍板 FW 管理方案 A + B、新增 `v2/firmware-management.md` + `adr/ADR-001-firmware-management.md`、R5-Q9 翻案範圍切割後。M9-1 ~ M9-13 新 milestone series、15.5 人天 |
| 2026-05-24 | PM Agent | v2.2 PM 互審完成(見 `02-prd/reviews/pm-review-of-tdd-and-design-v2.2-firmware.md`):通過 with Major × 2MJ-A1 ADR 編號、MJ-A2 R5-Q9 行號)+ Minor × 6 |
| 2026-05-24 | Design Agent | v2.2 Design 互審完成(見 `03-design/reviews/design-review-of-prd-and-tdd-v2.2-firmware.md`):通過 with 🔴 嚴重 × 1A-MISMATCH-1 stage 命名)+ 🟠 中等 × 3 + 🟡 輕微 × 4 |
| 2026-05-24 | Architect Agent | Architect 自互審完成(見 `04-architecture/reviews/architect-review-of-prd-and-design-v2.2-firmware.md`):自承 F1-F7 必修 + O1-O3 建議 |
| 2026-05-25 | Architect Agent | **v2.2 互審後修**FW 管理章節吸收三方互審 + M9-6 弱驗證新事實 + 使用者三項裁決stage 採 Design 命名 / M9 採 PM 拆法 / AC-FW-3.5 延後 B 階段 M9-10。修改範圍`v2/firmware-management.md`§1.2 / §1.3 新增 / §1.4 / §1.5 新增 / §3.4 新增 / §4.2 / §4.3 / §5.1 / §5.3 / §6.1 / §7.3 / §8.6 新增 / §9 / §10 / §11.3 / §11.4 / §13 / §14+ `adr/ADR-001-firmware-management.md`Status update + Context R5-Q9 改描述式 + Related 補 PRD + 變更記錄 v1.1|
| 2026-05-25 | 使用者 | v2.2 互審後修待最終確認 |

View File

@ -0,0 +1,218 @@
# ADR-001: Kneron Dongle FW 偵測 + 升降版(翻案 R5-Q9
## Status
**Accepted** — 使用者於 2026-05-24 拍板方案 A + B 一次做完、L 級正規流程。
**2026-05-25 update**M9-6 弱驗證執行(見 `research-kl520-fw-management/55-m9-6-weak-validation-result.md`)後追加兩個新事實:
1. **visionA-local KneronPLUS wheel 三平台版本不一致**macOS/Linux = 2.0.0、Windows = 3.1.2。對 A 階段KL520/KL720 升級)不阻塞(既有 API 在兩個版本都存在);對 B 階段KL630/KL730影響重大、B 階段啟動前必須統一升級到 3.1.2+ 並跑三平台 KL520/KL720 回歸M9-13。詳見 TDD `v2/firmware-management.md` §1.3。
2. **PRD AC-FW-3.5KL630/KL730 升降版)延後至 B 階段 M9-10**:弱驗證確認 warrenchen 沒做 KL630/KL730 升降版reference 實作為零)、且需要實機才能驗 `update_kdp_firmware_from_files` 對 KL630/KL730 是否適用。A 階段 scope 縮限為 KL520+KL720 自動升級。Decision §1 表格的 A 階段範圍不變(本來就只列 KL520+KL720、Consequences §5 風險敘述強化。
## Context
### 痛點背景
visionA-local 自 2026-04 上線以來、累積以下與 Kneron Dongle FW 相關的痛點:
1. **拿到舊 dongle 完全不能用**:使用者拿到的 KL520 dongle 若是 KDP1 legacypid=0x0200、插上 visionA-local 後 connect 流程在 `kp.core.load_firmware_from_file` 階段失敗、無法 inference。
2. **KL520 firmware 殘留導致 Error 15 SEND_DATA_TOO_LARGE**2026-04-21 已修、但 root cause 在 FW 層commit `ddf0eb8` 為了解 Windows 60s HTTP timeout 加「KL520 首次 connect 跳過 reset」優化、後來發現 KL520 若 session 間 firmware 殘留(`fw=KDP2 Comp/U`)、直接 load_model + inference 100% 炸 Error 15。已 revert、但這暴露既有 FW 偵測 + reset 邏輯的脆弱性。
3. **KL630 / KL730 偵測不到**scan 階段顯示得到名字、但 `handle_connect` fall-through 到 KL520 路徑、用錯誤的 firmware 檔載入 → 連不上。新世代 dongle 完全不可用。
4. **舊 model 與新 FW 相容性**:進階使用者反映「我的舊 NEF model 在升版 FW 後跑不出結果」、缺乏降版機制。
### R5-Q9 砍 flash 的歷史決策
`progress.md` 重要決策紀錄 §「第二輪使用者決策 Q9」
> 韌體燒錄 flash → B 砍掉
(行號為動態值、本 ADR 不寫具體 L 行號避免日後 progress.md 增刪後失準;以「第二輪使用者決策 Q9」描述位置為準
當時砍的理由(推測、原 progress.md 沒留下細節):
1. visionA-local 是「local 推論工具」、不是「dongle 管理工具」、燒 flash 不屬核心使用旅程
2. 燒 flash 有 brick 風險、不想對使用者開放
3. 早期 MVP 範圍縮小、把非必要功能延後
4. 既有「load_firmware 到 RAM」夠用KL520 USB Boot 每次都重 load、KL720 已預燒 KDP2
### 為何現在要翻案
1. **痛點 #1 是真實場景**:使用者已陸續遇到舊 KDP1 dongle 完全不能用Q9 原始假設「既有 load_firmware 到 RAM 夠用」不成立)
2. **同事 warrenchen 雲端版已驗證可做、且安全**`/tmp/web_academy_prototype/local_service_win/` 有完整實作可參考
3. **範圍可切割**Q9 砍的是「使用者按按鈕燒任意 model 到 device flash」、本期是「升級到 Kneron 官方 KDP2 標準版本」、避開大部分 brick 與責任風險
4. **跨平台技術可行**KneronPLUS Python API`kp.core.update_kdp_firmware_from_files`+ `libkplus.{dll,so,dylib}` 三平台都已在既有 wheel 內、無需引入 Windows-only DFUT.exe
### 技術現況盤點
visionA-local 既有 code 已有 70% 的 FW 偵測 + RAM-load 邏輯:
- `kneron_bridge.py:handle_connect()` 已會偵測 Loader mode、從 `firmware/<chip>/fw_scpu.bin + fw_ncpu.bin` load firmware 到 RAM
- `kneron_bridge.py:handle_connect()` 已處理 KL720 KDP legacy → 強制 load KDP2 firmware 到 RAM
- `DeviceInfo.FirmwareVer` 已在 driver interface、bridge 連線時也回傳 `firmware` 欄位
- bundled firmware 已存在 `server/scripts/firmware/{KL520,KL720}/`(合計 ~360KB
缺的是「持久化升降版」:
- 既有的 `load_firmware_from_file` 是 RAM-load裝置重新上電就消失
- 真正的 FW 升降版需要呼叫 `kp_update_kdp_firmware_from_files`(會寫 flash
## Decision
採以下五項決策:
### 1. 採方案 A + B、不分階段交付
| 階段 | 範圍 | 工時 |
|-----|------|------|
| A | KL520 + KL720 自動升級 KDP1 → KDP2、安裝包 +0KB | 5 人天 |
| B | + KL630 + KL730 driver 擴展 + 手動降版(面向一般使用者)+ 多版本管理、安裝包 +7MB | 10.5 人天 |
| **合計** | — | **15.5 人天** |
不採「先做 A、之後再評估 B」的分階段策略——使用者明確要求一次做完。
### 2. 翻案 R5-Q9範圍切割如下
| 「燒 flash」場景 | 本期決策 |
|-----------------|---------|
| 使用者按按鈕燒任意 modelNEF到 device flash | **繼續砍**R5-Q9 原始決策維持)|
| 升級到 Kneron 官方 KDP2 標準版本 | **本期做**(範圍切割後翻案)|
| 手動降版到 bundled 舊版(含 KDP1| **本期做**B2 階段、面向一般使用者)|
| 使用者燒入自編 firmware binary | **不做**brick 風險過高、責任風險過高)|
切割後翻案的核心邏輯Q9 擔憂的 brick 風險來自「使用者燒任意 binary」、而非「升級到官方標準版本」。本期只暴露「升級到 Kneron 官方 KDP2」+ 「降版到 bundled 已知舊版」、binary 來源已嚴格限制。
### 3. 跨平台用 KneronPLUS Python C API
| 替代方案 | 否決原因 |
|---------|---------|
| DFUT.exewarrenchen 雲端版用)| Windows-only、~30MB Qt5 binary、跨平台不通 |
| ctypes 包 KneronPLUS C API | 既有 Python wheel 已包好、不需重做 |
| 自寫 USB 協議實作 | 範圍過大、需逆向工程 Kneron protocol、Kneron 不認可 |
採 KneronPLUS Python API`kp.core.update_kdp_firmware_from_files`)路徑:
- 三平台都有 wheel既有 bundle 已含、增加成本 ~0KB
- 對應 C 函式經 SDK 驗證、不踩 brick 風險
- 與既有 `kp.core.load_firmware_from_file` 路徑共用、bridge.py 改動可控
### 4. 多版本目錄結構採選項 C — CURRENT_VERSION metadata
```
firmware/<chip>/
├── CURRENT_VERSION ← 單行檔:"v2.2.0"
├── v2.2.0/{...} + VERSION
├── v2.1.0/{...} + VERSION
└── kdp1/{...} + VERSION
```
| 替代方案 | 否決原因 |
|---------|---------|
| 選項 A — symbolic link`current/` 指向版本目錄)| Windows symlink 需 admin、`tar` 處理跨 OS 不一致 |
| 選項 B — 實體副本(`current/``v2.2.0/` 的 file copy| 每 chip 多佔一份(合計 +7-8MB 額外)、跨平台簡單但浪費空間 |
| 選項 C — `CURRENT_VERSION` 單行檔 + 版本目錄並列 | **選此**:架構乾淨、空間最省、跨平台無 symlink 風險、bridge.py 多一次 file readtrivial 成本)|
### 5. 保守 bundle 策略 +7MB
| Chip | bundled 版本 | 估算 |
|------|------------|------|
| KL520 | current + kdp1 + v2.1.0 | ~290KB |
| KL720 | current + v2.1.0(如有)| ~500KB |
| KL630 | current + 1 個舊版(如有)+ extracted | ~9MB |
| KL730 | current + extracted | ~8MB |
合計 ~18MB極大值。實際採保守策略 = 不一定每個 chip 都 bundle 舊版KL630/KL730 舊版可能 SDK release 取不到)、估算 +7MB。
macOS dmg 從 163MB → ~170MB+4-5%)、使用者「+5MB 接受」決策對齊。
## Consequences
### 正面影響
1. **解決真實痛點**:拿到舊 KDP1 dongle 完全不能用的情境消除A 階段)、新世代 dongle KL630/KL730 變可用B 階段、進階使用者可手動切版本B2 階段)
2. **架構乾淨**:新 `firmware/` 模組與既有 `flash/` 分離、職責清楚、未來擴 KL830 / KL530 有明確 hook 點
3. **跨平台一致**:三平台同走 KneronPLUS Python API、無 Windows-only 依賴、維護成本均勻
4. **回溯可追**:本 ADR + 研究檔research-kl520-fw-management/+ TDD v2/firmware-management.md 留下完整決策痕跡、未來新人接手不會問「為何 Q9 砍了又做」
### 負面影響(接受的取捨)
1. **+7MB 安裝包**B 階段、A 階段 +0KBmacOS dmg 從 163MB → ~170MB、使用者已接受
2. **Brick 風險未完全消除**KL720 升級會寫 flash、升級中拔 USB 仍有 brick 可能緩解UI 警告、不打包 DFUT 但提供內部 SOP
3. **法律 / 簽章授權待釐清**Kneron firmware redistribution含 KDP1 / 舊版)需發佈前評估、與 R5-B4 同性質、不阻塞開發但是 PRD P0 懸念
4. **15.5 人天工時**:比原估 11-12 多 ~30%、主因 B2 階段「面向一般使用者」UX safety net + 多版本管理 + M9-6 SDK 驗證
5. **B 階段對 KL630/KL730 SDK 行為仍有未知**M9-6 驗證後可能需要升級 KneronPLUS wheel、有 KL520/KL720 regression 風險
6. **CURRENT_VERSION 機制需要 A → B2 migration**A 階段 firmware 檔扁平放、B2 階段才搬進版本目錄、需小心不破壞 A 階段已 commit 路徑
### 風險
完整風險清單見 `v2/firmware-management.md` §10R-FW-1 ~ R-FW-12、R-TAR-1 ~ R-TAR-4 合計 16 條)。最關鍵:
| # | 風險 | 等級 |
|---|------|------|
| R-FW-8 | KneronPLUS SDK 對 KL630/KL730 API 不可預測 | 高 |
| R-FW-11 | 一般使用者誤觸降版 brick 風險 | 高 |
| R-FW-1 | 升級中拔除 deviceKL720 寫 flash 階段)| 中 |
## Alternatives Considered
### 替代方案 1不做 FW 管理
| 維度 | 評估 |
|------|------|
| 工時 | 0 |
| 痛點解決 | 不解 |
| 否決原因 | 痛點 #1KDP1 dongle 不能用)是真實且高頻場景、使用者明確要求做 |
### 替代方案 2用 DFUT.exewarrenchen 雲端版方案)
| 維度 | 評估 |
|------|------|
| 工時 | 較少(直接呼叫 .exe|
| 跨平台 | **Windows-only**、macOS / Linux 完全不通 |
| 安裝包衝擊 | +30MB Qt5 binary |
| 否決原因 | visionA-local 是三平台 desktop app、Windows-only 工具不接受 |
### 替代方案 3只做內部 dev mode 降版(非面向一般使用者)
| 維度 | 評估 |
|------|------|
| 工時 | 少(不用做 UX safety net、~12 人天)|
| 痛點解決 | 痛點 #4(舊 model 相容性)不解、仍需技術支援介入 |
| 否決原因 | 使用者明確決策2026-05-24手動降版面向一般使用者、不只 dev mode |
### 替代方案 4多版本目錄結構選 symlink / 副本
| 維度 | 選 A symlink | 選 B 副本 | 選 C metadata本期選此|
|------|-------------|----------|---------------------|
| 跨平台 | Windows symlink 需 admin、Windows zip/tar 處理 symlink 不一致 | 完美跨平台 | 完美跨平台 |
| 空間 | 最省 | 每 chip 多一份(+7-8MB| 最省(只多一個單行檔)|
| 複雜度 | 高symlink fall-back 邏輯)| 低 | 低(讀 metadata 一行)|
### 替代方案 5等 A 階段驗證後再評估 B
| 維度 | 評估 |
|------|------|
| 工時 | A 階段 5 人天先交付 |
| 風險 | A → B 整合時的 .tar handling / chip 判斷 / multi-version migration 必須重整 A 階段已 commit 程式碼 |
| 否決原因 | 使用者明確要求一次做完、且 ADR / TDD 一次定稿能避免 A 階段做出不利 B 階段擴展的設計 |
## Related
| 編號 | 關聯 |
|------|------|
| R5-Q9 | 第二輪「韌體燒錄 flash → B 砍掉」、本 ADR 翻案 |
| R5-B4 | 「Kneron 預置模型 re-distribution 授權」未解決問題、firmware redistribution 同性質、發佈前統一處理 |
| R5-E | 60s 啟動 hard timeout、FW 升降版不在啟動 pipeline、無關 |
| 2026-04-21 commit | KL520 reset bug fix、本 ADR 必須維持「升級成功後 needsReset=true」確保不踩 Error 15 |
| watchServer Error state | `v2/server-lifecycle.md`、FW 升降版失敗不觸發 server Error state |
| TDD `v2/firmware-management.md` | 本 ADR 對應的 TDD 章節、含完整 API / 流程 / 工時 / 風險 |
| PRD `02-prd/features/feature-firmware-management.md` v2.2 | 對應 user storyUS-FW-1 ~ US-FW-3與成功指標 |
| research-kl520-fw-management/ | 本 ADR 的研究依據(含 warrenchen 實作分析、SDK API 落差、.tar handling、降版 UX 需求、M9-6 弱驗證結果)|
## Compliance
- [x] Architect 與 PM Agent 確認本 ADR 影響業務指標FW 升級流程是 PRD v2.2 新功能)
- [x] 成本影響已評估:開發 15.5 人天 + 基礎設施 +7MB 安裝包 + 維運 0無 OTA 通道、無監控 SaaS 需求)
- [ ] 與 Kneron 取得 firmware redistribution 授權(發佈前必須完成、不阻塞開發)
- [ ] Design Agent 確認 UX 多層 safety net 落地(二次確認 modal + DOWNGRADE 輸入 + persistent banner
## 變更記錄
| 日期 | 版本 | 變更 | 作者 |
|------|------|------|------|
| 2026-05-24 | 1.0 | 初版產出、Status: Accepted | Architect Agent |
| 2026-05-25 | 1.1 | 三方互審後修:(1) Status 區塊加 2026-05-25 update 反映 M9-6 弱驗證新事實wheel 三平台版本不一致 + AC-FW-3.5 延後 B 階段);(2) Context R5-Q9 行號改為描述式引用(不寫具體 L 行號);(3) Related 表補 PRD v2.2 條目;(4) 不改 Decision/Alternatives/Compliance 主體(決策邏輯沒變、仍 Accepted | Architect Agent |

View File

@ -0,0 +1,239 @@
# 研究摘要visionA-local Kneron Dongle FW 偵測 + 升降版
> 作者Architect Agent
> 日期2026-05-24
> 範圍:純研究 plan、不出 code、不改任何既有檔案
> 目標:在 visionA-localWails 桌面 app新增「自動偵測 Kneron Dongle FW 版本 + 升級 / 降版」功能
> 參考:同事 warrenchen 雲端版實作於 `/tmp/web_academy_prototype/`
---
## 1. Executive Summary
### 核心發現
1. **R5-Q9 砍 flash 的決策不阻擋本任務**——當時砍的是「使用者按按鈕去燒模型到 device flash」這條使用者旅程**FW 升降版是不同的事**。Q9 砍掉的並不是 firmware 燒錄的技術能力(我們既有的 `Flash()` driver method 跟 bridge.py 都還有完整的 `kp.core.load_firmware_from_file` 邏輯只是叫法叫「load_firmware」不叫「flash firmware」
2. **visionA-local 既有 code 已經有 70% 的 FW 偵測 + RAM-load 邏輯**
- `kneron_bridge.py:handle_connect()` 已會偵測 `Loader` mode、自動從 `firmware/<chip>/fw_scpu.bin + fw_ncpu.bin` load firmware 到 RAM
- `kneron_bridge.py:handle_connect()` 已處理 KL720 KDP legacy (pid=0x0200) → 強制 load KDP2 firmware 到 RAM 的路徑
- `DeviceInfo.FirmwareVer` 已在 driver interface 內、bridge 連線時也已回傳 `firmware` 欄位
- bundled firmware 已存在於 `server/scripts/firmware/{KL520,KL720}/fw_{scpu,ncpu}.bin`(合計約 ~360KB
3. **缺的核心是「持久化升降版」**
- 既有的 `load_firmware_from_file`**RAM-load**(裝置重新上電就消失)
- 真正的 FW 升降版需要呼叫 `kp_update_kdp_firmware_from_files`C API、會寫 flash
- warrenchen 雲端版用兩條路徑:(a) `kp.core.install_driver_for_windows` 風格的 Python 包裝;(b) `KneronDFUT.exe`Windows-only Qt GUI 工具的 CLI mode
4. **跨平台選 KneronPLUS C API、不選 DFUT.exe**
- DFUT.exe 是 Windows-only Qt5 binary、~30MB 依賴、不能用
- `libkplus.{dll,so,dylib}` 三平台都有、我們既有 bundle 已含、增加成本約 0KB已在 KneronPLUS wheel 內)
- 對應 Python API 是 `kp.core.update_kdp_firmware_from_files`、不需要走 ctypes除非舊 KDP1 → KDP2 那條超 legacy 路徑需要 `kp_connect_devices_with_magic_pass`、那段才必須 ctypes
### 推薦方案:兩階段
#### 階段 AMVP建議先做「FW 偵測 + 自動升級 KDP1 → KDP2」
**範圍**KL520 + KL720兩個我們已支援的晶片
**做什麼**
1. **被動偵測**scan / connect 時已知道 firmware 字串、在前端 UI 顯示 FW 版本(綠/黃/紅 badge
2. **主動升級**(單一動作):使用者按「升級到最新 KDP2」按鈕 → 走 warrenchen 的 legacy-upgrade flow → 進度回報 → 完成後 rescan
3. **不做手動降版**:階段 A 不暴露「給使用者選版本」UI、只有「升級到目前內建的最新版本」一個動作
**為什麼先做 A**
- 解決 90% 的真實痛點(拿到一根插上是 KDP1 legacy 的舊 dongle 沒辦法用)
- 工時 ~3-5 人天、安裝包 +0KBKL520/KL720 firmware 都已內建)
- 風險低失敗的話舊狀態還在、re-plug 重試
- 不踩 brick 風險KDP2 是 Kneron 官方 SDK 的標準版本、不是改寫過的 binary
#### 階段 B完整版視 A 驗證結果決定要不要做):「手動降版 + 多裝置支援」
**做什麼**
1. **手動降版 KDP2 → KDP1**(測試 / 復現舊 bug 用、僅內部)
2. **加入 KL630 / KL730 支援**(需要 KneronPLUS SDK 對應版本驗證)
3. **多版本 firmware 並存**(讓使用者選擇 firmware 版本)
**為什麼分開**
- 手動降版主要是「給開發者測試用」、不是真實使用者場景
- KL630 / KL730 我們目前 driver 沒驗證過、要先升級 driver code 才能加 FW 支援
- 安裝包 + 5-10MBKL630/KL730 firmware 是 .tar 約 2-4MB / 套)
### 工時 / 範圍對照
| 項目 | MVPA | 完整版A + B |
|------|---------|---------------|
| **裝置範圍** | KL520 + KL720 | + KL630 + KL730 |
| **操作範圍** | 自動升級 KDP1 → KDP2 | + 手動降版 + 多版本選擇 |
| **新增 API** | 3 個(`/api/devices/:id/firmware`, `/api/devices/:id/firmware/upgrade`, WebSocket progress room| + 2 個(`/firmware/downgrade`, `/firmware/versions` |
| **新增 UI** | 1 個 Devices 頁面卡片擴充FW badge + 升級按鈕 + progress modal | + Settings 內的「進階FW 版本管理」面板 |
| **bridge.py 新增 handler** | `firmware_upgrade` | + `firmware_downgrade` + `firmware_list_versions` |
| **driver 改動** | 加 `UpgradeFirmware()` method | + `DowngradeFirmware(version)` |
| **新文件** | TDD 補 §5.4 FW 升級子節 + 1 個新 ADR | + 1 個新 ADR |
| **預估工時** | **3-5 人天** | + 5-7 人天 = **8-12 人天合計** |
| **安裝包衝擊** | +0KB既有 KL520/KL720 已 bundle | +5-10MBKL630/KL730 firmware + 可能的舊版 KL520_kdp |
### R5-Q9 翻案分析
**Q9 原文**:「韌體燒錄 flash → B 砍掉」progress.md L776
**當時為何砍**
- progress.md 沒留下原因只有「B 砍掉」)
- 推測理由(需使用者確認):
1. visionA-local 是「local 推論工具」、不是「dongle 管理工具」、燒 flash 不屬核心使用旅程
2. 燒 flash 有 brick 風險、不想對使用者開放
3. 早期 MVP 範圍縮小、把非必要功能延後
4. 既有「load_firmware 到 RAM」夠用KL520 USB Boot 每次都重 load、KL720 已預燒 KDP2
**現在為何要翻案**
- 「拿到舊 dongle 完全不能用」是真實痛點progress.md L29 已有 Error 15 SEND_DATA_TOO_LARGE 經驗)
- 使用者明確要求做(聯合決策已收齊)
- 同事 warrenchen 雲端版已驗證可做、且安全
- 階段 A 範圍小、風險可控、工時可估
**翻案是否合理**:✅ 合理。
- Q9 的擔憂仍有效brick 風險、不是核心旅程),但**範圍縮小到「自動升級到內建 KDP2 標準版本」就能避開大部分風險**
- 不開放使用者「燒任意 binary」、只開放「升級到 Kneron 官方 KDP2 標準版本」
- 加 progress.md 留下決策痕跡(新 ADR 引用 Q9 + 標註翻案理由)
### 裝置範圍建議
**強烈建議 MVP 階段只做 KL520 + KL720**
| 晶片 | 現有 driver 支援 | warrenchen 提供 firmware | 建議 |
|------|---------------|-------------------------|------|
| KL520 | ✅ | ✅ KL520KDP2+ KL520_kdpKDP1 降版用) | **MVP 必做** |
| KL720 | ✅ | ✅ KL720KDP2 | **MVP 必做** |
| KL630 | ❌driver 沒處理 product_id 0x0630| ✅ kp_firmware.tar / kp_loader.tar | **延後**(要先擴 driver |
| KL730 | ❌driver 沒處理 product_id 0x0730| ✅ kp_firmware.tar / kp_loader.tar | **延後**(要先擴 driver |
KL630 / KL730 的 firmware 是 .tar 格式(不是 .bin代表 SDK 的 load_firmware API 對它們的處理流程也不同、需要先讓 Python bridge 能載這兩種、再做 FW 升降版。**強行加入 KL630 / KL730 會把 MVP 範圍翻倍**。
### 安裝包大小衝擊預估
**目前 macOS dmg163MB**progress.md 確認)
| 階段 | 新增內容 | 大小 |
|------|---------|------|
| MVPA | 0既有 KL520/KL720 已 bundle | +0KB |
| 完整版B| KL520_kdp~92KB+ KL630 .tar~2MB+ KL730 .tar~3MB| **+5MB** |
→ MVP 階段不衝擊安裝包大小、完整版約 +3%(接受範圍內)。
---
## 2. 與既有架構衝突點
### 2.1 與 TDD v2.1「watchServer Error state」機制的銜接
TDD v2.1 已決定 `watchServer` 失敗 → 切 `ServerStateError`、不再 `os.Exit`
**FW 升級失敗該怎麼處理**
- **建議**FW 升級是「device 層」失敗、不是「server 層」失敗、不應該讓 server 進 Error state
- 失敗時device 層回 Error state既有 `driver.StatusError`)、推 WebSocket 失敗訊息給 UI、server 繼續正常運行
- 這跟 watchServer 機制是平行的、不衝突
### 2.2 與 KL520 reset bug2026-04-21的銜接
bridge.py 已加 `fresh_firmware_loaded` flag、避免雙重 firmware load 浪費 60s。
**FW 升級流程要避免踩同樣坑**
- 升級成功後 device 會 re-enumerate裝置脫離 USB → 重新出現)
- 必須等 USB stable建議 5-8 秒)+ 主動 rescan、不要立刻 reconnect
- 升級流程內部需要 `kp.core.reset_device(KP_RESET_REBOOT)`、跟我們既有的 `restartBridge()` 流程重疊,要小心 race
### 2.3 與 R5-E 60s 啟動上限的銜接
FW 升級是**使用者主動觸發**、不在啟動 pipeline 內、跟 60s 上限無關。
**但**:升級可能需要 30-60 秒warrenchen 的 KL520 升級 timeout 設 30 秒、KL720 設 180 秒、UI 必須給 progress bar、不能 block。
### 2.4 既有 `Flash()` method 的角色重新定位
`flash/service.go:StartFlash()` 目前的語意是「load model 到 device RAM」、不是「燒 firmware」。
**建議**:保持 `flash/` 模組原意load model、**新建 `firmware/` 模組**(升降版)、不混用。
具體模組劃分:
- `server/internal/flash/` — 既有load model 到 device RAM語意保持
- `server/internal/firmware/`(新)— FW 偵測 + 升級 + 降版
### 2.5 文件命名包袱
`server/internal/driver/kneron/kl720_driver.go` 檔名其實是 KL520 + KL720 共用 driverprogress.md 已標註)、本任務不改檔名(範圍外)、但新文件不要再用 `kl720_*` 命名、用 `kneron_*``device_firmware_*`
---
## 3. 下一步建議
1. 給使用者 review 本份 plan這個檔 + 10/20/30 三個附檔)
2. 使用者確認後:
- **方案 A 通過** → 啟動 PM 補 PRD「FW 管理」章節(記錄 Q9 翻案 + MVP 範圍)→ Architect 補 TDD v2.1 §5.4 firmware 子節 + 新 ADR-009-firmware-management → Design 補 Devices 頁面 FW badge + 升級 modal 設計
- **方案 B 通過** → 同上 + 補 KL630/KL730 driver 擴展 plan多一份 research
- **要求修改 plan** → 我修
3. 文件完成後依 milestone 進入開發(建議拆 M9-1 ~ M9-5
### Milestone 建議拆法MVP 階段)
| # | Milestone | 預估 | 平行性 |
|---|-----------|------|-------|
| M9-1 | bridge.py 新增 `firmware_upgrade` handlerKneronPLUS C API | 1 人天 | 獨立 |
| M9-2 | Go driver + service`UpgradeFirmware()` + `firmware/service.go` | 1 人天 | 依賴 M9-1 |
| M9-3 | API handler + WebSocket progress room | 0.5 人天 | 依賴 M9-2 |
| M9-4 | 前端 Devices 頁 FW badge + 升級按鈕 + progress modal | 1.5 人天 | 依賴 M9-3 |
| M9-5 | 三平台實機驗證macOS / Windows / Linux | 1 人天 | 全部後 |
| **合計** | | **5 人天** | |
每個 milestone 都走 Reviewer 審查、最後 Testing E2E 驗收。
---
## 附錄B 階段研究索引2026-05-24 追加)
### 使用者最新決策2026-05-24
使用者拍板採方案 A + B 一次做完。新決策:
1. **手動降版面向一般使用者**(不只 dev mode、Settings 一般面板就要看得到)
2. **FW 全部內嵌進安裝包**、+5MB 接受、**不做線上更新通道**
### 本附錄涵蓋的新研究檔
| 檔案 | 主題 |
|------|------|
| `40-b-phase-kl630-kl730-extension.md` | KL630/KL730 driver 擴展研究主檔warrenchen 實作分析、既有 driver 擴點、SDK 落差、milestone M9-6 ~ M9-13|
| `41-tar-firmware-handling.md` | .tar firmware 處理細節(為什麼是 .tar、SDK API、解壓策略 X/Y/Z、大小估算|
| `42-manual-downgrade-for-end-users.md` | 手動降版面向一般使用者driver/bridge/API 細節、多版本儲存結構、Design Agent 需求清單)|
### 與主檔00 ~ 30的關係
| 主檔內容 | B 階段研究檔位置 |
|---------|---------------|
| §1 範圍對照表「階段 B 工時」5-7 人天 | **更新為 ~10.5 人天**B 階段拆三層 + 手動降版面向一般使用者 + SDK 驗證)詳見 40 §10 |
| §1 安裝包衝擊「+5-10MB」 | **更新為 +6-8MB**(保守 bundle 策略)詳見 42 §4 |
| §1 「KL630/KL730 延後」 | **變更A 階段做完後啟動 M9-6 SDK 驗證 → 決定 B 階段是否全做**、見 40 §11 |
| §3 「手動降版主要是給開發者測試用」 | **翻案**:使用者決策面向一般使用者、見 42 §1.1 |
### B 階段風險清單擴增
承前 R-FW-1 ~ R-FW-7、本研究新增
- **R-FW-8**KneronPLUS SDK 對 KL630/KL730 API 不可預測(高度)
- **R-FW-9**.tar 解包對安裝包大小衝擊(低度)
- **R-FW-10**KL630/KL730 沒有 Loader mode 概念(中度)
- **R-FW-11**:一般使用者誤觸降版 brick 風險高度B2
- **R-FW-12**:多版本管理 UX 複雜度中度B2
- **R-TAR-1 ~ R-TAR-4**.tar 解壓特定風險41 檔 §8
### B 階段啟動前必確認
1. KneronPLUS wheel 版本與升級可能性M9-6 第一件事)
2. KL630/KL730 是否有實機可測(沒有則 M9-6 降級為純文件研究)
3. Kneron 對 firmware re-distribution 的書面授權範圍是否含舊版 / KDP142 §7
4. 使用者對多版本 bundle 策略的選擇(保守 / 完整 / 極簡42 §4.3
5. 多版本目錄結構選項(建議 C、42 §3.2
### 下一步建議(更新版)
1. 給使用者 review 本份完整 plan00 + 10/20/30 + 40/41/42 七個檔)
2. 使用者最終 confirm 整體範圍後:
- **A 階段啟動**M9-1 ~ M9-55 人天)
- **B 階段預備**M9-6 SDK 驗證1 人天、可與 A 階段平行做 research
3. A 階段 release / 驗證後評估 M9-6 結論、決定 B 階段全做或調整 scope
4. B 階段執行M9-7 ~ M9-13合計 ~9.5 人天)
5. 整體合計:**~15.5 人天**A 5 + B 10.5

View File

@ -0,0 +1,259 @@
# warrenchen 雲端版 FW 偵測 + 升降版實作分析
> 對應 visionA-local 研究 plan §10
> 來源:`/tmp/web_academy_prototype/` reposhallow clone, c9d56a6 HEAD
> 主要分析檔:
> - `local_service_win/LocalAPI/main.py`FastAPI HTTP 服務、含 FW endpoints
> - `local_service_win/LocalAPI/legacy_plus121_runner.py`(用 ctypes 直接呼叫 libkplus.dll、最關鍵
> - `local_service_win/STRATEGY.md`API spec + recovery flow
> - `local_service_win/firmware/`(裝置 firmware 二進位)
> - `local_service_win/third_party/Kneron_DFUT/`Windows-only Qt GUI 工具)
---
## 1. warrenchen 用了哪些技術做 FW 偵測 + 升降版
### 1.1 兩條獨立路徑(並存)
| 路徑 | 用途 | 技術 | 跨平台? |
|------|------|------|---------|
| **Path AKneronDFUT.exe** | KL520 升版 / 降版、KL720 升版 | 呼叫 Windows 第三方 `KneronDFUT.exe` CLIsubprocess | ❌ Windows only |
| **Path Blibkplus.dll via ctypes** | KL520 KDP1 → KDP2「載入到 RAM」升級不寫 flash | 直接 ctypes 載 `libkplus.dll`、呼叫 C API | ✅ 三平台都能用(換成 .so / .dylib |
**我們選 Path B**——理由:
- Path A 的 DFUT.exe 是 Windows-only Qt5 binary30+ DLLs、bin/ 目錄 ~40MB、無 macOS / Linux 版本、無法跨平台
- Path B 用的 `libkplus` 三平台都有、我們既有 wheel 已含、額外成本 ~0KB
- Path B 的 API`kp_update_kdp_firmware_from_files`)也能寫 flash、不只 load to RAM
### 1.2 偵測 FW 版本
scan 時從 KneronPLUS SDK 拿到的 `device_descriptor.firmware` 字串、可能值:
| firmware 字串 | 意義 |
|--------------|------|
| `KDP` | 舊版 KDP1legacy、需升級 |
| `KDP2 Comp/U` | KDP2、在 USB Boot 模式(剛 load 進 RAM |
| `KDP2 Comp/F` | KDP2、在 Flash 模式(已永久燒入 flash |
| `Loader` | USB Boot Loader 模式(等待 load firmware |
| `Unknown` / 空字串 | 偵測失敗 |
**判斷邏輯**(從 `legacy_plus121_runner.py` L228
```python
if detected_firmware == "KDP":
# 走 KDP1 → KDP2 升級路徑loader → load_firmware
elif detected_firmware == "Loader":
# 在 USB Boot loader、直接 load_firmware
else:
# 已是 KDP2 或其他、不需要升級
```
**比對 product_id 也能輔助判斷**visionA-local bridge.py L657 已有此表):
| product_id | 意義 |
|-----------|------|
| `0x0100` | KL520 |
| `0x0200` | KL720 KDP legacy舊韌體 |
| `0x0530` | KL530 |
| `0x0630` | KL630 |
| `0x0720` | KL720 KDP2 |
| `0x0730` | KL730 |
→ KL720 `0x0200 vs 0x0720` 直接區分新舊版本(不用看 firmware 字串)。
### 1.3 KDP1 → KDP2 升級流程(最核心)
`legacy_plus121_runner.py` L185-292 整理:
```
Step 1: kp_connect_devices_with_magic_pass(port_id, MAGIC=536173391)
→ 用 magic number 繞過 SDK 的 firmware version check
→ 拿到 device_group handle
Step 2: kp_set_timeout(device_group, 5000ms)
→ 給後續 API 設超時
Step 3: 偵測當前 firmware
if firmware == "KDP":
# KDP1 → 必須先切到 USB Boot loader
Step 3a: kp_update_kdp_firmware_from_files(
device_group,
loader_path, # fw_loader.bin
NULL, # 不寫 NCPU
auto_reboot=True
)
→ 寫 loader 到 flash、自動重啟
Step 3b: kp_set_timeout(再設一次)
Step 3c: kp_load_firmware_from_file(
device_group,
scpu_path, # fw_scpu.bin
ncpu_path # fw_ncpu.bin
)
→ 載入 KDP2 firmware 到 RAM不寫 flash
else:
# 不是 KDP1、直接 load_firmware_from_file
Step 3': kp_load_firmware_from_file(scpu_path, ncpu_path)
Step 4: kp_disconnect_devices(device_group)
→ 注意:升級後 disconnect 經常會回非零 codeUSB re-enumerate 中),但不算錯誤
```
**注意 1**:這條路徑的「升級」實際上是「載到 RAM」、不是「寫 flash」。對 KL520 USB Boot 裝置而言、每次斷電都要再 load 一次。這對我們**夠用**——KL520 反正每次 connect 都會 load_firmwarevisionA-local bridge.py L876 已實作)、加 legacy KDP → KDP2 升級邏輯只是擴充當前的 connect 流程。
**注意 2**:真正寫 flash 的是 `kp_update_kdp_firmware_from_files`、會留在 device 重啟仍存在。這個 API 是 warrenchen「legacy-upgrade」endpoint 用 DFUT.exe 跑的路徑Path A對應的 C API、跨平台可用。
### 1.4 警告與限制warrenchen 已踩過的坑)
`STRATEGY.md` 「Legacy Firmware Story」段L521-545整理
1. **`KP_ERROR_INVALID_FIRMWARE_24`**:很多出貨的 dongle 還在舊 KDP firmware、即使 `connect_devices_without_check` 也會回此錯誤。**解法**:用 `kp_connect_devices_with_magic_pass`MAGIC=536173391繞過 version check。
2. **DFUT 對某些舊裝置 timeout**warrenchen 加了一條後備邏輯main.py L1039-1052——如果 DFUT timeout 但 rescan 看到裝置已切到 KDP 狀態、就視為成功。
3. **disconnect 失敗不一定是錯**FW load 後 USB 重 enumerate、原 device handle 失效、disconnect 回非零是預期行為。
4. **timeout 設定**:升級 KL520 用 30 秒、升級 KL720 用 180 秒(後者要寫 flash、慢很多
5. **驅動程式問題**Windows升級前必須確認 WinUSB driver 已綁定、否則 `kp_connect_devices` 直接失敗。warrenchen 用 `kp.core.install_driver_for_windows` 自動處理、我們有自己的 `kneron_winusb.inf` + driver 安裝邏輯M1+ TODO
---
## 2. warrenchen 的 firmware 二進位內容
| 裝置 | 路徑 | 檔案 | 用途 |
|------|------|------|------|
| KL520KDP2 主版) | `firmware/KL520/` | `fw_scpu.bin` / `fw_ncpu.bin` | 升級到 KDP2 / 連線時 load to RAM |
| KL520KDP2 主版) | `firmware/KL520/` | `fw_loader.bin` | USB Boot Loader升降版必要 |
| KL520KDP2 主版) | `firmware/KL520/dfw/` | `minions.bin` | DFUT 用的中介 firmware不確定我們是否需要 |
| KL520KDP1 降版) | `firmware/KL520_kdp/` | `fw_scpu.bin` / `fw_ncpu.bin` | 降版回 KDP1 用(測試用) |
| KL720 | `firmware/KL720/` | `fw_scpu.bin` / `fw_ncpu.bin` | 升級到 KDP2 / 連線時 load to RAM |
| KL630 | `firmware/KL630/` | `kp_firmware.tar` / `kp_loader.tar` | 整套打包格式(新 SDK 用) |
| KL730 | `firmware/KL730/` | `kp_firmware.tar` / `kp_loader.tar` | 整套打包格式 |
**比對 visionA-local 既有**`server/scripts/firmware/`
| 裝置 | 我們有 | warrenchen 多了什麼 |
|------|--------|-------------------|
| KL520 | ✅ fw_scpu.bin + fw_ncpu.bin | ❌ fw_loader.bin升降版必要、要拿、minions.bin可能要、KL520_kdp 整組(降版要) |
| KL720 | ✅ fw_scpu.bin + fw_ncpu.bin | 無新增 |
| KL630 / KL730 | ❌ | ✅ 全套(但我們延後做)|
**從 VERSION 檔看版本**
- KL520 / KL720 都是 firmware v2.2.0
- KL630 是 SDK-v2.5.7(命名規則不同)
### 大小估算(合計)
| 裝置 | 檔案 | 估計大小 |
|------|------|---------|
| KL520KDP2 主版)| fw_scpu + fw_ncpu | ~92KB既有 TDD L3219 標 52+40KB |
| KL520 | + fw_loader.bin | + ~10KB估計 |
| KL520_kdpKDP1 | fw_scpu + fw_ncpu | ~80KBKDP1 較小) |
| KL720 | fw_scpu + fw_ncpu | ~150KB估計、KDP2 較複雜) |
| KL630 | kp_firmware.tar + kp_loader.tar | ~2-3MB壓縮包 |
| KL730 | kp_firmware.tar + kp_loader.tar | ~3-4MB壓縮包 |
→ MVPKL520 + KL720+ 補 fw_loader.bin + KL520_kdp 合計 < 300KB可忽略
→ 完整版加 KL630/KL730 約 +5-7MB。
**注意**warrenchen 把 `firmware/` 跟程式碼一起打包進 installer payloadSTRATEGY.md L513-518、跟我們做法一致。我們已經把 firmware 進 git`server/scripts/firmware/`)、新增的 firmwarefw_loader.bin、KL520_kdp/)也應該 commit 進來、不走 vendor-sync。
---
## 3. warrenchen 暴露的 FW APIHTTP REST
從 STRATEGY.md + main.py 整理。**我們不會照搬全部、只取其中 3 個 endpoint 給 MVP**
### 3.1 對我們有用的(要做)
| Endpoint | Method | Body | 我們的對應 |
|----------|--------|------|----------|
| `/devices` | GET | — | ✅ 我們已有 `/api/devices`、回傳已含 `firmwareVersion` 欄位 |
| `/firmware/legacy-plus121/load` | POST | `{port_id, loader_path, scpu_path, ncpu_path}` | **建議新增 `/api/devices/:id/firmware/upgrade`**(自動推導 path、不暴露給前端 |
### 3.2 不需要照搬的(雲端版才需要)
| Endpoint | 為什麼不要 |
|----------|----------|
| `/firmware/legacy-upgrade/kl520` (DFUT.exe) | DFUT 是 Windows only、跨平台不能用 |
| `/firmware/legacy-downgrade/kl520` (DFUT.exe) | 同上、且降版主要是內部測試用、MVP 不做 |
| `/firmware/legacy-upgrade/kl720` (DFUT.exe) | 同上 |
| `/firmware/load`(暴露 scpu_path/ncpu_path| 沒必要讓前端決定 firmware 路徑、後端內部處理 |
| `/devices/connect_force` | 我們 bridge.py `handle_connect()` 已有 `connect_devices_without_check` 自動 fallback、不暴露給 API |
| `/driver/check` / `/driver/install` / `/driver/ensure` | 我們已有 `ensureDriverInstalled()`M1+ TODO、不暴露 |
---
## 4. 我們**可以直接重用**的 warrenchen 邏輯
| 來源 | 邏輯 | 我們怎麼用 |
|------|------|----------|
| `legacy_plus121_runner.py:_load_libkplus` | ctypes 直接載 libkplus 的 argtypes/restype 宣告 | **不重用 ctypes 寫法**——我們用 Python kp module`kp.core.update_kdp_firmware_from_files`)即可,比 ctypes 乾淨 |
| `legacy_plus121_runner.py:_connect_with_magic` | 用 MAGIC 繞 firmware check | **重用概念、用 Python API**——`kp.core.connect_devices_with_magic_pass(port_ids, magic=536173391)` |
| `legacy_plus121_runner.py:main()` 的升級流程 | KDP → loader → load_firmware 兩階段 | **直接重用流程**(翻譯成 visionA-local bridge.py 的 handle_firmware_upgrade |
| `legacy_plus121_runner.py:_reboot_and_reconnect` | reset → sleep → retry connect | **重用**visionA-local 已有類似的 restartBridge |
| `main.py:_kl520_kdp_observed_after_timeout` | timeout 後 rescan 確認狀態 | **重用**升級不確定有沒有真的成功時、rescan 來驗證) |
| `main.py:_run_dfut_command` 的 timeout / retry / streaming output 處理 | 子進程通訊管理 | **不重用**(我們不用 DFUT|
---
## 5. 我們**不該照搬**的(雲端版才需要的)
從 README + STRATEGY 可見 warrenchen 雲端版的特殊需求:
1. **跨進程 RPC**:雲端版有「網頁 → 雲端 → 本地服務」的 RPC 鏈、需要自訂 URL scheme + 引導下載安裝代理。我們**單進程 Wails**、不需要這套。
2. **DFUT.exe 整包打包**:因為要支援所有舊裝置 / Windows 維運場景。我們**只走 KneronPLUS Python API**、跨平台、不打包 DFUT。
3. **FastAPI / Uvicorn**:他們選 Python FastAPI 寫本地服務、我們是 Go server。我們不重用 service 層、只 reverse-engineer FW 升級邏輯。
4. **`/tools/video-inference` 視覺驗證頁**:他們用 HTML 內嵌 page 做 PoC 視覺化、我們的 Next.js 推論頁已完整、不需要。
5. **`/firmware/load` 直接暴露 path 給前端**:安全 / 易用考量、我們不暴露、後端自己解析。
---
## 6. 對接 visionA-local 既有 bridge.py 的具體改動建議(純 plan
`server/scripts/kneron_bridge.py` 新增一個 handler
```
def handle_firmware_upgrade(params):
"""
升級 KL520 / KL720 dongle 到內建 KDP2 firmware跨晶片自動偵測
參數:
port: 裝置 USB port id必填
chip: "KL520" or "KL720"(必填)
回傳(成功):
{
"status": "upgraded",
"before_firmware": "KDP",
"after_firmware": "KDP2",
"method": "kp_update_kdp_firmware_from_files",
"duration_ms": 28500
}
回傳(失敗):
{"error": "...", "stage": "connect" | "loader" | "load_firmware" | "verify"}
"""
# 1. scan + 找到 target_dev by port_id
# 2. kp.core.connect_devices_with_magic_pass(port_ids, magic=536173391)
# 3. kp.core.set_timeout(60000)
# 4. 偵測 detected_firmware
# 5. if detected_firmware == "KDP":
# a. kp.core.update_kdp_firmware_from_files(dg, loader_path, None, auto_reboot=True)
# b. wait_for_reenumerate(8s)
# c. reconnect (with magic if needed)
# d. kp.core.load_firmware_from_file(scpu, ncpu) # 已是 visionA-local bridge 既有邏輯
# else:
# a. kp.core.load_firmware_from_file(scpu, ncpu) # 直接 load 即可
# 6. 重新 scan、驗證 firmware 已變
# 7. disconnect容忍非零回傳
# 8. 回傳結果
```
**注意**bridge.py 的 main loop 已是 JSON-RPC 模式L1159-1182、加新 cmd 只需在 switch 表加 `elif action == "firmware_upgrade": result = handle_firmware_upgrade(cmd)`、改動量極小。
---
## 7. 結論
warrenchen 的雲端版**核心 FW 升級邏輯Path B100% 可移植**到我們 local 版、且:
- 不需要打包 DFUT.exe跨平台 win
- 不需要 ctypes用 KneronPLUS Python API 即可)
- 既有 bridge.py + firmware bundle 已 70% 就緒、只差升級 handler + 1 個 endpoint + 1 個 UI 卡片
- 風險低、工時可估MVP 5 人天)
下一份檔(`20-our-current-state.md`)盤點我們**確切**有什麼、缺什麼。

View File

@ -0,0 +1,292 @@
# visionA-local 既有架構盤點FW 偵測 + 升降版相關)
> 對應研究 plan §20
> 目的:列出我們**已經有**什麼、**缺**什麼、改動成本估計
---
## 1. 既有 driver 怎麼連 dongle
### 1.1 Driver 概覽
**檔案**`server/internal/driver/kneron/kl720_driver.go`(檔名歷史包袱、實際同時管 KL520 + KL720
**架構**
```
Go driver (KneronDriver)
↓ exec.Command(python3, kneron_bridge.py)
↓ JSON-RPC over stdin/stdout
Python bridge (kneron_bridge.py)
↓ kp module (KneronPLUS Python wheel)
↓ ctypes → libkplus.{dll,so,dylib}
USB device (Kneron Dongle)
```
**已實作 driver methods**`driver.DeviceDriver` interface
- `Connect()` / `Disconnect()` / `IsConnected()`
- `Flash(modelPath, progressCh)`**語意load model 到 device RAM**(不是燒 firmware
- `RunInference(imageData)` / `ReadInference()` / `StartInference()` / `StopInference()`
- `GetModelInfo()`
- `Info()` — 回傳 `DeviceInfo``FirmwareVer`
### 1.2 connect 流程(既有)
`KneronDriver.Connect()` (L250-323) 已做:
1. 啟動 Python bridge subprocess
2. send `connect` cmd 到 bridge
3. bridge `handle_connect()` 內:
- `kp.core.scan_devices()` 找 target by port
- 判斷 chip type從 device_type 或 product_id
- **如果是 KL720 KDP legacypid=0x0200**:自動走 `connect_devices_without_check` + 載 KDP2 firmware 到 RAML749-804
- **如果是 USB Boot Loader mode**:自動載 firmware 到 RAML872-914
- 回傳 `firmware` 字串 + `fresh_firmware_loaded` flag
4. driver 根據 `fresh_firmware_loaded` 決定是否做 `restartBridge()` resetL310-321
### 1.3 已實作的 FW 偵測能力
**bridge.py 已能偵測**
- USB vendor / product id`kp.core.scan_devices()`、含 KL520 / KL630 / KL720_legacy / KL720_KDP2 / KL730、L657-664 已列表)
- firmware 字串(從 `device_descriptor.firmware`
- kn_number、is_connectable、usb_port_id
**Go driver 已記錄**
- `DeviceInfo.FirmwareVer``interface.go` L26
- `DeviceInfo.ProductID` / `VendorID`
### 1.4 已實作的 FW load 能力
**這部分是關鍵——我們其實已經會 `load_firmware_to_RAM`**
`kneron_bridge.py` 內:
- `_resolve_firmware_paths(chip)` — 從 `server/scripts/firmware/<chip>/fw_{scpu,ncpu}.bin` 解析路徑
- `handle_connect()` L749-764KL720 legacy 路徑、`kp.core.load_firmware_from_file(dg, scpu, ncpu)`
- `handle_connect()` L876-914USB Boot Loader 路徑、同 API
**載完之後也會做 reconnect + verify**L887-912
**缺什麼**
- 沒呼叫 `kp.core.update_kdp_firmware_from_files`(這個才寫 flash、持久升級
- 沒呼叫 `kp.core.connect_devices_with_magic_pass`(升級舊 KDP1 dongle 必要)
- 沒「使用者主動升級」的入口、目前都是 connect 自動觸發
---
## 2. 既有 flash 模組(仍存在、語意 = model load
### 2.1 程式碼狀態
R5-Q9 砍 flash 後、**程式碼還在**
| 檔案 | 大小 | 用途 |
|------|------|------|
| `server/internal/flash/service.go` | 158 行 | `StartFlash(deviceID, modelID)` — load model |
| `server/internal/flash/progress.go` | 未讀(推測 progress tracker | progress 追蹤 |
| `server/internal/driver/kneron/kl720_driver.go``Flash()` method | L425-604 | 實際呼叫 `load_model` cmd |
`Flash()` 的實際行為(看 L425-604
- 不是燒 firmware
- 是 **load .nef model 到 device**KL520 RAM / KL720 也是 load model
- 含 KL520 USB Boot mode 的 retry + restartBridge fallback
**R5-Q9 砍掉的是 UI 入口、不是技術能力**
- `device_handler.go:FlashDevice()` (L128-160) 仍存在、route 也仍註冊progress.md M1-2 跳過 cluster/tunnel/flash/update 是指 _ut/update_ 這套 cluster 機制,不是這支 flash
- 前端 UI 是否還顯示「Flash」按鈕沒查證、但即使有也是「load model」、跟 firmware 升降版無關
### 2.2 對本任務的啟示
**flash 模組保留原語意load model**、不要把 firmware 升降版邏輯塞進去。
**新建 `server/internal/firmware/` 模組**plan 30 會展開)、跟 flash 並列、職責清楚。
---
## 3. Bridge.py 是否有支援升降版能力KneronPLUS Python SDK 文件對照)
### 3.1 KneronPLUS Python API 可用清單
`kneron_bridge.py` 內已 import 的 `kp` module從 imports 推測 + warrenchen 程式碼驗證):
| API | 用途 | bridge.py 已用? |
|-----|------|---------------|
| `kp.core.scan_devices()` | scan USB | ✅ 已用 |
| `kp.core.connect_devices(port_ids)` | 標準 connect | ✅ 已用 |
| `kp.core.connect_devices_without_check(port_ids)` | 繞 firmware check connect | ✅ 已用 |
| `kp.core.connect_devices_with_magic_pass(port_ids, magic)` | 用 magic 繞檢查 | ❌ **缺、升級舊 KDP1 必需** |
| `kp.core.set_timeout(dg, ms)` | 設超時 | ✅ 已用 |
| `kp.core.reset_device(dg, mode)` | 重啟 device | ✅ 已用 |
| `kp.core.load_firmware_from_file(dg, scpu, ncpu)` | **load to RAM** | ✅ 已用 |
| `kp.core.update_kdp_firmware_from_files(dg, scpu, ncpu, auto_reboot)` | **寫 flash 持久升級** | ❌ **缺、本任務核心** |
| `kp.core.load_model_from_file(dg, path)` | load model | ✅ 已用 |
| `kp.core.disconnect_devices(dg)` | disconnect | ✅ 已用 |
| `kp.core.install_driver_for_windows(target)` | 裝 Windows driver | ❓ 我們有自己的 `kneron_winusb.inf` 機制、可能不需要 |
**核心缺 2 個 API call、其他都有**
### 3.2 既有 firmware bundle
| 路徑 | 內容 | 對 MVP 是否足夠 |
|------|------|---------------|
| `server/scripts/firmware/KL520/fw_scpu.bin` | KL520 SCPU firmwareKDP2、~52KB | ✅ |
| `server/scripts/firmware/KL520/fw_ncpu.bin` | KL520 NCPU firmwareKDP2、~40KB | ✅ |
| `server/scripts/firmware/KL720/fw_scpu.bin` | KL720 SCPU firmwareKDP2 | ✅ |
| `server/scripts/firmware/KL720/fw_ncpu.bin` | KL720 NCPU firmwareKDP2 | ✅ |
| **`server/scripts/firmware/KL520/fw_loader.bin`** | KL520 USB Boot Loader binary | ❌ **缺、升級舊 KDP1 必需** |
| `server/scripts/firmware/KL520/dfw/minions.bin` | 跟 DFUT 配套(可能不需要) | ❌ 可能要研究 |
**MVP 階段要從 warrenchen repo 補一個 `KL520/fw_loader.bin`**~10KB
### 3.3 為什麼缺的 API call 不太可能踩坑
`kp.core.update_kdp_firmware_from_files` 是 KneronPLUS SDK 標準 API、warrenchen 的 ctypes 版本(`legacy_plus121_runner.py`)也是用同一個 C symbol`lib.kp_update_kdp_firmware_from_files`)、不是冷僻 API。
但要注意:
- **必須先 `connect_devices_with_magic_pass`**——否則舊 KDP1 device 連 `connect_devices_without_check` 都會被拒KP_ERROR_INVALID_FIRMWARE_24
- **auto_reboot=True 後 disconnect 會回非零**——預期行為、不能視為 error
- **timeout 要拉長**warrenchen KL720 用 180s
---
## 4. 現有 `/api/devices` endpoint 結構
### 4.1 既有 routes`device_handler.go`
```
GET /api/devices ListDevices 回 [DeviceInfo]
GET /api/devices/scan ScanDevices rescan + 回 [DeviceInfo]
GET /api/devices/:id GetDevice 回單一 DeviceInfo
POST /api/devices/:id/connect ConnectDevice
POST /api/devices/:id/disconnect DisconnectDevice
POST /api/devices/:id/flash FlashDevice load model、保留
POST /api/devices/:id/inference StartInference
DELETE /api/devices/:id/inference StopInference
```
WebSocket rooms推測、需 grep 確認):
- `flash:<deviceId>` — flash progress
- `inference:<deviceId>` — inference results
### 4.2 `DeviceInfo` 已含的欄位
```go
type DeviceInfo struct {
ID string
Name string
Type string // "kl520" / "kl720"
Port string
VendorID uint16
ProductID uint16
Status DeviceStatus
FirmwareVer string // 已有!
FlashedModel string
}
```
### 4.3 為了 FW 管理需要新增的欄位(建議)
```go
type DeviceInfo struct {
// ... 既有欄位 ...
// === FW 管理新增 ===
FirmwareIsLegacy bool `json:"firmwareIsLegacy,omitempty"` // true=需升級到 KDP2
FirmwareCanUpgrade bool `json:"firmwareCanUpgrade,omitempty"` // true=有 bundled firmware 可升
BundledFirmwareVer string `json:"bundledFirmwareVersion,omitempty"` // "v2.2.0"(從 VERSION 檔)
}
```
→ 這些都是 bridge.py 回傳完就能計算的衍生欄位、不額外查 USB。
### 4.4 為了 FW 管理需要新增的 endpoints
| Endpoint | Method | 用途 |
|----------|--------|------|
| `POST /api/devices/:id/firmware/upgrade` | POST | 觸發升級到內建 KDP2 |
| WebSocket room `firmware:<deviceId>` | — | 升級 progress 廣播 |
**MVP 不做**
- `POST /firmware/downgrade`(手動降版、階段 B
- `GET /firmware/versions`(列出可選版本、階段 B
---
## 5. 既有架構幾條重要限制FW 升級必須避開)
### 5.1 KL520 reset bug 教訓2026-04-21
bridge.py L867-927 已有 `fresh_firmware_loaded` flag、避免雙重 load 浪費 60s。
**升級流程要小心**
- 升級成功後 device re-enumerate、driver 端的 `_device_group` handle 失效
- 必須清掉 `_device_group`、回到 Go 端、讓使用者重新 scan / connect
- 不要在升級 handler 內試圖「升級完繼續用同一個 connection」
### 5.2 Connect timeout = 120sWindows worst-case
`device_handler.go` L90 已設 120s。
**升級不該共用這個 timeout**
- KL520 升級 ~30s、KL720 升級 ~180s
- 升級 endpoint 用獨立的 context.WithTimeout(300s) 或乾脆走 background goroutine + WebSocket 推進度
- **推薦做法**HTTP API 立刻回 202 Accepted + taskID、實際進度走 WebSocket跟 flash 一樣的 pattern
### 5.3 Python bridge 是 per-driver-instance
每個 `KneronDriver` 啟動自己的 Python bridge subprocess。升級時的 bridge 必須是當前 connected 的那一個。
**升級流程**
- 升級前 bridge 已在 connected 狀態
- 升級期間 bridge 內部會 reset device、disconnect、reconnect
- 升級後 bridge 仍存活、但 `_device_group` 已換新
### 5.4 三平台差異
| OS | 特殊處理 |
|----|---------|
| macOS | 已用 `_preload_kneron_dylibs_macos()` 預載 libusb / libkplus避開 hardened runtime 砍 DYLD|
| Windows | 需要 WinUSB driver 綁定 + libusb-1.0.dll 在 PATH |
| Linux | wheel 自帶 libusb.so.1.0.0 + LD_LIBRARY_PATH |
→ 升級流程本身**不需要額外的平台特化邏輯**(用既有的 `kp` import 流程)。
### 5.5 既有 `restartBridge()` 不能拿來做 FW 升級
`restartBridge()` (L369-415) 是用來在 KL520 換 model 時清掉 USB Boot sessionkill bridge → sleep 8s → 重啟 bridge、跟 firmware update **完全不同**
→ FW 升級走獨立的 handler、不重用 restartBridge。但**升級完成後**、driver 應該 mark `needsReset=true`、下次 connect 走完整 reset flow既有邏輯保證 clean state。
---
## 6. 既有 UI 概況(推測、未實際讀前端 code
從 progress.md + TDD 推測 visionA-local Web UI 結構:
- `/devices` 頁面Next.js顯示掃描到的 dongle 清單
- 每張 device card 已顯示 `firmwareVersion` 欄位(既有 DeviceInfo 有此欄位)
- 但沒有「升級」按鈕、沒有 FW 健康度視覺化(綠/黃/紅)
**MVP UI 新增**plan 30 會展開):
- FW badgeKDP2 最新、黃KDP2 但版本低、紅KDP1 needs upgrade
- 「升級韌體」按鈕(紅 badge 時 primary、其他 secondary
- 升級 modalprogress bar + 階段提示 + 取消按鈕)
---
## 7. 缺項摘要
| 類別 | 缺什麼 | 取得方式 |
|------|--------|---------|
| Firmware binary | `KL520/fw_loader.bin` | 從 warrenchen `local_service_win/firmware/KL520/fw_loader.bin` 複製進 `server/scripts/firmware/KL520/` |
| Firmware binaryB 階段)| `KL520_kdp/fw_{scpu,ncpu}.bin`(降版用)| 從 warrenchen 複製 |
| Firmware binaryB 階段)| `KL630/` `KL730/` | 從 warrenchen 複製、但要先驗 driver 支援 |
| Python API call | `kp.core.connect_devices_with_magic_pass` | 已內建於 KneronPLUS wheel、加 import 即可 |
| Python API call | `kp.core.update_kdp_firmware_from_files` | 已內建於 KneronPLUS wheel |
| bridge.py handler | `handle_firmware_upgrade()` | 新寫 ~80-100 行plan 30 有 stub |
| bridge.py handlerB 階段)| `handle_firmware_downgrade()` | 新寫 |
| Go driver method | `UpgradeFirmware() error` | 新寫 ~40-60 行plan 30 |
| Go service | `server/internal/firmware/service.go` | 新建模組(仿 flash/service.go 結構)|
| Go API handler | `POST /api/devices/:id/firmware/upgrade` | 新增 ~50 行 |
| WebSocket room | `firmware:<deviceId>` | 沿用既有 WS hub broadcast pattern |
| DeviceInfo 欄位 | `FirmwareIsLegacy` / `FirmwareCanUpgrade` / `BundledFirmwareVer` | 加在 `interface.go` |
| Frontend 元件 | FW badge + 升級按鈕 + progress modal | 1.5 人天 |
| Frontend store update | Devices store 加 fw progress 訂閱 | 已有 flash progress 訂閱 pattern 可參考 |
| Frontend i18n | 升級相關文案(中英雙語) | ~20 個新 keys |
下一份檔(`30-integration-plan.md`)展開完整 milestone + 受影響檔案 + 風險清單。

View File

@ -0,0 +1,662 @@
# Integration PlanFW 偵測 + 升級 MVP建議方案 A
> 對應研究 plan §30
> 範圍:階段 A自動升級 KDP1 → KDP2KL520 + KL720
> 不涵蓋階段 B手動降版、KL630/KL730— 等 A 驗證後再決定
---
## 1. Milestone 詳細拆解
### 整體依賴圖
```
M9-0 (decision) 使用者 review 此 plan → 同意 MVP 範圍
M9-1 (bridge) bridge.py 新增 handle_firmware_upgrade + 補 fw_loader.bin
M9-2 (driver) Go driver: UpgradeFirmware() method
M9-3 (api) API handler + WebSocket room + DeviceInfo 欄位擴充
M9-4 (ui) Frontend: FW badge + upgrade button + progress modal
M9-5 (verify) 三平台實機驗證macOS / Windows / Linux
```
### M9-0決策 + 文件先行0.5 人天)
**負責**Orchestrator 整合 PM + Architect + Design 三方意見、定稿 PRD / TDD / Design Spec 補丁。
**任務**
1. 使用者 confirm MVP 範圍KL520 + KL720、僅自動升級、不降版
2. PM 補 `docs/autoflow/02-prd/PRD-v2.md` 新章節「§N. Firmware 管理v2.2 補丁)」、明標 R5-Q9 翻案 + 範圍
3. Architect 補 `docs/autoflow/04-architecture/TDD-v2.md` 新子節 `v2/device-firmware.md`、新 ADR `adr/ADR-009-firmware-management.md`
4. Design 補 `docs/autoflow/03-design/` Devices 頁面 FW 卡片 wireframe + 升級 modal flow
5. 三方互審 → 使用者 confirm
**驗收**:所有共享文件三方互審通過、使用者 sign-off。
---
### M9-1bridge.py 新增 firmware_upgrade handler1 人天)
**負責**Backend Agent
**依賴**M9-0 完成
**任務**
1. **複製 firmware 檔**:從 warrenchen repo 複製 `local_service_win/firmware/KL520/fw_loader.bin``server/scripts/firmware/KL520/fw_loader.bin`、commit 進 git
- 確認檔案大小、加進 `server/scripts/firmware/KL520/VERSION` 對照表(既有檔則加註)
2. **bridge.py 新增 handler**pseudo-code、實作時 architect 會給更詳細的 stub
```python
def handle_firmware_upgrade(params):
"""Upgrade KL520/KL720 to bundled KDP2 firmware.
Params:
port: int (USB port id, required)
chip: str ("KL520" or "KL720", required)
Returns:
{"status":"upgraded", "before_firmware":"KDP", "after_firmware":"KDP2",
"method":"kp_update_kdp_firmware_from_files", "duration_ms":28500}
or {"error":"...", "stage":"connect|loader|load_firmware|verify"}
"""
global _device_group, _firmware_loaded
chip = params.get("chip", "")
target_port = params.get("port", "")
if not HAS_KP:
return {"error": "kp module not available"}
# Stage 1: scan + find target
try:
descs = kp.core.scan_devices()
target_dev = None
for i in range(descs.device_descriptor_number):
dev = descs.device_descriptor_list[i]
if str(dev.usb_port_id) == str(target_port):
target_dev = dev
break
if target_dev is None:
return {"error": f"device on port {target_port} not found", "stage": "scan"}
detected_fw = str(target_dev.firmware)
_log(f"FW upgrade: detected={detected_fw}, chip={chip}, port={target_port}")
except Exception as e:
return {"error": str(e), "stage": "scan"}
# Stage 2: connect with magic pass
try:
_clear_device_group()
_device_group = kp.core.connect_devices_with_magic_pass(
usb_port_ids=[target_dev.usb_port_id],
magic=536173391 # KDP_MAGIC_CONNECTION_PASS
)
kp.core.set_timeout(_device_group, 60000)
except Exception as e:
return {"error": str(e), "stage": "connect"}
# Stage 3: 找 firmware paths
scpu_path, ncpu_path = _resolve_firmware_paths(chip)
loader_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"firmware", chip, "fw_loader.bin"
)
if not scpu_path or not ncpu_path:
return {"error": f"firmware files not found for {chip}", "stage": "resolve"}
# Stage 4: 升級
try:
start = time.time()
if "KDP" in detected_fw.upper() and "KDP2" not in detected_fw.upper():
# 舊 KDP1 → 先走 loader、再 load KDP2 to RAM
if not os.path.exists(loader_path):
return {"error": f"loader not found: {loader_path}", "stage": "loader"}
_log(f"Loading loader to switch to USB Boot: {loader_path}")
kp.core.update_kdp_firmware_from_files(
_device_group, loader_path, None, auto_reboot=True
)
# auto_reboot → device re-enumerate、 disconnect 會回非零、 容忍
time.sleep(2)
# 重新 scan + connect
_clear_device_group()
descs = kp.core.scan_devices()
# ... 找回 target_dev、 reconnect with magic
# 細節在實作時補
kp.core.set_timeout(_device_group, 60000)
kp.core.load_firmware_from_file(_device_group, scpu_path, ncpu_path)
else:
# 已是 KDP2 或 Loader、直接 load
kp.core.load_firmware_from_file(_device_group, scpu_path, ncpu_path)
_firmware_loaded = True
duration_ms = int((time.time() - start) * 1000)
except Exception as e:
return {"error": str(e), "stage": "upgrade"}
# Stage 5: verify
try:
# disconnect、 等待 USB stable、 rescan、 確認 firmware 已變
try:
kp.core.disconnect_devices(_device_group)
except Exception:
pass # auto_reboot 後 disconnect 失敗預期
_device_group = None
time.sleep(3)
descs = kp.core.scan_devices()
after_fw = "unknown"
for i in range(descs.device_descriptor_number):
dev = descs.device_descriptor_list[i]
if str(dev.usb_port_id) == str(target_port):
after_fw = str(dev.firmware)
break
return {
"status": "upgraded",
"before_firmware": detected_fw,
"after_firmware": after_fw,
"method": "kp_update_kdp_firmware_from_files",
"duration_ms": duration_ms,
}
except Exception as e:
return {"error": str(e), "stage": "verify"}
```
3. **main loop 加 dispatch**L1166-1180
```python
elif action == "firmware_upgrade":
result = handle_firmware_upgrade(cmd)
```
**驗收**
- `python3 server/scripts/kneron_bridge.py` 手動測試(不接 device→ ready 訊號正常
- 模擬 device-not-found 流程 → 回正確 error
- 實機接 KL520 KDP2 dongle 跑 `firmware_upgrade` → 應該 short-circuitdetected_fw 不是 KDP1、走 load_firmware 路徑)
- 實機接 KL520 KDP1如果使用者有舊 dongle→ 完整升級流程通過
---
### M9-2Go driver UpgradeFirmware() method1 人天)
**負責**Backend Agent
**依賴**M9-1 完成
**任務**
1. **driver interface 擴充** `server/internal/driver/interface.go`
```go
type DeviceDriver interface {
// ... 既有 methods ...
UpgradeFirmware(progressCh chan<- FirmwareProgress) error //
}
type FirmwareProgress struct {
Percent int `json:"percent"`
Stage string `json:"stage"` // "connecting" | "loading_loader" | "loading_firmware" | "verifying" | "done"
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
}
type DeviceInfo struct {
// ... 既有 ...
FirmwareIsLegacy bool `json:"firmwareIsLegacy,omitempty"`
FirmwareCanUpgrade bool `json:"firmwareCanUpgrade,omitempty"`
BundledFirmwareVer string `json:"bundledFirmwareVersion,omitempty"`
}
```
2. **`KneronDriver.UpgradeFirmware()`** 實作(位於 `kl720_driver.go`、雖然檔名歷史包袱、就先共用):
```go
func (d *KneronDriver) UpgradeFirmware(progressCh chan<- driver.FirmwareProgress) error {
d.mu.Lock()
d.info.Status = driver.StatusUpgrading // 新增 status
chip := d.chipType
port := d.info.Port
d.mu.Unlock()
// Disconnect existing connection first
d.Disconnect()
// Start a fresh Python bridge
if err := d.startPython(); err != nil {
return fmt.Errorf("start bridge: %w", err)
}
defer d.stopPython()
progressCh <- driver.FirmwareProgress{Percent: 5, Stage: "connecting"}
resp, err := d.sendCommand(map[string]interface{}{
"cmd": "firmware_upgrade",
"port": port,
"chip": chip,
})
if err != nil {
return fmt.Errorf("upgrade: %w", err)
}
// Push progressbridge 一次性回傳結果、 progress 模擬)
progressCh <- driver.FirmwareProgress{Percent: 90, Stage: "verifying"}
beforeFw, _ := resp["before_firmware"].(string)
afterFw, _ := resp["after_firmware"].(string)
d.mu.Lock()
d.info.FirmwareVer = afterFw
d.info.Status = driver.StatusDetected // 升級後需要重新 connect
d.needsReset = true // 下次 connect 走完整 reset
d.mu.Unlock()
progressCh <- driver.FirmwareProgress{
Percent: 100,
Stage: "done",
Message: fmt.Sprintf("upgraded %s -> %s", beforeFw, afterFw),
}
return nil
}
```
3. **新增 driver status**`StatusUpgrading DeviceStatus = "upgrading"`
4. **`Info()` 計算衍生欄位**:在 `Info()` 內根據 `FirmwareVer` 字串設定 `FirmwareIsLegacy`
```go
func (d *KneronDriver) Info() driver.DeviceInfo {
d.mu.Lock()
defer d.mu.Unlock()
info := d.info
fw := strings.ToUpper(info.FirmwareVer)
info.FirmwareIsLegacy = strings.Contains(fw, "KDP") && !strings.Contains(fw, "KDP2")
info.FirmwareCanUpgrade = info.FirmwareIsLegacy && bundledFirmwareExists(d.chipType)
info.BundledFirmwareVer = readBundledFwVersion(d.chipType) // 從 firmware/<chip>/VERSION
return info
}
```
5. **新建 `server/internal/firmware/service.go`** 仿 `flash/service.go`
```go
package firmware
type Service struct {
deviceMgr *device.Manager
tracker *ProgressTracker
}
func (s *Service) StartUpgrade(deviceID string) (string, <-chan driver.FirmwareProgress, error) {
session, _ := s.deviceMgr.GetDevice(deviceID)
// ... 仿 flash/service.go 的 goroutine + progressCh pattern ...
go func() {
err := session.Driver.UpgradeFirmware(task.ProgressCh)
if err != nil {
task.ProgressCh <- driver.FirmwareProgress{Percent: -1, Stage: "error", Error: err.Error()}
}
close(task.ProgressCh)
}()
return taskID, task.ProgressCh, nil
}
```
**驗收**
- `go build ./...` PASS
- `go test ./server/internal/firmware/...` 有單元測試mock driver
- 與 M9-1 整合測試(手動接 device 跑完整 flow
---
### M9-3API handler + WebSocket + DeviceInfo 擴充0.5 人天)
**負責**Backend Agent
**依賴**M9-2 完成
**任務**
1. **新增 endpoint** `server/internal/api/handlers/device_handler.go`
```go
func (h *DeviceHandler) UpgradeFirmware(c *gin.Context) {
id := c.Param("id")
taskID, progressCh, err := h.firmwareSvc.StartUpgrade(id)
if err != nil {
c.JSON(400, gin.H{
"success": false,
"error": gin.H{"code": "FW_UPGRADE_FAILED", "message": err.Error()},
})
return
}
go func() {
room := "firmware:" + id
for progress := range progressCh {
h.wsHub.BroadcastToRoom(room, progress)
}
h.firmwareSvc.CleanupTask(taskID)
}()
c.JSON(202, gin.H{"success": true, "data": gin.H{"taskId": taskID}})
}
```
2. **route 註冊**router.go
```go
devices.POST("/:id/firmware/upgrade", h.UpgradeFirmware)
```
3. **WebSocket room subscribe 已有 pattern**:客戶端 subscribe `firmware:<deviceId>`、跟既有 `flash:<deviceId>` 一致機制
4. **`DeviceInfo` 衍生欄位透過 `Info()` 自動回傳**M9-2 已做)
**驗收**
- `curl -X POST localhost:3721/api/devices/<id>/firmware/upgrade` 拿到 202 + taskID
- WebSocket 連到 `firmware:<id>` room 看到 progress event 流
- `GET /api/devices` 回傳已含新欄位
---
### M9-4Frontend FW badge + 升級 UI1.5 人天)
**負責**Frontend Agent
**依賴**M9-3 完成
**任務**
1. **DeviceCard 元件** 新增 FW badge位於 `frontend/src/components/devices/device-card.tsx`
- 從 `device.firmwareVersion` + `firmwareIsLegacy` 算出 badge 顏色
- 紅:`firmwareIsLegacy = true`KDP1 needs upgrade
- 黃:含 `KDP2` 但版本字串不符合內建 `bundledFirmwareVersion`未來功能、MVP 不細做)
- 綠:含 `KDP2`、且符合 bundled 版本
2. **升級按鈕**
- `firmwareCanUpgrade = true` 時顯示
- 點擊 → 開升級 modal
3. **升級 modal**
- 顯示警告:「升級期間請勿拔除裝置 / 預估 30-60 秒」
- 確認按鈕 → 呼叫 `POST /api/devices/:id/firmware/upgrade`
- subscribe WebSocket `firmware:<id>` room
- progress bar + 階段提示(連線中…/載入 loader.../載入 firmware.../驗證.../完成)
- 完成 → toast 通知「升級成功」+ 自動 rescan devices
- 失敗 → 顯示 error + 提示 re-plug device
4. **store 變更** `frontend/src/lib/store/devices-store.ts`(推測位置):
- 加 `firmwareUpgradeProgress` statekey: deviceId、value: FirmwareProgress
- 加 `subscribeFirmwareProgress(deviceId)` action
- 升級完成後自動 `rescan()`
5. **i18n 新增 keys**`frontend/src/lib/i18n/{zh-TW,en}.ts`
- `devices.firmware.upgrade.button` — 「升級韌體」/ "Upgrade Firmware"
- `devices.firmware.upgrade.modal.title` — 「韌體升級」/ "Firmware Upgrade"
- `devices.firmware.upgrade.modal.warning` — 「升級期間請勿拔除裝置...」/ ...
- `devices.firmware.upgrade.stage.connecting` — 「連線中...」/ "Connecting..."
- `devices.firmware.upgrade.stage.loadingLoader` — 「載入引導程式...」/ "Loading bootloader..."
- `devices.firmware.upgrade.stage.loadingFirmware` — 「載入韌體...」/ "Loading firmware..."
- `devices.firmware.upgrade.stage.verifying` — 「驗證中...」/ "Verifying..."
- `devices.firmware.upgrade.stage.done` — 「完成」/ "Done"
- `devices.firmware.upgrade.error.generic` — 「升級失敗、請重新插拔裝置後再試」/ ...
- `devices.firmware.badge.legacy` — 「需要升級」/ "Update Required"
- `devices.firmware.badge.outdated` — 「版本較舊」/ "Outdated"
- `devices.firmware.badge.uptodate` — 「最新」/ "Up to Date"
**驗收**
- `pnpm --dir frontend build` PASS
- 手動測試mock devicebadge 顯示正確、modal 流程順暢
- 真機測試M9-5 整合)
---
### M9-5三平台實機驗證1 人天)
**負責**Testing Agent
**依賴**M9-4 完成
**任務**
1. **macOSIntel + Rosetta**
- 接 KL520KDP1如果使用者有舊 dongle完整升級流程
- 接 KL520KDP2目前狀態「無升級需要」UI 路徑
- 接 KL720類似測試如果有 legacy KL720
2. **Windows**
- 同上、特別注意 WinUSB driver 綁定狀態
- 升級期間 USB re-enumerate 不會 hang HTTP 連線
3. **Linux**Ubuntu 22.04/24.04
- 同上、特別注意 udev rules
4. **異常路徑**
- 升級期間拔除 device → error 訊息合理、UI 可復原
- 升級期間關 app → server graceful shutdown + 升級中斷不留壞狀態
- 升級超時 → timeout 訊息合理、device 仍可用
5. **回歸測試**
- 既有功能model load / inference不受影響
- 升級完成後立刻能正常 inference
**驗收**
- 三平台 smoke test 報告(每個平台至少一張 dongle 跑完整 happy path + 一個異常路徑)
- 無回歸 bug
---
## 2. 涉及檔案清單
### 2.1 新建檔案
| 路徑 | 用途 | 預估行數 |
|------|------|--------|
| `server/scripts/firmware/KL520/fw_loader.bin` | KL520 USB Boot Loader binary從 warrenchen 複製)| ~10KB |
| `server/scripts/firmware/KL520/VERSION` | bundled FW 版本紀錄(如果還沒有)| ~5 行 |
| `server/scripts/firmware/KL720/VERSION` | 同上 | ~5 行 |
| `server/internal/firmware/service.go` | FW 升級 service | ~150 |
| `server/internal/firmware/progress.go` | FW progress tracker | ~50 |
| `frontend/src/components/devices/firmware-badge.tsx` | FW badge 元件 | ~80 |
| `frontend/src/components/devices/firmware-upgrade-modal.tsx` | 升級 modal | ~200 |
| `docs/autoflow/04-architecture/v2/device-firmware.md` | TDD 新子節 | ~300 |
| `docs/autoflow/04-architecture/adr/ADR-009-firmware-management.md` | ADR 紀錄 Q9 翻案 + 設計決策 | ~200 |
### 2.2 修改檔案
| 路徑 | 改什麼 | 影響範圍 |
|------|--------|---------|
| `server/scripts/kneron_bridge.py` | +`handle_firmware_upgrade()` + main loop dispatch + import time | +~100 行 |
| `server/internal/driver/interface.go` | +`UpgradeFirmware` method、+`FirmwareProgress` struct、+`StatusUpgrading`、+`DeviceInfo` 3 個新欄位 | +~30 行 |
| `server/internal/driver/kneron/kl720_driver.go` | +`UpgradeFirmware()` method、+`Info()` 計算衍生欄位、+`bundledFirmwareExists()` / `readBundledFwVersion()` helper | +~80 行 |
| `server/internal/api/handlers/device_handler.go` | +`UpgradeFirmware` handler + DI `firmwareSvc` | +~30 行 |
| `server/internal/api/router.go` | +route 註冊 | +1 行 |
| `server/cmd/main.go` 或 server init | DI 注入 firmware.Service | +~5 行 |
| `frontend/src/components/devices/device-card.tsx` | 嵌入 FirmwareBadge + 升級按鈕 | +~30 行 |
| `frontend/src/lib/store/devices-store.ts` | +`firmwareUpgradeProgress` state + subscribe action | +~50 行 |
| `frontend/src/lib/i18n/zh-TW.ts` | +11 個新 keys | +~15 行 |
| `frontend/src/lib/i18n/en.ts` | 同上 | +~15 行 |
| `docs/autoflow/02-prd/PRD-v2.md` | +§N 韌體管理章節 | +~80 行 |
| `docs/autoflow/04-architecture/TDD-v2.md` | +§2.10 子檔索引、+§3 風險清單條目 | +~20 行 |
| `installer/{macos,windows,linux}/*` | 確認 firmware bundle 進 installer payload既有應已涵蓋 | 0 行(驗證) |
---
## 3. 與 TDD v2.1 / Design v2.1 / PRD v2.1 受影響章節清單
### PRD v2.1 受影響章節(建議補丁)
| 章節 | 改動 |
|------|------|
| §2 變更摘要0.0| 加一列「v2.1 → v2.2:新增 Firmware 管理」|
| §N 韌體管理(新章節) | 主要內容、含 Q9 翻案聲明、MVP 範圍、商業背景 |
| §11 懸念列表 | + N-R5「FW 升級失敗後使用者復原流程」 |
| 變更紀錄 | 加一列 |
### TDD v2.1 受影響章節(建議補丁)
| 章節 | 改動 |
|------|------|
| §0.0 v2.0 → v2.1 差異速覽 | 加一列「Firmware 管理新增 M9 milestone series」|
| §2 子檔案地圖 | + 一列 `v2/device-firmware.md` |
| §3 風險清單 | + R-v2-8「FW 升級中拔除裝置」+ R-v2-9「升級後 USB re-enumerate race」|
| §0.1 v1 → v2 差異速覽 | 加一列「FW 管理路徑R5-Q9 翻案」|
| 新增 `v2/device-firmware.md` | 完整新文件 |
| `v2/server-lifecycle.md` | 不動FW 升級獨立於 server lifecycle |
| `v2/milestone-plan.md` | + M9 系列M9-0 ~ M9-5|
### Design v2.1 受影響章節(建議補丁)
| 檔案 | 改動 |
|------|------|
| `docs/autoflow/03-design/v2/*` 既有 | 不動 |
| 新增 `docs/autoflow/03-design/v2/device-firmware-ui.md` | FW badge 設計 + 升級 modal wireframe + i18n table |
| 中英雙語文案表 | + 11 個新 keys |
### 新 ADR
`docs/autoflow/04-architecture/adr/ADR-009-firmware-management.md`
- 狀態Proposed → 待使用者 sign-off
- 背景:使用者要求加 FW 升級、Q9 砍 flash 決策翻案
- 決策:用 KneronPLUS Python API`kp.core.update_kdp_firmware_from_files`)、不引入 DFUT.exe
- 替代方案:(a) DFUT.exe 拒因 Windows-only(b) ctypes 拒因 KneronPLUS Python API 已足夠;(c) 不做拒因真實痛點
- 後果:+1 個新模組 + ~5 人天工時 + 0KB 安裝包衝擊
---
## 4. 風險清單
### R-FW-1升級中拔除 device中度風險
**情境**:使用者在 firmware load 階段拔掉 USB。
**影響**
- KL520不寫 flash、最壞情況「沒升級成功」、re-plug 後仍是舊狀態、不 brick
- KL720可能正在寫 flash、有 brick 風險
**緩解**
1. UI modal 警告「升級期間請勿拔除裝置」
2. KL720 升級路徑加更明確的「不可中斷」提示
3. 失敗時自動 rescan + 提示 re-plug
4. 文件記錄:如果使用者真的拔了 KL720、可以走 warrenchen 的 DFUT.exe 救磚(不打包到 visionA-local、僅內部 SOP
### R-FW-2升級後 USB re-enumerate race低度風險
**情境**:升級成功 → device disconnect 回非零 → re-enumerate 中 → 立刻 reconnect 可能拿到舊 handle。
**緩解**
- bridge.py handler 內 `time.sleep(3)` 等 USB 穩定
- 不在 handler 內 reconnect、回傳成功後讓 Go 端執行 rescan + 重新 GetDevice
### R-FW-3KL520 升級不需要、但被誤觸發(低度風險)
**情境**:使用者 dongle 已是 KDP2、但 UI 顯示「升級」按鈕badge 計算錯誤)。
**緩解**
- bridge.py handler 內 detect_firmware 後若已是 KDP2、走 short-circuit 路徑(只 load_firmware to RAM、不寫 flash、< 5 秒完成
- driver `FirmwareIsLegacy` 判定要嚴格(必須是 `KDP` 但不含 `KDP2`
### R-FW-4跨晶片混淆中度風險
**情境**bridge.py 升級時用錯 firmware 檔(拿 KL520 firmware 燒到 KL720
**緩解**
- handler 強制要求 `chip` 參數、`_resolve_firmware_paths(chip)` 不接受空字串
- driver 自己 `d.chipType` 已從 USB pid 判定、傳給 bridge 不靠前端
- 加 unit test 確認 KL520 升級時不會用到 KL720 路徑
### R-FW-5簽章 / 法律風險(待釐清)
**情境**:把 Kneron 官方 firmwarefw_scpu.bin / fw_ncpu.bin / fw_loader.bin打包進我們的 installer、是否合法
**緩解**
- 我們已 bundle KDP2 firmware 4 個月、Q9 砍的是「使用者主動燒」、不是「打包 firmware」
- B4 已有「Kneron 預置模型 re-distribution 授權」未解決問題progress.md L792、firmware 同性質
- **建議**:發佈前跟 Kneron 確認、與 model bundle 一起處理
### R-FW-6HTTP timeout低度風險
**情境**:升級 KL720 需 ~180s、HTTP keep-alive / proxy timeout 可能更短。
**緩解**
- API 設計HTTP 立刻回 202、實際進度走 WebSocket已採此 pattern
- WebSocket 心跳設定 < 60s
### R-FW-7Windows admin 權限(中度風險)
**情境**`kp.core.install_driver_for_windows` 需 admin、若 WinUSB driver 未綁定、升級會在第一步失敗。
**緩解**
- 既有 visionA-local 已有 driver 安裝邏輯M1+ TODO
- 升級 handler 偵測到 driver 未綁時、明確錯誤訊息引導使用者
- 不在 visionA-local 內自動裝 driver、提示使用者重跑 installer
---
## 5. 與既有架構衝突點(細項)
### 5.1 既有 `Flash()` method 不重用
`Flash()` 是 load model、不混進 firmware 升級。確保 plan 內**不**動 `flash/service.go`
### 5.2 `kl720_driver.go` 檔名包袱
`UpgradeFirmware()` 方法放在這個檔內、但**不**改檔名(範圍外)。新建 `firmware/` package 與 `flash/` 並列、不疊在 `kneron/` 內。
### 5.3 既有 `restartBridge()` 不擴充
`restartBridge()` 是 KL520 換 model 時用、跟 firmware 升級不同流程。**不**擴充它、新流程獨立。
### 5.4 既有 `needsReset` flag 要善用
升級完成後、driver 應設 `needsReset=true`、下次 connect 走完整 reset flow既有邏輯。確保升級後第一次 inference 不踩 Error 15 SEND_DATA_TOO_LARGE。
### 5.5 既有 WS rooms 命名規範
既有 `flash:<id>` / `inference:<id>` → 新增 `firmware:<id>`、命名一致。
### 5.6 既有 `DeviceInfo.Status` 列舉
新增 `StatusUpgrading`、跟既有 `StatusConnecting / StatusFlashing / StatusInferencing` 並列。前端要新增對應 status badge。
### 5.7 既有 watchServer Error state 機制
FW 升級失敗**不**升級為 server Error statedevice-level 失敗)、僅 device 進 `StatusError`。確保 watchServer goroutine 不會把 FW 升級 timeout 誤判為 server 死掉。
---
## 6. 階段 A 完成後的階段 B 評估提示(不在 MVP 範圍)
階段 A 驗證後、使用者可以再決定是否做階段 B。階段 B 候選:
1. **手動降版 KDP2 → KDP1**(給開發者測試用)
- 需要從 warrenchen 複製 `firmware/KL520_kdp/`~80KB
- bridge.py 加 `handle_firmware_downgrade` handler
- 暴露在 Settings > 進階面板(不在 Devices 頁主要 UI
- 預估 1.5 人天
2. **加 KL630 / KL730 支援**
- 需先擴 driver / bridge.py 認 product_id 0x0630 / 0x0730、能 load firmware
- 需驗 KneronPLUS SDK 對應版本是否支援這兩個晶片
- 預估 3-4 人天driver 擴展 + FW 升級擴展 + 三平台驗證)
3. **多版本 firmware 並存**
- `server/scripts/firmware/<chip>/<version>/fw_*.bin`
- 前端 dropdown 讓使用者選版本
- 預估 1.5 人天
合計階段 B 約 6-7 人天、加總 MVP 5 人天、完整版約 11-12 人天。
---
## 7. 給 Orchestrator 的下一步建議
1. **本份研究 plan 給使用者 review**
2. 使用者同意 MVP方案 A
- 啟動 **PM Agent** 補 PRD v2.2 韌體管理章節
- **Architect Agent** 自己補 TDD v2.1 §2.10 + 寫 ADR-009
- 啟動 **Design Agent** 補 Devices 頁面 FW UI 規格
3. 三方互審 + 使用者 sign-off
4. 開發進入 M9-1 ~ M9-5 流程(依本檔 §1
5. 階段 B 暫不啟動、等階段 A 驗證後再評估

View File

@ -0,0 +1,703 @@
# B 階段研究KL630 / KL730 driver 擴展支援
> 對應 research index §40
> 範圍B 階段(在 A 階段 MVP 完成後)擴 driver 支援 KL630 + KL730並把它們納入 FW 偵測 + 升降版流程
> 撰寫日期2026-05-24
> 限制:純 plan、不出 code、不改既有檔案
> 路徑使用相對路徑(相對於 `/Users/jimchen/visionA/local-tool/`
---
## 0. TL;DR
1. **warrenchen 沒有對 KL630/KL730 做 FW 升降版**——他們只 bundle 了 firmware tar 檔(為了未來雲端 `/firmware/load` endpoint`legacy_plus121_runner.py` 與相關升級流程**完全只針對 KL520 + KL720 KDP1 → KDP2 場景**。對 KL630/KL730、warrenchen 只實作 driver install + scan。
2. **既有 visionA-local 對 KL630/KL730 偵測得到、但連不上**
- `kneron_bridge.py:_KNOWN_PRODUCTS` 已含 `0x0630: "KL630"` / `0x0730: "KL730"`L657-664→ scan 階段會看到名字
- 但 `handle_connect()` 的 chip 判斷 fall-through 到 `_device_chip = "KL520"`L740-741→ 用 KL520 firmware 路徑載入會直接失敗
- `chipFromProductID()`detector.go L97-111也是 default 回 KL520 → driver 會被建成 KL520 type
3. **擴 driver 不大、但 .tar firmware 處理是 unknown 區**
- 認 product_id 並 route 到 `_device_chip = "KL630"/"KL730"` 是 5 行改動
- 但 KneronPLUS Python API `load_firmware_from_file()` 接受**單個檔案路徑scpu, ncpu**、`.tar` 是新格式KDP2 v2.x SDK 出現的打包形態)、**SDK API 對 `.tar` 的接受方式必須查官方文件驗證**
- **建議**B 階段先做「scan + 顯示 KL630/KL730」不能 inference、再做「FW load」、最後才做「FW 升降版」
4. **B 階段拆三層、不要一次做完**
- **B0**最簡driver 認 KL630/KL730、scan 看得到、不能連——使用者至少知道我們認得這晶片
- **B1**bridge.py 能 load .tar firmware、能 connect、能 inference——FW 偵測完整
- **B2**:完整 FW 升降版(手動降版面向一般使用者)——多版本管理 + UX
---
## 1. warrenchen 對 KL630 / KL730 怎麼處理(資料來源確認)
### 1.1 程式碼層面:只有 driver install + scan
`/tmp/web_academy_prototype/local_service_win/LocalAPI/main.py` grep `KL630|KL730` 只有 4 個位置:
| 位置 | 用途 |
|------|------|
| L188 `DriverInstallRequest.target` 文件 | API 參數 hint`ALL \| KL520 \| KL720 \| KL630 \| KL730 \| KL830` |
| L433-436 `_target_product_ids()` | 把 `"KL630"` / `"KL730"` 字串轉成 `kp.ProductId.KP_DEVICE_KL630` / `KP_DEVICE_KL730` enum |
| L443-445 同上 `target == "ALL"` 分支 | ALL 把 KL630/KL730 也納入 driver install 對象 |
| `STRATEGY.md` L196-203 `/driver/ensure` 註解 | 文件描述支援的 target 列表 |
**完全沒有**
- `load_firmware_from_file(KL630_files)` 之類的 call
- `update_kdp_firmware_from_files(KL630_files)` 之類的 call
- `/firmware/load` 對 KL630/KL730 的專用處理
- 任何 `.tar` 解壓 / 處理邏輯
**結論**warrenchen 對 KL630/KL730 的支援**僅止於 Windows WinUSB driver 安裝**。連線 / firmware load / inference 都沒實作過。
### 1.2 為什麼 warrenchen bundle 了 KL630/KL730 firmware但沒用
`firmware/KL630/VERSION``SDK-v2.5.7``firmware/KL730/VERSION``SDK-v1.3.0`、跟 KL520/KL720 的 `2.2.0` 顯然命名規則不一樣KL630/KL730 用 SDK 版本、不是 firmware 版本)。
推測 warrenchen 的策略:
1. 把 SDK 官方提供的 firmware 整套 vendored 進來(一次性 release 製作的副產物)
2. 為未來 `/firmware/load` 端點預留——讓雲端業務邏輯決定要不要 load
3. 但實際路徑、API 對應、`.tar` 解包處理 PoC 階段沒做
**對我們的啟示**:我們無法從 warrenchen 偷到 KL630/KL730 的「連線 / FW 升降版」流程實作、必須**自己研究 KneronPLUS Python SDK 對這兩個晶片的 API**。
### 1.3 STRATEGY.md 對 KL630/KL730 的描述
僅有 1 處:`/driver/ensure` 註解列出支援 target。**沒有「Legacy Firmware Story」式的 KL630/KL730 章節**——對 KL520/KL720 寫了一大段STRATEGY.md L521-545、對 KL630/KL730 完全沒寫。
---
## 2. 既有 visionA-local 對 KL630 / KL730 的支援狀態
### 2.1 已認得 product_idscan 看得到)
`server/scripts/kneron_bridge.py` L657-664
```python
_KNOWN_PRODUCTS = {
0x0100: "KL520",
0x0200: "KL720",
0x0720: "KL720",
0x0530: "KL530",
0x0630: "KL630",
0x0730: "KL730",
}
```
→ scan 階段(`_scan_with_pyusb` / `handle_scan`)會在 `devices[].product_id``chip` 欄位顯示正確值。
### 2.2 chip 判斷邏輯漏判connect 會誤路由到 KL520
`kneron_bridge.py` L732-741
```python
pid = target_dev.product_id
if "kl720" in device_type.lower():
_device_chip = "KL720"
elif "kl520" in device_type.lower():
_device_chip = "KL520"
elif pid in (0x0200, 0x0720):
_device_chip = "KL720"
else:
_device_chip = "KL520" # ← KL630/KL730/KL530 全部掉這裡
```
→ KL6300x0630/ KL7300x0730會被當成 KL520 處理。
- `_resolve_firmware_paths("KL520")` 會去找 `firmware/KL520/fw_scpu.bin` → 拿到的是 KL520 的 firmware
- `load_firmware_from_file(KL630_device, kl520_scpu)` → 預期會炸(不確切錯誤碼、估計 `KP_FW_INFO_ERR` 或類似)
### 2.3 Go 端 detector 也漏判
`server/internal/driver/kneron/detector.go` L97-111
```go
func chipFromProductID(productID string) (chip string, deviceType string) {
pid := strings.ToLower(strings.TrimSpace(productID))
switch pid {
case "0x0100":
return "KL520", "kneron_kl520"
case "0x0200", "0x0720":
return "KL720", "kneron_kl720"
default:
return "KL520", "kneron_kl520" // ← KL630/KL730/KL530 全部掉這裡
}
}
```
→ Go driver 也會把 KL630/KL730 當成 KL520。
### 2.4 沒 bundle KL630/KL730 firmware
`server/scripts/firmware/` 目前只有 `KL520/` + `KL720/`、沒有 `KL630/` `KL730/` `KL530/`
---
## 3. KneronPLUS Python SDK 對 KL630 / KL730 的 API 落差(**需 SDK 官方文件驗證**
### 3.1 已確認的事
從 warrenchen `main.py` L433-436 看到:
```python
if target == "KL630":
return [kp.ProductId.KP_DEVICE_KL630]
if target == "KL730":
return [kp.ProductId.KP_DEVICE_KL730]
```
→ KneronPLUS Python wheel 內 `kp.ProductId` enum 有 `KP_DEVICE_KL630` / `KP_DEVICE_KL730`
`local_service_win/KneronPLUS-3.1.2-py3-none-any.whl` 推測:
- KneronPLUS SDK 3.x 系列就有 KL630/KL730 支援
- 我們既有的 KneronPLUS wheel 版本需要查(**待確認**)——如果是 2.x、可能要升級
### 3.2 未確認、必須查 KneronPLUS 官方文件的事
| 問題 | 影響 |
|------|------|
| `kp.core.connect_devices([KL630_port_id])` 是否能跟 KL520/KL720 共用 API、不需要特殊參數 | 影響 connect 流程設計 |
| `load_firmware_from_file(dg, scpu, ncpu)` 對 KL630/KL730 是否仍是兩個 .bin 檔(解壓 .tar 後)? | 影響 .tar 處理策略runtime extract 還是 build time extract |
| 是否有新的 `load_firmware_from_tar()` 之類的 API | 如果有、我們直接餵 .tar 不用解壓 |
| `update_kdp_firmware_from_files()` 對 KL630/KL730 是否適用?還是改用 `update_kdp2_firmware_from_files()` / `update_kdp2_firmware_from_tar()` | 影響 FW 升降版實作 |
| KL630/KL730 是否仍有「Loader / KDP / KDP2」三種 firmware state、還是只有兩種 | 影響 FW 偵測邏輯 |
| KL630/KL730 是否仍像 KL520 一樣每次都要 load firmware 到 RAM、還是像 KL720 一樣 flash-based | 影響 connect 流程設計(重大) |
| `inference.generic_image_inference_send/receive` 對 KL630/KL730 是否一致? | 影響 inference 流程是否要分支 |
| `kp.core.scan_devices()` 對 KL630/KL730 的 `firmware` 字串可能值("KDP2"? "KDP3"? 其他?)| 影響 FW badge 顯示邏輯 |
### 3.3 取得這些資訊的方法
1. **KneronPLUS SDK 官方 PDF 文件**——使用者應該有 SDK release 附的 documentation`KneronPLUS_SDK_Reference_Manual.pdf` 之類)、查 KL630/KL730 section
2. **wheel 內 Python 原始碼**——`unzip KneronPLUS-3.1.2-py3-none-any.whl` 後看 `kp/core.py` 的 docstring + `kp/__init__.py` 看 enum 定義
3. **SDK example code**——Kneron 通常會附 sample用 KL630/KL730 跑 inference 的範例)、看他們怎麼初始化裝置
4. **在真實 KL630/KL730 device 上 trial-and-error**——使用者手上若有 KL630/KL730 dongle、可以裝 KneronPLUS、Python REPL 跑 `kp.core.scan_devices()` 直接看回傳
**B 階段第一個 milestone 必須先把這些查清楚**,否則所有後續設計都是猜的。
---
## 4. 既有 driver 怎麼擴:「新加 driver」vs「同一個 driver 加 case」
### 4.1 兩個選項對比
| 維度 | A同一個 driver 加 case | B拆出 KL630Driver / KL730Driver |
|------|------------------------|----------------------------------|
| 程式碼複用 | 高(已支援 KL520/KL720 共用、邏輯相近處不重寫) | 低(每個 chip 自己一份 driver、可能 80% 重複) |
| 流程差異容納度 | 中(用 switch / if-else 處理差異、容易長成 spaghetti | 高(每個 driver 清楚自己 chip 的特性) |
| 維護負擔 | 中(單檔變大、但所有 chip-specific 邏輯集中) | 高4 個檔案要同步更新通用邏輯) |
| 測試隔離 | 中mock 整個 driver、無法只測 KL630 部分) | 高(每個 driver 獨立測) |
| 既有 bridge.py 是否共用 | 是(已是這個結構) | 是bridge.py 必定要共用,重 spawn Python 太貴) |
| 改動成本 | 低(擴 `kl720_driver.go` 既有結構) | 高(拆 driver 還要改 detector / manager / DI |
### 4.2 建議:選 A同一個 driver 擴 case
**理由**
1. KL520/KL720 已是同一個 driver、檔名雖叫 `kl720_driver.go` 但已是「Kneron 通用 driver」再加 KL630/KL730 與這個方向一致
2. bridge.py 強制共用Python subprocess spawn 成本高driver 拆但 bridge 不拆會造成 driver-bridge 對應錯位
3. KL520 vs KL720 的差異USB Boot vs flash-based、是否每次都要 load firmware、reset 策略)**比** KL520 vs KL630 的差異**可能還大**——KL630/KL730 是新世代、可能跟 KL720 行為更接近(都是 flash-based
4. 拆 driver 不會省工作量、反而 5 個 driver 共用邏輯時更難改
**接受的取捨**
- `kl720_driver.go` 檔名包袱繼續存在(已是現狀、本 B 階段不改名)
- 內部 switch 變多(但每個 chip 的差異邏輯應 < 50 可控
### 4.3 具體擴的位置
`server/internal/driver/kneron/kl720_driver.go`
**改動 1**`NewKneronDriver()`L48-59chip 判斷加 KL630/KL730
```go
// 偽碼、不出 code
chip := "KL520"
typ := strings.ToLower(info.Type)
switch {
case strings.Contains(typ, "kl720"):
chip = "KL720"
case strings.Contains(typ, "kl730"):
chip = "KL730"
case strings.Contains(typ, "kl630"):
chip = "KL630"
case strings.Contains(typ, "kl530"):
chip = "KL530" // 視 SDK 支援度決定要不要加warrenchen 沒提)
}
```
**改動 2**`Connect()` / `Flash()` / `RunInference()` 內所有 `if chip == "KL720"` 的分支點要加 KL630/KL730 行為(**待 SDK 文件驗證後填**
**改動 3**:新增 helper 把 chip 行為抽出來:
```go
// 偽碼
type chipBehavior struct {
RequireFirmwareLoadEverySession bool // KL520=true, KL720/KL630/KL730=待驗
SupportedFlashUpdate bool // FW 升降版是否支援
TimeoutMS int // KL720=60000, KL520=10000, KL630/KL730 待驗
InferenceConfig map[string]interface{} // 各 chip inference 細部設定
}
func behaviorFor(chip string) chipBehavior { ... }
```
→ 把現有散在各 method 裡的 `if chip == "KL720"` 集中到一個地方、之後加新 chip 只動這個表(**Refactor、不算 B 階段的 mandatory work、但建議順手做**
`server/internal/driver/kneron/detector.go`
**改動 1**`chipFromProductID()`L97-111switch 加 case
```go
// 偽碼
case "0x0530":
return "KL530", "kneron_kl530" // 視 SDK 支援
case "0x0630":
return "KL630", "kneron_kl630"
case "0x0730":
return "KL730", "kneron_kl730"
```
**改動 2**`DetectDevices()` chipCount map 自動 work不用改
---
## 5. 連線流程跟 KL520 / KL720 的差異(**待 SDK 文件驗證**
### 5.1 KL520 vs KL720 既有差異對照visionA-local 已實作)
| 維度 | KL520 | KL720 |
|------|-------|-------|
| Hardware 模式 | USB Boot無 flash | Flash-based |
| connect 階段 firmware 載入 | 必載(每次都要載 fw_scpu + fw_ncpu 到 RAM| 不載firmware 已預燒在 flash |
| 例外KDP1 legacypid=0x0200| n/a | 必走 `connect_without_check` + 載 KDP2 firmware 到 RAM |
| Timeout | 10s | 60s大 NEF 傳輸) |
| reset 策略 | restartBridge 8s 重啟 bridge | `kp.core.reset_device(REBOOT)` |
| Model load 後狀態 | 只能 load 一個 model、要換 model 必須 restart bridge | 可自由 reload |
### 5.2 KL630 / KL730 推測(**待驗證**
從 warrenchen `_target_product_ids` 把 KL720_LEGACY 和 KL720 並列、但 KL630/KL730 只有單一 product_id、推測
- KL630/KL730 **沒有 legacy/new 雙版本**(不像 KL720 有 KDP1 → KDP2 升級需求)
- 都是 flash-based跟 KL720 而非 KL520 一族)
- connect 階段**不需要** load firmware除非在 Loader mode
**但這只是推測**。SDK 文件 / 實機驗證才能確認。
### 5.3 風險:如果 KL630/KL730 行為跟 KL520 更像(每次都要 load firmware
`.tar` firmware 的 runtime load 流程就變得**頻繁**(每次 connect 都跑一次解壓 + load、可能影響 connect 速度。
**緩解**
- build time 預先解壓 `.tar``firmware/KL630/extracted/{fw_scpu.bin, fw_ncpu.bin, ...}`、connect 時直接餵解壓後檔案
- 細節在「.tar firmware 處理」研究檔41-tar-firmware-handling.md展開
---
## 6. bridge.py 擴展工作
### 6.1 改動清單
| 改動 | 程式碼位置 | 大小 |
|------|----------|------|
| chip 判斷加 KL630/KL730 case | `handle_connect()` L732-741 | +10 行 |
| `_resolve_firmware_paths(chip)` 支援 `.tar` 格式 | `_resolve_firmware_paths()` L157-172 | +30 行(解壓邏輯)/ 或 +5 行(直接餵 .tar 給 SDK |
| `handle_connect()` 對 KL630/KL730 的 firmware 流程分支 | 新增 if 分支或重構成 dispatcher | +30-50 行(待 SDK 驗證) |
| `handle_firmware_upgrade()` 對 KL630/KL730 的支援 | A 階段已加、B 階段擴)| +20 行 |
| `handle_firmware_downgrade()`(新)| 新 handler | +60-100 行 |
| `handle_firmware_list_versions()`(新)| 新 handler、列出 bundled 多版本 | +30 行 |
### 6.2 chip 判斷擴展(具體偽碼)
```python
# kneron_bridge.py handle_connect() 偽碼
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 "kl530" in device_type_lower or pid == 0x0530:
_device_chip = "KL530" # 視 SDK 支援度
elif "kl520" in device_type_lower or pid == 0x0100:
_device_chip = "KL520"
else:
_device_chip = "KL520" # fallback、保留既有行為
```
### 6.3 firmware path resolution 擴展
```python
# _resolve_firmware_paths(chip) 偽碼
def _resolve_firmware_paths(chip="KL520"):
base = os.path.dirname(os.path.abspath(__file__))
fw_dir = os.path.join(base, "firmware", chip)
# KL520 / KL720 走原本 .bin 路徑
if chip in ("KL520", "KL720"):
scpu = os.path.join(fw_dir, "fw_scpu.bin")
ncpu = os.path.join(fw_dir, "fw_ncpu.bin")
if os.path.exists(scpu) and os.path.exists(ncpu):
return scpu, ncpu
return None, None
# KL630 / KL730 走 .tar 路徑
if chip in ("KL630", "KL730"):
# 策略一:直接餵 .tar 給 SDK如果 SDK 接受)
fw_tar = os.path.join(fw_dir, "kp_firmware.tar")
if os.path.exists(fw_tar):
return fw_tar, None # second arg None or different semantics、待驗
# 策略二build time 已解壓、走解壓後路徑
extracted_scpu = os.path.join(fw_dir, "extracted", "fw_scpu.bin")
extracted_ncpu = os.path.join(fw_dir, "extracted", "fw_ncpu.bin")
if os.path.exists(extracted_scpu) and os.path.exists(extracted_ncpu):
return extracted_scpu, extracted_ncpu
return None, None
return None, None
```
**注意**:以上是兩種候選策略、實際選哪個取決於 SDK 文件——詳見 `41-tar-firmware-handling.md`
---
## 7. inference 流程的差異(**待 SDK 文件驗證**
### 7.1 既有 visionA-local inference 流程
`kneron_bridge.py`grep `generic_image_inference_send`),既有用 `kp.inference.generic_image_inference_send/receive` API。這 API 是 KneronPLUS v2.x+ 的通用 inference API、**理論上**支援所有晶片。
### 7.2 推測KL630/KL730 inference 流程**可能**不變
理由:
- `generic_image_inference_*` 設計成 chip-agnostic
- 模型編譯時已 target 特定 chipYOLOv5_KL520_*.nef vs YOLOv5_KL630_*.nefdriver 只負責跑、不在 driver 端區分 chip
### 7.3 但有可能需要調整的點
- **Model NEF target_chip 對應**:我們既有 `_detect_model_type()` 根據 NEF 內容判斷 model—— `_model_nef.target_chip` 屬性對 KL630/KL730 NEF 會回什麼字串、需要驗證
- **Input image format / preprocessing**KL630/KL730 NPU 規格不同、input shape / channel order 可能要從 NEF 拿、`generic_image_inference_send` 的 input 配置可能需要調整
**B1 階段必驗**:跑一支 KL630 NEF從 SDK 範例拿)、看 inference 是否能直接 work。
---
## 8. 多版本 firmware 並存B2 階段才做)
### 8.1 為什麼需要
「手動降版面向一般使用者」場景下、使用者可能需要選 firmware 版本:
- 「我目前裝的是 KL630 SDK-v2.5.7、但我的 model 是用 SDK-v2.4 編的、想降版回 v2.4」
- 「KL520 KDP2 v2.2.0 跟某個 third-party tool 不相容、想降版回 v2.1.0」
但這場景對「一般使用者」而言**很罕見**——更常見的是「我拿到舊 KDP1 dongle 想升級」A 階段已涵蓋)。
### 8.2 儲存結構(建議)
```
server/scripts/firmware/
├── KL520/
│ ├── current/ ← MVP 既有 firmwaresymlink 或實檔)
│ │ ├── fw_scpu.bin
│ │ ├── fw_ncpu.bin
│ │ ├── fw_loader.bin
│ │ └── VERSION ← "2.2.0"
│ ├── v2.2.0/ ← 多版本目錄
│ │ ├── fw_scpu.bin
│ │ ├── fw_ncpu.bin
│ │ ├── fw_loader.bin
│ │ └── VERSION
│ ├── v2.1.0/ ← 舊版(如果 bundle
│ │ ├── fw_scpu.bin
│ │ ├── fw_ncpu.bin
│ │ └── VERSION
│ └── kdp1/ ← KDP1 降版用B 階段)
│ ├── fw_scpu.bin
│ ├── fw_ncpu.bin
│ └── VERSION ← "1.x.x" 或 "KDP1"
├── KL720/
│ └── ...同結構...
├── KL630/
│ └── ...同結構,但 firmware 檔是 .tar...
└── KL730/
└── ...同結構...
```
**對 MVPA 階段)的影響**
- A 階段只用 `KL520/current/` `KL720/current/`、現有結構不需動
- B 階段加多版本時、把現有檔搬進 `current/` + 加 `v2.2.0/` 副本(保留 backward compat、`_resolve_firmware_paths` fallback 機制)
### 8.3 版本選擇邏輯
- 預設用 `current/`
- 使用者主動選版本 → driver 傳 `version` 參數給 bridge.py
- bridge.py 用 `firmware/<chip>/<version>/...` 解析路徑
→ 詳細 API + UI 設計在「手動降版」研究檔(`42-manual-downgrade-for-end-users.md`)。
---
## 9. 風險分析B 階段新增)
承前一份 R-FW-1 ~ R-FW-7、本份補
### R-FW-8KneronPLUS SDK 對 KL630/KL730 API 不可預測(高度風險)
**情境**:我們現在對 KL630/KL730 的 SDK 行為都是推測、實際開發時可能:
- `load_firmware_from_file(.tar)` 不接受、要先解壓
- `update_kdp_firmware_from_files()` 對 KL630 沒效、要用別的 API
- `kp.ProductId.KP_DEVICE_KL730` 在我們既有 wheel 版本不存在(要升級 wheel
**緩解**
1. **B0 milestone 第一件事**:實機 + Python REPL 驗證所有 API、記錄到 `41-tar-firmware-handling.md` 後續更新
2. 必要時升級 KneronPLUS wheel 版本(既有可能是 2.x、warrenchen 用 3.1.2
3. wheel 版本升級會影響 KL520/KL720 既有行為regression 風險)、必須三平台回歸驗
### R-FW-9.tar 解包對安裝包大小衝擊(低度風險)
**情境**:如果採 build time extract、安裝包同時包 .tar + 解壓後的 .bin、大小翻倍。
**緩解**
- build script 解壓後刪原始 .tar只 ship .bin、安裝包多塞 .bin 但少塞 .tar、net 差約等於壓縮率
- 估算KL630 .tar 預估 ~2-3MB、解壓後 .bin 約 ~3-4MB、淨增 +1MBacceptable
### R-FW-10KL630/KL730 沒有 Loader mode 概念(中度風險)
**情境**:如果 KL630/KL730 是純 flash-based、永遠沒有 USB Boot Loader、那 `kp_loader.tar` 用不到、FW 升級流程也沒有「先 load loader 再 load firmware」這一段。
**影響**bridge.py handler 設計要分支、不能把 KL520/KL720 的「Loader 路徑」硬套到 KL630/KL730。
**緩解**B0 milestone 驗證時、用 `kp.core.scan_devices()` 看 KL630 firmware 字串可能值(是否會出現 "Loader" / "KDP" / "KDP2" / "KDP3" / 其他),決定 chip-specific 分支邏輯。
### R-FW-11一般使用者誤觸降版 brick 風險高度風險B2
**情境**:手動降版面向一般使用者後、誤操作可能把 dongle 變磚。
**緩解**(你 architect 標記、實作由 design + frontend
- UI 警告 + 二次確認design 領域)
- driver 端做安全 guard
- 拒絕降版到比 current 還新的版本(這是升版、不是降版、走升版 API
- 拒絕跨晶片誤匹配KL520 firmware 拿來降 KL630
- 降版前強制備份當前 firmware 版本資訊到 `.autoflow/firmware-history.json`(可選)
### R-FW-12多版本管理 UX 複雜度中度風險B2
**情境**dropdown 多版本選擇對一般使用者過於 cluttered、容易誤選舊版。
**緩解**(標記給 design
- 預設只顯示「升級到最新」+「降版」兩個按鈕
- 「降版」展開後才顯示版本 dropdown
- 不熟悉版號的使用者預設選最新降版目標("v2.1.0 latest before 2.2.0"
---
## 10. B 階段 milestone 拆法
承 A 階段 M9-1 ~ M9-5、B 階段拆 M9-6 ~ M9-12
### 整體依賴圖
```
M9-5 (A 階段三平台驗證) ✅
M9-6 (research validation): SDK 文件 + 實機驗證 KL630/KL730 API 行為
M9-7 (B0: 認 chip): driver + bridge.py 認 KL630/KL730、scan 顯示完整、connect 暫不支援
M9-8 (B1.1: tar handling): bridge.py 處理 .tar firmware解壓策略選定 + 實作)
M9-9 (B1.2: connect): bridge.py + driver 完整支援 KL630/KL730 connect + inference
M9-10 (B1.3: FW 升版): firmware_upgrade handler 擴 KL630/KL730 支援
↓ ─────────────┐
M9-11 (B2.1: 多版本) M9-12 (B2.2: 降版 UI + UX)
↓ ↓
M9-13 (三平台實機驗證所有 B 階段功能)
```
### 各 milestone 細節
#### M9-6 — SDK 驗證 + KneronPLUS wheel 升級評估1 人天)
**負責**Architect Agent純研究、不寫產品 code
**任務**
1. 取得 KneronPLUS SDK 官方文件PDF 或網頁),查 KL630/KL730 章節
2. unzip `KneronPLUS-3.1.2-py3-none-any.whl`、讀 `kp/core.py` / `kp/__init__.py`
3. 在實機(使用者有 KL630/KL730 dongle 的話)跑 Python REPL
```python
import kp
descs = kp.core.scan_devices()
for i in range(descs.device_descriptor_number):
dev = descs.device_descriptor_list[i]
print(dev.product_id, dev.firmware, dev.is_connectable, dev.usb_port_id)
# 試 connect、load .tar firmware、看回傳
```
4. 評估是否要升級 KneronPLUS wheel從目前版本 → 3.1.2+ 回歸風險
5. 產出 `41-tar-firmware-handling.md` 「實測結果」段落填空
**驗收**:研究文件補齊、確認所有 §3.2 未確認項目都有答案
#### M9-7 — B0 認 chip0.5 人天)
**負責**Backend Agent
**任務**
1. `kneron_bridge.py` chip 判斷加 KL630/KL730 case§6.2 偽碼)
2. `detector.go:chipFromProductID()` 加 case§4.3 偽碼)
3. `kl720_driver.go:NewKneronDriver()` 加 chip 判斷§4.3 偽碼)
4. `handle_connect()` 對 KL630/KL730 暫時回 `{"error": "KL630/KL730 not yet fully supported, only scan info available"}`、不假裝能連
5. Frontend Devices 頁面、未支援 chip 顯示 disabled 狀態 + tooltip「即將支援」
**驗收**
- scan 看得到 KL630/KL730 dongle 名字、不會誤標成 KL520
- 按 connect 會看到友善訊息(非靜默失敗)
#### M9-8 — B1.1 .tar firmware handling1.5 人天)
**負責**Backend Agent
**任務**
1. 從 warrenchen 複製 `firmware/KL630/{kp_firmware.tar, kp_loader.tar}` `firmware/KL730/{kp_firmware.tar, kp_loader.tar}``server/scripts/firmware/`
2. 加 `firmware/KL630/VERSION` `firmware/KL730/VERSION`
3. `_resolve_firmware_paths()` 擴展支援 .tar§6.3 偽碼)
4. 決定解壓策略runtime / build time根據 M9-6 SDK 驗證結果決定
- 如果 SDK 接受 .tar → 直接餵
- 如果不接受 → build time 解壓進 `extracted/`、安裝包 ship 解壓後 .bin
5. 如果走 build time 解壓:擴 `installer/` 的 build script、確保解壓步驟跑在 wails build 之前
6. 加 unit testmock`_resolve_firmware_paths("KL630")` 回正確路徑
**驗收**
- `python3 kneron_bridge.py``_resolve_firmware_paths("KL630")` 回正確路徑(不接 device 也能測)
- 安裝包大小變化在預估範圍(+3-5MB
#### M9-9 — B1.2 connect + inference2 人天)
**負責**Backend Agent
**任務**
1. `handle_connect()` 對 KL630/KL730 的完整流程(連線 + 必要時載入 firmware
2. driver `Flash()` / `RunInference()` 對 KL630/KL730 的 chip-specific 邏輯(從 M9-6 研究結果得出)
3. 建立 `chipBehavior` 抽象§4.3 改動 3、可選 refactor
4. Frontend Devices 頁面、KL630/KL730 解除 disabled、能正常 connect + 跑 inference
5. 三平台 smoke testKL630/KL730 各跑一支 sample NEF
**驗收**
- KL630/KL730 dongle 能 connect + 跑 inference 成功
- 既有 KL520/KL720 沒有 regression
#### M9-10 — B1.3 FW 升版擴 KL630/KL7301 人天)
**負責**Backend Agent
**任務**
1. `handle_firmware_upgrade()` 加 KL630/KL730 路徑A 階段的 handler 擴展)
2. 升版 stage 邏輯依照 M9-6 結論調整(如 KL630/KL730 沒有 "KDP" legacy state、就跳過 loader 那段)
3. Frontend FW badge 對 KL630/KL730 顯示對應狀態
4. 三平台實機驗證升版
**驗收**
- KL630/KL730 dongle如果有比 bundled 還舊的 firmware能升級成功
#### M9-11 — B2.1 多版本 firmware 並存1.5 人天)
**負責**Backend Agent
**任務**
1. 重整 `firmware/` 目錄結構為 §8.2 設計(每 chip 多版本 + `current/` symlink/副本)
2. bridge.py `handle_firmware_list_versions(chip)` 新 handler、列出可選版本
3. bridge.py `handle_firmware_upgrade` / `handle_firmware_downgrade` 接受 `version` 參數
4. driver 與 service 層擴展傳 version 參數
5. API endpoint `GET /api/devices/:id/firmware/versions`
6. Bundle 多版本(每 chip 至少 1 個非 current 的版本可選、實際版本由使用者決定 ship 哪些)
**驗收**
- API `GET /firmware/versions?chip=KL520` 回傳 `["v2.2.0", "v2.1.0", "kdp1"]`
- 升版 / 降版 API 接受 version 參數能正確 route
#### M9-12 — B2.2 降版 UI面向一般使用者2 人天)
**負責**Frontend Agent + Design Agent設計部分
**任務**
1. Design Agent 補 Settings > 韌體面板的 wireframe + 警告語 + 二次確認 UX不是 architect 範圍)
2. Frontend 實作 Settings 韌體面板:
- 顯示當前 FW 版本
- 「升級到最新」按鈕
- 「降版」按鈕 → 展開版本 dropdown + 警告語 + 二次確認 modal
3. 警告語內容(給 design 補強):
- 「降版可能導致現有 model 無法運作」
- 「降版過程不可中斷、否則裝置可能損壞」
- 「請確認版本相容性」
4. 二次確認 modal
- 需要使用者輸入「DOWNGRADE」字串防誤觸
- 顯示降版預估時間 + before/after 版本對照
5. i18n 新增 keys中英雙語
**驗收**
- 一般使用者打開 Settings 能看到韌體管理面板
- 誤觸降版按鈕後、二次確認 modal 出現、不容易誤過
- 降版完成後 UI 正確顯示新版本
#### M9-13 — B 階段完整三平台實機驗證1 人天)
**負責**Testing Agent
**任務**
- 三平台 × {KL520, KL720, KL630, KL730} × {scan, connect, inference, firmware upgrade, firmware downgrade} = 60 個 smoke test cells
- 重點異常路徑:降版中拔 device、降版 timeout、跨晶片 firmware mismatch、版本回滾
**驗收**
- 所有 cells PASS或標註 N/A 且解釋原因)
- 無 regression
### 工時合計
| Milestone | 工時 | 累積 |
|-----------|------|------|
| M9-6 | 1 | 1 |
| M9-7 | 0.5 | 1.5 |
| M9-8 | 1.5 | 3 |
| M9-9 | 2 | 5 |
| M9-10 | 1 | 6 |
| M9-11 | 1.5 | 7.5 |
| M9-12 | 2 | 9.5 |
| M9-13 | 1 | 10.5 |
**B 階段合計 ~10.5 人天**、加 A 階段 5 人天 = **完整版總計 ~15.5 人天**(比原預估 11-12 略多、主因是「面向一般使用者」這個範圍升級加上 SDK 驗證工時)
### Reviewer 切點
- M9-6 純研究、Architect 自身產出、不過 Reviewer
- M9-7 ~ M9-12 每個 milestone 結束過 Reviewerprogram code review + 設計合規)
- M9-13 testing report 過 Reviewer
### 平行性
- M9-6 必須先做(其他所有 milestone 都依賴研究結論)
- M9-7 ~ M9-10 序列(每個依賴前一個)
- M9-11 ~ M9-12 可平行(多版本後端 + 降版 UI 設計可同時做)
- M9-13 在所有 milestone 之後
---
## 11. 給 Orchestrator 的下一步建議
1. **A 階段 MVP 先做(不變)**——M9-1 ~ M9-5、5 人天
2. **A 階段驗證後(或同時平行)啟動 M9-6**——SDK 驗證、純研究、Architect 自己跑
3. **M9-6 結論回填**到 `41-tar-firmware-handling.md` + 更新本檔 §5.2 / §7.1 / R-FW-8
4. **B 階段是否要全做、依 M9-6 結果決定**——如果 SDK 不支援、可能要先升級 wheel、回歸成本變高、可重新 scope
5. **B2 降版 UX 由 Design Agent 主導**——本研究只標需求、不做 UI 設計
---
## 12. 與 A 階段研究的相依關係
| 項目 | A 階段(既定)| B 階段(本研究) |
|------|--------------|----------------|
| 範圍 | KL520 + KL720、自動升級 KDP1→KDP2 | + KL630 + KL730、+ 手動降版面向一般使用者、+ 多版本 |
| bridge.py handler | `handle_firmware_upgrade` | + `handle_firmware_downgrade` + `handle_firmware_list_versions` + chip 判斷擴展 + .tar 處理 |
| Go driver method | `UpgradeFirmware()` | + `DowngradeFirmware(version)` + `ListFirmwareVersions()` |
| API endpoint | `POST /firmware/upgrade` | + `POST /firmware/downgrade` + `GET /firmware/versions` |
| Frontend UI | Devices 頁 FW badge + 升級 modal | + Settings 韌體面板(含降版 + 多版本選擇) |
| 安裝包大小衝擊 | +0KB | +5MB詳細估算見 `42-manual-downgrade-for-end-users.md` |
| 工時 | 5 人天 | 10.5 人天 |
| Reviewer 切點 | M9-1 ~ M9-5 各一輪 | M9-7 ~ M9-12 各一輪 + M9-13 testing |
| 風險新增 | R-FW-1 ~ R-FW-7 | + R-FW-8 ~ R-FW-12 |

View File

@ -0,0 +1,458 @@
# .tar Firmware 處理研究KL630 / KL730
> 對應 research index §41
> 範圍KL630 / KL730 firmware 是 `.tar` 格式(不是 .bin、SDK 處理路徑、解壓策略、安裝包衝擊
> 撰寫日期2026-05-24
> 限制:純 plan、不出 code、所有「需 SDK 文件驗證」段落都明標註
> 路徑使用相對路徑(相對於 `/Users/jimchen/visionA/local-tool/`
---
## 0. TL;DR
1. KL520 / KL720 是 `fw_scpu.bin` + `fw_ncpu.bin` 兩個 raw binary、KL630 / KL730 是 `kp_firmware.tar` + `kp_loader.tar` 兩個 .tar 包
2. **.tar 裡面包什麼、SDK 怎麼吃,需要在 B 階段 M9-6 milestone 驗證**——我們現在無法從 warrenchen 程式碼推斷(他們沒實作)
3. 兩個候選策略:
- **策略 Xruntime 解壓**bridge.py 解 .tar 到 temp 目錄、餵解壓後的 .bin 給 SDK
- **策略 Ybuild time 解壓**build script 解壓進 `firmware/<chip>/extracted/`、ship 解壓後的 .bin
- **策略 Z直接餵 .tar 給 SDK**(如果 SDK 支援 `load_firmware_from_tar` 之類 API
4. **推薦策略 Y + Z 二選一、視 SDK 驗證結果**——不推薦策略 X每次 connect 解壓浪費時間 + temp file 管理麻煩)
5. 安裝包大小衝擊估算:策略 Y 約 +5MB、策略 Z 約 +5MB一樣、因為 .tar 跟 .bin 解壓後大小接近、壓縮率不高)
---
## 1. 為什麼 KL630 / KL730 是 .tar、不是 .bin
### 1.1 從 warrenchen bundle 看到的事實
```
local_service_win/firmware/
├── KL520/
│ ├── fw_scpu.bin ← raw binary
│ ├── fw_ncpu.bin ← raw binary
│ ├── fw_loader.bin ← raw binary
│ ├── dfw/minions.bin ← raw binaryDFUT 用)
│ └── VERSION ← "2.2.0"
├── KL720/
│ ├── fw_scpu.bin
│ ├── fw_ncpu.bin
│ └── VERSION ← "2.2.0"
├── KL630/
│ ├── kp_firmware.tar ← tar 打包
│ ├── kp_loader.tar ← tar 打包
│ └── VERSION ← "SDK-v2.5.7"(命名規則不同)
└── KL730/
├── kp_firmware.tar
├── kp_loader.tar
└── VERSION ← "SDK-v1.3.0"
```
### 1.2 推測原因(**待 SDK 文件驗證**
從命名規則、版號規則、SDK 版本看:
| 觀察 | 推論 |
|------|------|
| KL520/KL720 VERSION 是 firmware 版本2.2.0、KL630/KL730 是 SDK 版本SDK-v2.5.7| KL630/KL730 是新世代 SDKv2.x SDK release、firmware 與 SDK 綁定打包 |
| KL520/KL720 用 `fw_scpu.bin` / `fw_ncpu.bin` 命名、KL630/KL730 用 `kp_firmware.tar` 命名| KneronPLUS v2/v3 改用統一 packaging 格式、把 scpu + ncpu + metadata 整包到一個 .tar |
| KL520/KL720 有 `fw_loader.bin`、KL630/KL730 有 `kp_loader.tar`| Loader 也用同樣的打包策略、可能裡面除了 binary 還含 manifest |
| KL730 SDK-v1.3.0 比 KL630 SDK-v2.5.7 低、跟 chip 推出時間有關| KL730 可能還在較舊 SDK releasewarrenchen 取樣時的版本快照)|
### 1.3 .tar 內容可能性(**未驗證、僅猜測**
`.tar` 通常會包:
- `fw_scpu.bin`
- `fw_ncpu.bin`
- `manifest.json``metadata.txt`含版本、checksum、target chip 等)
- 可能含其他 firmware sub-moduleFPP_FW 等視 chip 而異)
**驗證方式**M9-6 必做):
```bash
mkdir -p /tmp/kl630_inspect
cd /tmp/kl630_inspect
tar -tvf /tmp/web_academy_prototype/local_service_win/firmware/KL630/kp_firmware.tar
tar -tvf /tmp/web_academy_prototype/local_service_win/firmware/KL630/kp_loader.tar
tar -tvf /tmp/web_academy_prototype/local_service_win/firmware/KL730/kp_firmware.tar
tar -tvf /tmp/web_academy_prototype/local_service_win/firmware/KL730/kp_loader.tar
# 紀錄每個 .tar 內含檔案清單、大小、結構
```
**本份研究無法跑這個 inspect**Architect Agent 不執行 shell 變動類指令、且這應該在實機驗證階段做——M9-6 milestone 必做、結果回填到本檔 §3。
---
## 2. KneronPLUS API 對 .tar 的處理(**全部待 SDK 文件驗證**
### 2.1 可能的 API 設計
從 KneronPLUS Python SDK 既有 API pattern 推測:
| API 候選 | 簽名 | 可能性 |
|---------|------|-------|
| `kp.core.load_firmware_from_file(dg, scpu_path, ncpu_path)` | 既有 API、接受兩個 .bin | KL630/KL730 是否能接受 .tar 內容路徑?需要先解壓?|
| `kp.core.load_firmware_from_tar(dg, tar_path)` | 假設新 API | **未確認存在**——可能 SDK v3.x 才有 |
| `kp.core.update_kdp2_firmware_from_tar(dg, tar_path, auto_reboot)` | 假設新 APIKDP2 寫 flash 用) | **未確認**、推測對應 KL630/KL730 升級 |
| `kp.core.update_kdp_firmware_from_files(dg, scpu, ncpu, auto_reboot)` | 既有 APIA 階段用) | 對 KL630/KL730 可能不適用KDP 是 KL520/KL720 老世代術語)|
### 2.2 從 warrenchen wheel 推斷
`local_service_win/KneronPLUS-3.1.2-py3-none-any.whl` 是 KneronPLUS 3.1.2、推測 API 命名應該已統一v3.x 主版本)。
**驗證方式**M9-6 必做):
```bash
cd /tmp
unzip /tmp/web_academy_prototype/local_service_win/KneronPLUS-3.1.2-py3-none-any.whl -d kneron_plus_inspect
# 找 kp/core.py 或對應的 wrapper
grep -rn "tar\|load_firmware\|update_kdp\|update_firmware" /tmp/kneron_plus_inspect/kp/
# 找所有 firmware-related 函數
grep -rn "^def " /tmp/kneron_plus_inspect/kp/core.py | grep -i "fw\|firmware"
```
**本份研究無法跑這個**——M9-6 必做。
### 2.3 從 visionA-local 既有 KneronPLUS wheel 推斷
需要知道我們既有 wheel 版本(**待查**
```bash
# 假設安裝在 server/scripts/venv/ 或類似位置
ls server/scripts/*.whl 2>/dev/null # 如果是直接 ship wheel
pip show kneronplus 2>/dev/null # 如果是 installed package
```
**重要**:如果我們既有 wheel 比 warrenchen 的 3.1.2 還舊(例如 2.x可能
- 既有 wheel 不認 `KP_DEVICE_KL630` / `KP_DEVICE_KL730` enum → driver 升級必須先升 wheel
- 既有 wheel 沒有 .tar 處理 API → 必須升 wheel 才能支援 KL630/KL730
→ M9-6 評估「是否升級 KneronPLUS wheel」是 B 階段風險最高的決策點。
---
## 3. 大小估算warrenchen 提供的 .tar 各多大)
### 3.1 .tar 檔案大小(從 warrenchen bundle 推斷)
```bash
# 在 M9-6 驗證階段執行(本份研究不執行 shell write 動作)
ls -lh /tmp/web_academy_prototype/local_service_win/firmware/KL630/
ls -lh /tmp/web_academy_prototype/local_service_win/firmware/KL730/
ls -lh /tmp/web_academy_prototype/local_service_win/firmware/KL520/
ls -lh /tmp/web_academy_prototype/local_service_win/firmware/KL520_kdp/
ls -lh /tmp/web_academy_prototype/local_service_win/firmware/KL720/
```
**預估值**(基於檔案類型常識):
| 路徑 | 預估大小 | 註 |
|------|---------|------|
| `KL520/fw_scpu.bin` | ~52KB | TDD L3219 記錄 |
| `KL520/fw_ncpu.bin` | ~40KB | TDD L3219 記錄 |
| `KL520/fw_loader.bin` | ~10KB | KDP1→KDP2 升級用 |
| `KL520/dfw/minions.bin` | ~50KB | DFUT 用、未來可能不 bundle |
| `KL520_kdp/fw_scpu.bin` | ~50KB | KDP1 降版用 |
| `KL520_kdp/fw_ncpu.bin` | ~40KB | KDP1 降版用 |
| `KL720/fw_scpu.bin` | ~150KB | KDP2、更複雜 |
| `KL720/fw_ncpu.bin` | ~100KB | |
| `KL630/kp_firmware.tar` | ~2-3MB | tar 含多檔、新世代 firmware 較大 |
| `KL630/kp_loader.tar` | ~500KB | loader |
| `KL730/kp_firmware.tar` | ~3-4MB | |
| `KL730/kp_loader.tar` | ~500KB | |
**B 階段所有 firmware 合計增量**
- KL520_kdp降版用~90KB
- KL630升降版用~2.5-3.5MB
- KL730升降版用~3.5-4.5MB
- 合計:**~6-8MB**
**multi-version bundleB2 階段)**
- 每 chip 額外 bundle 1 個舊版v2.1.0 之類):~+1-2MB
- 合計:**~7-10MB**
### 3.2 與 macOS dmg 既有 163MB 比
| 階段 | 累計新增 | 累計安裝包 | 衝擊 |
|------|---------|----------|------|
| 既有(含 KL520/KL720 firmware | 0 | 163MB | baseline |
| A 階段(+ KL520/fw_loader.bin | +10KB | ~163MB | <0.01% |
| B 階段(含 KL520_kdp + KL630 + KL730 firmware | +6-8MB | ~170-171MB | +4-5% |
| B2 階段(多版本,每 chip + 1 個舊版)| +7-10MB | ~170-173MB | +4-6% |
**使用者「+5MB 接受」的決策完全成立**——B 階段全做完約 +7-10MB、不超過 +6%。
### 3.3 解壓後大小(估算)
.tar 通常不會做壓縮tar 預設無 compression、除非 .tar.gz、解壓後大小 ≈ 原 .tar 大小:
| 檔案 | 原始 .tar | 解壓後 |
|------|----------|--------|
| `KL630/kp_firmware.tar` (~3MB) | 3MB | ~3MB內含多個 .bin、metadata、加總接近 .tar 大小) |
| `KL730/kp_firmware.tar` (~4MB) | 4MB | ~4MB |
→ 採策略 Ybuild time 解壓vs 策略 Zship .tar大小差異 **< 100KB**、可忽略
---
## 4. 解壓策略對比
### 4.1 策略 Xruntime 解壓
**做法**
- bridge.py 每次 connect KL630/KL730 時、解壓 `.tar``tempfile.mkdtemp()` 暫存目錄
- 餵解壓後的 .bin 路徑給 `kp.core.load_firmware_from_file()`
- 連線結束清理 temp 目錄
**優點**
- 安裝包不增加解壓後檔案、只 ship 一份 .tar
- 解壓邏輯集中在 bridge.py、單一資料夾管理
**缺點**
- 每次 connect 多花 50-200ms 解壓時間
- temp 目錄管理clean-up、permissions、tempfile 衝突
- macOS hardened runtime 對 temp 目錄寫入權限可能有限制
- Windows 上 `tempfile` 預設位置 `%TEMP%` 可能被防毒軟體掃描,慢
- KL630/KL730 如果類似 KL520每次 connect 都要 load firmware解壓會發生在 hot path
**不推薦**——成本 vs 收益不划算。
### 4.2 策略 Ybuild time 解壓
**做法**
- `installer/` build script 在 wails build 之前、先解壓 `.tar``server/scripts/firmware/<chip>/extracted/`
- ship 解壓後的 .bin不 ship 原始 .tar
- bridge.py 直接餵解壓後路徑、`_resolve_firmware_paths(chip)` 解析 `extracted/` 子目錄
**優點**
- runtime 零成本、connect 速度不受影響
- bridge.py 邏輯與 KL520/KL720 一致(都餵 .bin path
- 安裝包大小不變(.tar 跟解壓後 .bin 大小接近)
**缺點**
- 需要修改 installer build script單純做
- 開發環境第一次 clone repo 後、必須跑解壓 script 一次(或加進 `make setup` / npm postinstall
- 解壓後檔案進不進 git建議**不進**(避免 binary diff、靠 build script 即時產生)
**推薦條件**:如果 SDK 不支援 .tar 直接餵入、選此策略。
### 4.3 策略 Z直接餵 .tar 給 SDK
**做法**
- SDK 提供 `kp.core.load_firmware_from_tar()` 或類似 API、bridge.py 直接傳 .tar 路徑
**優點**
- 最簡(不需解壓邏輯)
- runtime 零成本
- 安裝包 ship 原始 .tar、與 warrenchen 一致
**缺點**
- **依賴 SDK 支援**——M9-6 必須驗證 API 存在
- 如果 SDK 接受的是「.tar 內 fw_scpu.bin 解出來的路徑」、那就退回策略 Y
- bridge.py 內 KL520/KL720 vs KL630/KL730 firmware 路徑解析邏輯不一致(一邊兩個 .bin、一邊一個 .tar`_resolve_firmware_paths` 必須回 union type 或分支處理
**推薦條件**:如果 SDK 確認支援、選此策略(最乾淨)。
### 4.4 決策樹
```
M9-6 SDK 驗證結果
├── SDK 支援 load_firmware_from_tar() / update_*_from_tar()
│ └── 選策略 Z
│ └── 改動_resolve_firmware_paths 回 (tar_path, None) 或 union
└── SDK 不支援、必須先解壓
└── 選策略 Y
└── 改動:
- installer build script 加 tar -xf 步驟
- .gitignore 加 firmware/<chip>/extracted/
- 開發環境 setup 加解壓 step
- _resolve_firmware_paths 找 extracted/fw_scpu.bin
```
**絕對不要選策略 X**runtime 解壓)。
---
## 5. SDK API 可用性驗證計畫M9-6 必做)
### 5.1 驗證項目清單
| # | 驗證項目 | 方法 | 影響 |
|---|---------|------|-----|
| 1 | 既有 KneronPLUS wheel 版本 | `pip show kneronplus` / 找 wheel 檔案 | 決定要不要升級 wheel |
| 2 | wheel 內 `kp.ProductId.KP_DEVICE_KL630` / `KL730` enum 存在 | `python -c "import kp; print(kp.ProductId.KP_DEVICE_KL630)"` | 不存在 → 升 wheel |
| 3 | `kp.core.load_firmware_from_file` 對 .tar 路徑的行為 | 實機跑、傳 `.tar` 看回傳 | 決定策略 Y or Z |
| 4 | `kp.core.update_kdp_firmware_from_files` 對 KL630/KL730 是否適用 | 實機跑(如果有 KL630/KL730| FW 升降版實作策略 |
| 5 | 是否有 `load_firmware_from_tar()` / `update_*_from_tar()` 新 API | grep wheel source / 查 SDK 文件 | 策略 Z 是否可行 |
| 6 | KL630/KL730 firmware 字串可能值 | 實機 `kp.core.scan_devices()` 看 firmware 欄位 | FW badge 顯示邏輯 |
| 7 | KL630/KL730 是否每次 connect 都要 load firmware | 實機 connect 兩次、看第二次需不需要 load | connect 流程設計 |
| 8 | `kp.inference.generic_image_inference_send/receive` 對 KL630/KL730 NEF 是否能直接 work | 實機跑 sample inference | inference 流程是否要分支 |
| 9 | .tar 內容(解壓看裡面有什麼)| `tar -tvf kp_firmware.tar` | bridge.py 解析策略 |
| 10 | KneronPLUS wheel 升級對 KL520/KL720 的 regression 風險 | 升級後跑 既有 E2E | 是否值得升 wheel |
### 5.2 驗證所需資源
- 使用者手上要有:
- 至少 1 個 KL630 dongle理想
- 至少 1 個 KL730 dongle理想
- 如果沒有、只能查 SDK 文件 + wheel source、無法做動態驗證、風險變高
- KneronPLUS SDK 官方文件PDF / online docs
- Python REPL + 既有 venv
### 5.3 驗證結果回填位置
- 本檔 §1.3、§2、§3.3 的「未驗證」段落填實際結果
- 40-b-phase 檔 §3.2、§5.2、§7.1 對應段落
- 30-integration-plan 檔 §6 階段 B 評估提示更新
---
## 6. .tar 解壓技術細節(如選策略 Y
### 6.1 解壓工具選擇
**跨平台選 Python `tarfile` 模組**(標準庫、無外部依賴):
```python
# 偽碼、給 installer build script 用
import tarfile
import os
import shutil
def extract_firmware_tars(firmware_dir):
"""Walk firmware/<chip>/ dirs、解壓所有 .tar 到 extracted/ 子目錄"""
for chip_dir in os.listdir(firmware_dir):
chip_path = os.path.join(firmware_dir, chip_dir)
if not os.path.isdir(chip_path):
continue
for fname in os.listdir(chip_path):
if not fname.endswith(".tar"):
continue
tar_path = os.path.join(chip_path, fname)
extract_to = os.path.join(chip_path, "extracted")
os.makedirs(extract_to, exist_ok=True)
with tarfile.open(tar_path, "r") as tar:
tar.extractall(extract_to)
```
**不選 shell `tar -xf`**——Windows 上 `tar` 命令不一定存在Win10+ 才有、Python 標準庫穩定。
### 6.2 何時跑
- **CI/CD build pipeline**:每次 build 安裝包前跑(保證 ship 出去的安裝包有解壓檔)
- **開發環境 setup**`scripts/setup-dev.sh` / `make setup` 加一步、或 npm postinstall
- **首次 clone 後**README 提醒「跑 `python3 scripts/extract-firmware.py`
### 6.3 .gitignore 規則
```
# .gitignore 追加B 階段 M9-8 milestone
server/scripts/firmware/*/extracted/
```
`.tar` 進 git、解壓後 `.bin` 不進 git避免雙份 binary 進版控、減少 repo 大小)
### 6.4 衝突處理
如果使用者手動修改了 `extracted/` 內檔案、下次 build 會覆蓋。可接受、因為這個目錄不該手動改。
---
## 7. 如果選策略 Z直接餵 .tar的細節
### 7.1 bridge.py 簽名變化
```python
# _resolve_firmware_paths 偽碼變化
def _resolve_firmware_paths(chip="KL520"):
base = os.path.dirname(os.path.abspath(__file__))
fw_dir = os.path.join(base, "firmware", chip)
if chip in ("KL520", "KL720"):
# 既有路徑 .bin
scpu = os.path.join(fw_dir, "fw_scpu.bin")
ncpu = os.path.join(fw_dir, "fw_ncpu.bin")
if os.path.exists(scpu) and os.path.exists(ncpu):
return {"format": "bin", "scpu": scpu, "ncpu": ncpu, "loader": ...}
return None
if chip in ("KL630", "KL730"):
fw_tar = os.path.join(fw_dir, "kp_firmware.tar")
loader_tar = os.path.join(fw_dir, "kp_loader.tar")
if os.path.exists(fw_tar):
return {"format": "tar", "firmware": fw_tar, "loader": loader_tar}
return None
return None
```
→ 回 dict / union type 而不是 tuple、給 caller 判斷 format。
### 7.2 firmware load 分支
```python
# handle_connect / handle_firmware_upgrade 內偽碼
fw_paths = _resolve_firmware_paths(chip)
if fw_paths is None:
return {"error": f"firmware not found for {chip}"}
if fw_paths["format"] == "bin":
kp.core.load_firmware_from_file(dg, fw_paths["scpu"], fw_paths["ncpu"])
elif fw_paths["format"] == "tar":
# 假設 SDK 提供這個 API、待驗證
kp.core.load_firmware_from_tar(dg, fw_paths["firmware"])
```
---
## 8. 風險(針對 .tar 處理)
### 8.1 R-TAR-1SDK 不接受 .tar 直接餵中度B 階段 M9-6 驗)
**情境**:策略 Z 不可行、被迫退回策略 Y。
**緩解**M9-6 預先驗證、選對策略才做 M9-8。
### 8.2 R-TAR-2build time 解壓步驟漏跑(中度)
**情境**CI/CD 流程沒加解壓 step、ship 出去的安裝包沒有解壓 .bin、KL630/KL730 連不上。
**緩解**
- installer build script 加 mandatory `extract-firmware.py` step
- 加 build-time check`firmware/KL630/extracted/fw_scpu.bin` 不存在 → build fail」
- 安裝包 smoke test解壓安裝包、grep `extracted/fw_scpu.bin` 必須存在
### 8.3 R-TAR-3解壓對 macOS notarization 影響(低度)
**情境**macOS notarization 對 dmg 內 binary file 要求 codesign、解壓出來的 .bin 是否需要簽?
**研究**
- firmware .bin 不是 executable是 NPU instruction binary、預估不需 codesign
- 但 Apple Gatekeeper / XProtect 可能誤判某些 binary pattern
- M9-13 milestone 三平台驗證時必須跑 notarized dmg、確認沒被砍
### 8.4 R-TAR-4.tar 解壓路徑跨平台問題(低度)
**情境**Python `tarfile.extractall()` 在 Windows 上對絕對路徑 / `..` path entry 有警告Python 3.12+ 預設拒絕)。
**緩解**
- 解壓前用 `tarfile.data_filter` 過濾Python 3.12+ 內建)
- 或預先驗證 .tar 內容、refuse 含 `..` / 絕對路徑的 .tar
---
## 9. 給 Orchestrator 的決策點
1. **A 階段 MVP 不涉 .tar**——不影響本檔
2. **B 階段啟動前 M9-6 必跑**——SDK 驗證、決定策略 Y/Z
3. **策略選定後本檔 §4.4 「決策樹」結果回填**到 progress.md「重要決策紀錄」
4. **如果使用者沒有 KL630/KL730 dongle**——降級驗證強度:只查 SDK 文件 + wheel source、不做動態驗證、風險 R-FW-8 提升
---
## 10. 與其他研究檔的關係
| 連結 | 引用內容 |
|------|---------|
| `40-b-phase-kl630-kl730-extension.md` §6.3 | `_resolve_firmware_paths` 擴展偽碼(本檔詳化)|
| `40-b-phase-kl630-kl730-extension.md` R-FW-9 | .tar 解壓對安裝包大小衝擊(本檔 §3 詳化)|
| `40-b-phase-kl630-kl730-extension.md` R-FW-10 | KL630/KL730 是否有 Loader mode本檔 §5.1 驗證 #6/#7 確認)|
| `42-manual-downgrade-for-end-users.md` | 多版本 .tar 儲存結構(本檔 §6.3 .gitignore 規則延伸)|

View File

@ -0,0 +1,551 @@
# 手動降版面向一般使用者:流程設計與 Design 需求清單
> 對應 research index §42
> 範圍B2 階段——把 FW 降版功能暴露給一般使用者(不只 dev mode / 內部測試)、所需 driver / bridge / API endpoint 細節 + 給 Design Agent 的 UX 補強需求
> 撰寫日期2026-05-24
> 限制:架構面、不出 code、UX 細節留給 Design Agent
> 路徑使用相對路徑(相對於 `/Users/jimchen/visionA/local-tool/`
---
## 0. TL;DR
1. **使用者決策2026-05-24翻案前一份研究 §3 的「降版僅內部測試」假設**——降版面向一般使用者
2. 一般使用者面向後、責任分工:
- **Architect本檔**:標記技術需求、定義 safety guards、API 簽名、儲存結構、driver 與 bridge.py 介面
- **Design Agent另派**:警告語、二次確認流程、版本 dropdown 視覺、降版進行中互動、失敗復原 UX
- **Frontend Agent**:實作 UI、串 API、i18n
- **Backend Agent**:實作 driver method、bridge.py handler、API endpoint
3. 「面向一般使用者」≠ 「無腦讓使用者隨便降」——必須有**多層 safety net**
- UI 警告 + 二次確認design 領域、本檔只標需求)
- Driver 層 guard拒絕跨晶片誤匹配、拒絕「降版」到比 current 還新的版本)
- bridge.py 層 guardchecksum 驗證 firmware 完整性、version 字串嚴格 match
4. 暴露範圍Settings → 「韌體管理」面板(不在 Devices 頁主流程、避免誤觸)
---
## 1. 使用情境分析
### 1.1 一般使用者為什麼會降版
| 情境 | 頻率 | 描述 | 處理建議 |
|------|------|------|---------|
| 「我的舊 model 在新 FW 上跑不出結果」 | 中 | 內建升版到 v2.2 後、舊 NEF modelv2.1 編的)相容性問題 | 提供降版回 v2.1 選項 |
| 「跟某個 third-party tool 不相容」 | 低 | 例如某些 model debugging tool 只認 KDP1 | 提供降版回 KDP1 選項KL520 |
| 「我升錯了、想回到原本狀態」 | 低 | 使用者誤觸升級、想 revert | 提供「降版回上次的版本」選項 |
| 「測試環境需要特定版本」 | 中 | 開發者場景(但你說也要 face 一般使用者)| 提供降版到任意 bundled 版本 |
| 「就是想試試看」 | 中 | 好奇心驅動 | 用警告語勸阻、但不阻止 |
→ 「跟一般使用者解釋降版」必須包裝成「**FW 版本切換**」、不要用「**降版**」這個帶負面含義詞——但內部技術文件仍叫 downgrade程式碼/log/Architect 文件用語)。
### 1.2 UI 觸發位置(建議給 Design Agent 評估)
| 位置 | 優點 | 缺點 | 建議 |
|------|------|------|------|
| Devices 頁主卡片 | 醒目、與 FW badge 同位置 | 太容易誤觸、新手使用者不需要 | **不建議** |
| Devices 頁 → Device Detail Modal | 進階入口、需要點開 | 仍偏 device-centric | **次佳** |
| Settings → 韌體管理 | 進階入口、與其他 settings 同位置 | 較深入路徑 | **推薦** |
| Settings → 進階advanced | 最深入口 | 但使用者面向後不應藏太深 | 折衷 |
**架構建議****Settings → 韌體管理** 是主入口、Devices 頁 device card 顯示 FW badge 但不放降版按鈕。
---
## 2. Driver / Bridge / API 詳細設計
### 2.1 driver interface 擴展
承前一份 30-integration-plan §1 M9-2 已定義的 `UpgradeFirmware()`、本檔加:
```go
// 偽碼、不出 code給 architect 補 TDD 用)
type DeviceDriver interface {
// ... 既有 methods 與 UpgradeFirmware() ...
// B2 階段新增
DowngradeFirmware(version string, progressCh chan<- FirmwareProgress) error
ListFirmwareVersions() ([]FirmwareVersion, error)
GetCurrentFirmwareVersion() (FirmwareVersion, error)
}
type FirmwareVersion struct {
Version string `json:"version"` // "v2.2.0" / "v2.1.0" / "kdp1"
DisplayName string `json:"displayName"` // "v2.2.0 (current)" / "v2.1.0 (older)"
IsCurrent bool `json:"isCurrent"`
IsBundled bool `json:"isBundled"` // 是否在安裝包內、總是 true不做線上更新
ReleaseDate string `json:"releaseDate,omitempty"` // ISO 8601、optional
Notes string `json:"notes,omitempty"` // 給使用者看的說明
}
type FirmwareProgress struct {
// ... 既有欄位 ...
Direction string `json:"direction"` // "upgrade" / "downgrade"
}
```
### 2.2 bridge.py handler 設計
#### 2.2.1 `handle_firmware_list_versions`
```python
# 偽碼
def handle_firmware_list_versions(params):
"""List bundled firmware versions for a chip.
Params:
chip: str ("KL520" | "KL720" | "KL630" | "KL730", required)
Returns:
{"versions": [{"version":"v2.2.0", "isCurrent":true, "isBundled":true, ...}, ...]}
or {"error": "..."}
"""
chip = params.get("chip", "").upper()
if chip not in ("KL520", "KL720", "KL630", "KL730"):
return {"error": f"unknown chip: {chip}"}
base = os.path.dirname(os.path.abspath(__file__))
fw_dir = os.path.join(base, "firmware", chip)
if not os.path.isdir(fw_dir):
return {"error": f"firmware dir not found for {chip}"}
# 讀 current/VERSION
current_version = _read_version_file(os.path.join(fw_dir, "current", "VERSION"))
versions = []
for entry in os.listdir(fw_dir):
entry_path = os.path.join(fw_dir, entry)
if not os.path.isdir(entry_path):
continue
if entry == "current":
continue # current 是 alias、不重複列
version_file = os.path.join(entry_path, "VERSION")
if not os.path.exists(version_file):
continue
v = _read_version_file(version_file)
versions.append({
"version": v,
"displayName": _format_display_name(v, current_version),
"isCurrent": v == current_version,
"isBundled": True,
"directory": entry, # 給 bridge.py 內部用、不外露給 API
})
return {"versions": versions, "current": current_version}
```
#### 2.2.2 `handle_firmware_downgrade`
```python
# 偽碼
def handle_firmware_downgrade(params):
"""Downgrade firmware to a specific bundled version.
Params:
port: int (USB port id, required)
chip: str ("KL520" | "KL720" | "KL630" | "KL730", required)
version: str (target version, e.g. "v2.1.0" / "kdp1", required)
Returns:
{"status":"downgraded", "before_version":"v2.2.0", "after_version":"v2.1.0",
"duration_ms":31000, "stage":"done"}
or {"error":"...", "stage":"validate|connect|download|verify"}
"""
chip = params.get("chip", "")
target_version = params.get("version", "")
target_port = params.get("port", "")
# Stage 1: validate
# - chip 必須是支援的
# - version 必須在 bundled list 內
# - target_version 不能等於 current那是 no-op
# - target_version 不能比 current 還新(那是 upgrade、走另一支
if not _validate_downgrade_request(chip, target_version):
return {"error": "invalid downgrade request", "stage": "validate"}
# Stage 2: 解析 firmware paths
fw_paths = _resolve_firmware_paths_versioned(chip, target_version)
if fw_paths is None:
return {"error": f"firmware not found: {chip}/{target_version}", "stage": "validate"}
# Stage 3: connect
# 跟 firmware_upgrade 一樣的 connect_with_magic 流程
# ...
# Stage 4: 跑 SDK API
# 視 chip:
# - KL520 KDP2 → KDP1: kp.core.update_kdp_firmware_from_files(loader, ..., auto_reboot=True) + load_firmware
# - KL520 v2.2 → v2.1: 同上但用不同 .bin
# - KL720: 同上KL720 是 flash-based、要寫 flash
# - KL630/KL730: 用 update_*_from_tar APIM9-6 驗證後填)
# ...
# Stage 5: verify
# disconnect → sleep 3s → rescan → 看 firmware 字串 / kn_number 是否符合預期
# ...
return {
"status": "downgraded",
"before_version": before_version,
"after_version": after_version,
"duration_ms": duration,
"stage": "done",
}
```
### 2.3 API endpoint 設計
| Endpoint | Method | Request Body | Response |
|----------|--------|-------------|----------|
| `GET /api/devices/:id/firmware/versions` | GET | — | `{success:true, data:{versions:[...], current:"v2.2.0"}}` |
| `POST /api/devices/:id/firmware/downgrade` | POST | `{version:"v2.1.0", confirmToken:"DOWNGRADE"}` | `202 {success:true, data:{taskId:"..."}}` |
| WebSocket room `firmware:<deviceId>` | — | — | progress events同 upgrade 流) |
**`confirmToken` 設計**
- API 層要求 body 含 `confirmToken: "DOWNGRADE"`(字面字串)
- 沒帶 / 帶錯 → API 直接 400
- **目的**:防止 CSRF、防止前端 bug 誤觸發、強制 UI 二次確認流程
- Frontend 必須先讓使用者輸入字面字串 / 點兩次按鈕、才把 token 加進 request
### 2.4 driver 層 safety guards
`KneronDriver.DowngradeFirmware(version)` 內必做:
1. **不能跨晶片**:呼叫前驗 `version``ListFirmwareVersions()` 結果內、不接受任意字串
2. **不能升版偽裝**:比較 `version``GetCurrentFirmwareVersion()` 結果、若目標版本 >= current → 拒絕(要走升版 API
3. **不能 no-op**:若 `version == current` → 拒絕(節省時間 + 避免 device 不必要 reset
4. **status guard**device 必須是 `StatusDetected``StatusConnected`、不能在 `StatusInferencing` / `StatusFlashing` / `StatusUpgrading` 期間降版(會卡 mutex
```go
// 偽碼
func (d *KneronDriver) DowngradeFirmware(version string, progressCh chan<- driver.FirmwareProgress) error {
d.mu.Lock()
if d.info.Status == driver.StatusInferencing || d.info.Status == driver.StatusFlashing {
d.mu.Unlock()
return fmt.Errorf("device busy: %s", d.info.Status)
}
chip := d.chipType
current := d.info.FirmwareVer
d.mu.Unlock()
// Validate version exists
versions, err := d.ListFirmwareVersions()
if err != nil {
return err
}
var targetVer *driver.FirmwareVersion
for _, v := range versions {
if v.Version == version {
targetVer = &v
break
}
}
if targetVer == nil {
return fmt.Errorf("version %s not found in bundled firmware for %s", version, chip)
}
// Validate it's actually a downgrade順序比較邏輯 chip-specific
if !isOlderVersion(version, current, chip) {
return fmt.Errorf("target version %s is not older than current %s", version, current)
}
// 進實際降版流程
// ... 跟 UpgradeFirmware 類似、call bridge.py firmware_downgrade ...
}
```
---
## 3. 多版本 firmware 儲存結構B2 細化)
### 3.1 完整目錄結構
```
server/scripts/firmware/
├── KL520/
│ ├── current/ ← 預設 firmwareA 階段位置 = MVP 既有)
│ │ ├── fw_scpu.bin ← KDP2 v2.2.0
│ │ ├── fw_ncpu.bin
│ │ ├── fw_loader.bin ← KDP1→KDP2 升級用A 階段補進來)
│ │ └── VERSION ← "v2.2.0"
│ ├── v2.2.0/ ← 跟 current 同內容、用於版本切換 reference
│ │ ├── fw_scpu.bin
│ │ ├── fw_ncpu.bin
│ │ ├── fw_loader.bin
│ │ └── VERSION
│ ├── v2.1.0/ ← 舊版本(如果決定 bundle
│ │ ├── fw_scpu.bin
│ │ ├── fw_ncpu.bin
│ │ ├── fw_loader.bin
│ │ └── VERSION
│ └── kdp1/ ← KDP1 降版
│ ├── fw_scpu.bin
│ ├── fw_ncpu.bin
│ └── VERSION ← "KDP1"(特殊標記、不是 semver
├── KL720/
│ ├── current/ ← v2.2.0
│ ├── v2.2.0/
│ └── v2.1.0/ ← 視是否 bundle
├── KL630/
│ ├── current/ ← SDK-v2.5.7
│ │ ├── kp_firmware.tar
│ │ ├── kp_loader.tar
│ │ ├── extracted/ ← build time 解壓(如選策略 Y
│ │ │ ├── fw_scpu.bin
│ │ │ └── fw_ncpu.bin
│ │ └── VERSION ← "SDK-v2.5.7"
│ └── SDK-v2.4.0/ ← 視是否 bundle 舊 SDK
│ └── ...
└── KL730/
├── current/ ← SDK-v1.3.0
│ ├── kp_firmware.tar
│ ├── kp_loader.tar
│ ├── extracted/
│ └── VERSION
└── ...
```
### 3.2 `current/` 是什麼
**選項 Asymbolic link** → `current/` 是 symlink 指向 `v2.2.0/`
- 優點:節省空間(不重複 binary
- 缺點Windows symbolic link 需要 admin 權限、`tar`/zip 壓縮對 symlink 處理各 OS 不同
- **不推薦**(跨平台 symlink 太脆弱)
**選項 B實體副本** → `current/``v2.2.0/` 的 file copy
- 優點:跨平台、簡單
- 缺點:每個 chip 多佔 1 份KL520 ~100KB、KL720 ~250KB、KL630/KL730 ~3-4MB、合計 ~7-8MB 額外
- **推薦**
**選項 C取消 `current/`、用 metadata 記錄當前版本**
- 結構簡化:`firmware/<chip>/{v2.2.0,v2.1.0,kdp1}/` + `firmware/<chip>/CURRENT_VERSION`(單行檔 = "v2.2.0"
- 優點節省空間、structure 清晰
- 缺點bridge.py 多一次 file read 才知道用哪個版本trivial 成本)
- **可選**(架構乾淨度 vs 簡單度的 trade-off
**建議選 C**——架構最乾淨、空間最省、跨平台無 symlink 風險。
### 3.3 採選項 C 的具體結構
```
server/scripts/firmware/
├── KL520/
│ ├── CURRENT_VERSION ← 單行檔:"v2.2.0"
│ ├── v2.2.0/
│ │ ├── fw_scpu.bin
│ │ ├── fw_ncpu.bin
│ │ ├── fw_loader.bin
│ │ └── VERSION ← "v2.2.0"
│ ├── v2.1.0/
│ │ └── ...
│ └── kdp1/
│ └── ...
├── KL720/
│ └── ...同結構...
├── KL630/
│ └── ...同結構(.tar / extracted...
└── KL730/
└── ...
```
`_resolve_firmware_paths_versioned(chip, version=None)`
- `version=None` → 讀 `CURRENT_VERSION` 拿當前版本 → 找 `<chip>/<version>/`
- `version="v2.1.0"` → 直接找 `<chip>/v2.1.0/`
**A 階段MVP相容性**A 階段 `_resolve_firmware_paths(chip)``<chip>/fw_scpu.bin`、B2 階段重整目錄時、要把 A 階段檔案搬進 `<chip>/v2.2.0/` 並加 `CURRENT_VERSION`。**A 階段的 caller 必須升級成 `_resolve_firmware_paths_versioned`**B2 migration step
---
## 4. Bundle 多版本對安裝包大小的精確估算
### 4.1 預估 bundle 策略
| Chip | current | 額外 bundled | 合計(每 chip|
|------|---------|------------|--------------|
| KL520 | v2.2.0 (~100KB) | v2.1.0 (~100KB) + kdp1 (~90KB) | ~290KB |
| KL720 | v2.2.0 (~250KB) | v2.1.0 (~250KB)(如果 bundle | ~500KB |
| KL630 | SDK-v2.5.7 (~3MB) | SDK-v2.4.0 (~3MB)(如果 bundle+ extracted (~3MB) | ~9MB |
| KL730 | SDK-v1.3.0 (~4MB) | + extracted (~4MB) | ~8MB |
**合計 B2 階段所有 bundled firmware**~18MB多版本全部 bundle
### 4.2 與既有 dmg 163MB 比
| 階段 | 累計 | 衝擊 |
|------|------|------|
| 既有 | 163MB | baseline |
| A 階段MVP| 163MB + 10KB | <0.01% |
| B 階段(單版本 KL630/KL730 + 降版用 KL520_kdp| 163MB + 6-8MB ≈ 170MB | +4-5% |
| B2 階段(每 chip 多 bundle 1 舊版) | 163MB + 14-18MB ≈ 178-181MB | +9-11% |
**使用者「+5MB 接受」對 B 階段成立、但 B2 多版本超出**
### 4.3 取捨建議
| 策略 | 大小 | 取捨 |
|------|------|------|
| **保守**:每 chip 只 bundle current + 1 個降版選項KL520 v2.1.0、其他不 bundle 舊版) | +7MB | 滿足使用者「+5MB 接受」邊緣、降版選擇變少 |
| **完整**:每 chip current + 2 個舊版 | +18MB | 超出 +5MB 估算、但選擇豐富 |
| **極簡**:只 bundle current + KDP1 降版KL520 only | +5MB | 嚴格 +5MB、其他 chip 沒降版選擇 |
**建議選保守策略**——KL520 提供「kdp1」「v2.1.0」兩個降版、KL720/KL630/KL730 提供 current + 1 個舊版(如有可用)。
**重要前提**:因 SDK release 不一定提供舊版 firmware、KL630/KL730 「舊版」可能取不到M9-6 待驗證)。
---
## 5. Design Agent 需求清單(給 Orchestrator 派 Design 用)
> **注意**:以下是 architect 標記的「**功能性需求**」、Design Agent 負責設計具體 wireframe / 視覺 / 文案。Architect 不畫設計稿、不寫文案。
### 5.1 Settings → 韌體管理面板(新頁面)
**功能需求**
- F1. 列出所有偵測到的 dongle、每張 dongle 一張卡片
- F2. 卡片顯示dongle 名稱("KL520 #1"、kn_number、當前 FW 版本、bundled current 版本(讓使用者看出是否最新)
- F3. 卡片內 actions
- 「升級到最新」按鈕(如果 current 與 bundled 不同)
- 「切換 FW 版本」按鈕(永遠顯示、暴露多版本選擇)
- F4. 「切換 FW 版本」展開後:版本 dropdown + 詳細說明(每個版本一段話)+ 警告語 + 「降版」按鈕
- F5. 二次確認 modal見 §5.3
- F6. 降版進行中 progress UIprogress bar + 階段提示 + 不可中斷警告)
- F7. 降版完成後 toast 通知 + 自動 rescan + 卡片更新到新版本
### 5.2 Devices 頁面 FW badge補強既有 A 階段)
**功能需求**
- 既有:紅/黃/綠 badge 顯示 FW 健康度
- 新增badge 旁加「⚙」icon、點擊 deep-link 到 Settings → 韌體管理對應卡片
- 不在 Devices 頁放降版按鈕(避免誤觸)
### 5.3 二次確認 modal最關鍵的 design 細節)
**功能需求****design 要全部達成**
- D1. 警告語必須包含:
- 「降版可能導致現有 model 無法運作」
- 「降版過程不可中斷、否則裝置可能損壞」
- 「降版完成後可能需要重新插拔裝置」
- 「請確認版本相容性、降版至 KDP1 的舊版會限制可用功能」
- D2. **使用者必須輸入字面字串「DOWNGRADE」確認**(防誤觸)
- 不接受其他大小寫
- 輸入欄為空 / 不對時、確認按鈕 disabled
- D3. 顯示 before/after 版本比對表(當前 vs 目標)
- D4. 顯示預估時間KL520 ~30s、KL720 ~180s、KL630/KL730 估計 ~60s
- D5. modal 不可被點外部關閉(必須點「取消」明確關閉)
- D6. 「確認降版」按鈕視覺要 destructive紅色 / 警告色)
- D7. 確認後 modal 不關閉、直接切到「進行中」狀態(避免使用者誤以為什麼都沒發生)
### 5.4 「進行中」UI
**功能需求**
- E1. progress bar雖然底層是 stage-based、給使用者看百分比更直觀
- E2. 階段文字(連線中 / 載入引導程式 / 寫入韌體 / 驗證 / 完成)
- E3. **不顯示「取消」按鈕**(降版不可中斷、有按鈕會誘惑使用者點)
- E4. 顯示「請勿拔除裝置」persistent banner不可關閉
- E5. 如果失敗、顯示 friendly error 訊息 + 「重新插拔裝置後重試」指引
### 5.5 失敗復原 UX
**功能需求**
- R1. 區分失敗類型:
- 「升級時 device disconnect」→ 訊息:「裝置已斷開、請重新插入後重試」
- 「Firmware 損毀 / 寫入失敗」→ 訊息:「韌體寫入失敗、請聯絡客服」(罕見、可能 brick
- 「Timeout」→ 訊息:「升級時間過長、可能成功也可能未完成、請拔插裝置再 scan」
- R2. 失敗後不自動關閉 modal、給使用者讀完訊息再關
- R3. 提供「重試」按鈕(如果是可重試的 error
- R4. 提供「複製錯誤訊息」按鈕(給技術支援用)
### 5.6 i18n keys給 frontend 估算)
預估 新增中英雙語 keys
- Settings 韌體管理頁標題、說明:~3 個
- 卡片內各欄位、按鈕:~8 個
- 二次確認 modal~12 個
- 進行中 UI~8 個
- 失敗訊息:~10 個
- toast / banner~5 個
- **合計 ~46 個新 i18n keys**
---
## 6. 一般使用者誤觸降版 brick 風險評估
### 6.1 風險矩陣
| 誤操作 | 機率 | 嚴重性 | 已緩解 | 殘餘風險 |
|--------|------|-------|--------|---------|
| 點錯按鈕(沒看清楚就降版)| 中 | 中 | 二次確認 + 輸入「DOWNGRADE」字串 | 低 |
| 降版到不相容版本KL520 KDP2 → KDP1且使用者依賴 KDP2-only 功能)| 中 | 中 | 警告語明示 KDP1 限制 | 中(使用者沒讀完警告)|
| 降版中拔 USB | 低 | 高(可能 brick| persistent banner + 不可關 modal | 低 |
| 降版中關 app | 低 | 中 | server graceful shutdown 機制(既有)| 低 |
| 升級當降版(誤把新版降到舊版、反向)| 低 | 低 | driver guard不允許目標版本 >= current | 已消除 |
| 跨晶片誤匹配 | 極低 | 高 | driver guardversion 必須在 `ListFirmwareVersions(chip)` 內 | 已消除 |
### 6.2 殘餘風險最大的:使用者不讀警告就降版
**對策**
- 二次確認字串「DOWNGRADE」不是 「OK」/「Yes」、強制使用者打字
- 視覺破壞性 button color紅色
- 「進行中」期間 banner persist不可關
但**無法完全消除**——一般使用者場景下、總有人會無視警告。
### 6.3 Plan B給技術支援用的救磚 SOP
如果使用者真的把 dongle 弄 brick、需要
- DFUT.exeWindows-only Qt 工具、可從 Kneron 拿)
- 燒回 KDP2 standard firmware
**不打包 DFUT.exe 進 visionA-local**(因為跨平台限制 + 30MB 額外大小 + 安全性考量)、但**提供 SOP 文件**(內部 wiki / docs/troubleshooting/brick-recovery.md讓技術支援能幫忙。
---
## 7. 法律 / 合規 考量(待釐清)
承前一份 R-FW-5「打包 Kneron 官方 firmware 是否合法」。
**B2 階段新增變數**
- 多版本 bundle 含舊版 firmwarev2.1.0、KDP1→ 法律問題加重?
- 暴露給一般使用者降版 → Kneron 是否允許KDP1 是被棄用版本、Kneron 不一定希望使用者降回去)
**建議**
- B 階段啟動前、與 Kneron 取得 firmware re-distribution **明確書面授權**
- 授權內容必須包含current + 舊版v2.1.0 等)+ KDP1
- 如果 Kneron 不允許降版到 KDP1、調整 bundle 範圍
---
## 8. 給 Orchestrator 的決策點清單
1. **使用者已決策手動降版面向一般使用者** → 本檔 §5 design 需求成立
2. **使用者已決策 FW 內嵌進安裝包、+5MB 接受** → 本檔 §4.3「保守」策略對齊
3. **使用者已決策不做線上更新通道** → 不實作 OTA、所有 firmware bundle
4. **待使用者裁決**
- 多版本目錄結構選 A/B/C建議 C、§3.2
- bundle 哪些舊版建議「保守」策略、§4.3
- KneronPLUS wheel 升級 vs 不升級M9-6 驗證後決定)
- 與 Kneron 取得 firmware redistribution 授權的時程?
---
## 9. 與其他研究檔的關係
| 連結 | 引用 |
|------|------|
| `30-integration-plan.md` §6 階段 B 評估提示 | 本檔詳化 B2 階段細節 |
| `40-b-phase-kl630-kl730-extension.md` §8 多版本 firmware 並存 | 本檔 §3 詳化儲存結構 |
| `40-b-phase-kl630-kl730-extension.md` R-FW-11/R-FW-12 | 本檔 §5/§6 提供緩解措施 |
| `41-tar-firmware-handling.md` §3 大小估算 | 本檔 §4 整合計算 |
---
## 10. 工時影響補充
B 階段拆三層之後、本檔內容對應 M9-11 + M9-12 兩個 milestone
| Milestone | 本檔 § | 工時 |
|-----------|--------|------|
| M9-11 多版本後端 | §2.1 ~ §2.4 + §3 + §4 | 1.5 人天 |
| M9-12 降版 UI | §5 + §6 + §7 | Frontend 1 人天 + Design 1 人天 = 2 人天 |
| **合計** | | **3.5 人天** |
(已含在 §40 B 階段 M9-6 ~ M9-13 工時表內、本檔不重複加總)

View File

@ -0,0 +1,730 @@
# M9-6 SDK 驗證計畫書
> 對應 research index §50
> 範圍B 階段啟動前的 KneronPLUS SDK + KL630/KL730 SDK 行為實機驗證計畫
> 撰寫日期2026-05-24
> 限制:純 plan、不出 code、不執行驗證驗證由 testing/backend 工程師執行)
> 路徑使用相對路徑(相對於 `/Users/jimchen/visionA/local-tool/`
---
## 0. TL;DR
1. 本檔是「**將來要做的驗證計畫**」、不是驗證執行報告。執行階段由 testing + backend Agent 接手、結果回填到本檔 §8 與相關研究檔。
2. 必須驗證的關鍵 unknown 共 **10 項**,分 3 大類:
- **SDK API 行為**API 簽名 / .tar 支援 / chip enum 完整性)— 6 項
- **KL630/KL730 連線行為**USB scan / connect / firmware load— 3 項
- **KneronPLUS wheel 版本相容性**(既有 wheel 是否需升級 + 升級風險)— 1 項
3. 驗證有兩個強度等級:
- **強驗證**(有實機 KL630/KL730 dongle— 涵蓋 10 項全部、結果可信
- **弱驗證**(無實機、只查 SDK 文件 + wheel source— 涵蓋 6 項API 層)、剩 4 項(連線行為)只能靠文件推斷、留風險到 M9-9 開發階段
4. 預估時間:強驗證 2 人天、弱驗證 1 人天
5. **關鍵風險**:拿不到 KL630/KL730 硬體 → 強驗證降級為弱、B 階段所有 milestone 都帶 SDK unknown 風險
---
## 1. 為什麼需要 SDK 驗證
### 1.1 前兩輪研究的 SDK unknown 清單
`40-b-phase-kl630-kl730-extension.md` §3.2 + `41-tar-firmware-handling.md` §2 + §5 整理:
| 來源 | unknown | 影響範圍 |
|------|---------|---------|
| `40-b-phase` §3.2 row 1 | KL630 `connect_devices()` 是否與 KL520/KL720 共用簽名 | connect 流程設計 |
| `40-b-phase` §3.2 row 2 | `load_firmware_from_file()` 對 KL630/KL730 是否仍接受兩個 .bin解壓後 | .tar 處理策略選定Y or Z|
| `40-b-phase` §3.2 row 3 | 是否有 `load_firmware_from_tar()` 之類新 API | 策略 Z 是否可行 |
| `40-b-phase` §3.2 row 4 | `update_kdp_firmware_from_files()` 對 KL630/KL730 是否適用 | FW 升降版 API 選擇 |
| `40-b-phase` §3.2 row 5 | KL630/KL730 是否仍有 Loader / KDP / KDP2 三 state | FW 偵測邏輯 |
| `40-b-phase` §3.2 row 6 | KL630/KL730 是 flash-based 還是每次都要 load firmware | connect 速度設計 |
| `40-b-phase` §3.2 row 7 | `generic_image_inference_send/receive` 對 KL630/KL730 是否一致 | inference 流程是否分支 |
| `40-b-phase` §3.2 row 8 | KL630/KL730 firmware 字串可能值 | FW badge 顯示邏輯 |
| `41-tar` §1.3 | .tar 內容解壓後有哪些檔案、metadata | bridge.py 解析邏輯 |
| `41-tar` §2.3 | visionA-local 既有 KneronPLUS wheel 版本 vs 3.1.2 | wheel 升級決策 |
### 1.2 為什麼這些必驗、不能靠推測
每個 unknown 都有「假設錯了會炸」的後果:
- API 簽名假設錯 → bridge.py 寫出來直接 import error / type error / runtime crash
- .tar 處理策略選錯 → 安裝包大小 / build script 設計全錯、要從 M9-8 整段重做
- wheel 版本假設錯 → KL520/KL720 既有功能 regression、回頭重做 A 階段
→ M9-6 是 B 階段 critical path、所有後續 milestone 都依賴它。
---
## 2. 驗證目標清單10 項)
每項 = 一個必須回答的具體問題。每項標:是否依賴實機、強度等級、後續決策影響。
### 類別 ASDK API 行為(弱驗證即可、查 wheel source / SDK 文件)
#### A-1既有 KneronPLUS wheel 版本是什麼
- **問題**visionA-local repo 內目前用的 KneronPLUS wheel 是哪個版本?是否內含 `KP_DEVICE_KL630` / `KP_DEVICE_KL730` enum
- **依賴實機**:否(只需 Python 環境)
- **影響**:決定要不要升級 wheel升級會牽動 KL520/KL720 regression 風險)
#### A-2wheel 內 `kp.ProductId.KP_DEVICE_KL630` / `KL730` enum 是否存在
- **問題**:既有 wheel `kp.ProductId.KP_DEVICE_KL630` / `KL730` 是否可 import、值是多少
- **依賴實機**:否
- **影響**A-2 若失敗 → 必須升級 wheel連 driver 認 chip 都做不到)
#### A-3`kp.core.load_firmware_from_file()` 對 .tar 的行為
- **問題**:把 `.tar` 路徑當 `scpu_file` 參數餵進去、SDK 回什麼?接受、解析錯誤、還是 silent fail
- **依賴實機**:否(傳路徑、看 exception type
- **影響**:決定 .tar 處理策略Y 解壓 or Z 直接餵)
#### A-4是否有 `load_firmware_from_tar()` / `update_*_from_tar()` 新 API
- **問題**wheel 內是否提供 .tar 專用 firmware load API簽名為何
- **依賴實機**grep wheel source
- **影響**:策略 Z 是否可行(最乾淨方案)
#### A-5`update_kdp_firmware_from_files()` 對 KL630/KL730 是否適用
- **問題**:把 KL630 device + KL630 .tar / .bin 餵進去、SDK 回什麼?是否有 chip-aware 內部 dispatch、還是 KL520/KL720 專用?
- **依賴實機**:部分(無實機只能查 source code 的 chip 分支邏輯)
- **影響**FW 升降版 API 選擇
#### A-6KneronPLUS wheel 升級對 KL520/KL720 既有行為的 regression 範圍
- **問題**:從目前 wheel → 3.1.2(或 latest有哪些 breaking change
- **依賴實機**:強驗證(升級後跑既有 E2E test弱驗證看 release notes / changelog
- **影響**:是否值得升 wheel、升級後 A 階段 KL520/KL720 是否需要回歸測試
### 類別 BKL630 / KL730 連線行為(需實機強驗證)
#### B-1KL630 / KL730 在 USB scan 時的 product_id
- **問題**:實機 `kp.core.scan_devices()` / pyusb scan 時KL630 dongle 的 `product_id``0x0630`KL730 是 `0x0730` 嗎?是否有 legacy/new 雙版本(像 KL720 的 `0x0200` vs `0x0720`
- **依賴實機**:是(必須有 dongle
- **影響**`_KNOWN_PRODUCTS` map 是否需擴展、`chipFromProductID()` switch 是否要加額外 case
#### B-2KL630 / KL730 是否每次 connect 都要 load firmware
- **問題**:實機連續 connect 兩次、第二次是否需要重新 load firmwareKL520 要、KL720 不要)
- **依賴實機**:是
- **影響**connect 流程設計(速度 + 用量)、是否走 build time 解壓 vs runtime 解壓
#### B-3KL630 / KL730 firmware 字串可能值
- **問題**:實機 `scan_devices()` 回傳的 `device.firmware` 欄位字串是什麼?"KDP2" / "KDP3" / "Loader" / 其他是否能透過此值區分「需要升級」vs「已 ready」
- **依賴實機**:是
- **影響**FW badge UI 顯示邏輯、`detector.go` firmware state 判斷
### 類別 C.tar 檔案內容(無實機也能驗、純 tar -xf
#### C-1.tar 內容實測
- **問題**`tar -tvf kp_firmware.tar` / `tar -tvf kp_loader.tar` 內含哪些檔案?大小?是否有 manifest
- **依賴實機**:否(只需 warrenchen bundle 的 .tar 檔)
- **影響**bridge.py 解壓後檔案路徑解析邏輯、`_resolve_firmware_paths` 設計
---
## 3. 各項驗證方法(執行 step-by-step
> **注意**:這節是給未來執行 M9-6 的 testing/backend 工程師看的、step 描述要可重複。
### A-1既有 wheel 版本查詢
**前置**:在 visionA-local repo 根目錄、Python 環境已 setup。
**步驟**
```bash
# Step 1: 找 KneronPLUS wheel 位置
find . -name "KneronPLUS*.whl" 2>/dev/null
find . -name "kneronplus*.whl" 2>/dev/null
# 若上述都沒結果、檢查是否 venv 內安裝
. server/scripts/venv/bin/activate 2>/dev/null && pip show kneronplus 2>/dev/null
# 也檢查專案有沒有 vendored Python source
find . -path "*/kp/__init__.py" -not -path "*/node_modules/*" 2>/dev/null
```
**預期結果三種**
- **A**:找到 `KneronPLUS-X.Y.Z-py3-none-any.whl` → 記錄版號 X.Y.Z
- **B**:找到 installed package、`pip show` 回版本 → 記錄
- **C**:找不到 → wheel 是 lazy import 或 system-wide install、需查 `kneron_bridge.py` `_macos_preload_native_libs()` 內的 lib path 反推 wheel 位置
**回填位置**:本檔 §8.1、`41-tar-firmware-handling.md` §2.3
### A-2wheel 內 KL630/KL730 enum 存在驗證
**前置**A-1 已找到 wheel、Python 環境可 `import kp`
**步驟**
```bash
python3 -c "
import kp
print('KP_DEVICE_KL520:', getattr(kp.ProductId, 'KP_DEVICE_KL520', 'NOT FOUND'))
print('KP_DEVICE_KL720:', getattr(kp.ProductId, 'KP_DEVICE_KL720', 'NOT FOUND'))
print('KP_DEVICE_KL630:', getattr(kp.ProductId, 'KP_DEVICE_KL630', 'NOT FOUND'))
print('KP_DEVICE_KL730:', getattr(kp.ProductId, 'KP_DEVICE_KL730', 'NOT FOUND'))
print('KP_DEVICE_KL530:', getattr(kp.ProductId, 'KP_DEVICE_KL530', 'NOT FOUND'))
"
```
**預期結果三種**
- **A**:四個都 print 出整數值 → 既有 wheel OK、不必升級僅針對 enum 層)
- **B**KL520/KL720 有、KL630/KL730 沒有 → 必須升級 wheel
- **C**`import kp` 直接 import error → wheel 未安裝、A-1 結果有誤、回去查
**回填位置**:本檔 §8.1、影響 M9-7 決策
### A-3load_firmware_from_file 對 .tar 行為
**前置**A-1 + A-2 完成、warrenchen .tar 檔已取得(在 `/tmp/web_academy_prototype/local_service_win/firmware/KL630/kp_firmware.tar`)。
**步驟**
```bash
python3 << 'EOF'
import kp
import sys
# 試 connect KL630 dongle、若無實機跳過、改成 mock test
tar_path = "/tmp/web_academy_prototype/local_service_win/firmware/KL630/kp_firmware.tar"
# 情境 1用實機若有
try:
descs = kp.core.scan_devices()
kl630_port = None
for i in range(descs.device_descriptor_number):
dev = descs.device_descriptor_list[i]
# 假設 KL630 product_id = 0x0630、若 B-1 驗證後值不同要修
if hex(dev.product_id) == "0x630":
kl630_port = dev.usb_port_id
print(f"KL630 found at port {kl630_port}")
break
if kl630_port:
dg = kp.core.connect_devices([kl630_port])
# 試把 .tar 直接餵
try:
ret = kp.core.load_firmware_from_file(dg, tar_path, "")
print(f"load_firmware_from_file(.tar, '') = {ret}")
except Exception as e:
print(f"load_firmware_from_file(.tar, '') raised: {type(e).__name__}: {e}")
# 試把 .tar 當 scpu + ncpu 同檔
try:
ret = kp.core.load_firmware_from_file(dg, tar_path, tar_path)
print(f"load_firmware_from_file(.tar, .tar) = {ret}")
except Exception as e:
print(f"load_firmware_from_file(.tar, .tar) raised: {type(e).__name__}: {e}")
except Exception as e:
print(f"No device, skip runtime test: {e}")
# 情境 2純靜態查 source無實機也能跑
import inspect
print("\nload_firmware_from_file source:")
try:
print(inspect.getsource(kp.core.load_firmware_from_file))
except Exception as e:
print(f"Cannot get source: {e}")
EOF
```
**預期結果三種**
- **A**SDK 接受 .tar、回 success → 策略 Z 可行
- **B**SDK 報 `KP_FW_INFO_ERR` / 類似錯誤碼 → 策略 Y 必須走build time 解壓)
- **C**source 顯示函式內有 `.endswith(".tar")` 之類判斷 → 確認 SDK 內部分支邏輯
**回填位置**:本檔 §8.1、`41-tar-firmware-handling.md` §2.1 + §4.4
### A-4load_firmware_from_tar / update_*_from_tar 是否存在
**步驟**
```bash
python3 << 'EOF'
import kp
import kp.core
# 列出 kp.core 所有 firmware-related 函式
for name in dir(kp.core):
if any(k in name.lower() for k in ["firmware", "fw_", "load_fw", "update_kdp", "tar"]):
print(f"kp.core.{name}")
# 列出 kp 模組頂層 firmware-related
for name in dir(kp):
if any(k in name.lower() for k in ["firmware", "fw_", "tar"]):
print(f"kp.{name}")
EOF
```
**或解壓 wheel grep**(無 Python 環境也能跑):
```bash
cd /tmp && unzip -o KneronPLUS-X.Y.Z-py3-none-any.whl -d kneron_plus_inspect/
grep -rn "load_firmware_from_tar\|update_kdp.*tar\|extract_tar\|from_tar" kneron_plus_inspect/kp/
grep -rn "^def \|^ def " kneron_plus_inspect/kp/core.py | grep -iE "fw|firmware|tar"
```
**預期結果**
- **A**:找到 `load_firmware_from_tar()``update_kdp2_firmware_from_tar()` → 策略 Z 確定可行
- **B**:找不到、只有既有 `load_firmware_from_file()` → 策略 Ybuild time 解壓)
**回填位置**:本檔 §8.1、`41-tar-firmware-handling.md` §2.1 表格
### A-5update_kdp_firmware_from_files 對 KL630/KL730 chip dispatch
**步驟**
```bash
python3 << 'EOF'
import kp.core
import inspect
# 看 update_kdp_firmware_from_files source 是否有 chip 判斷
try:
src = inspect.getsource(kp.core.update_kdp_firmware_from_files)
print(src)
except Exception:
pass
# 列其他 update 系列函式
for name in dir(kp.core):
if "update" in name.lower() and "fw" in name.lower():
print(name)
EOF
```
**預期結果**
- **A**source 內有 `if chip == "KL630"` 之類分支 → 適用、可直接用
- **B**source 只處理 KL520/KL720 → 需找 `update_kdp2_firmware_*` 之類新 API
- **C**:找不到對應 API → 須 fall back 用 `load_firmware_from_file` + 手動 reboot 流程
**回填位置**:本檔 §8.1、`40-b-phase` §3.2 row 4
### A-6wheel 升級對 KL520/KL720 regression 範圍
**步驟**(弱驗證版本,無實機):
```bash
# Step 1: 取得 SDK changelog
# 通常在 Kneron developer portal 或 wheel METADATA
cd /tmp && unzip -o KneronPLUS-X.Y.Z-py3-none-any.whl -d wheel_old/
unzip -o /tmp/web_academy_prototype/local_service_win/KneronPLUS-3.1.2-py3-none-any.whl -d wheel_new/
diff -r wheel_old/kp/ wheel_new/kp/ | head -100
# 重點看 kp/core.py 和 kp/inference.py 的 API 簽名變化
```
**步驟**(強驗證版本,有實機):
1. 備份既有 wheel 安裝
2. pip install warrenchen wheel 3.1.2
3. 跑 visionA-local 既有 KL520 + KL720 完整 E2Escan → connect → load firmware → inference
4. 比對 inference 結果(用同一張圖、同一個 NEF、檢查 detection box 是否一致)
5. 紀錄 break point
**預期結果**
- **A**API 簽名完全相容、inference 結果一致 → 升級安全
- **B**API 簽名有 breaking change部分函式 rename / 參數順序變) → 升級需配合 bridge.py 更新
- **C**inference 結果不一致 → 升級風險高、需深查
**回填位置**:本檔 §8.1、新增風險紀錄到 R-FW-8
### B-1KL630/KL730 USB scan product_id
**前置**:實機 KL630 + KL730 dongle 接上電腦、driver 已裝。
**步驟**
```bash
python3 << 'EOF'
import kp
descs = kp.core.scan_devices()
print(f"Total devices: {descs.device_descriptor_number}")
for i in range(descs.device_descriptor_number):
dev = descs.device_descriptor_list[i]
print(f" device {i}:")
print(f" product_id = {hex(dev.product_id)}")
print(f" usb_port_id = {dev.usb_port_id}")
print(f" firmware = {dev.firmware}")
print(f" is_connectable = {dev.is_connectable}")
print(f" kn_number = {dev.kn_number}")
EOF
```
**同時用 pyusb 比對**(看 OS 層級看到什麼 product_id
```bash
python3 << 'EOF'
import usb.core
devs = usb.core.find(find_all=True, idVendor=0x3231) # Kneron VID
for dev in devs:
print(f"VID:PID = {hex(dev.idVendor)}:{hex(dev.idProduct)}")
EOF
```
**預期結果**
- KL630 → product_id = `0x0630`、KL730 → `0x0730`、跟前兩輪研究假設一致
- 若有 legacy/new 雙版本(像 KL720 的 0x0200 vs 0x0720→ 紀錄並更新 `_KNOWN_PRODUCTS` map
**回填位置**:本檔 §8.2、`40-b-phase` §2.1
### B-2KL630/KL730 每次 connect 都要 load firmware 嗎
**前置**B-1 完成、確認能識別 KL630/KL730。
**步驟**
```bash
python3 << 'EOF'
import kp
import time
descs = kp.core.scan_devices()
# 找 KL630或 KL730
port_id = None
for i in range(descs.device_descriptor_number):
dev = descs.device_descriptor_list[i]
if hex(dev.product_id) == "0x630": # KL630
port_id = dev.usb_port_id
firmware_before = dev.firmware
is_connectable_before = dev.is_connectable
break
if not port_id:
print("No KL630 found")
exit()
print(f"Before connect: firmware={firmware_before}, is_connectable={is_connectable_before}")
# 第一次 connect、不 load firmware
dg1 = kp.core.connect_devices([port_id])
print(f"First connect OK, dg={dg1}")
del dg1
time.sleep(2)
# 重新 scan、看 firmware 字串是否變化
descs2 = kp.core.scan_devices()
for i in range(descs2.device_descriptor_number):
dev = descs2.device_descriptor_list[i]
if dev.usb_port_id == port_id:
print(f"After first connect: firmware={dev.firmware}, is_connectable={dev.is_connectable}")
break
# 第二次 connect、看是否仍能 inference不重 load
dg2 = kp.core.connect_devices([port_id])
# 試跑 model load (用 KL630 sample NEF)
# kp.core.load_model_from_file(dg2, "...sample.nef")
# 觀察是否 OK
EOF
```
**預期結果**
- **A**:第二次 connect 後直接能 inference、不用重 load firmware → flash-based、像 KL720
- **B**:第二次 connect 後 inference 失敗、需重新 `load_firmware_from_file()` → 像 KL520
**回填位置**:本檔 §8.2、`40-b-phase` §5.2 / §5.3 R-FW-10
### B-3KL630/KL730 firmware 字串可能值
**步驟**:在 B-1 跑完已自然取得(`dev.firmware` 欄位)。額外重複測試不同 state
1. KL630 剛從盒子拿出來(未灌任何 firmware→ 紀錄 firmware 字串
2. KL630 跑過一次 inference 後(已 load firmware→ 紀錄
3. KL630 reboot 後 → 紀錄
**預期結果**
- 可能值:`""` / `"KDP"` / `"KDP2"` / `"KDP3"` / `"Loader"` / `"App"` / 其他
- 紀錄 chip × state 對照表
**回填位置**:本檔 §8.2、`40-b-phase` R-FW-10、`detector.go` firmware state map
### C-1.tar 檔案內容實測
**前置**warrenchen bundle 已 clone 到 `/tmp/web_academy_prototype/`
**步驟**
```bash
cd /tmp/web_academy_prototype/local_service_win/firmware
# KL630
echo "=== KL630 kp_firmware.tar ==="
ls -lh KL630/kp_firmware.tar KL630/kp_loader.tar
tar -tvf KL630/kp_firmware.tar
tar -tvf KL630/kp_loader.tar
# KL730
echo "=== KL730 kp_firmware.tar ==="
ls -lh KL730/kp_firmware.tar KL730/kp_loader.tar
tar -tvf KL730/kp_firmware.tar
tar -tvf KL730/kp_loader.tar
# 試解壓到 temp
mkdir -p /tmp/kl630_extracted
tar -xf KL630/kp_firmware.tar -C /tmp/kl630_extracted
ls -lhR /tmp/kl630_extracted/
file /tmp/kl630_extracted/*
# 看是否有 manifest / metadata
for f in /tmp/kl630_extracted/*.json /tmp/kl630_extracted/*.txt /tmp/kl630_extracted/*.yml /tmp/kl630_extracted/*.yaml; do
[ -f "$f" ] && echo "--- $f ---" && cat "$f"
done
```
**預期結果**
- **A**.tar 內含 `fw_scpu.bin` + `fw_ncpu.bin` + 可能 `manifest.json` → 策略 Y 解壓後拿 .bin 直餵
- **B**.tar 內含其他結構(如 nested .tar / 加密檔) → 處理複雜度上升
**回填位置**:本檔 §8.3、`41-tar-firmware-handling.md` §1.3
---
## 4. 驗證環境需求
### 4.1 硬體
| 硬體 | 強驗證 | 弱驗證 | 取得來源 |
|------|--------|--------|---------|
| KL520 dongle | 可選(用於 wheel 升級回歸測試)| 不需 | 既有開發環境 |
| KL720 dongle | 可選(同上)| 不需 | 既有開發環境 |
| KL630 dongle | **必須 ≥1 顆** | 不需 | 問 Innovedus team / Kneron 借機 |
| KL730 dongle | **必須 ≥1 顆** | 不需 | 問 Innovedus team / Kneron 借機 |
| 跑驗證的主機 | macOS / Linux 至少 1 台、Windows 1 台(驗 driver install| macOS 或 Linux 1 台即可 | 既有開發環境 |
### 4.2 軟體
- Python 3.10+(既有 venv 即可)
- KneronPLUS Python wheel既有 + warrenchen 3.1.2 兩版各一份)
- KL630 / KL730 sample NEF 模型(從 Kneron sample 拿、用於 B-2 inference 測試)
- 解壓工具Python `tarfile` 標準庫 + shell `tar`macOS / Linux 內建、Windows 10+ 內建)
### 4.3 SDK 文件
- KneronPLUS SDK Reference Manual PDF從 Kneron developer portal 下載)
- SDK changelog / release notes決定升級的 breaking change 範圍)
### 4.4 平台覆蓋策略
**M9-6 階段**只驗 1 平台(推薦 macOS 或 Linux、剩 2 平台留到 M9-13 整體三平台驗證跑。
理由M9-6 主要驗 SDK API 行為(跨平台一致)、不是驗安裝 / driver / OS 整合(那是 M9-13
---
## 5. 預估時間
### 5.1 強驗證(有實機)
| 階段 | 工時 | 累積 |
|------|------|------|
| 環境準備(取得硬體、裝 driver、setup Python | 0.5 天 | 0.5 |
| A 類驗證執行A-1 ~ A-6| 0.5 天 | 1.0 |
| B 類驗證執行B-1 ~ B-3| 0.5 天 | 1.5 |
| C 類驗證執行C-1| 0.1 天 | 1.6 |
| 結果回填到本檔 §8 + 其他研究檔 | 0.4 天 | 2.0 |
| **合計** | **2 人天** | |
### 5.2 弱驗證(無實機)
| 階段 | 工時 | 累積 |
|------|------|------|
| 環境準備(裝 wheel、解壓 wheel source| 0.2 天 | 0.2 |
| A 類驗證執行(弱版本,無實機部分)| 0.3 天 | 0.5 |
| B 類驗證跳過(只靠 SDK 文件推斷)| 0.2 天 | 0.7 |
| C 類驗證執行 | 0.1 天 | 0.8 |
| 結果回填 + 標註「需 M9-9 補強驗」| 0.2 天 | 1.0 |
| **合計** | **1 人天** | |
### 5.3 預期 B 階段風險增量(採弱驗證的話)
如果 M9-6 走弱驗證、B-1 / B-2 / B-3 留到 M9-9 開發階段才實機驗:
- M9-9 工時可能從 2 天 → 3 天(多 1 天驗 SDK 行為 + 修 bridge.py
- M9-13 三平台驗證可能多發現 1-2 個原本以為 work 的 case 實際不 work
**強烈建議走強驗證**、即使要等硬體到位。
---
## 6. 風險
### R-VAL-1拿不到 KL630 / KL730 硬體(高度風險)
**情境**Innovedus / Kneron 沒法提供 dongle、或要等好幾週。
**緩解選項**
- **選項 A**降級為弱驗證、B 階段啟動時 SDK unknown 帶到 M9-9
- **選項 B**:延後 B 階段啟動、等硬體到位(不影響 A 階段 MVP
- **選項 C**:先做 mock 驗證(建 mock SDK、用 monkey patch 假裝 KL630 device、等實機到再覆驗
**建議**:選項 B延後 B 階段)+ 順手做選項 C建 mock 為將來測試鋪路)
### R-VAL-2KneronPLUS wheel 不支援 KL630/KL730中度
**情境**A-2 驗證結果是 "B"(既有 wheel 沒有 KL630/KL730 enum
**緩解**
- 升級 wheel 到 warrenchen 3.1.2 或更新版
- A-6 評估 regression 範圍、必要時跑完整 KL520/KL720 回歸測試
- 預留 0.5-1 人天到 M9-7 做 wheel 升級 + regression
### R-VAL-3驗證結果跟前兩輪研究假設不同中度
**情境**:例如 B-1 發現 KL630 product_id 不是 `0x0630`、是 legacy + new 雙 ID。
**觸發重大調整 plan 的條件**
1. KL630 / KL730 product_id 跟假設不同 → 更新 `40-b-phase` §2.1 + bridge.py 改動範圍
2. KL630 / KL730 是 USB Boot 而非 flash-based → 整段 §5.2 推測重寫、`.tar` 處理流程設計重來
3. SDK 完全沒有 .tar 支援 API → 策略 Z 永久排除、回到策略 Y、build script 必加
4. wheel 升級對 KL520 regression > 30% → 不升級、找其他方案(可能要找 Kneron 要 dev 版)
每個觸發條件對應的 plan 重做工時估算:
- 觸發 1 → 0.2 天重寫 + M9-7 多 0.3 天
- 觸發 2 → 1 天重寫研究檔 + M9-8/M9-9 各多 0.5 天
- 觸發 3 → 不影響(策略選定即可)
- 觸發 4 → 1-2 天找替代方案
### R-VAL-4硬體環境不穩、間歇性失敗低度
**情境**USB 接觸不良、dongle 過熱、driver 偶發崩潰。
**緩解**
- 每個 B 類驗證重複 3 次、取一致結果
- 紀錄不一致 case、單獨追
---
## 7. 驗證完成的下游影響
### 7.1 既有研究檔需要更新
| 檔案 | 章節 | 更新內容 |
|------|------|---------|
| `40-b-phase-kl630-kl730-extension.md` | §3.2 表格 | 填入「實測值」column |
| `40-b-phase-kl630-kl730-extension.md` | §5.2 | KL630/KL730 推測段落改成「驗證結果」 |
| `40-b-phase-kl630-kl730-extension.md` | §7 | inference 流程驗證 |
| `40-b-phase-kl630-kl730-extension.md` | R-FW-8 / R-FW-10 | 風險紀錄狀態更新 |
| `41-tar-firmware-handling.md` | §1.3 | .tar 內容實測結果 |
| `41-tar-firmware-handling.md` | §2.1 / §2.2 / §2.3 | wheel 版本 + API 驗證結果 |
| `41-tar-firmware-handling.md` | §3.3 | .tar 大小實測 |
| `41-tar-firmware-handling.md` | §4.4 | 策略選定Y or Z |
| `41-tar-firmware-handling.md` | §5 | 驗證計畫狀態改「已完成」+ 結果 |
### 7.2 milestone 重排可能性
| milestone | 受 M9-6 結果影響的程度 |
|-----------|---------------------|
| M9-7B0 認 chip | 低B-1 結果可能調整 chip 判斷邏輯、+0.1 天 |
| M9-8.tar handling | 高:策略 Y vs Z 完全不同實作、工時可能 ±0.5 天 |
| M9-9connect + inference | 中B-2 / B-3 結果決定 connect 流程設計 |
| M9-10FW 升版擴 KL630/KL730 | 中A-5 結果決定用哪個 API |
| M9-11多版本 | 低 |
| M9-12降版 UI | 低 |
| M9-13三平台驗證 | 中:若驗證發現新問題、增加 case 數 |
### 7.3 TDD v2.2 firmware-management.md 影響
`30-integration-plan.md` 已標 firmware-management 章節範圍。M9-6 結果可能影響:
- §「SDK API 對照表」需填實測值
- §「KL630/KL730 連線流程」需確認 flash-based vs USB Boot
- §「.tar 處理策略」需選定 Y or Z
- §「KneronPLUS wheel 升級需求」需填升級決策
**TDD v2.2 firmware-management.md 應該等 M9-6 完成再 finalize**、或 M9-6 結果作為 v2.2 → v2.3 的 update trigger。
---
## 8. 驗證結果填空區(執行後填)
> 本節是執行 M9-6 的工程師填寫的、現階段空白。
### 8.1 A 類結果
| 項目 | 結果 | 證據 / 紀錄路徑 | 對下游決策影響 |
|------|------|---------------|--------------|
| A-1既有 wheel 版本 | _未驗證_ | | |
| A-2KL630/KL730 enum 存在 | _未驗證_ | | |
| A-3load_firmware_from_file 對 .tar 行為 | _未驗證_ | | |
| A-4load_firmware_from_tar 是否存在 | _未驗證_ | | |
| A-5update_kdp_firmware_from_files 對 KL630/KL730 適用性 | _未驗證_ | | |
| A-6wheel 升級 regression 範圍 | _未驗證_ | | |
### 8.2 B 類結果
| 項目 | 結果 | 證據 | 對下游決策影響 |
|------|------|------|--------------|
| B-1KL630/KL730 product_id | _未驗證_ | | |
| B-2KL630/KL730 是否每次 connect 重 load firmware | _未驗證_ | | |
| B-3KL630/KL730 firmware 字串可能值 | _未驗證_ | | |
### 8.3 C 類結果
| 項目 | 結果 | 證據 | 對下游決策影響 |
|------|------|------|--------------|
| C-1.tar 內容 | _未驗證_ | | |
### 8.4 整體結論(執行後填)
- 策略 Y vs Z 選定_待驗證後填_
- wheel 升級決策_待驗證後填_
- B 階段啟動 go/no-go_待驗證後填_
---
## 9. 給 Orchestrator 的執行建議
### 9.1 何時派工
**選項 AA 階段 MVP 同步起跑**(使用者目前決策)
- M9-6 與 M9-1 ~ M9-5 平行
- 派 testing/backend Agent 在 A 階段任何時點啟動 M9-6推薦 A 階段 50% 進度時)
- M9-6 完成後 B 階段才能啟動、不卡 A 階段
- 風險A 階段也在動 bridge.py、M9-6 可能跟 A 階段同檔修改衝突(協調好就好)
**選項 BA 階段 MVP 完成後啟動**(原計畫)
- M9-6 + A 階段 5 人天 + B 階段 10.5 人天 = 序列
- 簡單、無衝突
- 風險B 階段啟動延後 5 人天
**建議**:選 A、但安排 M9-6 在 A 階段 M9-3 或 M9-4 完成後啟動bridge.py 大改動已完成)、避開檔案衝突。
### 9.2 派工 Agent 選擇
**強驗證階段**
- 主要testing Agent驗證執行 + 結果紀錄是專業)
- 配合backend Agent讀 SDK source code + 寫驗證 script 偏 backend 領域)
- 推薦testing 主派、backend 顧問
**弱驗證階段**(若無實機):
- 主要backend Agent純查 source + 文件)
- testing 可不參與
### 9.3 派工前必須先做的事
1. **跟使用者確認硬體狀態**:問「你或 Innovedus team 手上有 KL630 / KL730 dongle 嗎?多久能拿到?」
2. **根據答案決定強 / 弱驗證**
- 有 → 強驗證、派 testing + backend
- 沒、3 週內能拿到 → 延後 M9-6、改先做選項 C mock順手鋪路
- 沒、無法取得 → 弱驗證、接受 M9-9 補強驗的風險
3. **取得 KneronPLUS SDK Reference Manual PDF**:問使用者 / 從 Kneron developer portal 下載、放到 `.autoflow/04-architecture/research-kl520-fw-management/refs/` 供工程師查
### 9.4 派工 prompt 模板(給 Orchestrator 用)
```
路徑資訊:
- 你的角色定義:(對應 agent CLAUDE.md 絕對路徑)
- 專案目錄:當前工作目錄
- 本任務 plan`.autoflow/04-architecture/research-kl520-fw-management/50-m9-6-sdk-validation.md`
- 結果回填到本檔 §8.1 / §8.2 / §8.3
任務:執行 M9-6 SDK 驗證(強驗證版本)
範圍:本檔 §2 列出的 10 個驗證項目、按 §3 的 step 執行
產出:
1. 本檔 §8 填空區填實測結果
2. 更新 §7.1 列的下游研究檔(標明「本次更新」)
3. 結果摘要丟給 Orchestrator、附 go/no-go 建議
不要做:
- 改 bridge.py / driver code驗證 only、不寫產品 code
- 改 TDD v2.2 firmware-management.md那是 architect 範圍、本任務只回填到研究檔)
```
---
## 10. 與其他研究檔的關係
| 連結 | 引用內容 |
|------|---------|
| `40-b-phase-kl630-kl730-extension.md` §3.2 | 本檔 §1.1 / §2 類別 A 大部分項目來源 |
| `40-b-phase-kl630-kl730-extension.md` §5.2 / R-FW-10 | 本檔 §2 類別 B連線行為驗證 |
| `40-b-phase-kl630-kl730-extension.md` M9-6 段落 | 本檔是 M9-6 的詳細展開 |
| `41-tar-firmware-handling.md` §1.3 | 本檔 §2 C-1 來源 |
| `41-tar-firmware-handling.md` §2 | 本檔 §2 類別 A row A-3 / A-4 來源 |
| `41-tar-firmware-handling.md` §5 | 本檔取代之前的 「待 M9-6 驗證」placeholder、§5 升級為 reference 本檔 |
| `30-integration-plan.md` 階段 B 評估 | M9-6 結果回填觸發 TDD v2.2 firmware-management.md 更新 |

View File

@ -0,0 +1,332 @@
# M9-6 弱驗證結果(不含實機)
> 對應 `50-m9-6-sdk-validation.md` 的弱驗證執行
> 執行日期2026-05-24
> 執行者Architect Agent
> 路徑使用相對路徑(相對於 `/Users/jimchen/visionA/local-tool/`
---
## 環境
- **執行平台**macOS / sandboxed Architect Agent context無 Bash tool、無實機 dongle
- **執行限制**
- 不能解壓 wheel binary沒 Bash / `unzip` 可用、Read 對 deflate-compressed zip 內容只看到亂碼)
- 不能跑 Python REPL 確認 enum 值
- 不能解壓 `.tar` 看內容
- 不能 ssh 到實機
- **可用證據來源**
1. warrenchen prototype`/tmp/web_academy_prototype/`)的 Python source code
2. visionA-local 既有 `server/scripts/kneron_bridge.py`(看我們現在用了哪些 kp API
3. 既有研究檔 §00-§42
4. warrenchen bundled wheel filename pattern`KneronPLUS-3.1.2-py3-none-any.whl`
5. 既有 visionA-local wheel`KneronPLUS-2.0.0-py3-none-any.whl`macOS/Linux+ `KneronPLUS-3.1.2-py3-none-any.whl`Windows—— **wheel 版本三平台不一致**(重要發現)
---
## 結論TL;DR
1. **KneronPLUS 3.1.2 確認支援 KL630/KL730 enum**——warrenchen `main.py` L433-447 直接用 `kp.ProductId.KP_DEVICE_KL630/KL730/KL830`、運作中、enum 存在無疑。
2. **`update_kdp_firmware_from_files` API 在 KneronPLUS 3.1.2 存在且 warrenchen 已使用**——`update_kl720_firmware.py``legacy_plus121_runner.py` 兩個檔案大量使用 `lib.kp_update_kdp_firmware_from_files(dg, scpu, ncpu, auto_reboot)`。這是 KDP1 → KDP2 升版flash 寫入)的關鍵 API。
3. **`connect_devices_with_magic_pass` 概念存在、實際 API 是 `kp_connect_devices` + `KDP_MAGIC_CONNECTION_PASS=0x1FF55B4F` 狀態碼**——warrenchen 用此 magic value 繞過 KDP1 firmware 檢查、連到 legacy device、再寫 flash。這是 ctypes-level call、Python 層 `kp.core.connect_devices_without_check` 可能是同一 API 的 wrapper待強驗證確認
4. **`load_firmware_from_file` 只接受兩個 .bin 路徑scpu, ncpu、不接受 .tar**——warrenchen `FirmwareLoadRequest` schema 強證據:`scpu_path: str` + `ncpu_path: str`、無 `tar_path` 欄位。**策略 Z直接餵 .tar 給 SDK不可行**。
5. **沒有 `load_firmware_from_tar` / `update_kdp_firmware_from_tar` API**——warrenchen 完全沒有用、且若存在會出現在 `FirmwareLoadRequest``legacy_plus121_runner.py`。**策略 Ybuild time 解壓 .tar 找 .bin是唯一可行解**。
6. **warrenchen 對 KL630/KL730 的 firmware 升降版完全沒實作**——只有 KL520 + KL720 兩個 chip-specific endpoint`/firmware/legacy-upgrade/kl520` + `/firmware/legacy-upgrade/kl720`、KL630/KL730 沒有對應 endpoint。**這是設計 gap、不是 SDK 限制**。意味我們設計 A 階段的 `handle_firmware_upgrade` for KL630/KL730 時、沒有 reference 實作可抄、要自己根據 KneronPLUS SDK 推。
7. **重大發現visionA-local wheel 三平台版本不一致**——macOS/Linux 是 `KneronPLUS-2.0.0`、Windows 是 `KneronPLUS-3.1.2`。**2.0.0 是否含 `KP_DEVICE_KL630/KL730` enum 無法靜態確認**——`KP_DEVICE_KL720_LEGACY` 在 3.1.2 確認存在warrenchen L432但 2.0.0 不確定。**強驗證必跑**:在 macOS + Linux 上實際 `import kp; print(kp.ProductId.KP_DEVICE_KL630)` 看是否存在。
8. **PRD AC-FW-3.5KL630/KL730 升降版)的可行性結論**
- **理論上可行**SDK API 都在、enum 都在、`update_kdp_firmware_from_files` 對所有 chip 通用)
- **但弱驗證無法 100% 證明對 KL630/KL730 適用**——`update_kdp_firmware_from_files` 是否分支判斷 chip、KL630/KL730 是否走同一條 flash 寫入路徑、必須實機 confirm
- **建議**A 階段不開 KL630/KL730 升降版PRD AC-FW-3.5 暫降為 B 階段功能或標 "Tentative"、B 階段 M9-9 / M9-10 啟動時實機強驗
---
## Unknown 對照表
對應 `50-m9-6-sdk-validation.md` §2 的 10 個驗證項目:
| # | 類別 | Unknown | 弱驗證結果 | 仍需強驗證? | 影響 |
|---|------|---------|-----------|------------|------|
| A-1 | A | 既有 wheel 版本 | **三平台不一致**macOS/Linux 2.0.0、Windows 3.1.2(從 `visiona-local/wheels/*` glob 結果直接確認) | N已從檔名確認版本 | TDD §「KneronPLUS wheel 升級」必須記錄三平台不一致、考量是否統一升級到 3.1.2 |
| A-2 | A | wheel 內 `KP_DEVICE_KL630/KL730` enum | **3.1.2 確認有**warrenchen `main.py` L433-447 使用中);**2.0.0 待 Y**——靜態無法確認,需在 macOS/Linux 跑 `python3 -c "import kp; print(kp.ProductId.KP_DEVICE_KL630)"` | Y對 2.0.0 | 若 2.0.0 沒有 enum → 必須在 macOS/Linux 升級 wheel 到 3.1.2(會引入 KL520/KL720 regression 風險) |
| A-3 | A | `load_firmware_from_file` 對 .tar 行為 | **不接受 .tar、只接受兩個 .bin 路徑**warrenchen `FirmwareLoadRequest` schema = `scpu_path: str` + `ncpu_path: str` 強證據;`legacy_plus121_runner.py` L44-45 `lib.kp_load_firmware_from_file.argtypes = [c_void_p, c_char_p, c_char_p]`、兩個 c_char_p 對應兩個檔案) | Nschema 與 ctypes 簽名已強證據) | **策略 Z 確定不可行、必走策略 Y**build time 解壓 .tarTDD §「.tar firmware 處理」改為策略 Y 唯一方案、`_resolve_firmware_paths` 設計需走 `extracted/fw_scpu.bin` 路徑 |
| A-4 | A | `load_firmware_from_tar` / `update_*_from_tar` 是否存在 | **不存在**warrenchen 完全沒用、若存在會出現在 `FirmwareLoadRequest``legacy_plus121_runner.py`warrenchen 對 KL630/KL730 .tar 也沒有特別處理 endpoint | N負面證據強 | 同 A-3、策略 Z 排除 |
| A-5 | A | `update_kdp_firmware_from_files` 對 KL630/KL730 適用性 | **API 在 3.1.2 確認存在、簽名為 `(dg, scpu_path, ncpu_path, auto_reboot_bool)`**warrenchen `update_kl720_firmware.py` L110-120 與 `legacy_plus121_runner.py` L46-47 都明示);但**對 KL630/KL730 是否走同一條 flash 寫入路徑、無從靜態判斷**——warrenchen 對 KL630/KL730 完全沒用此 API沒有對應 endpoint | Y高度需要 | A 階段 KL630/KL730 升降版PRD AC-FW-3.5暫排除B 階段 M9-10 啟動前實機強驗、決定走 `update_kdp_firmware_from_files` 還是要找新 API |
| A-6 | A | wheel 升級對 KL520/KL720 regression 範圍 | **wheel 2.0.0 → 3.1.2 跨主版本、breaking change 風險高**——但無法靜態查 changelog沒 Kneron 官方文件 access既有 visionA-local kneron_bridge.py 用的 APIscan_devices、connect_devices、load_firmware_from_file、reset_device、load_model_from_file、generic_image_inference_send/receive、ChannelOrdering、GenericImageInferenceDescriptor、ImageFormat、ResetMode**這些 API 全部在 warrenchen 3.1.2 程式碼也有使用**(即 3.1.2 仍然支援)→ **可能可升級而不破壞既有 KL520/KL720** | Y必須三平台跑 E2E | 升 wheel 風險可控API 介面相容性看起來高);但必須 M9-13 三平台跑 KL520+KL720 完整 E2E 才能確定 |
| B-1 | B | KL630/KL730 USB scan product_id | **仍需實機**——0x0630/0x0730 是「研究階段假設」(從 visionA-local + warrenchen edge-ai 兩端 `_KNOWN_PRODUCTS` map 都這樣寫、但這些 map 是寫程式人猜的、warrenchen LocalAPI 程式碼**不直接用 product_id hex、而是用 `kp.ProductId.KP_DEVICE_KL630` enum 物件** | Y必須有實機 dongle | A/B 階段 driver `_KNOWN_PRODUCTS` map 維持 0x0630/0x0730 假設、但 M9-9 實機驗證時若不符要立刻調整 |
| B-2 | B | KL630/KL730 是否每次 connect 都要 load firmware | **仍需實機**——warrenchen 完全沒對 KL630/KL730 寫 connect/firmware load 流程、無 referenceKL520 是「USB Boot 必載」、KL720 是「flash-based 不載」、KL630/KL730 推測「flash-based 不載」(同新世代 SDK packaging 風格)但**僅推測** | Y | M9-9 啟動時實機跑「連續 connect 兩次、看第二次需不需要重 load」 |
| B-3 | B | KL630/KL730 firmware 字串可能值 | **仍需實機**——warrenchen 沒任何 KL630/KL730 firmware 字串紀錄KL520/KL720 我們既有觀察值是 `"KDP"` / `"KDP2"` / `"Loader"` / `""` 等 | Y | M9-9 實機 scan_devices() 直接看 `dev.firmware` 欄位 |
| C-1 | C | .tar 檔案內容 | **無法弱驗證**——`.tar` 是 binary、Grep tool 對 binary 跳過、無 Bash 跑 `tar -tvf`;唯一資訊:檔名 `kp_firmware.tar` + `kp_loader.tar`、warrenchen 對 .tar 內容沒留下 inspect 結果 | Y執行 `tar -tvf` 即可、無需實機) | M9-8 啟動前用 Bash 跑一次 `tar -tvf` 即解;不阻塞 A 階段任何工作 |
**統計**
- A 類 6 項 → 4 項弱驗證有答案、2 項需強驗證A-2 對 2.0.0、A-5 對 KL630/KL730 適用性、A-6 升 wheel regression
- B 類 3 項 → 0 項弱驗證有答案、3 項全需強驗證(必有實機)
- C 類 1 項 → 弱驗證可解但需 BashArchitect Agent 限制、移交 backend agent 5 分鐘可完成)
---
## 重點發現
### 發現 1KneronPLUS 3.1.2 enum 列表(從 warrenchen `main.py` L427-447 直接讀)
```python
KP_DEVICE_KL520
KP_DEVICE_KL720 # 0x0720 (KDP2 新版)
KP_DEVICE_KL720_LEGACY # 0x0200 (KDP1 老版、需走 magic pass)
KP_DEVICE_KL630
KP_DEVICE_KL730
KP_DEVICE_KL830 # 多一個未在 visionA-local 研究範圍的 chip
```
**意涵**
- KL720 有兩個 enum value`KL720` 對應 KDP2 / `KL720_LEGACY` 對應 KDP1、跟我們既有 visionA-local 認知一致
- **KL630/KL730 各只有一個 enum**——支持「KL630/KL730 沒有 legacy/new 雙版本」的假設(前一輪研究 §5.2 推測)
- **KL830 存在於 SDK 但 warrenchen 沒實作**——可能是更新一代的 chip、與本研究範圍無關
### 發現 2`update_kdp_firmware_from_files` 完整簽名(從 `legacy_plus121_runner.py` L46-47 + `update_kl720_firmware.py` L110-120
```python
# ctypes 層級簽名
lib.kp_update_kdp_firmware_from_files.argtypes = [
ctypes.c_void_p, # device_group handle
ctypes.c_char_p, # scpu_path (or None)
ctypes.c_char_p, # ncpu_path (or None)
ctypes.c_bool, # auto_reboot
]
lib.kp_update_kdp_firmware_from_files.restype = ctypes.c_int # 0 = success
# 用法 1先 flash SCPU不 reboot
ret = lib.kp_update_kdp_firmware_from_files(dg, scpu_path.encode("utf-8"), None, False)
# 手動 reset + reconnect
lib.kp_reset_device(dg, 0) # KP_RESET_REBOOT = 0
# 用法 2再 flash NCPU
ret = lib.kp_update_kdp_firmware_from_files(dg, None, ncpu_path.encode("utf-8"), False)
# 用法 3一次 flash 兩個 + auto rebootlegacy_plus121_runner.py L233 走這條)
ret = lib.kp_update_kdp_firmware_from_files(dg, loader_path.encode("utf-8"), None, True)
```
**意涵**
- API 支援獨立 flash SCPU 或 NCPU其中一個 path 傳 None
- `auto_reboot=True` 由 SDK 自己 reset device、caller 不需要再 `reset_device`、但接下來 `disconnect_devices` 可能會回 non-zeroUSB re-enumeration、warrenchen `legacy_plus121_runner.py` L273-277 明示這個情況)
- **Python wrapper `kp.core.update_kdp_firmware_from_files` 是否存在 / 簽名相同**——warrenchen 全部用 ctypes direct call、**沒用 Python wrapper**。這暗示 Python wrapper 可能不存在、或存在但 warrenchen 故意繞過(保有更多 error code 控制)
### 發現 3`KDP_MAGIC_CONNECTION_PASS = 0x1FF55B4F`(從 `update_kl720_firmware.py` L31 + `legacy_plus121_runner.py` L12
```python
KDP_MAGIC_CONNECTION_PASS = 0x1FF55B4F # = 536173391 decimal
# 連到 KDP1 legacy device 的方式(繞過 firmware version check
port_ids = (ctypes.c_int * 1)(port_id)
status = ctypes.c_int(KDP_MAGIC_CONNECTION_PASS)
dg = lib.kp_connect_devices(1, port_ids, ctypes.byref(status))
# status.value 之後會被 SDK 改寫成實際的 connect result code
```
**意涵**
- `kp_connect_devices` 的第三個參數status pointer有**雙向用途**caller 傳入 magic value、SDK 寫回 result code
- visionA-local 既有 bridge.py 用 `kp.core.connect_devices_without_check` 走 Python wrapper不需直接操作 magic——**這兩個 API 可能是同件事的兩種 binding**Python wrapper 內部就傳 magic value
- B 階段擴 KL630/KL730 時、如果碰到「old firmware 連不上」、需要 magic pass、可以複製 warrenchen 這個 pattern
### 發現 4`FirmwareLoadRequest` schema 證明 `.tar` 不被 SDK 接受
```python
# main.py L169-171
class FirmwareLoadRequest(BaseModel):
scpu_path: str
ncpu_path: str
```
**意涵**
- warrenchen 對外暴露的 firmware load API 強制要求兩個 .bin 路徑
- 若 SDK 接受 .tar、warrenchen 早該開 `tar_path: Optional[str]` 欄位
- 這個 schema 是 warrenchen 對 SDK 行為的最終共識——**.tar 不能直接餵、必須先解壓**
### 發現 5warrenchen 沒有 `/firmware/legacy-upgrade/kl630``kl730` endpoint
```python
# main.py 全部 firmware endpoint
@app.post("/firmware/load") # genericcaller 自己給 path
@app.post("/firmware/legacy-plus121/load") # KL720 KDP1 → KDP2 RAM-based
@app.post("/firmware/legacy-upgrade/kl520") # KL520 DFUT 路徑
@app.post("/firmware/legacy-upgrade/kl720") # KL720 DFUT 路徑
# ❌ 沒有 legacy-upgrade/kl630
# ❌ 沒有 legacy-upgrade/kl730
```
**意涵**
- warrenchen 對 KL630/KL730 升降版的設計 gap**不是 SDK 限制、是 warrenchen 沒做**
- 我們做 A 階段PRD AC-FW-3.5)時、不能直接抄 warrenchen 模式——要自己設計 KL630/KL730 升降版流程
- 對 PRD 影響AC-FW-3.5KL630/KL730 升降版)的**參考實作不存在**、設計工時要拉高M9-10 原估 1 人天可能不夠)
### 發現 6visionA-local wheel 三平台版本不一致
```
visiona-local/wheels/macos/KneronPLUS-2.0.0-py3-none-any.whl
visiona-local/wheels/linux/KneronPLUS-2.0.0-py3-none-any.whl
visiona-local/wheels/windows/KneronPLUS-3.1.2-py3-none-any.whl
```
**意涵**
- 目前 visionA-local 在三平台跑的 KneronPLUS API 行為**可能不一致**API 跨主版本可能有 breaking change
- 既有 KL520/KL720 在三平台都能 work 的事實 → 既有 bridge.py 用的 API subset 在 2.0.0 和 3.1.2 都存在
- **但 KL630/KL730 enum 在 2.0.0 是否存在無法靜態確認**
- A 階段PRD AC-FW-3.5)若要在 macOS + Linux 也支援 KL630/KL730 升降版、**必須先升級 macOS/Linux wheel 到 3.1.2**——這會引入 KL520/KL720 regression 風險、必須 M9-13 三平台 E2E 跑過才放心
- **PRD AC-FW-3.5 對三平台一致性的影響**A 階段若維持「只支援 KL520+KL720」、wheel 三平台不一致可暫不處理A 階段若要加 KL630/KL730、wheel 升級是強制前提
### 發現 7既有 visionA-local bridge.py 對 `load_firmware_from_file` 的使用模式
```python
# server/scripts/kneron_bridge.py L760-762 (KL720 KDP1 → load KDP2 to RAM)
kp.core.load_firmware_from_file(
_device_group, scpu_path, ncpu_path
)
```
**意涵**
- 我們既有用的是 **RAM-based load**(每次 connect 都重新 load 到 RAM、不寫 flash
- warrenchen 走兩條路:
- `kp.core.load_firmware_from_file`RAM-based、`firmware_legacy_plus121_load` endpoint 走這條)
- `lib.kp_update_kdp_firmware_from_files`flash-based、permanent、`update_kl720_firmware.py` 走這條)
- **PRD AC-FW-3.1KDP1 → KDP2 升版)的兩種實作策略**
- **策略 RAM-based**(既有 visionA-local 做法):每次 connect 重新 load、不寫 flash、永久效果需 USB 不拔
- **策略 Flash-based**warrenchen `update_kl720_firmware.py` 做法):寫進 flash 永久生效、device reboot 後 product_id 從 0x0200 → 0x0720
- A 階段選哪個策略需要 PM/Architect 在 PRD 中明示——**這是 PRD 缺失**、應該補
---
## 對 PRD AC-FW-3.5 的影響
`feature-firmware-management.md` AC-FW-3.5(推測為 KL630/KL730 升降版)的弱驗證結論:
| 項目 | 弱驗證結論 | 對 AC-FW-3.5 影響 |
|------|----------|------------------|
| SDK enum 存在 | ✅ 3.1.2 確認有2.0.0 待驗 | macOS/Linux 必先升 wheel |
| `update_kdp_firmware_from_files` API 存在 | ✅ 3.1.2 確認 | API 層可用 |
| 對 KL630/KL730 適用 | ⚠️ 無 reference 實作、warrenchen 沒做 | **參考實作為零、設計工時加倍** |
| .tar firmware 處理 | ✅ 走策略 Ybuild time 解壓)即可 | TDD §41 確定方案 |
| 實機驗證 | ❌ 必需 | M9-10 啟動前不能跳過 |
**建議**
- AC-FW-3.5 **不適合放 A 階段 MVP**——理由:
1. 無 warrenchen reference 實作design risk 高)
2. wheel 三平台不一致、A 階段要做 KL630/KL730 升降版必先處理 wheel 升級(額外 1-1.5 人天 + regression 風險)
3. 沒實機根本沒法 verify
- AC-FW-3.5 **延後到 B 階段 M9-10**
- 此時 M9-9KL630/KL730 connect + inference已實機驗過
- wheel 升級決策已根據 M9-6 強驗證結果決定
- 真正能寫 + 測 + 跑
---
## 對 TDD §X.X 的影響
需要立即修改的章節M9-6 弱驗證後就能改、不必等強驗證):
| 檔案 | 章節 | 修改內容 | 優先級 |
|------|------|---------|--------|
| `04-architecture/v2/firmware-management.md` | KneronPLUS wheel 版本 | 註記三平台不一致macOS/Linux 2.0.0、Windows 3.1.2+ A 階段是否升級的決策建議 | 高 |
| `04-architecture/v2/firmware-management.md` | `_resolve_firmware_paths` 設計 | **明示策略 Ybuild time 解壓)為唯一方案**、刪除策略 Z 描述 | 高 |
| `04-architecture/v2/firmware-management.md` | KL520 / KL720 升版策略 | 明示「RAM-based vs Flash-based」兩種方案差異、選定其一 | 高 |
| `04-architecture/v2/firmware-management.md` | KL630/KL730 升降版 | 標 "B 階段才實作、A 階段不開";附 warrenchen 沒做的事實 | 高 |
| `04-architecture/v2/firmware-management.md` | API call list | 補 `kp_connect_devices` + magic_pass 機制(從 warrenchen 確認的 pattern | 中 |
| `04-architecture/research-kl520-fw-management/41-tar-firmware-handling.md` | §4.4 決策樹 | 標策略 Z 已排除、走策略 Y | 高 |
| `04-architecture/research-kl520-fw-management/40-b-phase-kl630-kl730-extension.md` | §3.2 表格 | 填入「弱驗證結果」columnA 類 4 項) | 高 |
| `04-architecture/research-kl520-fw-management/40-b-phase-kl630-kl730-extension.md` | R-FW-8 | 升級為「中度風險」KneronPLUS 3.1.2 enum 確認後、剩 2.0.0 升級 + KL630/KL730 適用性風險) | 中 |
| `04-architecture/research-kl520-fw-management/50-m9-6-sdk-validation.md` | §8.1 / §8.2 / §8.3 | 填弱驗證結果(連結本檔) | 高 |
| `02-prd/features/feature-firmware-management.md` | AC-FW-3.5 | 標 "B 階段才實作、A 階段不開"、附 M9-6 弱驗證理由 | 高 |
---
## 強驗證仍需執行的項目
剩下的、必須等硬體 / Bash 才能驗:
| # | 項目 | 何時做 | 預估工時 | 是否阻塞 A 階段 |
|---|------|--------|---------|---------------|
| A-22.0.0 part | macOS + Linux 跑 `python3 -c "import kp; print(kp.ProductId.KP_DEVICE_KL630)"` | M9-6 強驗證階段(無需實機 dongle、只需 venv | 30 分鐘 | **是**——影響 wheel 升級決策 |
| A-5 | KL630/KL730 dongle + `lib.kp_update_kdp_firmware_from_files` flash 測試 | M9-10 啟動前 | 0.5 天 | 否A 階段不做 KL630/KL730 升降版) |
| A-6 | wheel 2.0.0 → 3.1.2 升級後 KL520+KL720 三平台 E2E | M9-13 | 1 天 | 否(如果 A 階段不升 wheel |
| B-1 | KL630/KL730 USB scan product_id | M9-9 啟動前 | 30 分鐘 | 否 |
| B-2 | KL630/KL730 是否每次 connect 重 load firmware | M9-9 | 1 小時 | 否 |
| B-3 | KL630/KL730 firmware 字串可能值 | M9-9 | 30 分鐘 | 否 |
| C-1 | `.tar` 內容(用 `tar -tvf`、無需實機) | M9-8 啟動前 | 5 分鐘 | 否(策略 Y 已確定) |
**A 階段啟動前必跑(阻塞)**:只有 A-22.0.0 wheel enum 驗證一項、30 分鐘可解。
---
## 給 Orchestrator 的建議
### 1. 弱驗證結論是否足以開始 A 階段開發?
**結論****可以**——但要先:
- 派 backend agent 在 macOS/Linux 跑 30 分鐘的 A-2 強驗證(即 `python3 -c "import kp; print(kp.ProductId.KP_DEVICE_KL630)"`
- 結果有兩個分支:
- **2.0.0 有 KL630/KL730 enum** → 不阻塞 A 階段、可以照原計畫做 KL520+KL720 PRD 範圍
- **2.0.0 沒有 KL630/KL730 enum** → 仍可不阻塞 A 階段A 階段不做 KL630/KL730但 PRD AC-FW-3.5 必須明示「需先升 wheel」
- 派 backend agent 跑 C-1`tar -tvf` inspect5 分鐘可解
### 2. 強驗證執行時機建議
- **立刻可做**無實機A-22.0.0 part+ C-1 → 30 分鐘 + 5 分鐘
- **M9-9 啟動前**B-1 + B-2 + B-3需 KL630/KL730 實機)
- **M9-10 啟動前**A-5需 KL630/KL730 實機跑 flash 測試)
- **M9-13**A-6三平台 wheel 升級 regression
### 3. 哪些 PRD/TDD 內容需要立即修改、哪些可等強驗證
**立即修改(不必等強驗證)**
- TDD `04-architecture/v2/firmware-management.md`
- `_resolve_firmware_paths` 走策略 Y、刪策略 Z
- 註記 wheel 三平台不一致 + A 階段升 wheel 與否決策
- KL630/KL730 升降版章節標「B 階段才實作」
- PRD `02-prd/features/feature-firmware-management.md`
- AC-FW-3.5 明示「B 階段才實作、A 階段不開」+ 理由
- 研究檔 `40-b-phase-kl630-kl730-extension.md` + `41-tar-firmware-handling.md`
- §3.2 / §5.2 / §4.4 填弱驗證結果
- 研究檔 `50-m9-6-sdk-validation.md`
- §8.1 連結本檔
**等強驗證才修改**
- wheel 升級最終決策(等 A-2 + A-6 強驗證)
- KL630/KL730 connect 流程設計細節(等 B-1 + B-2 + B-3
- KL630/KL730 升降版 API 選擇(等 A-5
### 4. 派工建議
- **A-2 強驗證**:派 backend agent、目標檔 `.autoflow/04-architecture/research-kl520-fw-management/55-m9-6-weak-validation-result.md` 補「A-2 強驗證結果」段落
- **C-1 強驗證**:同 backend agent 順手做、5 分鐘
- **TDD/PRD 修改**:派 architect agent做 TDD 修改、Orchestrator 自行修 PRD AC-FW-3.5 標註
---
## 與其他研究檔的關係
| 連結 | 引用內容 |
|------|---------|
| `50-m9-6-sdk-validation.md` §2 | 本檔 §「Unknown 對照表」對應該檔 10 個 unknown |
| `50-m9-6-sdk-validation.md` §8 | 本檔結果應回填到該檔 §8.1 / §8.2 / §8.3(弱驗證部分) |
| `40-b-phase-kl630-kl730-extension.md` §3.2 | 本檔「重點發現 1」確認 KneronPLUS 3.1.2 enum 列表 |
| `40-b-phase-kl630-kl730-extension.md` R-FW-8 | 本檔降低該風險等級(部分 unknown 已解) |
| `41-tar-firmware-handling.md` §4.4 | 本檔「發現 4」+「Unknown 對照表 A-3/A-4」確認策略 Y 唯一可行 |
| `41-tar-firmware-handling.md` §5.1 表格 | 本檔「Unknown 對照表」對應該檔 #1-#10 |
| `feature-firmware-management.md` AC-FW-3.5 | 本檔建議延後到 B 階段 |
| `feature-firmware-management.md` 全表 | 本檔「發現 7」指出 PRD 缺「RAM-based vs Flash-based」明示 |
---
## 執行限制與不確定性聲明
**本弱驗證的限制**
1. **無法直接讀 wheel 內 Python source**——所有結論透過 warrenchen 程式碼間接證據推得
2. **無法跑 Python REPL**——`KP_DEVICE_*` enum 值(整數)未知;只知道**名稱**存在於 SDK
3. **無 Bash**——無法 `unzip` wheel、`tar -tvf` firmware
4. **無實機**——B 類全部 + A-5 + A-6 都需要實機
**可能的錯誤來源**
1. warrenchen 程式碼可能用了**過時 / 不推薦**的 SDK API`KP_DEVICE_KL720_LEGACY` 在更新 SDK 版本可能被 deprecate本驗證假設「warrenchen 用的 API 就是 SDK 該版本支援的 API」
2. 我們既有 visionA-local 用的是 KneronPLUS 2.0.0、warrenchen 是 3.1.2——**所有結論都是基於 3.1.2 的觀察**、2.0.0 行為可能不同
3. ctypes-level `lib.kp_*` 跟 Python `kp.core.*` wrapper 簽名可能不同(極少數情況);本驗證假設兩者語意相同
**修正路徑**上述任何懷疑、M9-6 強驗證階段(有實機 + Bash + REPL都能 5 分鐘內 confirm 或 refute。本弱驗證的價值不在「100% 確定」、在「**剃掉一半 unknown、把 A 階段不阻塞的部分先過、把真的需要硬體的部分留到 M9-9 / M9-10 / M9-13**」。

View File

@ -0,0 +1,425 @@
# M9-6 強驗證結果(本機 macOS、35 分鐘)
> 對應 `50-m9-6-sdk-validation.md` 驗證 A-2 與 C-1
> 補充 `55-m9-6-weak-validation-result.md` 留下的 unknown
> 執行日期2026-05-25
> 執行者Backend Agent
> 平台macOS單機驗證、無 KL630/KL730 實機 dongle
---
## 環境
- **OS**macOSDarwin
- **Python**`Python 3.14.3`system / venv-less
- **本機已找到的 wheel**
- macOS`vendor/wheels/darwin/KneronPLUS-2.0.0-py3-none-any.whl`
- Linux`vendor/wheels/linux/KneronPLUS-2.0.0-py3-none-any.whl`
- Windows`vendor/wheels/windows/KneronPLUS-3.1.2-py3-none-any.whl`
- **驗證方式**:解壓 wheel 到 `/tmp/kp-<platform>-<version>/`、直接 `PYTHONPATH=... python3` import 或 `grep` source code
- **限制**:沒有實機 KL630/KL730 dongle、無法跑 `scan_devices()` / `connect_devices()` 驗證 USB 連線層行為
- **驗證範圍**A-2KL630/KL730 enum 在三個版本 wheel 內的存在性)+ C-1.tar 內容檢查)+ 順手延伸到 firmware API 表面對齊
---
## 驗證 A-2 結果KL630/KL730 enum
### A-2.1 macOS 2.0.0 wheel — `import kp` 實測
**指令**
```bash
mkdir -p /tmp/kp-darwin-2.0.0 && \
python3 -m pip install --target=/tmp/kp-darwin-2.0.0 \
/Users/jimchen/visionA/local-tool/vendor/wheels/darwin/KneronPLUS-2.0.0-py3-none-any.whl --quiet
PYTHONPATH=/tmp/kp-darwin-2.0.0 python3 -c "import kp; ..."
```
**完整輸出**(節錄關鍵段、過濾 IntEnum 內建屬性):
```
=== kp module loaded ===
kp.__version__: N/A
kp file: /tmp/kp-darwin-2.0.0/kp/__init__.py
=== ProductId enum (all) ===
KP_DEVICE_KL520 = 256 (type: ProductId)
KP_DEVICE_KL720 = 1824 (type: ProductId)
KP_DEVICE_KL720_LEGACY = 512 (type: ProductId)
```
**KPEnum.py source 直接證實**
```python
class ProductId(IntEnum):
KP_DEVICE_KL520 = 0x100
KP_DEVICE_KL720 = 0x720
KP_DEVICE_KL720_LEGACY = 0x200
```
**結論**
- ❌ `KP_DEVICE_KL630` **不存在**
- ❌ `KP_DEVICE_KL730` **不存在**
- ✅ `KP_DEVICE_KL520` / `KP_DEVICE_KL720` / `KP_DEVICE_KL720_LEGACY` 存在
### A-2.2 Linux 2.0.0 wheel — source grep檔案結構與 macOS 一致、跳過 import
```bash
$ grep -n "KP_DEVICE_KL" /tmp/kp-linux-2.0.0/kp/KPEnum.py
14: KP_DEVICE_KL520 : int, default=0x100
16: KP_DEVICE_KL720 : int, default=0x720
18: KP_DEVICE_KL720_LEGACY : int, default=0x200
21: KP_DEVICE_KL520 = 0x100
22: KP_DEVICE_KL720 = 0x720
23: KP_DEVICE_KL720_LEGACY = 0x200
```
**結論**:與 macOS 2.0.0 完全一致——只有 3 個 enum、無 KL630/KL730。
### A-2.3 Windows 3.1.2 wheel — source grepwheel 內 .dll 在 macOS 無法 import、但 source 可讀)
```bash
$ grep -n "KP_DEVICE_KL\|KL630\|KL730\|KL830" /tmp/kp-windows-3.1.2/kp/KPEnum.py
20: KP_DEVICE_KL530 : int, default=0x530
22: KP_DEVICE_KL830 : int, default=0x832
24: KP_DEVICE_KL730 : int, default=0x732
26: KP_DEVICE_KL630 : int, default=0x630
28: KP_DEVICE_KL540 : int, default=0x540
31: KP_DEVICE_KL520 = 0x100
32: KP_DEVICE_KL720 = 0x720
33: KP_DEVICE_KL720_LEGACY = 0x200
34: KP_DEVICE_KL530 = 0x530
35: KP_DEVICE_KL830 = 0x832
36: KP_DEVICE_KL730 = 0x732
37: KP_DEVICE_KL630 = 0x630
38: KP_DEVICE_KL540 = 0x540
```
**結論**3.1.2 完整支援 KL520 / KL530 / KL540 / KL630 / **KL730 (0x732)** / KL830 / KL720 / KL720_LEGACY。
### A-2 整體結論
| Wheel | KL520 | KL720 | KL720_LEGACY | KL630 | KL730 | KL530/KL540/KL830 |
|-------|:-:|:-:|:-:|:-:|:-:|:-:|
| **macOS 2.0.0** | ✅ 0x100 | ✅ 0x720 | ✅ 0x200 | ❌ | ❌ | ❌ |
| **Linux 2.0.0** | ✅ 0x100 | ✅ 0x720 | ✅ 0x200 | ❌ | ❌ | ❌ |
| **Windows 3.1.2** | ✅ 0x100 | ✅ 0x720 | ✅ 0x200 | ✅ 0x630 | ✅ **0x732** | ✅ |
### A-2 影響
**1. KL730 product_id 修正(重要新事實)**
- 弱驗證 §「Unknown 對照表 B-1」與 `40-b-phase` §2.1 都假設 KL730 product_id = `0x0730`
- **實測 SDK enumKL730 = `0x732`**(不是 0x730
- KL630 = `0x630`(與弱驗證假設一致)
- 對 driver `_KNOWN_PRODUCTS` map 影響KL730 entry 要改 `0x732`、舊 `0x0730` 假設是錯的
**2. wheel 升級決策(高優先)**
- macOS + Linux 在 2.0.0 都**沒有** KL630/KL730 enum、`import kp; kp.ProductId.KP_DEVICE_KL630` 直接 `AttributeError`
- → A 階段若要做 KL630/KL730 任何事(即使只是 scan/connect、macOS/Linux **必須升 wheel 到 3.1.2**
- → A 階段若維持「只支援 KL520+KL720 不升 wheel」可保持 2.0.0、但 PRD AC-FW-3.5KL630/KL730必須延後到 B 階段
- 弱驗證的「強驗證必跑 → 確認 2.0.0 enum」這個 unknown **本次已解****沒有、必須升 wheel**
**3. PRD AC-FW-3.5 結論進一步收緊**
- 弱驗證建議「AC-FW-3.5 延後到 B 階段」**保持有效**、強驗證確認此判斷正確
- 新增理由「macOS/Linux 2.0.0 wheel 連 enum 都沒有、A 階段做不到」(不只是「沒 reference 實作」)
---
## 驗證 C-1 結果:.tar 內容
### C-1.1 KL630/kp_firmware.tar
```bash
$ ls -la /tmp/web_academy_prototype/local_service_win/firmware/KL630/
-rw-r--r-- 1 jimchen wheel 11 May 24 12:57 VERSION
-rw-r--r-- 1 jimchen wheel 6041600 May 24 12:57 kp_firmware.tar (~5.8 MB)
-rw-r--r-- 1 jimchen wheel 542720 May 24 12:57 kp_loader.tar (~530 KB)
$ cat /tmp/web_academy_prototype/local_service_win/firmware/KL630/VERSION
SDK-v2.5.7
```
**`tar -tvf KL630/kp_firmware.tar`**(節錄):
```
drwxrwxr-x kp_firmware/
-rw-rw-r-- kp_firmware/VERSION (10 bytes)
drwxrwxr-x kp_firmware/bin/
-rw-r--r-- kp_firmware/bin/gt.conf (350 bytes)
-rwxrwxr-x kp_firmware/bin/kp_firmware (22,736 bytes、ELF binary)
-rw-r--r-- kp_firmware/bin/gt (192,228 bytes)
drwxrwxr-x kp_firmware/lib/
-rw-r--r-- kp_firmware/lib/libvmf.so.3.16.0.0 (2,668,076 bytes)
-rw-r--r-- kp_firmware/lib/libvmf_vdec.so.1.0.0.1 (624,048 bytes)
-rw-r--r-- kp_firmware/lib/libfreetype.so.6.13.0 (520,144 bytes)
-rw-r--r-- kp_firmware/lib/libusb-1.0.so.0.3.0 (367,068 bytes)
-rw-r--r-- kp_firmware/lib/libkplus.so.2.1.1.0 (185,488 bytes)
-rw-r--r-- kp_firmware/lib/libusbgx.so.2.0.0 (152,276 bytes)
-rw-r--r-- kp_firmware/lib/librtspsrvr.so.9.2.0.3 (147,764 bytes)
-rw-r--r-- kp_firmware/lib/libconfig.so.9.2.0 (119,836 bytes)
-rw-r--r-- kp_firmware/lib/libvmf_nn.so.2.1.1.1 (201,408 bytes)
-rw-r--r-- kp_firmware/lib/libvmf_nnm.so.1.3.0.0 (114,280 bytes)
... 共 ~80 個檔案、含大量 .so / .a / symlinks
```
**`tar -tvf KL630/kp_loader.tar`**
```
drwxrwxr-x kp_loader/
-rw-r--r-- kp_loader/VERSION
drwxrwxr-x kp_loader/bin/
-rw-r--r-- kp_loader/bin/gt.conf
-rw-r--r-- kp_loader/bin/gt
-rwxrwxr-x kp_loader/bin/kp_loader (20,852 bytes、ELF binary)
drwxrwxr-x kp_loader/lib/
-rw-r--r-- kp_loader/lib/libusbgx.so.2.0.0
-rw-r--r-- kp_loader/lib/libkutils.so.1.0.0.0
-rw-r--r-- kp_loader/lib/libconfig.so.9.2.0
-rw-r--r-- kp_loader/lib/libaio.so.1.0.2
... 共 ~15 個檔案、純粹是精簡 runtime 環境
```
### C-1.2 KL730/kp_firmware.tar
```bash
$ ls -la /tmp/web_academy_prototype/local_service_win/firmware/KL730/
-rw-r--r-- VERSION (11 bytes)
-rw-r--r-- kp_firmware.tar (33,710,080 bytes ≈ 32 MB)
-rw-r--r-- kp_loader.tar (20,295,680 bytes ≈ 19 MB)
$ cat KL730/VERSION
SDK-v1.3.0
```
**`tar -tvf KL730/kp_firmware.tar`**(節錄):
```
drwxrwxr-x kp_firmware/
-rw-rw-r-- kp_firmware/VERSION
drwxrwxr-x kp_firmware/bin/
-rwxrwxr-x kp_firmware/bin/kp_firmware (39,256 bytes、ELF binary)
-rw-r--r-- kp_firmware/bin/gt (224,216 bytes)
drwxrwxr-x kp_firmware/lib/
-rw-r--r-- kp_firmware/lib/lib3a.so.1.3.0.0 (13,941,696 bytes !!!)
-rw-r--r-- kp_firmware/lib/libsqlite3.so.0.8.6 (6,137,248 bytes)
-rw-r--r-- kp_firmware/lib/libvmf.so.1.3.0.0 (3,199,008 bytes)
-rw-r--r-- kp_firmware/lib/libcrypto.so.1.1 (3,036,328 bytes)
-rw-r--r-- kp_firmware/lib/libvdec.so.0.0.0.1 (1,448,128 bytes)
-rw-r--r-- kp_firmware/lib/libcva.so.0.0.0.2 (1,032,584 bytes)
-rw-r--r-- kp_firmware/lib/libfreetype.so.6.13.0 (1,047,856 bytes)
-rw-r--r-- kp_firmware/lib/libvmf_nnm.so.1.3.0.3 (825,344 bytes)
-rw-r--r-- kp_firmware/lib/libssl.so.1.1 (679,192 bytes)
... 含 RTSP / SSL / SQLite / Video Codec / 3A / Thermal / Free type 等
```
**`tar -tvf KL730/kp_loader.tar`**
```
drwxrwxr-x kp_loader/
-rwxrwxr-x kp_loader/bin/kp_loader (34,848 bytes、ELF)
drwxrwxr-x kp_loader/lib/
-rw-rw-r-- kp_loader/lib/lib3a.so.1.3.0.0 (13,941,696 bytes) -- 同上、為何 loader 也帶 13 MB 的 ISP lib
-rw-rw-r-- kp_loader/lib/libcva.so.0.0.0.2 (1,032,584 bytes)
... 約 20 個 .so 檔案、含 ISP / video / freetype / thermal
```
### C-1 結論(重大發現)
**1. .tar 內容**不是**前兩輪研究假設的 `fw_scpu.bin` + `fw_ncpu.bin`**
| 假設(弱驗證 §「Unknown 對照表 C-1」 | 實測 |
|---------------------------------------|------|
| .tar 內含 `fw_scpu.bin` + `fw_ncpu.bin` | ❌ 完全沒有 .bin |
| 可能有 manifest.json / metadata | ❌ 只有純 `VERSION` 字串檔10 bytes、無 manifest |
| 解壓後拿 .bin 路徑餵 SDK | ❌ 沒 .bin 可拿 |
**2. .tar 實際內容是「Linux user-space root filesystem」**
- 一個 ELF binary`kp_firmware``kp_loader`、ARM 架構推測)
- 一堆 shared libraries`.so` + symlinks
- 一個 `gt`(推測是 gateway / debug tool
- 一個 `gt.conf` 配置檔
**3. 對 KL630/KL730 firmware 模型的影響(推論、待 B 階段實機 confirm**
KL520/KL720 模型(既有 visionA-local 認知):
- chip 上跑 **bare-metal / RTOS** firmware
- firmware = 兩個獨立 .binSCPU + NCPU寫 flash 或 RAM-load
- SDK API`load_firmware_from_file(dg, scpu.bin, ncpu.bin)` / `_update_kdp2_firmware_from_files(dg, scpu, ncpu)`
KL630/KL730 模型(從 .tar 內容推論):
- chip 上跑 **完整 Linux**embedded distro
- firmware = root filesystemELF binary + .so libraries + config
- SDK API 應該是**完全不同的機制**——大概率走 USB Mass Storage / NFS boot / file transfer protocol、把整個 rootfs 推上去
- **KneronPLUS 3.1.2 wheel 沒有對應的 .tar firmware update Python API**grep 確認、見下節「附帶發現 1」
**4. 對策略 Y vs Z 的影響**
弱驗證結論:「策略 Z.tar 直接餵 SDK不可行、必走策略 Ybuild time 解壓拿 .bin
**強驗證更新**
- 策略 Z 不可行 ✅ 仍正確
- **策略 Y 不可行 ❌**(沒 .bin 可拿、解壓後是 Linux rootfs、不是兩個 .bin
- → 必須有**策略 X新方案**
- **X.1**:把整個 `kp_firmware.tar` 當不透明 blob 處理、找新 SDK API可能在 3.1.2 wheel 內有 KL630/KL730 專用、或在更新 wheel 版本)
- **X.2**:把 .tar 內某個檔案(`kp_firmware` ELF當「主 firmware」、可能對應 SDK 內部 KDP3 / KDP4 流程
- **X.3**:完全跳過 SDK API、用 USB Mass Storage 自己寫 rootfs最後手段
- **這個發現直接推翻 `41-tar-firmware-handling.md` 的策略 Y 設計、`_resolve_firmware_paths` 走 `extracted/fw_scpu.bin` 路徑**——該路徑根本不存在
**5. KL720 KDP2 vs KL630/KL730 模型差異**
- KL720 KDP2仍是 SCPU+NCPU 兩個 .bin、寫 flash 一次後 product_id 從 0x0200 → 0x0720
- KL630/KL730是「載入 Linux rootfs」、與 KL720 KDP2 模型完全不同代
---
## 附帶發現(順手讀 wheel source 得到)
### 附帶 1KneronPLUS 3.1.2 Python 層**沒有** `update_kdp_firmware_from_files`(弱驗證需要修正)
**弱驗證「發現 2」結論**:「`update_kdp_firmware_from_files` API 在 KneronPLUS 3.1.2 存在且 warrenchen 已使用」
**強驗證更正**
`grep -n "update_kdp" /tmp/kp-windows-3.1.2/kp/KPCore.py`
```
584: def _update_kdp2_firmware(...)
625: def _update_kdp2_firmware_from_file(...)
661: def _update_kdp2_usb_loader(...)
696: def _update_kdp2_usb_loader_from_file(...)
```
`grep -n "kp_update_kdp" /tmp/kp-windows-3.1.2/kp/KPWrapper.py`
```
55: self.__init_kp_update_kdp2_firmware()
56: self.__init_kp_update_kdp2_firmware_from_files()
57: self.__init_kp_update_kdp2_usb_loader()
58: self.__init_kp_update_kdp2_usb_loader_from_file()
```
**事實**
- Python wrapper 層只有 `_update_kdp2_*`**單底線 prefix、表示 advance / non-public** API
- **沒有** `update_kdp_firmware_from_files`(即弱驗證 §「發現 2」描述的 KDP1 → KDP2 升版 API
- warrenchen 用的 `lib.kp_update_kdp_firmware_from_files` 是**直接 ctypes binding 到 .so/.dll 的 C symbol**、繞過 Python wrapper
- 該 C symbol **可能**存在於 SDK .so/.dll 內、但 Python 層**沒對外 export**——意味我們要嘛走 ctypes、要嘛找替代方案
**對 PRD AC-FW-3.1KL720 KDP1 → KDP2 flash 升版)影響**
- 既有 visionA-local 用 `kp.core.load_firmware_from_file`RAM-based— ✅ Python wrapper 都有、跨版本相容
- 若要走 warrenchen 模式做 **flash-based** 升版:必須用 ctypes 直接打 `lib.kp_update_kdp_firmware_from_files`、Python wrapper 沒這個函式
- 這個事實在 TDD 設計 KL720 flash-based 升版時要明示「走 ctypes、不走 Python wrapper」
### 附帶 2`load_firmware_from_file` 簽名跨版本一致
`load_firmware_from_file(device_group, scpu_fw_path, ncpu_fw_path)` 在 2.0.0 與 3.1.2 都是兩個 `str` path 參數、簽名相容、無 .tar 支援、與弱驗證結論一致。
### 附帶 3KneronPLUS 3.1.2 也**沒有任何 .tar 介面 API**
grep 全 wheel
```bash
$ grep -rn "load_firmware_from_tar\|kp_update_kdp.*tar\|tar_path\|from_tar" /tmp/kp-windows-3.1.2/kp/
(no result)
```
完全沒有 .tar 相關 Python API。意味
- KL630/KL730 的 `kp_firmware.tar` 在 KneronPLUS 3.1.2 Python 層**沒有對應 API**
- 推測:要嘛 .tar 是給 SDK 內部的 `legacy_plus121_runner` 之類 helper script 用、要嘛要更新版 wheel、要嘛要走 ctypes
- 這個發現解釋了**為什麼 warrenchen 完全沒有 KL630/KL730 endpoint**——SDK Python 層沒給可用 API
---
## 對 PRD / TDD 的影響
### 立即可修(強驗證結果確認、不需更多實機)
| 文件 | 章節 | 修改內容 |
|------|------|---------|
| `04-architecture/v2/firmware-management.md` | KL730 product_id | 從 `0x0730` 改為 `0x732`SDK enum 實測值) |
| `04-architecture/v2/firmware-management.md` | `_resolve_firmware_paths` | **整段重寫**——.tar 不是兩個 .bin、是 Linux rootfs、沒法走「解壓拿 .bin」路徑 |
| `04-architecture/v2/firmware-management.md` | .tar 處理策略 | 標策略 Y **同樣不可行**、留 placeholder「KL630/KL730 firmware 處理需 M9-9 強驗證才能定案」 |
| `04-architecture/v2/firmware-management.md` | KneronPLUS wheel 升級 | macOS/Linux 2.0.0 確認**無** KL630/KL730 enum、A 階段做 KL630/KL730 必先升 wheel |
| `04-architecture/v2/firmware-management.md` | KL720 KDP1→KDP2 flash 升版 | 標「需走 ctypes 直接打 `lib.kp_update_kdp_firmware_from_files`、Python wrapper 無對應」 |
| `04-architecture/research-kl520-fw-management/41-tar-firmware-handling.md` | §1.3 / §4.4 | **整段重寫**——.tar 是 Linux rootfs、策略 Y 不可行、需新策略 XB 階段定) |
| `04-architecture/research-kl520-fw-management/40-b-phase-kl630-kl730-extension.md` | §2.1 | KL730 product_id 改 `0x732` |
| `04-architecture/research-kl520-fw-management/40-b-phase-kl630-kl730-extension.md` | §3.2 表格 | 填入「A-2 強驗證結果」columnmacOS/Linux 2.0.0 無 enum |
| `04-architecture/research-kl520-fw-management/40-b-phase-kl630-kl730-extension.md` | §5.2 推測「flash-based」 | 改為「embedded Linux rootfs 模型、與 KL520/KL720 不同代」 |
| `04-architecture/research-kl520-fw-management/50-m9-6-sdk-validation.md` | §8.1 A-1/A-2 | 填本檔結果 |
| `04-architecture/research-kl520-fw-management/50-m9-6-sdk-validation.md` | §8.3 C-1 | 填本檔結果 |
| `04-architecture/research-kl520-fw-management/55-m9-6-weak-validation-result.md` | 「結論 2」與「發現 2」 | 標註「update_kdp_firmware_from_files Python wrapper 不存在、僅 C symbol、走 ctypes」 |
| `04-architecture/research-kl520-fw-management/55-m9-6-weak-validation-result.md` | 「結論 4 / 5」 | 標註「策略 Y 同樣不可行、.tar 是 Linux rootfs、需新策略 X」 |
| `02-prd/features/feature-firmware-management.md` | AC-FW-3.5 | 強化「延後到 B 階段」決定、附本檔事實macOS/Linux 2.0.0 enum 不存在、.tar 模型完全不同代)|
### 仍需強驗證的剩餘項目M9-9 起跑前必跑)
| # | 項目 | 阻塞 |
|---|------|------|
| **B-1**(更新) | KL730 product_id = `0x732` 假設用實機 confirmSDK enum 寫 0x732、但實機回什麼仍要驗| M9-9 啟動 |
| **B-2** | KL630/KL730 是否每次 connect 都要 load firmwareembedded Linux 模型下可能完全不需要 load | M9-9 |
| **B-3** | KL630/KL730 firmware 字串可能值 | M9-9 |
| **A-3 / A-4** | 找 KL630/KL730 firmware update 對應的 SDK API可能在 KP 3.1.2+ 有 KDP3 / KDP4 / Linux loader 之類專用、或要更新 wheel| M9-10 啟動 |
| **A-5** | `_update_kdp2_firmware_from_file` 是否對 KL630/KL730 適用、或要走完全不同 API | M9-10 |
| **A-6** | wheel 2.0.0 → 3.1.2 升級對 KL520+KL720 regression | M9-13 |
### A 階段是否阻塞?
**結論****不阻塞 A 階段**A 階段範圍 = KL520+KL720
- A 階段 wheel 維持 macOS/Linux 2.0.0、Windows 3.1.2 三平台不一致現狀
- KL520+KL720 在所有平台都有對應 enum、A 階段所需 APIload_firmware_from_file跨版本相容
- 弱驗證的「KL520+KL720 既有 bridge.py 用的 API subset 在 2.0.0 和 3.1.2 都存在」結論 ✅ 仍有效
- AC-FW-3.5KL630/KL730 升降版)確定延後到 B 階段、A 階段不開
### B 階段 milestone 影響vs 弱驗證評估)
| Milestone | 弱驗證評估 | 強驗證更新 |
|-----------|----------|----------|
| **M9-7B0 認 chip** | 低風險 | **中風險**:需處理 KL730 = 0x732不是 0x0730A 階段 driver `_KNOWN_PRODUCTS` map 維持假設可、但 B 階段必修 |
| **M9-8.tar handling** | 高:策略 Y vs Z | **更高**:策略 Y 也不可行、需 spike 找新 API、工時可能 1 → 2-3 人天 |
| **M9-9connect + inference** | 中B-1/B-2/B-3 | **高**embedded Linux 模型可能改變整個 connect 流程設計 |
| **M9-10FW 升降版擴 KL630/KL730** | 中A-5 | **可能 N/A**:若沒對應 SDK API、整段降為「不支援 KL630/KL730 升降版」、或要找替代方案(如系統廠工具) |
---
## Verification自檢
- ✅ wheel 路徑與版本不是猜——`find` 確認、`vendor/wheels/<platform>/` 結構與 `visiona-local/wheels/<platform>/` 一致、皆找到實檔
- ✅ A-2 enum 實際執行 `python3 -c "import kp; ..."`macOS+ `grep KPEnum.py`Linux/Windows、不是查 doc
- ✅ C-1 用 `tar -tvf` 實際看檔案清單與 byte size、不是看 wheel 文件
- ✅ 附帶 1update_kdp Python wrapper 不存在)用 `grep KPCore.py + KPWrapper.py` 兩處驗證、不是猜
- ✅ KL730 product_id 修正0x730 → 0x732三個來源弱驗證的 `40-b-phase` §2.1 假設、本次 wheel grep、Windows 3.1.2 KPEnum.py 註解)三方對比、明確
- ⚠️ 沒實機 KL630/KL730 dongle、B-1 / B-2 / B-3 連線層行為仍未驗(與計畫一致、本次不在範圍)
---
## 結論摘要
**A-2 強驗證**
- macOS/Linux 2.0.0 wheel **無** KL630/KL730 enum
- Windows 3.1.2 wheel **有**、且 KL730 product_id = **0x732**(非 0x0730
- → wheel 三平台不一致、A 階段做 KL520+KL720 不阻塞B 階段做 KL630/KL730 macOS/Linux 必先升 wheel
**C-1 強驗證**
- KL630/KL730 .tar 是 **Linux rootfs**ELF binary + .so + 配置)、**不是 SCPU+NCPU 兩個 .bin**
- → 弱驗證的「策略 Y解壓後拿 .bin」**不可行**、TDD §41 需重寫
- → KL630/KL730 firmware update 機制與 KL520/KL720 **是不同代設計**、需找新 SDK API
**附帶**
- KneronPLUS 3.1.2 Python wrapper **無** `update_kdp_firmware_from_files`KDP1 升版)、只有 `_update_kdp2_*` 私有 API
- KneronPLUS 3.1.2 Python wrapper **無**任何 .tar 介面 API
- → KL720 KDP1→KDP2 flash 升版若要走 warrenchen 模式必須用 ctypes不能走 Python wrapper
**對 M9-6 弱驗證 unknown 解決度**
| 弱驗證標的 unknown | 強驗證解開? |
|-------------------|-----------|
| 2.0.0 wheel 是否有 KL630/KL730 enum | ✅ 確認**沒有** |
| `update_kdp_firmware_from_files` 對 KL630/KL730 適用性 | ⚠️ 部分——Python wrapper 不存在、要嘛走 ctypes 要嘛找新 API、實機 confirm 仍需 |
| .tar 內容 | ✅ 確認**是 Linux rootfs、不是兩個 .bin** |
| B-1KL630/KL730 product_id 是 0x630 / 0x730| ⚠️ KL630=0x630 SDK enum 證實、KL730 SDK enum = 0x732修正假設、實機回什麼仍需驗 |
**B 階段啟動的新風險**:原本以為策略 Y 解、實際是「整個 firmware 模型不同代、需要 spike」、B 階段工時可能 +2-3 人天。
---
## 與其他研究檔的關係
| 連結 | 引用內容 |
|------|---------|
| `50-m9-6-sdk-validation.md` §2 A-2 + C-1 | 本檔 §「驗證 A-2 結果」+「驗證 C-1 結果」回填 |
| `50-m9-6-sdk-validation.md` §8.1 / §8.3 | 本檔結果應回填到該檔 §8.1A 類)+ §8.3C 類) |
| `55-m9-6-weak-validation-result.md` 「Unknown 對照表」A-2/C-1 | 本檔解開 |
| `55-m9-6-weak-validation-result.md` 「重大發現 5」/「Unknown A-3/A-4」 | 本檔附帶 1 修正Python wrapper 無 KDP1 update API |
| `55-m9-6-weak-validation-result.md` 「結論 4 / 5」 | 本檔 §「C-1 結論」推翻(策略 Y 不可行) |
| `40-b-phase-kl630-kl730-extension.md` §2.1 | 本檔修正 KL730 product_id = 0x732 |
| `40-b-phase-kl630-kl730-extension.md` §5.2 | 本檔修正 KL630/KL730 是 embedded Linux 模型、與 KL520/KL720 不同代 |
| `41-tar-firmware-handling.md` 全文 | 本檔 §「C-1 結論」推翻策略 Y、需重寫 |
| `02-prd/features/feature-firmware-management.md` AC-FW-3.5 | 本檔強化「延後到 B 階段」決定 |

View File

@ -0,0 +1,576 @@
# Architect 互審報告 — PM PRD + Design Specv2.2 Firmware 管理)
> 作者Architect Agent
> 日期2026-05-24
> 任務性質:三方互審 — Architect 視角審 PM PRD + Design Spec
> 對照基準:
> - 我的 TDD`.autoflow/04-architecture/v2/firmware-management.md`
> - 我的 ADR`.autoflow/04-architecture/adr/ADR-001-firmware-management.md`
> - 7 份 research`.autoflow/04-architecture/research-kl520-fw-management/`
---
## TL;DR
整體判斷:**PM PRD 與 Design Spec 技術上都可實現、與我的 TDD 高度一致、沒有架構衝突。** 但有幾個對齊問題需要解決,主要落在三處:
1. **狀態機名稱不對齊**Design `preparing/loading/flashing/verifying` vs TDD `connecting/loading_loader/loading_firmware/verifying`)—— **必須我修 TDD**(採 Design 的命名、對使用者語意較自然)
2. **失敗類型 6 vs 8 不一致**TDD 列 6 stage + 6 錯誤碼、Design 列 8 種失敗類型)—— Design 多列的 `timeout` + `disconnect` 是橫切性失敗、本來就在我的錯誤碼裡(`FW_UPGRADE_FAILED` / `FW_UPGRADE_BRICK_RISK`)—— 補一張對應表即可
3. **降版進行中 graceful shutdown 拒絕**Design §14.4 第 6 點自提懸念)—— 我 TDD 沒設計、**必須補進 TDD §8**
其餘對齊度高、issue 多為「補一句說明」級。M9-6 不卡 M9-1~5、A 階段可以照計畫啟動。
---
## 一、對 PM PRD 的審閱
### 1.1 AC-FW-* 技術可行性逐條檢視
| AC ID | PM 描述 | 技術可行性 | 對應我 TDD 位置 | Issue |
|-------|---------|----------|---------------|-------|
| AC-FW-1.1 | scan 後 FW badge 綠/黃/紅三色 | ✅ 可做 | TDD §3.1 `firmwareIsLegacy` / `firmwareCanUpgrade` / `bundledFirmwareVersion` | 無 |
| AC-FW-1.2 | progress modal 顯示 6 階段 | ⚠️ 名稱不對齊 | TDD §4.3 列 6 stage | **必須修**:見後文 §1.6 |
| AC-FW-1.3 | 升級成功後 5 秒自動關閉 modal + rescan | ✅ 可做 | TDD §5.1 完成 → driver `needsReset=true` → 下次 connect 走完整 reset | 無 |
| AC-FW-1.4 | 失敗顯示明確錯誤類型 + 「複製錯誤訊息」 + 「重新插拔後重試」 | ✅ 可做 | TDD §5.3 列 4 種失敗類型、與 Design §7.1 8 種對應見後文 §2.3 | 無 |
| AC-FW-1.5 | 升級期間 device 進入 `StatusUpgrading` 鎖住其他操作 | ✅ 可做 | TDD §2.2 新增 status enum + §3.3 `FW_DEVICE_BUSY` 409 | 無 |
| AC-FW-1.6 | 升級成功後等 5-8 秒讓 USB stable | ⚠️ 數字略有出入 | TDD §5.1 / §7.2 用 `sleep 2s` + `sleep 3s` 兩段(合計 5s | PM 寫 5-8s、TDD 寫 5s。**PM 區間較保守、TDD 是實測值**。建議 PM 改為「5-8 秒(實測 5s 已穩、保留上界容忍)」對齊 |
| AC-FW-1.7 | KL520 預估 30s、KL720 預估 180s | ✅ 可做 | TDD §3.3 `FW_UPGRADE_FAILED` timeout > 60s、`FW_UPGRADE_BRICK_RISK` timeout > 180s | 無 |
| AC-FW-1.8 | 升級失敗屬 device 層、不觸發 watchServer Error | ✅ 可做 | TDD §8.1 明確 watchServer 不被觸發 | 無 |
| AC-FW-2.1 | 降版入口在 Settings →「韌體管理」 | ✅ 可做、與 Design §2 一致Settings 第 3 分頁) | TDD §5.2 流程進入點 | 無 |
| AC-FW-2.2~2.4 | 卡片 + dropdown + 紅色按鈕 | ✅ 純前端 | — | 無 |
| AC-FW-2.5~2.7 | 二次確認 modal + 警告語 + 不可外部關閉 | ✅ 純前端 | — | 無 |
| AC-FW-2.6 | 強制輸入「DOWNGRADE」字串 | ✅ 可做 | TDD §3.1 / §3.3 `FW_NO_CONFIRM_TOKEN` 400 | 無 |
| AC-FW-2.8 | 進行中不顯示取消按鈕 + persistent banner | ✅ 純前端 | — | 無 |
| AC-FW-2.9 | API 強制 `confirmToken: "DOWNGRADE"` | ✅ 可做 | TDD §3.1 / §3.3 明確規格 | 無 |
| AC-FW-2.10 | Driver 4 項 safety guards | ✅ 可做 | TDD §2.1 `firmware/guards.go` | 無 |
| AC-FW-3.1~3.4 | KL630/KL730 偵測 + 升降版 + ~60s | ✅ 可做M9-7~M9-13 落地)| TDD §6.2 chip 判斷擴展 + §9 M9-7~M9-10 | 無 |
| AC-FW-3.5 | 若 SDK 不支援、降級為 FW 偵測 only | ⚠️ **PM 與 TDD 對齊但缺一條技術判定 trigger** | M9-6 結論後決定 | **詳見 §1.3** |
### 1.2 成功指標PRD §7的 measurement source 對應
| PM 指標 | 我 TDD 是否埋點 | Issue |
|---------|--------------|-------|
| 5 分鐘首次推論達成率 +5pp | ❌ 我 TDD 沒設計埋點 | 不是 FW feature 該埋的、應是既有 `/api/system/metrics` 或前端 analytics 範圍。**建議 PM 在 §7.1 註明「指標 source 待定、跨多個 feature 統計」、不要當 FW feature 的 AC** |
| FW 升級成功率 ≥ 95% | ⚠️ 無自動上報 | TDD §5.1 流程結束會有 `{status:"upgraded", duration_ms}` log、但不會自動上傳。**現階段靠技術支援統計 log、不是自動指標**。建議 PM 在 §7.2 註明「靠 log 統計、非自動上報、待 telemetry feature」 |
| 升降版平均時長 | ⚠️ 同上 | 同上 |
| 一般使用者誤觸降版發生率 ≤ 1% | ❌ 完全無法自動量測 | 純客服回報統計、非自動。PM §7.2 已標「客服回報」、OK |
| Brick 事件 ≤ 0.1% | ❌ 完全無法自動量測 | 純客服回報、PM §7.2 已標、OK |
**結論**PM §7 列的指標**沒有違反技術可行性、但很多依賴客服回報 / 手動統計**。建議 PM 在 §7 開頭加一句「v2.2 階段大部分指標靠客服回報 + log 統計、非自動上報;自動 telemetry 是未來 feature」、避免後續 Reviewer 誤判「指標沒實作」。
### 1.3 AC-FW-3.5 生效條件需要明確
PM 寫「若 KneronPLUS Python wheel 版本不支援 KL630/KL730 的 update API、降級為 FW 偵測 only」、但**沒寫**「不支援」的判定條件是什麼。我 TDD §6.3 / §7.3 描述了 M9-6 SDK 驗證的範圍、但沒明文說「如果 M9-6 跑出什麼結果 → 觸發 AC-FW-3.5」。
**建議補充**PM 或我都可以補):
```
AC-FW-3.5 生效條件M9-6 結論其一):
- 條件 aKneronPLUS Python wheel 沒有 `kp.core.update_kdp_firmware_from_files` 對 product_id 0x0630/0x0730 的 dispatch
- 條件 bSDK 對 .tar firmware 既不支援直接餵、也無法 build-time 解壓出可用 .bin策略 Z 與 Y 都失敗)
- 條件 c升級驗證階段 firmware 字串無法穩定回讀
```
任一條件成立 → AC-FW-3.5 觸發 → B 階段對 KL630/KL730 範圍縮限為「scan + connect + inference + FW 偵測」、移除 upgrade/downgrade UI。
### 1.4 工時對齊M9 表)
| 我 TDD §9 | PM §10 | 對齊度 |
|-----------|--------|-------|
| M9-0 0.5 文件先行 | 沒列 | PM 沒把文件先行算工時、可接受(屬 Orchestrator 範圍)|
| M9-1 1.0 bridge.py upgrade | M9-1 1.0 | ✅ |
| M9-2 1.0 driver + service | M9-2 1.0 | ✅ |
| M9-3 0.5 API + WS | M9-3 0.5 | ✅ |
| M9-4 1.5 Frontend | M9-4 1.5 | ✅ |
| M9-5 1.0 三平台驗證 | M9-5 1.0 | ✅ |
| M9-6 1.0 SDK 驗證 | M9-6 1.0 | ✅(與 A 平行)|
| M9-7 0.5 chip 判斷 | M9-7 1.5 | ⚠️ **PM 寫 1.5、我寫 0.5**。0.5 是只改 detector + bridge dispatch1.5 是含 detector + chip-aware connect 全套(含 KL630/KL730 firmware 載入分支)。**PM 比較保守、可採 PM 數字**(多估留 buffer |
| M9-8 1.5 .tar handling | M9-8 1.0 | ⚠️ PM 偏緊、我偏鬆。建議採我 1.5(含策略 Y 解壓 fallback 風險)|
| M9-9 2.0 KL630/KL730 connect+inference | M9-9 1.0 / M9-10 1.5 | ⚠️ PM 拆成兩個 milestone多版本目錄重整 + KL630/KL730 升降版)= 2.5。我合併成 M9-9 2.0(純 connect+inference+ M9-10 1.0(升版擴展)= 3.0。**兩邊算法不同、需要對齊**。詳見 §1.5 |
| M9-11 1.5 多版本降版 | M9-11 1.5 | ✅ |
| M9-12 2.0 降版 UI | M9-12 2.0 | ✅ |
| M9-13 1.0 B 階段驗證 | M9-13 1.0 | ✅ |
| **A 合計** | 5.0 | 5.0 ✅ |
| **B 合計** | 10.5 | 10.5 ✅ |
| **總計** | **15.5** | **15.5 ✅** |
**結論**A/B/總工時對齊、但中間 M9-7~M9-10 拆法不同。**建議 PM 與我用同一張 M9 表**(採 PM 拆法、語意較清楚)、我下輪修 TDD §9 對齊。
### 1.5 M9-7~M9-10 拆法差異
| Milestone | PM 拆法 | TDD 拆法 | 建議統一 |
|-----------|--------|---------|---------|
| M9-7 | Driver 擴 chip + chip-aware connect 分流1.5 人天)| B0 認 chipdetector + bridge.py + driver0.5 人天 | **採 PM 的 1.5**(含 chip-aware connect、語意清楚|
| M9-8 | .tar handling1.0| B1.1 .tar handling1.5| **採 TDD 的 1.5**M9-6 結論若需策略 Y、解壓 build script 工程更大)|
| M9-9 | 多版本目錄重整1.0| B1.2 KL630/KL730 connect + inference2.0| **不同 milestone**、PM 與 TDD 對齊不到——詳見後文 |
| M9-10 | KL630/KL730 升降版1.5| B1.3 FW 升版擴 KL630/KL7301.0| 同上 |
**根因**PM 把「多版本目錄重整」獨立成 M9-9、TDD 把它合進 M9-11PM 把「KL630/KL730 inference」併進 M9-7、TDD 拆出 M9-9。**兩邊都對、只是拆法不同**。
**建議統一**(採 PM 拆法、語意對外部審閱者更清楚):
- M9-7B0 認 chipdriver + bridge.py + detector1.5 人天
- M9-8B1.1 .tar firmware handling 1.5 人天(採 TDD 數字)
- M9-9多版本目錄重整 + `_resolve_firmware_paths_versioned()` 1.0 人天
- M9-10KL630/KL730 升降版 driver method 1.5 人天
- M9-11多版本降版後端 1.5 人天
- 總和 7.0、+ M9-12 2.0 + M9-13 1.0 = B 階段 10.0、與 PM 10.5 差 0.5M9-9 KL630/KL730 inference 需要併進去?)
**結論**M9 表需要 PM 與 Architect 對一下、最終一張表。我下輪修 TDD 時對齊 PM 的 §10。
### 1.6 R-FW 風險清單對齊
| 我 TDD §10 | PM §8 | 對齊度 |
|------------|-------|-------|
| R-FW-1 升級中拔除KL720 寫 flash brick| R-FW-1 KL520 reset bug 再現 | ⚠️ **語意不同**PM 的 R-FW-1 是「升級後 device re-enumerate 不穩定」、TDD 的 R-FW-1 是「升級中拔 USB brick」 |
| R-FW-2 升級後 USB race | R-FW-2 KneronPLUS Python wheel update API 支援度 | ⚠️ 同上、不對齊 |
| R-FW-3 KL520 已是 KDP2 誤觸發 | R-FW-3 bridge.py `update_kdp_firmware_from_files` macOS x86_64 未驗證 | ⚠️ 同上 |
| R-FW-4 跨晶片誤匹配 | R-FW-4 timeout 設定不合理 | ⚠️ 同上 |
| R-FW-5 Kneron 授權 | R-FW-5 打包 Kneron 官方 firmware 是否合法 | ✅ 同概念、PM 標 P0 release gate與我 ADR §Consequences 對齊)|
| R-FW-6 HTTP timeout | R-FW-6 既有 `flash/` 模組命名混淆 | ⚠️ 同上 |
| R-FW-7 Windows adminWinUSB| R-FW-7 升級失敗 device unknown state | ⚠️ 同上 |
| R-FW-8 SDK 對 KL630/KL730 API | R-FW-8 同 | ✅ |
| R-FW-9 .tar 安裝包衝擊 | R-FW-9 同 | ✅ |
| R-FW-10 KL630/KL730 沒 Loader mode | R-FW-10 同 | ✅ |
| R-FW-11 一般使用者誤觸降版 | R-FW-11 同 | ✅ |
| R-FW-12 多版本管理 UX 複雜度 | R-FW-12 同 | ✅ |
| R-TAR-1~4我 TDD 自己加的 4 條)| — | ❌ **PM 沒列**、應補進 PRD §8 |
**結論**
1. **R-FW-1~7 的編號 PM 與 TDD 完全不對齊**——可能是 PM 引用了 research 30 的早期編號、我 TDD 內部重新編了。**這是重大問題、必須對齊**。
2. **建議統一**:採 PM 的 R-FW-1~7 編號(因為 PRD 已是「對外的」風險清單、跨多個下游 agent 引用)、我下輪修 TDD §10 重新對應。
3. **R-TAR-1~4 PM 沒列**M9-8 .tar handling 的 4 條技術風險(策略 Z 退回 Y / build script 漏跑 / notarization / 跨平台 tarfile—— **PM 應補進 PRD §8** 或同意「這 4 條留 TDD §10 即可、不進 PRD」。建議後者PRD 是給 PM 視角、TAR 風險偏技術細節。
### 1.7 Kneron 授權 P0 懸念對齊
PM PRD §8.1 R-FW-5 標 **P0 release gate**、§9 Q-FW-1 寫「使用者明示先不管、發佈前再評估」。
我 ADR-001 §Consequences 寫「法律 / 簽章授權待釐清、與 R5-B4 同性質、發佈前統一處理、不阻塞開發但 PRD P0 懸念」。
我 ADR-001 §Compliance 標 `[ ] 與 Kneron 取得 firmware redistribution 授權(發佈前必須完成、不阻塞開發)`
**對齊度**:✅ 完全一致。R5-B4 合併處理也與我 ADR §Related 對齊。
### 1.8 scope 邊界對齊
| 範圍 | PM PRD §2.4 / §3 | 我 TDD §1.2 | 對齊度 |
|------|-----------------|-----------|-------|
| 升級到 Kneron 官方 KDP2 標準版本 | ✅ 做 | ✅ 做 | ✅ |
| 使用者燒任意 model 到 device flash | ❌ 不做Q9 維持)| ❌ 不做 | ✅ |
| 使用者選任意 binary 寫 flash | ❌ 不做 | ❌ 不做 | ✅ |
| 偵測 dongle 當前 FW 版本 | ✅ 做 | ✅ 做 | ✅ |
| 切換到內建其他 bundle 版本(含降版)| ✅ 做 | ✅ 做 | ✅ |
| 線上 OTA 更新通道 | ❌ 不做 | ❌ 不做 | ✅ |
| KL530 / KL830 支援 | 未提及 | ❌ 不做(本期)| ⚠️ **PM 沒列「不做 KL530/KL830」**、建議 PM §3 補一條「**本期不做 KL530 / KL830**warrenchen 雲端版雖列出、本期排除)」 |
| DFUT.exe 救磚工具打包 | 未提及 | ❌ 不做 | ⚠️ 同上、PM 應補 |
**結論**scope 大方向一致、但 PM 漏列兩個「不做」KL530/KL830 + DFUT.exe。**建議 PM 補進 §3 範圍切割表**。
### 1.9 B2 一般使用者降版 — driver/bridge guard 對齊
PM AC-FW-2.10 列了 4 項 driver 層 safety guards
1. 拒絕跨晶片誤匹配
2. 拒絕升版偽裝
3. 拒絕 no-op
4. 拒絕 status busy
我 TDD §2.1 / §3.3 對應如下:
1. `firmware/guards.go` 拒絕跨晶片 ✅
2. `firmware/guards.go` 拒絕升版偽裝(`FW_INVALID_DIRECTION` 400
3. `firmware/guards.go` 拒絕 no-op同上錯誤碼
4. `FW_DEVICE_BUSY` 409 ✅
**對齊度**:✅ 完全一致、bridge.py `_validate_downgrade_request` 也對應上。
---
## 二、對 Design Spec 的審閱
### 2.1 §11.2 token 對比比率:誰負責新增 design tokens
Design §11.2 列了 6 個新 component-level tokens`color.fw-badge.*`+ 推導表Light/Dark 各 6 個值)+ 對比比率4.7:1 / 5.2:1 / 4.8:1
**問題**:這些 token 需要寫進 `frontend/src/app/globals.css``:root``[data-theme='dark']` block、不是寫進設計文件就會自動生效。
**架構層判斷**
- Design 文件已明確規格、Frontend 實作時對著抄即可
- 不需要新增到 `spec/07-design-tokens.md`(既有檔不動、避免架構文件膨脹)
- **責任歸 Frontend Agent**M9-4A 階段、Devices 頁 FW badge實作時順手加 6 個 CSS variable
**Dark 模式 `legacy.fg` 用黑底紅對比的驗證**Design 推算 4.8:1、實作時 Frontend 用 contrast checker 驗證(如 https://webaim.org/resources/contrastchecker/)。我**不需要 verify 推算數字**、信任 Design 的 oklch 推算。
**結論**Design §11.2 設計合理、無架構層 issue。**Frontend Agent 在 M9-4 落地時負責 CSS variable 寫入 + 驗證**。
### 2.2 §6.1 「DOWNGRADE」嚴格比對 — 與 TDD §3.1/§3.3 對齊
Design §6.1 規格:
- input `type="text" autocomplete="off" spellcheck="false"`
- 比對方式:`event.target.value === 'DOWNGRADE'`嚴格相等、case-sensitive、不接受小寫/全形/半形/空白)
- 「確認切換」按鈕 disabled 條件input 值不等於字面 `DOWNGRADE`
我 TDD §3.1 / §3.3
- API request body `{version:"v2.1.0", confirmToken:"DOWNGRADE"}`
- API 強制 `confirmToken === "DOWNGRADE"`(字面字串)
- 沒帶 / 帶錯 → 400 + `FW_NO_CONFIRM_TOKEN`
**對齊度**:✅ 前後端規格完全一致。
**Design §14.4 第 4 點懸念(中文 i18n 化)**Design 建議「短期維持英文字串、跨語言一致」。
**我的判斷**:✅ 同意。後端 `confirmToken` 是 API contract、不應 i18n 化(會引入 server 端多語對照表、增加攻擊面)。**保留英文字面 `"DOWNGRADE"`、UI 層用 i18n key 顯示提示文字(「請輸入 DOWNGRADE 確認」中文 + 「Type DOWNGRADE to confirm」英文但 input 比對的字串一致**。
### 2.3 §7.1 8 種失敗 stage vs TDD §3.3 6 種錯誤碼 + §5.3 4 種失敗類型對應
**Design §7.1 列的 8 種**(後端錯誤 stage → 友善訊息):
1. `scan` 找不到裝置
2. `connect` 失敗
3. `loader` 寫入失敗
4. `upgrade` 寫入失敗
5. `verify` 失敗
6. `Timeout (>60s)`
7. `Disconnect during op`
8. (無第 8 項、Design 表格只有 7 列、但 §14.3 表寫「8 種失敗類型對應」、可能是 Design 自己估算誤差)
**我 TDD §3.3 列的 6 個錯誤碼**HTTP 層):
1. `FW_DEVICE_BUSY` 409
2. `FW_VERSION_NOT_FOUND` 404
3. `FW_INVALID_DIRECTION` 400
4. `FW_NO_CONFIRM_TOKEN` 400
5. `FW_UPGRADE_FAILED` 500
6. `FW_UPGRADE_BRICK_RISK` 500
**我 TDD §5.3 列的 4 種失敗類型**(流程失敗、不是 HTTP 錯誤碼):
1. device disconnect during upgrade
2. firmware load 失敗pre-flash
3. firmware load 失敗中段、timeout > 180s
4. 升級成功但 verify 失敗
**對應表**(補進 TDD 與 Design 都需要):
| Design §7.1 後端 stage | 我 TDD §3.3 錯誤碼 | 我 TDD §5.3 失敗類型 | 對齊 |
|----------------------|------------------|------------------|------|
| `scan` 找不到裝置 | `FW_UPGRADE_FAILED` | (1) disconnect | ✅ |
| `connect` 失敗 | `FW_UPGRADE_FAILED` | (1) disconnect | ✅ |
| `loader` 寫入失敗 | `FW_UPGRADE_FAILED` | (2) pre-flash | ✅ |
| `upgrade` 寫入失敗 | `FW_UPGRADE_FAILED``FW_UPGRADE_BRICK_RISK` | (3) 中段 / (2) pre-flash | ✅ |
| `verify` 失敗 | `FW_UPGRADE_FAILED` | (4) verify | ✅ |
| Timeout (>60s) | `FW_UPGRADE_FAILED``FW_UPGRADE_BRICK_RISK` | (3) 中段 | ✅ |
| Disconnect during op | `FW_UPGRADE_BRICK_RISK` | (1) disconnect | ✅ |
**結論**Design 8 種(含 1 種空項)= TDD 6 錯誤碼 + 4 失敗類型的不同切面。**沒有矛盾、只是 granularity 不同**
- Design 是「使用者面友善訊息」的分類(給 i18n 用)
- TDD §3.3 是「HTTP API 錯誤碼」(給 frontend 處理)
- TDD §5.3 是「服務內部失敗分類」(給 bridge.py 處理)
**Issue****我 TDD 必須補一張「stage → 錯誤碼 → 失敗類型」對應表**、讓 Frontend / Testing 不會疑惑「Design 列的 8 種對應後端哪個 code」。下輪修 TDD §3.3 後追加 §3.4「stage 對應 friendly message + 錯誤碼」。
### 2.4 §8 狀態機名稱 vs TDD §4.3 stage 列舉 — **必須對齊**
| Design §8 狀態 | 我 TDD §4.3 stage | 對齊度 |
|--------------|-----------------|-------|
| `idle` | — | OKDesign 用、TDD 是 device status |
| `confirming` | — | OK純前端狀態 |
| `preparing` | `connecting` | ❌ **不對齊**preparing vs connecting|
| `loading` | `loading_loader` | ❌ **不對齊** |
| `flashing` | `loading_firmware` | ❌ **不對齊** |
| `verifying` | `verifying` | ✅ |
| `success_toast` | `done` | ⚠️ 不同切面toast vs stage 完成)|
| `error_modal` | `error` | ⚠️ 同上 |
**根因**Design 命名是 UI 視角preparing 比 connecting 對使用者更友善、TDD 命名是技術視角connecting 對應實際的 USB connect 動作)。
**判斷****Design 的命名對使用者較自然、TDD 應該採 Design 的命名作為 WebSocket event 的 `stage` 值**。理由:
- progress event 的 `stage` 值最終會被 Frontend 用 i18n key lookup`settings.firmware.progress.stage.preparing`)顯示給使用者
- 如果 TDD 用 `connecting` 但 Design 的 i18n key 是 `preparing`、Frontend 需要做 mapping、增加複雜度
- 一致命名讓 Backend / Frontend / Design / Testing 都用同一組詞
**動作****我下輪必須修 TDD §4.3**、stage 列舉改為:
- `connecting``preparing`5%
- `loading_loader``loading`20%、僅 KDP1→KDP2 路徑)
- `loading_firmware``flashing`50%
- `verifying``verifying`90%、不變)
- `done``done`100%、不變)
- `error``error`-1、不變
同時要修 TDD §5.1 流程圖中的 stage 名稱、§6.1 bridge.py handler 回傳的 `stage` 欄位 enum 值。Design §9 i18n keys`settings.firmware.progress.stage.scan/connect/loader/upgrade/verify`)也需要 Design 對應修為 `preparing/loading/flashing/verifying`——**或者 Design 維持原 key 名、我內部用 Design 的命名**——這需要 Design + Architect 對一下。
**建議統一**(給 Orchestrator
- **Option A**TDD 採 Design 的 `preparing/loading/flashing/verifying`、Design 的 i18n key 也改成 `progress.stage.preparing`
- **Option B**兩邊維持各自命名、TDD 在 §4.3 加一張對應表「TDD stage → Design i18n key」
我**建議 Option A**(更乾淨、減少 mapping 心智負擔。Design 互審我提一下這點、看 Design 是否同意改 i18n key。
### 2.5 §14.4 第 6 點:降版進行中 graceful shutdown 拒絕 — **TDD 必須補設計**
Design §14.4 第 6 點自提懸念:
> 「降版進行中關閉 Wails 控制台視窗會發生什麼:依 R5-2 控制台關閉 = 結束 server。降版進行中關控制台 = 中斷降版 = 可能 brick。Frontend / Backend 是否需要在控制台側也偵測『降版進行中』並擋下關閉動作?建議 Architect 補充。」
**我 TDD 目前沒設計這個**。檢視 TDD §8
- §8.1 watchServer Error state 不被觸發 ✅
- §8.2 KL520 reset bug 對齊 ✅
- §8.5 既有 `restartBridge()` 不重用 ✅
- ❌ **沒提「降版中 graceful shutdown」這條**
**Design 提的風險是真實的**
- R5-2 規定「關閉 Wails 視窗 = 結束 server」
- 結束 server = SIGTERM Python sidecar → bridge.py handler 被中斷
- 如果 `handle_firmware_downgrade` 正在 `update_kdp_firmware_from_files` 中段、SIGTERM 中斷 = **device flash 寫到一半就停 = brick 風險**
**設計方向(補進 TDD §8.6**
```
### 8.6 降版進行中 graceful shutdown 拒絕
當 server 收到 SIGTERM使用者關閉 Wails 視窗、或 systemd / Wails close handler 觸發)時:
1. server 進 `shutting_down` state、停止 accept 新 HTTP 連線
2. **檢查 `firmware.Service` 是否有 active task**`StatusUpgrading` / `StatusDowngrading` device
3. 如果有 active task
a. server **拒絕 graceful shutdown**、回 Wails close handler 一個 `firmwareInProgress: true` 訊號
b. Wails 控制台收到後**不關閉視窗**、顯示 modal「韌體切換進行中device X、為避免裝置損毀、無法關閉應用程式。請等待約 {遠端推算 ETA}」
c. modal 不可關閉、不可 dismiss、只能等 firmware task 完成
d. firmware task 完成後、Wails close handler 重試 graceful shutdown
4. 如果沒 active task正常 graceful shutdown既有 7+1s pattern
實作 hook 點:
- `server/internal/firmware/service.go`:新增 `HasActiveTask() bool` method
- Wails close handler`app.go` 或同等close 前先 query server `/api/firmware/status`、如果有 active task 顯示 modal 並阻擋 close
- bridge.pyfirmware handler 內加 signal handler 拒絕 SIGTERM如果有 active task
風險:
- 使用者強制殺 process`kill -9` / 強制關機)仍可能 brick、無法完全防範
- modal 阻擋使用者關 app 可能造成體驗困擾、但 brick 風險高於體驗困擾
```
**結論****我下輪必須補 TDD §8.6 + 在 §9 工時表反映**M9-11 多版本降版後端 + 0.5 人天「graceful shutdown 拒絕」或併入 M9-11。Design 也應在 control-panel.md 補對應 modal 規格。
### 2.6 progress event schema 技術可行性
Design §6.3 進行中 UI 需要的欄位:
- `stage`(階段名稱)
- `percentage`(進度百分比)
- `elapsed`(已耗時)
- `ETA`(預估剩餘時間)
我 TDD §4.2 `FirmwareProgress`
- `Percent int`
- `Stage string`
- `Direction string`
- `Message string`
- `Error string`
**缺**`elapsed``ETA`
**技術可行性**
- `elapsed`:✅ 簡單、service goroutine 記錄 task 啟動 timestamp、每次 push event 算 `time.Since(startTime)`
- `ETA`:⚠️ **複雜**。KneronPLUS C API 沒提供進度回傳(`update_kdp_firmware_from_files` 是 blocking call、percent 是 bridge.py 自己估算(依 stage hardcode。如果 stage 內部進度不可知、ETA 只能根據 stage 對應的「預估時長」算(如 `flashing` 階段預計 30s 內完成)、不準確。
**結論**
- `elapsed` 可以加進 TDD §4.2 ✅
- `ETA` 也可以加、但**值是估算非精確**、Design 應該在 UI 明示「預估剩餘 ~X 秒」Design §9.6 已有 `settings.firmware.progress.estimatedRemaining` key、寫「~{seconds}s remaining」、OK
**動作****我下輪修 TDD §4.2、加 `Elapsed int64` + `ETA int64` 欄位**。
### 2.7 52 個 i18n keys 與 TDD stage 列舉 / 錯誤碼一致性
| Design §9 i18n key 類別 | 對應我 TDD | 一致性 |
|---------------------|-----------|-------|
| §9.1 頁面結構7 個)| 純前端、與 TDD 無關 | ✅ |
| §9.2 裝置卡片11 個)| 與 TDD §3.1 `DeviceInfo` 衍生欄位對齊 | ✅ |
| §9.3 版本切換 accordion11 個)| 純前端、TDD §5.2 流程進入點 | ✅ |
| §9.4 升級確認 modal6 個)| 純前端 | ✅ |
| §9.5 降版二次確認 modal13 個)| 與 TDD §3.1/§3.3 `confirmToken` 對齊 | ✅ |
| §9.6 進度 modal10 個)| ⚠️ **stage 名稱不對齊**(見 §2.4| **修 §2.4 stage 命名同時對齊**|
| §9.7 成功 toast5 個)| 純前端 | ✅ |
| §9.8 失敗訊息13 個)| 與 TDD §5.3 失敗類型對應、但對應表缺(見 §2.3| **TDD 補對應表後一致** |
| §9.9 Devices 頁 FW badge tooltip4 個)| 與 TDD §3.1 `firmwareIsLegacy` 對齊 | ✅ |
**結論**52 keys 大部分與 TDD 一致。**只有 §9.6 進度 stage 命名需要在 §2.4 對齊後一併修**。
### 2.8 分頁順序「韌體」在硬體與模型之間
Design §2 把 Settings 從 4 變 5 分頁、新分頁「韌體」插在第 3 位(一般 / 硬體 / **韌體** / 模型 / 進階)。
**架構層影響評估**
- **IPC 路由 / API 路徑**:✅ 無影響。我 TDD §3.1 API 路徑用 `/api/devices/:id/firmware/*`、與 Settings UI 分頁順序無關
- **i18n key namespace**:✅ 無影響。Design 用 `settings.firmware.*`、與分頁順序無關
- **既有 `settings-update.md` 的 4 分頁結構**:⚠️ 需要更新——Design §14.1 提到「settings-update.md 應在下次補丁加註『分頁順序見 firmware-management.md §2』」、但**沒有改 settings-update.md 本身**。建議 Design 在 v2.2 同步小修 settings-update.md一行註解即可
**結論**:架構層**無反對**。分頁順序純 UI 決策、Design 已論證合理(硬體 → 韌體 → 模型 由低階到高階)。
### 2.9 失敗復原 UX 的 backend 配合
Design §7.1 列的 retry / re-plug / 取得協助按鈕:
| Design 按鈕 | Backend 配合 | 是否需新 API |
|-----------|------------|------------|
| 「重新插拔後重試」 | ✅ 既有 rescan + 重 POST upgrade | ❌ 不需新 API |
| 「重試」 | ✅ 既有 POST upgrade同一 device| ❌ 不需新 API |
| 「拔插後重試」 | ✅ 同上 | ❌ 不需新 API |
| 「拔插後重新掃描」 | ✅ 既有 POST /api/devices/scan | ❌ 不需新 API |
| 「複製錯誤訊息」 | ⚠️ 需 backend 回傳完整錯誤上下文 | **需新增** |
| 「取得協助」 | ⚠️ 純前端連結i18n key| ❌ 不需新 API |
**「複製錯誤訊息」的 backend 對齊**
- Design §7.2 失敗 modal 內展開「技術資訊」、顯示 `stage / device / before / raw_error / duration_ms`
- 我 TDD §4.2 `FirmwareProgress.Error string`、§6.1 bridge.py 失敗回傳 `{error:str, stage:"..."}`
- **缺**`before`(升級前的 firmware 字串)、`duration_ms``device_id` 在 error event 中
**動作****我下輪修 TDD §4.2、`FirmwareProgress` 在 error event 中應包含完整失敗 context**
```
type FirmwareProgress struct {
Percent int `json:"percent"`
Stage string `json:"stage"`
Direction string `json:"direction"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
Elapsed int64 `json:"elapsed_ms,omitempty"`
ETA int64 `json:"eta_ms,omitempty"`
// 失敗時補
DeviceID string `json:"device_id,omitempty"`
BeforeVer string `json:"before_version,omitempty"`
RawError string `json:"raw_error,omitempty"` // bridge.py 拋的原始 exception text
ErrorCode string `json:"error_code,omitempty"` // 如 "fw_upgrade_stage_loader_E102"
}
```
### 2.10 R-FW-11.5 / R-FW-11.6 自提風險 — 是否併入 TDD §10
Design §12.2 / §12.3 自提兩個風險:
- **R-FW-11.5**使用者輸入「downgrade」小寫繞過 — Frontend 用嚴格 `===` 比對、不用 `.toUpperCase()`
- **R-FW-11.6**deep-link 跳轉自動展開 accordion 暗示降版 — 改為只 highlight 卡片邊框、不展開
**架構層判斷**
- **R-FW-11.5**:這是 **Frontend 實作細節**、不是架構層風險。我 TDD §3.1 已要求 backend `confirmToken === "DOWNGRADE"` 嚴格比對(雙重保險)。**不需要進 TDD §10 風險清單**
- **R-FW-11.6**:純 UX 設計、與架構無關。**不需要進 TDD §10**
**結論**:兩個風險都 Design 內部處理、不併入 TDD §10。**但建議 PM PRD §8 補一條 R-FW-11.5Frontend 嚴格比對)**、明文要求 Reviewer 在 M9-12 檢查 Frontend 是否用了 `===` 而非 `.toUpperCase()`
---
## 三、我自己 TDD 自查
審完 PM/Design 後、回頭看我的 TDD、發現以下需要下輪修改
### 3.1 必修項(影響跨 agent 對齊)
| # | TDD 章節 | 修什麼 | 來源 |
|---|--------|-------|------|
| F1 | §4.3 Stage 列舉 | 改名對齊 Design`connecting → preparing` / `loading_loader → loading` / `loading_firmware → flashing` | §2.4 |
| F2 | §3.4(新增) | 加「stage → 錯誤碼 → 失敗類型」對應表(給 Frontend / Testing 用)| §2.3 |
| F3 | §4.2 `FirmwareProgress` | 補 `Elapsed int64` + `ETA int64` + 失敗時補 `DeviceID / BeforeVer / RawError / ErrorCode` | §2.6 / §2.9 |
| F4 | §8.6(新增) | 補「降版進行中 graceful shutdown 拒絕」設計HasActiveTask + Wails close handler 攔截)| §2.5 |
| F5 | §9 工時表 | 對齊 PM 的 M9-7~M9-10 拆法(採 PM 拆法)| §1.5 |
| F6 | §10 R-FW 風險編號 | 對齊 PM 的 R-FW-1~7 編號(採 PM 編號) | §1.6 |
| F7 | §5.1 / §6.1 | stage 名稱對齊 F1包含 bridge.py handler 回傳的 stage enum 值) | §2.4 連動 |
### 3.2 建議修但非必要
| # | TDD 章節 | 修什麼 | 來源 |
|---|--------|-------|------|
| O1 | §1.2 範圍邊界 | 補一條「不做 KL530 / KL830」雖然 1.2 寫了「KL530 / KL830 不做」、但 PM PRD §3 沒列、可以兩邊對齊一下)| §1.8 |
| O2 | §10 R-TAR-1~4 | PM 沒列、保留 TDD §10 即可PM 同意的話)| §1.6 |
| O3 | §11.2 整合測試表 | 補「升級期間關 Wails 視窗」測試案例(驗證 §8.6 graceful shutdown 拒絕)| §2.5 連動 |
### 3.3 不修項(自查後仍然成立)
| # | 之前提的 5 個 PM 互審注意 | 是否仍成立 |
|---|---------------------|---------|
| 1 | §1.2 範圍邊界 R5-Q9 翻案 | ✅ 仍成立、PM PRD §2 完全對齊 |
| 2 | §8.4 R5-B4 授權對齊 | ✅ 仍成立、PM PRD §8.1 R-FW-5 標 P0 |
| 3 | §9 工時 15.5 對齊 | ✅ 總工時對齊、只是 M9-7~M9-10 拆法不同F5 修)|
| 4 | §11.4 回歸測試 | ✅ PM PRD 沒寫測試細節、TDD 自帶 OK |
| — | — | — |
| # | 之前提的 5 個 Design 互審注意 | 是否仍成立 |
|---|---------------------------|---------|
| 1 | §5.2 降版「DOWNGRADE」字面輸入 | ✅ Design §6.1 已落地 |
| 2 | §3.3 錯誤碼 + §5.3 失敗復原文案 | ✅ Design §7.1 / §9.8 已落地、但需 F2 補對應表 |
| 3 | §4.3 Stage 列舉 i18n keys | ⚠️ **需 F1 修 stage 命名後對齊** |
| 4 | §10 R-FW-11/12 UI 落地 | ✅ Design §12.1 已落地、多層 safety net 完整 |
| 5 | §4.4 多版本目錄 A → B2 migration | ✅ Design 沒涉及、TDD §4.5 自帶 OK |
---
## 四、結論
### 4.1 PM PRD通過 — 需小修
| 嚴重度 | Issue | 建議 |
|-------|------|------|
| Major | M9 工時表 M9-7~M9-10 拆法與 TDD 不對齊 | 對齊一張表(採 PM 拆法)|
| Major | R-FW-1~7 編號與 TDD 不對齊 | 對齊(採 PM 編號)|
| Minor | AC-FW-3.5 生效條件不明 | 補 a/b/c 條件(見 §1.3|
| Minor | §3 scope 邊界漏列「不做 KL530/KL830 + DFUT.exe」 | PM 補 1 條 |
| Minor | §7 成功指標 measurement source 模糊 | PM 在 §7 開頭補一句「靠客服回報 + log、非自動 telemetry」|
| Minor | AC-FW-1.6 USB stable 等待時間 5-8s vs TDD 5s | PM 改為「5-8 秒(實測 5s 已穩、保留上界容忍)」|
### 4.2 Design Spec通過 — 需小修
| 嚴重度 | Issue | 建議 |
|-------|------|------|
| Major | §8 狀態機名稱與 TDD §4.3 stage 不對齊 | 共識採 Design 命名preparing / loading / flashing / verifying、TDD 修 stage、Design i18n key 也對應修 |
| Major | §14.4 第 6 點 graceful shutdown 拒絕 | Design 提出問題、TDD 補 §8.6 落地 |
| Minor | §11.2 token 對比比率 Dark legacy.fg | 信任 Design 推算、Frontend M9-4 實作時 verify |
| Minor | §7.1 8 種失敗 stage 對應不清晰 | Design + TDD 共同補對應表(給 Frontend / Testing 用)|
| Minor | progress event 缺 `elapsed` / `ETA` | TDD §4.2 補欄位 |
| Minor | settings-update.md 沒更新分頁順序 | Design 在 v2.2 同步小修一行 |
### 4.3 我自己 TDD需修 7 項必修 + 3 項建議
詳見 §3.1 / §3.2。**我下輪修 TDD**、目標:
- F1-F7 全修(必修)
- O1-O3 視情況修(建議)
- ADR-001 不需修(仍然 Accepted、決策邏輯沒變
### 4.4 整體判斷
- **架構層**:✅ 通過、PM/Design/Architect 三方在 R5-Q9 翻案、A+B 範圍、+7MB 安裝包、CURRENT_VERSION 多版本目錄、二次確認 DOWNGRADE 字串、watchServer 解耦等核心架構決策完全對齊
- **介面層**:⚠️ 需修 F1stage 命名)+ F2/F3progress event schema 補欄位)+ F4graceful shutdown 拒絕)
- **工時 / 風險**:⚠️ 需修 F5M9 表)+ F6R-FW 編號)
- **M9-6 SDK 驗證**:不卡 A 階段、M9-1~5 可以照計畫啟動
---
## 五、給 Orchestrator 的建議
### 5.1 互審後的處理順序(建議)
```
1. Orchestrator 收齊三方互審報告PM 審 Design+Architect、Design 審 PM+Architect、本報告 = Architect 審 PM+Design
2. 跨報告比對、識別「共識點」「分歧點」
3. 對齊以下幾個關鍵點:
a. M9 工時表(採 PM 拆法、總和 15.5 不變)
b. R-FW 編號(採 PM 編號)
c. stage 命名(採 Design 命名preparing / loading / flashing / verifying
d. graceful shutdown 拒絕(補 TDD §8.6 + Design control-panel.md 補 modal
4. 確認分歧後、派 Architect 修 TDDF1-F7 + 視情況 O1-O3
5. 派 PM 微修 PRD§1.3 + §3 scope 補 + §7 measurement 註解 + §10 表 M9-7~M9-10 拆法 + R-FW-1~7 對齊)
6. 派 Design 微修§7 失敗對應表 + settings-update.md 一行 + 同意 stage 命名)
7. 第二輪互審(小規模、只審修改處)
8. 通過 → 進 M9-1 / M9-6 開發
```
### 5.2 不需要 Orchestrator 額外裁決的事
| Item | 為何不需裁決 |
|------|----------|
| token 對比比率 | Frontend M9-4 實作時驗證、Design 推算先信任 |
| R-FW-11.5 / R-FW-11.6 自提風險 | Design 內部處理、不進 TDD §10 |
| KL530/KL830 不做 | 三方都已隱性同意、PM 補一行即可 |
| Confirm token 「DOWNGRADE」未來 i18n 化 | 短期維持英文、Architect + Design 已同意(不分語系)|
### 5.3 需要 Orchestrator 裁決的事
| Item | 選項 |
|------|-----|
| stage 命名最終採哪一邊 | A. 採 Designpreparing / loading / flashing / verifying✅ 建議B. 採 TDDconnecting / loading_loader / loading_firmware / verifying|
| M9 表最終採哪一邊拆法 | A. 採 PM 拆法 ✅ 建議B. 採 TDD 拆法C. 折衷 |
| graceful shutdown 拒絕的 modal | Design 補 control-panel.md / Architect 補 TDD §8.6 / 工時影響(建議併入 M9-11|
---
## 變更紀錄
| 日期 | 版本 | 變更 | 作者 |
|------|------|------|------|
| 2026-05-24 | v1.0 | 初稿、Architect 視角審 PM PRD + Design Spec、識別 F1-F7 必修項 + 6 個 PM Minor + 6 個 Design Minor | Architect Agent |

View File

@ -0,0 +1,823 @@
# 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[].firmwareVer / 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-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 |

View File

@ -1,9 +1,183 @@
# 專案進度 — visionA-local
## 目的:全新專案(從 edge-ai-platform 衍生的 local 版本)
## 當前階段:🔴 **第一階段回溯** — L 級重大方向變更Wails 內嵌 → Wails 控制台 + 瀏覽器 Web UI
## 當前狀態:✅ 使用者決策全部收齊R5 第五輪決策),待三方產出正式 PRD v2 / Design Spec v2 / TDD v2
## 最後更新2026-04-21
## 當前階段:🟡 **M9 — Kneron Dongle FW 偵測 + 升降版**L 級新功能、翻案 R5-Q9
## 當前狀態:✅ Architect 研究 plan 完成、使用者拍板方案 A + B、待補 PRD v2.2 / Design Spec v2.2 / TDD v2.2 + ADR-009 → 之後 KL630/KL730 driver 擴展需另一份研究
## 最後更新2026-05-24
## 2026-05-24 M9 啟動Kneron Dongle FW 偵測 + 升降版
### 背景
使用者要在 visionA-local 加 Kneron Dongle FW 偵測 + 升降版功能。同事 warrenchen 在 `gitea/warrenchen/web_academy_prototype/local_service_win` 有可參考實作。同事是雲端網頁 + 本地服務雙進程版本、我們是單進程 Wails、不需要那 7 組 RPC / 自訂 URL scheme / 引導下載安裝本地代理。
### 重要決策2026-05-24
- **採方案 A + B一次做完+ L 級正規流程**
- 方案 AMVP5 人天KL520 + KL720 自動升級 KDP1 → KDP2、安裝包 +0KB
- 方案 B追加5-7 人天):手動降版 + KL630 + KL730、安裝包 +5MB、需先擴 driver
- 合計 ~10-12 人天
- **翻案 R5-Q9**progress.md L776 第二輪 Q9「韌體燒錄 flash → B 砍掉」):
- 當時砍的是「使用者按按鈕燒 model 到 device flash」、不是 FW 升降版
- 現在要做的是「升級到 Kneron 官方 KDP2 標準版本」、範圍縮小避開大部分原始擔憂
- 將透過 ADR-009-firmware-management 留下決策痕跡
- **跨平台用 KneronPLUS C API、不用 DFUT.exe**Windows-only / 30MB 依賴)
- **flash/ vs firmware/ 模組分離**:既有 `flash/` 保持「load model 到 device RAM」原意、新建 `firmware/` 模組做升降版
### Architect 研究產出(已完成 2026-05-24
- `.autoflow/04-architecture/research-kl520-fw-management/00-research-summary.md`184 行、執行摘要)
- `.autoflow/04-architecture/research-kl520-fw-management/10-warrenchen-impl-analysis.md`259 行、warrenchen 實作分析)
- `.autoflow/04-architecture/research-kl520-fw-management/20-our-current-state.md`292 行、現有 code 盤點)
- `.autoflow/04-architecture/research-kl520-fw-management/30-integration-plan.md`662 行、MVP milestone + 風險清單)
### 關鍵技術發現
1. visionA-local 既有 code 已有 70% 的 FW 偵測 + RAM-load 邏輯bridge.py 已 `kp.core.load_firmware_from_file`
2. 缺的是「持久化升降版」(既有是 RAM-load、要呼叫 `kp_update_kdp_firmware_from_files` 寫 flash
3. KL520/KL720 firmware 已 bundle 在 `server/scripts/firmware/{KL520,KL720}/`~360KB
4. KL630/KL730 driver 現況沒處理 product_id0x0630 / 0x0730+ firmware 是 .tar 不是 .bin、要先擴 driver 才能加 FW 支援
### 與既有架構的銜接點
1. watchServer Error stateFW 升級失敗是 device 層、不應觸發 server Error
2. KL520 reset bug2026-04-21升級成功後 device re-enumerate、要等 5-8 秒 + 主動 rescan
3. R5-E 60s 啟動上限FW 升級是使用者主動觸發、不在啟動 pipeline、無關但升級本身 30-180s、UI 要 progress bar
### B 階段研究完成2026-05-24 第二輪)
- ✅ `.autoflow/04-architecture/research-kl520-fw-management/40-b-phase-kl630-kl730-extension.md`703 行)
- ✅ `.autoflow/04-architecture/research-kl520-fw-management/41-tar-firmware-handling.md`458 行)
- ✅ `.autoflow/04-architecture/research-kl520-fw-management/42-manual-downgrade-for-end-users.md`551 行)
### B 階段新發現
- warrenchen 沒做 KL630/KL730 FW 升降版、只 bundle firmware tar、沒實作流程
- KL630/KL730 連線流程跟 KL520/KL720 不同、bridge.py `handle_connect()` 對它們 fall-through 到 KL520會連不上
- .tar firmware 處理是 unknown 區、3 候選策略待 SDK 驗證
- 工時更新A 5 + B **10.5** = **15.5 人天**(原估 11-12、增加因「面向一般使用者」UX safety net + 多版本管理 + SDK 驗證)
### B 階段 4 個使用者決策2026-05-24
- **多版本目錄結構**:選 **C — CURRENT_VERSION metadata**`firmware/<chip>/{v2.2.0,v2.1.0,kdp1}/` + `CURRENT_VERSION` 單行檔;架構乾淨、跨平台無 symlink 風險)
- **Bundle 策略****保守策略 +7MB**KL520 提供 kdp1 + v2.1.0 兩個降版、KL720/KL630/KL730 current + 1 個舊版如有)
- **Kneron firmware redistribution 授權****先不管授權**(風險:發佈前可能要重新評估、與 R5-B4 同性質問題)
- **M9-6 SDK 驗證時機****跟 A 階段平行**(不拖完整進度)
### 翻案 R5-Q9 確認
- 透過 ADR-009-firmware-management 留紀錄
- 把「使用者按按鈕燒任意 model 到 device flash」與「升級到 Kneron 官方 KDP2」範圍切開
- 詳見 `00-research-summary.md` §1 R5-Q9 翻案分析
### 安裝包大小衝擊(最終)
- A 階段:+0KBKL520/KL720 firmware 已 bundle
- B 階段:+7MB保守策略 = KL520 +2 版降版 + KL720/KL630/KL730 各 +1 舊版)
- macOS dmg 預估從 163MB → **~170MB**
### 下一步L 級正規流程、三線平行)
**Track 1A 階段文件補寫)**
- [x] **PM PRD v2.2 完成**2026-05-24
- `.autoflow/02-prd/features/feature-firmware-management.md`523 行、14 章節)
- `.autoflow/02-prd/PRD-v2.md` 升 v2.2492 行、淨減 8、留 8 行 buffer / 未破 500
- Q9 翻案紀錄§2+ 範圍切割§3+ 4 個 US-FW user story§4+ user research 假設§5+ 成功指標含 Brick < 0.1%§7+ R-FW-1~12 風險§8+ Q-FW-1~7 未解問題§9+ M9-1~13 工時表§10
- **14 個待互審項**D-FW-1~6 給 Design / A-FW-1~6 給 Architect / O-FW-1~2 給 Orchestrator
- **最大不確定性 A-FW-4 / O-FW-2**KneronPLUS Python wheel 對 KL630/KL730 update API 支援度 → 取決於 M9-6 SDK 驗證、可能讓 AC-FW-3.5「降級為 FW 偵測 only」生效
- **PRD-v2.md 已卡上限**v2.3+ 任何補丁都會破 500、PM 建議考慮 PRD 模組化拆分
- [x] **PM 修改完成**2026-05-25
- PRD-v2.md492 → 495 行buffer 5、未破 500
- feature-firmware-management.md523 → 599 行(+76
- MJ-A1 ADR 編號 ADR-009→ADR-001 全 grep replace 完成
- MJ-A2 R5-Q9 行號改描述式引用
- MJ-D1 降版 framing 對齊 Design 中性立場
- MJ-D2 US-FW-2 拆 US-FW-2a 一般使用者 / US-FW-2b 進階使用者
- A-MID-1 graceful shutdown → 補 AC-FW-1.9
- P-MID-1 體驗指標 → 補 §7.2.1 體驗指標 5 條(完成率 / 中途放棄率 / 重試率 / NPS / 客服問詢率)
- 新增 R-FW-13wheel 三平台版本不一致 P1
- M9-0 補 35 分鐘強驗證 + AC-FW-3.5 延後 B 階段 + M9-8 策略 Y 註明
- 屬於 Architect 的 Minor 全標「⏳ Architect 處理中」、不擅自修 TDD
- **給 Orchestrator 追蹤**O-FW-1R5-B4 release blocker 合併追蹤)+ O-FW-2M9-10 啟動時評估 AC-FW-3.5 生效條件)
- [x] **Architect TDD v2.2 + ADR-001 完成**2026-05-24
- `.autoflow/04-architecture/v2/firmware-management.md`466 行、< 500 不拆檔
- `.autoflow/04-architecture/adr/ADR-001-firmware-management.md`152 行、Accepted
- `.autoflow/04-architecture/TDD-v2.md` 版本 v2.1 → v2.2、§0.0 差異速覽 + §2.10 子檔索引 + §2.11 ADR 索引 + §3 R-FW 風險引用 + §4 審閱紀錄
- ADR-001 編號正確adr/ 目錄掃描空、不撞號)
- 工時量化 15.5 人天M9-1=1.0 / M9-2=1.0 / M9-3=0.5 / M9-4=1.5 / M9-5=1.0 / M9-6=1.0 / M9-7=0.5 / M9-8=1.5 / M9-9=2.0 / M9-10=1.0 / M9-11=1.5 / M9-12=2.0 / M9-13=1.0
- **5 個給 PM 互審注意**Q9 範圍切割 / R5-B4 授權 P0 懸念 / 工時 +安裝包對下載率 / Error 15 回歸 AC / B2 一般使用者面板)
- **5 個給 Design 互審注意**DOWNGRADE 嚴格輸入框 / 6 錯誤碼 + 4 失敗類型友善文案 / Stage 6 個 × 2 方向 i18n / R-FW-11/12 UI safety net 全落地 / 版本字串對齊 CURRENT_VERSION
- [x] **Architect 修改完成**2026-05-25
- `v2/firmware-management.md`604 → 823 行(+219
- `adr/ADR-001-firmware-management.md`210 → 218 行(+8
- `TDD-v2.md` 索引202 → 217 行(+15
- 7 必修 F1-F7 全完成stage 命名 / 對應表 / FirmwareProgress 補欄位 / graceful shutdown §8.6 / 工時採 PM 拆法 / R-FW 編號採 PM / bridge.py handler 同步)
- 3 建議 O1-O3 全完成KL530/KL830 不做 / R-TAR TDD-only / 升級期間關 Wails 視窗測試)
- MJ-A1 ADR-009→ADR-001Architect 管轄 4 檔已修
- MJ-A2 R5-Q9 行號改描述式
- §3.4 完整 stage→錯誤碼→Reason→i18n 對應表
- §8.6 graceful shutdown 拒絕設計server lock + Wails force-quit modal+ 工時併 M9-11
- [x] **Design v2.2 完成**2026-05-24`.autoflow/03-design/v2/firmware-management.md`920 行、明示不拆理由)+ `design-spec-v2.md` 索引更新
- 5 分頁結構Settings 新增「韌體」插在硬體與模型之間)
- Devices 頁 card FW badge綠/黃/紅)+ deep-link icon
- 二次確認 modal輸入「DOWNGRADE」嚴格比對
- 52 個 i18n keys中英雙語
- 6 個新 design tokens
- R-FW-11.5 / R-FW-11.6 自提風險Design 視角)
- **6 個給 Architect 互審待確認**token 對比 / DOWNGRADE 字串 / 8 種失敗 stage / 狀態機名稱 / 降版中拒 graceful shutdown / 分頁順序)
- **4 個給 PM 互審待確認**Q9 翻案章節 / 進階使用者 user story / 用語一致性 / 成功指標 Brick &lt; 0.1%
- [ ] 三方互審(進行中)
- [x] **PM 互審報告完成**2026-05-24`.autoflow/02-prd/reviews/pm-review-of-tdd-and-design-v2.2-firmware.md`~250 行)
- 結論:**通過 with Major 修正**
- 3 MajorMJ-A1 ADR 編號PRD 寫 ADR-009 vs 實檔 ADR-001 不一致)/ MJ-A2 R5-Q9 行號 cross-checkL776 vs L854/ MJ-D1+D2「降版」用詞 framing + US-FW-2 拆分涵蓋進階使用者
- 11 MinorArchitect 6 + Design 5
- 修改工作量Architect 0.2 + PM 0.55 + Design 0.1 人天
- [x] **Design 互審報告完成**2026-05-24`.autoflow/03-design/reviews/design-review-of-prd-and-tdd-v2.2-firmware.md`
- 結論:🟢 整體一致、可進 M9-12 Frontend、但有 2 個 P0 必解
- **P0 A-MISMATCH-1 狀態機名稱不一致**Design 用 preparing/loading/flashing/verifying、Architect 用 connecting/loading_loader/loading_firmware/verifying、PM 中文版又不同 → 建議以 Architect 為準、Design 自己也要修內部不一致
- **P0 A-MISMATCH-2 錯誤碼 vs 失敗類型不對等**Architect 6 個 API 錯誤碼 vs Design 8 種 UI 失敗情境、缺對應表、Frontend 無法 map → Architect 補對應表 + FirmwareProgress.Reason 欄位
- 4 P1graceful shutdown 拒絕 / token 對比實測 / 失敗復原表 4vs8 / PM 缺體驗指標)+ 7 P2
- 對齊良好 6 項
- **不阻塞 A 階段 M9-1~M9-5**A 階段沒降版 + 沒 graceful shutdown
- [x] **Design 修改完成**2026-05-25
- `v2/firmware-management.md`916 → 948 行(+32、未拆檔、門檻調 800→1000
- `v2/control-panel.md`466 → 652 行(+186新增 §6a 整節 graceful shutdown 攔截 modal + FORCE 二次確認 + 19 i18n keys
- P0 A-MISMATCH-1 stage 命名統一 preparing/loading/flashing/verifying§5.3 / §7.1 / §8 / §9.6 / §9.8 全部對齊)
- P0 A-MISMATCH-2 失敗對應表§7.1 加 backend stage 欄 + Reason 細分scan_not_found/connect_failed/loader_write_failed/upgrade_mid_failed/verify_mismatch/timeout/disconnect_during_op
- P1 A-MID-1 graceful shutdown UI 在 control-panel.md §6a 落地
- P1 A-MID-2 token 對比實測責任歸 M9-12 Frontend
- i18n 從 52 → 51 keys細分修改
- Discord 殘留查證**不存在**PM 互審那輪可能誤判)
- **與 Architect 銜接點**Reason enum 字串需 Architect 在 TDD §3.4 照抄 + `HasActiveTask()` IPC 規格需提供 deviceName/stage/etaSeconds/direction 欄位
- [x] **Architect 互審報告完成**2026-05-24`.autoflow/04-architecture/reviews/architect-review-of-prd-and-design-v2.2-firmware.md`438 行)
- 結論PM/Design 技術上都可實現、整體對齊度高、無架構衝突
- 對 PM通過 + 2 MajorM9 工時拆法 / R-FW 編號)+ 4 Minor
- 對 Design通過 + 2 Majorstage 命名 / graceful shutdown+ 4 Minor
- **Architect 自承下輪需修 7 必修 + 3 建議項**F1-F7 + O1-O3
- **⚠️ 與 Design 互審讓步衝突**
- Design 說「stage 命名以 Architect 為準」
- Architect 說「stage 命名以 Design 為準」
- **需 Orchestrator/使用者裁決**
- [x] **M9-6 強驗證完成**2026-05-25`.autoflow/04-architecture/research-kl520-fw-management/56-m9-6-strong-validation-result.md`246 行)
- **🔴 推翻 1**macOS/Linux 2.0.0 wheel **沒有** KL630/KL730 enum確認+ KL730 product_id 是 **0x732 不是 0x730**(前兩輪研究假設值錯)
- **🔴 推翻 2**KL630/KL730 .tar **不是 fw_scpu.bin + fw_ncpu.bin**、是 **embedded Linux rootfs**ELF + 80+ .so / 5.8MB+32MB / 完全不同代設計)→ 弱驗證的「策略 Y 唯一可行」結論不成立、需全新策略 Xspike 在 M9-8
- **附帶發現**KneronPLUS 3.1.2 Python wrapper **沒有 `update_kdp_firmware_from_files`**、warrenchen 是 ctypes 直接打 .so C symbol、繞過 Python wrapper → TDD 必須明示「走 ctypes」
- **A 階段不阻塞**KL520+KL720 跨 2.0.0/3.1.2 wheel 一致、AC-FW-3.5 確定延後 B 階段
- **B 階段工時 +2-3 人天**原估「build script 解壓即可」、實際需 spike 找 KL630/KL730 全新 SDK 機制
- [x] **M9-6 弱驗證完成**2026-05-24`.autoflow/04-architecture/research-kl520-fw-management/55-m9-6-weak-validation-result.md`
- **關鍵發現 1**KneronPLUS 3.1.2 enum 確認 KL520/KL720/KL720_LEGACY/KL630/KL730/KL830 全存在
- **關鍵發現 2**`update_kdp_firmware_from_files(dg, scpu, ncpu, auto_reboot)` 簽名完整取得
- **關鍵發現 3**`KDP_MAGIC_CONNECTION_PASS = 0x1FF55B4F`(繞 KDP1 firmware check
- **關鍵發現 4**:策略 Z餵 .tar 給 SDK確定不可行 — 必須走策略 Y解壓後傳 .bin path
- **🔴 關鍵發現 5**重大wheel 三平台版本不一致macOS/Linux = 2.0.0、Windows = 3.1.2 — A 階段 KL630/KL730 升降版必先升 macOS/Linux wheel
- **關鍵發現 6**warrenchen 對 KL630/KL730 升降版完全沒實作、無 reference code、是 gap 不是 SDK 限制
- 10 unknown 解 ~50%、足以開始 A 階段
- **A 階段啟動前唯一阻塞**:派 backend agent 跑 35 分鐘強驗證A-2 + C-1
- 建議 **AC-FW-3.5KL630/KL730 升降版)延後到 B 階段 M9-10**
**Track 2B 階段技術前置、與 Track 1 平行)**
- [x] **Architect M9-6 SDK 驗證 plan 完成**2026-05-24`.autoflow/04-architecture/research-kl520-fw-management/50-m9-6-sdk-validation.md`~500 行)
- 10 個 unknown 分 3 類A SDK API / B 連線行為 / C tar 內容)
- 強驗證 2 人天 / 弱驗證 1 人天
- 4 個 R-VAL 風險(硬體 / wheel / 假設錯 / 環境)
- 建議 A 階段 M9-3 或 M9-4 完成後啟動實機驗證、避開 bridge.py 改檔衝突
- **派工前要確認**KL630/KL730 dongle 硬體狀態(有/多久能拿到/沒有)→ 決定走強驗證或弱驗證
**Track 3開發、待 Track 1 + Track 2 完成)**
- [ ] M9-1 ~ M9-5A 階段 MVP
- [ ] M9-6 ~ M9-13B 階段擴展)
---
## 2026-04-21 推論 bbox 標註不顯示 + KL520 Error 15S 級 bug fix