first commit
This commit is contained in:
379
app.go
Normal file
379
app.go
Normal file
@@ -0,0 +1,379 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"SatMaster/backend/doppler"
|
||||
"SatMaster/backend/flexradio"
|
||||
"SatMaster/backend/propagator"
|
||||
"SatMaster/backend/rotor"
|
||||
"SatMaster/backend/tle"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App holds all subsystems and is the Wails binding target.
|
||||
type App struct {
|
||||
dopplerEnabled bool
|
||||
rotorEnabled bool
|
||||
rotorAzOnly bool
|
||||
trackFreqMode bool // Track freq+mode for current sat
|
||||
trackAzimuth bool // Track azimuth for current sat
|
||||
savedRxSlice int
|
||||
savedTxSlice int
|
||||
ctx context.Context
|
||||
|
||||
tleManager *tle.Manager
|
||||
propagator *propagator.Engine
|
||||
flexRadio *flexradio.Client
|
||||
rotorClient *rotor.PstRotatorClient
|
||||
dopplerCalc *doppler.Calculator
|
||||
|
||||
// Tracking state
|
||||
trackedSat string
|
||||
trackingOn bool
|
||||
cancelTrack context.CancelFunc
|
||||
watchlist []string
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{
|
||||
dopplerEnabled: true,
|
||||
rotorEnabled: true,
|
||||
rotorAzOnly: true,
|
||||
trackFreqMode: false,
|
||||
trackAzimuth: false,
|
||||
savedRxSlice: 0, // defaults — overridden by SetSliceConfig from Settings on connect
|
||||
savedTxSlice: 1,
|
||||
tleManager: tle.NewManager(),
|
||||
propagator: propagator.NewEngine(),
|
||||
flexRadio: flexradio.NewClient(),
|
||||
rotorClient: rotor.NewPstRotatorClient(),
|
||||
dopplerCalc: doppler.NewCalculator(),
|
||||
watchlist: []string{"ISS (ZARYA)", "AO-7", "AO-27", "SO-50", "RS-44", "AO-91", "FO-29"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
log.Println("[SatMaster] Startup")
|
||||
|
||||
// Load TLEs on startup (try remote, fallback local)
|
||||
go func() {
|
||||
if err := a.tleManager.FetchAndCache(); err != nil {
|
||||
log.Printf("[TLE] Remote fetch failed: %v — loading local cache", err)
|
||||
a.tleManager.LoadLocal()
|
||||
}
|
||||
runtime.EventsEmit(ctx, "tle:loaded", a.tleManager.SatelliteNames())
|
||||
// Immediately emit positions after TLE load
|
||||
allSats := a.tleManager.All()
|
||||
log.Printf("[TLE] Loaded %d satellites, watchlist=%v", len(allSats), a.watchlist)
|
||||
var initSats []*tle.Satellite
|
||||
for _, name := range a.watchlist {
|
||||
if s := a.tleManager.Get(name); s != nil {
|
||||
initSats = append(initSats, s)
|
||||
}
|
||||
}
|
||||
if len(initSats) == 0 {
|
||||
log.Printf("[TLE] Watchlist resolved 0 sats, using all %d", len(allSats))
|
||||
initSats = allSats
|
||||
} else {
|
||||
log.Printf("[TLE] Watchlist resolved %d sats", len(initSats))
|
||||
}
|
||||
if positions := a.propagator.AllPositions(initSats, time.Now()); len(positions) > 0 {
|
||||
log.Printf("[TLE] Emitting %d initial positions", len(positions))
|
||||
runtime.EventsEmit(ctx, "sat:positions", positions)
|
||||
}
|
||||
}()
|
||||
|
||||
// Emit position updates every second (filtered by watchlist)
|
||||
go a.positionLoop(ctx)
|
||||
}
|
||||
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
a.StopTracking()
|
||||
a.flexRadio.Disconnect()
|
||||
}
|
||||
|
||||
// positionLoop emits sat positions to frontend every second.
|
||||
func (a *App) positionLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Build sat list from watchlist; fall back to all if empty/unresolved
|
||||
allSats := a.tleManager.All()
|
||||
var sats []*tle.Satellite
|
||||
for _, name := range a.watchlist {
|
||||
if s := a.tleManager.Get(name); s != nil {
|
||||
sats = append(sats, s)
|
||||
}
|
||||
}
|
||||
if len(sats) == 0 {
|
||||
sats = allSats
|
||||
}
|
||||
positions := a.propagator.AllPositions(sats, time.Now())
|
||||
log.Printf("[Loop] watchlist=%d resolved=%d positions=%d", len(a.watchlist), len(sats), len(positions))
|
||||
runtime.EventsEmit(ctx, "sat:positions", positions)
|
||||
|
||||
// If tracking active, update doppler + rotor
|
||||
if a.trackingOn && a.trackedSat != "" {
|
||||
a.updateTracking()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) updateTracking() {
|
||||
sat := a.tleManager.Get(a.trackedSat)
|
||||
if sat == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
pos := a.propagator.Position(sat, now)
|
||||
if pos == nil {
|
||||
return
|
||||
}
|
||||
obs := a.propagator.ObserverPosition()
|
||||
|
||||
// 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 {
|
||||
upFreq, downFreq := a.dopplerCalc.Correct(pos, obs, now)
|
||||
if err := a.flexRadio.SetFrequency(downFreq, upFreq); err != nil {
|
||||
log.Printf("[Doppler] SetFrequency error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Rotor: only when globally enabled AND tracking enabled for this sat AND el >= 0
|
||||
if a.rotorClient.IsConnected() && a.rotorEnabled && a.trackAzimuth && pos.Elevation >= 0 {
|
||||
var rotErr error
|
||||
if a.rotorAzOnly {
|
||||
rotErr = a.rotorClient.SetAzOnly(pos.Azimuth)
|
||||
} else {
|
||||
rotErr = a.rotorClient.SetAzEl(pos.Azimuth, pos.Elevation)
|
||||
}
|
||||
if rotErr != nil {
|
||||
log.Printf("[Rotor] error: %v", rotErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Wails-exposed methods ──────────────────────────────────────────────────
|
||||
|
||||
// GetSatelliteList returns all satellite names from loaded TLEs.
|
||||
func (a *App) GetSatelliteList() []string {
|
||||
return a.tleManager.SatelliteNames()
|
||||
}
|
||||
|
||||
// GetPasses returns upcoming passes for a satellite over the next 24h.
|
||||
func (a *App) GetPasses(satName string, hours float64) []propagator.Pass {
|
||||
sat := a.tleManager.Get(satName)
|
||||
if sat == nil {
|
||||
return nil
|
||||
}
|
||||
return a.propagator.ComputePasses(sat, time.Now(), hours)
|
||||
}
|
||||
|
||||
// GetCurrentPosition returns az/el/lat/lon for a satellite right now.
|
||||
func (a *App) GetCurrentPosition(satName string) *propagator.SatPosition {
|
||||
sat := a.tleManager.Get(satName)
|
||||
if sat == nil {
|
||||
return nil
|
||||
}
|
||||
return a.propagator.Position(sat, time.Now())
|
||||
}
|
||||
|
||||
// SetObserverLocation sets the QTH for pass prediction and Doppler.
|
||||
func (a *App) SetObserverLocation(lat, lon, altM float64) {
|
||||
a.propagator.SetObserver(lat, lon, altM)
|
||||
a.dopplerCalc.SetObserver(lat, lon, altM)
|
||||
}
|
||||
|
||||
// SetSatelliteFrequencies configures nominal uplink/downlink for Doppler.
|
||||
func (a *App) SetSatelliteFrequencies(downHz, upHz float64) {
|
||||
a.dopplerCalc.SetNominal(downHz, upHz)
|
||||
}
|
||||
|
||||
// SetSatelliteMode sets the FlexRadio slice mode for satellite operation.
|
||||
// Called automatically when a frequency is selected in the frontend.
|
||||
func (a *App) SetSatelliteMode(mode string) {
|
||||
if !a.flexRadio.IsConnected() {
|
||||
return
|
||||
}
|
||||
flexMode := flexradio.SatModeToFlex(mode)
|
||||
if err := a.flexRadio.SetMode(flexMode); err != nil {
|
||||
log.Printf("[FlexRadio] SetMode error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// StartTracking begins active tracking of a satellite.
|
||||
func (a *App) StartTracking(satName string) string {
|
||||
sat := a.tleManager.Get(satName)
|
||||
if sat == nil {
|
||||
return "Satellite not found: " + satName
|
||||
}
|
||||
// Reset per-satellite tracking toggles on satellite change
|
||||
if a.trackedSat != satName {
|
||||
a.trackFreqMode = false
|
||||
a.trackAzimuth = false
|
||||
}
|
||||
a.trackedSat = satName
|
||||
a.trackingOn = true
|
||||
log.Printf("[Tracking] Started: %s", satName)
|
||||
return "OK"
|
||||
}
|
||||
|
||||
// StopTracking stops active tracking.
|
||||
func (a *App) StopTracking() {
|
||||
a.trackingOn = false
|
||||
a.trackedSat = ""
|
||||
if a.cancelTrack != nil {
|
||||
a.cancelTrack()
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectFlexRadio connects to the FlexRadio 8600 SmartSDR TCP API.
|
||||
func (a *App) ConnectFlexRadio(host string, port int) string {
|
||||
if err := a.flexRadio.Connect(host, port); err != nil {
|
||||
return "Error: " + err.Error()
|
||||
}
|
||||
// Restore saved slice config and query slices
|
||||
go func() {
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
a.flexRadio.SetSlices(a.savedRxSlice, a.savedTxSlice)
|
||||
a.flexRadio.QuerySlices()
|
||||
}()
|
||||
return "OK"
|
||||
}
|
||||
|
||||
// DisconnectFlexRadio closes the FlexRadio connection.
|
||||
func (a *App) DisconnectFlexRadio() {
|
||||
a.flexRadio.Disconnect()
|
||||
}
|
||||
|
||||
// ConnectRotor connects to PstRotator UDP.
|
||||
func (a *App) ConnectRotor(host string, port int) string {
|
||||
if err := a.rotorClient.Connect(host, port); err != nil {
|
||||
return "Error: " + err.Error()
|
||||
}
|
||||
return "OK"
|
||||
}
|
||||
|
||||
// DisconnectRotor closes the PstRotator connection.
|
||||
func (a *App) DisconnectRotor() {
|
||||
a.rotorClient.Disconnect()
|
||||
}
|
||||
|
||||
// RefreshTLE forces a re-fetch of TLE data.
|
||||
func (a *App) RefreshTLE() string {
|
||||
if err := a.tleManager.FetchAndCache(); err != nil {
|
||||
return "Error: " + err.Error()
|
||||
}
|
||||
runtime.EventsEmit(a.ctx, "tle:loaded", a.tleManager.SatelliteNames())
|
||||
return "OK"
|
||||
}
|
||||
|
||||
// SetRotorAzOnly sets azimuth-only mode (true) or az+el mode (false).
|
||||
func (a *App) SetRotorAzOnly(azOnly bool) {
|
||||
a.rotorAzOnly = azOnly
|
||||
mode := "Az+El"
|
||||
if azOnly {
|
||||
mode = "Az only"
|
||||
}
|
||||
log.Printf("[Rotor] mode: %s", mode)
|
||||
}
|
||||
|
||||
// SetRotorEnabled enables or disables rotator tracking.
|
||||
func (a *App) SetRotorEnabled(enabled bool) {
|
||||
a.rotorEnabled = enabled
|
||||
log.Printf("[Rotor] tracking %s", map[bool]string{true: "enabled", false: "disabled"}[enabled])
|
||||
}
|
||||
|
||||
// GetRotorEnabled returns current rotator tracking state.
|
||||
func (a *App) GetRotorEnabled() bool {
|
||||
return a.rotorEnabled
|
||||
}
|
||||
|
||||
// SetTrackFreqMode enables/disables frequency+mode tracking for current satellite.
|
||||
func (a *App) SetTrackFreqMode(enabled bool) {
|
||||
a.trackFreqMode = enabled
|
||||
// Reset dead-band so freq is sent immediately when tracking starts
|
||||
if enabled {
|
||||
a.flexRadio.ResetDeadband()
|
||||
}
|
||||
log.Printf("[Doppler] Freq/Mode tracking: %v", enabled)
|
||||
}
|
||||
|
||||
// SetTrackAzimuth enables/disables azimuth tracking for current satellite.
|
||||
func (a *App) SetTrackAzimuth(enabled bool) {
|
||||
a.trackAzimuth = enabled
|
||||
// Reset rotor dead-band so it moves immediately
|
||||
if enabled {
|
||||
a.rotorClient.ResetDeadband()
|
||||
}
|
||||
log.Printf("[Rotor] Azimuth tracking: %v", enabled)
|
||||
}
|
||||
|
||||
// SetDopplerEnabled enables or disables Doppler correction.
|
||||
func (a *App) SetDopplerEnabled(enabled bool) {
|
||||
a.dopplerEnabled = enabled
|
||||
log.Printf("[Doppler] %s", map[bool]string{true: "enabled", false: "disabled"}[enabled])
|
||||
}
|
||||
|
||||
// GetDopplerEnabled returns current Doppler state.
|
||||
func (a *App) GetDopplerEnabled() bool {
|
||||
return a.dopplerEnabled
|
||||
}
|
||||
|
||||
// GetFlexRadioStatus returns connection status.
|
||||
func (a *App) GetFlexRadioStatus() bool {
|
||||
return a.flexRadio.IsConnected()
|
||||
}
|
||||
|
||||
// GetSliceConfig returns current RX/TX slice indices.
|
||||
func (a *App) GetSliceConfig() map[string]int {
|
||||
rx, tx := a.flexRadio.GetSlices()
|
||||
return map[string]int{"rx": rx, "tx": tx}
|
||||
}
|
||||
|
||||
// SetSliceConfig sets RX and TX slice indices (0=A, 1=B, ...).
|
||||
func (a *App) SetSliceConfig(rxIdx, txIdx int) {
|
||||
a.savedRxSlice = rxIdx
|
||||
a.savedTxSlice = txIdx
|
||||
a.flexRadio.SetSlices(rxIdx, txIdx)
|
||||
log.Printf("[FlexRadio] Slice config saved: RX=%d TX=%d", rxIdx, txIdx)
|
||||
}
|
||||
|
||||
// GetRotorStatus returns connection status.
|
||||
func (a *App) GetRotorStatus() bool {
|
||||
return a.rotorClient.IsConnected()
|
||||
}
|
||||
|
||||
// GetTLEAge returns the age of the TLE cache in hours.
|
||||
func (a *App) GetTLEAge() float64 {
|
||||
return a.tleManager.AgeHours()
|
||||
}
|
||||
|
||||
// GetGroundtrack returns lat/lon points for the next N minutes of orbit.
|
||||
func (a *App) GetGroundtrack(satName string, minutes float64) []propagator.GroundtrackPoint {
|
||||
sat := a.tleManager.Get(satName)
|
||||
if sat == nil {
|
||||
return nil
|
||||
}
|
||||
return a.propagator.ComputeGroundtrack(sat, time.Now(), minutes)
|
||||
}
|
||||
|
||||
// GetWatchlist returns the current list of satellites to display on map.
|
||||
func (a *App) GetWatchlist() []string {
|
||||
return a.watchlist
|
||||
}
|
||||
|
||||
// SetWatchlist sets which satellites to display on the map.
|
||||
func (a *App) SetWatchlist(names []string) {
|
||||
a.watchlist = names
|
||||
}
|
||||
Reference in New Issue
Block a user