387 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			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
 | 
						|
}
 |