jim800121chen c54f16fca0 Initial commit: visionA monorepo with local-tool subproject
local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
            Wails IPC raise endpoint, stale process cleanup

.autoflow/: full PRD / Design Spec / Architecture / Testing docs
            (4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:10:38 +08:00

174 lines
4.2 KiB
Go

package device
import (
"fmt"
"log"
"sync"
"visiona-local/server/internal/driver"
"visiona-local/server/internal/driver/kneron"
mockdriver "visiona-local/server/internal/driver/mock"
"visiona-local/server/pkg/logger"
)
type Manager struct {
registry *DriverRegistry
sessions map[string]*DeviceSession
eventBus chan DeviceEvent
mockMode bool
scriptPath string
logBroadcaster *logger.Broadcaster
mu sync.RWMutex
}
func NewManager(registry *DriverRegistry, mockMode bool, mockCount int, scriptPath string) *Manager {
m := &Manager{
registry: registry,
sessions: make(map[string]*DeviceSession),
eventBus: make(chan DeviceEvent, 100),
mockMode: mockMode,
scriptPath: scriptPath,
}
if mockMode {
for i := 0; i < mockCount; i++ {
id := fmt.Sprintf("mock-device-%d", i+1)
d := mockdriver.Factory(id, i)
m.sessions[id] = NewSession(d)
}
}
return m
}
// 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() {
if m.mockMode {
return
}
// 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)
}
}
// 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 {
if m.mockMode {
return m.ListDevices()
}
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)
}
}
// Return current list.
devices := make([]driver.DeviceInfo, 0, len(m.sessions))
for _, s := range m.sessions {
devices = append(devices, s.Driver.Info())
}
return devices
}
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
}
func (m *Manager) GetDevice(id string) (*DeviceSession, error) {
m.mu.RLock()
defer m.mu.RUnlock()
s, ok := m.sessions[id]
if !ok {
return nil, fmt.Errorf("device not found: %s", id)
}
return s, nil
}
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
}