Reported as JTDX "Fake It" causing CAT disconnections. Fake It is not the
cause — it is what made an existing loop fire, and a regression of mine from
this morning is what closed that loop.
The station has an inbound remote-call row and an outbound N1MM RadioInfo row
on the same port, 2241, so every RadioInfo datagram OpsLog sends arrives
straight back on the loopback. The remote-call parser strips XML tags and takes
the last token as the callsign: for a RadioInfo that is
<ActiveRadioNr>1</ActiveRadioNr>, i.e. "1" — and <Freq> became a tune request
for the frequency the rig was already on.
That was harmless only while <Freq> was misread. It is in tens of Hz, the parser
assumed MHz, every echoed tune failed "out of the 11-digit CAT range", and the
loop died on the error. Teaching it the unit (331db58) closed the loop.
Fake It shifts the dial for each over rather than using split, so every
transmission changed the state twice — and each change published a RadioInfo,
which came back as a set, which changed the state again. The log shows the dial
oscillating 24915000/24915500 three times in 200 ms, then "timeout answering
IF;" and the shared link down. Every over, all morning.
Two guards, because one was not enough to be sure: a RadioInfo payload is never
a remote-call request, and a callsign has a letter in it — so the next program
to broadcast its state on that port cannot drive the rig either. Reload also
names an inbound and an outbound row sharing a port, which is the arrangement
that permitted this and which no settings panel shows.
75 lines
2.8 KiB
Go
75 lines
2.8 KiB
Go
package udp
|
|
|
|
import (
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// feed runs one datagram through a remote-call listener and returns the event
|
|
// it produced, or nil.
|
|
func feed(t *testing.T, pkt []byte) *Event {
|
|
t.Helper()
|
|
out := make(chan Event, 4)
|
|
s := &Server{
|
|
cfg: Config{ID: 1, Name: "DX HUNTER", ServiceType: ServiceRemoteCall, Port: 2241},
|
|
out: out,
|
|
}
|
|
s.handle(pkt, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 2241})
|
|
select {
|
|
case ev := <-out:
|
|
return &ev
|
|
case <-time.After(200 * time.Millisecond):
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// A RadioInfo datagram must never be read as a remote-call request.
|
|
//
|
|
// This is the loop from a reported session. An inbound remote-call row and an
|
|
// outbound RadioInfo row shared port 2241, so every datagram OpsLog sent came
|
|
// straight back on the loopback. The tag-stripping heuristic read its last
|
|
// token — <ActiveRadioNr>1</ActiveRadioNr> — as the callsign "1", and <Freq> as
|
|
// a tune request for the frequency the rig was already on.
|
|
//
|
|
// It stayed harmless only while <Freq> was misread as MHz: the tune failed "out
|
|
// of the CAT range" and the loop died there. Reading the unit correctly closed
|
|
// it, and with JTDX "Fake It" — which shifts the dial for every over — each
|
|
// transmission set off a burst of sets echoing between OpsLog and itself until
|
|
// the rig stopped answering IF; and the shared CAT link dropped.
|
|
func TestRadioInfoIsNotARemoteCall(t *testing.T) {
|
|
pkt := BuildN1MMRadioInfo("F5PHW", 24_915_000, 24_915_000, "FT8", "F5PHW")
|
|
|
|
// The trap this closes: the payload really does parse as a plausible dial
|
|
// frequency, so nothing downstream would have questioned it.
|
|
m := remoteFreqRe.FindStringSubmatch(string(pkt))
|
|
if m == nil {
|
|
t.Fatal("the RadioInfo no longer carries a <Freq> — this test is checking nothing")
|
|
}
|
|
if hz := remoteTuneHz(m[1]); hz != 24_915_000 {
|
|
t.Fatalf("remoteTuneHz(%q) = %d — the dial frequency back is what made the loop live", m[1], hz)
|
|
}
|
|
|
|
if ev := feed(t, pkt); ev != nil {
|
|
t.Errorf("a RadioInfo produced a remote-call event %+v — the rig would be re-tuned to where it already is", *ev)
|
|
}
|
|
}
|
|
|
|
// A callsign has a letter in it. Refusing by shape as well as by name means the
|
|
// next program to broadcast its state on this port cannot drive the rig either.
|
|
func TestRemoteCallNeedsALetter(t *testing.T) {
|
|
for _, body := range []string{"<CALLSIGN>1</CALLSIGN>", "<CALLSIGN>0</CALLSIGN>", "12345"} {
|
|
if ev := feed(t, []byte(body)); ev != nil {
|
|
t.Errorf("%q was accepted as a callsign: %+v", body, *ev)
|
|
}
|
|
}
|
|
// A genuine spot click still gets through, tune request and all.
|
|
ev := feed(t, []byte("<CALLSIGN>OJ0YL<FREQ>10.112<MODE>CW"))
|
|
if ev == nil {
|
|
t.Fatal("a genuine spot click produced no event")
|
|
}
|
|
if ev.DXCall != "OJ0YL" || ev.TuneFreqHz != 10_112_000 || ev.TuneMode != "CW" {
|
|
t.Errorf("spot click decoded as %+v, want OJ0YL / 10112000 Hz / CW", *ev)
|
|
}
|
|
}
|