package driver import "time" type DeviceDriver interface { Info() DeviceInfo Connect() error Disconnect() error IsConnected() bool Flash(modelPath string, opts FlashOptions, progressCh chan<- FlashProgress) error StartInference() error StopInference() error ReadInference() (*InferenceResult, error) RunInference(imageData []byte) (*InferenceResult, error) GetModelInfo() (*ModelInfo, error) } type DeviceInfo struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` Port string `json:"port"` VendorID uint16 `json:"vendorId,omitempty"` ProductID uint16 `json:"productId,omitempty"` Status DeviceStatus `json:"status"` FirmwareVer string `json:"firmwareVersion,omitempty"` FlashedModel string `json:"flashedModel,omitempty"` } type DeviceStatus string const ( StatusDetected DeviceStatus = "detected" StatusConnecting DeviceStatus = "connecting" StatusConnected DeviceStatus = "connected" StatusFlashing DeviceStatus = "flashing" StatusInferencing DeviceStatus = "inferencing" StatusError DeviceStatus = "error" StatusDisconnected DeviceStatus = "disconnected" ) // FlashOptions 帶入 model metadata,供 driver 在 load model 時傳給硬體 bridge。 // // 為什麼用 struct 而不是多帶兩個參數:載入模型需要的 metadata 之後還會長 // (如前處理色彩格式、top-K),用 struct 之後新增欄位不必再改 interface 簽章 // 與所有 test fake。 // // 兩個欄位都是 optional —— 空值代表「未指定」,bridge 端會 fallback 到既有的 // model id / 檔名 heuristics(維持既有 detection 行為不變)。 type FlashOptions struct { // TaskType 為 models.json 宣告的推論類型("classification" / // "object_detection")。bridge 端有指定就不再用檔名猜測。 TaskType string // Labels 是 class index → 顯示名稱的對應表,純顯示層用途、非推論必要輸入。 // 沒帶時 classification 輸出原始 enum(class_N)、detection 沿用 COCO。 Labels []string // InputWidth / InputHeight 是 models.json / metadata.json 宣告的模型輸入 // 尺寸。 // // ⚠️ 這是**最後手段**,不是可信來源:bridge 端會優先向 SDK 問模型自己 // 宣告的 input tensor shape,只有 SDK 沒回報時才用這組值。原因是這裡的 // 數字是人在上傳表單填的,實際案例是使用者填了 640x640 但模型根本不是 // 那個尺寸 —— 尺寸錯了 NPU 不會報錯,只會安靜地給出錯的推論結果。 // // 零值 = 未宣告,bridge 端會忽略並往下 fallback。 InputWidth int InputHeight int } // InferenceOptions 是推論期可即時調整的解析設定。 // // 與 FlashOptions 的分工:FlashOptions 在「把 model 載進裝置」時一次性帶入; // InferenceOptions 則是在**同一個已載入的 model 上**改變輸出的解讀方式, // 不需要重燒(KL520 重燒要數十秒)。 // // 兩個欄位的零值語意刻意不同,因為要能表達「不動」與「清空」兩種意圖: // // TaskType == "" → 不改變當前解析方式 // Labels == nil → 不改變當前 label 表 // Labels == []string{} → 清空 label 表,回到原始 enum(class_N) // // ⚠️ 因此 Labels 的判斷必須用 `!= nil` 而非 `len() > 0` —— 用長度判斷會讓 // 「清空」這個合法意圖永遠送不出去。 type InferenceOptions struct { TaskType string Labels []string } type FlashProgress struct { Percent int `json:"percent"` Stage string `json:"stage"` Message string `json:"message,omitempty"` Error string `json:"error,omitempty"` } type InferenceResult struct { DeviceID string `json:"deviceId,omitempty"` ModelID string `json:"modelId,omitempty"` TaskType string `json:"taskType"` Timestamp int64 `json:"timestamp"` LatencyMs float64 `json:"latencyMs"` Classifications []ClassResult `json:"classifications,omitempty"` Detections []DetectionResult `json:"detections,omitempty"` // Batch image fields (omitted for single-image/camera/video modes) ImageIndex int `json:"imageIndex,omitempty"` TotalImages int `json:"totalImages,omitempty"` Filename string `json:"filename,omitempty"` // Video progress fields (omitted for non-video modes) FrameIndex int `json:"frameIndex,omitempty"` TotalFrames int `json:"totalFrames,omitempty"` } type ClassResult struct { Label string `json:"label"` Confidence float64 `json:"confidence"` // ClassIndex 是模型輸出的原始類別索引,供前端在 label 缺漏時 fallback 顯示。 // 刻意不加 omitempty —— index 0 是合法類別,omitempty 會把它吃掉。 ClassIndex int `json:"classIndex"` } type DetectionResult struct { Label string `json:"label"` Confidence float64 `json:"confidence"` BBox BBox `json:"bbox"` } type BBox struct { X float64 `json:"x"` Y float64 `json:"y"` Width float64 `json:"width"` Height float64 `json:"height"` } type ModelInfo struct { ID string `json:"id"` Name string `json:"name"` LoadedAt time.Time `json:"loadedAt"` }