fix(antenna,udp): name the reason tracking is off; infer the remote-tune unit

The SteppIR report was not a SteppIR fault. In the log the antenna starts,
answers every poll, and sits at 21050 kHz while the rig works 21074 — and the
status line only prints when the frame CHANGES, so a link that is alive and
parked looks identical to one that died. The one line that explained it said
"ultrabeam: follow loop stopped", which covers two quite different situations
and, at startup where nothing was running, reads as a fault. Tracking was simply
switched off. It now says which of the two it is, names the antenna type, and
states the consequence — that the antenna will not follow the rig.

Same log, a second and unrelated fault: every launch failed a tune request from
DXHunter with "frequency 2107400000000 out of the 11-digit CAT range".
<FREQ> was read as MHz, but DXHunter had echoed back the value OpsLog itself
published in the N1MM RadioInfo broadcast, whose <Freq> is in tens of Hz. Both
units come from the same peer, so the unit is now inferred: try MHz, kHz, Hz,
tens of Hz, and keep the first that lands on an amateur band — anything a
station is asked to tune to is in one. Hz before tens of Hz because their only
overlap, 40 m against 4 m, is far more likely to be 40 m. Nothing plausible
tunes nothing and says so, rather than sending the rig to a wrong band.
This commit is contained in:
2026-08-16 00:27:53 +02:00
parent 62e20de69c
commit 331db58705
4 changed files with 84 additions and 8 deletions
@@ -0,0 +1,31 @@
package udp
import "testing"
// A remote-call <FREQ> arrives in whatever unit the sender happens to use, and
// the same sender uses more than one. Every form has to land on the same dial
// frequency, because the alternative is a rig sent to the wrong band.
func TestRemoteTuneUnits(t *testing.T) {
const m20 = 14_074_000
for _, c := range []struct {
in string
want int64
why string
}{
{"14.074", m20, "MHz, the documented DXHunter form"},
{"10.136", 10_136_000, "MHz, 30 m"},
{"14074", m20, "kHz"},
{"14074.0", m20, "kHz with a decimal point"},
{"1407400", m20, "tens of Hz — what N1MM RadioInfo publishes, echoed back"},
{"2107400", 21_074_000, "the value from the field log that failed every time"},
{"14074000", m20, "Hz"},
{"7000000", 7_000_000, "Hz on 40 m, not tens of Hz on 4 m"},
{"0", 0, "no frequency"},
{"999999999999", 0, "nothing plausible — tune nothing rather than guess"},
{"abc", 0, "not a number"},
} {
if got := remoteTuneHz(c.in); got != c.want {
t.Errorf("remoteTuneHz(%q) = %d, want %d (%s)", c.in, got, c.want, c.why)
}
}
}
+33 -3
View File
@@ -17,6 +17,38 @@ import (
"hamlog/internal/applog"
)
// remoteTuneHz turns a <FREQ> value from a remote-call packet into Hz.
//
// The field has NO agreed unit, and the same sender uses two of them. DXHunter
// documents "<FREQ>10.136" — MHz — but a log from a working station showed it
// echoing "<FREQ>2107400" straight back: the frequency OpsLog had just
// published to it in the N1MM RadioInfo broadcast, whose <Freq> is in units of
// 10 Hz. Read as MHz, that asked the rig for 2 107 400 MHz, and every tune
// request failed with "out of the 11-digit CAT range" from the first second
// after launch.
//
// So the unit is inferred: try each one and keep the first that lands on an
// amateur band. Anything a station is asked to tune to is, by definition, in
// one. Hz is tried before 10 Hz because the one overlap between them — a 40 m
// frequency in Hz reads as a 4 m one in tens of Hz — is far more likely to be
// 40 m. Nothing plausible means nothing is tuned: a wrong band is worse than a
// request that visibly did nothing.
func remoteTuneHz(s string) int64 {
v, err := strconv.ParseFloat(s, 64)
if err != nil || v <= 0 {
return 0
}
for _, hz := range []int64{int64(v * 1e6), int64(v * 1e3), int64(v), int64(v * 10)} {
if bandFromHz(hz) != "" {
return hz
}
}
// Refusing in silence is how the previous version's failure looked from the
// outside: a spot clicked in another program, and nothing happening here.
applog.Printf("udp: remote_call <FREQ>%s is not a frequency in any amateur band in MHz, kHz or Hz — not tuning\n", s)
return 0
}
// remoteFreqRe / remoteModeRe pull the optional tune request out of a
// ServiceRemoteCall packet: "<FREQ>10.136" (MHz) and "<MODE>FT8". Both accept
// an optional closing tag for proper-XML senders.
@@ -397,9 +429,7 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
// otherwise leave their values as stray tokens and corrupt the
// "last token = callsign" heuristic.
if m := remoteFreqRe.FindStringSubmatch(text); m != nil {
if mhz, err := strconv.ParseFloat(m[1], 64); err == nil && mhz > 0 {
ev.TuneFreqHz = int64(mhz * 1e6)
}
ev.TuneFreqHz = remoteTuneHz(m[1])
text = strings.Replace(text, m[0], " ", 1)
}
if m := remoteModeRe.FindStringSubmatch(text); m != nil {