package api import ( "log" "sync" "time" "git.rouggy.com/rouggy/ShackMaster/internal/config" "git.rouggy.com/rouggy/ShackMaster/internal/devices/antennagenius" "git.rouggy.com/rouggy/ShackMaster/internal/devices/powergenius" "git.rouggy.com/rouggy/ShackMaster/internal/devices/rotatorgenius" "git.rouggy.com/rouggy/ShackMaster/internal/devices/tunergenius" "git.rouggy.com/rouggy/ShackMaster/internal/devices/ultrabeam" "git.rouggy.com/rouggy/ShackMaster/internal/devices/webswitch" "git.rouggy.com/rouggy/ShackMaster/internal/services/solar" "git.rouggy.com/rouggy/ShackMaster/internal/services/weather" ) type DeviceManager struct { config *config.Config webSwitch *webswitch.Client powerGenius *powergenius.Client tunerGenius *tunergenius.Client antennaGenius *antennagenius.Client rotatorGenius *rotatorgenius.Client ultrabeam *ultrabeam.Client solarClient *solar.Client weatherClient *weather.Client hub *Hub statusMu sync.RWMutex lastStatus *SystemStatus updateInterval time.Duration stopChan chan struct{} // Auto frequency tracking freqThreshold int // Threshold for triggering update (Hz) autoTrackEnabled bool ultrabeamDirection int // User-selected direction (0=normal, 1=180, 2=bi-dir) lastFreqUpdateTime time.Time // Last time we sent frequency update freqUpdateCooldown time.Duration // Minimum time between updates } type SystemStatus struct { WebSwitch *webswitch.Status `json:"webswitch"` PowerGenius *powergenius.Status `json:"power_genius"` TunerGenius *tunergenius.Status `json:"tuner_genius"` AntennaGenius *antennagenius.Status `json:"antenna_genius"` RotatorGenius *rotatorgenius.Status `json:"rotator_genius"` Ultrabeam *ultrabeam.Status `json:"ultrabeam"` Solar *solar.SolarData `json:"solar"` Weather *weather.WeatherData `json:"weather"` Timestamp time.Time `json:"timestamp"` } func NewDeviceManager(cfg *config.Config, hub *Hub) *DeviceManager { return &DeviceManager{ config: cfg, hub: hub, updateInterval: 1 * time.Second, // Update status every second stopChan: make(chan struct{}), freqThreshold: 25000, // 25 kHz default autoTrackEnabled: true, // Enabled by default ultrabeamDirection: 0, // Normal direction by default freqUpdateCooldown: 2 * time.Second, // Wait 2 seconds between updates } } func (dm *DeviceManager) Initialize() error { log.Println("Initializing device manager...") // Initialize WebSwitch dm.webSwitch = webswitch.New(dm.config.Devices.WebSwitch.Host) // Initialize Power Genius dm.powerGenius = powergenius.New( dm.config.Devices.PowerGenius.Host, dm.config.Devices.PowerGenius.Port, ) // Initialize Tuner Genius dm.tunerGenius = tunergenius.New( dm.config.Devices.TunerGenius.Host, dm.config.Devices.TunerGenius.Port, ) // Initialize Antenna Genius dm.antennaGenius = antennagenius.New( dm.config.Devices.AntennaGenius.Host, dm.config.Devices.AntennaGenius.Port, ) // Initialize Rotator Genius log.Printf("Initializing RotatorGenius: host=%s port=%d", dm.config.Devices.RotatorGenius.Host, dm.config.Devices.RotatorGenius.Port) dm.rotatorGenius = rotatorgenius.New( dm.config.Devices.RotatorGenius.Host, dm.config.Devices.RotatorGenius.Port, ) // Initialize Ultrabeam log.Printf("Initializing Ultrabeam: host=%s port=%d", dm.config.Devices.Ultrabeam.Host, dm.config.Devices.Ultrabeam.Port) dm.ultrabeam = ultrabeam.New( dm.config.Devices.Ultrabeam.Host, dm.config.Devices.Ultrabeam.Port, ) // Initialize Solar data client dm.solarClient = solar.New() // Initialize Weather client dm.weatherClient = weather.New( dm.config.Weather.OpenWeatherMapAPIKey, dm.config.Location.Latitude, dm.config.Location.Longitude, ) // Start device polling in background (non-blocking) go func() { if err := dm.powerGenius.Start(); err != nil { log.Printf("Warning: Failed to start PowerGenius polling: %v", err) } }() go func() { if err := dm.tunerGenius.Start(); err != nil { log.Printf("Warning: Failed to start TunerGenius polling: %v", err) } }() go func() { if err := dm.antennaGenius.Start(); err != nil { log.Printf("Warning: Failed to start AntennaGenius polling: %v", err) } }() log.Println("About to launch RotatorGenius goroutine...") go func() { log.Println("Starting RotatorGenius polling goroutine...") if err := dm.rotatorGenius.Start(); err != nil { log.Printf("Warning: Failed to start RotatorGenius polling: %v", err) } }() log.Println("RotatorGenius goroutine launched") log.Println("About to launch Ultrabeam goroutine...") go func() { log.Println("Starting Ultrabeam polling goroutine...") if err := dm.ultrabeam.Start(); err != nil { log.Printf("Warning: Failed to start Ultrabeam polling: %v", err) } }() log.Println("Ultrabeam goroutine launched") log.Println("Device manager initialized") return nil } func (dm *DeviceManager) Start() error { log.Println("Starting device monitoring...") go dm.monitorDevices() return nil } func (dm *DeviceManager) Stop() { log.Println("Stopping device manager...") close(dm.stopChan) // Close all connections if dm.powerGenius != nil { dm.powerGenius.Close() } if dm.tunerGenius != nil { dm.tunerGenius.Close() } if dm.antennaGenius != nil { dm.antennaGenius.Close() } if dm.rotatorGenius != nil { dm.rotatorGenius.Close() } if dm.ultrabeam != nil { dm.ultrabeam.Stop() } } func (dm *DeviceManager) monitorDevices() { ticker := time.NewTicker(dm.updateInterval) defer ticker.Stop() for { select { case <-ticker.C: dm.updateStatus() case <-dm.stopChan: return } } } func (dm *DeviceManager) updateStatus() { status := &SystemStatus{ Timestamp: time.Now(), } // Query all devices // WebSwitch - get actual relay states if wsStatus, err := dm.webSwitch.GetStatus(); err == nil { status.WebSwitch = wsStatus } else { log.Printf("WebSwitch error: %v", err) } // Power Genius if pgStatus, err := dm.powerGenius.GetStatus(); err == nil { status.PowerGenius = pgStatus } else { log.Printf("Power Genius error: %v", err) } // Tuner Genius if tgStatus, err := dm.tunerGenius.GetStatus(); err == nil { status.TunerGenius = tgStatus } else { log.Printf("Tuner Genius error: %v", err) } // Antenna Genius if agStatus, err := dm.antennaGenius.GetStatus(); err == nil { status.AntennaGenius = agStatus } else { log.Printf("Antenna Genius error: %v", err) } // Rotator Genius if rgStatus, err := dm.rotatorGenius.GetStatus(); err == nil { status.RotatorGenius = rgStatus } else { log.Printf("Rotator Genius error: %v", err) } // Ultrabeam if ubStatus, err := dm.ultrabeam.GetStatus(); err == nil { status.Ultrabeam = ubStatus // Sync direction with Ultrabeam if not yet set (first time or after restart) // This prevents auto-track from using wrong direction before user changes it if dm.ultrabeamDirection == 0 && ubStatus.Direction != 0 { dm.ultrabeamDirection = ubStatus.Direction log.Printf("Auto-track: Initialized direction from Ultrabeam: %d", dm.ultrabeamDirection) } } else { log.Printf("Ultrabeam error: %v", err) } // Auto frequency tracking: Update Ultrabeam when TunerGenius frequency differs from Ultrabeam if dm.autoTrackEnabled && status.TunerGenius != nil && status.TunerGenius.Connected && status.Ultrabeam != nil && status.Ultrabeam.Connected { tunerFreqKhz := int(status.TunerGenius.FreqA) // TunerGenius frequency is already in kHz ultrabeamFreqKhz := status.Ultrabeam.Frequency // Ultrabeam frequency in kHz // Ignore invalid frequencies or out of Ultrabeam range (40M-6M) // This prevents retraction when slice is closed (FreqA becomes 0) // Ultrabeam VL2.3 only covers 7000-54000 kHz (40M to 6M) if tunerFreqKhz < 7000 || tunerFreqKhz > 54000 { return // Out of range, skip auto-track } freqDiff := tunerFreqKhz - ultrabeamFreqKhz if freqDiff < 0 { freqDiff = -freqDiff } // Convert diff to Hz for comparison with threshold (which is in Hz) freqDiffHz := freqDiff * 1000 // Don't send command if motors are already moving if status.Ultrabeam.MotorsMoving != 0 { // Motors moving - wait for them to finish return } if freqDiffHz >= dm.freqThreshold { // Use current Ultrabeam direction if user hasn't explicitly set one directionToUse := dm.ultrabeamDirection if directionToUse == 0 && status.Ultrabeam.Direction != 0 { directionToUse = status.Ultrabeam.Direction } // Check cooldown to prevent rapid fire commands timeSinceLastUpdate := time.Since(dm.lastFreqUpdateTime) if timeSinceLastUpdate < dm.freqUpdateCooldown { log.Printf("Auto-track: Cooldown active (%v remaining), skipping update", dm.freqUpdateCooldown-timeSinceLastUpdate) } else { log.Printf("Auto-track: Frequency differs by %d kHz, updating Ultrabeam to %d kHz (direction=%d)", freqDiff, tunerFreqKhz, directionToUse) // Send to Ultrabeam with saved or current direction if err := dm.ultrabeam.SetFrequency(tunerFreqKhz, directionToUse); err != nil { log.Printf("Auto-track: Failed to update Ultrabeam: %v (will retry)", err) } else { log.Printf("Auto-track: Successfully sent frequency to Ultrabeam") dm.lastFreqUpdateTime = time.Now() // Update cooldown timer } } } } // Solar Data (fetched every 15 minutes, cached) if solarData, err := dm.solarClient.GetSolarData(); err == nil { status.Solar = solarData } else { log.Printf("Solar data error: %v", err) } // Weather Data (fetched every 10 minutes, cached) if weatherData, err := dm.weatherClient.GetWeatherData(); err == nil { status.Weather = weatherData } else { log.Printf("Weather data error: %v", err) } // Update cached status dm.statusMu.Lock() dm.lastStatus = status dm.statusMu.Unlock() // Broadcast to all connected clients if dm.hub != nil { dm.hub.BroadcastStatusUpdate(status) } } func (dm *DeviceManager) GetStatus() *SystemStatus { dm.statusMu.RLock() defer dm.statusMu.RUnlock() if dm.lastStatus == nil { return &SystemStatus{ Timestamp: time.Now(), } } return dm.lastStatus } // Device control methods func (dm *DeviceManager) WebSwitch() *webswitch.Client { return dm.webSwitch } func (dm *DeviceManager) PowerGenius() *powergenius.Client { return dm.powerGenius } func (dm *DeviceManager) TunerGenius() *tunergenius.Client { return dm.tunerGenius } func (dm *DeviceManager) AntennaGenius() *antennagenius.Client { return dm.antennaGenius } func (dm *DeviceManager) RotatorGenius() *rotatorgenius.Client { return dm.rotatorGenius } func (dm *DeviceManager) Ultrabeam() *ultrabeam.Client { return dm.ultrabeam } func (dm *DeviceManager) SetAutoTrack(enabled bool, thresholdHz int) { dm.autoTrackEnabled = enabled dm.freqThreshold = thresholdHz } func (dm *DeviceManager) SetUltrabeamDirection(direction int) { dm.ultrabeamDirection = direction log.Printf("Ultrabeam direction set to: %d", direction) }