feat(sat): the uplink keeps the correction the operator makes

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.
This commit is contained in:
2026-09-10 18:53:03 +02:00
parent d0d29659cb
commit 72696a5c0c
11 changed files with 320 additions and 7 deletions
+12
View File
@@ -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.
+15
View File
@@ -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
+27
View File
@@ -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