fix(pskr): a ten-minute window, both directions, and always an answer

Measured against DXHunter on the same station at the same second: 18
decodes here against 27 there.

- The window was five minutes. PSK Reporter's uploaders batch their
  reports, most of them every five, so a five-minute window catches
  about one upload cycle per station. Ten, as DXHunter has always had
  behind a label that says four.

- The history query ran once per target and only on the narrow feed.
  It now runs in both scopes and again every five minutes while a
  station is watched, which is the cadence the uploaders keep.

- It asked only what the target RECEIVED. Both directions now, so
  "who is hearing him" starts full too.

- The suggested call offset looked for a run of empty slots and said
  nothing when there was none — exactly the case it exists for: a
  hundred decodes across a 2800 Hz passband leave no gap. Failing a
  gap it names the quietest slot, ties to the higher offset, never
  above the ceiling.

Chase new: the receiver squares now follow the radius that was asked
for (it was one fixed ring whatever the setting said, so raising it
bought nothing), and the panel says what the feed is doing rather than
leaving an empty list to speak for itself. A change of hunt empties it:
its rows are verdicts reached under the old rule and nothing re-judged
them. Its own band selection, because what a station CAN work and what
is worth watching tonight are different questions.
This commit is contained in:
2026-09-05 21:45:39 +02:00
parent a41626955e
commit 470eaf5d80
4 changed files with 206 additions and 31 deletions
+90 -1
View File
@@ -36,6 +36,28 @@ import (
// have one without the other. They only share the feed, which either one starts.
const keyChaseNew = "cluster.chase_new"
// keyChaseNewBandsOff lists the bands the panel is NOT to show, comma
// separated.
//
// Stored as the EXCLUSIONS rather than the selection, so that an empty setting
// — every operator who has never opened this — means "show them all", and so
// that a band added to the station's list later appears here on its own. Stored
// the other way round, a selection saved today would silently hide 4 m the day
// a transverter arrives.
const keyChaseNewBandsOff = "cluster.chase_new_bands_off"
// ChaseNewBands is the band vocabulary of the panel's own filter: HF through
// 70 cm, in the order an operator reads a band plan.
//
// A fixed list, not the station's: the two answer different questions. The
// station list says what this station can work at all, and still applies —
// this one says what is worth WATCHING right now, which changes with the hour
// and the season, not with the shack.
var ChaseNewBands = []string{
"160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m",
"6m", "4m", "2m", "70cm",
}
// chaseNewMax bounds the panel. A list nobody can read to the bottom is not more
// information, and the oldest rows are the least likely to still be on the air.
const chaseNewMax = 200
@@ -149,6 +171,69 @@ var chaseModes = map[string]bool{
// atomic rather than the settings store.
func (a *App) chaseNewEnabled() bool { return a.chaseNewOn.Load() }
// GetChaseNewBands returns the bands the panel is set to SHOW — every band in
// the vocabulary that has not been switched off.
func (a *App) GetChaseNewBands() []string {
off := map[string]bool{}
for _, b := range splitCSV(a.settingOr(keyChaseNewBandsOff, "")) {
off[strings.ToLower(strings.TrimSpace(b))] = true
}
out := make([]string, 0, len(ChaseNewBands))
for _, b := range ChaseNewBands {
if !off[b] {
out = append(out, b)
}
}
return out
}
// SetChaseNewBands takes the bands to SHOW and stores the complement.
//
// Nothing is filtered out of the store retroactively: the rows already
// collected stay until they age out, and the next message is the one that
// obeys. A panel that emptied itself on a settings click would look like it had
// lost the feed.
func (a *App) SetChaseNewBands(show []string) {
want := map[string]bool{}
for _, b := range show {
want[strings.ToLower(strings.TrimSpace(b))] = true
}
var off []string
for _, b := range ChaseNewBands {
if !want[b] {
off = append(off, b)
}
}
a.setSetting(keyChaseNewBandsOff, strings.Join(off, ","))
a.refreshChaseNewBands()
applog.Printf("chase new: showing %d of %d bands", len(ChaseNewBands)-len(off), len(ChaseNewBands))
}
// refreshChaseNewBands re-reads the panel's own band filter into the cache the
// feed consults. Called at startup, and whenever the selection is saved.
func (a *App) refreshChaseNewBands() {
off := map[string]bool{}
for _, b := range splitCSV(a.settingOr(keyChaseNewBandsOff, "")) {
if b = strings.ToLower(strings.TrimSpace(b)); b != "" {
off[b] = true
}
}
a.chaseNewBandsOff.Store(off)
}
// chaseNewBandShown is the per-message half: a map read on the MQTT goroutine.
func (a *App) chaseNewBandShown(band string) bool {
v := a.chaseNewBandsOff.Load()
if v == nil {
return true // nothing loaded yet — better to show than to swallow
}
off, ok := v.(map[string]bool)
if !ok {
return true
}
return !off[strings.ToLower(strings.TrimSpace(band))]
}
// chaseBandAllowed reports whether a band is one the operator uses.
//
// PSK Reporter carries every band its receivers listen on, including microwave
@@ -217,7 +302,11 @@ func (a *App) feedChaseNew(sp pskr.Spot) {
// Both tests before the status lookup: they are map reads on short keys and
// they throw away most of the feed, so nothing further down pays for a band
// this station cannot work or a mode nobody answers.
if !chaseModes[mode] || !a.chaseBandAllowed(band) {
// Two band tests, and they answer two questions: can this station work the
// band at all (its own list), and does the operator want to WATCH it right
// now (this panel's own filter). A station with 2 m equipment that is
// chasing HF tonight needs the second one.
if !chaseModes[mode] || !a.chaseBandAllowed(band) || !a.chaseNewBandShown(band) {
return
}