corrected autotrack still working when deactivated

This commit is contained in:
2026-01-12 21:36:01 +01:00
parent 414d802d37
commit 4f9e1e88eb
3 changed files with 86 additions and 270 deletions

View File

@@ -45,9 +45,6 @@ type DeviceManager struct {
lastFreqUpdateTime time.Time // Last time we sent frequency update lastFreqUpdateTime time.Time // Last time we sent frequency update
freqUpdateCooldown time.Duration // Minimum time between updates freqUpdateCooldown time.Duration // Minimum time between updates
// Cached Ultrabeam state for FlexRadio interlock (avoid mutex contention)
ultrabeamMotorsMoving int
ultrabeamStateMu sync.RWMutex
} }
type SystemStatus struct { type SystemStatus struct {
@@ -67,7 +64,7 @@ func NewDeviceManager(cfg *config.Config, hub *Hub) *DeviceManager {
return &DeviceManager{ return &DeviceManager{
config: cfg, config: cfg,
hub: hub, hub: hub,
updateInterval: 1 * time.Second, // Update status every second updateInterval: 200 * time.Millisecond, // Update status every second
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
freqThreshold: 25000, // 25 kHz default freqThreshold: 25000, // 25 kHz default
autoTrackEnabled: true, // Enabled by default autoTrackEnabled: true, // Enabled by default
@@ -89,12 +86,14 @@ func (dm *DeviceManager) Initialize() error {
) )
// Initialize Tuner Genius // 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.tunerGenius = tunergenius.New(
dm.config.Devices.TunerGenius.Host, dm.config.Devices.TunerGenius.Host,
dm.config.Devices.TunerGenius.Port, dm.config.Devices.TunerGenius.Port,
) )
// Initialize Antenna Genius // 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.antennaGenius = antennagenius.New(
dm.config.Devices.AntennaGenius.Host, dm.config.Devices.AntennaGenius.Host,
dm.config.Devices.AntennaGenius.Port, dm.config.Devices.AntennaGenius.Port,
@@ -123,18 +122,6 @@ func (dm *DeviceManager) Initialize() error {
dm.config.Devices.FlexRadio.InterlockName, 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) // Set callback for immediate frequency changes (no waiting for update cycle)
dm.flexRadio.SetFrequencyChangeCallback(func(freqMHz float64) { dm.flexRadio.SetFrequencyChangeCallback(func(freqMHz float64) {
dm.handleFrequencyChange(freqMHz) dm.handleFrequencyChange(freqMHz)
@@ -213,6 +200,11 @@ func (dm *DeviceManager) Start() error {
// This provides instant auto-track response instead of waiting for updateStatus cycle // This provides instant auto-track response instead of waiting for updateStatus cycle
func (dm *DeviceManager) handleFrequencyChange(freqMHz float64) { func (dm *DeviceManager) handleFrequencyChange(freqMHz float64) {
// Check if ultrabeam is initialized // Check if ultrabeam is initialized
// Check if auto-track is enabled
if !dm.autoTrackEnabled {
return
}
if dm.ultrabeam == nil { if dm.ultrabeam == nil {
return return
} }
@@ -359,34 +351,6 @@ func (dm *DeviceManager) updateStatus() {
dm.ultrabeamDirection = ubStatus.Direction 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 { } else {
log.Printf("Ultrabeam error: %v", err) 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 // Auto frequency tracking: Update Ultrabeam when radio frequency differs from Ultrabeam
// Priority: FlexRadio (fast) > TunerGenius (slow backup) if dm.autoTrackEnabled {
var radioFreqKhz int // TunerGenius tracking (FlexRadio uses immediate callback)
var radioSource string var radioFreqKhz int
var radioSource string
if dm.flexRadio != nil && status.FlexRadio != nil && status.FlexRadio.Connected && status.FlexRadio.Frequency > 0 { if status.TunerGenius != nil && status.TunerGenius.Connected {
// Use FlexRadio frequency (in MHz, convert to kHz) // Fallback to TunerGenius frequency (already in kHz)
radioFreqKhz = int(status.FlexRadio.Frequency * 1000) radioFreqKhz = int(status.TunerGenius.FreqA)
radioSource = "FlexRadio" radioSource = "TunerGenius"
} 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 radioFreqKhz > 0 && status.Ultrabeam != nil && status.Ultrabeam.Connected { if radioFreqKhz > 0 && status.Ultrabeam != nil && status.Ultrabeam.Connected {
ultrabeamFreqKhz := status.Ultrabeam.Frequency // Ultrabeam frequency in kHz ultrabeamFreqKhz := status.Ultrabeam.Frequency // Ultrabeam frequency in kHz
// Only do auto-track if frequency is in Ultrabeam range (40M-6M: 7000-54000 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 // This prevents retraction when slice is closed (FreqA becomes 0) or on out-of-range bands
if radioFreqKhz >= 7000 && radioFreqKhz <= 54000 { if radioFreqKhz >= 7000 && radioFreqKhz <= 54000 {
freqDiff := radioFreqKhz - ultrabeamFreqKhz freqDiff := radioFreqKhz - ultrabeamFreqKhz
if freqDiff < 0 { if freqDiff < 0 {
freqDiff = -freqDiff freqDiff = -freqDiff
} }
// Convert diff to Hz for comparison with threshold (which is in Hz) // Convert diff to Hz for comparison with threshold (which is in Hz)
freqDiffHz := freqDiff * 1000 freqDiffHz := freqDiff * 1000
// Don't send command if motors are already moving // Don't send command if motors are already moving
if status.Ultrabeam.MotorsMoving == 0 { if status.Ultrabeam.MotorsMoving == 0 {
if freqDiffHz >= dm.freqThreshold { if freqDiffHz >= dm.freqThreshold {
// Use user's explicitly set direction, or fallback to current Ultrabeam direction // Use user's explicitly set direction, or fallback to current Ultrabeam direction
directionToUse := dm.ultrabeamDirection directionToUse := dm.ultrabeamDirection
if !dm.ultrabeamDirectionSet && status.Ultrabeam.Direction != 0 { if !dm.ultrabeamDirectionSet && status.Ultrabeam.Direction != 0 {
directionToUse = status.Ultrabeam.Direction directionToUse = status.Ultrabeam.Direction
} }
// Check cooldown to prevent rapid fire commands // Check cooldown to prevent rapid fire commands
timeSinceLastUpdate := time.Since(dm.lastFreqUpdateTime) timeSinceLastUpdate := time.Since(dm.lastFreqUpdateTime)
if timeSinceLastUpdate < dm.freqUpdateCooldown { if timeSinceLastUpdate < dm.freqUpdateCooldown {
log.Printf("Auto-track: Cooldown active (%v remaining), skipping update", dm.freqUpdateCooldown-timeSinceLastUpdate) 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 { } else {
log.Printf("Auto-track: Successfully sent frequency to Ultrabeam") log.Printf("Auto-track (%s): Frequency differs by %d kHz, updating Ultrabeam to %d kHz (direction=%d)", radioSource, freqDiff, radioFreqKhz, directionToUse)
dm.lastFreqUpdateTime = time.Now() // Update cooldown timer
// 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 { func (dm *DeviceManager) GetStatus() *SystemStatus {

View File

@@ -20,10 +20,6 @@ type Client struct {
connMu sync.Mutex // For connection management connMu sync.Mutex // For connection management
writeMu sync.Mutex // For writing to connection (separate from reads) writeMu sync.Mutex // For writing to connection (separate from reads)
interlockID string
interlockName string
interlockMu sync.RWMutex
lastStatus *Status lastStatus *Status
statusMu sync.RWMutex statusMu sync.RWMutex
@@ -34,27 +30,20 @@ type Client struct {
stopChan chan struct{} stopChan chan struct{}
// Callbacks // Callbacks
checkTransmitAllowed func() bool onFrequencyChange func(freqMHz float64)
onFrequencyChange func(freqMHz float64)
} }
func New(host string, port int, interlockName string) *Client { func New(host string, port int, interlockName string) *Client {
return &Client{ return &Client{
host: host, host: host,
port: port, port: port,
interlockName: interlockName, stopChan: make(chan struct{}),
stopChan: make(chan struct{}),
lastStatus: &Status{ lastStatus: &Status{
Connected: false, 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 // SetFrequencyChangeCallback sets the callback function called when frequency changes
func (c *Client) SetFrequencyChangeCallback(callback func(freqMHz float64)) { func (c *Client) SetFrequencyChangeCallback(callback func(freqMHz float64)) {
c.onFrequencyChange = callback c.onFrequencyChange = callback
@@ -104,10 +93,11 @@ func (c *Client) Start() error {
// Start message listener // Start message listener
go c.messageLoop() go c.messageLoop()
// Create interlock (no sleep needed, connection is synchronous) // Subscribe to slice updates for frequency tracking
if err := c.createInterlock(); err != nil { log.Println("FlexRadio: Subscribing to slice updates...")
log.Printf("FlexRadio: Failed to create interlock: %v", err) _, err := c.sendCommand("sub slice all")
return err if err != nil {
log.Printf("FlexRadio: Warning - failed to subscribe to slices: %v", err)
} }
return nil return nil
@@ -223,7 +213,6 @@ func (c *Client) messageLoop() {
continue continue
} }
log.Printf("FlexRadio RX: %s", line)
c.handleMessage(line) c.handleMessage(line)
} }
@@ -233,7 +222,6 @@ func (c *Client) messageLoop() {
func (c *Client) handleMessage(msg string) { func (c *Client) handleMessage(msg string) {
// Response format: R<seq>|<status>|<data> // Response format: R<seq>|<status>|<data>
if strings.HasPrefix(msg, "R") { if strings.HasPrefix(msg, "R") {
c.handleResponse(msg)
return return
} }
@@ -256,36 +244,6 @@ func (c *Client) handleMessage(msg string) {
} }
} }
func (c *Client) handleResponse(msg string) {
// Format: R<seq>|<status>|<data>
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) { func (c *Client) handleStatus(msg string) {
// Format: S<handle>|<key>=<value> ... // Format: S<handle>|<key>=<value> ...
parts := strings.SplitN(msg, "|", 2) 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) // Check for slice updates (frequency changes)
if strings.Contains(msg, "slice") { if strings.Contains(msg, "slice") {
if rfFreq, ok := statusMap["RF_frequency"]; ok { 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=<name> serial=<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) { func (c *Client) GetStatus() (*Status, error) {
c.statusMu.RLock() c.statusMu.RLock()
defer c.statusMu.RUnlock() defer c.statusMu.RUnlock()
@@ -419,32 +304,3 @@ func (c *Client) GetStatus() (*Status, error) {
return &status, nil 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()
}
}

View File

@@ -14,9 +14,7 @@
$: if (status?.heading !== undefined && status?.heading !== null) { $: if (status?.heading !== undefined && status?.heading !== null) {
const newHeading = status.heading; const newHeading = status.heading;
const oldHeading = heading; const oldHeading = heading;
console.log(`RotatorGenius heading update: ${oldHeading} -> ${newHeading}`);
if (heading === null) { if (heading === null) {
// First time: accept any value // First time: accept any value
heading = newHeading; heading = newHeading;
@@ -56,7 +54,7 @@
try { try {
hasTarget = true; // Mark that we have a target hasTarget = true; // Mark that we have a target
// Subtract 10 degrees to compensate for rotator momentum // Subtract 10 degrees to compensate for rotator momentum
const adjustedHeading = (targetHeading - 10 + 360) % 360; const adjustedHeading = (targetHeading + 360) % 360;
await api.rotator.setHeading(adjustedHeading); await api.rotator.setHeading(adjustedHeading);
} catch (err) { } catch (err) {
console.error('Failed to set heading:', err); console.error('Failed to set heading:', err);