Files
ShackMaster/internal/api/device_manager.go
2026-01-13 23:11:58 +01:00

518 lines
15 KiB
Go

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/flexradio"
"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
flexRadio *flexradio.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)
ultrabeamDirectionSet bool // True if user has explicitly set a direction
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"`
FlexRadio *flexradio.Status `json:"flexradio,omitempty"`
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: 200 * time.Millisecond, // 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: 500 * time.Millisecond, // 500ms cooldown (was 2sec)
}
}
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
log.Printf("Initializing TunerGenius: host=%s port=%d", dm.config.Devices.TunerGenius.Host, dm.config.Devices.TunerGenius.Port)
dm.tunerGenius = tunergenius.New(
dm.config.Devices.TunerGenius.Host,
dm.config.Devices.TunerGenius.Port,
)
// Initialize Antenna Genius
log.Printf("Initializing AntennaGenius: host=%s port=%d", dm.config.Devices.AntennaGenius.Host, dm.config.Devices.AntennaGenius.Port)
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 FlexRadio if enabled
if dm.config.Devices.FlexRadio.Enabled {
log.Printf("Initializing FlexRadio: host=%s port=%d", dm.config.Devices.FlexRadio.Host, dm.config.Devices.FlexRadio.Port)
dm.flexRadio = flexradio.New(
dm.config.Devices.FlexRadio.Host,
dm.config.Devices.FlexRadio.Port,
)
// Set callback for immediate frequency changes (no waiting for update cycle)
dm.flexRadio.SetFrequencyChangeCallback(func(freqMHz float64) {
dm.handleFrequencyChange(freqMHz)
})
// Set callback to check if transmit is allowed (based on Ultrabeam motors)
dm.flexRadio.SetTransmitCheckCallback(func() bool {
// Get current Ultrabeam status
ubStatus, err := dm.ultrabeam.GetStatus()
if err != nil || ubStatus == nil {
// If we cannot get status, allow transmit (fail-safe)
return true
}
// Block transmit if motors are moving
motorsMoving := ubStatus.MotorsMoving != 0
if motorsMoving {
log.Printf("FlexRadio PTT check: Motors moving (bitmask=%d) - BLOCKING", ubStatus.MotorsMoving)
} else {
log.Printf("FlexRadio PTT check: Motors stopped - ALLOWING")
}
return !motorsMoving
})
}
// 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")
// Start FlexRadio if enabled
if dm.flexRadio != nil {
log.Println("Starting FlexRadio connection...")
go func() {
if err := dm.flexRadio.Start(); err != nil {
log.Printf("Warning: Failed to start FlexRadio: %v", err)
}
}()
log.Println("FlexRadio goroutine launched")
}
log.Println("Device manager initialized")
return nil
}
func (dm *DeviceManager) Start() error {
log.Println("Starting device monitoring...")
go dm.monitorDevices()
return nil
}
// handleFrequencyChange is called immediately when FlexRadio frequency changes
// This provides instant auto-track response instead of waiting for updateStatus cycle
func (dm *DeviceManager) handleFrequencyChange(freqMHz float64) {
// Check if ultrabeam is initialized
// Check if auto-track is enabled
if !dm.autoTrackEnabled {
return
}
if dm.ultrabeam == nil {
return
}
// Check cooldown first
timeSinceLastUpdate := time.Since(dm.lastFreqUpdateTime)
if timeSinceLastUpdate < dm.freqUpdateCooldown {
return
}
// Use cached status instead of calling GetStatus (which can block)
dm.statusMu.RLock()
hasStatus := dm.lastStatus != nil
var ubStatus *ultrabeam.Status
if hasStatus {
ubStatus = dm.lastStatus.Ultrabeam
}
dm.statusMu.RUnlock()
if ubStatus == nil || !ubStatus.Connected {
return
}
// Don't update if motors are already moving
if ubStatus.MotorsMoving != 0 {
return
}
freqKhz := int(freqMHz * 1000)
ultrabeamFreqKhz := ubStatus.Frequency
// Only track if in Ultrabeam range (7-54 MHz)
if freqKhz < 7000 || freqKhz > 54000 {
return
}
freqDiff := freqKhz - ultrabeamFreqKhz
if freqDiff < 0 {
freqDiff = -freqDiff
}
freqDiffHz := freqDiff * 1000
if freqDiffHz >= dm.freqThreshold {
directionToUse := dm.ultrabeamDirection
if !dm.ultrabeamDirectionSet && ubStatus.Direction != 0 {
directionToUse = ubStatus.Direction
}
log.Printf("Auto-track (immediate): Updating to %d kHz (diff=%d kHz)", freqKhz, freqDiff)
if err := dm.ultrabeam.SetFrequency(freqKhz, directionToUse); err != nil {
log.Printf("Auto-track (immediate): Failed: %v", err)
} else {
dm.lastFreqUpdateTime = time.Now()
}
}
}
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 user hasn't explicitly set one
// This prevents auto-track from using wrong direction before user changes it
if !dm.ultrabeamDirectionSet {
dm.ultrabeamDirection = ubStatus.Direction
}
} else {
log.Printf("Ultrabeam error: %v", err)
}
// FlexRadio (use direct cache access to avoid mutex contention)
if dm.flexRadio != nil {
// Access lastStatus directly from FlexRadio's internal cache
// The messageLoop updates this in real-time, no need to block on GetStatus
frStatus, err := dm.flexRadio.GetStatus()
if err == nil && frStatus != nil {
status.FlexRadio = frStatus
}
}
// Auto frequency tracking: Update Ultrabeam when radio frequency differs from Ultrabeam
if dm.autoTrackEnabled {
// TunerGenius tracking (FlexRadio uses immediate callback)
var radioFreqKhz int
var radioSource string
if status.TunerGenius != nil && status.TunerGenius.Connected {
// Fallback to TunerGenius frequency (already in kHz)
radioFreqKhz = int(status.TunerGenius.FreqA)
radioSource = "TunerGenius"
}
if radioFreqKhz > 0 && status.Ultrabeam != nil && status.Ultrabeam.Connected {
ultrabeamFreqKhz := status.Ultrabeam.Frequency // Ultrabeam frequency in kHz
// Only do auto-track if frequency is in Ultrabeam range (40M-6M: 7000-54000 kHz)
// This prevents retraction when slice is closed (FreqA becomes 0) or on out-of-range bands
if radioFreqKhz >= 7000 && radioFreqKhz <= 54000 {
freqDiff := radioFreqKhz - 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 {
if freqDiffHz >= dm.freqThreshold {
// Use user's explicitly set direction, or fallback to current Ultrabeam direction
directionToUse := dm.ultrabeamDirection
if !dm.ultrabeamDirectionSet && 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 (%s): Frequency differs by %d kHz, updating Ultrabeam to %d kHz (direction=%d)", radioSource, freqDiff, radioFreqKhz, directionToUse)
// Send to Ultrabeam with saved or current direction
if err := dm.ultrabeam.SetFrequency(radioFreqKhz, 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
}
}
}
}
}
// If out of range, simply skip auto-track but continue with status broadcast
}
// 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
dm.ultrabeamDirectionSet = true // Mark that user has explicitly set direction
log.Printf("Ultrabeam direction set to: %d (user choice)", direction)
}