fix(decodes): a decode's mode is a marker, not a mode name
Every station on an already-worked band was flagged NEW MODE. A WSJT-X Decode does not carry the mode's name. It carries the one-character marker from the decode line - "~" for FT8, "+" for FT4 - and that character was passed straight through as though it were a mode. The status resolver then compared "~" against the modes worked for the entity, matched nothing, and concluded the mode had never been worked. Same cause put "~ -07" in the comment of every decode spot pushed to the FlexRadio panadapter, which nobody had traced back. Resolved through a marker table, with the mode from the sender's last Status as the fallback - Status is the message that carries the real name. So an unlisted or future marker degrades to correct rather than to nonsense, and a sender that puts the name in the field directly is believed as-is. With neither available the mode is left empty, which makes the resolver answer "worked": the safe side, since a wrong mode invents a new-mode flag exactly as the marker did.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package udp
|
||||
|
||||
import "testing"
|
||||
|
||||
// A Decode does not carry the mode's NAME. It carries the one-character marker
|
||||
// from the decode line — "~" for FT8, "+" for FT4 — and that character used to
|
||||
// be passed on as if it were a mode. Everything downstream compared it against
|
||||
// the modes in the log, matched nothing, and reported every station on an
|
||||
// already-worked band as a NEW MODE.
|
||||
func TestDecodeModeNameResolvesTheMarker(t *testing.T) {
|
||||
for raw, want := range map[string]string{
|
||||
"~": "FT8",
|
||||
"+": "FT4",
|
||||
"#": "JT65",
|
||||
"@": "JT9",
|
||||
} {
|
||||
if got := DecodeModeName(raw, "FT8"); got != want {
|
||||
t.Errorf("DecodeModeName(%q) = %q, want %q", raw, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A sender that puts the real name in the field is believed as-is — several do,
|
||||
// and the marker table must not get in their way.
|
||||
func TestDecodeModeNameKeepsARealName(t *testing.T) {
|
||||
for _, raw := range []string{"FT8", "ft4", "JS8", "Q65"} {
|
||||
if got := DecodeModeName(raw, ""); got == "" || got != upper(raw) {
|
||||
t.Errorf("DecodeModeName(%q) = %q, want the name itself", raw, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The safety net: an unknown marker falls back to the mode from the sender's
|
||||
// last Status, which always carries the real name. This is what keeps a future
|
||||
// or unlisted marker degrading to correct rather than to nonsense.
|
||||
func TestDecodeModeNameFallsBackToStatus(t *testing.T) {
|
||||
if got := DecodeModeName("%", "FT4"); got != "FT4" {
|
||||
t.Errorf("unknown marker resolved to %q, want the Status mode FT4", got)
|
||||
}
|
||||
if got := DecodeModeName("", "FT8"); got != "FT8" {
|
||||
t.Errorf("empty mode resolved to %q, want the Status mode FT8", got)
|
||||
}
|
||||
// Nothing known at all is empty rather than a guess: an empty mode makes the
|
||||
// status resolver answer "worked", which is the safe side — a wrong mode
|
||||
// would invent a new-mode flag exactly as the marker did.
|
||||
if got := DecodeModeName("%", ""); got != "" {
|
||||
t.Errorf("with no Status mode the result was %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func upper(s string) string {
|
||||
out := []rune(s)
|
||||
for i, r := range out {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
out[i] = r - 32
|
||||
}
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
@@ -183,6 +183,9 @@ type Server struct {
|
||||
// lastFrom is the address each program's packets arrive from — where a Reply
|
||||
// has to be sent. See SendReply.
|
||||
lastFrom map[string]*net.UDPAddr
|
||||
// lastMode is the mode NAME from each program's last Status, used to resolve
|
||||
// a Decode's one-character mode marker.
|
||||
lastMode map[string]string
|
||||
lastDX string // WSJT: last non-empty DX Call seen, to detect a clear
|
||||
|
||||
// badPkts counts datagrams this listener could not parse, so the diagnostic
|
||||
@@ -436,6 +439,14 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
||||
}
|
||||
s.trPeriod[w.ProgramID] = w.TRPeriod
|
||||
}
|
||||
// The mode NAME, which only Status carries: a Decode gives the
|
||||
// one-character marker instead. See DecodeModeName.
|
||||
if w.Mode != "" {
|
||||
if s.lastMode == nil {
|
||||
s.lastMode = map[string]string{}
|
||||
}
|
||||
s.lastMode[w.ProgramID] = w.Mode
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
if !w.IsDecode && (w.TxMessage != "" || w.DECall != "") {
|
||||
@@ -452,6 +463,7 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
||||
s.mu.Lock()
|
||||
dial := s.dialHz[w.ProgramID]
|
||||
tr := s.trPeriod[w.ProgramID]
|
||||
statusMode := s.lastMode[w.ProgramID]
|
||||
s.mu.Unlock()
|
||||
if dial <= 0 {
|
||||
// No Status from THIS instance yet. Guessing with another
|
||||
@@ -464,7 +476,7 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
||||
ev.DecodeFreqHz = dial + w.DeltaFreqHz
|
||||
ev.DecodeSNR = w.SNR
|
||||
ev.DecodeCQ = w.IsCQ
|
||||
ev.Mode = w.Mode
|
||||
ev.Mode = DecodeModeName(w.Mode, statusMode)
|
||||
ev.DecodeMsg = w.DecodeMsg
|
||||
ev.DecodeAt = decodeTime(w.DecodeMsSinceMidnight)
|
||||
ev.DecodeTRPeriod = tr
|
||||
|
||||
@@ -453,3 +453,50 @@ func readQString(r *bytes.Reader) (string, error) {
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
// decodeModeChar maps the single character WSJT-X puts in a Decode's mode field
|
||||
// to the mode it stands for.
|
||||
//
|
||||
// A Decode does NOT carry the mode's name. It carries the one-character marker
|
||||
// that appears in the decode line and in ALL.TXT — "~" for FT8, "+" for FT4 —
|
||||
// and that character was being passed straight through as if it were a mode.
|
||||
// Everything downstream then compared "~" against the modes in the log, matched
|
||||
// nothing, and called every station on an already-worked band a new MODE.
|
||||
//
|
||||
// The table covers what is common; anything missing falls back to the mode from
|
||||
// the sender's last Status, which carries the real name — so an unlisted or
|
||||
// future marker degrades to correct rather than to nonsense.
|
||||
var decodeModeChar = map[string]string{
|
||||
"~": "FT8",
|
||||
"+": "FT4",
|
||||
"#": "JT65",
|
||||
"@": "JT9",
|
||||
"&": "MSK144",
|
||||
":": "Q65",
|
||||
"`": "FST4",
|
||||
}
|
||||
|
||||
// DecodeModeName resolves a Decode's mode field to a real mode name. statusMode
|
||||
// is the mode from the same program's last Status, used when the field is a
|
||||
// marker we do not know, or empty.
|
||||
func DecodeModeName(raw, statusMode string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if m, ok := decodeModeChar[raw]; ok {
|
||||
return m
|
||||
}
|
||||
// A mode name is at least two alphanumeric characters ("FT8", "JS8", "Q65").
|
||||
// Anything shorter, or carrying punctuation, is a marker rather than a name.
|
||||
if len(raw) >= 2 {
|
||||
named := true
|
||||
for _, r := range raw {
|
||||
if !(r >= 'A' && r <= 'Z') && !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') {
|
||||
named = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if named {
|
||||
return strings.ToUpper(raw)
|
||||
}
|
||||
}
|
||||
return strings.ToUpper(strings.TrimSpace(statusMode))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user