123 lines
4.0 KiB
Go
123 lines
4.0 KiB
Go
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)
|
|
})
|
|
}
|