added SkyCAT support

This commit is contained in:
2026-03-25 00:18:12 +01:00
parent 313d22be2e
commit 5b31a9a9d8
8 changed files with 432 additions and 22 deletions

93
app.go
View File

@@ -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
View 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.

View File

@@ -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)

View File

@@ -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>

View File

@@ -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">
{#if $settings.radioType === 'skycat'}
<div class="conn {$skycatConnected ? 'on' : 'off'}">
<span class="conn-dot"></span>
<span>SKYCAT</span>
</div>
{:else}
<div class="conn {$flexConnected ? 'on' : 'off'}"> <div class="conn {$flexConnected ? 'on' : 'off'}">
<span class="conn-dot"></span> <span class="conn-dot"></span>
<span>FLEX</span> <span>FLEX</span>
</div> </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>

View File

@@ -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

View File

@@ -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,
} }
} }