fix(udp): ignore N0CALL, and stop returning a grid as a callsign
N0CALL is what WSJT-X transmits under when its owner never set a callsign. It has a letter, a digit and an ordinary shape, so nothing rejected it: it was spotted, coloured, counted as a new WPX prefix, and now that CQ grids are read it would have put a grid into the worked index under a callsign nobody holds. The obvious fix - adding it to looksLikeCall's reject list - was wrong, and the test caught it before it shipped. That function answers "could this token be a callsign at all", and the CQ grammar uses it to decide whether the word after CQ is a modifier (DX, NA, a zone) or the call itself. Teaching it that N0CALL is not a callsign made "CQ N0CALL JN36" skip a slot and return JN36. Shape and policy are different questions and now live in different functions. Chasing that turned up the real defect behind it: ANY unrecognised word after CQ made the parser skip a slot, and a four-character grid passes every shape test a callsign does. "CQ FOO JN36" returned JN36 as the sender - logged, spotted and coloured as a station. A grid in the callsign slot is now refused outright.
This commit is contained in:
+6
-2
@@ -2,8 +2,12 @@
|
|||||||
{
|
{
|
||||||
"version": "0.24.5",
|
"version": "0.24.5",
|
||||||
"date": "",
|
"date": "",
|
||||||
"en": [],
|
"en": [
|
||||||
"fr": []
|
"N0CALL and NOCALL are ignored in the digital decodes. WSJT-X transmits under N0CALL when its owner never set a callsign, and it has a perfectly ordinary shape, so it was spotted, coloured, counted as a new prefix and could bring a grid into the worked index. Fixed at the same time: an unrecognised word after CQ made the parser skip a slot, and since a grid passes every shape test a callsign does, the station grid came back as its callsign."
|
||||||
|
],
|
||||||
|
"fr": [
|
||||||
|
"N0CALL et NOCALL sont ignorés dans les décodes digitaux. WSJT-X émet sous N0CALL quand son propriétaire n a jamais réglé son indicatif, et sa forme est parfaitement ordinaire — il était donc spotté, coloré, compté comme nouveau préfixe et pouvait faire entrer un grid dans l index des contacts. Corrigé au passage : un mot non reconnu après CQ faisait sauter un cran au parseur, et comme un grid passe tous les tests de forme d un indicatif, c est le grid de la station qui revenait comme son indicatif."
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"version": "0.24.4",
|
"version": "0.24.4",
|
||||||
|
|||||||
@@ -41,16 +41,16 @@ const (
|
|||||||
|
|
||||||
// Config is one user-defined UDP connection.
|
// Config is one user-defined UDP connection.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Direction Direction `json:"direction"`
|
Direction Direction `json:"direction"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
ServiceType ServiceType `json:"service_type"`
|
ServiceType ServiceType `json:"service_type"`
|
||||||
Multicast bool `json:"multicast"`
|
Multicast bool `json:"multicast"`
|
||||||
MulticastGroup string `json:"multicast_group"`
|
MulticastGroup string `json:"multicast_group"`
|
||||||
DestinationIP string `json:"destination_ip"` // outbound only
|
DestinationIP string `json:"destination_ip"` // outbound only
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repo is the persistence layer for UDP integration rows.
|
// Repo is the persistence layer for UDP integration rows.
|
||||||
|
|||||||
@@ -37,19 +37,19 @@ func TestParseN1MMContactInfo(t *testing.T) {
|
|||||||
t.Fatal("expected a loggable contact, got ok=false")
|
t.Fatal("expected a loggable contact, got ok=false")
|
||||||
}
|
}
|
||||||
want := map[string]string{
|
want := map[string]string{
|
||||||
"<call:5>VE9AA": "callsign",
|
"<call:5>VE9AA": "callsign",
|
||||||
"<qso_date:8>20240315": "date",
|
"<qso_date:8>20240315": "date",
|
||||||
"<time_on:6>142530": "time",
|
"<time_on:6>142530": "time",
|
||||||
"<band:3>20m": "band",
|
"<band:3>20m": "band",
|
||||||
"<mode:2>CW": "mode",
|
"<mode:2>CW": "mode",
|
||||||
"<freq:9>14.025000": "freq",
|
"<freq:9>14.025000": "freq",
|
||||||
"<rst_sent:3>599": "rst sent",
|
"<rst_sent:3>599": "rst sent",
|
||||||
"<rst_rcvd:3>599": "rst rcvd",
|
"<rst_rcvd:3>599": "rst rcvd",
|
||||||
"<gridsquare:4>FN65": "grid",
|
"<gridsquare:4>FN65": "grid",
|
||||||
"<name:4>Mike": "name",
|
"<name:4>Mike": "name",
|
||||||
"<stx:1>1": "stx serial",
|
"<stx:1>1": "stx serial",
|
||||||
"<srx:2>42": "srx serial",
|
"<srx:2>42": "srx serial",
|
||||||
"<eor>": "terminator",
|
"<eor>": "terminator",
|
||||||
}
|
}
|
||||||
for sub, label := range want {
|
for sub, label := range want {
|
||||||
if !strings.Contains(adif, sub) {
|
if !strings.Contains(adif, sub) {
|
||||||
|
|||||||
@@ -284,19 +284,32 @@ func wsjtSender(message string) (call string, isCQ bool, grid string) {
|
|||||||
// Skip an optional modifier after CQ (DX / a region like NA / a zone like
|
// 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).
|
// 020) — it never looks like a callsign (no letter+digit mix).
|
||||||
idx := 1
|
idx := 1
|
||||||
if len(f) > 2 && !looksLikeCall(f[1]) {
|
// A word after CQ that cannot be a callsign is a modifier — UNLESS it is a
|
||||||
|
// placeholder, which does occupy the callsign slot, just uselessly. Without
|
||||||
|
// that exception "CQ NOCALL JN36" skipped a slot it should have rejected.
|
||||||
|
if len(f) > 2 && !looksLikeCall(f[1]) && !isPlaceholderCall(f[1]) {
|
||||||
idx = 2
|
idx = 2
|
||||||
}
|
}
|
||||||
if idx < len(f) && looksLikeCall(f[idx]) {
|
if idx < len(f) {
|
||||||
if idx+1 < len(f) && isGridField(f[idx+1]) {
|
c := f[idx]
|
||||||
grid = f[idx+1]
|
if isPlaceholderCall(c) {
|
||||||
|
return "", true, ""
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
// station's grid gets logged, spotted and coloured as its callsign.
|
||||||
|
if looksLikeCall(c) && !isGridField(c) {
|
||||||
|
if idx+1 < len(f) && isGridField(f[idx+1]) {
|
||||||
|
grid = f[idx+1]
|
||||||
|
}
|
||||||
|
return c, true, grid
|
||||||
}
|
}
|
||||||
return f[idx], true, grid
|
|
||||||
}
|
}
|
||||||
return "", true, ""
|
return "", true, ""
|
||||||
}
|
}
|
||||||
// Standard exchange: the DE (sender) call is the second token.
|
// Standard exchange: the DE (sender) call is the second token.
|
||||||
if len(f) >= 2 && looksLikeCall(f[1]) {
|
if len(f) >= 2 && looksLikeCall(f[1]) && !isPlaceholderCall(f[1]) {
|
||||||
return f[1], false, ""
|
return f[1], false, ""
|
||||||
}
|
}
|
||||||
return "", false, ""
|
return "", false, ""
|
||||||
@@ -317,6 +330,24 @@ func isGridField(s string) bool {
|
|||||||
s[2] >= '0' && s[2] <= '9' && s[3] >= '0' && s[3] <= '9'
|
s[2] >= '0' && s[2] <= '9' && s[3] >= '0' && s[3] <= '9'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isPlaceholderCall reports a callsign nobody actually holds.
|
||||||
|
//
|
||||||
|
// N0CALL is WSJT-X's default: someone who installed it and never set their own
|
||||||
|
// callsign transmits under it. It has a letter and a digit and a perfectly
|
||||||
|
// ordinary shape, so nothing else rejects it — it would be spotted, coloured,
|
||||||
|
// counted as a new prefix, and now that CQ grids are read it would bring a grid
|
||||||
|
// into the worked index. NOCALL (letter O) has no digit and already fails the
|
||||||
|
// shape test; it is listed so the rule can be read without deducing that.
|
||||||
|
//
|
||||||
|
// Kept OUT of looksLikeCall deliberately. That function answers "could this
|
||||||
|
// token be a callsign at all", and the CQ grammar uses it to decide whether the
|
||||||
|
// word after CQ is a modifier (DX, NA, a zone) or the callsign itself. Teaching
|
||||||
|
// it that N0CALL is not a callsign made "CQ N0CALL JN36" skip to the next token
|
||||||
|
// and return the GRID as the sender. Shape and policy are different questions.
|
||||||
|
func isPlaceholderCall(s string) bool {
|
||||||
|
return s == "N0CALL" || s == "NOCALL"
|
||||||
|
}
|
||||||
|
|
||||||
// looksLikeCall is a loose callsign test: 3–12 chars of A–Z/0–9//, with at least
|
// looksLikeCall is a loose callsign test: 3–12 chars of A–Z/0–9//, with at least
|
||||||
// one letter AND one digit. Rejects the fixed exchange tokens (RR73/RRR/73) that
|
// one letter AND one digit. Rejects the fixed exchange tokens (RR73/RRR/73) that
|
||||||
// would otherwise pass.
|
// would otherwise pass.
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ func TestWSJTSender(t *testing.T) {
|
|||||||
{"CQ K1ABC FN42AB", "K1ABC", true, ""}, // 6-char: WSJT-X never sends it here
|
{"CQ K1ABC FN42AB", "K1ABC", true, ""}, // 6-char: WSJT-X never sends it here
|
||||||
{"CQ K1ABC 73", "K1ABC", true, ""}, // bare sign-off
|
{"CQ K1ABC 73", "K1ABC", true, ""}, // bare sign-off
|
||||||
{"CQ K1ABC SS42", "K1ABC", true, ""}, // S is past R — no such field
|
{"CQ K1ABC SS42", "K1ABC", true, ""}, // S is past R — no such field
|
||||||
|
// Placeholder callsigns: WSJT-X's default, from an operator who never set
|
||||||
|
// their own. A letter and a digit, so nothing else here catches it.
|
||||||
|
{"CQ N0CALL JN36", "", true, ""},
|
||||||
|
{"W2XYZ N0CALL -10", "", false, ""},
|
||||||
|
{"CQ NOCALL JN36", "", true, ""},
|
||||||
|
// An unknown word after CQ made the parser skip a slot; a grid passes every
|
||||||
|
// shape test a callsign does, so it came back AS the callsign.
|
||||||
|
{"CQ FOO JN36", "", true, ""},
|
||||||
// Non-callsign / free text → no sender.
|
// Non-callsign / free text → no sender.
|
||||||
{"TNX 73 GL", "", false, ""},
|
{"TNX 73 GL", "", false, ""},
|
||||||
{"K1ABC RR73", "", false, ""}, // only one call + a token → 2nd token not a call
|
{"K1ABC RR73", "", false, ""}, // only one call + a token → 2nd token not a call
|
||||||
|
|||||||
Reference in New Issue
Block a user