chore: release v0.27.12
This commit is contained in:
+150
@@ -0,0 +1,150 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user