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.
149 lines
5.1 KiB
Go
149 lines
5.1 KiB
Go
package udp
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"time"
|
|
)
|
|
|
|
// Sending WSJT-X UDP messages, as opposed to parsing them (wsjt.go).
|
|
//
|
|
// This exists for Logger32, and for anything else that listens on the WSJT-X
|
|
// interface rather than for plain text. Logger32's "additional WSJT/JTDX UDP
|
|
// sockets" receivers — ports 2250, 2251, 2252 — are documented as receiving
|
|
// "UDP logging packets": they speak the WSJT-X v2 protocol, and a raw ADIF
|
|
// record posted to them is discarded without a word.
|
|
//
|
|
// That is a real distinction and not a detail: an operator can watch OpsLog
|
|
// report "sent 1153 bytes to 127.0.0.1:2250" and see nothing whatsoever appear
|
|
// in Logger32, because both statements are true.
|
|
|
|
// buildQString encodes a Qt QString/QByteArray as QDataStream writes it: a
|
|
// big-endian int32 length followed by the UTF-8 bytes. A negative length means
|
|
// null, which is not what we ever want here — an empty string is length 0.
|
|
func buildQString(s string) []byte {
|
|
out := make([]byte, 4, 4+len(s))
|
|
binary.BigEndian.PutUint32(out, uint32(len(s)))
|
|
return append(out, s...)
|
|
}
|
|
|
|
// BuildWSJTLoggedADIF frames a WSJT-X "Logged ADIF" datagram (message type 12).
|
|
//
|
|
// uint32 magic 0xadbccbda
|
|
// uint32 schema 2
|
|
// uint32 type 12
|
|
// QString id the sending program's name
|
|
// QString adif the ADIF record
|
|
//
|
|
// id matters more than it looks: a receiver uses it to tell instances apart, and
|
|
// some show it in their log. "OpsLog" is honest — pretending to be WSJT-X would
|
|
// make a second instance of the real thing indistinguishable from us.
|
|
func BuildWSJTLoggedADIF(id, adif string) []byte {
|
|
var b bytes.Buffer
|
|
var hdr [12]byte
|
|
binary.BigEndian.PutUint32(hdr[0:4], wsjtMagic)
|
|
binary.BigEndian.PutUint32(hdr[4:8], 2) // schema 2 — what WSJT-X 2.x speaks
|
|
binary.BigEndian.PutUint32(hdr[8:12], wsjtMsgLoggedADIF)
|
|
b.Write(hdr[:])
|
|
b.Write(buildQString(id))
|
|
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
|
|
}
|