models.json 宣告的 taskType/labels/inputSize 原本在 flash 時被丟棄 (只傳 modelPath),導致 bridge 只能靠檔名猜測模型類型與尺寸。 - FlashOptions 帶 TaskType/Labels/InputWidth/InputHeight - 抽出 buildLoadModelCommand,四處 load_model 呼叫點(初次 + 三條 retry 路徑)統一走它,並加測試釘住呼叫點數量與「不得有手寫 payload」 —— 讓漏改 retry 路徑在結構上不可能發生 - ClassResult 加 ClassIndex(不加 omitempty,index 0 是合法值) 用 FlashOptions struct 而非裸參數,未來擴充欄位不需再動 interface 與所有 test fake。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
141 lines
5.2 KiB
Go
141 lines
5.2 KiB
Go
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"`
|
||
}
|