fix(bandopen): the watch switch now gates the detector
"Watch for band openings" only ever governed the extra DATA SOURCES — the RBN nodes and the PSK Reporter feed. The detector itself ran on every ordinary cluster spot regardless, so an operator who had never enabled the watch still got 10 m and 6 m opening banners from a feature he had deliberately left off. The flag is cached on bandOpenState rather than read per spot: this is the cluster hot path, where a settings query per spot is exactly what the rest of this file avoids. startBandOpenFeed owns it, and it already runs at startup and on every save, so the switch takes effect without a restart. Switching it off also clears the live badges and the accumulated spot window. The badges only fade on a timer fed by spots the detector no longer looks at, so they would otherwise hang there until the next restart; and dropping the window means switching back on starts from what is on the air rather than from an hour-old burst. The remembered openings are kept — those really happened.
This commit is contained in:
@@ -126,7 +126,13 @@ func (a *App) startBandOpenFeed() {
|
|||||||
a.pskr = nil
|
a.pskr = nil
|
||||||
}
|
}
|
||||||
s := a.GetBandOpenSettings()
|
s := a.GetBandOpenSettings()
|
||||||
|
a.bandOpen.on.Store(s.Enabled)
|
||||||
if !s.Enabled {
|
if !s.Enabled {
|
||||||
|
// Put out whatever is currently lit. Leaving the badges up would keep
|
||||||
|
// announcing an opening from a watch that is now off, and they only fade
|
||||||
|
// on a timer fed by spots this path no longer looks at — so they would
|
||||||
|
// hang there until the app restarted.
|
||||||
|
a.clearBandOpenings()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Every spot is measured from the operator's position. Without one there is
|
// Every spot is measured from the operator's position. Without one there is
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hamlog/internal/applog"
|
"hamlog/internal/applog"
|
||||||
@@ -21,6 +22,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type bandOpenState struct {
|
type bandOpenState struct {
|
||||||
|
// on mirrors the "Watch for band openings" setting.
|
||||||
|
//
|
||||||
|
// Cached rather than read per spot: this is the cluster hot path, where a
|
||||||
|
// settings query per spot is exactly what the rest of this file avoids.
|
||||||
|
// startBandOpenFeed owns it — it runs at startup and again on every save, so
|
||||||
|
// the switch takes effect without a restart.
|
||||||
|
on atomic.Bool
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
det *bandopen.Detector
|
det *bandopen.Detector
|
||||||
last []bandopen.Opening // most recent first, for the UI
|
last []bandopen.Opening // most recent first, for the UI
|
||||||
@@ -45,6 +53,16 @@ const maxRememberedOpenings = 20
|
|||||||
|
|
||||||
// detectBandOpening feeds one spot to the detector and announces a hit.
|
// detectBandOpening feeds one spot to the detector and announces a hit.
|
||||||
func (a *App) detectBandOpening(s cluster.Spot) {
|
func (a *App) detectBandOpening(s cluster.Spot) {
|
||||||
|
// The watch has to be switched on.
|
||||||
|
//
|
||||||
|
// It was not checked here at all: the setting only ever governed the extra
|
||||||
|
// DATA SOURCES (the RBN nodes and the PSK Reporter feed), while the detector
|
||||||
|
// itself ran on every ordinary cluster spot. So an operator who had never
|
||||||
|
// enabled the watch still got opening banners, from a feature they had
|
||||||
|
// deliberately left off.
|
||||||
|
if !a.bandOpen.on.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
// No operator grid = no distance and no bearing on the spot, and the whole
|
// No operator grid = no distance and no bearing on the spot, and the whole
|
||||||
// detection rests on those two. Say nothing rather than guess.
|
// detection rests on those two. Say nothing rather than guess.
|
||||||
if !a.opSet || s.DistanceKm <= 0 || !bandopen.Watched(s.Band) {
|
if !a.opSet || s.DistanceKm <= 0 || !bandopen.Watched(s.Band) {
|
||||||
@@ -122,6 +140,21 @@ func (a *App) GetLiveOpenings() []bandopen.Opening {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clearBandOpenings puts out every lit badge and forgets the detector's window.
|
||||||
|
// Called when the watch is switched off: the badges fade on a timer fed by
|
||||||
|
// spots the detector no longer looks at, so without this they would stay up
|
||||||
|
// until the next restart. The remembered list is left alone — those openings
|
||||||
|
// really did happen, and the operator may still want to see what he missed.
|
||||||
|
func (a *App) clearBandOpenings() {
|
||||||
|
a.bandOpen.mu.Lock()
|
||||||
|
defer a.bandOpen.mu.Unlock()
|
||||||
|
a.bandOpen.live = nil
|
||||||
|
a.bandOpen.aliveUntil = nil
|
||||||
|
// Drop the accumulated spot window too, so switching the watch back on starts
|
||||||
|
// from what is on the air now rather than from an hour-old burst.
|
||||||
|
a.bandOpen.det = nil
|
||||||
|
}
|
||||||
|
|
||||||
// announceOpening logs and pushes one detection. Shared by both feeds — the
|
// announceOpening logs and pushes one detection. Shared by both feeds — the
|
||||||
// cluster path here and the PSK Reporter path in bandopen_sources.go — so an
|
// cluster path here and the PSK Reporter path in bandopen_sources.go — so an
|
||||||
// opening reads the same however it was noticed.
|
// opening reads the same however it was noticed.
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hamlog/internal/bandopen"
|
||||||
|
"hamlog/internal/cluster"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The "Watch for band openings" switch has to gate the DETECTOR, not just the
|
||||||
|
// extra data sources.
|
||||||
|
//
|
||||||
|
// It originally governed only the RBN nodes and the PSK Reporter feed, while
|
||||||
|
// the detector itself ran on every ordinary cluster spot — so an operator who
|
||||||
|
// had never enabled the watch still got opening banners for a feature he had
|
||||||
|
// deliberately left off. That is what this pins.
|
||||||
|
func TestBandOpenWatchGatesTheDetector(t *testing.T) {
|
||||||
|
spot := func() cluster.Spot {
|
||||||
|
return cluster.Spot{
|
||||||
|
DXCall: "EA1ABC", Band: "6m", DistanceKm: 1400, ShortPath: 210,
|
||||||
|
ReceivedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switched off: the spot must not even reach the detector.
|
||||||
|
off := &App{opSet: true, opLat: 48.0, opLon: 2.0}
|
||||||
|
off.detectBandOpening(spot())
|
||||||
|
if off.bandOpen.det != nil {
|
||||||
|
t.Error("the detector ran with the watch switched off")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switched on: the same spot is accepted (one spot is not an opening, so
|
||||||
|
// nothing is announced — but the detector now exists and is collecting).
|
||||||
|
on := &App{opSet: true, opLat: 48.0, opLon: 2.0}
|
||||||
|
on.bandOpen.on.Store(true)
|
||||||
|
on.detectBandOpening(spot())
|
||||||
|
if on.bandOpen.det == nil {
|
||||||
|
t.Error("the detector did not run with the watch switched on")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switching the watch off must put the badges out. They fade on a timer fed by
|
||||||
|
// spots the detector no longer looks at, so left alone they would stay lit
|
||||||
|
// until the next restart.
|
||||||
|
func TestClearBandOpeningsPutsTheBadgesOut(t *testing.T) {
|
||||||
|
a := &App{}
|
||||||
|
a.bandOpen.live = map[string]bandopen.Opening{"6m": {Band: "6m", Calls: 9}}
|
||||||
|
a.bandOpen.aliveUntil = map[string]time.Time{"6m": time.Now().Add(time.Hour)}
|
||||||
|
a.bandOpen.det = bandopen.New(bandopen.DefaultConfig())
|
||||||
|
a.bandOpen.last = []bandopen.Opening{{Band: "6m", Calls: 9}}
|
||||||
|
|
||||||
|
a.clearBandOpenings()
|
||||||
|
|
||||||
|
if got := a.GetLiveOpenings(); len(got) != 0 {
|
||||||
|
t.Errorf("a badge stayed lit after the watch was switched off: %v", got)
|
||||||
|
}
|
||||||
|
if a.bandOpen.det != nil {
|
||||||
|
t.Error("the accumulated spot window survived — switching back on would start from a stale burst")
|
||||||
|
}
|
||||||
|
// The history is NOT cleared: those openings really happened.
|
||||||
|
if len(a.GetBandOpenings()) != 1 {
|
||||||
|
t.Error("the remembered openings were thrown away")
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -11,7 +11,7 @@
|
|||||||
"Motorized antennas: each covered band now has its own tune frequency, set in a box under the band in Settings, and that is where the band button in Station Control sends the antenna. Left empty a band keeps its default, and a frequency that is not in its band is refused rather than sent to the elements.",
|
"Motorized antennas: each covered band now has its own tune frequency, set in a box under the band in Settings, and that is where the band button in Station Control sends the antenna. Left empty a band keeps its default, and a frequency that is not in its band is refused rather than sent to the elements.",
|
||||||
"Awards: a single award can now be exported on its own, next to the whole-catalogue export. Sharing one award meant handing over your entire catalogue.",
|
"Awards: a single award can now be exported on its own, next to the whole-catalogue export. Sharing one award meant handing over your entire catalogue.",
|
||||||
"Awards: each reference now has its own validity window, so a reference that ceased to exist counts for QSOs made while it existed and not for later ones. The dates were already stored and were never applied; left empty a reference follows the award's own window.",
|
"Awards: each reference now has its own validity window, so a reference that ceased to exist counts for QSOs made while it existed and not for later ones. The dates were already stored and were never applied; left empty a reference follows the award's own window.",
|
||||||
"DX cluster: the \"No colour on worked\" and \"Colour unworked here\" options are withdrawn from the filter panel and are off for everyone."
|
"Band openings: the watch switch now governs the detection itself. It only ever controlled the extra data sources, so opening banners appeared even for operators who had left the option off; switching it off also puts out any badge still lit."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Les QSO enregistrés depuis WSJT-X portent désormais la météo spatiale et la distance, comme ceux saisis à la main. Le chemin UDP posait le profil station, le DXCC et les défauts QSL mais ni SFI, ni A, ni K, ni distance — un opérateur en numérique avait donc ces champs vides sur tout son log. La météo spatiale n est posée que sur un contact de moins d un jour : sinon un logiciel qui rediffuse son historique se verrait attribuer les relevés de ce matin sur des contacts du mois dernier.",
|
"Les QSO enregistrés depuis WSJT-X portent désormais la météo spatiale et la distance, comme ceux saisis à la main. Le chemin UDP posait le profil station, le DXCC et les défauts QSL mais ni SFI, ni A, ni K, ni distance — un opérateur en numérique avait donc ces champs vides sur tout son log. La météo spatiale n est posée que sur un contact de moins d un jour : sinon un logiciel qui rediffuse son historique se verrait attribuer les relevés de ce matin sur des contacts du mois dernier.",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"Antennes motorisées : chaque bande couverte a désormais sa propre fréquence d accord, saisie dans une case sous la bande dans les Réglages, et c est là que le bouton de bande du Contrôle station envoie l antenne. Laissée vide, une bande garde son défaut, et une fréquence hors de sa bande est refusée plutôt qu envoyée aux éléments.",
|
"Antennes motorisées : chaque bande couverte a désormais sa propre fréquence d accord, saisie dans une case sous la bande dans les Réglages, et c est là que le bouton de bande du Contrôle station envoie l antenne. Laissée vide, une bande garde son défaut, et une fréquence hors de sa bande est refusée plutôt qu envoyée aux éléments.",
|
||||||
"Diplômes : un diplôme peut désormais être exporté seul, à côté de l export du catalogue complet. Partager un seul diplôme obligeait à livrer tout son catalogue.",
|
"Diplômes : un diplôme peut désormais être exporté seul, à côté de l export du catalogue complet. Partager un seul diplôme obligeait à livrer tout son catalogue.",
|
||||||
"Diplômes : chaque référence a désormais sa propre fenêtre de validité, si bien qu une référence qui a cessé d exister compte pour les QSO faits de son vivant et pas pour les suivants. Les dates étaient déjà stockées et n étaient jamais appliquées ; laissée vide, une référence suit la fenêtre du diplôme.",
|
"Diplômes : chaque référence a désormais sa propre fenêtre de validité, si bien qu une référence qui a cessé d exister compte pour les QSO faits de son vivant et pas pour les suivants. Les dates étaient déjà stockées et n étaient jamais appliquées ; laissée vide, une référence suit la fenêtre du diplôme.",
|
||||||
"Cluster DX : les options « Pas de couleur sur les faits » et « Colorer les non faits ici » sont retirées du panneau de filtres et désactivées pour tout le monde."
|
"Ouvertures de bande : l interrupteur de la veille gouverne désormais la détection elle-même. Il ne pilotait que les sources de données supplémentaires, si bien que les bandeaux d ouverture apparaissaient même chez les opérateurs qui avaient laissé l option désactivée ; la désactiver éteint aussi les badges encore allumés."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user