feat(sat): Doppler tracking on the radio
The hard part of satellite tuning is not the arithmetic, it is deciding who owns the dial. A tracker that forces both frequencies fights the operator every time they turn the knob to follow a station across a linear transponder; one that never touches the receiver leaves them chasing a signal that slides nine kilohertz across a 70 cm pass. So the operator owns the receiver and the tracker follows them. Every second it asks the radio where the receiver actually is. Where it put it, nothing has changed. Further than a dial-turn's tolerance, and the operator has chosen a station: what they landed on is converted back into a nominal frequency, and the transmitter is derived from that. Which is the division of labour on a linear bird — the operator listens, the radio does the sums. Three ways to reach the radio, because a satellite pair is a shape of operating rather than a manufacturer's feature. An IC-9700 or IC-9100 is asked for its OWN satellite mode: it pairs main and sub, gives full duplex, and keeps the dials linked the way its designers meant, which is always better than an imitation built out of split. A Flex gets two slices, A the downlink and B the uplink, created when missing, because "slice B does not exist" is not something to make an operator fix at the start of a ten-minute pass. Everything else gets the downlink, and is told so — half the job announced beats half the job hidden. What goes in the log is the NOMINAL pair. Two stations working each other through a transponder read different numbers off their dials at the same instant; the only figure they can both agree on is the transponder's own. FREQ is the uplink and FREQ_RX the downlink — the one place a satellite QSO differs from every other kind, and the reason FREQ alone cannot describe one.
This commit is contained in:
@@ -832,6 +832,50 @@ func (m *Manager) IcomDo(fn func(IcomController) error) error {
|
||||
})
|
||||
}
|
||||
|
||||
// SatTuner is a backend that can be put on a satellite: a receiver on one band
|
||||
// and a transmitter on another, both moving under Doppler, at the same time.
|
||||
//
|
||||
// It is a separate interface from the per-manufacturer ones because what a
|
||||
// satellite needs is not a manufacturer's feature — it is a shape of operating
|
||||
// that a FlexRadio and an IC-9700 both provide and reach in completely
|
||||
// different ways. A backend that cannot do it simply does not implement this,
|
||||
// and the caller falls back to tuning the downlink alone rather than pretending.
|
||||
type SatTuner interface {
|
||||
// SetSatellite arms or disarms satellite operation: the rig's own satellite
|
||||
// mode where it has one, two slices where it has those. Disarming must leave
|
||||
// the radio somewhere an operator can work from, not half-configured.
|
||||
SetSatellite(on bool) error
|
||||
// TuneSatellite points the receiver at downHz and the transmitter at upHz,
|
||||
// both already Doppler-corrected. Modes are ADIF names ("SSB", "FM", "CW");
|
||||
// an empty one leaves that side's mode alone.
|
||||
TuneSatellite(downHz, upHz int64, downMode, upMode string) error
|
||||
// SatReceiveHz is where the receiver actually is. The operator tunes it to
|
||||
// follow a station across a linear transponder, and that dial movement is
|
||||
// the input the whole tracker works from — without reading it back, a
|
||||
// tracker fights the operator instead of helping them.
|
||||
SatReceiveHz() (int64, error)
|
||||
}
|
||||
|
||||
// SatCapable reports whether the active backend can hold a satellite pair.
|
||||
func (m *Manager) SatCapable() bool {
|
||||
m.mu.RLock()
|
||||
b := m.backend
|
||||
m.mu.RUnlock()
|
||||
_, ok := b.(SatTuner)
|
||||
return ok
|
||||
}
|
||||
|
||||
// SatDo dispatches a satellite control onto the CAT goroutine.
|
||||
func (m *Manager) SatDo(fn func(SatTuner) error) error {
|
||||
return m.exec(func(b Backend) error {
|
||||
st, ok := b.(SatTuner)
|
||||
if !ok {
|
||||
return fmt.Errorf("this radio cannot hold a satellite pair from OpsLog")
|
||||
}
|
||||
return fn(st)
|
||||
})
|
||||
}
|
||||
|
||||
// exec marshals a backend operation onto the CAT goroutine. Returns the
|
||||
// operation's error or a "busy"/"not running" error if dispatch failed.
|
||||
func (m *Manager) exec(fn func(Backend) error) error {
|
||||
|
||||
Reference in New Issue
Block a user