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:
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hamlog/internal/sat"
|
||||
)
|
||||
|
||||
// The dial arithmetic has to be the exact inverse of the correction, or every
|
||||
// touch of the knob would nudge the nominal frequency a little further off and
|
||||
// the uplink would walk across the passband over a pass.
|
||||
func TestSatNominalFromDialRoundTrip(t *testing.T) {
|
||||
// A range of range rates: hard approach, drifting, hard recession. ±8 km/s
|
||||
// covers a low orbit overhead.
|
||||
for _, rate := range []float64{-8, -3.2, -0.4, 0, 0.4, 3.2, 8} {
|
||||
p := sat.Position{RangeRate: rate}
|
||||
for _, nominal := range []int64{29_450_000, 145_900_000, 435_850_000, 10_489_675_000} {
|
||||
sh := sat.Doppler(p, nominal, 0)
|
||||
factor := -rate / satLightKmS
|
||||
got := satNominalFromDial(sh.DownHz, factor)
|
||||
if diff := got - nominal; diff > 1 || diff < -1 {
|
||||
t.Errorf("rate %.1f km/s, %d Hz: heard %d, came back as %d (%+d)",
|
||||
rate, nominal, sh.DownHz, got, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SAT_MODE is what goes on a QSL card, and the letters are the uplink's then
|
||||
// the downlink's — the order operators write and the order ADIF wants.
|
||||
func TestSatModeLetters(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
up, down int64
|
||||
want string
|
||||
}{
|
||||
{"FO-29: 2 m up, 70 cm down", 145_950_000, 435_850_000, "V/U"},
|
||||
{"AO-91: 70 cm up, 2 m down", 435_250_000, 145_960_000, "U/V"},
|
||||
{"AO-7 mode A: 2 m up, 10 m down", 145_900_000, 29_450_000, "V/A"},
|
||||
{"QO-100: 13 cm up, 3 cm down", 2_400_175_000, 10_489_675_000, "S/X"},
|
||||
{"receive only", 0, 145_800_000, ""},
|
||||
} {
|
||||
if got := satModeLetters(tc.up, tc.down); got != tc.want {
|
||||
t.Errorf("%s: got %q, wanted %q", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user