diff --git a/internal/api/device_manager.go b/internal/api/device_manager.go index 8f5aa54..2c0fda3 100644 --- a/internal/api/device_manager.go +++ b/internal/api/device_manager.go @@ -126,6 +126,26 @@ func (dm *DeviceManager) Initialize() error { 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 diff --git a/internal/devices/flexradio/flexradio.go b/internal/devices/flexradio/flexradio.go index 276f714..625829e 100644 --- a/internal/devices/flexradio/flexradio.go +++ b/internal/devices/flexradio/flexradio.go @@ -29,15 +29,22 @@ type Client struct { running bool stopChan chan struct{} + // Interlock + interlockID string // ID retourné par la radio (ex: "000000F4") + interlockName string // Nom de notre interlock + interlockMu sync.RWMutex // Protection pour l'ID + // Callbacks - onFrequencyChange func(freqMHz float64) + onFrequencyChange func(freqMHz float64) + checkTransmitAllowed func() bool // Returns true if transmit allowed (motors not moving) } func New(host string, port int, interlockName string) *Client { return &Client{ - host: host, - port: port, - stopChan: make(chan struct{}), + host: host, + port: port, + interlockName: interlockName, + stopChan: make(chan struct{}), lastStatus: &Status{ Connected: false, }, @@ -49,6 +56,11 @@ func (c *Client) SetFrequencyChangeCallback(callback func(freqMHz float64)) { c.onFrequencyChange = callback } +// SetTransmitCheckCallback sets the callback to check if transmit is allowed +func (c *Client) SetTransmitCheckCallback(callback func() bool) { + c.checkTransmitAllowed = callback +} + func (c *Client) Connect() error { c.connMu.Lock() defer c.connMu.Unlock() @@ -93,6 +105,12 @@ func (c *Client) Start() error { // Start message listener go c.messageLoop() + // Create interlock + log.Printf("FlexRadio: Creating interlock '%s'...", c.interlockName) + if err := c.createInterlock(); err != nil { + log.Printf("FlexRadio: Warning - failed to create interlock: %v", err) + } + // Subscribe to slice updates for frequency tracking log.Println("FlexRadio: Subscribing to slice updates...") _, err := c.sendCommand("sub slice all") @@ -213,6 +231,7 @@ func (c *Client) messageLoop() { continue } + log.Printf("FlexRadio RX: %s", line) c.handleMessage(line) } @@ -222,6 +241,7 @@ func (c *Client) messageLoop() { func (c *Client) handleMessage(msg string) { // Response format: R|| if strings.HasPrefix(msg, "R") { + c.handleResponse(msg) return } @@ -244,6 +264,32 @@ func (c *Client) handleMessage(msg string) { } } +func (c *Client) handleResponse(msg string) { + // Format: R|| + // Example: R21|0|000000F4 + parts := strings.SplitN(msg, "|", 3) + if len(parts) < 2 { + return + } + + status := parts[1] + if status != "0" { + log.Printf("FlexRadio: Command error: status=%s, message=%s", status, msg) + return + } + + // Check if this is interlock create response (has data in 3rd part) + if len(parts) >= 3 && parts[2] != "" { + interlockID := parts[2] + + c.interlockMu.Lock() + c.interlockID = interlockID + c.interlockMu.Unlock() + + log.Printf("FlexRadio: ✅ Interlock created successfully with ID: %s", interlockID) + } +} + func (c *Client) handleStatus(msg string) { // Format: S|= ... parts := strings.SplitN(msg, "|", 2) @@ -286,6 +332,18 @@ func (c *Client) handleStatus(msg string) { } } } + // Check for interlock PTT_REQUESTED + // Format: S0|interlock ... state=PTT_REQUESTED ... + if strings.Contains(msg, "interlock") { + if state, ok := statusMap["state"]; ok { + log.Printf("FlexRadio: Interlock state changed to: %s", state) + + if state == "PTT_REQUESTED" { + // PTT requested - we MUST respond within 500ms! + c.handlePTTRequest() + } + } + } } func (c *Client) GetStatus() (*Status, error) { @@ -304,3 +362,53 @@ func (c *Client) GetStatus() (*Status, error) { return &status, nil } + +// createInterlock creates an interlock on the radio +func (c *Client) createInterlock() error { + // Format: interlock create type=ANT name= serial=ShackMaster + // Type ANT = External antenna controller (state always controlled by us) + cmd := fmt.Sprintf("interlock create type=ANT name=%s serial=ShackMaster", c.interlockName) + + log.Printf("FlexRadio: Sending interlock create command: %s", cmd) + + _, err := c.sendCommand(cmd) + if err != nil { + return fmt.Errorf("failed to send interlock create: %w", err) + } + + // The response will be parsed in handleResponse() + // Format: R|0| (ex: R21|0|000000F4) + + return nil +} + +// handlePTTRequest is called when FlexRadio sends PTT_REQUESTED +// We MUST respond within 500ms with ready or not_ready +func (c *Client) handlePTTRequest() { + log.Println("FlexRadio: 🔴 PTT REQUESTED - checking if transmit 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'") + cmd := fmt.Sprintf("interlock ready %s", interlockID) + c.sendCommand(cmd) + } else { + log.Println("FlexRadio: ❌ Transmit BLOCKED - sending 'not_ready'") + cmd := fmt.Sprintf("interlock not_ready %s", interlockID) + c.sendCommand(cmd) + } +}