The ADIF datagram reached Logger32's socket and still nothing appeared in its log. WSJT-X sends TWO messages for every contact — QSO Logged (type 5), the structured one, and Logged ADIF (type 12) — and receivers differ on which they read: MacLoggerDX takes the structured one by default and offers the ADIF as an option. We were sending only the half Logger32 does not use. Both now go out on that row, in WSJT-X's own order. Nothing double-logs: any receiver built for WSJT-X already sees both from the real thing. Type 5 is read positionally, so the field order is pinned by a test against NetworkMessage.hpp — one field out of place shifts every one after it and the receiver files nonsense without complaining. QDateTime is Qt's own encoding: Julian day, milliseconds since midnight, then the time spec, which is sent as UTC (1) because a spec of 0 would have the receiver re-read the contact in its own zone. The Julian arithmetic is checked against known values; a day out there files every QSO on the wrong date.
140 lines
4.8 KiB
Go
140 lines
4.8 KiB
Go
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 = "<CALL:5>VK9XX<BAND:3>20m<MODE:3>FT8<QSO_DATE:8>20260814<TIME_ON:6>174433<EOR>"
|
|
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", "<EOR>")
|
|
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("<EOR>")
|
|
'<', '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)
|
|
}
|
|
}
|