first commit
This commit is contained in:
241
backend/flexradio/client.go
Normal file
241
backend/flexradio/client.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package flexradio
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client manages a TCP connection to FlexRadio SmartSDR API.
|
||||
// For satellite operation:
|
||||
// - Slice A (index 0) = RX downlink (436 MHz for SO-50)
|
||||
// - Slice B (index 1) = TX uplink (145 MHz for SO-50)
|
||||
type Client struct {
|
||||
mu sync.Mutex
|
||||
conn net.Conn
|
||||
scanner *bufio.Scanner
|
||||
connected atomic.Bool
|
||||
seqNum uint32
|
||||
|
||||
rxSlice int // Slice index for RX (downlink) — default 0 = Slice A
|
||||
txSlice int // Slice index for TX (uplink) — default 1 = Slice B
|
||||
lastDownHz float64 // Last sent RX frequency (dead-band)
|
||||
lastUpHz float64 // Last sent TX frequency (dead-band)
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
return &Client{rxSlice: 0, txSlice: 1} // defaults — always overridden by SetSliceConfig on connect
|
||||
}
|
||||
|
||||
// Connect establishes TCP connection to FlexRadio SmartSDR API (port 4992).
|
||||
func (c *Client) Connect(host string, port int) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.connected.Load() {
|
||||
c.disconnectLocked()
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connexion FlexRadio %s : %w", addr, err)
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
c.scanner = bufio.NewScanner(conn)
|
||||
c.connected.Store(true)
|
||||
|
||||
go c.readLoop()
|
||||
|
||||
log.Printf("[FlexRadio] Connecté à %s (RX=Slice %s, TX=Slice %s)",
|
||||
addr, sliceLetter(c.rxSlice), sliceLetter(c.txSlice))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect closes the connection.
|
||||
func (c *Client) Disconnect() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.disconnectLocked()
|
||||
}
|
||||
|
||||
func (c *Client) disconnectLocked() {
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
c.connected.Store(false)
|
||||
log.Println("[FlexRadio] Déconnecté")
|
||||
}
|
||||
|
||||
func (c *Client) IsConnected() bool {
|
||||
return c.connected.Load()
|
||||
}
|
||||
|
||||
// SetFrequency applies Doppler-corrected frequencies to both slices.
|
||||
// downHz = corrected RX frequency → rxSlice (default Slice A)
|
||||
// upHz = corrected TX frequency → txSlice (default Slice B)
|
||||
// Only sends command if frequency changed by more than 1 Hz (dead-band)
|
||||
func (c *Client) SetFrequency(downHz, upHz float64) error {
|
||||
if !c.connected.Load() {
|
||||
return fmt.Errorf("non connecté")
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
// RX downlink — use "slice t" (tune) command
|
||||
if downHz > 0 {
|
||||
downMHz := downHz / 1e6
|
||||
if abs(downHz-c.lastDownHz) >= 1.0 { // 1 Hz dead-band
|
||||
cmd := fmt.Sprintf("slice t %d %.6f", c.rxSlice, downMHz)
|
||||
if err := c.sendCommand(cmd); err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
c.lastDownHz = downHz
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TX uplink — use "slice t" (tune) command
|
||||
if upHz > 0 {
|
||||
upMHz := upHz / 1e6
|
||||
if abs(upHz-c.lastUpHz) >= 1.0 {
|
||||
cmd := fmt.Sprintf("slice t %d %.6f", c.txSlice, upMHz)
|
||||
if err := c.sendCommand(cmd); err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
c.lastUpHz = upHz
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("FlexRadio SetFrequency: %v", errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetDeadband forces next frequency command to be sent regardless of change.
|
||||
func (c *Client) ResetDeadband() {
|
||||
c.lastDownHz = 0
|
||||
c.lastUpHz = 0
|
||||
}
|
||||
|
||||
func abs(x float64) float64 {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// SetSlices configures which slice indices to use for RX and TX.
|
||||
// Default: rx=0 (Slice A), tx=1 (Slice B)
|
||||
func (c *Client) SetSlices(rxIdx, txIdx int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.rxSlice = rxIdx
|
||||
c.txSlice = txIdx
|
||||
log.Printf("[FlexRadio] Slices configurées: RX=Slice %s, TX=Slice %s",
|
||||
sliceLetter(rxIdx), sliceLetter(txIdx))
|
||||
}
|
||||
|
||||
// GetSlices returns current RX and TX slice indices.
|
||||
func (c *Client) GetSlices() (rx, tx int) {
|
||||
return c.rxSlice, c.txSlice
|
||||
}
|
||||
|
||||
// QuerySlices sends a slice list request to discover available slices.
|
||||
func (c *Client) QuerySlices() error {
|
||||
return c.sendCommand("slice list")
|
||||
}
|
||||
|
||||
// SetMode sets the demodulation mode on RX and/or TX slices.
|
||||
// mode: "usb", "lsb", "cw", "am", "sam", "fm", "nfm", "dfm", "digl", "digu", "rtty"
|
||||
// For satellite: RX and TX usually have the same mode (FM for FM sats, LSB/USB for linear)
|
||||
func (c *Client) SetMode(mode string) error {
|
||||
if !c.connected.Load() {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
mode = strings.ToLower(strings.TrimSpace(mode))
|
||||
var errs []error
|
||||
// Set mode on RX slice
|
||||
if err := c.sendCommand(fmt.Sprintf("slice s %d mode=%s", c.rxSlice, mode)); err != nil {
|
||||
errs = append(errs, fmt.Errorf("RX mode: %w", err))
|
||||
}
|
||||
// Set mode on TX slice (same mode for satellite split operation)
|
||||
if err := c.sendCommand(fmt.Sprintf("slice s %d mode=%s", c.txSlice, mode)); err != nil {
|
||||
errs = append(errs, fmt.Errorf("TX mode: %w", err))
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("SetMode: %v", errs)
|
||||
}
|
||||
log.Printf("[FlexRadio] Mode set to %s on slices %s/%s",
|
||||
strings.ToUpper(mode), sliceLetter(c.rxSlice), sliceLetter(c.txSlice))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SatModeToFlex converts a satellite DB mode string to FlexRadio mode.
|
||||
// FM satellites use "fm", linear transponders use "lsb" or "usb".
|
||||
func SatModeToFlex(satMode string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(satMode)) {
|
||||
case "FM", "NFM":
|
||||
return "fm"
|
||||
case "LSB":
|
||||
return "lsb"
|
||||
case "USB":
|
||||
return "usb"
|
||||
case "CW", "CW/DATA":
|
||||
return "cw"
|
||||
case "APRS", "DATA", "DIGI":
|
||||
return "digl"
|
||||
case "AM":
|
||||
return "am"
|
||||
default:
|
||||
return "usb" // safe default
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) sendCommand(cmd string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conn == nil {
|
||||
return fmt.Errorf("non connecté")
|
||||
}
|
||||
|
||||
seq := atomic.AddUint32(&c.seqNum, 1)
|
||||
line := fmt.Sprintf("C%d|%s\n", seq, cmd)
|
||||
|
||||
c.conn.SetWriteDeadline(time.Now().Add(2 * time.Second))
|
||||
_, err := fmt.Fprint(c.conn, line)
|
||||
if err != nil {
|
||||
c.connected.Store(false)
|
||||
return fmt.Errorf("envoi commande: %w", err)
|
||||
}
|
||||
log.Printf("[FlexRadio] → %s", strings.TrimSpace(line))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) readLoop() {
|
||||
for c.scanner.Scan() {
|
||||
line := c.scanner.Text()
|
||||
// Log ALL responses for debugging
|
||||
log.Printf("[FlexRadio] ← %s", line)
|
||||
}
|
||||
c.connected.Store(false)
|
||||
log.Println("[FlexRadio] Disconnected")
|
||||
}
|
||||
|
||||
func sliceLetter(idx int) string {
|
||||
letters := []string{"A", "B", "C", "D", "E", "F", "G", "H"}
|
||||
if idx < len(letters) {
|
||||
return letters[idx]
|
||||
}
|
||||
return fmt.Sprintf("%d", idx)
|
||||
}
|
||||
Reference in New Issue
Block a user