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, "~") } }