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 }