package udp import ( "bytes" "encoding/binary" "testing" "time" ) // What we send has to be what we can read back: Logger32 and the rest decode // this with the same rules our own parser uses, so a round trip through // ParseWSJT is the honest check. // // The case behind it: OpsLog reported "sent 1153 bytes to 127.0.0.1:2250" and // Logger32 showed nothing. Both were true — its UDP sockets speak the WSJT-X // protocol and discard a bare ADIF record without a word. func TestWSJTLoggedADIFRoundTrips(t *testing.T) { const rec = "VK9XX20mFT820260814174433" pkt := BuildWSJTLoggedADIF("OpsLog", rec) ev, ok, err := ParseWSJT(pkt) if err != nil { t.Fatalf("ParseWSJT: %v", err) } if !ok { t.Fatal("our own parser did not recognise the datagram we build") } if ev.LoggedADIF != rec { t.Errorf("ADIF = %q, want %q", ev.LoggedADIF, rec) } if ev.ProgramID != "OpsLog" { t.Errorf("id = %q, want OpsLog — a receiver uses it to tell instances apart", ev.ProgramID) } } // The header is fixed and other programs match on it byte for byte. func TestWSJTLoggedADIFHeader(t *testing.T) { pkt := BuildWSJTLoggedADIF("OpsLog", "") want := []byte{ 0xad, 0xbc, 0xcb, 0xda, // magic 0x00, 0x00, 0x00, 0x02, // schema 2 0x00, 0x00, 0x00, 0x0c, // type 12 — Logged ADIF 0x00, 0x00, 0x00, 0x06, // len("OpsLog") 'O', 'p', 's', 'L', 'o', 'g', 0x00, 0x00, 0x00, 0x05, // len("") '<', 'E', 'O', 'R', '>', } if !bytes.Equal(pkt, want) { t.Errorf("packet = % X\nwant % X", pkt, want) } } // The julian day has to match Qt's, because the receiver turns it back into a // date. A day out here files every contact on the wrong day. func TestJulianDayMatchesKnownValues(t *testing.T) { for _, c := range []struct { date time.Time want int64 }{ // Checked against the standard tables: JDN is the count at noon UTC. {time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), 2451545}, {time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), 2440588}, {time.Date(2026, 8, 15, 0, 0, 0, 0, time.UTC), 2461268}, } { if got := julianDay(c.date); got != c.want { t.Errorf("julianDay(%s) = %d, want %d", c.date.Format("2006-01-02"), got, c.want) } } } // QDateTime is julian day, then milliseconds since midnight, then the time // spec. A receiver reads them positionally, and a spec of 0 (local time) would // have it re-interpret a UTC contact in its own zone. func TestQDateTimeIsUTCAndPositional(t *testing.T) { got := buildQDateTimeUTC(time.Date(2026, 8, 15, 17, 44, 33, 0, time.UTC)) if len(got) != 13 { t.Fatalf("%d bytes, want 13", len(got)) } if jd := int64(binary.BigEndian.Uint64(got[0:8])); jd != 2461268 { t.Errorf("julian day = %d, want 2461268", jd) } wantMs := uint32((17*3600 + 44*60 + 33) * 1000) if ms := binary.BigEndian.Uint32(got[8:12]); ms != wantMs { t.Errorf("ms since midnight = %d, want %d", ms, wantMs) } if got[12] != 1 { t.Errorf("time spec = %d, want 1 (UTC)", got[12]) } } // The QSO Logged message is read positionally: one field out of place shifts // every one after it, and the receiver silently files nonsense. This pins the // order against WSJT-X's NetworkMessage.hpp. func TestWSJTQSOLoggedFieldOrder(t *testing.T) { pkt := BuildWSJTQSOLogged("OpsLog", LoggedQSO{ DXCall: "VK9XX", DXGrid: "QH30", TxFreqHz: 14074000, Mode: "FT8", ReportSent: "-12", ReportRcvd: "-08", TxPower: "50", Comments: "tnx", Name: "Ken", TimeOn: time.Date(2026, 8, 15, 17, 44, 0, 0, time.UTC), TimeOff: time.Date(2026, 8, 15, 17, 45, 0, 0, time.UTC), MyCall: "F4BPO", MyGrid: "IN95", OperatorCall: "F4BPO", PropMode: "", }) r := bytes.NewReader(pkt) var magic, schema, mtype uint32 binary.Read(r, binary.BigEndian, &magic) binary.Read(r, binary.BigEndian, &schema) binary.Read(r, binary.BigEndian, &mtype) if magic != wsjtMagic || schema != 2 || mtype != wsjtMsgQSOLogged { t.Fatalf("header magic=%08x schema=%d type=%d", magic, schema, mtype) } id, _ := readQString(r) if id != "OpsLog" { t.Errorf("id = %q", id) } r.Seek(13, 1) // QSO off call, _ := readQString(r) grid, _ := readQString(r) var freq uint64 binary.Read(r, binary.BigEndian, &freq) mode, _ := readQString(r) sent, _ := readQString(r) rcvd, _ := readQString(r) if call != "VK9XX" || grid != "QH30" || freq != 14074000 || mode != "FT8" || sent != "-12" || rcvd != "-08" { t.Errorf("got call=%q grid=%q freq=%d mode=%q sent=%q rcvd=%q", call, grid, freq, mode, sent, rcvd) } pwr, _ := readQString(r) comments, _ := readQString(r) name, _ := readQString(r) if pwr != "50" || comments != "tnx" || name != "Ken" { t.Errorf("got pwr=%q comments=%q name=%q", pwr, comments, name) } r.Seek(13, 1) // QSO on op, _ := readQString(r) myCall, _ := readQString(r) myGrid, _ := readQString(r) if op != "F4BPO" || myCall != "F4BPO" || myGrid != "IN95" { t.Errorf("got op=%q myCall=%q myGrid=%q", op, myCall, myGrid) } }