451 lines
10 KiB
Go
451 lines
10 KiB
Go
package flexradio
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Client struct {
|
|
host string
|
|
port int
|
|
|
|
conn net.Conn
|
|
reader *bufio.Reader
|
|
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
|
|
|
|
cmdSeq int
|
|
cmdSeqMu sync.Mutex
|
|
|
|
running bool
|
|
stopChan chan struct{}
|
|
|
|
// Callbacks
|
|
checkTransmitAllowed func() bool
|
|
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{}),
|
|
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
|
|
}
|
|
|
|
func (c *Client) Connect() error {
|
|
c.connMu.Lock()
|
|
defer c.connMu.Unlock()
|
|
|
|
if c.conn != nil {
|
|
return nil
|
|
}
|
|
|
|
addr := fmt.Sprintf("%s:%d", c.host, c.port)
|
|
log.Printf("FlexRadio: Connecting to %s...", addr)
|
|
|
|
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect: %w", err)
|
|
}
|
|
|
|
c.conn = conn
|
|
c.reader = bufio.NewReader(conn)
|
|
|
|
log.Println("FlexRadio: Connected successfully")
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Start() error {
|
|
if c.running {
|
|
return nil
|
|
}
|
|
|
|
if err := c.Connect(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update connected status
|
|
c.statusMu.Lock()
|
|
if c.lastStatus != nil {
|
|
c.lastStatus.Connected = true
|
|
}
|
|
c.statusMu.Unlock()
|
|
|
|
c.running = true
|
|
|
|
// 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
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Stop() {
|
|
if !c.running {
|
|
return
|
|
}
|
|
|
|
c.running = false
|
|
close(c.stopChan)
|
|
|
|
c.connMu.Lock()
|
|
if c.conn != nil {
|
|
c.conn.Close()
|
|
c.conn = nil
|
|
c.reader = nil
|
|
}
|
|
c.connMu.Unlock()
|
|
|
|
// Update connected status
|
|
c.statusMu.Lock()
|
|
if c.lastStatus != nil {
|
|
c.lastStatus.Connected = false
|
|
}
|
|
c.statusMu.Unlock()
|
|
}
|
|
|
|
func (c *Client) getNextSeq() int {
|
|
c.cmdSeqMu.Lock()
|
|
defer c.cmdSeqMu.Unlock()
|
|
c.cmdSeq++
|
|
return c.cmdSeq
|
|
}
|
|
|
|
func (c *Client) sendCommand(cmd string) (string, error) {
|
|
// Use writeMu instead of connMu to avoid blocking on messageLoop reads
|
|
c.writeMu.Lock()
|
|
defer c.writeMu.Unlock()
|
|
|
|
c.connMu.Lock()
|
|
conn := c.conn
|
|
c.connMu.Unlock()
|
|
|
|
if conn == nil {
|
|
return "", fmt.Errorf("not connected")
|
|
}
|
|
|
|
seq := c.getNextSeq()
|
|
fullCmd := fmt.Sprintf("C%d|%s\n", seq, cmd)
|
|
|
|
log.Printf("FlexRadio TX: %s", strings.TrimSpace(fullCmd))
|
|
|
|
_, err := conn.Write([]byte(fullCmd))
|
|
if err != nil {
|
|
c.connMu.Lock()
|
|
c.conn = nil
|
|
c.reader = nil
|
|
c.connMu.Unlock()
|
|
return "", fmt.Errorf("failed to send command: %w", err)
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
func (c *Client) messageLoop() {
|
|
log.Println("FlexRadio: Message loop started")
|
|
|
|
for c.running {
|
|
c.connMu.Lock()
|
|
if c.conn == nil || c.reader == nil {
|
|
c.connMu.Unlock()
|
|
time.Sleep(1 * time.Second)
|
|
if err := c.Connect(); err != nil {
|
|
log.Printf("FlexRadio: Reconnect failed: %v", err)
|
|
continue
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Set read deadline to allow periodic checks
|
|
c.conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
|
|
line, err := c.reader.ReadString('\n')
|
|
c.connMu.Unlock()
|
|
|
|
if err != nil {
|
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
|
// Timeout is expected, continue
|
|
continue
|
|
}
|
|
log.Printf("FlexRadio: Read error: %v", err)
|
|
c.connMu.Lock()
|
|
if c.conn != nil {
|
|
c.conn.Close()
|
|
c.conn = nil
|
|
c.reader = nil
|
|
}
|
|
c.connMu.Unlock()
|
|
|
|
// Update connected status
|
|
c.statusMu.Lock()
|
|
if c.lastStatus != nil {
|
|
c.lastStatus.Connected = false
|
|
}
|
|
c.statusMu.Unlock()
|
|
continue
|
|
}
|
|
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
log.Printf("FlexRadio RX: %s", line)
|
|
c.handleMessage(line)
|
|
}
|
|
|
|
log.Println("FlexRadio: Message loop stopped")
|
|
}
|
|
|
|
func (c *Client) handleMessage(msg string) {
|
|
// Response format: R<seq>|<status>|<data>
|
|
if strings.HasPrefix(msg, "R") {
|
|
c.handleResponse(msg)
|
|
return
|
|
}
|
|
|
|
// Status format: S<handle>|<key>=<value> ...
|
|
if strings.HasPrefix(msg, "S") {
|
|
c.handleStatus(msg)
|
|
return
|
|
}
|
|
|
|
// Version/handle format: V<version>|H<handle>
|
|
if strings.HasPrefix(msg, "V") {
|
|
log.Printf("FlexRadio: Version/Handle received: %s", msg)
|
|
return
|
|
}
|
|
|
|
// Message format: M<handle>|<message>
|
|
if strings.HasPrefix(msg, "M") {
|
|
log.Printf("FlexRadio: Message: %s", msg)
|
|
return
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// Format: S<handle>|<key>=<value> ...
|
|
parts := strings.SplitN(msg, "|", 2)
|
|
if len(parts) < 2 {
|
|
return
|
|
}
|
|
|
|
data := parts[1]
|
|
|
|
// Parse key=value pairs
|
|
pairs := strings.Fields(data)
|
|
statusMap := make(map[string]string)
|
|
|
|
for _, pair := range pairs {
|
|
kv := strings.SplitN(pair, "=", 2)
|
|
if len(kv) == 2 {
|
|
statusMap[kv[0]] = kv[1]
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
freq, err := strconv.ParseFloat(rfFreq, 64)
|
|
if err == nil {
|
|
c.statusMu.Lock()
|
|
oldFreq := c.lastStatus.Frequency
|
|
c.lastStatus.Frequency = freq
|
|
c.statusMu.Unlock()
|
|
|
|
// Only log significant frequency changes (> 1 kHz)
|
|
if oldFreq == 0 || (freq-oldFreq)*(freq-oldFreq) > 0.001 {
|
|
log.Printf("FlexRadio: Frequency updated to %.6f MHz", freq)
|
|
|
|
// Trigger callback for immediate auto-track
|
|
if c.onFrequencyChange != nil {
|
|
go c.onFrequencyChange(freq)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
c.statusMu.RLock()
|
|
defer c.statusMu.RUnlock()
|
|
|
|
if c.lastStatus == nil {
|
|
return &Status{Connected: false}, nil
|
|
}
|
|
|
|
// Create a copy
|
|
status := *c.lastStatus
|
|
|
|
// DON'T lock connMu here - it causes 4-second blocking!
|
|
// The messageLoop updates Connected status, and we trust the cached value
|
|
|
|
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()
|
|
}
|
|
}
|