chore: release v0.26.3
This commit is contained in:
+122
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
// Panadapter auto-zoom on a spot click.
|
||||
//
|
||||
// A CW signal is 100 Hz wide, an FT8 slot 50 Hz in a 3 kHz sub-band, an SSB
|
||||
// station 2.7 kHz. One panadapter width cannot serve all three: at 200 kHz the
|
||||
// CW station you clicked is a hair, and at 25 kHz an FT8 window shows one
|
||||
// eighth of the traffic. So the width follows the MODE of the spot, which is
|
||||
// the only thing that determines how much spectrum is worth looking at.
|
||||
//
|
||||
// The command is SmartSDR's own: "display pan s <pan> bandwidth=<MHz>", with an
|
||||
// optional re-centre. Same approach as DXHunter, which is where the defaults
|
||||
// below come from.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"hamlog/internal/cat"
|
||||
)
|
||||
|
||||
const keyFlexZoom = "flex.spot_zoom"
|
||||
|
||||
// FlexZoomSettings is the per-mode-group visible width, in kHz.
|
||||
//
|
||||
// kHz and not MHz: an operator thinks "25 kHz of CW", and a form asking for
|
||||
// 0.025 invites a zero too many — which on a panadapter is not an error message
|
||||
// but a display that has silently gone somewhere else.
|
||||
type FlexZoomSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
CWkHz int `json:"cw_khz"`
|
||||
SSBkHz int `json:"ssb_khz"`
|
||||
DigikHz int `json:"digi_khz"`
|
||||
// Centre re-centres the panadapter on EVERY spot, not just the ones that
|
||||
// would otherwise fall outside the new window — the display always follows.
|
||||
// Off by default: an operator who has arranged their window around a run
|
||||
// frequency does not necessarily want it moved every time they look at
|
||||
// someone else, and a spot already on screen stays where it is either way.
|
||||
Centre bool `json:"centre"`
|
||||
}
|
||||
|
||||
// defaultFlexZoom — the widths DXHunter settled on, in kHz.
|
||||
var defaultFlexZoom = FlexZoomSettings{Enabled: false, CWkHz: 25, SSBkHz: 200, DigikHz: 50, Centre: false}
|
||||
|
||||
// flexZoomGroup maps a spot's mode to the three widths worth distinguishing.
|
||||
//
|
||||
// The mode arrives as whatever the cluster comment implied — "CW", "FT8",
|
||||
// "USB", "RTTY" — so this takes the ADIF-ish name, not the Flex one. Anything
|
||||
// unrecognised is digital, which is the safe end: a 50 kHz window is readable
|
||||
// for every mode, where 25 kHz is not.
|
||||
func flexZoomGroup(mode string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(mode)) {
|
||||
case "CW", "CWR", "CWL", "CWU":
|
||||
return "cw"
|
||||
case "SSB", "USB", "LSB", "AM", "FM", "NFM", "DFM", "SAM", "PHONE", "DV":
|
||||
return "ssb"
|
||||
}
|
||||
return "digi"
|
||||
}
|
||||
|
||||
// GetFlexZoom returns the settings, defaults included.
|
||||
func (a *App) GetFlexZoom() FlexZoomSettings {
|
||||
out := defaultFlexZoom
|
||||
if raw := a.settingOr(keyFlexZoom, ""); raw != "" {
|
||||
var s FlexZoomSettings
|
||||
if err := json.Unmarshal([]byte(raw), &s); err == nil {
|
||||
out = normFlexZoom(s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// normFlexZoom clamps the widths to something a panadapter can actually show.
|
||||
// 1 kHz is narrower than most filters; 14 MHz is a whole HF spectrum.
|
||||
func normFlexZoom(s FlexZoomSettings) FlexZoomSettings {
|
||||
clamp := func(v, def int) int {
|
||||
if v < 1 || v > 14000 {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
s.CWkHz = clamp(s.CWkHz, defaultFlexZoom.CWkHz)
|
||||
s.SSBkHz = clamp(s.SSBkHz, defaultFlexZoom.SSBkHz)
|
||||
s.DigikHz = clamp(s.DigikHz, defaultFlexZoom.DigikHz)
|
||||
return s
|
||||
}
|
||||
|
||||
// SaveFlexZoom persists the settings.
|
||||
func (a *App) SaveFlexZoom(s FlexZoomSettings) error {
|
||||
b, err := json.Marshal(normFlexZoom(s))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.setSetting(keyFlexZoom, string(b))
|
||||
return nil
|
||||
}
|
||||
|
||||
// FlexZoomForSpot resizes the panadapter for a spot that was just clicked.
|
||||
//
|
||||
// Called from the click handler, AFTER the rig has been tuned: the zoom is a
|
||||
// view change and must never delay the frequency change an operator is waiting
|
||||
// for. Does nothing at all unless the option is on and the backend is a Flex —
|
||||
// no other radio has a panadapter to zoom.
|
||||
func (a *App) FlexZoomForSpot(mode string, freqHz int64) {
|
||||
if a.cat == nil {
|
||||
return
|
||||
}
|
||||
z := a.GetFlexZoom()
|
||||
if !z.Enabled {
|
||||
return
|
||||
}
|
||||
khz := z.DigikHz
|
||||
switch flexZoomGroup(mode) {
|
||||
case "cw":
|
||||
khz = z.CWkHz
|
||||
case "ssb":
|
||||
khz = z.SSBkHz
|
||||
}
|
||||
_ = a.cat.FlexDo(func(fc cat.FlexController) error {
|
||||
return fc.ZoomPan(float64(khz)/1000, float64(freqHz)/1e6, z.Centre)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user