# 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//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:` — flash progress - `inference:` — 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:` | — | 升級 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:` | 沿用既有 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 + 受影響檔案 + 風險清單。