Files
FlexDXClusterGui/flexradio.go
2025-10-23 03:18:14 +02:00

387 lines
9.4 KiB
Go

package main
import (
"bufio"
"context"
"fmt"
"math"
"net"
"os"
"regexp"
"time"
)
var CommandNumber int = 1
type FlexSpot struct {
ID int
CommandNumber int
FlexSpotNumber int
DX string
FrequencyMhz string
FrequencyHz string
Band string
Mode string
FlexMode string
Source string
SpotterCallsign string
TimeStamp int64
UTCTime string
LifeTime string
Priority string
OriginalComment string
Comment string
Color string
BackgroundColor string
NewDXCC bool
NewBand bool
NewMode bool
NewSlot bool
Worked bool
InWatchlist bool
CountryName string
DXCC string
}
type Discovery struct {
IP string
NickName string
Model string
Serial string
Version string
}
type FlexClient struct {
Address string
Port string
Timeout time.Duration
LogWriter *bufio.Writer
Reader *bufio.Reader
Writer *bufio.Writer
Conn *net.TCPConn
MsgChan chan string
Repo FlexDXClusterRepository
TCPServer *TCPServer
HTTPServer *HTTPServer
IsConnected bool
ctx context.Context
cancel context.CancelFunc
reconnectAttempts int
maxReconnectAttempts int
baseReconnectDelay time.Duration
maxReconnectDelay time.Duration
Enabled bool
}
func NewFlexClient(repo FlexDXClusterRepository, TCPServer *TCPServer, SpotChanToFlex chan TelnetSpot, httpServer *HTTPServer) *FlexClient {
ctx, cancel := context.WithCancel(context.Background())
enabled := Cfg.General.FlexRadioSpot && (Cfg.Flex.IP != "" || Cfg.Flex.Discover)
return &FlexClient{
Port: "4992",
MsgChan: TCPServer.MsgChan,
Repo: repo,
TCPServer: TCPServer,
HTTPServer: httpServer,
IsConnected: false,
Enabled: enabled,
ctx: ctx,
cancel: cancel,
maxReconnectAttempts: -1, // -1 = infini
baseReconnectDelay: 5 * time.Second, // Délai initial
maxReconnectDelay: 5 * time.Minute, // Max 5 minutes
}
}
func (fc *FlexClient) calculateBackoff() time.Duration {
delay := time.Duration(float64(fc.baseReconnectDelay) * math.Pow(1.5, float64(fc.reconnectAttempts)))
if delay > fc.maxReconnectDelay {
delay = fc.maxReconnectDelay
}
return delay
}
func (fc *FlexClient) resolveAddress() (string, error) {
if Cfg.Flex.IP == "" && !Cfg.Flex.Discover {
return "", fmt.Errorf("you must either turn FlexRadio Discovery on or provide an IP address")
}
if Cfg.Flex.Discover {
Log.Debug("Attempting FlexRadio discovery...")
discoveryDone := make(chan struct {
success bool
discovery *Discovery
}, 1)
go func() {
ok, d := DiscoverFlexRadio()
discoveryDone <- struct {
success bool
discovery *Discovery
}{ok, d}
}()
select {
case result := <-discoveryDone:
if result.success {
Log.Infof("Found: %s with Nick: %s, Version: %s, Serial: %s - using IP: %s",
result.discovery.Model, result.discovery.NickName, result.discovery.Version,
result.discovery.Serial, result.discovery.IP)
return result.discovery.IP, nil
}
case <-time.After(10 * time.Second):
Log.Warn("Discovery timeout after 10 seconds")
}
if Cfg.Flex.IP == "" {
return "", fmt.Errorf("could not discover any FlexRadio on the network and no IP provided")
}
Log.Warn("Discovery failed, using configured IP")
}
return Cfg.Flex.IP, nil
}
func (fc *FlexClient) connect() error {
ip, err := fc.resolveAddress()
if err != nil {
return err
}
fc.Address = ip
addr, err := net.ResolveTCPAddr("tcp", fc.Address+":"+fc.Port)
if err != nil {
return fmt.Errorf("cannot resolve address %s:%s: %w", fc.Address, fc.Port, err)
}
Log.Debugf("Attempting to connect to FlexRadio at %s:%s (attempt %d)",
fc.Address, fc.Port, fc.reconnectAttempts+1)
conn, err := net.DialTimeout("tcp", addr.String(), 10*time.Second)
if err != nil {
return fmt.Errorf("could not connect to FlexRadio: %w", err)
}
tcpConn, ok := conn.(*net.TCPConn)
if !ok {
conn.Close()
return fmt.Errorf("connection is not a TCP connection")
}
fc.Conn = tcpConn
fc.Reader = bufio.NewReader(fc.Conn)
fc.Writer = bufio.NewWriter(fc.Conn)
fc.IsConnected = true
fc.reconnectAttempts = 0
if err := fc.Conn.SetKeepAlive(true); err != nil {
Log.Warn("Could not set keep alive:", err)
}
Log.Infof("✅ Connected to FlexRadio at %s:%s", fc.Address, fc.Port)
return nil
}
func (fc *FlexClient) StartFlexClient() {
fc.LogWriter = bufio.NewWriter(os.Stdout)
fc.Timeout = 600 * time.Second
if !fc.Enabled {
Log.Info("FlexRadio integration disabled in config - skipping")
return
}
for {
select {
case <-fc.ctx.Done():
Log.Info("Flex Client shutting down...")
return
default:
}
// Tentative de connexion
err := fc.connect()
if err != nil {
fc.IsConnected = false
fc.reconnectAttempts++
backoff := fc.calculateBackoff()
// Message moins alarmiste
if fc.reconnectAttempts == 1 {
Log.Warnf("FlexRadio not available: %v", err)
Log.Info("FlexDXCluster will continue without FlexRadio and retry connection periodically")
} else {
Log.Debugf("FlexRadio still not available. Next retry in %v", backoff)
}
// Attendre avant de réessayer
select {
case <-fc.ctx.Done():
return
case <-time.After(backoff):
continue
}
}
// Connexion réussie, initialiser le Flex
fc.initializeFlex()
// Démarrer la lecture (bloquant jusqu'à déconnexion)
fc.ReadLine()
// Si ReadLine se termine (déconnexion), réessayer
fc.IsConnected = false
Log.Warn("FlexRadio connection lost. Will retry...")
time.Sleep(5 * time.Second)
}
}
func (fc *FlexClient) initializeFlex() {
subSpotAllCmd := fmt.Sprintf("C%v|sub spot all", CommandNumber)
fc.Write(subSpotAllCmd)
CommandNumber++
clrSpotAllCmd := fmt.Sprintf("C%v|spot clear", CommandNumber)
fc.Write(clrSpotAllCmd)
CommandNumber++
Log.Debug("Subscribed to spots on FlexRadio and cleared all spots from panadapter")
}
func (fc *FlexClient) Close() {
fc.cancel()
if fc.Conn != nil {
fc.Conn.Close()
}
}
// ✅ SendSpot - Méthode simplifiée pour envoyer une commande au Flex
func (fc *FlexClient) SendSpot(stringSpot string) {
if fc.IsConnected {
fc.Write(stringSpot)
}
}
func (fc *FlexClient) ReadLine() {
defer func() {
if fc.Conn != nil {
fc.Conn.Close()
}
}()
for {
select {
case <-fc.ctx.Done():
return
default:
}
// Timeout sur la lecture
fc.Conn.SetReadDeadline(time.Now().Add(5 * time.Minute))
message, err := fc.Reader.ReadString(byte('\n'))
if err != nil {
Log.Debugf("Error reading from FlexRadio: %s", err)
return
}
fc.Conn.SetReadDeadline(time.Time{})
regRespSpot := *regexp.MustCompile(`R(\d+)\|0\|(\d+)\n`)
respSpot := regRespSpot.FindStringSubmatch(message)
if len(respSpot) > 0 {
spot, _ := fc.Repo.FindSpotByCommandNumber(respSpot[1])
_, err := fc.Repo.UpdateFlexSpotNumberByID(respSpot[2], *spot)
if err != nil {
Log.Errorf("Could not update Flex spot number in database: %s", err)
}
}
// Response when a spot is clicked
regTriggerSpot := *regexp.MustCompile(`.*spot (\d+) triggered.*\n`)
respTrigger := regTriggerSpot.FindStringSubmatch(message)
if len(respTrigger) > 0 {
spot, err := fc.Repo.FindSpotByFlexSpotNumber(respTrigger[1])
Log.Debugf("Spot %s has been triggered", message)
if err != nil {
Log.Errorf("could not find spot by flex spot number in database: %s", err)
}
// Sending the callsign to Log4OM
SendUDPMessage([]byte("<CALLSIGN>" + spot.DX))
}
// Status when a spot is deleted
regSpotDeleted := *regexp.MustCompile(`S\d+\|spot (\d+) removed`)
respDelete := regSpotDeleted.FindStringSubmatch(message)
if len(respDelete) > 0 {
fc.Repo.DeleteSpotByFlexSpotNumber(respDelete[1])
Log.Debugf("Spot deleted from Flex Panadater and database: ", message)
}
}
}
func (fc *FlexClient) Write(data string) (n int, err error) {
if fc.Conn == nil || fc.Writer == nil || !fc.IsConnected {
return 0, fmt.Errorf("not connected to FlexRadio")
}
n, err = fc.Writer.Write([]byte(data + "\n"))
if err == nil {
err = fc.Writer.Flush()
}
return
}
func DiscoverFlexRadio() (bool, *Discovery) {
if Cfg.Flex.Discover {
Log.Debugln("FlexRadio Discovery is turned on...searching for radio on the network")
pc, err := net.ListenPacket("udp", ":4992")
if err != nil {
Log.Errorf("Could not receive UDP packets to discover FlexRadio: %v", err)
return false, nil
}
defer pc.Close()
pc.SetReadDeadline(time.Now().Add(10 * time.Second))
buf := make([]byte, 1024)
for {
n, _, err := pc.ReadFrom(buf)
if err != nil {
// Timeout atteint
return false, nil
}
discoverRe := regexp.MustCompile(`discovery_protocol_version=.*\smodel=(.*)\sserial=(.*)\sversion=(.*)\snickname=(.*)\scallsign=.*\sip=(.*)\sport=.*`)
match := discoverRe.FindStringSubmatch(string(buf[:n]))
if len(match) > 0 {
d := &Discovery{
NickName: match[4],
Model: match[1],
Serial: match[2],
Version: match[3],
IP: match[5],
}
return true, d
}
}
} else {
Log.Infoln("FlexRadio Discovery is turned off...using IP provided in the config file")
}
return false, nil
}