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
+75 -50
View File
@@ -259,30 +259,30 @@ type FlexSliceInfo struct {
}
type FlexTXState struct {
Available bool `json:"available"` // backend is Flex and handshaked
Model string `json:"model,omitempty"`
Available bool `json:"available"` // backend is Flex and handshaked
Model string `json:"model,omitempty"`
// Slices lists every in-use receiver slice (A/B/C/D…) so the panel can show
// them all and highlight the active one. The active slice drives everything.
Slices []FlexSliceInfo `json:"slices,omitempty"`
RFPower int `json:"rf_power"`
TunePower int `json:"tune_power"`
Tune bool `json:"tune"` // tune carrier active
Transmitting bool `json:"transmitting"` // interlock state = TRANSMITTING
VoxEnable bool `json:"vox_enable"`
VoxLevel int `json:"vox_level"`
VoxDelay int `json:"vox_delay"`
ProcEnable bool `json:"proc_enable"`
ProcLevel int `json:"proc_level"`
Mon bool `json:"mon"`
MonLevel int `json:"mon_level"`
MicLevel int `json:"mic_level"`
TXFilterLow int `json:"tx_filter_low"` // TX filter low cut (Hz)
TXFilterHigh int `json:"tx_filter_high"` // TX filter high cut (Hz)
Slices []FlexSliceInfo `json:"slices,omitempty"`
RFPower int `json:"rf_power"`
TunePower int `json:"tune_power"`
Tune bool `json:"tune"` // tune carrier active
Transmitting bool `json:"transmitting"` // interlock state = TRANSMITTING
VoxEnable bool `json:"vox_enable"`
VoxLevel int `json:"vox_level"`
VoxDelay int `json:"vox_delay"`
ProcEnable bool `json:"proc_enable"`
ProcLevel int `json:"proc_level"`
Mon bool `json:"mon"`
MonLevel int `json:"mon_level"`
MicLevel int `json:"mic_level"`
TXFilterLow int `json:"tx_filter_low"` // TX filter low cut (Hz)
TXFilterHigh int `json:"tx_filter_high"` // TX filter high cut (Hz)
// Mic profiles (SmartSDR): the list of available profiles + the loaded one.
MicProfile string `json:"mic_profile,omitempty"`
MicProfiles []string `json:"mic_profiles,omitempty"`
ATUStatus string `json:"atu_status,omitempty"`
ATUMemories bool `json:"atu_memories"`
MicProfile string `json:"mic_profile,omitempty"`
MicProfiles []string `json:"mic_profiles,omitempty"`
ATUStatus string `json:"atu_status,omitempty"`
ATUMemories bool `json:"atu_memories"`
// Active RX slice DSP controls.
RXAvail bool `json:"rx_avail"` // an RX slice exists
Split bool `json:"split"` // RX/TX on separate slices
@@ -304,6 +304,20 @@ type FlexTXState struct {
ANFLevel int `json:"anf_level"`
WNB bool `json:"wnb"`
WNBLevel int `json:"wnb_level"`
// SmartSDR v4 DSP (8000/Aurora series): NRL/ANFL (legacy LMS), NRS
// (spectral subtraction), RNN (AI NR), ANFT (FFT notch), NRF (NR w/filter).
// DSPV4 is true once the radio reported any of these keys — gates the UI.
LMSNR bool `json:"lms_nr"`
LMSNRLevel int `json:"lms_nr_level"`
LMSANF bool `json:"lms_anf"`
LMSANFLevel int `json:"lms_anf_level"`
SpeexNR bool `json:"speex_nr"`
SpeexNRLevel int `json:"speex_nr_level"`
RNN bool `json:"rnn"`
ANFT bool `json:"anft"`
NRF bool `json:"nrf"`
NRFLevel int `json:"nrf_level"`
DSPV4 bool `json:"dsp_v4"`
// RIT/XIT — offsets applied to the active slice's RX / TX frequency without
// moving the slice. The offset survives the switch being turned off, so
// re-enabling restores it, exactly like the radio's own knob.
@@ -337,7 +351,7 @@ type FlexMeter struct {
Src string `json:"src,omitempty"` // SLC / TX- / RAD / AMP…
Name string `json:"name,omitempty"` // FWDPWR, SWR, LEVEL, PATEMP…
Unit string `json:"unit,omitempty"`
Slice int `json:"slice"` // for SLC meters, the slice index it belongs to; -1 otherwise
Slice int `json:"slice"` // for SLC meters, the slice index it belongs to; -1 otherwise
Value float64 `json:"value"`
Lo float64 `json:"lo"`
Hi float64 `json:"hi"`
@@ -386,6 +400,17 @@ type FlexController interface {
SetAPFLevel(int) error
SetWNB(bool) error
SetWNBLevel(int) error
// SmartSDR v4 DSP (8000/Aurora): NRL / ANFL / NRS / RNN / ANFT / NRF.
SetLMSNR(bool) error
SetLMSNRLevel(int) error
SetLMSANF(bool) error
SetLMSANFLevel(int) error
SetSpeexNR(bool) error
SetSpeexNRLevel(int) error
SetRNN(bool) error
SetANFT(bool) error
SetNRF(bool) error
SetNRFLevel(int) error
SetRIT(bool) error
SetRITFreq(int) error
SetXIT(bool) error
@@ -445,7 +470,7 @@ type IcomTXState struct {
// Transmit + live status (polled).
Transmitting bool `json:"transmitting"`
Split bool `json:"split"`
SMeter int `json:"s_meter"` // 0-100 (raw 0-255; S9≈120)
SMeter int `json:"s_meter"` // 0-100 (raw 0-255; S9≈120)
PowerMeter int `json:"power_meter"` // 0-100 (TX Po)
SWRMeter int `json:"swr_meter"` // 0-100 (TX SWR)
// RIT / ΔTX (XIT).
@@ -456,20 +481,20 @@ type IcomTXState struct {
KeySpeedWPM int `json:"key_speed_wpm"` // current KEY SPEED in WPM
BreakIn int `json:"break_in"` // CW break-in: 0=OFF, 1=SEMI, 2=FULL
// Set controls.
RFPower int `json:"rf_power"` // 0-100 (TX output)
MicGain int `json:"mic_gain"` // 0-100
AFGain int `json:"af_gain"`
RFGain int `json:"rf_gain"`
NB bool `json:"nb"`
NBLevel int `json:"nb_level"`
NR bool `json:"nr"`
NRLevel int `json:"nr_level"`
ANF bool `json:"anf"`
APF bool `json:"apf"` // audio peak filter (CW only)
AGC string `json:"agc,omitempty"` // FAST | MID | SLOW
Preamp int `json:"preamp"` // 0=off, 1=P.AMP1, 2=P.AMP2
Att int `json:"att"` // dB attenuation, 0=off
Filter int `json:"filter"` // 1 | 2 | 3 (FIL1/2/3)
RFPower int `json:"rf_power"` // 0-100 (TX output)
MicGain int `json:"mic_gain"` // 0-100
AFGain int `json:"af_gain"`
RFGain int `json:"rf_gain"`
NB bool `json:"nb"`
NBLevel int `json:"nb_level"`
NR bool `json:"nr"`
NRLevel int `json:"nr_level"`
ANF bool `json:"anf"`
APF bool `json:"apf"` // audio peak filter (CW only)
AGC string `json:"agc,omitempty"` // FAST | MID | SLOW
Preamp int `json:"preamp"` // 0=off, 1=P.AMP1, 2=P.AMP2
Att int `json:"att"` // dB attenuation, 0=off
Filter int `json:"filter"` // 1 | 2 | 3 (FIL1/2/3)
// Antenna (IC-7610 = ANT1/ANT2).
Antenna int `json:"antenna"` // 1 | 2 (0 = unknown)
// Filter fine controls: Twin PBT + manual notch (0-100, 50 = centre).
@@ -510,20 +535,20 @@ type IcomController interface {
SetMicGain(int) error
SetIcomSplit(bool) error
TuneATU() error
SetScope(bool) error // enable/disable the spectrum-scope waveform stream
SetScopeMode(bool) error // true = fixed span, false = center-on-VFO
SetScope(bool) error // enable/disable the spectrum-scope waveform stream
SetScopeMode(bool) error // true = fixed span, false = center-on-VFO
SetScopeEdges(int64, int64) error // point the fixed scope at low..high Hz (centre/pan)
ScopeData() ScopeSweep // latest assembled sweep (empty until enabled)
SetRIT(int) error // RIT/ΔTX offset in signed Hz
SetRITOn(bool) error // RIT on/off
SetXITOn(bool) error // ΔTX (XIT) on/off
SendCW(string) error // key a CW message via the rig's keyer (CI-V 0x17)
StopCW() error // abort the CW message being sent
SetKeySpeed(int) error // CW keyer speed in WPM
SetBreakIn(int) error // CW break-in: 0=OFF, 1=SEMI, 2=FULL
SetAntenna(int) error // 1 = ANT1, 2 = ANT2
SetPBTInner(int) error // Twin PBT inside (0-100, 50 = centre)
SetPBTOuter(int) error // Twin PBT outside (0-100, 50 = centre)
ScopeData() ScopeSweep // latest assembled sweep (empty until enabled)
SetRIT(int) error // RIT/ΔTX offset in signed Hz
SetRITOn(bool) error // RIT on/off
SetXITOn(bool) error // ΔTX (XIT) on/off
SendCW(string) error // key a CW message via the rig's keyer (CI-V 0x17)
StopCW() error // abort the CW message being sent
SetKeySpeed(int) error // CW keyer speed in WPM
SetBreakIn(int) error // CW break-in: 0=OFF, 1=SEMI, 2=FULL
SetAntenna(int) error // 1 = ANT1, 2 = ANT2
SetPBTInner(int) error // Twin PBT inside (0-100, 50 = centre)
SetPBTOuter(int) error // Twin PBT outside (0-100, 50 = centre)
SetManualNotch(bool) error
SetNotchPos(int) error // manual-notch position (0-100, 50 = centre)
SetSquelch(int) error
+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) ──