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
|
// lastFrom is the address each program's packets arrive from — where a Reply
|
||||||
// has to be sent. See SendReply.
|
// has to be sent. See SendReply.
|
||||||
lastFrom map[string]*net.UDPAddr
|
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
|
lastDX string // WSJT: last non-empty DX Call seen, to detect a clear
|
||||||
|
|
||||||
// badPkts counts datagrams this listener could not parse, so the diagnostic
|
// 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
|
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()
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
if !w.IsDecode && (w.TxMessage != "" || w.DECall != "") {
|
if !w.IsDecode && (w.TxMessage != "" || w.DECall != "") {
|
||||||
@@ -452,6 +463,7 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
dial := s.dialHz[w.ProgramID]
|
dial := s.dialHz[w.ProgramID]
|
||||||
tr := s.trPeriod[w.ProgramID]
|
tr := s.trPeriod[w.ProgramID]
|
||||||
|
statusMode := s.lastMode[w.ProgramID]
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
if dial <= 0 {
|
if dial <= 0 {
|
||||||
// No Status from THIS instance yet. Guessing with another
|
// 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.DecodeFreqHz = dial + w.DeltaFreqHz
|
||||||
ev.DecodeSNR = w.SNR
|
ev.DecodeSNR = w.SNR
|
||||||
ev.DecodeCQ = w.IsCQ
|
ev.DecodeCQ = w.IsCQ
|
||||||
ev.Mode = w.Mode
|
ev.Mode = DecodeModeName(w.Mode, statusMode)
|
||||||
ev.DecodeMsg = w.DecodeMsg
|
ev.DecodeMsg = w.DecodeMsg
|
||||||
ev.DecodeAt = decodeTime(w.DecodeMsSinceMidnight)
|
ev.DecodeAt = decodeTime(w.DecodeMsSinceMidnight)
|
||||||
ev.DecodeTRPeriod = tr
|
ev.DecodeTRPeriod = tr
|
||||||
|
|||||||
@@ -453,3 +453,50 @@ func readQString(r *bytes.Reader) (string, error) {
|
|||||||
}
|
}
|
||||||
return string(buf), nil
|
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