feat: multiple amplifiers + SmartSDR v4 DSP filters

Multi-amp: Settings->Amplifier becomes a list (amps.json, legacy single-amp keys auto-migrate to entry #1); one client per enabled amp with per-id bindings (GetAmplifiers/SaveAmplifiers/GetAmpStatuses/AmpOperate/AmpPower/AmpPowerLevel/AmpFanMode); legacy a.pgxl/a.spe/a.acom point at the first enabled amp of each family. Amp cards in FlexPanel and Station Control gain a dropdown to pick the amp; the status bar shows one clickable chip per amp. Use case: two SPEs run in parallel.

Flex v4 DSP (8000/Aurora): NRL/ANFL (lms_nr/lms_anf), NRS (speex_nr), NRF (nrf) with level sliders, RNN (rnnoise) and ANFT on/off — keys per the FlexLib slice docs; the section only shows when the radio reports these keys (dsp_v4 flag), so 6000-series panels are unchanged.
This commit is contained in:
2026-07-22 17:25:10 +02:00
parent 4c75680689
commit b7dd8d4852
12 changed files with 1051 additions and 411 deletions
+96 -16
View File
@@ -30,10 +30,10 @@ type Flex struct {
conn net.Conn
dialCancel context.CancelFunc // cancels an in-flight Connect dial (Interrupt/Stop); nil when not dialing
wmu sync.Mutex // serialises writes to conn
seq int
handle string
model string
gotHandle bool
seq int
handle string
model string
gotHandle bool
slices map[int]*flexSlice
tx flexTX // transmit/ATU state pushed by the radio (FlexRadio tab)
@@ -90,12 +90,27 @@ type flexSlice struct {
apfLevel int
wnb bool // wideband noise blanker
wnbLevel int
rit bool // receive incremental tuning enabled
ritFreq int // RIT offset in Hz (negative = down)
xit bool // transmit incremental tuning enabled
xitFreq int // XIT offset in Hz
filterLo int // slice filter low cut (Hz)
filterHi int // slice filter high cut (Hz)
// SmartSDR v4 DSP (8000/Aurora series). Key names per the FlexLib slice
// docs: lms_nr(NRL) / lms_anf(ANFL) / speex_nr(NRS) / rnnoise(RNN) /
// anft(ANFT) / nrf(NRF). Older radios never report them — dspV4 flips true
// the first time any of these keys appears, and gates the extra UI rows.
lmsNR bool // NRL — legacy LMS noise reduction
lmsNRLevel int
lmsANF bool // ANFL — legacy LMS auto-notch
lmsANFLevel int
speexNR bool // NRS — spectral subtraction NR
speexNRLevel int
rnnoise bool // RNN — AI noise reduction (on/off only)
anft bool // ANFT — FFT-based auto-notch (on/off only)
nrf bool // NRF — noise reduction w/ filter
nrfLevel int
dspV4 bool
rit bool // receive incremental tuning enabled
ritFreq int // RIT offset in Hz (negative = down)
xit bool // transmit incremental tuning enabled
xitFreq int // XIT offset in Hz
filterLo int // slice filter low cut (Hz)
filterHi int // slice filter high cut (Hz)
rxAnt string // selected RX antenna (e.g. ANT1, ANT2, RX_A)
txAnt string // selected TX antenna
antList []string // antennas valid for this slice (RX side)
@@ -809,6 +824,26 @@ func (f *Flex) handleStatus(payload string) {
s.wnb = val == "1"
case "wnb_level":
s.wnbLevel = atoiDefault(val, s.wnbLevel)
case "lms_nr":
s.lmsNR, s.dspV4 = val == "1", true
case "lms_nr_level":
s.lmsNRLevel, s.dspV4 = atoiDefault(val, s.lmsNRLevel), true
case "lms_anf":
s.lmsANF, s.dspV4 = val == "1", true
case "lms_anf_level":
s.lmsANFLevel, s.dspV4 = atoiDefault(val, s.lmsANFLevel), true
case "speex_nr":
s.speexNR, s.dspV4 = val == "1", true
case "speex_nr_level":
s.speexNRLevel, s.dspV4 = atoiDefault(val, s.speexNRLevel), true
case "rnnoise":
s.rnnoise, s.dspV4 = val == "1", true
case "anft":
s.anft, s.dspV4 = val == "1", true
case "nrf":
s.nrf, s.dspV4 = val == "1", true
case "nrf_level":
s.nrfLevel, s.dspV4 = atoiDefault(val, s.nrfLevel), true
case "rit_on":
s.rit = val == "1"
case "rit_freq":
@@ -1328,6 +1363,17 @@ func (f *Flex) FlexState() FlexTXState {
st.APFLevel = rx.apfLevel
st.WNB = rx.wnb
st.WNBLevel = rx.wnbLevel
st.LMSNR = rx.lmsNR
st.LMSNRLevel = rx.lmsNRLevel
st.LMSANF = rx.lmsANF
st.LMSANFLevel = rx.lmsANFLevel
st.SpeexNR = rx.speexNR
st.SpeexNRLevel = rx.speexNRLevel
st.RNN = rx.rnnoise
st.ANFT = rx.anft
st.NRF = rx.nrf
st.NRFLevel = rx.nrfLevel
st.DSPV4 = rx.dspV4
st.RIT = rx.rit
st.RITFreq = rx.ritFreq
st.XIT = rx.xit
@@ -1396,6 +1442,26 @@ func (f *Flex) sendSlice(param string, val any) error {
rx.apf = val == "1"
case "apf_level":
rx.apfLevel = toInt(val)
case "lms_nr":
rx.lmsNR = val == "1"
case "lms_nr_level":
rx.lmsNRLevel = toInt(val)
case "lms_anf":
rx.lmsANF = val == "1"
case "lms_anf_level":
rx.lmsANFLevel = toInt(val)
case "speex_nr":
rx.speexNR = val == "1"
case "speex_nr_level":
rx.speexNRLevel = toInt(val)
case "rnnoise":
rx.rnnoise = val == "1"
case "anft":
rx.anft = val == "1"
case "nrf":
rx.nrf = val == "1"
case "nrf_level":
rx.nrfLevel = toInt(val)
case "rxant":
rx.rxAnt = fmt.Sprint(val)
case "txant":
@@ -1515,14 +1581,15 @@ func (f *Flex) SetNR(on bool) error { return f.sendSlice("nr", boolFlex(on))
func (f *Flex) SetNRLevel(l int) error { return f.sendSlice("nr_level", clampLevel(l)) }
func (f *Flex) SetANF(on bool) error { return f.sendSlice("anf", boolFlex(on)) }
func (f *Flex) SetANFLevel(l int) error { return f.sendSlice("anf_level", clampLevel(l)) }
// RIT/XIT — an offset applied to the RX (RIT) or TX (XIT) frequency of the active
// slice, without moving the slice itself. SmartSDR keeps the offset even while the
// switch is off, so turning RIT back on restores the last offset — same as the
// radio's own knob.
func (f *Flex) SetRIT(on bool) error { return f.sendSlice("rit_on", boolFlex(on)) }
func (f *Flex) SetRITFreq(hz int) error { return f.sendSlice("rit_freq", clampOffset(hz)) }
func (f *Flex) SetXIT(on bool) error { return f.sendSlice("xit_on", boolFlex(on)) }
func (f *Flex) SetXITFreq(hz int) error { return f.sendSlice("xit_freq", clampOffset(hz)) }
func (f *Flex) SetRIT(on bool) error { return f.sendSlice("rit_on", boolFlex(on)) }
func (f *Flex) SetRITFreq(hz int) error { return f.sendSlice("rit_freq", clampOffset(hz)) }
func (f *Flex) SetXIT(on bool) error { return f.sendSlice("xit_on", boolFlex(on)) }
func (f *Flex) SetXITFreq(hz int) error { return f.sendSlice("xit_freq", clampOffset(hz)) }
// clampOffset keeps a RIT/XIT offset inside what SmartSDR accepts (±99 999 Hz).
func clampOffset(hz int) int {
@@ -1537,8 +1604,21 @@ func clampOffset(hz int) int {
func (f *Flex) SetAPF(on bool) error { return f.sendSlice("apf", boolFlex(on)) }
func (f *Flex) SetAPFLevel(l int) error { return f.sendSlice("apf_level", clampLevel(l)) }
func (f *Flex) SetWNB(on bool) error { return f.sendSlice("wnb", boolFlex(on)) }
func (f *Flex) SetWNBLevel(l int) error { return f.sendSlice("wnb_level", clampLevel(l)) }
// SmartSDR v4 DSP (8000/Aurora series) — the extra noise tools SmartSDR ships
// beyond nb/nr/anf/wnb. Key names per the FlexLib slice command docs.
func (f *Flex) SetLMSNR(on bool) error { return f.sendSlice("lms_nr", boolFlex(on)) }
func (f *Flex) SetLMSNRLevel(l int) error { return f.sendSlice("lms_nr_level", clampLevel(l)) }
func (f *Flex) SetLMSANF(on bool) error { return f.sendSlice("lms_anf", boolFlex(on)) }
func (f *Flex) SetLMSANFLevel(l int) error { return f.sendSlice("lms_anf_level", clampLevel(l)) }
func (f *Flex) SetSpeexNR(on bool) error { return f.sendSlice("speex_nr", boolFlex(on)) }
func (f *Flex) SetSpeexNRLevel(l int) error { return f.sendSlice("speex_nr_level", clampLevel(l)) }
func (f *Flex) SetRNN(on bool) error { return f.sendSlice("rnnoise", boolFlex(on)) }
func (f *Flex) SetANFT(on bool) error { return f.sendSlice("anft", boolFlex(on)) }
func (f *Flex) SetNRF(on bool) error { return f.sendSlice("nrf", boolFlex(on)) }
func (f *Flex) SetNRFLevel(l int) error { return f.sendSlice("nrf_level", clampLevel(l)) }
func (f *Flex) SetWNB(on bool) error { return f.sendSlice("wnb", boolFlex(on)) }
func (f *Flex) SetWNBLevel(l int) error { return f.sendSlice("wnb_level", clampLevel(l)) }
// ── CW keyer controls (top-level "cw" commands) ──