fix(alerts): one band plan, not two that disagree

A spot at 10131.5 showed as DATA in the cluster list and raised an alert
announcing SSB. Two tables answered the same question: the frontend knew
10.130-10.150 is the 30 m data segment, while the alert engine knew a CW
range that stopped at 10.130 and ended with a bare 'return SSB' for
everything else.

That fall-through was the fault. A band plan that answers 'SSB' for
anything it does not recognise is not a band plan, it is a default wearing
one — and it fed the alert text, the FlexRadio spot colours and the mode
the radio is told to switch to.

The Go table is now transcribed from the frontend's, segment for segment,
and returns nothing where it knows nothing. The order matters and is now
load-bearing — the FT8 and FT4 watering holes are listed BEFORE the wide
data blocks they sit inside — so there is a test that fails if anybody
sorts the table by frequency and quietly turns every FT8 hole into DATA.

WPM counts as CW while we are here: nothing else is reported in words per
minute, and RBN puts it on every line.
This commit is contained in:
2026-08-26 21:09:52 +02:00
parent 4c2638a7e5
commit 9c7983b13e
4 changed files with 169 additions and 28 deletions
+18 -26
View File
@@ -264,9 +264,18 @@ func matchWildcard(pattern, v string) bool {
return re.MatchString(v)
}
// InferMode guesses a spot's mode from its comment and frequency. Cluster spots
// don't carry a mode field, so we read common tags (FT8/FT4/CW/RTTY/…) then fall
// back to the digital watering holes and the band-plan CW/phone split.
// InferMode works out a spot's mode from its comment, then from the band plan.
//
// Cluster lines carry no mode field, so this is all there is — and it feeds the
// alert rules, the FlexRadio panadapter colours and the spot the radio is told
// about. It used to end with a bare "return SSB" for anything its handful of CW
// ranges did not cover, which is how a 30 m FT8 spot at 10131.5 raised an alert
// announcing SSB while the cluster list beside it said DATA.
//
// The comment still wins when it names a mode: a spotter who wrote FT8 knows
// better than any table. Below that is the shared band plan (bandplan.go), and
// below THAT is nothing — an empty mode, which callers read as "unknown" rather
// than as a claim.
func InferMode(comment string, freqHz int64) string {
c := strings.ToUpper(comment)
switch {
@@ -280,30 +289,13 @@ func InferMode(comment string, freqHz int64) string {
return "PSK"
case strings.Contains(c, "JS8"):
return "JS8"
case strings.Contains(c, "CW"):
// WPM is as good as the word CW: nothing else is reported in words per
// minute, and RBN spots carry it on every line.
case strings.Contains(c, "CW"), strings.Contains(c, "WPM"):
return "CW"
case strings.Contains(c, "SSB") || strings.Contains(c, "USB") || strings.Contains(c, "LSB") || strings.Contains(c, "PH"):
case strings.Contains(c, "SSB"), strings.Contains(c, "USB"),
strings.Contains(c, "LSB"), strings.Contains(c, "PH"):
return "SSB"
}
khz := float64(freqHz) / 1000
// FT8 watering holes (…074) and FT4 (…080/…140) as a fallback.
for _, f := range []float64{1840, 3573, 7074, 10136, 14074, 18100, 21074, 24915, 28074, 50313} {
if khz >= f-1 && khz <= f+3 {
return "FT8"
}
}
// Band-plan CW segments (bottom of each band).
switch {
case khz >= 1810 && khz <= 1840,
khz >= 3500 && khz <= 3570,
khz >= 7000 && khz <= 7040,
khz >= 10100 && khz <= 10130,
khz >= 14000 && khz <= 14070,
khz >= 18068 && khz <= 18095,
khz >= 21000 && khz <= 21070,
khz >= 24890 && khz <= 24910,
khz >= 28000 && khz <= 28070:
return "CW"
}
return "SSB"
return modeFromFrequency(freqHz)
}