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>
260 lines
13 KiB
Markdown
260 lines
13 KiB
Markdown
# warrenchen 雲端版 FW 偵測 + 升降版實作分析
|
||
|
||
> 對應 visionA-local 研究 plan §10
|
||
> 來源:`/tmp/web_academy_prototype/` repo(shallow 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 A:KneronDFUT.exe** | KL520 升版 / 降版、KL720 升版 | 呼叫 Windows 第三方 `KneronDFUT.exe` CLI(subprocess) | ❌ Windows only |
|
||
| **Path B:libkplus.dll via ctypes** | KL520 KDP1 → KDP2「載入到 RAM」升級(不寫 flash) | 直接 ctypes 載 `libkplus.dll`、呼叫 C API | ✅ 三平台都能用(換成 .so / .dylib) |
|
||
|
||
**我們選 Path B**——理由:
|
||
- Path A 的 DFUT.exe 是 Windows-only Qt5 binary(30+ 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` | 舊版 KDP1(legacy、需升級) |
|
||
| `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 經常會回非零 code(USB re-enumerate 中),但不算錯誤
|
||
```
|
||
|
||
**注意 1**:這條路徑的「升級」實際上是「載到 RAM」、不是「寫 flash」。對 KL520 USB Boot 裝置而言、每次斷電都要再 load 一次。這對我們**夠用**——KL520 反正每次 connect 都會 load_firmware(visionA-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 二進位內容
|
||
|
||
| 裝置 | 路徑 | 檔案 | 用途 |
|
||
|------|------|------|------|
|
||
| KL520(KDP2 主版) | `firmware/KL520/` | `fw_scpu.bin` / `fw_ncpu.bin` | 升級到 KDP2 / 連線時 load to RAM |
|
||
| KL520(KDP2 主版) | `firmware/KL520/` | `fw_loader.bin` | USB Boot Loader(升降版必要) |
|
||
| KL520(KDP2 主版) | `firmware/KL520/dfw/` | `minions.bin` | DFUT 用的中介 firmware(不確定我們是否需要) |
|
||
| KL520(KDP1 降版) | `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(命名規則不同)
|
||
|
||
### 大小估算(合計)
|
||
|
||
| 裝置 | 檔案 | 估計大小 |
|
||
|------|------|---------|
|
||
| KL520(KDP2 主版)| fw_scpu + fw_ncpu | ~92KB(既有 TDD L3219 標 52+40KB) |
|
||
| KL520 | + fw_loader.bin | + ~10KB(估計) |
|
||
| KL520_kdp(KDP1) | fw_scpu + fw_ncpu | ~80KB(KDP1 較小) |
|
||
| 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(壓縮包) |
|
||
|
||
→ MVP(KL520 + KL720)+ 補 fw_loader.bin + KL520_kdp 合計 < 300KB、可忽略。
|
||
→ 完整版加 KL630/KL730 約 +5-7MB。
|
||
|
||
**注意**:warrenchen 把 `firmware/` 跟程式碼一起打包進 installer payload(STRATEGY.md L513-518)、跟我們做法一致。我們已經把 firmware 進 git(`server/scripts/firmware/`)、新增的 firmware(fw_loader.bin、KL520_kdp/)也應該 commit 進來、不走 vendor-sync。
|
||
|
||
---
|
||
|
||
## 3. warrenchen 暴露的 FW API(HTTP 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 B)100% 可移植**到我們 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`)盤點我們**確切**有什麼、缺什麼。
|