first commit

This commit is contained in:
2026-03-24 23:24:36 +01:00
commit a69394a05b
1638 changed files with 891299 additions and 0 deletions

379
app.go Normal file
View 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
}