151 lines
4.6 KiB
Go
151 lines
4.6 KiB
Go
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
|
|
}
|