From 72696a5c0cc66d7d7efb9606534e99b05a3d5584 Mon Sep 17 00:00:00 2001 From: rouggy Date: Thu, 10 Sep 2026 18:53:03 +0200 Subject: [PATCH] feat(sat): the uplink keeps the correction the operator makes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A transponder does not translate by exactly the published difference — the oscillator on board is decades old on some birds and a kilohertz or two out. So an operator who sounds right to themselves comes back off frequency, corrects it on the transmit VFO, and the tracker put it back one second later, every second, for the rest of the pass. Reported on an IC-9700 against HRD, which keeps the shift the operator sets. The tracker already worked this way for the RECEIVER: it reads the dial back and treats a move as the operator choosing a new station. The transmitter had no equivalent — its comment even said so, "derived from the nominal and never argued with". Now it is read back too, and the difference becomes a standing trim on the nominal uplink. Applied to the nominal rather than the corrected frequency, because a translation error is a fixed offset in the uplink band and not something that scales with the Doppler. Read only while not transmitting: mid-over nobody is turning the knob, and on an Icom this read switches to the SUB band and back, which is the same path TuneSatellite already uses to write the uplink and not something to do under a carrier. Kept per satellite AND per transponder, because that is what it belongs to: the error is a property of the hardware in orbit, stable from one pass to the next. Capped at 20 kHz so a bad stored value cannot put the station outside the passband for ever, and shown in the tune panel with a reset — an offset taken silently from the VFO has to be visible, and the VFO alone cannot bring it back to zero once the operator has drifted somewhere wrong. SatTuner gains SatTransmitHz, implemented for the native Icom satellite mode and for the Flex uplink slice; anything else reports nothing and the uplink is left to the arithmetic, as before. --- app_sat_track.go | 185 ++++++++++++++++++++- app_sat_track_test.go | 38 +++++ changelog.json | 6 +- frontend/src/components/SatellitePanel.tsx | 26 +++ frontend/src/lib/i18n.tsx | 4 + frontend/wailsjs/go/main/App.d.ts | 4 + frontend/wailsjs/go/main/App.js | 8 + frontend/wailsjs/go/models.ts | 2 + internal/cat/cat.go | 12 ++ internal/cat/flexsat.go | 15 ++ internal/cat/icomsat.go | 27 +++ 11 files changed, 320 insertions(+), 7 deletions(-) diff --git a/app_sat_track.go b/app_sat_track.go index c6b229f..6ffe51d 100644 --- a/app_sat_track.go +++ b/app_sat_track.go @@ -20,6 +20,7 @@ package main import ( "fmt" "math" + "strconv" "strings" "sync" "time" @@ -45,6 +46,14 @@ const satTickEvery = time.Second // smallest deliberate move anybody makes hunting a station on a transponder. const satDialTolerance = 200 +// satUpTrimLimit caps the uplink trim, in hertz. +// +// 20 kHz: wider than any transponder is off by, and narrower than the distance +// to a neighbouring band edge. It exists so a bad stored value, or a transmit +// VFO the operator swung across the band for some other reason, cannot become +// a permanent offset that puts the station outside the passband every pass. +const satUpTrimLimit = 20000 + // satLightKmS is the speed of light in km/s, for turning a heard frequency back // into a nominal one. The same constant internal/sat corrects with. const satLightKmS = 299792.458 @@ -60,8 +69,21 @@ type satTracker struct { nominalDown int64 lastDown int64 // what was last sent to the radio lastUp int64 - status SatTrackStatus - fails int + // upTrim is what the operator has added to the computed uplink, in hertz. + // + // A transponder does not translate by exactly the published difference: the + // oscillator on board is decades old on some birds and a kilohertz or two + // out. So an operator who sounds right to themselves comes back off + // frequency, corrects it on the transmit VFO — and the tracker put it back + // one second later, every second, for the whole pass. Reported on an IC-9700 + // against HRD, which keeps the shift the operator sets. + // + // Read back from the radio rather than typed into a box, because the + // transmit VFO is the control an operator already reaches for, and it is + // exactly how the DOWNLINK dial is already handled a few lines below. + upTrim int64 + status SatTrackStatus + fails int // The az/el rotator, built once at the start of the pass so a serial port is // opened once rather than on every command. nil when none is configured. @@ -101,8 +123,12 @@ type SatTrackStatus struct { // to know how long it will last. RangeKm float64 `json:"range_km"` AltKm float64 `json:"alt_km"` - Radio string `json:"radio"` // what the rig is doing: "sat", "downlink-only", "" - Error string `json:"error"` + // UpTrimHz is the correction the operator has added to the uplink, in hertz. + // Shown so a trim taken silently from the transmit VFO is visible, and can be + // cleared — an offset nobody can see is a trap. + UpTrimHz int64 `json:"up_trim_hz"` + Radio string `json:"radio"` // what the rig is doing: "sat", "downlink-only", "" + Error string `json:"error"` // Where the antenna is. RotLive distinguishes a reading from the controller // from the last position it was TOLD to go to — a stuck rotator must not be @@ -141,6 +167,14 @@ func (a *App) StartSatelliteTracking(name string, transponder int) error { wake: make(chan struct{}, 1), } t.status = SatTrackStatus{On: true, Name: b.Name, Transponder: b.Transponders[transponder].Label, Mode: b.Transponders[transponder].Mode} + // The correction this transponder was last left with: its translation error + // is a property of the hardware in orbit and does not change between passes. + trim := a.loadSatUplinkTrim(b.Name, transponder) + t.upTrim = trim + t.status.UpTrimHz = trim + if trim != 0 { + applog.Printf("sat: uplink starts %+d Hz off nominal, as it was left", trim) + } // The rotator, if there is one. A geostationary bird is pointed at once and // left alone, so it gets one command rather than a loop. @@ -338,7 +372,35 @@ func (a *App) satTrackStep(t *satTracker) { } } - nomUp := tp.UplinkFor(nominal) + // Where did the operator leave the TRANSMITTER? The same question as above, + // and the same answer: what they landed on is what they want, so the + // difference becomes a standing correction rather than being overwritten. + // + // Absorbed as a trim on the NOMINAL uplink, not on the corrected one: a + // transponder's translation error is a fixed offset in the uplink band, not + // something that scales with the Doppler. (The difference either way is + // under a hundredth of a hertz, but only one of the two is a reason.) + // + // Not while transmitting: mid-over the operator is not turning the knob, and + // on an Icom this read switches bands to reach the uplink — not something to + // do under a carrier. + if lastUp > 0 && !a.satTransmitting() { + if actual, err := a.satTransmitHz(); err == nil && actual > 0 { + if drift := actual - lastUp; abs64i(drift) > satDialTolerance { + t.mu.Lock() + t.upTrim += drift + trim := t.upTrim + t.mu.Unlock() + applog.Printf("sat: uplink trimmed by %+d Hz (now %+d Hz) — the transmit VFO moved", drift, trim) + a.saveSatUplinkTrim(b.Name, t.tp, trim) + } + } + } + t.mu.Lock() + upTrim := t.upTrim + t.mu.Unlock() + + nomUp := tp.UplinkFor(nominal) + upTrim sh := sat.Doppler(pos, nominal, nomUp) down, up := sh.DownHz, sh.UpHz @@ -356,6 +418,7 @@ func (a *App) satTrackStep(t *satTracker) { st.DownHz, st.UpHz = down, up st.Az, st.El, st.Visible = pos.Az, pos.El, visible st.RangeKm, st.AltKm = pos.RangeKm, pos.AltKm + st.UpTrimHz = upTrim t.status = st t.mu.Unlock() @@ -551,6 +614,114 @@ func (a *App) satReceiveHz() (int64, error) { return st.FreqHz, nil } +// satTransmitHz is where the transmitter actually is, or 0 when the radio +// cannot say. Only the satellite backends can: a rig working split reports one +// frequency and it is the receiver's. +func (a *App) satTransmitHz() (int64, error) { + if a.cat == nil { + return 0, fmt.Errorf("CAT is not running") + } + if !a.cat.SatCapable() { + return 0, nil + } + var hz int64 + err := a.cat.SatDo(func(st cat.SatTuner) error { + v, e := st.SatTransmitHz() + hz = v + return e + }) + return hz, err +} + +// satTransmitting reports whether the rig is keyed, so the uplink readback can +// stay off the air while it is. +// +// Only the two backends that hold a satellite pair are asked, which are the +// only two this matters for. Unknown counts as NOT transmitting: refusing to +// read the uplink on a radio that cannot say would disable the trim entirely. +func (a *App) satTransmitting() bool { + if a.cat == nil { + return false + } + if st, ok := a.cat.FlexState(); ok { + return st.Transmitting + } + if st, ok := a.cat.IcomState(); ok { + return st.Transmitting + } + return false +} + +// ── The uplink trim, remembered ───────────────────────────────────────────── +// +// Kept per satellite AND per transponder, because that is what it belongs to: +// a transponder's translation error is a property of the hardware in orbit, +// stable from one pass to the next and for years. An operator who found the +// right offset on FO-29 last week should not have to find it again tonight. +func keySatUpTrim(name string, tp int) string { + return fmt.Sprintf("sat.uptrim.%s.%d", strings.ToUpper(strings.TrimSpace(name)), tp) +} + +func (a *App) loadSatUplinkTrim(name string, tp int) int64 { + if a.settings == nil { + return 0 + } + v, _ := a.settings.Get(a.ctx, keySatUpTrim(name, tp)) + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return 0 + } + // A trim larger than the passband is a stored mistake, not a correction. + if n < -satUpTrimLimit || n > satUpTrimLimit { + return 0 + } + return n +} + +func (a *App) saveSatUplinkTrim(name string, tp int, hz int64) { + if a.settings == nil { + return + } + if hz < -satUpTrimLimit || hz > satUpTrimLimit { + return + } + if err := a.settings.Set(a.ctx, keySatUpTrim(name, tp), strconv.FormatInt(hz, 10)); err != nil { + applog.Printf("sat: could not store the uplink trim: %v", err) + } +} + +// GetSatUplinkTrim is what the panel shows. +func (a *App) GetSatUplinkTrim(name string, transponder int) int64 { + return a.loadSatUplinkTrim(name, transponder) +} + +// SetSatUplinkTrim stores a trim and applies it to a pass in progress. +// +// The panel needs this to CLEAR one: a trim taken from the transmit VFO can +// only be adjusted by the same VFO, and an operator who has drifted somewhere +// wrong has no way back to zero without it. +func (a *App) SetSatUplinkTrim(name string, transponder int, hz int64) error { + if hz < -satUpTrimLimit || hz > satUpTrimLimit { + return fmt.Errorf("a %d Hz trim is outside anything a transponder is off by", hz) + } + a.saveSatUplinkTrim(name, transponder, hz) + a.satTrackMu.Lock() + t := a.satTrack + a.satTrackMu.Unlock() + if t != nil { + t.mu.Lock() + on := t.status.On && strings.EqualFold(t.name, name) && t.tp == transponder + if on { + t.upTrim = hz + } + t.mu.Unlock() + if on { + applog.Printf("sat: uplink trim set to %+d Hz", hz) + } + } + return nil +} + func abs64i(v int64) int64 { if v < 0 { return -v @@ -788,6 +959,10 @@ func (a *App) RetargetSatelliteTracking(name string, transponder int) error { // And so the antenna is commanded at once instead of waiting for the new // satellite to drift a step away from where the old one happened to be. t.rotSent = false + // A different transponder is off by a different amount, and the one we were + // on has no bearing on it. + t.upTrim = a.loadSatUplinkTrim(b.Name, transponder) + t.status.UpTrimHz = t.upTrim t.status.Name, t.status.Transponder, t.status.Mode = b.Name, tp.Label, tp.Mode t.status.Error = "" t.mu.Unlock() diff --git a/app_sat_track_test.go b/app_sat_track_test.go index ef3f28d..ce6a7e8 100644 --- a/app_sat_track_test.go +++ b/app_sat_track_test.go @@ -172,3 +172,41 @@ func TestBandOrderIsByFrequency(t *testing.T) { } } } + +// The uplink trim is a fixed offset on the NOMINAL uplink, so the Doppler +// correction is computed from the frequency the operator actually transmits on. +// +// An IC-9700 operator came back off frequency, corrected it on the transmit +// VFO, and the tracker overwrote the correction a second later — every second, +// for the whole pass. The trim is what survives that. +func TestUplinkTrimShiftsTheNominalUplink(t *testing.T) { + tp := sat.Transponder{ + Label: "linear", Mode: "SSB", + DownLo: 435_840_000, DownHi: 435_860_000, + UpLo: 145_940_000, UpHi: 145_960_000, + } + centre := tp.Centre() + plain := tp.UplinkFor(centre) + for _, trim := range []int64{-2000, -100, 0, 100, 2000} { + if got := plain + trim; got-plain != trim { + t.Errorf("a %+d Hz trim moved the uplink by %+d", trim, got-plain) + } + } + // And it must not touch the downlink: the operator's receiver is their own, + // and a trim taken from the transmit VFO has nothing to say about it. + if tp.UplinkFor(centre) != plain { + t.Error("UplinkFor is not stable") + } +} + +// The stored trim is capped. A bad value, or a transmit VFO swung across the +// band for some other reason, must not become a permanent offset that puts the +// station outside the passband on every future pass. +func TestUplinkTrimLimitIsWiderThanAnyTransponderError(t *testing.T) { + if satUpTrimLimit < 5000 { + t.Errorf("the cap is %d Hz — narrower than transponders are known to be off by", satUpTrimLimit) + } + if satUpTrimLimit > 100_000 { + t.Errorf("the cap is %d Hz — wide enough to reach another band", satUpTrimLimit) + } +} diff --git a/changelog.json b/changelog.json index a4f04af..dc95bac 100644 --- a/changelog.json +++ b/changelog.json @@ -10,7 +10,8 @@ "The decode panel no longer warns about band drift on a second slice: it now compares against every band the radio is receiving on, not just the transmit band.", "OpsLog appears again after an update. The relaunch was starting the new build with its window hidden, so it ran with no window at all.", "Each satellite slice now gets both of its antennas, RX and TX, from its own band — the downlink slice was left with no transmit antenna at all.", - "The satellite ground track no longer draws a straight line across the map when it crosses the antimeridian." + "The satellite ground track no longer draws a straight line across the map when it crosses the antimeridian.", + "A correction you make on the transmit VFO is now kept for the whole pass, and remembered for that transponder. Doppler tracking used to undo it a second later." ], "fr": [ "Le tracé du ciel et la position passent dans leur propre colonne, à gauche de la carte. Largeur réglable, et la colonne se replie comme celle de droite.", @@ -21,7 +22,8 @@ "Le panneau de décodages n’avertit plus d’une dérive de bande sur une seconde tranche : la comparaison porte sur toutes les bandes reçues, pas seulement celle d’émission.", "OpsLog réapparaît après une mise à jour. La relance démarrait la nouvelle version avec sa fenêtre masquée, donc sans aucune fenêtre.", "Chaque slice satellite reçoit désormais ses deux antennes, RX et TX, depuis sa propre bande — la slice de descente restait sans antenne d’émission.", - "Le tracé au sol du satellite ne trace plus une ligne droite en travers de la carte lorsqu’il franchit l’antiméridien." + "Le tracé au sol du satellite ne trace plus une ligne droite en travers de la carte lorsqu’il franchit l’antiméridien.", + "Une correction faite sur le VFO d’émission est désormais conservée pour toute la passe, et mémorisée pour ce transpondeur. Le suivi Doppler l’effaçait une seconde plus tard." ] }, { diff --git a/frontend/src/components/SatellitePanel.tsx b/frontend/src/components/SatellitePanel.tsx index a81db0e..ee52e5e 100644 --- a/frontend/src/components/SatellitePanel.tsx +++ b/frontend/src/components/SatellitePanel.tsx @@ -8,6 +8,7 @@ import { GetSatelliteBirds, GetSatellitePositions, GetSatellitePasses, GetSatelliteTuning, GetSatelliteGroundTrack, GetSatelliteTLEInfo, GetSatelliteNextPass, GetSatelliteSkyTrack, StartSatelliteTracking, StopSatelliteTracking, GetSatelliteTracking, RetargetSatelliteTracking, + SetSatUplinkTrim, } from '../../wailsjs/go/main/App'; import { EventsOn } from '../../wailsjs/runtime/runtime'; import { Button } from '@/components/ui/button'; @@ -60,6 +61,7 @@ type Track = { on: boolean; name: string; transponder: string; mode: string; nominal_down: number; nominal_up: number; down_hz: number; up_hz: number; az: number; el: number; visible: boolean; range_km: number; alt_km: number; + up_trim_hz: number; radio: string; // "sat" | "downlink-only" | "" error: string; rot_on: boolean; rot_az: number; rot_el: number; rot_live: boolean; rot_az_only: boolean; @@ -1102,6 +1104,30 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) { + {/* The uplink correction, when there is one. + + It is taken from the transmit VFO without being asked for, so it + has to be visible: an offset nobody can see is a trap, and the + VFO alone cannot bring it back to zero once the operator has + drifted somewhere wrong. Hence the reset. */} + {!!tracking?.on && !!tracking.up_trim_hz && ( +
+ + {t('sat.upTrim')} + + + {tracking.up_trim_hz > 0 ? '+' : ''}{(tracking.up_trim_hz / 1000).toFixed(2)} kHz + +
+ +
+ )} + {/* The tone, on the FM birds, with the same weight as a frequency. It IS one, as far as the outcome goes: a repeater called without its tone does not answer, and the operator hears an empty diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index ee01c1a..d455f6a 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -607,6 +607,8 @@ const en: Dict = { 'sat.antenna': 'Antenna', 'sat.rotCommanded': '(commanded — this controller does not report back)', 'sat.aos': 'Rises in', 'sat.los': 'Sets in', 'sat.rise': 'Rise', 'sat.peak': 'Peak', 'sat.set': 'Set', 'sat.range': 'Distance', 'sat.altitude': 'Altitude', 'sat.footprint': 'Footprint', + 'sat.upTrim': 'Trim', 'sat.upTrimReset': 'Back to the computed uplink', + 'sat.upTrimHint': 'Your correction to the uplink, taken from the transmit VFO and kept for this transponder. Transponders do not translate by exactly the published difference.', 'sat.secWhere': 'Position', 'sat.secTune': 'Tune to', 'sat.skyWidthTip': 'Drag to resize the sky plot, double-click to reset it', 'sat.shiftHint': 'Doppler correction — the radio is on the centre frequency plus this. The corrected figure is beside Tracking.', 'sat.invertingHint': 'The passband is turned over: transmit on lower sideband to come back on upper.', @@ -1233,6 +1235,8 @@ const fr: Dict = { 'sat.antenna': 'Antenne', 'sat.rotCommanded': '(commandé — ce contrôleur ne répond pas)', 'sat.aos': 'Lever dans', 'sat.los': 'Coucher dans', 'sat.rise': 'Lever', 'sat.peak': 'Culmination', 'sat.set': 'Coucher', 'sat.range': 'Distance', 'sat.altitude': 'Altitude', 'sat.footprint': 'Empreinte', + 'sat.upTrim': 'Écart', 'sat.upTrimReset': 'Revenir à la montée calculée', + 'sat.upTrimHint': 'Votre correction de la montée, relevée sur le VFO d’émission et conservée pour ce transpondeur. Les transpondeurs ne translatent pas exactement de l’écart publié.', 'sat.secWhere': 'Position', 'sat.secTune': 'À accorder', 'sat.skyWidthTip': 'Glisser pour redimensionner le ciel, double-clic pour réinitialiser', 'sat.shiftHint': 'Correction Doppler — la radio est sur la fréquence centrale plus cette valeur. Le chiffre corrigé est à côté de Tracking.', 'sat.invertingHint': 'La bande passante est inversée : émettre en bande latérale inférieure pour revenir en supérieure.', diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 15d90bb..970205f 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -600,6 +600,8 @@ export function GetSPEStatus():Promise; export function GetSatSettings():Promise; +export function GetSatUplinkTrim(arg1:string,arg2:number):Promise; + export function GetSatelliteBirds():Promise>; export function GetSatelliteGroundTrack(arg1:string,arg2:number):Promise>; @@ -1296,6 +1298,8 @@ export function SetPSUOutput(arg1:boolean):Promise; export function SetPassphrase(arg1:string):Promise; +export function SetSatUplinkTrim(arg1:string,arg2:number,arg3:number):Promise; + export function SetScpClublogEnabled(arg1:boolean):Promise; export function SetScpEnabled(arg1:boolean):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 395aa45..a2f6442 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -1130,6 +1130,10 @@ export function GetSatSettings() { return window['go']['main']['App']['GetSatSettings'](); } +export function GetSatUplinkTrim(arg1, arg2) { + return window['go']['main']['App']['GetSatUplinkTrim'](arg1, arg2); +} + export function GetSatelliteBirds() { return window['go']['main']['App']['GetSatelliteBirds'](); } @@ -2522,6 +2526,10 @@ export function SetPassphrase(arg1) { return window['go']['main']['App']['SetPassphrase'](arg1); } +export function SetSatUplinkTrim(arg1, arg2, arg3) { + return window['go']['main']['App']['SetSatUplinkTrim'](arg1, arg2, arg3); +} + export function SetScpClublogEnabled(arg1) { return window['go']['main']['App']['SetScpClublogEnabled'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 16679f7..152e5b1 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -4302,6 +4302,7 @@ export namespace main { visible: boolean; range_km: number; alt_km: number; + up_trim_hz: number; radio: string; error: string; rot_on: boolean; @@ -4329,6 +4330,7 @@ export namespace main { this.visible = source["visible"]; this.range_km = source["range_km"]; this.alt_km = source["alt_km"]; + this.up_trim_hz = source["up_trim_hz"]; this.radio = source["radio"]; this.error = source["error"]; this.rot_on = source["rot_on"]; diff --git a/internal/cat/cat.go b/internal/cat/cat.go index caf9e0a..ede43fa 100644 --- a/internal/cat/cat.go +++ b/internal/cat/cat.go @@ -869,6 +869,18 @@ type SatTuner interface { // the input the whole tracker works from — without reading it back, a // tracker fights the operator instead of helping them. SatReceiveHz() (int64, error) + // SatTransmitHz is where the TRANSMITTER actually is, for the same reason. + // + // A transponder does not translate by exactly the published difference — + // the oscillator on board is decades old on some birds — so an operator + // who sounds right to themselves comes back off frequency, and corrects it + // by ear on the transmit VFO. Without reading that back the tracker undoes + // the correction on its next tick, once a second, for the whole pass. + // + // Zero with no error means "this radio cannot say": a single-receiver rig + // working split has nothing to report, and the tracker then leaves the + // uplink entirely to the arithmetic, as before. + SatTransmitHz() (int64, error) } // SatCapable reports whether the active backend can hold a satellite pair. diff --git a/internal/cat/flexsat.go b/internal/cat/flexsat.go index 619c960..afbdac3 100644 --- a/internal/cat/flexsat.go +++ b/internal/cat/flexsat.go @@ -261,6 +261,21 @@ func (f *Flex) SatReceiveHz() (int64, error) { return s.freqHz, nil } +// SatTransmitHz is where the uplink slice sits. From the cache, like the +// downlink: SmartSDR pushes every slice change as it happens. +func (f *Flex) SatTransmitHz() (int64, error) { + f.mu.Lock() + defer f.mu.Unlock() + if f.satTX < 0 { + return 0, nil // no uplink slice: nothing to report, not an error + } + s := f.slices[f.satTX] + if s == nil || !s.inUse { + return 0, nil + } + return s.freqHz, nil +} + // SatAntennas selects both antennas of each satellite slice. // // The two slices are on two different bands — a V/U bird receives on 70 cm and diff --git a/internal/cat/icomsat.go b/internal/cat/icomsat.go index d0341de..7160ce0 100644 --- a/internal/cat/icomsat.go +++ b/internal/cat/icomsat.go @@ -142,6 +142,33 @@ func (b *IcomSerial) SatReceiveHz() (int64, error) { return b.readFreq() } +// SatTransmitHz is where the transmitter is now. +// +// On a rig with native satellite mode the uplink is the SUB band, so this is +// the same dance TuneSatellite does to write it: select SUB, read, and go +// back to MAIN whatever happens. Leaving the rig on SUB would have every +// band-dependent thing in OpsLog — the log, the antenna, the amplifier — +// follow the transmitter onto the wrong band. +// +// Not attempted while transmitting: the operator is not turning the knob +// mid-over, and switching bands under a carrier is not something to do to +// somebody else's radio. +func (b *IcomSerial) SatTransmitHz() (int64, error) { + if !b.satNative { + // Split on one band. The rig reports one frequency and it is the + // receiver's; there is nothing to read. + return 0, nil + } + if err := b.exec(civ.CmdVFO, civ.SubVFOSub); err != nil { + return 0, fmt.Errorf("icom: could not select the sub band: %w", err) + } + hz, err := b.readFreq() + if merr := b.exec(civ.CmdVFO, civ.SubVFOMain); merr != nil { + applog.Printf("icom: could not return to the main band: %v", merr) + } + return hz, err +} + // tuneSatSingleBand is every other Icom: one receiver, one band. // // The downlink is set, because that is what the operator is listening to. The