diff --git a/internal/devices/flexradio/flexradio.go b/internal/devices/flexradio/flexradio.go index 3ff67f9..59db910 100644 --- a/internal/devices/flexradio/flexradio.go +++ b/internal/devices/flexradio/flexradio.go @@ -9,6 +9,7 @@ import ( "strings" "sync" "time" + "unicode" ) type Client struct { @@ -17,8 +18,8 @@ type Client struct { conn net.Conn reader *bufio.Reader - connMu sync.Mutex // For connection management - writeMu sync.Mutex // For writing to connection (separate from reads) + connMu sync.Mutex + writeMu sync.Mutex lastStatus *Status statusMu sync.RWMutex @@ -29,24 +30,21 @@ type Client struct { running bool stopChan chan struct{} - // Reconnection settings reconnectInterval time.Duration - reconnectAttempts int // Track attempts for logging + reconnectAttempts int 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] + activeSlices []int activeSlicesMu sync.RWMutex - sliceListTimer *time.Timer // Timer pour vérifier périodiquement les slices + sliceListTimer *time.Timer + + onFrequencyChange func(freqMHz float64) + checkTransmitAllowed func() bool } func New(host string, port int) *Client { @@ -57,7 +55,7 @@ func New(host string, port int) *Client { reconnectInterval: 5 * time.Second, maxReconnectDelay: 60 * time.Second, radioInfo: make(map[string]string), - activeSlices: []int{}, // Initialiser vide + activeSlices: []int{}, lastStatus: &Status{ Connected: false, RadioOn: false, @@ -65,12 +63,12 @@ func New(host string, port int) *Client { } } -// SetReconnectInterval sets the reconnection interval (default 5 seconds) +// SetReconnectInterval sets the reconnection interval func (c *Client) SetReconnectInterval(interval time.Duration) { c.reconnectInterval = interval } -// SetMaxReconnectDelay sets the maximum delay for exponential backoff (default 60 seconds) +// SetMaxReconnectDelay sets the maximum delay for exponential backoff func (c *Client) SetMaxReconnectDelay(delay time.Duration) { c.maxReconnectDelay = delay } @@ -103,7 +101,7 @@ func (c *Client) Connect() error { c.conn = conn c.reader = bufio.NewReader(conn) - c.reconnectAttempts = 0 // Reset attempts on successful connection + c.reconnectAttempts = 0 log.Println("FlexRadio: TCP connection established") return nil @@ -117,14 +115,13 @@ func (c *Client) Start() error { // 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.lastStatus.RadioOn = false } c.statusMu.Unlock() @@ -136,69 +133,29 @@ func (c *Client) Start() error { // Start reconnection monitor go c.reconnectionMonitor() - // Start radio status checker (checks if radio is actually on) + // Start radio status checker go c.radioStatusChecker() + // Start slice list checker + go c.sliceListChecker() + // 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) + c.SendInfo() - // 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.SendSliceList() - 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") - } - } + time.Sleep(500 * time.Millisecond) + c.SubscribeToSlices() }() } - 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 @@ -207,10 +164,13 @@ func (c *Client) Stop() { c.running = false close(c.stopChan) - // Stop info check timer + // Stop timers if c.infoCheckTimer != nil { c.infoCheckTimer.Stop() } + if c.sliceListTimer != nil { + c.sliceListTimer.Stop() + } c.connMu.Lock() if c.conn != nil { @@ -226,28 +186,28 @@ func (c *Client) Stop() { c.lastStatus.Connected = false c.lastStatus.RadioOn = false c.lastStatus.RadioInfo = "Disconnected" + c.lastStatus.ActiveSlices = 0 + c.lastStatus.Frequency = 0 + c.lastStatus.Mode = "" + c.lastStatus.Tx = false } 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 - } - } +// Helper functions for common commands +func (c *Client) SendInfo() error { + return c.sendCommand("info") } -// checkRadioStatus sends "info" command to check if radio is actually powered on -func (c *Client) checkRadioStatus() { +func (c *Client) SendSliceList() error { + return c.sendCommand("slice list") +} + +func (c *Client) SubscribeToSlices() error { + return c.sendCommand("sub slice all") +} + +func (c *Client) sendCommand(cmd string) error { c.writeMu.Lock() defer c.writeMu.Unlock() @@ -256,215 +216,7 @@ func (c *Client) checkRadioStatus() { 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") + return fmt.Errorf("not connected") } seq := c.getNextSeq() @@ -492,44 +244,41 @@ func (c *Client) sendCommand(cmd string) (string, error) { } c.statusMu.Unlock() - return "", fmt.Errorf("failed to send command: %w", err) + return fmt.Errorf("failed to send command: %w", err) } - return "", nil + return nil +} + +func (c *Client) getNextSeq() int { + c.cmdSeqMu.Lock() + defer c.cmdSeqMu.Unlock() + c.cmdSeq++ + return c.cmdSeq } 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() @@ -538,7 +287,6 @@ func (c *Client) messageLoop() { } c.connMu.Unlock() - // Update status c.statusMu.Lock() if c.lastStatus != nil { c.lastStatus.Connected = false @@ -554,111 +302,42 @@ func (c *Client) messageLoop() { 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)) -} - +// Message handling - SIMPLIFIED VERSION func (c *Client) handleMessage(msg string) { - // Response format: R|| - if strings.HasPrefix(msg, "R") { - c.handleResponse(msg) + msg = strings.TrimSpace(msg) + if msg == "" { return } - // Status format: S|= ... - if strings.HasPrefix(msg, "S") { - c.handleStatus(msg) - return - } + // DEBUG: Log tous les messages reçus + log.Printf("FlexRadio RAW: %s", msg) - // Version/handle format: V|H - if strings.HasPrefix(msg, "V") { - log.Printf("FlexRadio: Version/Handle received: %s", msg) - return - } - - // Message format: M| - if strings.HasPrefix(msg, "M") { + // Router selon le premier caractère + switch msg[0] { + case 'R': // Réponse à une commande + c.handleCommandResponse(msg) + case 'S': // Message de statut + c.handleStatusMessage(msg) + case 'V': // Version/Handle + log.Printf("FlexRadio: Version/Handle: %s", msg) + case 'M': // Message général log.Printf("FlexRadio: Message: %s", msg) - return + default: + log.Printf("FlexRadio: Unknown message type: %s", msg) } } -func (c *Client) handleResponse(msg string) { +func (c *Client) handleCommandResponse(msg string) { // Format: R|| - // Example: R21|0|000000F4 parts := strings.SplitN(msg, "|", 3) if len(parts) < 3 { + log.Printf("FlexRadio: Malformed response: %s", msg) return } @@ -666,58 +345,122 @@ func (c *Client) handleResponse(msg string) { 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) - } + seq, _ := strconv.Atoi(seqStr) if status != "0" { - log.Printf("FlexRadio: Command error: status=%s, message=%s", status, msg) + log.Printf("FlexRadio: Command error (seq=%d): %s", seq, msg) return } - // Check if this is an info response - if strings.Contains(data, "model=") { - log.Printf("FlexRadio: Received info response for sequence %d", seq) + log.Printf("FlexRadio: Command success (seq=%d)", seq) + + // Identifier le type de réponse par son contenu + switch { + case strings.Contains(data, "model="): 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) + case isSliceListResponse(data): c.parseSliceListResponse(data) - return + + default: + log.Printf("FlexRadio: Generic response: %s", data) } } -// 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 + return true } - // 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 + for _, char := range data { + if !unicode.IsDigit(char) && char != ' ' { + return false } } 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",... +func (c *Client) handleStatusMessage(msg string) { + parts := strings.SplitN(msg, "|", 2) + if len(parts) < 2 { + return + } + handle := parts[0][1:] + data := parts[1] + + statusMap := make(map[string]string) + pairs := strings.Fields(data) + + for _, pair := range pairs { + if kv := strings.SplitN(pair, "=", 2); len(kv) == 2 { + statusMap[kv[0]] = kv[1] + } + } + + switch { + case strings.Contains(msg, "interlock"): + c.handleInterlockStatus(handle, statusMap) + + case strings.Contains(msg, "slice"): + c.handleSliceStatus(handle, statusMap) + + case strings.Contains(msg, "radio"): + c.handleRadioStatus(handle, statusMap) + + default: + log.Printf("FlexRadio: Unknown status (handle=%s): %s", handle, msg) + } +} + +func (c *Client) handleInterlockStatus(handle string, statusMap map[string]string) { + c.statusMu.Lock() + defer c.statusMu.Unlock() + + if state, ok := statusMap["state"]; ok { + c.lastStatus.Tx = (state == "TRANSMIT" || state == "TUNE") + log.Printf("FlexRadio: Interlock state=%s, TX=%v", state, c.lastStatus.Tx) + } +} + +func (c *Client) handleSliceStatus(handle string, statusMap map[string]string) { + c.statusMu.Lock() + defer c.statusMu.Unlock() + + if rfFreq, ok := statusMap["RF_frequency"]; ok { + if freq, err := strconv.ParseFloat(rfFreq, 64); err == nil && freq > 0 { + c.lastStatus.Frequency = freq + c.lastStatus.RadioInfo = fmt.Sprintf("Active on %.3f MHz", freq) + + if c.onFrequencyChange != nil { + go c.onFrequencyChange(freq) + } + } + } + + if mode, ok := statusMap["mode"]; ok { + c.lastStatus.Mode = mode + } + + if tx, ok := statusMap["tx"]; ok { + c.lastStatus.Tx = (tx == "1") + } +} + +func (c *Client) handleRadioStatus(handle string, statusMap map[string]string) { + if slices, ok := statusMap["slices"]; ok { + if num, err := strconv.Atoi(slices); err == nil { + c.statusMu.Lock() + c.lastStatus.NumSlices = num + c.statusMu.Unlock() + } + } +} + +func (c *Client) parseInfoResponse(data string) { log.Printf("FlexRadio: Parsing info response: %s", data) - // Split by comma, but handle quoted values pairs := []string{} current := "" inQuotes := false @@ -737,7 +480,6 @@ func (c *Client) parseInfoResponse(data string) { pairs = append(pairs, strings.TrimSpace(current)) } - // Parse each pair c.radioInfoMu.Lock() c.radioInfo = make(map[string]string) @@ -747,7 +489,6 @@ func (c *Client) parseInfoResponse(data string) { 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] } @@ -759,16 +500,54 @@ func (c *Client) parseInfoResponse(data string) { 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 + go func() { + time.Sleep(300 * time.Millisecond) + c.SendSliceList() + }() +} + +func (c *Client) parseSliceListResponse(data string) { + 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 { - // Update callsign and model + c.lastStatus.ActiveSlices = len(slices) + + if len(slices) == 0 { + c.lastStatus.Frequency = 0 + c.lastStatus.Mode = "" + c.lastStatus.RadioInfo = "Radio is on without any active slice" + } + } + c.statusMu.Unlock() + + log.Printf("FlexRadio: Active slices updated: %v (total: %d)", slices, len(slices)) +} + +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 + + c.radioInfoMu.RLock() if callsign, ok := c.radioInfo["callsign"]; ok { c.lastStatus.Callsign = callsign } @@ -783,28 +562,138 @@ func (c *Client) parseInfoResponse(data string) { c.lastStatus.NumSlices = numSlices } } + c.radioInfoMu.RUnlock() - // 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 { + if isOn && c.lastStatus.Frequency == 0 && 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)" } } +} + +func (c *Client) reconnectionMonitor() { + log.Println("FlexRadio: Reconnection monitor started") + + for c.running { + c.connMu.Lock() + connected := (c.conn != nil) + c.connMu.Unlock() + + if !connected { + c.reconnectAttempts++ + + 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 + + go func() { + time.Sleep(500 * time.Millisecond) + c.SendInfo() + }() + } + case <-c.stopChan: + return + } + } else { + select { + case <-time.After(10 * time.Second): + case <-c.stopChan: + return + } + } + } +} + +func (c *Client) calculateReconnectDelay() time.Duration { + delay := c.reconnectInterval + + if c.reconnectAttempts > 1 { + multiplier := 1 << (c.reconnectAttempts - 1) + delay = c.reconnectInterval * time.Duration(multiplier) + + if delay > c.maxReconnectDelay { + delay = c.maxReconnectDelay + } + } + + return delay +} + +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 { + 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) + + 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(500 * time.Millisecond) // Petite pause - c.getActiveSlices() - }() + log.Println("FlexRadio: TCP connection reestablished") + return nil +} - log.Printf("FlexRadio: Radio is powered on and responding (total slices: %d, active slices: %d)", - c.lastStatus.NumSlices, c.lastStatus.ActiveSlices) +func (c *Client) radioStatusChecker() { + c.infoCheckTimer = time.NewTimer(10 * time.Second) + + for c.running { + select { + case <-c.infoCheckTimer.C: + c.SendInfo() + c.infoCheckTimer.Reset(10 * time.Second) + case <-c.stopChan: + return + } + } +} + +func (c *Client) sliceListChecker() { + c.sliceListTimer = time.NewTimer(10 * time.Second) + + for c.running { + select { + case <-c.sliceListTimer.C: + if c.IsRadioOn() { + c.SendSliceList() + } + c.sliceListTimer.Reset(10 * time.Second) + case <-c.stopChan: + return + } + } } func (c *Client) GetStatus() (*Status, error) { @@ -819,86 +708,10 @@ func (c *Client) GetStatus() (*Status, error) { }, nil } - // Create a copy status := *c.lastStatus - return &status, nil } -func (c *Client) handleStatus(msg string) { - // Format: S|= ... - 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() diff --git a/internal/devices/flexradio/types.go b/internal/devices/flexradio/types.go index 8af11d4..96b9a09 100644 --- a/internal/devices/flexradio/types.go +++ b/internal/devices/flexradio/types.go @@ -14,12 +14,3 @@ type Status struct { NumSlices int `json:"num_slices"` // From info command ActiveSlices int `json:"active_slices"` // Count of active slices } - -// InterlockState represents possible interlock states -const ( - InterlockStateReady = "READY" - InterlockStateNotReady = "NOT_READY" - InterlockStatePTTRequested = "PTT_REQUESTED" - InterlockStateTransmitting = "TRANSMITTING" - InterlockStateUnkeyRequested = "UNKEY_REQUESTED" -)