chore: release v0.26.2

This commit is contained in:
2026-08-21 18:21:33 +02:00
parent c49faf9a14
commit 0cd243fe00
28 changed files with 1600 additions and 345 deletions
@@ -0,0 +1,42 @@
package udp
import "testing"
// A compound or non-standard callsign does not fit in a 77-bit FT8 message, so
// it travels as a hash and is printed between angle brackets. Those decodes
// used to be discarded whole — including the one message an operator is waiting
// for, the reply that says a station answered them.
func TestWsjtSenderHashedCallsign(t *testing.T) {
for _, c := range []struct {
msg, call string
cq bool
grid string
}{
// The reply that went missing: the sender is the bracketed call.
{"F4BPO <ZA/IW2JOP> -24", "ZA/IW2JOP", false, ""},
{"F4BPO <ZA/IW2JOP> R-14", "ZA/IW2JOP", false, ""},
// The other way round: we are the hashed one, the sender is plain.
{"<ZA/IW2JOP> F4BPO R-14", "F4BPO", false, ""},
// A CQ from a hashed call, with and without a grid.
{"CQ <ZA/IW2JOP> KM09", "ZA/IW2JOP", true, "KM09"},
{"CQ <ZA/IW2JOP>", "ZA/IW2JOP", true, ""},
{"CQ DX <ZA/IW2JOP> KM09", "ZA/IW2JOP", true, "KM09"},
// The compound form: one station signs off with another while answering a
// third. The sender is after the semicolon — reading from the left names
// OK1UA, who did not send it.
{"OK1UA RR73; DH1HRN <ZA/IW2JOP> -08", "ZA/IW2JOP", false, ""},
{"K1ABC RR73; W9XYZ <KH1/KH7Z> -08", "KH1/KH7Z", false, ""},
// Unchanged: the ordinary forms.
{"F4BPO W6YGO R-03", "W6YGO", false, ""},
{"CQ N2BJ EN61", "N2BJ", true, "EN61"},
// A hash the receiver could not resolve is NOT a callsign, and must not
// become one.
{"F4BPO <...> -24", "", false, ""},
} {
call, cq, grid := wsjtSender(c.msg)
if call != c.call || cq != c.cq || grid != c.grid {
t.Errorf("wsjtSender(%q) = (%q, %v, %q), want (%q, %v, %q)",
c.msg, call, cq, grid, c.call, c.cq, c.grid)
}
}
}
+30 -7
View File
@@ -372,8 +372,29 @@ func ParseWSJT(pkt []byte) (WSJTEvent, bool, error) {
// third slot, never a locator.
//
// Returns "" for free-text / telemetry / hashed-call messages we can't resolve.
// unbracket strips the angle brackets FT8 puts around a HASHED callsign.
//
// A compound or non-standard call does not fit in a 77-bit message, so the
// protocol sends a hash of it and the receiving software prints what it
// resolved between angle brackets: "F4BPO <ZA/IW2JOP> -24". The brackets are
// notation, not part of the callsign — but every character test here rejected
// them, so the whole decode was thrown away as unparseable. Every reply from a
// station with a compound call was invisible: precisely the stations worth
// seeing, and precisely the message that says one of them answered you.
func unbracket(s string) string {
return strings.TrimSuffix(strings.TrimPrefix(s, "<"), ">")
}
func wsjtSender(message string) (call string, isCQ bool, grid string) {
f := strings.Fields(strings.ToUpper(strings.TrimSpace(message)))
msg := strings.ToUpper(strings.TrimSpace(message))
// The compound form — "OK1UA RR73; DH1HRN <ZA/IW2JOP> -08" — is one station
// signing off with another while answering a third. The sender is in the
// part AFTER the semicolon; reading the line from the left names OK1UA, who
// did not send it.
if i := strings.LastIndexByte(msg, ';'); i >= 0 {
msg = strings.TrimSpace(msg[i+1:])
}
f := strings.Fields(msg)
if len(f) == 0 {
return "", false, ""
}
@@ -381,11 +402,11 @@ func wsjtSender(message string) (call string, isCQ bool, grid string) {
// Skip an optional modifier after CQ (DX / a region like NA / a zone like
// 020) — it never looks like a callsign (no letter+digit mix).
idx := 1
if len(f) > 2 && !looksLikeCall(f[1]) {
if len(f) > 2 && !looksLikeCall(unbracket(f[1])) {
idx = 2
}
if idx < len(f) {
c := f[idx]
c := unbracket(f[idx])
// A GRID sitting in the callsign slot means the real call was
// unparseable and the skip above went one word too far. JN36 has letters
// and digits and passes every shape test there is, so without this the
@@ -411,11 +432,13 @@ func wsjtSender(message string) (call string, isCQ bool, grid string) {
//
// isGridField is what keeps the reports and sign-offs out: -05, R-05, RRR,
// 73 and RR73 all fail it, on length, charset or by name.
if len(f) >= 2 && looksLikeCall(f[1]) {
if len(f) >= 3 && isGridField(f[2]) {
return f[1], false, f[2]
if len(f) >= 2 {
if c := unbracket(f[1]); looksLikeCall(c) {
if len(f) >= 3 && isGridField(f[2]) {
return c, false, f[2]
}
return c, false, ""
}
return f[1], false, ""
}
return "", false, ""
}