chore: release v0.19.2

This commit is contained in:
2026-07-09 16:51:57 +02:00
parent 5ae2bad549
commit 19c2de5f61
37 changed files with 1395 additions and 73 deletions
+94
View File
@@ -38,6 +38,8 @@ type Flex struct {
slices map[int]*flexSlice
tx flexTX // transmit/ATU state pushed by the radio (FlexRadio tab)
amp flexAmp // external amplifier (PowerGenius XL) state
micProfiles []string // available mic profiles (SmartSDR "profile mic list")
micProfile string // currently loaded mic profile
txSetAt map[string]time.Time // status field → when WE last set it (ignore the radio's lagging echo briefly)
lastStateSig string // last logged derived-state signature (log only on change)
boundClientID string // GUI client (SmartSDR) we bound to; "" until bound. Binding lets this non-GUI client receive GUI-tied data (CW pitch/speed, break-in delay, RF power).
@@ -85,6 +87,8 @@ type flexSlice struct {
anfLevel int
apf bool // CW audio peaking filter
apfLevel int
wnb bool // wideband noise blanker
wnbLevel int
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)
@@ -108,6 +112,8 @@ type flexTX struct {
mon bool
monLevel int
micLevel int
filterLow int // TX filter low cut (Hz)
filterHigh int // TX filter high cut (Hz)
atuStatus string
atuMemories bool
// CW keyer params (set via the top-level "cw" commands).
@@ -209,6 +215,8 @@ func (f *Flex) Connect() error {
f.send("sub amplifier all") // external amplifier (PowerGenius XL) operate/standby
f.send("sub radio all") // radio-wide incl. interlock (TX/RX state)
f.send("sub cwx all") // CWX: the LIVE CW speed/pitch/break-in (transmit holds only a static default)
f.send("sub profile all") // mic/global/tx profiles (for the mic-profile dropdown)
f.send("profile mic info") // request the current mic profile list + selection
f.send("sub client all") // learn the GUI client (SmartSDR) so we can bind to it (below)
f.startMeters(conn) // open the UDP VITA-49 stream for live meters
if f.spotsEnabled {
@@ -445,6 +453,12 @@ func (f *Flex) handleStatus(payload string) {
f.tx.cwBreakInDelay = atoiDefault(val, f.tx.cwBreakInDelay)
case "mic_level", "miclevel":
f.tx.micLevel = atoiDefault(val, f.tx.micLevel)
// TX filter: the transmit STATUS reports the passband as lo/hi (the
// SET command is filter_low/filter_high — a SmartSDR quirk).
case "lo", "filter_low":
f.tx.filterLow = atoiDefault(val, f.tx.filterLow)
case "hi", "filter_high":
f.tx.filterHigh = atoiDefault(val, f.tx.filterHigh)
}
}
f.mu.Unlock()
@@ -526,6 +540,35 @@ func (f *Flex) handleStatus(payload string) {
}
f.mu.Unlock()
}
// Mic-profile object — "profile mic list=A^B^C" (available profiles) and
// "profile mic current=<name>" (loaded one). Names can contain spaces, so
// values are taken from the raw payload after the key. Logged once so the
// exact field names are confirmable on real hardware.
if len(fields) >= 2 && fields[0] == "profile" && fields[1] == "mic" {
debugLog.Printf("Flex: profile status: %s", payload)
if i := strings.Index(payload, "list="); i >= 0 {
var profs []string
for _, p := range strings.Split(payload[i+len("list="):], "^") {
if p = strings.TrimSpace(p); p != "" {
profs = append(profs, p)
}
}
f.mu.Lock()
f.micProfiles = profs
f.mu.Unlock()
}
// The loaded profile arrives as current=<name> (some firmwares:
// selection=<name>); accept either.
for _, key := range []string{"current=", "selection="} {
if i := strings.Index(payload, key); i >= 0 {
cur := strings.TrimSpace(payload[i+len(key):])
f.mu.Lock()
f.micProfile = cur
f.mu.Unlock()
break
}
}
}
// Interlock object — transmit state (RECEIVE / TRANSMITTING / …).
if len(fields) >= 1 && fields[0] == "interlock" {
f.mu.Lock()
@@ -744,6 +787,10 @@ func (f *Flex) handleStatus(payload string) {
s.apf = val == "1"
case "apf_level":
s.apfLevel = atoiDefault(val, s.apfLevel)
case "wnb":
s.wnb = val == "1"
case "wnb_level":
s.wnbLevel = atoiDefault(val, s.wnbLevel)
case "filter_lo":
s.filterLo = atoiDefault(val, s.filterLo)
case "filter_hi":
@@ -1164,6 +1211,10 @@ func (f *Flex) FlexState() FlexTXState {
Mon: f.tx.mon,
MonLevel: f.tx.monLevel,
MicLevel: f.tx.micLevel,
TXFilterLow: f.tx.filterLow,
TXFilterHigh: f.tx.filterHigh,
MicProfile: f.micProfile,
MicProfiles: f.micProfiles,
ATUStatus: f.tx.atuStatus,
ATUMemories: f.tx.atuMemories,
// CW keyer (defaults applied so the sliders show sane values pre-read).
@@ -1214,6 +1265,8 @@ func (f *Flex) FlexState() FlexTXState {
st.ANFLevel = rx.anfLevel
st.APF = rx.apf
st.APFLevel = rx.apfLevel
st.WNB = rx.wnb
st.WNBLevel = rx.wnbLevel
st.FilterLo = rx.filterLo
st.FilterHi = rx.filterHi
st.RXAnt = rx.rxAnt
@@ -1391,6 +1444,8 @@ 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)) }
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)) }
// ── CW keyer controls (top-level "cw" commands) ──
@@ -1608,6 +1663,45 @@ func (f *Flex) SetMic(l int) error {
return f.txSet(fmt.Sprintf("transmit set miclevel=%d", l), "mic_level", func(t *flexTX) { t.micLevel = l })
}
// SetTXFilter sets the transmit-audio bandwidth (low + high cut, in Hz). SmartSDR
// clamps to legal values per mode; we just pass them through in one command so
// both edges move together.
func (f *Flex) SetTXFilter(low, high int) error {
if low < 0 {
low = 0
}
if high < low {
high = low
}
// Guard both status field names (the echo comes back as lo/hi, not
// filter_low/high) so the radio's lagging echo doesn't snap the inputs back.
f.mu.Lock()
f.txSetAt["hi"] = time.Now()
f.mu.Unlock()
return f.txSet(fmt.Sprintf("transmit set filter_low=%d filter_high=%d", low, high), "lo",
func(t *flexTX) { t.filterLow, t.filterHigh = low, high })
}
// SetMicProfile loads a SmartSDR mic profile by name (quoted, as names may hold
// spaces). Optimistically caches the selection.
func (f *Flex) SetMicProfile(name string) error {
name = strings.TrimSpace(name)
if name == "" {
return fmt.Errorf("flex: empty mic profile")
}
f.mu.Lock()
connected := f.conn != nil
if connected {
f.micProfile = name
}
f.mu.Unlock()
if !connected {
return fmt.Errorf("flex: not connected")
}
f.send(fmt.Sprintf("profile mic load \"%s\"", name))
return nil
}
func (f *Flex) ATUStart() error {
if !f.connected() {
return fmt.Errorf("flex: not connected")