913 lines
22 KiB
Go
913 lines
22 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)
|
|
|
|
lastStatus *Status
|
|
statusMu sync.RWMutex
|
|
|
|
cmdSeq int
|
|
cmdSeqMu sync.Mutex
|
|
|
|
running bool
|
|
stopChan chan struct{}
|
|
|
|
// Reconnection settings
|
|
reconnectInterval time.Duration
|
|
reconnectAttempts int // Track attempts for logging
|
|
maxReconnectDelay time.Duration
|
|
|
|
// Radio info from "info" command
|
|
radioInfo map[string]string
|
|
radioInfoMu sync.RWMutex
|
|
lastInfoCheck time.Time
|
|
infoCheckTimer *time.Timer
|
|
|
|
// Callbacks
|
|
onFrequencyChange func(freqMHz float64)
|
|
checkTransmitAllowed func() bool // Returns true if transmit allowed (motors not moving)
|
|
|
|
activeSlices []int // Liste des slices actives [0, 1, 2, 3]
|
|
activeSlicesMu sync.RWMutex
|
|
sliceListTimer *time.Timer // Timer pour vérifier périodiquement les slices
|
|
}
|
|
|
|
func New(host string, port int) *Client {
|
|
return &Client{
|
|
host: host,
|
|
port: port,
|
|
stopChan: make(chan struct{}),
|
|
reconnectInterval: 5 * time.Second,
|
|
maxReconnectDelay: 60 * time.Second,
|
|
radioInfo: make(map[string]string),
|
|
activeSlices: []int{}, // Initialiser vide
|
|
lastStatus: &Status{
|
|
Connected: false,
|
|
RadioOn: false,
|
|
},
|
|
}
|
|
}
|
|
|
|
// SetReconnectInterval sets the reconnection interval (default 5 seconds)
|
|
func (c *Client) SetReconnectInterval(interval time.Duration) {
|
|
c.reconnectInterval = interval
|
|
}
|
|
|
|
// SetMaxReconnectDelay sets the maximum delay for exponential backoff (default 60 seconds)
|
|
func (c *Client) SetMaxReconnectDelay(delay time.Duration) {
|
|
c.maxReconnectDelay = delay
|
|
}
|
|
|
|
// SetFrequencyChangeCallback sets the callback function called when frequency changes
|
|
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()
|
|
|
|
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)
|
|
c.reconnectAttempts = 0 // Reset attempts on successful connection
|
|
|
|
log.Println("FlexRadio: TCP connection established")
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Start() error {
|
|
if c.running {
|
|
return nil
|
|
}
|
|
|
|
// Try initial connection
|
|
if err := c.Connect(); err != nil {
|
|
log.Printf("FlexRadio: Initial connection failed: %v", err)
|
|
// Don't return error, let reconnection handle it
|
|
}
|
|
|
|
// Update connected status
|
|
c.statusMu.Lock()
|
|
if c.lastStatus != nil {
|
|
c.lastStatus.Connected = (c.conn != nil)
|
|
c.lastStatus.RadioOn = false // Will be updated by checkRadioStatus
|
|
}
|
|
c.statusMu.Unlock()
|
|
|
|
c.running = true
|
|
|
|
// Start message listener
|
|
go c.messageLoop()
|
|
|
|
// Start reconnection monitor
|
|
go c.reconnectionMonitor()
|
|
|
|
// Start radio status checker (checks if radio is actually on)
|
|
go c.radioStatusChecker()
|
|
|
|
// Try to get initial radio info and subscribe to slices
|
|
if c.conn != nil {
|
|
go func() {
|
|
// Petite pause pour laisser la connexion s'établir
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// D'abord vérifier le statut de la radio
|
|
c.checkRadioStatus()
|
|
|
|
// Puis s'abonner aux updates des slices
|
|
time.Sleep(500 * time.Millisecond)
|
|
log.Println("FlexRadio: Subscribing to slice updates...")
|
|
|
|
c.writeMu.Lock()
|
|
defer c.writeMu.Unlock()
|
|
|
|
c.connMu.Lock()
|
|
conn := c.conn
|
|
c.connMu.Unlock()
|
|
|
|
if conn != nil {
|
|
seq := c.getNextSeq()
|
|
subscribeCmd := fmt.Sprintf("C%d|sub slice all\n", seq)
|
|
|
|
conn.SetWriteDeadline(time.Now().Add(2 * time.Second))
|
|
_, err := conn.Write([]byte(subscribeCmd))
|
|
conn.SetWriteDeadline(time.Time{})
|
|
|
|
if err != nil {
|
|
log.Printf("FlexRadio: Failed to subscribe to slices: %v", err)
|
|
} else {
|
|
log.Println("FlexRadio: Successfully subscribed to slice updates")
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
go c.sliceListChecker()
|
|
|
|
return nil
|
|
}
|
|
|
|
// sliceListChecker vérifie périodiquement la liste des slices
|
|
func (c *Client) sliceListChecker() {
|
|
// Vérifier toutes les 10 secondes
|
|
c.sliceListTimer = time.NewTimer(10 * time.Second)
|
|
|
|
for c.running {
|
|
select {
|
|
case <-c.sliceListTimer.C:
|
|
if c.IsRadioOn() {
|
|
c.getActiveSlices()
|
|
}
|
|
c.sliceListTimer.Reset(10 * time.Second)
|
|
case <-c.stopChan:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) Stop() {
|
|
if !c.running {
|
|
return
|
|
}
|
|
|
|
c.running = false
|
|
close(c.stopChan)
|
|
|
|
// Stop info check timer
|
|
if c.infoCheckTimer != nil {
|
|
c.infoCheckTimer.Stop()
|
|
}
|
|
|
|
c.connMu.Lock()
|
|
if c.conn != nil {
|
|
c.conn.Close()
|
|
c.conn = nil
|
|
c.reader = nil
|
|
}
|
|
c.connMu.Unlock()
|
|
|
|
// Update status
|
|
c.statusMu.Lock()
|
|
if c.lastStatus != nil {
|
|
c.lastStatus.Connected = false
|
|
c.lastStatus.RadioOn = false
|
|
c.lastStatus.RadioInfo = "Disconnected"
|
|
}
|
|
c.statusMu.Unlock()
|
|
}
|
|
|
|
// radioStatusChecker periodically checks if the radio is actually powered on
|
|
func (c *Client) radioStatusChecker() {
|
|
// Check every 10 seconds
|
|
c.infoCheckTimer = time.NewTimer(10 * time.Second)
|
|
|
|
for c.running {
|
|
select {
|
|
case <-c.infoCheckTimer.C:
|
|
c.checkRadioStatus()
|
|
c.infoCheckTimer.Reset(10 * time.Second)
|
|
case <-c.stopChan:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkRadioStatus sends "info" command to check if radio is actually powered on
|
|
func (c *Client) checkRadioStatus() {
|
|
c.writeMu.Lock()
|
|
defer c.writeMu.Unlock()
|
|
|
|
c.connMu.Lock()
|
|
conn := c.conn
|
|
c.connMu.Unlock()
|
|
|
|
if conn == nil {
|
|
c.updateRadioStatus(false, "No TCP connection")
|
|
return
|
|
}
|
|
|
|
seq := c.getNextSeq()
|
|
infoCmd := fmt.Sprintf("C%d|info\n", seq)
|
|
|
|
log.Printf("FlexRadio: Checking radio status with 'info' command...")
|
|
|
|
// Set timeout for the check
|
|
conn.SetWriteDeadline(time.Now().Add(2 * time.Second))
|
|
_, err := conn.Write([]byte(infoCmd))
|
|
conn.SetWriteDeadline(time.Time{}) // Clear deadline
|
|
|
|
if err != nil {
|
|
log.Printf("FlexRadio: Failed to send info command: %v", err)
|
|
c.updateRadioStatus(false, "Failed to send info command")
|
|
return
|
|
}
|
|
|
|
c.lastInfoCheck = time.Now()
|
|
log.Println("FlexRadio: Info command sent, waiting for response...")
|
|
}
|
|
|
|
// updateRadioStatus updates the radio status based on info command response
|
|
func (c *Client) updateRadioStatus(isOn bool, info string) {
|
|
c.statusMu.Lock()
|
|
defer c.statusMu.Unlock()
|
|
|
|
if c.lastStatus != nil {
|
|
c.lastStatus.RadioOn = isOn
|
|
c.lastStatus.RadioInfo = info
|
|
|
|
// Update callsign and model from radioInfo if available
|
|
c.radioInfoMu.RLock()
|
|
if callsign, ok := c.radioInfo["callsign"]; ok {
|
|
c.lastStatus.Callsign = callsign
|
|
}
|
|
if model, ok := c.radioInfo["model"]; ok {
|
|
c.lastStatus.Model = model
|
|
}
|
|
if softwareVer, ok := c.radioInfo["software_ver"]; ok {
|
|
c.lastStatus.SoftwareVer = softwareVer
|
|
}
|
|
if numSlicesStr, ok := c.radioInfo["num_slice"]; ok {
|
|
if numSlices, err := strconv.Atoi(numSlicesStr); err == nil {
|
|
c.lastStatus.NumSlices = numSlices
|
|
}
|
|
}
|
|
c.radioInfoMu.RUnlock()
|
|
|
|
// If radio is on but no frequency, update info message
|
|
if isOn && c.lastStatus.Frequency == 0 {
|
|
c.lastStatus.RadioInfo = "Radio is on without any active slice"
|
|
}
|
|
}
|
|
}
|
|
|
|
// reconnectionMonitor handles automatic reconnection attempts
|
|
func (c *Client) reconnectionMonitor() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Printf("FlexRadio: Recovered from panic in reconnectionMonitor: %v", r)
|
|
}
|
|
}()
|
|
log.Println("FlexRadio: Reconnection monitor started")
|
|
|
|
for c.running {
|
|
c.connMu.Lock()
|
|
connected := (c.conn != nil)
|
|
c.connMu.Unlock()
|
|
|
|
if !connected {
|
|
c.reconnectAttempts++
|
|
|
|
// Calculate delay with exponential backoff
|
|
delay := c.calculateReconnectDelay()
|
|
|
|
log.Printf("FlexRadio: Attempting to reconnect in %v (attempt %d)...", delay, c.reconnectAttempts)
|
|
|
|
select {
|
|
case <-time.After(delay):
|
|
if err := c.reconnect(); err != nil {
|
|
log.Printf("FlexRadio: Reconnection attempt %d failed: %v", c.reconnectAttempts, err)
|
|
} else {
|
|
log.Printf("FlexRadio: Reconnected successfully on attempt %d", c.reconnectAttempts)
|
|
c.reconnectAttempts = 0 // Reset on success
|
|
|
|
// Check radio status after reconnection
|
|
go func() {
|
|
time.Sleep(500 * time.Millisecond)
|
|
c.checkRadioStatus()
|
|
}()
|
|
}
|
|
case <-c.stopChan:
|
|
return
|
|
}
|
|
} else {
|
|
// If connected, wait a bit before checking again
|
|
select {
|
|
case <-time.After(10 * time.Second):
|
|
// Just check connection status periodically
|
|
case <-c.stopChan:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// calculateReconnectDelay calculates delay with exponential backoff
|
|
func (c *Client) calculateReconnectDelay() time.Duration {
|
|
// Start with base interval
|
|
delay := c.reconnectInterval
|
|
|
|
// Apply exponential backoff: 5s, 10s, 20s, 40s, etc.
|
|
if c.reconnectAttempts > 1 {
|
|
multiplier := 1 << (c.reconnectAttempts - 1) // 2^(attempts-1)
|
|
delay = c.reconnectInterval * time.Duration(multiplier)
|
|
|
|
// Cap at maximum delay
|
|
if delay > c.maxReconnectDelay {
|
|
delay = c.maxReconnectDelay
|
|
}
|
|
}
|
|
|
|
return delay
|
|
}
|
|
|
|
// reconnect attempts to establish a new connection
|
|
func (c *Client) reconnect() error {
|
|
c.connMu.Lock()
|
|
defer c.connMu.Unlock()
|
|
|
|
// Close existing connection if any
|
|
if c.conn != nil {
|
|
c.conn.Close()
|
|
c.conn = nil
|
|
c.reader = nil
|
|
}
|
|
|
|
addr := fmt.Sprintf("%s:%d", c.host, c.port)
|
|
log.Printf("FlexRadio: Reconnecting to %s...", addr)
|
|
|
|
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
|
if err != nil {
|
|
// Update status
|
|
c.statusMu.Lock()
|
|
if c.lastStatus != nil {
|
|
c.lastStatus.Connected = false
|
|
c.lastStatus.RadioOn = false
|
|
c.lastStatus.RadioInfo = "Disconnected"
|
|
}
|
|
c.statusMu.Unlock()
|
|
return fmt.Errorf("reconnect failed: %w", err)
|
|
}
|
|
|
|
c.conn = conn
|
|
c.reader = bufio.NewReader(conn)
|
|
|
|
// Update TCP connection status
|
|
c.statusMu.Lock()
|
|
if c.lastStatus != nil {
|
|
c.lastStatus.Connected = true
|
|
c.lastStatus.RadioInfo = "TCP connected, checking radio..."
|
|
}
|
|
c.statusMu.Unlock()
|
|
|
|
go func() {
|
|
time.Sleep(1 * time.Second)
|
|
c.checkRadioStatus() // Envoie la commande info
|
|
time.Sleep(500 * time.Millisecond)
|
|
c.getActiveSlices() // Demande la liste des slices
|
|
}()
|
|
|
|
log.Println("FlexRadio: TCP connection reestablished")
|
|
return nil
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
// Vérifier si la connexion est encore valide
|
|
// en essayant de lire l'adresse distante
|
|
if conn.RemoteAddr() == nil {
|
|
c.connMu.Lock()
|
|
if c.conn != nil {
|
|
c.conn.Close()
|
|
c.conn = nil
|
|
c.reader = nil
|
|
}
|
|
c.connMu.Unlock()
|
|
return "", fmt.Errorf("connection closed")
|
|
}
|
|
|
|
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 {
|
|
// Mark connection as broken
|
|
c.connMu.Lock()
|
|
if c.conn != nil {
|
|
c.conn.Close()
|
|
c.conn = nil
|
|
c.reader = nil
|
|
}
|
|
c.connMu.Unlock()
|
|
|
|
// Update status
|
|
c.statusMu.Lock()
|
|
if c.lastStatus != nil {
|
|
c.lastStatus.Connected = false
|
|
c.lastStatus.RadioOn = false
|
|
c.lastStatus.RadioInfo = "Connection lost"
|
|
}
|
|
c.statusMu.Unlock()
|
|
|
|
return "", fmt.Errorf("failed to send command: %w", err)
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
func (c *Client) messageLoop() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Printf("FlexRadio: Recovered from panic in messageLoop: %v", r)
|
|
}
|
|
}()
|
|
log.Println("FlexRadio: Message loop started")
|
|
|
|
for c.running {
|
|
c.connMu.Lock()
|
|
if c.conn == nil || c.reader == nil {
|
|
c.connMu.Unlock()
|
|
// Connection is broken, wait for reconnection
|
|
time.Sleep(1 * time.Second)
|
|
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)
|
|
|
|
// Mark connection as broken
|
|
c.connMu.Lock()
|
|
if c.conn != nil {
|
|
c.conn.Close()
|
|
c.conn = nil
|
|
c.reader = nil
|
|
}
|
|
c.connMu.Unlock()
|
|
|
|
// Update status
|
|
c.statusMu.Lock()
|
|
if c.lastStatus != nil {
|
|
c.lastStatus.Connected = false
|
|
c.lastStatus.RadioOn = false
|
|
c.lastStatus.RadioInfo = "Connection lost"
|
|
}
|
|
c.statusMu.Unlock()
|
|
continue
|
|
}
|
|
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
// DEBUG: Log tous les messages reçus
|
|
log.Printf("FlexRadio RAW: %s", line)
|
|
|
|
c.handleMessage(line)
|
|
}
|
|
|
|
log.Println("FlexRadio: Message loop stopped")
|
|
}
|
|
|
|
func (c *Client) getActiveSlices() error {
|
|
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()
|
|
sliceCmd := fmt.Sprintf("C%d|slice list\n", seq)
|
|
|
|
log.Printf("FlexRadio: Requesting slice list...")
|
|
|
|
conn.SetWriteDeadline(time.Now().Add(2 * time.Second))
|
|
_, err := conn.Write([]byte(sliceCmd))
|
|
conn.SetWriteDeadline(time.Time{})
|
|
|
|
if err != nil {
|
|
log.Printf("FlexRadio: Failed to send slice list command: %v", err)
|
|
return err
|
|
}
|
|
|
|
log.Printf("FlexRadio: Slice list command sent (seq=%d)", seq)
|
|
return nil
|
|
}
|
|
|
|
// parseSliceListResponse parse la réponse de "slice list"
|
|
func (c *Client) parseSliceListResponse(data string) {
|
|
// Format: "0" ou "0 1" ou "0 1 2" ou "0 1 2 3"
|
|
slices := []int{}
|
|
|
|
if strings.TrimSpace(data) != "" {
|
|
parts := strings.Fields(data)
|
|
for _, part := range parts {
|
|
if sliceNum, err := strconv.Atoi(part); err == nil {
|
|
slices = append(slices, sliceNum)
|
|
}
|
|
}
|
|
}
|
|
|
|
c.activeSlicesMu.Lock()
|
|
c.activeSlices = slices
|
|
c.activeSlicesMu.Unlock()
|
|
|
|
c.statusMu.Lock()
|
|
if c.lastStatus != nil {
|
|
c.lastStatus.ActiveSlices = len(slices)
|
|
|
|
// Si plus de slices actives, réinitialiser fréquence et mode
|
|
if len(slices) == 0 {
|
|
c.lastStatus.Frequency = 0
|
|
c.lastStatus.Mode = ""
|
|
c.lastStatus.RadioInfo = "Radio is on without any active slice"
|
|
}
|
|
// Note: La fréquence/mode seront mis à jour par les messages de statut des slices
|
|
}
|
|
c.statusMu.Unlock()
|
|
|
|
log.Printf("FlexRadio: Active slices updated: %v (total: %d)", slices, len(slices))
|
|
}
|
|
|
|
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>
|
|
// Example: R21|0|000000F4
|
|
parts := strings.SplitN(msg, "|", 3)
|
|
if len(parts) < 3 {
|
|
return
|
|
}
|
|
|
|
seqStr := strings.TrimPrefix(parts[0], "R")
|
|
status := parts[1]
|
|
data := parts[2]
|
|
|
|
// Log the sequence for debugging
|
|
seq, err := strconv.Atoi(seqStr)
|
|
if err == nil {
|
|
log.Printf("FlexRadio: Response for sequence %d, status=%s", seq, status)
|
|
}
|
|
|
|
if status != "0" {
|
|
log.Printf("FlexRadio: Command error: status=%s, message=%s", status, msg)
|
|
return
|
|
}
|
|
|
|
// Check if this is an info response
|
|
if strings.Contains(data, "model=") {
|
|
log.Printf("FlexRadio: Received info response for sequence %d", seq)
|
|
c.parseInfoResponse(data)
|
|
return
|
|
}
|
|
|
|
// Check if this is a slice list response
|
|
// La réponse est juste une liste de nombres: "0" ou "0 1" etc.
|
|
// On vérifie si c'est une réponse numérique simple
|
|
if isSliceListResponse(data) {
|
|
log.Printf("FlexRadio: Received slice list response: %s", data)
|
|
c.parseSliceListResponse(data)
|
|
return
|
|
}
|
|
}
|
|
|
|
// isSliceListResponse vérifie si la réponse est une liste de slices
|
|
func isSliceListResponse(data string) bool {
|
|
data = strings.TrimSpace(data)
|
|
if data == "" {
|
|
return true // Liste vide
|
|
}
|
|
|
|
// Vérifie si c'est une liste de nombres séparés par des espaces
|
|
parts := strings.Fields(data)
|
|
for _, part := range parts {
|
|
if _, err := strconv.Atoi(part); err != nil {
|
|
return false // Pas un nombre
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *Client) parseInfoResponse(data string) {
|
|
// Parse key=value pairs from info response
|
|
// Example: model="FLEX-8600",chassis_serial="2725-1213-8600-3867",name="F4BPO-8600",callsign="F4BPO",...
|
|
|
|
log.Printf("FlexRadio: Parsing info response: %s", data)
|
|
|
|
// Split by comma, but handle quoted values
|
|
pairs := []string{}
|
|
current := ""
|
|
inQuotes := false
|
|
|
|
for _, char := range data {
|
|
if char == '"' {
|
|
inQuotes = !inQuotes
|
|
}
|
|
if char == ',' && !inQuotes {
|
|
pairs = append(pairs, strings.TrimSpace(current))
|
|
current = ""
|
|
} else {
|
|
current += string(char)
|
|
}
|
|
}
|
|
if current != "" {
|
|
pairs = append(pairs, strings.TrimSpace(current))
|
|
}
|
|
|
|
// Parse each pair
|
|
c.radioInfoMu.Lock()
|
|
c.radioInfo = make(map[string]string)
|
|
|
|
for _, pair := range pairs {
|
|
kv := strings.SplitN(pair, "=", 2)
|
|
if len(kv) == 2 {
|
|
key := strings.TrimSpace(kv[0])
|
|
value := strings.TrimSpace(kv[1])
|
|
|
|
// Remove quotes if present
|
|
if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
|
|
value = value[1 : len(value)-1]
|
|
}
|
|
|
|
c.radioInfo[key] = value
|
|
log.Printf("FlexRadio Info: %s = %s", key, value)
|
|
}
|
|
}
|
|
|
|
c.radioInfoMu.Unlock()
|
|
|
|
// Update radio status - radio is definitely on if we got info response
|
|
c.updateRadioStatus(true, "Radio is on")
|
|
|
|
// NE PAS remettre ActiveSlices à 0 ici !
|
|
// La réponse info ne contient pas l'état des slices actives
|
|
// On garde la valeur actuelle de ActiveSlices
|
|
|
|
c.statusMu.Lock()
|
|
if c.lastStatus != nil {
|
|
// Update callsign and model
|
|
if callsign, ok := c.radioInfo["callsign"]; ok {
|
|
c.lastStatus.Callsign = callsign
|
|
}
|
|
if model, ok := c.radioInfo["model"]; ok {
|
|
c.lastStatus.Model = model
|
|
}
|
|
if softwareVer, ok := c.radioInfo["software_ver"]; ok {
|
|
c.lastStatus.SoftwareVer = softwareVer
|
|
}
|
|
if numSlicesStr, ok := c.radioInfo["num_slice"]; ok {
|
|
if numSlices, err := strconv.Atoi(numSlicesStr); err == nil {
|
|
c.lastStatus.NumSlices = numSlices
|
|
}
|
|
}
|
|
|
|
// NE PAS modifier ActiveSlices - garder la valeur actuelle
|
|
// c.lastStatus.ActiveSlices reste inchangé
|
|
|
|
// Mettre à jour RadioInfo en fonction de l'état actuel
|
|
if c.lastStatus.ActiveSlices == 0 {
|
|
c.lastStatus.RadioInfo = "Radio is on without any active slice"
|
|
} else if c.lastStatus.Frequency > 0 {
|
|
c.lastStatus.RadioInfo = fmt.Sprintf("Active on %.3f MHz", c.lastStatus.Frequency)
|
|
} else {
|
|
c.lastStatus.RadioInfo = "Radio is on with active slice(s)"
|
|
}
|
|
}
|
|
c.statusMu.Unlock()
|
|
|
|
go func() {
|
|
time.Sleep(500 * time.Millisecond) // Petite pause
|
|
c.getActiveSlices()
|
|
}()
|
|
|
|
log.Printf("FlexRadio: Radio is powered on and responding (total slices: %d, active slices: %d)",
|
|
c.lastStatus.NumSlices, c.lastStatus.ActiveSlices)
|
|
}
|
|
|
|
func (c *Client) GetStatus() (*Status, error) {
|
|
c.statusMu.RLock()
|
|
defer c.statusMu.RUnlock()
|
|
|
|
if c.lastStatus == nil {
|
|
return &Status{
|
|
Connected: false,
|
|
RadioOn: false,
|
|
RadioInfo: "Not initialized",
|
|
}, nil
|
|
}
|
|
|
|
// Create a copy
|
|
status := *c.lastStatus
|
|
|
|
return &status, nil
|
|
}
|
|
|
|
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 slice updates (frequency changes)
|
|
if strings.Contains(msg, "slice") {
|
|
log.Printf("FlexRadio: Slice status update received")
|
|
|
|
c.statusMu.Lock()
|
|
defer c.statusMu.Unlock()
|
|
|
|
// Update frequency if present
|
|
if rfFreq, ok := statusMap["RF_frequency"]; ok {
|
|
freq, err := strconv.ParseFloat(rfFreq, 64)
|
|
if err == nil && freq > 0 {
|
|
oldFreq := c.lastStatus.Frequency
|
|
c.lastStatus.Frequency = freq
|
|
c.lastStatus.RadioInfo = fmt.Sprintf("Active on %.3f MHz", freq)
|
|
|
|
log.Printf("FlexRadio: Frequency updated from %.6f to %.6f MHz", oldFreq, freq)
|
|
|
|
// Trigger callback for immediate auto-track
|
|
if c.onFrequencyChange != nil {
|
|
go c.onFrequencyChange(freq)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update mode if present
|
|
if mode, ok := statusMap["mode"]; ok {
|
|
c.lastStatus.Mode = mode
|
|
log.Printf("FlexRadio: Mode updated to %s", mode)
|
|
}
|
|
|
|
// Update TX status if present
|
|
if tx, ok := statusMap["tx"]; ok {
|
|
c.lastStatus.Tx = (tx == "1")
|
|
log.Printf("FlexRadio: TX status updated to %v", c.lastStatus.Tx)
|
|
}
|
|
}
|
|
|
|
// Check for interlock updates (TX state)
|
|
if strings.Contains(msg, "interlock") {
|
|
log.Printf("FlexRadio: Interlock status update received")
|
|
|
|
c.statusMu.Lock()
|
|
defer c.statusMu.Unlock()
|
|
|
|
// Update TX status based on interlock state
|
|
if state, ok := statusMap["state"]; ok {
|
|
// Les états possibles: RECEIVE, TRANSMIT, TUNE, etc.
|
|
// RECEIVE = réception, TRANSMIT = émission
|
|
c.lastStatus.Tx = (state == "TRANSMIT" || state == "TUNE")
|
|
log.Printf("FlexRadio: Interlock state: %s, TX=%v", state, c.lastStatus.Tx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// IsRadioOn returns true if radio is powered on and responding
|
|
func (c *Client) IsRadioOn() bool {
|
|
c.statusMu.RLock()
|
|
defer c.statusMu.RUnlock()
|
|
|
|
if c.lastStatus == nil {
|
|
return false
|
|
}
|
|
|
|
return c.lastStatus.RadioOn
|
|
}
|