package flexradio import ( "bufio" "fmt" "log" "net" "strconv" "strings" "sync" "time" "unicode" ) type Client struct { host string port int conn net.Conn reader *bufio.Reader connMu sync.Mutex writeMu sync.Mutex lastStatus *Status statusMu sync.RWMutex cmdSeq int cmdSeqMu sync.Mutex running bool stopChan chan struct{} reconnectInterval time.Duration reconnectAttempts int maxReconnectDelay time.Duration radioInfo map[string]string radioInfoMu sync.RWMutex lastInfoCheck time.Time infoCheckTimer *time.Timer activeSlices []int activeSlicesMu sync.RWMutex sliceListTimer *time.Timer onFrequencyChange func(freqMHz float64) checkTransmitAllowed func() bool } 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{}, lastStatus: &Status{ Connected: false, RadioOn: false, }, } } // SetReconnectInterval sets the reconnection interval func (c *Client) SetReconnectInterval(interval time.Duration) { c.reconnectInterval = interval } // SetMaxReconnectDelay sets the maximum delay for exponential backoff 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 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) } // Update connected status c.statusMu.Lock() if c.lastStatus != nil { c.lastStatus.Connected = (c.conn != nil) c.lastStatus.RadioOn = false } c.statusMu.Unlock() c.running = true // Start message listener go c.messageLoop() // Start reconnection monitor go c.reconnectionMonitor() // 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() { time.Sleep(500 * time.Millisecond) c.SendInfo() time.Sleep(500 * time.Millisecond) c.SendSliceList() time.Sleep(500 * time.Millisecond) c.SubscribeToSlices() }() } return nil } func (c *Client) Stop() { if !c.running { return } c.running = false close(c.stopChan) // Stop timers if c.infoCheckTimer != nil { c.infoCheckTimer.Stop() } if c.sliceListTimer != nil { c.sliceListTimer.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.lastStatus.ActiveSlices = 0 c.lastStatus.Frequency = 0 c.lastStatus.Mode = "" c.lastStatus.Tx = false } c.statusMu.Unlock() } // Helper functions for common commands func (c *Client) SendInfo() error { return c.sendCommand("info") } 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() 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 { // 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) getNextSeq() int { c.cmdSeqMu.Lock() defer c.cmdSeqMu.Unlock() c.cmdSeq++ return c.cmdSeq } 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) continue } 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() { 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() 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 } c.handleMessage(line) } log.Println("FlexRadio: Message loop stopped") } // Message handling - SIMPLIFIED VERSION func (c *Client) handleMessage(msg string) { msg = strings.TrimSpace(msg) if msg == "" { return } // DEBUG: Log tous les messages reçus log.Printf("FlexRadio RAW: %s", msg) // 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) default: log.Printf("FlexRadio: Unknown message type: %s", msg) } } func (c *Client) handleCommandResponse(msg string) { // Format: R|| parts := strings.SplitN(msg, "|", 3) if len(parts) < 3 { log.Printf("FlexRadio: Malformed response: %s", msg) return } seqStr := strings.TrimPrefix(parts[0], "R") status := parts[1] data := parts[2] seq, _ := strconv.Atoi(seqStr) if status != "0" { log.Printf("FlexRadio: Command error (seq=%d): %s", seq, msg) return } 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) case isSliceListResponse(data): c.parseSliceListResponse(data) default: log.Printf("FlexRadio: Generic response: %s", data) } } func isSliceListResponse(data string) bool { data = strings.TrimSpace(data) if data == "" { return true } for _, char := range data { if !unicode.IsDigit(char) && char != ' ' { return false } } return true } 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 == "TRANSMITTING" || 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() // Quand on reçoit un message de slice, on a au moins une slice active c.lastStatus.ActiveSlices = 1 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) } } else if freq == 0 { // Fréquence 0 dans le message de slice = slice inactive c.lastStatus.Frequency = 0 c.lastStatus.RadioInfo = "Slice inactive" } } 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) 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)) } 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]) if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' { value = value[1 : len(value)-1] } c.radioInfo[key] = value } } c.radioInfoMu.Unlock() c.updateRadioStatus(true, "Radio is on") 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 { c.lastStatus.ActiveSlices = len(slices) // NE PAS effacer la fréquence ici ! // La fréquence est gérée par handleSliceStatus // Seulement mettre à jour RadioInfo si vraiment pas de slices if len(slices) == 0 && c.lastStatus.Frequency == 0 { c.lastStatus.RadioInfo = "Radio is on without any active slice" } else if len(slices) == 0 && c.lastStatus.Frequency > 0 { // Cas spécial : fréquence mais pas de slice dans la liste // Peut arriver temporairement, garder l'info actuelle c.lastStatus.RadioInfo = fmt.Sprintf("Active on %.3f MHz", c.lastStatus.Frequency) } } 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 } 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 isOn && c.lastStatus.Frequency == 0 && c.lastStatus.ActiveSlices == 0 { c.lastStatus.RadioInfo = "Radio is on without any active slice" } } } 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() log.Println("FlexRadio: TCP connection reestablished") return nil } 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) { c.statusMu.RLock() defer c.statusMu.RUnlock() if c.lastStatus == nil { return &Status{ Connected: false, RadioOn: false, RadioInfo: "Not initialized", }, nil } status := *c.lastStatus return &status, nil } // 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 }