fix(udp): send the structured QSO Logged too, not only the ADIF one

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.
This commit is contained in:
2026-08-15 00:55:24 +02:00
parent d95c998081
commit dc14990b44
5 changed files with 252 additions and 11 deletions
+98
View File
@@ -3,6 +3,7 @@ package udp
import (
"bytes"
"encoding/binary"
"time"
)
// Sending WSJT-X UDP messages, as opposed to parsing them (wsjt.go).
@@ -48,3 +49,100 @@ func BuildWSJTLoggedADIF(id, adif string) []byte {
b.Write(buildQString(adif))
return b.Bytes()
}
// LoggedQSO is what a WSJT-X "QSO Logged" message carries. Declared here rather
// than taking a qso.QSO so this package stays free of the logbook — it speaks a
// wire protocol and nothing else.
type LoggedQSO struct {
DXCall string
DXGrid string
TxFreqHz uint64
Mode string
ReportSent string
ReportRcvd string
TxPower string
Comments string
Name string
TimeOn time.Time // UTC
TimeOff time.Time // UTC
OperatorCall string
MyCall string
MyGrid string
ExchangeSent string
ExchangeRcvd string
PropMode string
}
// BuildWSJTQSOLogged frames a WSJT-X "QSO Logged" datagram (message type 5).
//
// This is the message most loggers listen for — MacLoggerDX takes it by default
// and offers the ADIF one as an alternative, and WSJT-X itself sends BOTH for
// every contact. Sending only the ADIF form was the reason a QSO reached
// Logger32's socket and never reached its log.
//
// Field order is fixed by WSJT-X's NetworkMessage.hpp and cannot be rearranged:
// a receiver reads them positionally, so one field out of place shifts every
// one after it.
func BuildWSJTQSOLogged(id string, q LoggedQSO) []byte {
var b bytes.Buffer
var hdr [12]byte
binary.BigEndian.PutUint32(hdr[0:4], wsjtMagic)
binary.BigEndian.PutUint32(hdr[4:8], 2)
binary.BigEndian.PutUint32(hdr[8:12], wsjtMsgQSOLogged)
b.Write(hdr[:])
b.Write(buildQString(id))
b.Write(buildQDateTimeUTC(q.TimeOff))
b.Write(buildQString(q.DXCall))
b.Write(buildQString(q.DXGrid))
var f [8]byte
binary.BigEndian.PutUint64(f[:], q.TxFreqHz)
b.Write(f[:])
b.Write(buildQString(q.Mode))
b.Write(buildQString(q.ReportSent))
b.Write(buildQString(q.ReportRcvd))
b.Write(buildQString(q.TxPower))
b.Write(buildQString(q.Comments))
b.Write(buildQString(q.Name))
b.Write(buildQDateTimeUTC(q.TimeOn))
b.Write(buildQString(q.OperatorCall))
b.Write(buildQString(q.MyCall))
b.Write(buildQString(q.MyGrid))
b.Write(buildQString(q.ExchangeSent))
b.Write(buildQString(q.ExchangeRcvd))
b.Write(buildQString(q.PropMode))
return b.Bytes()
}
// buildQDateTimeUTC encodes a QDateTime the way QDataStream has since Qt 5:
//
// qint64 Julian day number
// quint32 milliseconds since midnight
// quint8 time spec — 1 is UTC, which is the only one we ever send
//
// Everything in the logbook is already UTC, and a receiver that guessed local
// time from a spec of 0 would file the contact in the wrong hour.
func buildQDateTimeUTC(t time.Time) []byte {
t = t.UTC()
out := make([]byte, 0, 13)
var jd [8]byte
binary.BigEndian.PutUint64(jd[:], uint64(julianDay(t)))
out = append(out, jd[:]...)
ms := uint32(t.Hour()*3600000 + t.Minute()*60000 + t.Second()*1000 + t.Nanosecond()/1e6)
var m [4]byte
binary.BigEndian.PutUint32(m[:], ms)
out = append(out, m[:]...)
return append(out, 1) // Qt::UTC
}
// julianDay is the standard Gregorian-to-JDN conversion. Integer division
// throughout — this is the arithmetic Qt uses, and a floating-point version
// lands a day out at the edges.
func julianDay(t time.Time) int64 {
y := int64(t.Year())
m := int64(t.Month())
d := int64(t.Day())
a := (14 - m) / 12
y2 := y + 4800 - a
m2 := m + 12*a - 3
return d + (153*m2+2)/5 + 365*y2 + y2/4 - y2/100 + y2/400 - 32045
}