package handlers import ( "context" "fmt" "os" "runtime" "time" "visiona-local/server/internal/api/ws" "visiona-local/server/internal/device" "visiona-local/server/internal/driver" "visiona-local/server/internal/flash" "visiona-local/server/internal/inference" "github.com/gin-gonic/gin" ) // udevRuleInstalled checks if the Kneron udev rule is installed on Linux. func udevRuleInstalled() bool { _, err := os.Stat("/etc/udev/rules.d/99-kneron.rules") return err == nil } type DeviceHandler struct { deviceMgr *device.Manager flashSvc *flash.Service inferenceSvc *inference.Service wsHub *ws.Hub // fwHandler 提供 firmware 衍生欄位 helper(M9-3);可為 nil(test / 環境 // 無 firmware bundle 時)、此時 4 個 firmware 衍生欄位用 fallback 空值。 fwHandler *FirmwareHandler } func NewDeviceHandler( deviceMgr *device.Manager, flashSvc *flash.Service, inferenceSvc *inference.Service, wsHub *ws.Hub, ) *DeviceHandler { return &DeviceHandler{ deviceMgr: deviceMgr, flashSvc: flashSvc, inferenceSvc: inferenceSvc, wsHub: wsHub, } } // SetFirmwareHandler 注入 firmware handler 給 device handler 用、避免 // 構造函式 signature 破壞性變更(既有 caller 不需更新)。 func (h *DeviceHandler) SetFirmwareHandler(fw *FirmwareHandler) { h.fwHandler = fw } // deviceWithFirmware 是回給前端的 DeviceInfo 加 firmware 衍生欄位 // (TDD §3.1 line 131)。embedded driver.DeviceInfo 確保既有欄位 JSON // 平坦展開、與 FirmwareDerivedFields 同層、不破壞既有 frontend client。 // // Reviewer M9-3 第 1 輪 Major-1 修正:FirmwareDerivedFields 不再含 // `firmwareVer` 鍵。Frontend 直接讀 driver.DeviceInfo 既有的 // `firmwareVersion` 鍵(見 driver/interface.go DeviceInfo.FirmwareVer)。 // 兩鍵原本指向同一個 firmware 字串、會讓 frontend 困惑哪個是 SoT。 type deviceWithFirmware struct { driver.DeviceInfo FirmwareDerivedFields } // enrichDevices 把 device list 包上 firmware 衍生欄位。fwHandler 為 nil 時 // 仍回原始 list(衍生欄位走預設 zero value)、不阻塞 list endpoint。 func (h *DeviceHandler) enrichDevices(devices []driver.DeviceInfo) []deviceWithFirmware { out := make([]deviceWithFirmware, 0, len(devices)) for _, d := range devices { entry := deviceWithFirmware{DeviceInfo: d} if h.fwHandler != nil { entry.FirmwareDerivedFields = h.fwHandler.DeriveFirmwareFields(d.Type, d.FirmwareVer) } else { // fwHandler 缺省 fallback:衍生欄位用 zero value(前端會看到 // canUpgrade=false / isLegacy=false / bundled="unknown"、合理)。 // firmware 字串 frontend 直接從 d.FirmwareVer (JSON 鍵 firmwareVersion) // 讀、不在這裡複製。 entry.FirmwareDerivedFields = FirmwareDerivedFields{ BundledFirmwareVersion: "unknown", } } out = append(out, entry) } return out } func (h *DeviceHandler) ScanDevices(c *gin.Context) { devices := h.deviceMgr.Rescan() resp := gin.H{ // M9-3:附加 firmware 衍生欄位(firmwareVer / firmwareIsLegacy / // firmwareCanUpgrade / bundledFirmwareVersion)讓前端決定是否顯示 // 升級按鈕。enrichDevices 在 fwHandler=nil 時仍回有用內容。 "devices": h.enrichDevices(devices), } // Linux: 0 裝置 + udev rule 不存在 → 提示使用者安裝 USB 權限 if runtime.GOOS == "linux" && len(devices) == 0 && !udevRuleInstalled() { resp["udevHint"] = true } c.JSON(200, gin.H{"success": true, "data": resp}) } func (h *DeviceHandler) ListDevices(c *gin.Context) { devices := h.deviceMgr.ListDevices() resp := gin.H{ "devices": h.enrichDevices(devices), } if runtime.GOOS == "linux" && len(devices) == 0 && !udevRuleInstalled() { resp["udevHint"] = true } c.JSON(200, gin.H{"success": true, "data": resp}) } func (h *DeviceHandler) GetDevice(c *gin.Context) { id := c.Param("id") session, err := h.deviceMgr.GetDevice(id) if err != nil { c.JSON(404, gin.H{ "success": false, "error": gin.H{"code": "DEVICE_NOT_FOUND", "message": err.Error()}, }) return } c.JSON(200, gin.H{"success": true, "data": session.Driver.Info()}) } func (h *DeviceHandler) ConnectDevice(c *gin.Context) { id := c.Param("id") // KL520 USB Boot flow now includes mandatory reset + firmware reload on // first connect (required for inference to work — see kl720_driver.go // needsReset block). Worst-case path on Windows: Loader-mode reconnect // retry (16s) + firmware load (~31s) + reboot wait + second reconnect // (~13s) = ~60-65s. Use 120s to leave headroom and avoid spurious 504s. ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second) defer cancel() errCh := make(chan error, 1) go func() { errCh <- h.deviceMgr.Connect(id) }() select { case err := <-errCh: if err != nil { c.JSON(400, gin.H{ "success": false, "error": gin.H{"code": "CONNECT_FAILED", "message": err.Error()}, }) return } c.JSON(200, gin.H{"success": true}) case <-ctx.Done(): c.JSON(504, gin.H{ "success": false, "error": gin.H{"code": "CONNECT_TIMEOUT", "message": fmt.Sprintf("device connect timed out after 60s for %s", id)}, }) } } func (h *DeviceHandler) DisconnectDevice(c *gin.Context) { id := c.Param("id") if err := h.deviceMgr.Disconnect(id); err != nil { c.JSON(400, gin.H{ "success": false, "error": gin.H{"code": "DISCONNECT_FAILED", "message": err.Error()}, }) return } c.JSON(200, gin.H{"success": true}) } func (h *DeviceHandler) FlashDevice(c *gin.Context) { id := c.Param("id") var req struct { ModelID string `json:"modelId"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{ "success": false, "error": gin.H{"code": "BAD_REQUEST", "message": "modelId is required"}, }) return } taskID, progressCh, err := h.flashSvc.StartFlash(id, req.ModelID) if err != nil { c.JSON(400, gin.H{ "success": false, "error": gin.H{"code": "FLASH_FAILED", "message": err.Error()}, }) return } // Forward progress to WebSocket, then cleanup task (M2 fix) go func() { room := "flash:" + id for progress := range progressCh { h.wsHub.BroadcastToRoom(room, progress) } h.flashSvc.CleanupTask(taskID) }() c.JSON(200, gin.H{"success": true, "data": gin.H{"taskId": taskID}}) } func (h *DeviceHandler) StartInference(c *gin.Context) { id := c.Param("id") resultCh := make(chan *driver.InferenceResult, 10) if err := h.inferenceSvc.Start(id, resultCh); err != nil { c.JSON(400, gin.H{ "success": false, "error": gin.H{"code": "INFERENCE_ERROR", "message": err.Error()}, }) return } // Forward results to WebSocket, enriching with device ID go func() { room := "inference:" + id for result := range resultCh { result.DeviceID = id h.wsHub.BroadcastToRoom(room, result) } }() c.JSON(200, gin.H{"success": true}) } func (h *DeviceHandler) StopInference(c *gin.Context) { id := c.Param("id") if err := h.inferenceSvc.Stop(id); err != nil { c.JSON(400, gin.H{ "success": false, "error": gin.H{"code": "INFERENCE_ERROR", "message": err.Error()}, }) return } c.JSON(200, gin.H{"success": true}) }