first commit

This commit is contained in:
2026-03-24 23:24:36 +01:00
commit a69394a05b
1638 changed files with 891299 additions and 0 deletions

241
backend/flexradio/client.go Normal file
View 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)
}