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>
293 lines
13 KiB
Markdown
293 lines
13 KiB
Markdown
# 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 legacy(pid=0x0200)**:自動走 `connect_devices_without_check` + 載 KDP2 firmware 到 RAM(L749-804)
|
||
- **如果是 USB Boot Loader mode**:自動載 firmware 到 RAM(L872-914)
|
||
- 回傳 `firmware` 字串 + `fresh_firmware_loaded` flag
|
||
4. driver 根據 `fresh_firmware_loaded` 決定是否做 `restartBridge()` reset(L310-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-764:KL720 legacy 路徑、`kp.core.load_firmware_from_file(dg, scpu, ncpu)`
|
||
- `handle_connect()` L876-914:USB 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 firmware(KDP2、~52KB) | ✅ |
|
||
| `server/scripts/firmware/KL520/fw_ncpu.bin` | KL520 NCPU firmware(KDP2、~40KB) | ✅ |
|
||
| `server/scripts/firmware/KL720/fw_scpu.bin` | KL720 SCPU firmware(KDP2) | ✅ |
|
||
| `server/scripts/firmware/KL720/fw_ncpu.bin` | KL720 NCPU firmware(KDP2) | ✅ |
|
||
| **`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 = 120s(Windows 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 session(kill 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 badge(綠:KDP2 最新、黃:KDP2 但版本低、紅:KDP1 needs upgrade)
|
||
- 「升級韌體」按鈕(紅 badge 時 primary、其他 secondary)
|
||
- 升級 modal(progress bar + 階段提示 + 取消按鈕)
|
||
|
||
---
|
||
|
||
## 7. 缺項摘要
|
||
|
||
| 類別 | 缺什麼 | 取得方式 |
|
||
|------|--------|---------|
|
||
| Firmware binary | `KL520/fw_loader.bin` | 從 warrenchen `local_service_win/firmware/KL520/fw_loader.bin` 複製進 `server/scripts/firmware/KL520/` |
|
||
| Firmware binary(B 階段)| `KL520_kdp/fw_{scpu,ncpu}.bin`(降版用)| 從 warrenchen 複製 |
|
||
| Firmware binary(B 階段)| `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 handler(B 階段)| `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 + 受影響檔案 + 風險清單。
|