added SkyCAT support
This commit is contained in:
227
backend/skycat/client.go
Normal file
227
backend/skycat/client.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package skycat
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SatMode defines the satellite operating mode for SkyCAT.
|
||||
type SatMode string
|
||||
|
||||
const (
|
||||
ModeDuplex SatMode = "Duplex"
|
||||
ModeSplit SatMode = "Split"
|
||||
ModeSimplex SatMode = "Simplex"
|
||||
)
|
||||
|
||||
// Client manages a TCP connection to skycatd / rigctld.
|
||||
// Protocol: simple newline-terminated text commands (HamLib-compatible).
|
||||
//
|
||||
// Key commands:
|
||||
//
|
||||
// U Duplex — satellite duplex mode (RX≠TX)
|
||||
// U Split — split VFO mode
|
||||
// U Simplex — simplex mode
|
||||
// F {hz} — set RX frequency (Hz, integer)
|
||||
// I {hz} — set TX frequency (Hz, integer)
|
||||
// M {mode} 0 — set RX mode (FM, USB, LSB, CW…)
|
||||
// X {mode} 0 — set TX mode
|
||||
type Client struct {
|
||||
mu sync.Mutex
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
connected atomic.Bool
|
||||
satMode SatMode
|
||||
lastDownHz float64
|
||||
lastUpHz float64
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
return &Client{satMode: ModeDuplex}
|
||||
}
|
||||
|
||||
// Connect establishes a TCP connection to skycatd (default port 4532).
|
||||
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("SkyCAT connect %s: %w", addr, err)
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
c.reader = bufio.NewReader(conn)
|
||||
c.connected.Store(true)
|
||||
|
||||
// Start read loop to drain responses (skycatd sends RPRT lines)
|
||||
go c.readLoop()
|
||||
|
||||
// Set satellite duplex mode immediately after connect
|
||||
log.Printf("[SkyCAT] Connected to %s", addr)
|
||||
c.sendLocked(fmt.Sprintf("U %s", c.satMode))
|
||||
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("[SkyCAT] Disconnected")
|
||||
}
|
||||
|
||||
// IsConnected returns true if currently connected.
|
||||
func (c *Client) IsConnected() bool {
|
||||
return c.connected.Load()
|
||||
}
|
||||
|
||||
// SetSatMode configures the operating mode (Duplex/Split/Simplex).
|
||||
// Sends the U command to skycatd.
|
||||
func (c *Client) SetSatMode(mode SatMode) error {
|
||||
c.satMode = mode
|
||||
return c.sendCommand(fmt.Sprintf("U %s", mode))
|
||||
}
|
||||
|
||||
// SetFrequency sends corrected RX and TX frequencies to skycatd.
|
||||
// downHz = RX downlink (command F), upHz = TX uplink (command I).
|
||||
// Dead-band: 1 Hz.
|
||||
func (c *Client) SetFrequency(downHz, upHz float64) error {
|
||||
if !c.connected.Load() {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
var errs []error
|
||||
|
||||
if downHz > 0 && absf(downHz-c.lastDownHz) >= 1.0 {
|
||||
if err := c.sendLocked(fmt.Sprintf("F %.0f", downHz)); err != nil {
|
||||
errs = append(errs, fmt.Errorf("RX freq: %w", err))
|
||||
} else {
|
||||
c.lastDownHz = downHz
|
||||
}
|
||||
}
|
||||
|
||||
if upHz > 0 && absf(upHz-c.lastUpHz) >= 1.0 {
|
||||
if err := c.sendLocked(fmt.Sprintf("I %.0f", upHz)); err != nil {
|
||||
errs = append(errs, fmt.Errorf("TX freq: %w", err))
|
||||
} else {
|
||||
c.lastUpHz = upHz
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("SkyCAT SetFrequency: %v", errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMode sets RX and TX modes (FM, USB, LSB, CW…).
|
||||
func (c *Client) SetMode(mode string) error {
|
||||
mode = strings.ToUpper(strings.TrimSpace(mode))
|
||||
var errs []error
|
||||
if err := c.sendCommand(fmt.Sprintf("M %s 0", mode)); err != nil {
|
||||
errs = append(errs, fmt.Errorf("RX mode: %w", err))
|
||||
}
|
||||
if err := c.sendCommand(fmt.Sprintf("X %s 0", mode)); err != nil {
|
||||
errs = append(errs, fmt.Errorf("TX mode: %w", err))
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("SkyCAT SetMode: %v", errs)
|
||||
}
|
||||
log.Printf("[SkyCAT] Mode → %s", mode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetDeadband forces next frequency command to be sent immediately.
|
||||
func (c *Client) ResetDeadband() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.lastDownHz = 0
|
||||
c.lastUpHz = 0
|
||||
}
|
||||
|
||||
// SatModeToSkyCAT converts a satellite DB mode string to SkyCAT mode string.
|
||||
func SatModeToSkyCAT(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 "PKTLSB"
|
||||
case "AM":
|
||||
return "AM"
|
||||
default:
|
||||
return "USB"
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) sendCommand(cmd string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.sendLocked(cmd)
|
||||
}
|
||||
|
||||
// sendLocked sends a command — caller must hold c.mu.
|
||||
func (c *Client) sendLocked(cmd string) error {
|
||||
if c.conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
line := cmd + "\n"
|
||||
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("send: %w", err)
|
||||
}
|
||||
log.Printf("[SkyCAT] → %s", strings.TrimSpace(line))
|
||||
return nil
|
||||
}
|
||||
|
||||
// readLoop drains responses from skycatd (RPRT 0 = OK, RPRT -1 = error).
|
||||
func (c *Client) readLoop() {
|
||||
for {
|
||||
line, err := c.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if c.connected.Load() {
|
||||
log.Printf("[SkyCAT] Read error: %v", err)
|
||||
c.connected.Store(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("[SkyCAT] ← %s", strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
|
||||
func absf(x float64) float64 {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
Reference in New Issue
Block a user