chore: release v0.19.2
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user