diff --git a/bandopen_sources.go b/bandopen_sources.go index 930542c..f999d7a 100644 --- a/bandopen_sources.go +++ b/bandopen_sources.go @@ -20,9 +20,9 @@ import ( "hamlog/internal/applog" "hamlog/internal/bandopen" + "hamlog/internal/cluster" "hamlog/internal/geo" "hamlog/internal/gridcache" - "hamlog/internal/cluster" "hamlog/internal/pskr" ) @@ -129,6 +129,7 @@ func (a *App) startBandOpenFeed() { } s := a.GetBandOpenSettings() a.bandOpen.on.Store(s.Enabled) + a.setBandOpenBands(s.Bands) 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 @@ -204,7 +205,7 @@ func (a *App) startBandOpenFeed() { // so it does the least possible: the detector's own window and de-duplication // by callsign are what turn that flood into one announcement. func (a *App) feedBandOpen(s pskr.Spot) { - if !bandopen.Watched(s.Band) { + if !a.bandOpenWanted(s.Band) { return } a.bandOpen.mu.Lock() diff --git a/bandopening.go b/bandopening.go index d75c71a..18e3987 100644 --- a/bandopening.go +++ b/bandopening.go @@ -28,8 +28,15 @@ type bandOpenState struct { // 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 + on atomic.Bool + // bands is the operator's SELECTED set, held as map[string]bool. + // + // bandopen.Watched only says which bands the detector is capable of; it + // knows nothing about the chips in Settings. Nothing checked the selection + // on either feed path, so unticking 10 m and 2 m changed the subscription and + // left the cluster path announcing them anyway. + bands atomic.Value + mu sync.Mutex det *bandopen.Detector last []bandopen.Opening // most recent first, for the UI // live holds the announced openings that are still going, keyed by band, and @@ -60,7 +67,7 @@ func (a *App) detectBandOpening(s cluster.Spot) { // 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() { + if !a.bandOpen.on.Load() || !a.bandOpenWanted(s.Band) { return } // No operator grid = no distance and no bearing on the spot, and the whole @@ -140,6 +147,41 @@ func (a *App) GetLiveOpenings() []bandopen.Opening { return out } +// bandOpenWanted reports whether the operator has this band ticked. Falls back +// to the detector's own set until a selection has been stored, so a band is +// never silently dropped before the settings have been read. +func (a *App) bandOpenWanted(band string) bool { + if !bandopen.Watched(band) { + return false + } + sel, ok := a.bandOpen.bands.Load().(map[string]bool) + if !ok || len(sel) == 0 { + return true + } + return sel[strings.ToLower(strings.TrimSpace(band))] +} + +// setBandOpenBands records the selection and puts out badges for bands that +// have just been unticked — they fade on a timer fed by spots the detector no +// longer looks at, so they would otherwise stay lit until the next restart. +func (a *App) setBandOpenBands(bands []string) { + sel := make(map[string]bool, len(bands)) + for _, b := range bands { + sel[strings.ToLower(strings.TrimSpace(b))] = true + } + a.bandOpen.bands.Store(sel) + + a.bandOpen.mu.Lock() + defer a.bandOpen.mu.Unlock() + for b := range a.bandOpen.live { + if len(sel) > 0 && !sel[b] { + delete(a.bandOpen.live, b) + delete(a.bandOpen.aliveUntil, b) + applog.Printf("bandopen: %s is no longer watched — badge cleared", strings.ToUpper(b)) + } + } +} + // 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 diff --git a/bandopening_gate_test.go b/bandopening_gate_test.go index 7acb4d7..eeaa220 100644 --- a/bandopening_gate_test.go +++ b/bandopening_gate_test.go @@ -63,3 +63,56 @@ func TestClearBandOpeningsPutsTheBadgesOut(t *testing.T) { t.Error("the remembered openings were thrown away") } } + +// Unticking a band in Settings must actually stop its announcements. +// +// Nothing checked the operator's selection on either feed path: bandopen.Watched +// only says which bands the detector is CAPABLE of. So a station with just 6 m +// ticked still got 10 m and 2 m badges, from the cluster path and — once grid +// chasing widened the subscription to every band — from PSK Reporter too. +func TestBandOpenHonoursTheSelectedBands(t *testing.T) { + a := &App{opSet: true, opLat: 48, opLon: 2} + a.bandOpen.on.Store(true) + a.setBandOpenBands([]string{"6m"}) + + if !a.bandOpenWanted("6m") { + t.Error("6m is ticked and was refused") + } + for _, b := range []string{"10m", "2m", "4m"} { + if a.bandOpenWanted(b) { + t.Errorf("%s is not ticked and was accepted", b) + } + } + // A band the detector cannot watch at all stays out whatever is stored. + if a.bandOpenWanted("20m") { + t.Error("20m is not a watched band and was accepted") + } + + // And the cluster path must obey it too. + a.detectBandOpening(cluster.Spot{ + DXCall: "K1ABC", Band: "10m", DistanceKm: 6000, ShortPath: 280, ReceivedAt: time.Now(), + }) + if a.bandOpen.det != nil { + t.Error("a spot on an unticked band reached the detector") + } +} + +// Unticking a band puts its badge out. They fade on a timer fed by spots the +// detector no longer looks at, so it would otherwise stay lit until a restart. +func TestUntickingABandClearsItsBadge(t *testing.T) { + a := &App{} + a.bandOpen.live = map[string]bandopen.Opening{ + "6m": {Band: "6m", Calls: 9}, + "10m": {Band: "10m", Calls: 5}, + } + a.bandOpen.aliveUntil = map[string]time.Time{ + "6m": time.Now().Add(time.Hour), + "10m": time.Now().Add(time.Hour), + } + a.setBandOpenBands([]string{"6m"}) + + live := a.GetLiveOpenings() + if len(live) != 1 || live[0].Band != "6m" { + t.Errorf("live openings = %v, want only 6m", live) + } +} diff --git a/changelog.json b/changelog.json index 37b0946..47d7788 100644 --- a/changelog.json +++ b/changelog.json @@ -5,12 +5,14 @@ "en": [ "Cluster: the grid cache now holds 100,000 callsigns and rotates instead of emptying itself, so locators stop vanishing from the list.", "New option \"Chase new grids\": locators learnt from your decodes and from PSK Reporter are kept in their own database, with their source.", - "Band openings: the PSK Reporter feed is now filtered at the broker, which cuts it from about 83 messages a second to under two." + "Band openings: the PSK Reporter feed is now filtered at the broker, which cuts it from about 83 messages a second to under two.", + "Band openings: unticking a band now actually stops its announcements, and puts its badge out." ], "fr": [ "Cluster : le cache de locators garde 100 000 indicatifs et tourne au lieu de se vider, les locators ne disparaissent donc plus de la liste.", "Nouvelle option « Chasse aux nouveaux carrés » : les locators appris de tes décodes et de PSK Reporter sont conservés dans leur propre base, avec leur source.", - "Ouvertures de bande : le flux PSK Reporter est désormais filtré chez le broker, ce qui le fait passer d environ 83 messages par seconde à moins de deux." + "Ouvertures de bande : le flux PSK Reporter est désormais filtré chez le broker, ce qui le fait passer d environ 83 messages par seconde à moins de deux.", + "Ouvertures de bande : décocher une bande arrête réellement ses annonces et éteint son badge." ] }, {