# 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`)盤點我們**確切**有什麼、缺什麼。