Files
OpsLog/internal/integrations/udp/dxclear_test.go
T
rouggy 76022ff91c fix(udp): several FT8 programs no longer fight over the callsign field
Reported from a station running MSHV, WSJT-X and JTDX together: click a
call in MSHV and the entry field filled, emptied, refilled — once a
second — with the map zooming in and out to match.

Two causes, both about reading one program's statement as another's.

The clear was tracked per LISTENER. Several decoders commonly share one
multicast group, so an idle WSJT-X reporting no DX Call — which is simply
true, and which it repeats every second — was read as MSHV abandoning the
station it was calling. "The operator cleared the DX Call" is a statement
about one program, never about a socket, so it is now tracked per
program, and a clear carries the id of whoever made it.

And nothing arbitrated between them. The program that announces a station
now holds the entry field, and the others cannot touch it until it lets
go: it clears its own call, it stops sending (closed), or the QSO is
logged. That is the operator's own suggestion, and it is the right one —
between overs there is no way to tell "I have nothing" from "I am not the
one you are working" except by remembering who was.

Refusing another program's callsign is logged once per focus, not once a
second: an operator whose second decoder "stopped filling the call" needs
something to read.
2026-09-07 21:23:11 +02:00

55 lines
1.8 KiB
Go

package udp
import "testing"
// Three decoders on one multicast group, which is the ordinary setup. MSHV is
// working a station; WSJT-X and JTDX are idle and say so once a second each.
//
// Read across the listener rather than per program, every one of those idle
// Status packets was a "the operator cleared the DX Call" — so OpsLog emptied
// the entry field, MSHV's next Status refilled it, and the entry blinked and
// the map zoomed at 1 Hz for as long as all three were running.
func TestDXClearIsPerProgram(t *testing.T) {
s := &Server{}
if s.noteDXCall("MSHV", "F5NNN") {
t.Fatal("taking up a station is not a clear")
}
// The idle ones, interleaved, as they arrive on the wire.
for i := 0; i < 3; i++ {
if s.noteDXCall("WSJT-X", "") {
t.Fatal("an idle WSJT-X was read as MSHV clearing its call")
}
if s.noteDXCall("JTDX", "") {
t.Fatal("an idle JTDX was read as MSHV clearing its call")
}
if s.noteDXCall("MSHV", "F5NNN") {
t.Fatal("MSHV repeating the same station is not a clear")
}
}
// MSHV's own clear is still an edge, and only once: the Status that follows
// is just as empty and must not re-clear a field the operator may have
// typed into since.
if !s.noteDXCall("MSHV", "") {
t.Error("MSHV clearing its own DX Call was not reported")
}
if s.noteDXCall("MSHV", "") {
t.Error("the clear repeated on the next identical Status")
}
}
// Each program's edge is its own: WSJT-X letting go says nothing about MSHV.
func TestDXClearOfOneProgramLeavesTheOthers(t *testing.T) {
s := &Server{}
s.noteDXCall("MSHV", "F5NNN")
s.noteDXCall("WSJT-X", "DL1ABC")
if !s.noteDXCall("WSJT-X", "") {
t.Error("WSJT-X clearing its own call should be reported")
}
if s.noteDXCall("MSHV", "F5NNN") {
t.Error("MSHV's unchanged call was disturbed by WSJT-X's clear")
}
}