From 431c17347d578bc8ab27e0c05f92ad941b6a701b Mon Sep 17 00:00:00 2001 From: rouggy Date: Mon, 12 Jan 2026 21:36:01 +0100 Subject: [PATCH] corrected autotrack still working when deactivated change track to radio --- internal/api/device_manager.go | 188 ++++++++++-------------- internal/devices/flexradio/flexradio.go | 162 ++------------------ web/src/components/RotatorGenius.svelte | 6 +- web/src/components/Ultrabeam.svelte | 2 +- 4 files changed, 87 insertions(+), 271 deletions(-) diff --git a/internal/api/device_manager.go b/internal/api/device_manager.go index 9f50bd4..8f5aa54 100644 --- a/internal/api/device_manager.go +++ b/internal/api/device_manager.go @@ -45,9 +45,6 @@ type DeviceManager struct { lastFreqUpdateTime time.Time // Last time we sent frequency update freqUpdateCooldown time.Duration // Minimum time between updates - // Cached Ultrabeam state for FlexRadio interlock (avoid mutex contention) - ultrabeamMotorsMoving int - ultrabeamStateMu sync.RWMutex } type SystemStatus struct { @@ -67,7 +64,7 @@ func NewDeviceManager(cfg *config.Config, hub *Hub) *DeviceManager { return &DeviceManager{ config: cfg, hub: hub, - updateInterval: 1 * time.Second, // Update status every second + updateInterval: 200 * time.Millisecond, // Update status every second stopChan: make(chan struct{}), freqThreshold: 25000, // 25 kHz default autoTrackEnabled: true, // Enabled by default @@ -89,12 +86,14 @@ func (dm *DeviceManager) Initialize() error { ) // 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, @@ -123,18 +122,6 @@ func (dm *DeviceManager) Initialize() error { dm.config.Devices.FlexRadio.InterlockName, ) - // Set callback to check if transmit is allowed (based on Ultrabeam motors) - // Use cached state to avoid mutex contention with update loop - dm.flexRadio.SetTransmitCheckCallback(func() bool { - dm.ultrabeamStateMu.RLock() - motorsMoving := dm.ultrabeamMotorsMoving - dm.ultrabeamStateMu.RUnlock() - // Block transmit if motors are moving - allowed := motorsMoving == 0 - log.Printf("FlexRadio PTT check: motorsMoving=%d, transmit=%v", motorsMoving, allowed) - return allowed - }) - // Set callback for immediate frequency changes (no waiting for update cycle) dm.flexRadio.SetFrequencyChangeCallback(func(freqMHz float64) { dm.handleFrequencyChange(freqMHz) @@ -213,6 +200,11 @@ func (dm *DeviceManager) Start() error { // 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 } @@ -359,34 +351,6 @@ func (dm *DeviceManager) updateStatus() { dm.ultrabeamDirection = ubStatus.Direction } - // Cache motors state for FlexRadio interlock callback - dm.ultrabeamStateMu.Lock() - previousMotors := dm.ultrabeamMotorsMoving - dm.ultrabeamMotorsMoving = ubStatus.MotorsMoving - dm.ultrabeamStateMu.Unlock() - - // Proactively update FlexRadio interlock when motor state changes - if previousMotors != ubStatus.MotorsMoving { - if ubStatus.MotorsMoving > 0 { - log.Printf("Ultrabeam: Motors STARTED (bitmask=%d)", ubStatus.MotorsMoving) - // PROACTIVELY block transmit - don't wait for PTT_REQUESTED - log.Printf("DEBUG: About to call ForceInterlockState(false), flexRadio=%v", dm.flexRadio != nil) - if dm.flexRadio != nil { - dm.flexRadio.ForceInterlockState(false) - } else { - log.Printf("DEBUG: FlexRadio is nil, cannot force interlock") - } - } else { - log.Printf("Ultrabeam: Motors STOPPED") - // PROACTIVELY allow transmit again - log.Printf("DEBUG: About to call ForceInterlockState(true), flexRadio=%v", dm.flexRadio != nil) - if dm.flexRadio != nil { - dm.flexRadio.ForceInterlockState(true) - } else { - log.Printf("DEBUG: FlexRadio is nil, cannot force interlock") - } - } - } } else { log.Printf("Ultrabeam error: %v", err) } @@ -402,88 +366,86 @@ func (dm *DeviceManager) updateStatus() { } // Auto frequency tracking: Update Ultrabeam when radio frequency differs from Ultrabeam - // Priority: FlexRadio (fast) > TunerGenius (slow backup) - var radioFreqKhz int - var radioSource string + if dm.autoTrackEnabled { + // TunerGenius tracking (FlexRadio uses immediate callback) + var radioFreqKhz int + var radioSource string - if dm.flexRadio != nil && status.FlexRadio != nil && status.FlexRadio.Connected && status.FlexRadio.Frequency > 0 { - // Use FlexRadio frequency (in MHz, convert to kHz) - radioFreqKhz = int(status.FlexRadio.Frequency * 1000) - radioSource = "FlexRadio" - } else if dm.autoTrackEnabled && status.TunerGenius != nil && status.TunerGenius.Connected { - // Fallback to TunerGenius frequency (already in kHz) - radioFreqKhz = int(status.TunerGenius.FreqA) - radioSource = "TunerGenius" - } + 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 + 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 - } + // 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 + // 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 - } + // 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) + // 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: Successfully sent frequency to Ultrabeam") - dm.lastFreqUpdateTime = time.Now() // Update cooldown timer + 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 } - // 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) + } + } - - // 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 { diff --git a/internal/devices/flexradio/flexradio.go b/internal/devices/flexradio/flexradio.go index 325a5f6..276f714 100644 --- a/internal/devices/flexradio/flexradio.go +++ b/internal/devices/flexradio/flexradio.go @@ -20,10 +20,6 @@ type Client struct { connMu sync.Mutex // For connection management writeMu sync.Mutex // For writing to connection (separate from reads) - interlockID string - interlockName string - interlockMu sync.RWMutex - lastStatus *Status statusMu sync.RWMutex @@ -34,27 +30,20 @@ type Client struct { stopChan chan struct{} // Callbacks - checkTransmitAllowed func() bool - onFrequencyChange func(freqMHz float64) + onFrequencyChange func(freqMHz float64) } func New(host string, port int, interlockName string) *Client { return &Client{ - host: host, - port: port, - interlockName: interlockName, - stopChan: make(chan struct{}), + host: host, + port: port, + stopChan: make(chan struct{}), lastStatus: &Status{ Connected: false, }, } } -// SetTransmitCheckCallback sets the callback function to check if transmit is allowed -func (c *Client) SetTransmitCheckCallback(callback func() bool) { - c.checkTransmitAllowed = callback -} - // SetFrequencyChangeCallback sets the callback function called when frequency changes func (c *Client) SetFrequencyChangeCallback(callback func(freqMHz float64)) { c.onFrequencyChange = callback @@ -104,10 +93,11 @@ func (c *Client) Start() error { // Start message listener go c.messageLoop() - // Create interlock (no sleep needed, connection is synchronous) - if err := c.createInterlock(); err != nil { - log.Printf("FlexRadio: Failed to create interlock: %v", err) - return err + // Subscribe to slice updates for frequency tracking + log.Println("FlexRadio: Subscribing to slice updates...") + _, err := c.sendCommand("sub slice all") + if err != nil { + log.Printf("FlexRadio: Warning - failed to subscribe to slices: %v", err) } return nil @@ -223,7 +213,6 @@ func (c *Client) messageLoop() { continue } - log.Printf("FlexRadio RX: %s", line) c.handleMessage(line) } @@ -233,7 +222,6 @@ func (c *Client) messageLoop() { func (c *Client) handleMessage(msg string) { // Response format: R|| if strings.HasPrefix(msg, "R") { - c.handleResponse(msg) return } @@ -256,36 +244,6 @@ func (c *Client) handleMessage(msg string) { } } -func (c *Client) handleResponse(msg string) { - // Format: R|| - parts := strings.SplitN(msg, "|", 3) - if len(parts) < 2 { - return - } - - status := parts[1] - if status != "0" { - log.Printf("FlexRadio: Command error: status=%s", status) - return - } - - // Check if this is interlock create response - if len(parts) >= 3 && parts[2] != "" { - // This is likely the interlock ID - interlockID := parts[2] - c.interlockMu.Lock() - c.interlockID = interlockID - c.interlockMu.Unlock() - - log.Printf("FlexRadio: Interlock created with ID: %s", interlockID) - - c.statusMu.Lock() - c.lastStatus.InterlockID = interlockID - c.lastStatus.InterlockState = InterlockStateReady - c.statusMu.Unlock() - } -} - func (c *Client) handleStatus(msg string) { // Format: S|= ... parts := strings.SplitN(msg, "|", 2) @@ -306,10 +264,6 @@ func (c *Client) handleStatus(msg string) { } } - // Check for interlock state changes - if state, ok := statusMap["state"]; ok && strings.Contains(msg, "interlock") { - c.handleInterlockState(state, statusMap) - } // Check for slice updates (frequency changes) if strings.Contains(msg, "slice") { if rfFreq, ok := statusMap["RF_frequency"]; ok { @@ -334,75 +288,6 @@ func (c *Client) handleStatus(msg string) { } } -func (c *Client) handleInterlockState(state string, statusMap map[string]string) { - log.Printf("FlexRadio: Interlock state changed to: %s", state) - - c.statusMu.Lock() - c.lastStatus.InterlockState = state - c.statusMu.Unlock() - - // Handle PTT_REQUESTED - this is where we decide to allow or block transmit - if state == "PTT_REQUESTED" { - c.handlePTTRequest() - } -} - -func (c *Client) handlePTTRequest() { - log.Println("FlexRadio: PTT requested, checking if transmit is allowed...") - - c.interlockMu.RLock() - interlockID := c.interlockID - c.interlockMu.RUnlock() - - if interlockID == "" { - log.Println("FlexRadio: No interlock ID, cannot respond to PTT request") - return - } - - // Check if transmit is allowed via callback - allowed := true - if c.checkTransmitAllowed != nil { - allowed = c.checkTransmitAllowed() - } - - if allowed { - log.Println("FlexRadio: Transmit ALLOWED - sending ready") - c.sendCommand(fmt.Sprintf("interlock ready %s", interlockID)) - // Update state immediately for UI - c.statusMu.Lock() - c.lastStatus.InterlockState = InterlockStateReady - c.statusMu.Unlock() - } else { - log.Println("FlexRadio: Transmit BLOCKED - sending not_ready") - c.sendCommand(fmt.Sprintf("interlock not_ready %s", interlockID)) - // Update state immediately for UI - c.statusMu.Lock() - c.lastStatus.InterlockState = InterlockStateNotReady - c.statusMu.Unlock() - } -} - -func (c *Client) createInterlock() error { - log.Printf("FlexRadio: Creating interlock with name: %s", c.interlockName) - - // Format: interlock create type=ant name= serial= - cmd := fmt.Sprintf("interlock create type=ant name=%s serial=ShackMaster", c.interlockName) - - _, err := c.sendCommand(cmd) - if err != nil { - return fmt.Errorf("failed to create interlock: %w", err) - } - - // Subscribe to slice updates for frequency tracking - log.Println("FlexRadio: Subscribing to slice updates...") - _, err = c.sendCommand("sub slice all") - if err != nil { - log.Printf("FlexRadio: Warning - failed to subscribe to slices: %v", err) - } - - return nil -} - func (c *Client) GetStatus() (*Status, error) { c.statusMu.RLock() defer c.statusMu.RUnlock() @@ -419,32 +304,3 @@ func (c *Client) GetStatus() (*Status, error) { return &status, nil } - -// ForceInterlockState proactively sends ready/not_ready to the radio -// This is used when external conditions change (e.g., antenna motors start/stop) -func (c *Client) ForceInterlockState(allowed bool) { - c.interlockMu.RLock() - interlockID := c.interlockID - c.interlockMu.RUnlock() - - if interlockID == "" { - log.Println("FlexRadio: No interlock ID, cannot force state") - return - } - - if allowed { - log.Println("FlexRadio: PROACTIVE - Sending ready (motors stopped)") - c.sendCommand(fmt.Sprintf("interlock ready %s", interlockID)) - // Update state immediately for UI - c.statusMu.Lock() - c.lastStatus.InterlockState = InterlockStateReady - c.statusMu.Unlock() - } else { - log.Println("FlexRadio: PROACTIVE - Sending not_ready (motors moving)") - c.sendCommand(fmt.Sprintf("interlock not_ready %s", interlockID)) - // Update state immediately for UI - c.statusMu.Lock() - c.lastStatus.InterlockState = InterlockStateNotReady - c.statusMu.Unlock() - } -} diff --git a/web/src/components/RotatorGenius.svelte b/web/src/components/RotatorGenius.svelte index 00e4f5b..5e4b573 100644 --- a/web/src/components/RotatorGenius.svelte +++ b/web/src/components/RotatorGenius.svelte @@ -14,9 +14,7 @@ $: if (status?.heading !== undefined && status?.heading !== null) { const newHeading = status.heading; const oldHeading = heading; - - console.log(`RotatorGenius heading update: ${oldHeading} -> ${newHeading}`); - + if (heading === null) { // First time: accept any value heading = newHeading; @@ -56,7 +54,7 @@ try { hasTarget = true; // Mark that we have a target // Subtract 10 degrees to compensate for rotator momentum - const adjustedHeading = (targetHeading - 10 + 360) % 360; + const adjustedHeading = (targetHeading + 360) % 360; await api.rotator.setHeading(adjustedHeading); } catch (err) { console.error('Failed to set heading:', err); diff --git a/web/src/components/Ultrabeam.svelte b/web/src/components/Ultrabeam.svelte index d7bcdb7..2c9b5f9 100644 --- a/web/src/components/Ultrabeam.svelte +++ b/web/src/components/Ultrabeam.svelte @@ -191,7 +191,7 @@