package main // PSK Reporter target analysis — the panel beside the FT decodes. // // The FT decodes list says who is on the air. It cannot say whether the station // you are about to call can hear you, and on FT8 that is the only question that // matters: a pileup is invisible from this end, and calling a DX who is not // hearing your region at all is the commonest way to spend twenty minutes for // nothing. // // internal/pskrtgt does the work. This file is the wiring: settings, lifecycle, // and the bindings the panel polls. import ( "fmt" "strings" "hamlog/internal/applog" "hamlog/internal/pskrtgt" ) const ( keyPSKTargetOn = "pskrtgt.enabled" // the operator's master switch keyPSKTargetScope = "pskrtgt.scope" // "target" (narrow) | "band" (whole band) ) // PSKTargetSettings is the panel's shape in Settings. type PSKTargetSettings struct { Enabled bool `json:"enabled"` // Scope is how much of the PSK Reporter feed is subscribed to. The narrow // one is three filters about two callsigns; the wide one is every FTx report // on the band. The difference is a few messages a second against several // hundred, which on an old PC is the difference between a panel and a // problem — so the narrow one is the default, and the wide one is there for // an operator who switches target constantly and has the machine for it. Scope string `json:"scope"` } func (a *App) GetPSKTargetSettings() PSKTargetSettings { scope := a.settingOr(keyPSKTargetScope, string(pskrtgt.ScopeTarget)) if scope != string(pskrtgt.ScopeBand) { scope = string(pskrtgt.ScopeTarget) } return PSKTargetSettings{ Enabled: a.settingOr(keyPSKTargetOn, "0") == "1", Scope: scope, } } func (a *App) SavePSKTargetSettings(s PSKTargetSettings) error { on := "0" if s.Enabled { on = "1" } scope := s.Scope if scope != string(pskrtgt.ScopeBand) { scope = string(pskrtgt.ScopeTarget) } a.setSetting(keyPSKTargetOn, on) a.setSetting(keyPSKTargetScope, scope) // Rebuilt rather than reconfigured: the scope decides the subscriptions, and // a watcher that changed them under itself would have to unpick filters it // no longer knows the shape of. It reconnects on the next Watch call, which // the panel makes every second anyway. a.stopPSKTarget() applog.Printf("pskr target: %v (scope %s)", s.Enabled, scope) return nil } // pskTargetWatcher returns the watcher, building it on first use. Nothing is // connected until a target is set, so an operator who never opens the panel // never opens a socket to the broker. func (a *App) pskTargetWatcher() *pskrtgt.Watcher { a.pskTgtMu.Lock() defer a.pskTgtMu.Unlock() if a.pskTgt != nil { return a.pskTgt } s := a.GetPSKTargetSettings() a.pskTgt = pskrtgt.New(pskrtgt.Config{ Scope: pskrtgt.Scope(s.Scope), MyCall: a.opCall, MyGrid: a.opGrid, // Only a fallback, for a station with no grid set — see the package. Continent: func(call string) string { if a.dxcc == nil { return "" } if m, ok := a.dxcc.Lookup(call); ok { return m.Continent } return "" }, Logf: applog.Printf, }) return a.pskTgt } func (a *App) stopPSKTarget() { a.pskTgtMu.Lock() w := a.pskTgt a.pskTgt = nil a.pskTgtMu.Unlock() if w != nil { w.Stop() } } // SetPSKTarget points the analysis at a callsign — normally the station the // operator has just clicked or is calling. An empty callsign takes the feed // down: no target, no subscription. // // mode is the target's own (FT8/FT4), and dialHz the operator's dial, which is // what turns a report's frequency into an audio offset. func (a *App) SetPSKTarget(call, mode string, dialHz int64) error { if !a.GetPSKTargetSettings().Enabled { a.stopPSKTarget() return nil } call = strings.ToUpper(strings.TrimSpace(call)) if call == "" { a.stopPSKTarget() return nil } w := a.pskTargetWatcher() // The operator's identity is re-asserted on every call rather than captured // once: a profile switch changes both the callsign "he decoded you" looks // for and the square "near me" is measured from. w.SetOperator(a.opCall, a.opGrid) if err := w.Watch(call, mode, dialHz); err != nil { return fmt.Errorf("PSK Reporter: %w", err) } return nil } // GetPSKAnalysis is what the panel polls. Always answers, even with the feature // off or no target set — the panel says WHY it is empty, and it can only do // that if it is told. func (a *App) GetPSKAnalysis() pskrtgt.Analysis { s := a.GetPSKTargetSettings() a.pskTgtMu.Lock() w := a.pskTgt a.pskTgtMu.Unlock() if w == nil { return pskrtgt.Analysis{Enabled: s.Enabled} } an := w.Snapshot() an.Enabled = s.Enabled return an }