242 lines
6.1 KiB
Go
242 lines
6.1 KiB
Go
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)
|
|
}
|