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|| if strings.HasPrefix(msg, "R") { c.handleResponse(msg) return } // Status format: S|= ... if strings.HasPrefix(msg, "S") { c.handleStatus(msg) return } // 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") { log.Printf("FlexRadio: Message: %s", msg) return } } func (c *Client) handleResponse(msg string) { // Format: R|| // 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|= ... 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 }