jim800121chen 46514d77d7 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>
2026-05-25 07:40:56 +08:00

293 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 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 + 受影響檔案 + 風險清單。