added SkyCAT support
This commit is contained in:
93
app.go
93
app.go
@@ -9,11 +9,21 @@ import (
|
|||||||
"SatMaster/backend/flexradio"
|
"SatMaster/backend/flexradio"
|
||||||
"SatMaster/backend/propagator"
|
"SatMaster/backend/propagator"
|
||||||
"SatMaster/backend/rotor"
|
"SatMaster/backend/rotor"
|
||||||
|
"SatMaster/backend/skycat"
|
||||||
"SatMaster/backend/tle"
|
"SatMaster/backend/tle"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RadioClient is the common interface for FlexRadio and SkyCAT backends.
|
||||||
|
type RadioClient interface {
|
||||||
|
IsConnected() bool
|
||||||
|
SetFrequency(downHz, upHz float64) error
|
||||||
|
SetMode(mode string) error
|
||||||
|
ResetDeadband()
|
||||||
|
Disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
// App holds all subsystems and is the Wails binding target.
|
// App holds all subsystems and is the Wails binding target.
|
||||||
type App struct {
|
type App struct {
|
||||||
dopplerEnabled bool
|
dopplerEnabled bool
|
||||||
@@ -23,11 +33,13 @@ type App struct {
|
|||||||
trackAzimuth bool // Track azimuth for current sat
|
trackAzimuth bool // Track azimuth for current sat
|
||||||
savedRxSlice int
|
savedRxSlice int
|
||||||
savedTxSlice int
|
savedTxSlice int
|
||||||
|
radioType string // "flex" or "skycat"
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
tleManager *tle.Manager
|
tleManager *tle.Manager
|
||||||
propagator *propagator.Engine
|
propagator *propagator.Engine
|
||||||
flexRadio *flexradio.Client
|
flexRadio *flexradio.Client
|
||||||
|
skyCat *skycat.Client
|
||||||
rotorClient *rotor.PstRotatorClient
|
rotorClient *rotor.PstRotatorClient
|
||||||
dopplerCalc *doppler.Calculator
|
dopplerCalc *doppler.Calculator
|
||||||
|
|
||||||
@@ -47,9 +59,11 @@ func NewApp() *App {
|
|||||||
trackAzimuth: false,
|
trackAzimuth: false,
|
||||||
savedRxSlice: 0, // defaults — overridden by SetSliceConfig from Settings on connect
|
savedRxSlice: 0, // defaults — overridden by SetSliceConfig from Settings on connect
|
||||||
savedTxSlice: 1,
|
savedTxSlice: 1,
|
||||||
|
radioType: "flex",
|
||||||
tleManager: tle.NewManager(),
|
tleManager: tle.NewManager(),
|
||||||
propagator: propagator.NewEngine(),
|
propagator: propagator.NewEngine(),
|
||||||
flexRadio: flexradio.NewClient(),
|
flexRadio: flexradio.NewClient(),
|
||||||
|
skyCat: skycat.NewClient(),
|
||||||
rotorClient: rotor.NewPstRotatorClient(),
|
rotorClient: rotor.NewPstRotatorClient(),
|
||||||
dopplerCalc: doppler.NewCalculator(),
|
dopplerCalc: doppler.NewCalculator(),
|
||||||
watchlist: []string{"ISS (ZARYA)", "AO-7", "AO-27", "SO-50", "RS-44", "AO-91", "FO-29"},
|
watchlist: []string{"ISS (ZARYA)", "AO-7", "AO-27", "SO-50", "RS-44", "AO-91", "FO-29"},
|
||||||
@@ -92,9 +106,18 @@ func (a *App) startup(ctx context.Context) {
|
|||||||
go a.positionLoop(ctx)
|
go a.positionLoop(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// activeRadio returns the currently selected radio client.
|
||||||
|
func (a *App) activeRadio() RadioClient {
|
||||||
|
if a.radioType == "skycat" {
|
||||||
|
return a.skyCat
|
||||||
|
}
|
||||||
|
return a.flexRadio
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) shutdown(ctx context.Context) {
|
func (a *App) shutdown(ctx context.Context) {
|
||||||
a.StopTracking()
|
a.StopTracking()
|
||||||
a.flexRadio.Disconnect()
|
a.flexRadio.Disconnect()
|
||||||
|
a.skyCat.Disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// positionLoop emits sat positions to frontend every second.
|
// positionLoop emits sat positions to frontend every second.
|
||||||
@@ -142,9 +165,9 @@ func (a *App) updateTracking() {
|
|||||||
obs := a.propagator.ObserverPosition()
|
obs := a.propagator.ObserverPosition()
|
||||||
|
|
||||||
// Doppler: only when globally enabled AND tracking enabled for this sat AND el >= 0
|
// Doppler: only when globally enabled AND tracking enabled for this sat AND el >= 0
|
||||||
if a.flexRadio.IsConnected() && pos.Elevation >= 0 && a.dopplerEnabled && a.trackFreqMode {
|
if a.activeRadio().IsConnected() && pos.Elevation >= 0 && a.dopplerEnabled && a.trackFreqMode {
|
||||||
upFreq, downFreq := a.dopplerCalc.Correct(pos, obs, now)
|
upFreq, downFreq := a.dopplerCalc.Correct(pos, obs, now)
|
||||||
if err := a.flexRadio.SetFrequency(downFreq, upFreq); err != nil {
|
if err := a.activeRadio().SetFrequency(downFreq, upFreq); err != nil {
|
||||||
log.Printf("[Doppler] SetFrequency error: %v", err)
|
log.Printf("[Doppler] SetFrequency error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,15 +222,21 @@ func (a *App) SetSatelliteFrequencies(downHz, upHz float64) {
|
|||||||
a.dopplerCalc.SetNominal(downHz, upHz)
|
a.dopplerCalc.SetNominal(downHz, upHz)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSatelliteMode sets the FlexRadio slice mode for satellite operation.
|
// SetSatelliteMode sets the radio mode for satellite operation.
|
||||||
// Called automatically when a frequency is selected in the frontend.
|
// Called automatically when a frequency is selected in the frontend.
|
||||||
func (a *App) SetSatelliteMode(mode string) {
|
func (a *App) SetSatelliteMode(mode string) {
|
||||||
if !a.flexRadio.IsConnected() {
|
radio := a.activeRadio()
|
||||||
|
if !radio.IsConnected() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
flexMode := flexradio.SatModeToFlex(mode)
|
var converted string
|
||||||
if err := a.flexRadio.SetMode(flexMode); err != nil {
|
if a.radioType == "skycat" {
|
||||||
log.Printf("[FlexRadio] SetMode error: %v", err)
|
converted = skycat.SatModeToSkyCAT(mode)
|
||||||
|
} else {
|
||||||
|
converted = flexradio.SatModeToFlex(mode)
|
||||||
|
}
|
||||||
|
if err := radio.SetMode(converted); err != nil {
|
||||||
|
log.Printf("[Radio] SetMode error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +333,7 @@ func (a *App) SetTrackFreqMode(enabled bool) {
|
|||||||
a.trackFreqMode = enabled
|
a.trackFreqMode = enabled
|
||||||
// Reset dead-band so freq is sent immediately when tracking starts
|
// Reset dead-band so freq is sent immediately when tracking starts
|
||||||
if enabled {
|
if enabled {
|
||||||
a.flexRadio.ResetDeadband()
|
a.activeRadio().ResetDeadband()
|
||||||
}
|
}
|
||||||
log.Printf("[Doppler] Freq/Mode tracking: %v", enabled)
|
log.Printf("[Doppler] Freq/Mode tracking: %v", enabled)
|
||||||
}
|
}
|
||||||
@@ -330,11 +359,57 @@ func (a *App) GetDopplerEnabled() bool {
|
|||||||
return a.dopplerEnabled
|
return a.dopplerEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFlexRadioStatus returns connection status.
|
// GetFlexRadioStatus returns FlexRadio connection status.
|
||||||
func (a *App) GetFlexRadioStatus() bool {
|
func (a *App) GetFlexRadioStatus() bool {
|
||||||
return a.flexRadio.IsConnected()
|
return a.flexRadio.IsConnected()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetRadioType switches between "flex" and "skycat" backends.
|
||||||
|
func (a *App) SetRadioType(radioType string) {
|
||||||
|
a.radioType = radioType
|
||||||
|
log.Printf("[Radio] Backend switched to: %s", radioType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRadioType returns the current radio backend type.
|
||||||
|
func (a *App) GetRadioType() string {
|
||||||
|
return a.radioType
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectSkyCAT connects to skycatd TCP server.
|
||||||
|
func (a *App) ConnectSkyCAT(host string, port int) string {
|
||||||
|
if err := a.skyCat.Connect(host, port); err != nil {
|
||||||
|
return "Error: " + err.Error()
|
||||||
|
}
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisconnectSkyCAT closes the SkyCAT connection.
|
||||||
|
func (a *App) DisconnectSkyCAT() {
|
||||||
|
a.skyCat.Disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSkyCATStatus returns SkyCAT connection status.
|
||||||
|
func (a *App) GetSkyCATStatus() bool {
|
||||||
|
return a.skyCat.IsConnected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSkyCATMode sets the SkyCAT satellite operating mode (Duplex/Split/Simplex).
|
||||||
|
func (a *App) SetSkyCATMode(mode string) string {
|
||||||
|
var m skycat.SatMode
|
||||||
|
switch mode {
|
||||||
|
case "Split":
|
||||||
|
m = skycat.ModeSplit
|
||||||
|
case "Simplex":
|
||||||
|
m = skycat.ModeSimplex
|
||||||
|
default:
|
||||||
|
m = skycat.ModeDuplex
|
||||||
|
}
|
||||||
|
if err := a.skyCat.SetSatMode(m); err != nil {
|
||||||
|
return "Error: " + err.Error()
|
||||||
|
}
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
|
|
||||||
// GetSliceConfig returns current RX/TX slice indices.
|
// GetSliceConfig returns current RX/TX slice indices.
|
||||||
func (a *App) GetSliceConfig() map[string]int {
|
func (a *App) GetSliceConfig() map[string]int {
|
||||||
rx, tx := a.flexRadio.GetSlices()
|
rx, tx := a.flexRadio.GetSlices()
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -8,7 +8,7 @@
|
|||||||
import PolarPlot from './components/PolarPlot.svelte'
|
import PolarPlot from './components/PolarPlot.svelte'
|
||||||
import { Go, onWailsEvent } from './lib/wails.js'
|
import { Go, onWailsEvent } from './lib/wails.js'
|
||||||
import { get } from 'svelte/store'
|
import { get } from 'svelte/store'
|
||||||
import { satPositions, satList, trackedSat, trackedPosition, passes, tleAge, tleLoaded, flexConnected, rotorConnected, settings, watchlist, soundEnabled, passesAllCache } from './stores/satstore.js'
|
import { satPositions, satList, trackedSat, trackedPosition, passes, tleAge, tleLoaded, flexConnected, rotorConnected, skycatConnected, settings, watchlist, soundEnabled, passesAllCache } from './stores/satstore.js'
|
||||||
|
|
||||||
let activeTab = 'map'
|
let activeTab = 'map'
|
||||||
let cleanups = []
|
let cleanups = []
|
||||||
@@ -43,6 +43,15 @@
|
|||||||
await Go.SetRotorAzOnly(s.rotorAzOnly ?? true)
|
await Go.SetRotorAzOnly(s.rotorAzOnly ?? true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (s.autoConnectSkycat && s.radioType === 'skycat') {
|
||||||
|
const res = await Go.ConnectSkyCAT(s.skycatHost, s.skycatPort)
|
||||||
|
if (res === 'OK') {
|
||||||
|
skycatConnected.set(true)
|
||||||
|
await Go.SetSkyCATMode(s.skycatSatMode || 'Duplex')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sync radio type to backend on startup
|
||||||
|
if (s.radioType) await Go.SetRadioType(s.radioType)
|
||||||
|
|
||||||
const names = await Go.GetSatelliteList()
|
const names = await Go.GetSatelliteList()
|
||||||
if (names) satList.set(names)
|
if (names) satList.set(names)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { settings, flexConnected, rotorConnected, soundEnabled } from '../stores/satstore.js'
|
import { settings, flexConnected, rotorConnected, soundEnabled, skycatConnected } from '../stores/satstore.js'
|
||||||
import { Go } from '../lib/wails.js'
|
import { Go } from '../lib/wails.js'
|
||||||
import { maidenheadToLatLon, latLonToMaidenhead } from '../lib/maidenhead.js'
|
import { maidenheadToLatLon, latLonToMaidenhead } from '../lib/maidenhead.js'
|
||||||
|
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
let availableVoices = []
|
let availableVoices = []
|
||||||
let selectedVoiceName = ''
|
let selectedVoiceName = ''
|
||||||
|
|
||||||
|
let skycatStatus = ''
|
||||||
|
|
||||||
// Load slice config on mount
|
// Load slice config on mount
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@@ -48,6 +50,28 @@
|
|||||||
flexStatus = `Slices: RX=Slice ${'ABCDEFGH'[rxSlice]}, TX=Slice ${'ABCDEFGH'[txSlice]}`
|
flexStatus = `Slices: RX=Slice ${'ABCDEFGH'[rxSlice]}, TX=Slice ${'ABCDEFGH'[txSlice]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function connectSkycat() {
|
||||||
|
skycatStatus = 'Connecting…'
|
||||||
|
settings.update(s => ({ ...s, skycatHost: local.skycatHost, skycatPort: local.skycatPort, skycatSatMode: local.skycatSatMode }))
|
||||||
|
const res = await Go.ConnectSkyCAT(local.skycatHost, local.skycatPort)
|
||||||
|
if (res === 'OK') {
|
||||||
|
skycatConnected.set(true)
|
||||||
|
skycatStatus = 'Connected ✓'
|
||||||
|
await Go.SetSkyCATMode(local.skycatSatMode || 'Duplex')
|
||||||
|
} else { skycatStatus = res || 'Error' }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnectSkycat() {
|
||||||
|
await Go.DisconnectSkyCAT()
|
||||||
|
skycatConnected.set(false)
|
||||||
|
skycatStatus = 'Disconnected'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRadioType() {
|
||||||
|
settings.update(s => ({ ...s, radioType: local.radioType }))
|
||||||
|
await Go.SetRadioType(local.radioType)
|
||||||
|
}
|
||||||
|
|
||||||
function saveVoice() {
|
function saveVoice() {
|
||||||
settings.update(s => ({ ...s, ttsVoiceName: selectedVoiceName }))
|
settings.update(s => ({ ...s, ttsVoiceName: selectedVoiceName }))
|
||||||
}
|
}
|
||||||
@@ -163,6 +187,58 @@
|
|||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Radio Backend Selector -->
|
||||||
|
<section class="card">
|
||||||
|
<h3>📻 Radio Backend</h3>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Active backend</label>
|
||||||
|
<select bind:value={local.radioType} on:change={saveRadioType}>
|
||||||
|
<option value="flex">FlexRadio 8600 (direct TCP)</option>
|
||||||
|
<option value="skycat">SkyCAT / rigctld (universal)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p class="hint">
|
||||||
|
{#if local.radioType === 'skycat'}
|
||||||
|
SkyCAT mode: connect skycatd.exe or rigctld.exe for your radio, then connect below.
|
||||||
|
{:else}
|
||||||
|
FlexRadio mode: direct TCP connection to SmartSDR API port 4992.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SkyCAT -->
|
||||||
|
{#if local.radioType === 'skycat'}
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h3>🛰 SkyCAT / rigctld</h3>
|
||||||
|
<span class="badge {$skycatConnected ? 'ok' : 'off'}">{$skycatConnected ? 'CONNECTED' : 'OFFLINE'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Host / IP</label>
|
||||||
|
<input type="text" bind:value={local.skycatHost} placeholder="127.0.0.1" />
|
||||||
|
<label>TCP Port</label>
|
||||||
|
<input type="number" bind:value={local.skycatPort} placeholder="4532" />
|
||||||
|
<label>Sat Mode</label>
|
||||||
|
<select bind:value={local.skycatSatMode}>
|
||||||
|
<option value="Duplex">Duplex (RX≠TX freq)</option>
|
||||||
|
<option value="Split">Split VFO</option>
|
||||||
|
<option value="Simplex">Simplex</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
{#if $skycatConnected}
|
||||||
|
<button class="btn-danger" on:click={disconnectSkycat}>Disconnect</button>
|
||||||
|
{:else}
|
||||||
|
<button class="btn-primary" on:click={connectSkycat}>Connect</button>
|
||||||
|
{/if}
|
||||||
|
{#if skycatStatus}
|
||||||
|
<span class="status {skycatStatus.includes('✓') ? 'ok-txt' : 'err-txt'}">{skycatStatus}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p class="hint">Start skycatd.exe first: <code>skycatd -m IC-9700 -r COM9 -t 4532</code></p>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- FlexRadio -->
|
<!-- FlexRadio -->
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
@@ -308,7 +384,7 @@
|
|||||||
<!-- About -->
|
<!-- About -->
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h3>ℹ️ About</h3>
|
<h3>ℹ️ About</h3>
|
||||||
<p><strong>SatMaster v1.0</strong> — F4BPO</p>
|
<p><strong>SatMaster v0.2</strong> — F4BPO</p>
|
||||||
<p>Amateur satellite tracking · FlexRadio Doppler correction · PstRotator control</p>
|
<p>Amateur satellite tracking · FlexRadio Doppler correction · PstRotator control</p>
|
||||||
<p class="hint">SGP4 via akhenakh/sgp4 · Go + Wails + Svelte</p>
|
<p class="hint">SGP4 via akhenakh/sgp4 · Go + Wails + Svelte</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { trackedSat, trackedPosition, flexConnected, rotorConnected, tleAge, settings, dopplerEnabled, rotorEnabled, soundEnabled } from '../stores/satstore.js'
|
import { trackedSat, trackedPosition, flexConnected, rotorConnected, skycatConnected, tleAge, settings, dopplerEnabled, rotorEnabled, soundEnabled } from '../stores/satstore.js'
|
||||||
import { formatFreq, formatShift, formatRange, computeDopplerShift } from '../lib/utils.js'
|
import { formatFreq, formatShift, formatRange, computeDopplerShift } from '../lib/utils.js'
|
||||||
import { onMount, onDestroy } from 'svelte'
|
import { onMount, onDestroy } from 'svelte'
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $flexConnected}
|
{#if ($settings.radioType === 'skycat' && $skycatConnected) || ($settings.radioType !== 'skycat' && $flexConnected)}
|
||||||
<button class="doppler-toggle {$dopplerEnabled ? 'don' : 'doff'}" on:click={() => dopplerEnabled.update(v => !v)} title={$dopplerEnabled ? 'Doppler ON — click to disable' : 'Doppler OFF — click to enable'}>
|
<button class="doppler-toggle {$dopplerEnabled ? 'don' : 'doff'}" on:click={() => dopplerEnabled.update(v => !v)} title={$dopplerEnabled ? 'Doppler ON — click to disable' : 'Doppler OFF — click to enable'}>
|
||||||
<span>⟳ DOPPLER {$dopplerEnabled ? 'ON' : 'OFF'}</span>
|
<span>⟳ DOPPLER {$dopplerEnabled ? 'ON' : 'OFF'}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -72,10 +72,17 @@
|
|||||||
<span>🔊 SOUND {$soundEnabled ? 'ON' : 'OFF'}</span>
|
<span>🔊 SOUND {$soundEnabled ? 'ON' : 'OFF'}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="conn-group">
|
<div class="conn-group">
|
||||||
<div class="conn {$flexConnected ? 'on' : 'off'}">
|
{#if $settings.radioType === 'skycat'}
|
||||||
<span class="conn-dot"></span>
|
<div class="conn {$skycatConnected ? 'on' : 'off'}">
|
||||||
<span>FLEX</span>
|
<span class="conn-dot"></span>
|
||||||
</div>
|
<span>SKYCAT</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="conn {$flexConnected ? 'on' : 'off'}">
|
||||||
|
<span class="conn-dot"></span>
|
||||||
|
<span>FLEX</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="conn {$rotorConnected ? 'on' : 'off'}">
|
<div class="conn {$rotorConnected ? 'on' : 'off'}">
|
||||||
<span class="conn-dot"></span>
|
<span class="conn-dot"></span>
|
||||||
<span>ROTOR</span>
|
<span>ROTOR</span>
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ export const Go = {
|
|||||||
SetSatelliteMode: (mode) => wailsCall('main.App.SetSatelliteMode', mode),
|
SetSatelliteMode: (mode) => wailsCall('main.App.SetSatelliteMode', mode),
|
||||||
SetTrackFreqMode: (v) => wailsCall('main.App.SetTrackFreqMode', v),
|
SetTrackFreqMode: (v) => wailsCall('main.App.SetTrackFreqMode', v),
|
||||||
SetTrackAzimuth: (v) => wailsCall('main.App.SetTrackAzimuth', v),
|
SetTrackAzimuth: (v) => wailsCall('main.App.SetTrackAzimuth', v),
|
||||||
|
// SkyCAT / rigctld
|
||||||
|
ConnectSkyCAT: (host, port) => wailsCall('main.App.ConnectSkyCAT', host, port),
|
||||||
|
DisconnectSkyCAT: () => wailsCall('main.App.DisconnectSkyCAT'),
|
||||||
|
GetSkyCATStatus: () => wailsCall('main.App.GetSkyCATStatus'),
|
||||||
|
SetRadioType: (type) => wailsCall('main.App.SetRadioType', type),
|
||||||
|
SetSkyCATMode: (mode) => wailsCall('main.App.SetSkyCATMode', mode),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wails event listener wrapper
|
// Wails event listener wrapper
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ trackedSat.subscribe(name => {
|
|||||||
|
|
||||||
// Connection states
|
// Connection states
|
||||||
export const flexConnected = writable(false)
|
export const flexConnected = writable(false)
|
||||||
|
export const skycatConnected = writable(false)
|
||||||
export const dopplerEnabled = writable(true)
|
export const dopplerEnabled = writable(true)
|
||||||
|
|
||||||
// Passes panel state — persisted in memory so tab switches don't reset filters
|
// Passes panel state — persisted in memory so tab switches don't reset filters
|
||||||
@@ -55,22 +56,27 @@ const DEFAULT_SETTINGS = {
|
|||||||
qthLat: 48.7,
|
qthLat: 48.7,
|
||||||
qthLon: 2.55,
|
qthLon: 2.55,
|
||||||
qthAlt: 100,
|
qthAlt: 100,
|
||||||
|
radioType: 'flex', // 'flex' | 'skycat'
|
||||||
flexHost: '192.168.1.100',
|
flexHost: '192.168.1.100',
|
||||||
flexPort: 4992,
|
flexPort: 4992,
|
||||||
|
skycatHost: '127.0.0.1',
|
||||||
|
skycatPort: 4532,
|
||||||
|
skycatSatMode: 'Duplex', // 'Duplex' | 'Split' | 'Simplex'
|
||||||
|
autoConnectSkycat: false,
|
||||||
rotorHost: '127.0.0.1',
|
rotorHost: '127.0.0.1',
|
||||||
rotorPort: 12000,
|
rotorPort: 12000,
|
||||||
rotorAzOnly: true, // true = azimuth only, false = az+el
|
rotorAzOnly: true,
|
||||||
downlinkHz: 145800000,
|
downlinkHz: 145800000,
|
||||||
uplinkHz: 145200000,
|
uplinkHz: 145200000,
|
||||||
minElFilter: 5,
|
minElFilter: 5,
|
||||||
autoConnectFlex: false,
|
autoConnectFlex: false,
|
||||||
autoConnectRotor: false,
|
autoConnectRotor: false,
|
||||||
rxSlice: 0, // Slice A = RX downlink
|
rxSlice: 0,
|
||||||
txSlice: 1, // Slice B = TX uplink
|
txSlice: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings version — bump this to force reset of specific fields
|
// Settings version — bump this to force reset of specific fields
|
||||||
const SETTINGS_VERSION = 3
|
const SETTINGS_VERSION = 4
|
||||||
|
|
||||||
// Load persisted settings from localStorage
|
// Load persisted settings from localStorage
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
@@ -85,6 +91,10 @@ function loadSettings() {
|
|||||||
...parsed,
|
...parsed,
|
||||||
rxSlice: DEFAULT_SETTINGS.rxSlice,
|
rxSlice: DEFAULT_SETTINGS.rxSlice,
|
||||||
txSlice: DEFAULT_SETTINGS.txSlice,
|
txSlice: DEFAULT_SETTINGS.txSlice,
|
||||||
|
radioType: DEFAULT_SETTINGS.radioType,
|
||||||
|
skycatHost: DEFAULT_SETTINGS.skycatHost,
|
||||||
|
skycatPort: DEFAULT_SETTINGS.skycatPort,
|
||||||
|
skycatSatMode: DEFAULT_SETTINGS.skycatSatMode,
|
||||||
_version: SETTINGS_VERSION,
|
_version: SETTINGS_VERSION,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user