Reported from a real shack: JTDX decoding FT4 came through as Q65. A Decode carries a one-character marker rather than a mode name, and the forks do not agree on that one — so the character alone cannot answer, and its wrong answer reached everything behind it: new mode, new slot, the mode filter, all computed against a mode nobody was using. The sending program's own Status settles it. It comes from that program, names the mode in full, and is re-sent whenever it changes, so it knows what is being decoded in a way one character never can. Only the ambiguous marker consults it: the ones both forks agree on keep answering from the table, Status or no Status, and with no Status at all ':' still reads as Q65 — WSJT-X's meaning, the older and commoner.
140 lines
5.2 KiB
Go
140 lines
5.2 KiB
Go
package udp
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"net"
|
|
"testing"
|
|
)
|
|
|
|
// A Decode does not carry the mode's NAME. It carries the one-character marker
|
|
// from the decode line — "~" for FT8, "+" for FT4 — and that character used to
|
|
// be passed on as if it were a mode. Everything downstream compared it against
|
|
// the modes in the log, matched nothing, and reported every station on an
|
|
// already-worked band as a NEW MODE.
|
|
func TestDecodeModeNameResolvesTheMarker(t *testing.T) {
|
|
for raw, want := range map[string]string{
|
|
"~": "FT8",
|
|
"+": "FT4",
|
|
"#": "JT65",
|
|
"@": "JT9",
|
|
} {
|
|
if got := DecodeModeName(raw, "FT8"); got != want {
|
|
t.Errorf("DecodeModeName(%q) = %q, want %q", raw, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A sender that puts the real name in the field is believed as-is — several do,
|
|
// and the marker table must not get in their way.
|
|
func TestDecodeModeNameKeepsARealName(t *testing.T) {
|
|
for _, raw := range []string{"FT8", "ft4", "JS8", "Q65"} {
|
|
if got := DecodeModeName(raw, ""); got == "" || got != upper(raw) {
|
|
t.Errorf("DecodeModeName(%q) = %q, want the name itself", raw, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The safety net: an unknown marker falls back to the mode from the sender's
|
|
// last Status, which always carries the real name. This is what keeps a future
|
|
// or unlisted marker degrading to correct rather than to nonsense.
|
|
func TestDecodeModeNameFallsBackToStatus(t *testing.T) {
|
|
if got := DecodeModeName("%", "FT4"); got != "FT4" {
|
|
t.Errorf("unknown marker resolved to %q, want the Status mode FT4", got)
|
|
}
|
|
if got := DecodeModeName("", "FT8"); got != "FT8" {
|
|
t.Errorf("empty mode resolved to %q, want the Status mode FT8", got)
|
|
}
|
|
// Nothing known at all is empty rather than a guess: an empty mode makes the
|
|
// status resolver answer "worked", which is the safe side — a wrong mode
|
|
// would invent a new-mode flag exactly as the marker did.
|
|
if got := DecodeModeName("%", ""); got != "" {
|
|
t.Errorf("with no Status mode the result was %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func upper(s string) string {
|
|
out := []rune(s)
|
|
for i, r := range out {
|
|
if r >= 'a' && r <= 'z' {
|
|
out[i] = r - 32
|
|
}
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
// Resolving the marker must not DESTROY it. A Reply is matched by the receiving
|
|
// application against its own decode list field for field, and the mode field it
|
|
// compares is the one it sent — the marker. Sending the resolved name instead is
|
|
// accepted silently and simply never transmits: JTDX finds no matching decode,
|
|
// and no error is reported anywhere to say so. Reported as "clicking a CQ does
|
|
// nothing in JTDX".
|
|
//
|
|
// So a Decode has to carry BOTH: Mode for the log and the status resolver,
|
|
// DecodeModeRaw for the Reply.
|
|
func TestDecodeKeepsTheRawModeMarkerForReplies(t *testing.T) {
|
|
qstr := func(b *bytes.Buffer, v string) {
|
|
binary.Write(b, binary.BigEndian, int32(len(v)))
|
|
b.WriteString(v)
|
|
}
|
|
var p bytes.Buffer
|
|
binary.Write(&p, binary.BigEndian, uint32(wsjtMagic))
|
|
binary.Write(&p, binary.BigEndian, uint32(2)) // schema
|
|
binary.Write(&p, binary.BigEndian, uint32(wsjtMsgDecode)) // type 2
|
|
qstr(&p, "JTDX") // id
|
|
binary.Write(&p, binary.BigEndian, uint8(1)) // is_new
|
|
binary.Write(&p, binary.BigEndian, uint32(45_000_000)) // time (ms since midnight)
|
|
binary.Write(&p, binary.BigEndian, int32(-6)) // snr
|
|
binary.Write(&p, binary.BigEndian, float64(0.2)) // delta_time
|
|
binary.Write(&p, binary.BigEndian, uint32(1500)) // delta_frequency
|
|
qstr(&p, "~") // mode — the MARKER
|
|
qstr(&p, "CQ EN35UKR") // message
|
|
|
|
out := make(chan Event, 4)
|
|
s := &Server{
|
|
out: out,
|
|
cfg: Config{Name: "JTDX", ServiceType: ServiceWSJT},
|
|
lastMode: map[string]string{"JTDX": "FT8"},
|
|
dialHz: map[string]int64{"JTDX": 14074000},
|
|
}
|
|
s.handle(p.Bytes(), &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 2237})
|
|
|
|
var ev Event
|
|
select {
|
|
case ev = <-out:
|
|
default:
|
|
t.Fatal("no event emitted for the decode")
|
|
}
|
|
if ev.Mode != "FT8" {
|
|
t.Errorf("resolved Mode = %q, want FT8 (what the log and the status resolver need)", ev.Mode)
|
|
}
|
|
if ev.DecodeModeRaw != "~" {
|
|
t.Errorf("DecodeModeRaw = %q, want %q — a Reply built from this will not match and JTDX will not transmit",
|
|
ev.DecodeModeRaw, "~")
|
|
}
|
|
}
|
|
|
|
// Reported from a real shack: JTDX decoding FT4 showed up as Q65.
|
|
//
|
|
// The two forks disagree about ":" — Q65 in WSJT-X, FT4 in JTDX — so the
|
|
// character alone cannot answer, and the wrong answer poisons every verdict
|
|
// that follows: new mode, new slot, the mode filter. The program's own Status
|
|
// names the mode in full and settles it.
|
|
func TestAmbiguousModeCharDefersToTheProgram(t *testing.T) {
|
|
cases := []struct {
|
|
raw, status, want, why string
|
|
}{
|
|
{":", "FT4", "FT4", "JTDX decoding FT4"},
|
|
{":", "Q65", "Q65", "WSJT-X decoding Q65"},
|
|
{":", "", "Q65", "no Status yet — WSJT-X's reading, the older and commoner"},
|
|
// The unambiguous markers are unaffected, Status or no Status.
|
|
{"~", "FT4", "FT8", "a tilde is FT8 in both"},
|
|
{"+", "", "FT4", "a plus is FT4 in both"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := DecodeModeName(c.raw, c.status); got != c.want {
|
|
t.Errorf("DecodeModeName(%q, %q) = %q, want %q — %s", c.raw, c.status, got, c.want, c.why)
|
|
}
|
|
}
|
|
}
|