A 階段第三個 milestone、暴露 firmware service 給 Frontend / Wails control panel。
New / modified:
- server/internal/api/handlers/firmware_handler.go: 新檔 465 行(upgrade + active-tasks endpoint + WS broadcast goroutine)
- server/internal/api/handlers/firmware_handler_test.go: 新檔 938 行、26+ subtests
- server/internal/api/handlers/device_handler.go: +47 行(3 個 firmware 衍生欄位)
- server/internal/api/router.go: +23 行
- server/main.go: +10 行(wire firmware service + handler)
4 endpoints 全到位(對齊 TDD §3.1):
- GET /api/devices: 加 firmwareIsLegacy / firmwareCanUpgrade / bundledFirmwareVersion(firmwareVersion 沿用既有 DeviceInfo 鍵)
- POST /api/devices/scan: 同步走 enrichDevices
- POST /api/devices/:id/firmware/upgrade: 202 + {taskId}
- GET /api/firmware/active-tasks: HasActiveTask + GetActiveTaskInfo
- WebSocket room firmware:<deviceID> broadcast 對齊 §4.2
關鍵設計:
- 3 層 interface(firmwareBroadcaster / firmwareService / deviceLookupSource)+ DeviceManagerAdapter 解 import cycle
- bundledVersion cache(只 cache success、避免 thundering herd / poison)
- isLegacyFirmware 對齊 bridge.py 規則(legacy_exact set + KDP1.x prefix + KDP2-9 forward-compat)+ parity 真值表測試
- 5 個錯誤碼齊全(DEVICE_NOT_FOUND / FW_UNSUPPORTED_CHIP / FW_DEVICE_BUSY / FW_UPGRADE_FAILED / FW_UPGRADE_BRICK_RISK)
Reviewer 兩輪審查:
- Round 1: 0 Critical / 1 Major / 3 Minor / 5 Suggestion
- Round 2: 0 Critical / 0 Major / 0 Minor / 3 極小 Suggestion(全部 backend 不需處理、純評估)
- Major 1(JSON 雙鍵衝突 firmwareVer vs firmwareVersion)方案 A 完全到位、3 個 test 鎖定 regression
TDD 同步:firmware-management.md §3.1 line 131 firmwareVer → firmwareVersion 對齊實作。
測試:go test ./... -race -count=1 全綠(handlers 2.489s / api 3.522s / ws 4.623s / device 1.931s / firmware 2.695s / driver/kneron 5.583s / model 5.022s)
SIGTERM main.go 整合留 M9-4.5(與 Wails OnBeforeClose 一起做)。
下一步:M9-4 Frontend Devices 頁 FW badge + 升級 modal + i18n(1.5 人天)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
466 lines
19 KiB
Go
466 lines
19 KiB
Go
package handlers
|
||
|
||
// firmware_handler.go — M9-3:把 firmware service 暴露給 HTTP / WebSocket 層。
|
||
//
|
||
// 提供三個 endpoint(TDD §3.1):
|
||
// 1. POST /api/devices/:id/firmware/upgrade — 啟動升級、202 + taskID
|
||
// 2. GET /api/firmware/active-tasks — 給 Wails control panel
|
||
// graceful shutdown 偵測用(§8.6.2)
|
||
// 3. WebSocket room "firmware:<deviceID>" — progress event broadcast
|
||
//
|
||
// device_handler 端的 `firmwareVer / firmwareIsLegacy / firmwareCanUpgrade /
|
||
// bundledFirmwareVersion` 衍生欄位由本檔提供的 helper 計算、再由 device handler
|
||
// 在 ListDevices / ScanDevices response 套用(見 device_handler.go FirmwareInfo
|
||
// helper)。
|
||
//
|
||
// 不在 M9-3 範圍:
|
||
// - GET /api/devices/:id/firmware/versions — M9-11(B2 階段)
|
||
// - POST /api/devices/:id/firmware/downgrade — M9-11/12
|
||
// - SIGTERM graceful shutdown 整合 main.go — 留 M9-4 或更後
|
||
// - Frontend UI — M9-4
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
|
||
"visiona-local/server/internal/device"
|
||
"visiona-local/server/internal/firmware"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// Public API:handler + deps
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
// firmwareBroadcaster 把 ws.Hub.BroadcastToRoom 抽象出來、test 可注入 spy。
|
||
// 採與 SystemHandler 相同 pattern(shutdownNotifyBroadcaster)、避免 handler
|
||
// 直接綁死 *ws.Hub。
|
||
type firmwareBroadcaster interface {
|
||
BroadcastToRoom(room string, data interface{})
|
||
}
|
||
|
||
// firmwareService 是 firmware.Service 的 minimum surface、給 handler 用。
|
||
// 抽介面是為了 unit test 不用啟 Python bridge / 真的 driver。
|
||
type firmwareService interface {
|
||
UpgradeFirmware(ctx context.Context, deviceID, chip string) (string, <-chan firmware.FirmwareProgress, error)
|
||
CleanupTask(deviceID string)
|
||
HasActiveTask() bool
|
||
GetActiveTaskInfo() []*firmware.ActiveTaskInfo
|
||
}
|
||
|
||
// FirmwareHandler 處理 firmware 相關 HTTP endpoint + WebSocket broadcast。
|
||
type FirmwareHandler struct {
|
||
svc firmwareService
|
||
deviceMgr deviceLookupSource
|
||
wsHub firmwareBroadcaster
|
||
|
||
// bundledVersions cache 從 firmware/<chip>/VERSION 讀進來的版本字串、
|
||
// 避免每次 ListDevices 都打 disk I/O(A 階段檔案不變)。
|
||
bundledMu sync.RWMutex
|
||
bundledVersions map[string]string // chip → "v2.2.0"
|
||
firmwareDir string // server/scripts/firmware/ 絕對路徑(caller 注入)
|
||
}
|
||
|
||
// deviceLookupSource 是 device manager 對 handler 的最小介面、test 可 mock。
|
||
type deviceLookupSource interface {
|
||
GetDevice(id string) (*device.DeviceSession, error)
|
||
}
|
||
|
||
// NewFirmwareHandler 建立 handler。firmwareDir 是 server/scripts/firmware/ 的
|
||
// 絕對路徑(含 KL520/、KL720/ 子目錄);空字串時 bundledFirmwareVersion
|
||
// 衍生欄位會回 "unknown"。
|
||
func NewFirmwareHandler(
|
||
svc firmwareService,
|
||
deviceMgr deviceLookupSource,
|
||
wsHub firmwareBroadcaster,
|
||
firmwareDir string,
|
||
) *FirmwareHandler {
|
||
return &FirmwareHandler{
|
||
svc: svc,
|
||
deviceMgr: deviceMgr,
|
||
wsHub: wsHub,
|
||
bundledVersions: make(map[string]string),
|
||
firmwareDir: firmwareDir,
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 1. POST /api/devices/:id/firmware/upgrade
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
// UpgradeDevice 啟動 firmware 升級流程。
|
||
//
|
||
// Response:
|
||
// - 202 Accepted + {success:true, data:{taskId}}:goroutine 啟動、progress
|
||
// event 走 WebSocket room "firmware:<id>"
|
||
// - 404:device 不存在
|
||
// - 400:chip 不支援(A 階段 only KL520/KL720)
|
||
// - 409:device 已有 active firmware task / server shutting down
|
||
// - 500:service 拒絕(不可恢復錯誤)
|
||
//
|
||
// 注意:本 handler 不阻塞 HTTP 連線、立即回 202、實際升級在 goroutine 跑。
|
||
// 客戶端應 subscribe WebSocket room 接收進度。
|
||
func (h *FirmwareHandler) UpgradeDevice(c *gin.Context) {
|
||
id := c.Param("id")
|
||
|
||
// 1. 找 device、取 chip
|
||
session, err := h.deviceMgr.GetDevice(id)
|
||
if err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{
|
||
"success": false,
|
||
"error": gin.H{"code": "DEVICE_NOT_FOUND", "message": err.Error()},
|
||
})
|
||
return
|
||
}
|
||
info := session.Driver.Info()
|
||
chip := ChipFromDeviceType(info.Type)
|
||
if !firmware.SupportedUpgradeChip(chip) {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"success": false,
|
||
"error": gin.H{
|
||
"code": "FW_UNSUPPORTED_CHIP",
|
||
"message": "A 階段僅支援 KL520/KL720(chip=" + chip + ")",
|
||
},
|
||
})
|
||
return
|
||
}
|
||
|
||
// 2. 呼 service 啟動升級
|
||
//
|
||
// 刻意用 context.Background() 而非 c.Request.Context():
|
||
//
|
||
// - HTTP request ctx 在我們回 202 後會立即 cancel(gin 結束 handler
|
||
// 即釋放 connection),若沿用 request ctx 會把背景 goroutine 也 cancel
|
||
// 掉、升級流程立刻中斷、device brick 風險極高。
|
||
// - Service 內部自己包 timeout(runUpgrade → UpgradeTimeoutFor + margin、
|
||
// 見 service.go runUpgrade)、不會永久 hang。
|
||
// - Graceful shutdown 透過 service.GracefulShutdown() 主動觸發、不靠
|
||
// 外層 ctx cancel(見 service.go GracefulShutdown)。
|
||
//
|
||
// 結論:Background 在這裡是刻意設計、非疏忽。
|
||
taskID, progressCh, err := h.svc.UpgradeFirmware(context.Background(), id, chip)
|
||
if err != nil {
|
||
status, code := classifyServiceError(err)
|
||
c.JSON(status, gin.H{
|
||
"success": false,
|
||
"error": gin.H{"code": code, "message": err.Error()},
|
||
})
|
||
return
|
||
}
|
||
|
||
// 3. spawn forward goroutine:消費 progressCh、broadcast 到 WS room
|
||
go h.forwardProgressToWS(id, progressCh)
|
||
|
||
c.JSON(http.StatusAccepted, gin.H{
|
||
"success": true,
|
||
"data": gin.H{"taskId": taskID},
|
||
})
|
||
}
|
||
|
||
// forwardProgressToWS 把 service 的 progress events 廣播到 WebSocket room
|
||
// "firmware:<deviceID>"、Frontend modal subscribe 該 room 接進度。
|
||
//
|
||
// progressCh 由 service 在終態(done/error)後 close、本 goroutine 退出時
|
||
// 呼叫 CleanupTask 移除 tracker entry。
|
||
func (h *FirmwareHandler) forwardProgressToWS(deviceID string, progressCh <-chan firmware.FirmwareProgress) {
|
||
room := "firmware:" + deviceID
|
||
for ev := range progressCh {
|
||
// schema 對齊 TDD §4.2、外掛 type 標籤讓 client 統一 dispatch。
|
||
// 也保留 deviceId / direction / stage / percent / elapsedMs / etaMs 等
|
||
// FirmwareProgress 既有欄位(json marshal 直接展開)。
|
||
h.wsHub.BroadcastToRoom(room, firmwareProgressMessage{
|
||
Type: "firmware_progress",
|
||
FirmwareProgress: ev,
|
||
})
|
||
}
|
||
// progressCh closed → service 已 markDone、可移除 tracker entry。
|
||
h.svc.CleanupTask(deviceID)
|
||
}
|
||
|
||
// firmwareProgressMessage 是 WebSocket payload wrapper、加 type 欄位讓
|
||
// 前端可在同一 room 內 dispatch 不同訊息(雖然目前 firmware: room 只有
|
||
// firmware_progress、保留結構彈性)。
|
||
//
|
||
// 使用 embedded struct:FirmwareProgress 的所有 json field 會直接展開到
|
||
// 同層、不會多一層巢狀。
|
||
type firmwareProgressMessage struct {
|
||
Type string `json:"type"`
|
||
firmware.FirmwareProgress
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 2. GET /api/firmware/active-tasks
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
// ListActiveTasks 回傳所有進行中 firmware task 的 snapshot、給 Wails control
|
||
// panel 在 OnBeforeClose 偵測「是否有任務不可中斷」用(TDD §8.6.2)。
|
||
//
|
||
// Response:
|
||
//
|
||
// {
|
||
// "success": true,
|
||
// "data": {
|
||
// "hasActive": true|false,
|
||
// "tasks": [
|
||
// {"taskId":..., "deviceId":..., "deviceName":..., "chip":...,
|
||
// "direction":..., "stage":..., "elapsedMs":..., "etaSeconds":...}
|
||
// ]
|
||
// }
|
||
// }
|
||
func (h *FirmwareHandler) ListActiveTasks(c *gin.Context) {
|
||
tasks := h.svc.GetActiveTaskInfo()
|
||
// 用 nil → []:JSON encode 空 slice 為 [] 而非 null、Frontend 更好處理
|
||
if tasks == nil {
|
||
tasks = []*firmware.ActiveTaskInfo{}
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": gin.H{
|
||
"hasActive": len(tasks) > 0,
|
||
"tasks": tasks,
|
||
},
|
||
})
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 3. DeviceInfo 衍生欄位 helper(給 device_handler 用)
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
// FirmwareDerivedFields 是 device list / scan response 上的 firmware 衍生
|
||
// 欄位(TDD §3.1 line 131)。
|
||
//
|
||
// 由 device handler 在 ListDevices / ScanDevices 套用到每個 device entry
|
||
// 上、Frontend 用這些欄位決定是否顯示「升級韌體」按鈕。
|
||
//
|
||
// Reviewer M9-3 第 1 輪 Major-1(JSON schema 雙鍵衝突修正):
|
||
// 原本還有 `firmwareVer string` 欄位、但 device list response 的
|
||
// driver.DeviceInfo 已內建 `firmwareVersion`(見 driver/interface.go line 26)、
|
||
// 兩鍵指向同一個 firmware 字串會讓 frontend 混淆。改採顯式 3 欄位、frontend
|
||
// 直接讀 device entry 既有的 `firmwareVersion` 鍵。WS broadcast schema 不變
|
||
// (已用 `beforeVersion` / `afterVersion`、與此處不衝突)。
|
||
type FirmwareDerivedFields struct {
|
||
FirmwareIsLegacy bool `json:"firmwareIsLegacy"`
|
||
FirmwareCanUpgrade bool `json:"firmwareCanUpgrade"`
|
||
BundledFirmwareVersion string `json:"bundledFirmwareVersion"`
|
||
}
|
||
|
||
// DeriveFirmwareFields 從 DeviceInfo 計算衍生欄位(不打 service、純字串
|
||
// 判斷、適合塞 list endpoint 每筆 entry)。
|
||
//
|
||
// 規則:
|
||
// - FirmwareIsLegacy:對齊 bridge.py `_fw_classify_legacy`(kneron_bridge.py
|
||
// line 1463)— 顯式列舉 KDP1 字串變體 + KDP2-KDP9 prefix 放行、避免
|
||
// 對未來 firmware 誤判。
|
||
// - FirmwareCanUpgrade:chip 在 A 階段支援清單(KL520/KL720)+ IsLegacy=true
|
||
// - BundledFirmwareVersion:讀 firmware/<chip>/VERSION(cache)
|
||
//
|
||
// 注意:本 helper 不再回 firmwareVer 欄位、caller 直接使用 DeviceInfo 既有
|
||
// `firmwareVersion` 鍵;參數 firmwareVer 只供 IsLegacy 判定用。
|
||
func (h *FirmwareHandler) DeriveFirmwareFields(deviceType, firmwareVer string) FirmwareDerivedFields {
|
||
chip := ChipFromDeviceType(deviceType)
|
||
bundled := h.bundledVersionFor(chip)
|
||
isLegacy := isLegacyFirmware(firmwareVer)
|
||
canUpgrade := firmware.SupportedUpgradeChip(chip) && isLegacy
|
||
|
||
return FirmwareDerivedFields{
|
||
FirmwareIsLegacy: isLegacy,
|
||
FirmwareCanUpgrade: canUpgrade,
|
||
BundledFirmwareVersion: bundled,
|
||
}
|
||
}
|
||
|
||
// bundledVersionFor 讀 firmware/<chip>/VERSION、cache 結果。第一次 miss
|
||
// 時讀 disk、後續直接回 cache。讀檔失敗回 "unknown"(不 panic、不阻塞
|
||
// device list)。
|
||
//
|
||
// Reviewer M9-3 第 1 輪 Minor M-3:只 cache success。失敗(檔不存在 / 讀
|
||
// 錯 / 空檔)不寫 cache、下次 list device 會重試。情境:CI 第一次 build
|
||
// 時 firmware 檔還沒 ready、若 cache 空字串將永遠回 "unknown";改成不 cache
|
||
// 失敗、檔準備好後第一次 list 就能讀到新版本。
|
||
func (h *FirmwareHandler) bundledVersionFor(chip string) string {
|
||
if h.firmwareDir == "" || chip == "" {
|
||
return "unknown"
|
||
}
|
||
h.bundledMu.RLock()
|
||
v, ok := h.bundledVersions[chip]
|
||
h.bundledMu.RUnlock()
|
||
if ok {
|
||
return v
|
||
}
|
||
|
||
versionPath := filepath.Join(h.firmwareDir, chip, "VERSION")
|
||
f, err := os.Open(versionPath)
|
||
if err != nil {
|
||
// 沒 VERSION 檔(如 CI first build)→ 不 cache、下次重試
|
||
return "unknown"
|
||
}
|
||
defer f.Close()
|
||
|
||
buf, err := io.ReadAll(io.LimitReader(f, 64)) // VERSION 應只是一行版本字串
|
||
if err != nil {
|
||
// 讀錯 → 不 cache、下次重試
|
||
return "unknown"
|
||
}
|
||
parsed := strings.TrimSpace(string(buf))
|
||
if parsed == "" {
|
||
// 空檔 → 不 cache、下次重試
|
||
return "unknown"
|
||
}
|
||
// 只 cache 成功讀到的版本字串
|
||
h.cacheBundledVersion(chip, parsed)
|
||
return parsed
|
||
}
|
||
|
||
func (h *FirmwareHandler) cacheBundledVersion(chip, version string) {
|
||
h.bundledMu.Lock()
|
||
h.bundledVersions[chip] = version
|
||
h.bundledMu.Unlock()
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// helpers(exported / unexported)
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
// ChipFromDeviceType 把 driver.DeviceInfo.Type 字串轉成 firmware chip
|
||
// 識別字串("KL520" / "KL720")。對應 detector.go:chipFromProductID
|
||
// 的反向轉換、未識別時回空字串。
|
||
//
|
||
// 注意:刻意公開、device_handler 也可能直接用(雖然目前透過
|
||
// DeriveFirmwareFields 走)。
|
||
func ChipFromDeviceType(deviceType string) string {
|
||
low := strings.ToLower(deviceType)
|
||
switch {
|
||
case strings.Contains(low, "kl520"):
|
||
return firmware.ChipKL520
|
||
case strings.Contains(low, "kl720"):
|
||
return firmware.ChipKL720
|
||
case strings.Contains(low, "kl630"):
|
||
return "KL630"
|
||
case strings.Contains(low, "kl730"):
|
||
return "KL730"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
// isLegacyFirmware 判斷 firmware 字串是否為 KDP1 legacy。
|
||
//
|
||
// 對齊 bridge.py `_fw_classify_legacy`(kneron_bridge.py line 1463)的判定
|
||
// 規則、與 Python 端保持一致行為(Reviewer M9-3 第 1 輪 S-1:跨端 drift 防護)。
|
||
//
|
||
// 規則差異(與 bridge.py 對應):
|
||
//
|
||
// 1. 已知 KDP1 字串顯式列舉(明示比對、不靠 substring):
|
||
// "KDP", "KDP1", "USB BOOT", "USB BOOT LOADER", "LOADER", "BOOTLOADER"
|
||
// 2. KDP1.x 變體("KDP1.0", "KDP1.5", "KDP1 alpha" 等)→ legacy
|
||
// 3. KDP2-KDP9 prefix 明示放行(forward-compat 未來 firmware、避免 substring
|
||
// match 對 "KDP3" 之類字串誤判 legacy)
|
||
// 4. 未知 firmware 字串(如 "NEF" / "K3")→ 保守 default = 不 legacy
|
||
// (避免誤觸 loader stage brick device;若實際為 legacy、verify 階段
|
||
// 會 detect verify_mismatch、不致 brick)
|
||
//
|
||
// 注意:Go 端不接收 product_id(DeviceInfo.ProductID 雖有、但 ListDevices
|
||
// response 是 driver.DeviceInfo embedded、product_id 對應 KL720 KDP legacy
|
||
// 判定走 chip type 即可、不需要在這裡覆蓋)。bridge.py 端的 product_id ==
|
||
// 0x0200 短路在 connect / upgrade 流程內部處理、不影響 list endpoint
|
||
// canUpgrade 判定。
|
||
func isLegacyFirmware(firmwareVer string) bool {
|
||
fw := strings.ToUpper(strings.TrimSpace(firmwareVer))
|
||
|
||
// 1. 已知 KDP1 legacy firmware 字串完整列舉
|
||
// 注意:bridge.py 把 "" 視為 legacy(USB Boot state 不回 firmware
|
||
// string);但 Go 端 list endpoint 看到空 firmware 字串通常是 device
|
||
// 還沒 connect 過 — 保守不視為 legacy(不顯示升級按鈕),與 bridge.py
|
||
// 的 connect 流程語意分離(connect 流程進入 loader 是另一條路徑)。
|
||
if fw == "" {
|
||
return false
|
||
}
|
||
switch fw {
|
||
case "KDP", "KDP1", "USB BOOT", "USB BOOT LOADER", "LOADER", "BOOTLOADER":
|
||
return true
|
||
}
|
||
|
||
// 2. KDP1.x(KDP1.0 / KDP1.5 等)
|
||
if strings.HasPrefix(fw, "KDP1.") || strings.HasPrefix(fw, "KDP1 ") {
|
||
return true
|
||
}
|
||
|
||
// 3. 明示放行 KDP2 / KDP3+(forward-compat、避免 substring match 對未來
|
||
// firmware 誤判)
|
||
for _, prefix := range []string{"KDP2", "KDP3", "KDP4", "KDP5", "KDP6", "KDP7", "KDP8", "KDP9"} {
|
||
if strings.HasPrefix(fw, prefix) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 4. 未知 firmware 字串 → 保守不 legacy(與 bridge.py 一致)
|
||
return false
|
||
}
|
||
|
||
// classifyServiceError 把 firmware.Service 的 error 對應到 HTTP status 和
|
||
// 錯誤碼(TDD §3.3)。
|
||
//
|
||
// Reviewer M9-3 第 1 輪 S-3:unknown error 路徑加 log,方便除錯非預期錯誤
|
||
// 類型——未來 service 新增 sentinel error 但忘了在這裡掛上時、log 能即時
|
||
// 揭露問題。
|
||
func classifyServiceError(err error) (int, string) {
|
||
switch {
|
||
case errors.Is(err, firmware.ErrDeviceNotFound):
|
||
return http.StatusNotFound, "DEVICE_NOT_FOUND"
|
||
case errors.Is(err, firmware.ErrUnsupportedChip):
|
||
return http.StatusBadRequest, "FW_UNSUPPORTED_CHIP"
|
||
case errors.Is(err, firmware.ErrDeviceBusy):
|
||
return http.StatusConflict, "FW_DEVICE_BUSY"
|
||
case errors.Is(err, firmware.ErrUpgradeBrickRisk):
|
||
return http.StatusInternalServerError, "FW_UPGRADE_BRICK_RISK"
|
||
case errors.Is(err, firmware.ErrUpgradeFailed):
|
||
return http.StatusInternalServerError, "FW_UPGRADE_FAILED"
|
||
default:
|
||
log.Printf("[firmware_handler] classifyServiceError: unknown error type %T: %v", err, err)
|
||
return http.StatusInternalServerError, "FW_UPGRADE_FAILED"
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// device.Manager → firmware.DeviceLookup adapter
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
// DeviceManagerAdapter 把 *device.Manager 包成 firmware.DeviceLookup、
|
||
// 讓 main.go 在 NewService 時可以直接傳給 firmware 模組、不破壞 device.Manager
|
||
// 既有 API。
|
||
//
|
||
// 公開出來、供 main.go 在 wire 階段使用。
|
||
type DeviceManagerAdapter struct {
|
||
mgr *device.Manager
|
||
}
|
||
|
||
// NewDeviceManagerAdapter 建立 adapter。
|
||
func NewDeviceManagerAdapter(mgr *device.Manager) *DeviceManagerAdapter {
|
||
return &DeviceManagerAdapter{mgr: mgr}
|
||
}
|
||
|
||
// GetUpgradeDriver 實作 firmware.DeviceLookup。
|
||
//
|
||
// 把 device session 內的 driver(*kneron.KneronDriver)轉成
|
||
// firmware.UpgradeDriver 介面。KneronDriver 已內建 Info() +
|
||
// UpgradeFirmware(ctx, chip, progressCh) 兩個 method、直接 type-assert。
|
||
func (a *DeviceManagerAdapter) GetUpgradeDriver(deviceID string) (firmware.UpgradeDriver, error) {
|
||
session, err := a.mgr.GetDevice(deviceID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
drv, ok := session.Driver.(firmware.UpgradeDriver)
|
||
if !ok {
|
||
return nil, errors.New("driver does not implement firmware.UpgradeDriver")
|
||
}
|
||
return drv, nil
|
||
}
|