package device import ( "fmt" "log" "sync" "visiona-agent/server/internal/driver" "visiona-agent/server/internal/driver/kneron" "visiona-agent/server/pkg/logger" ) // fakeSerialNumber is the placeholder kn_number reported by the Python // bridge pyusb fallback when the Kneron SDK is unavailable (e.g. macOS // without the dylib). It is not a real hardware serial, so it must never be // used as a routing key (multiple devices could collide on it). Treated the // same as "no serial" (ADR-018 §2.2 / R2). const fakeSerialNumber = "0x00000000" type Manager struct { registry *DriverRegistry sessions map[string]*DeviceSession // serialToLocalID maps a device's hardware serial (Kneron kn_number, // e.g. "0x1A2B3C4D") to its local synthetic session key ("kl520-0"). // // Why it exists (ADR-018 serial routing): cloud-side requests arrive with // an identifier that never matched the local sessions key (the cloud used // its own DB UUID). The serial is the only identifier stable across both // layers, so GetDevice does a dual lookup: sessions[id] first (backward // compatible with local synthetic IDs), then serialToLocalID[id]. // Empty and fake ("0x00000000") serials are never indexed. serialToLocalID map[string]string eventBus chan DeviceEvent scriptPath string logBroadcaster *logger.Broadcaster mu sync.RWMutex } func NewManager(registry *DriverRegistry, scriptPath string) *Manager { return &Manager{ registry: registry, sessions: make(map[string]*DeviceSession), serialToLocalID: make(map[string]string), eventBus: make(chan DeviceEvent, 100), scriptPath: scriptPath, } } // SetLogBroadcaster attaches a log broadcaster so that Kneron driver // and bridge logs are forwarded to the frontend. func (m *Manager) SetLogBroadcaster(b *logger.Broadcaster) { m.logBroadcaster = b // Also set on any already-registered kneron drivers. m.mu.RLock() defer m.mu.RUnlock() for _, s := range m.sessions { if kd, ok := s.Driver.(*kneron.KneronDriver); ok { kd.SetLogBroadcaster(b) } } } func (m *Manager) Start() { // Detect real Kneron devices (KL520, KL720, etc.) via Python bridge. devices := kneron.DetectDevices(m.scriptPath) if len(devices) == 0 { log.Println("No Kneron devices detected") return } m.mu.Lock() defer m.mu.Unlock() for _, info := range devices { d := kneron.NewKneronDriver(info, m.scriptPath) if m.logBroadcaster != nil { d.SetLogBroadcaster(m.logBroadcaster) } m.sessions[info.ID] = NewSession(d) log.Printf("Registered Kneron device: %s (%s, type=%s)", info.Name, info.ID, info.Type) } m.rebuildSerialIndexLocked() } // Rescan re-detects connected Kneron devices. New devices are registered, // removed devices are cleaned up, and existing devices are left untouched. func (m *Manager) Rescan() []driver.DeviceInfo { detected := kneron.DetectDevices(m.scriptPath) // Build a set of detected device IDs. detectedIDs := make(map[string]driver.DeviceInfo, len(detected)) for _, info := range detected { detectedIDs[info.ID] = info } m.mu.Lock() defer m.mu.Unlock() // Remove devices that are no longer present. for id, s := range m.sessions { if _, exists := detectedIDs[id]; !exists { log.Printf("Device removed: %s", id) s.Driver.Disconnect() delete(m.sessions, id) } } // Add newly detected devices. for _, info := range detected { if _, exists := m.sessions[info.ID]; !exists { d := kneron.NewKneronDriver(info, m.scriptPath) if m.logBroadcaster != nil { d.SetLogBroadcaster(m.logBroadcaster) } m.sessions[info.ID] = NewSession(d) log.Printf("Registered Kneron device: %s (%s, type=%s)", info.Name, info.ID, info.Type) } } m.rebuildSerialIndexLocked() // Return current list. devices := make([]driver.DeviceInfo, 0, len(m.sessions)) for _, s := range m.sessions { devices = append(devices, s.Driver.Info()) } return devices } // rebuildSerialIndexLocked rebuilds serialToLocalID from the current // sessions. Caller must hold m.mu (write lock). // // Rebuilding (instead of incremental add/delete) keeps the index trivially // consistent with sessions across Start/Rescan, including device removal. // Empty serials are skipped; the fake serial "0x00000000" is skipped because // multiple SDK-less devices report the same value (routing would be // ambiguous). Real kn_numbers are unique per physical dongle; should a // duplicate ever appear it is logged and only one entry wins. func (m *Manager) rebuildSerialIndexLocked() { idx := make(map[string]string, len(m.sessions)) for id, s := range m.sessions { serial := s.Driver.Info().SerialNumber if serial == "" || serial == fakeSerialNumber { continue } if prev, dup := idx[serial]; dup { log.Printf("WARNING: duplicate device serial %s (devices %s and %s); serial routing keeps one entry only", serial, prev, id) continue } idx[serial] = id } m.serialToLocalID = idx } func (m *Manager) ListDevices() []driver.DeviceInfo { m.mu.RLock() defer m.mu.RUnlock() devices := make([]driver.DeviceInfo, 0, len(m.sessions)) for _, s := range m.sessions { devices = append(devices, s.Driver.Info()) } return devices } // GetDevice resolves a device session by identifier with a dual lookup // (ADR-018 serial routing, additive and backward compatible): // // 1. sessions[id] — the local synthetic key ("kl520-0"); every existing // caller keeps working unchanged. // 2. serialToLocalID[id] — the hardware serial (kn_number, "0x1A2B3C4D"); // lets cloud-proxied requests (flash / inference / camera / connect / // disconnect all converge here) address a physical dongle by serial. func (m *Manager) GetDevice(id string) (*DeviceSession, error) { m.mu.RLock() defer m.mu.RUnlock() if s, ok := m.sessions[id]; ok { return s, nil } if localID, ok := m.serialToLocalID[id]; ok { if s, ok := m.sessions[localID]; ok { return s, nil } } return nil, fmt.Errorf("device not found: %s", id) } func (m *Manager) Connect(id string) error { s, err := m.GetDevice(id) if err != nil { return err } if err := s.Driver.Connect(); err != nil { return err } m.eventBus <- DeviceEvent{Event: "updated", Device: s.Driver.Info()} return nil } func (m *Manager) Disconnect(id string) error { s, err := m.GetDevice(id) if err != nil { return err } if err := s.Driver.Disconnect(); err != nil { return err } m.eventBus <- DeviceEvent{Event: "updated", Device: s.Driver.Info()} return nil } func (m *Manager) Events() <-chan DeviceEvent { return m.eventBus }