Files
ShackMaster/internal/devices/flexradio/flexradio.go
2026-01-16 01:17:28 +01:00

738 lines
16 KiB
Go

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<seq>|<status>|<data>
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
}