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 }