## 後端(Phase 1) 新增 flash 模組(從 edge-ai-platform 搬入): - server/internal/flash/service.go:StartFlash + 模型相容性檢查 + 晶片 NEF 解析 - server/internal/flash/progress.go:Flash 進度追蹤器 - server/internal/api/ws/flash_ws.go:WebSocket 推送 flash 進度 - device_handler.go:新增 FlashDevice method + flashSvc 欄位 - router.go:新增 POST /api/devices/:id/flash + WS /ws/devices/:id/flash-progress - main.go:初始化 flash.NewService 並傳入 router 推論/攝影機/MJPEG/inference WebSocket 之前 M1 已搬好,不需改動。 Python bridge (kneron_bridge.py) 與 edge-ai-platform 完全相同,不需改動。 ## 前端 store + hooks(Phase 2) - stores/flash-store.ts(新):Zustand store — startFlash / updateProgress / retryFlash / reset - hooks/use-flash-progress.ts(新):WebSocket hook 接收 flash 進度 inference-store / camera-store / inference types / use-inference-stream / use-websocket 之前 M1 已搬好,不需改動。 ## 前端 UI 元件(Phase 3) - components/devices/flash-dialog.tsx(新):模型載入對話框 + 硬體相容性檢查 - components/devices/flash-progress.tsx(新):Flash 進度條 + 錯誤重試 camera-inference-view / camera-feed / camera-overlay / source-selector / inference-panel / performance-metrics / classification-result / confidence-slider / video-progress / batch-image-thumbnails 之前 M1 已搬好。 ## 前端頁面整合(Phase 4) - workspace/page.tsx:繁中硬編碼、顯示已載入模型名稱 - workspace/[deviceId]/workspace-client.tsx:加入 FlashDialog 按鈕 + 繁中硬編碼 - devices/[id]/device-detail-client.tsx:加入 FlashDialog + 「進入工作區」按鈕(模型已載入才顯示) - device-card.tsx:已連線 + 模型已載入時顯示「工作區」快捷按鈕 ## 使用者操作流程 裝置列表 → 連線 → 管理 → 載入模型 → 進入工作區 → 選攝影機/圖片/影片 → 開始推論 → 看 bounding box / FPS / latency 或:裝置列表 → 工作區(已有模型)→ 直接推論 ## 不搬的東西 - cluster/* 全部不搬(已砍 cluster 功能) - relay / tunnel 相關不搬 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
package flash
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"visiona-local/server/internal/device"
|
|
"visiona-local/server/internal/driver"
|
|
"visiona-local/server/internal/model"
|
|
)
|
|
|
|
// isCompatible checks if any of the model's supported hardware types match
|
|
// the device type. The match is case-insensitive and also checks if the
|
|
// device type string contains the hardware name (e.g. "kneron_kl720" contains "KL720").
|
|
func isCompatible(modelHardware []string, deviceType string) bool {
|
|
dt := strings.ToUpper(deviceType)
|
|
for _, hw := range modelHardware {
|
|
if strings.ToUpper(hw) == dt || strings.Contains(dt, strings.ToUpper(hw)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// resolveModelPath checks if a chip-specific NEF file exists for the given
|
|
// model. For cross-platform models whose filePath points to a KL520 NEF,
|
|
// this tries to find the equivalent KL720 NEF (and vice versa).
|
|
//
|
|
// Resolution: data/nef/kl520/kl520_20001_... → data/nef/kl720/kl720_20001_...
|
|
func resolveModelPath(filePath string, deviceType string) string {
|
|
if filePath == "" {
|
|
return filePath
|
|
}
|
|
|
|
targetChip := ""
|
|
if strings.Contains(strings.ToLower(deviceType), "kl720") {
|
|
targetChip = "kl720"
|
|
} else if strings.Contains(strings.ToLower(deviceType), "kl520") {
|
|
targetChip = "kl520"
|
|
}
|
|
if targetChip == "" {
|
|
return filePath
|
|
}
|
|
|
|
// Already points to the target chip directory — use as-is.
|
|
if strings.Contains(filePath, "/"+targetChip+"/") {
|
|
return filePath
|
|
}
|
|
|
|
// Try to swap chip prefix in both directory and filename.
|
|
dir := filepath.Dir(filePath)
|
|
base := filepath.Base(filePath)
|
|
|
|
sourceChip := ""
|
|
if strings.Contains(dir, "kl520") {
|
|
sourceChip = "kl520"
|
|
} else if strings.Contains(dir, "kl720") {
|
|
sourceChip = "kl720"
|
|
}
|
|
|
|
if sourceChip != "" && sourceChip != targetChip {
|
|
newDir := strings.Replace(dir, sourceChip, targetChip, 1)
|
|
newBase := strings.Replace(base, sourceChip, targetChip, 1)
|
|
candidate := filepath.Join(newDir, newBase)
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
return candidate
|
|
}
|
|
}
|
|
|
|
return filePath
|
|
}
|
|
|
|
type Service struct {
|
|
deviceMgr *device.Manager
|
|
modelRepo *model.Repository
|
|
tracker *ProgressTracker
|
|
}
|
|
|
|
func NewService(deviceMgr *device.Manager, modelRepo *model.Repository) *Service {
|
|
return &Service{
|
|
deviceMgr: deviceMgr,
|
|
modelRepo: modelRepo,
|
|
tracker: NewProgressTracker(),
|
|
}
|
|
}
|
|
|
|
func (s *Service) StartFlash(deviceID, modelID string) (string, <-chan driver.FlashProgress, error) {
|
|
session, err := s.deviceMgr.GetDevice(deviceID)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("device not found: %w", err)
|
|
}
|
|
if !session.Driver.IsConnected() {
|
|
return "", nil, fmt.Errorf("device not connected")
|
|
}
|
|
|
|
m, err := s.modelRepo.GetByID(modelID)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("model not found: %w", err)
|
|
}
|
|
|
|
// Check hardware compatibility
|
|
deviceInfo := session.Driver.Info()
|
|
if !isCompatible(m.SupportedHardware, deviceInfo.Type) {
|
|
return "", nil, fmt.Errorf("model not compatible with device type %s", deviceInfo.Type)
|
|
}
|
|
|
|
// Use the model's .nef file path if available, otherwise fall back to modelID.
|
|
modelPath := m.FilePath
|
|
if modelPath == "" {
|
|
modelPath = modelID
|
|
}
|
|
|
|
// Resolve chip-specific NEF (e.g. KL520 path → KL720 equivalent).
|
|
modelPath = resolveModelPath(modelPath, deviceInfo.Type)
|
|
|
|
taskID := fmt.Sprintf("flash-%s-%s", deviceID, modelID)
|
|
task := s.tracker.Create(taskID, deviceID, modelID)
|
|
|
|
go func() {
|
|
defer func() {
|
|
task.Done = true
|
|
close(task.ProgressCh)
|
|
}()
|
|
// Brief pause to allow the WebSocket client to connect before
|
|
// progress messages start flowing.
|
|
time.Sleep(500 * time.Millisecond)
|
|
if err := session.Driver.Flash(modelPath, task.ProgressCh); err != nil {
|
|
task.ProgressCh <- driver.FlashProgress{
|
|
Percent: -1,
|
|
Stage: "error",
|
|
Error: err.Error(),
|
|
}
|
|
}
|
|
}()
|
|
|
|
return taskID, task.ProgressCh, nil
|
|
}
|