feat: Management of multiple slices on Flexradio

This commit is contained in:
2026-07-07 19:35:25 +02:00
parent 6b9307b427
commit 6db90abcad
8 changed files with 276 additions and 103 deletions
+169 -100
View File
@@ -774,21 +774,19 @@ func (f *Flex) ReadState() (RigState, error) {
if !f.gotHandle {
return st, nil // connected TCP but radio hasn't handshaked yet
}
rx, tx := f.pickSlicesLocked()
if rx == nil && tx == nil {
main, rxS, txSplit := f.operatingLocked()
if main == nil {
return st, nil
}
if tx == nil {
tx = rx
}
if rx == nil {
rx = tx
}
st.FreqHz = tx.freqHz
st.Mode = flexModeToADIF(tx.mode)
if rx.freqHz != tx.freqHz {
// Main frequency/mode = the ACTIVE slice (what the operator is on). Only a
// genuine same-band split adds a separate TX freq; then ADIF convention wants
// FreqHz = TX and RxFreqHz = RX.
st.FreqHz = main.freqHz
st.Mode = flexModeToADIF(main.mode)
if rxS != nil && txSplit != nil {
st.Split = true
st.RxFreqHz = rx.freqHz
st.RxFreqHz = rxS.freqHz
st.FreqHz = txSplit.freqHz
}
sig := fmt.Sprintf("%d/%d/%v/%s", st.FreqHz, st.RxFreqHz, st.Split, st.Mode)
if sig != f.lastStateSig {
@@ -798,76 +796,130 @@ func (f *Flex) ReadState() (RigState, error) {
return st, nil
}
// pickSlicesLocked chooses the TX and RX slices among in-use slices. TX is the
// slice flagged tx=1. RX is the slice you actually receive on — the NON-TX slice
// (preferring the active/focused one), NOT simply the active slice: tuning the
// TX slice makes it the active/focused slice, which would otherwise collapse RX
// onto TX and hide the split. Caller holds f.mu.
func (f *Flex) pickSlicesLocked() (rx, tx *flexSlice) {
idxs := make([]int, 0, len(f.slices))
for i, s := range f.slices {
if s.inUse {
idxs = append(idxs, i)
}
}
sort.Ints(idxs)
var active, txS, nonTx, first *flexSlice
for _, i := range idxs {
s := f.slices[i]
if first == nil {
first = s
}
if s.active {
active = s
}
if s.tx {
txS = s
} else if nonTx == nil {
nonTx = s
}
}
tx = txS
if tx == nil {
if active != nil {
tx = active
} else {
tx = first
}
}
// RX = the receive slice: the active one if it isn't the TX slice, else the
// first non-TX slice; fall back to TX (simplex) when there's only one slice.
switch {
case active != nil && active != tx:
rx = active
case nonTx != nil:
rx = nonTx
default:
rx = tx
}
return rx, tx
}
// activeSliceIndexLocked returns the slice index to send commands to (the active
// slice, else the lowest in-use index, else 0). Caller holds f.mu.
func (f *Flex) activeSliceIndexLocked() int {
best, found := 1<<30, false
// mainSliceLocked is the operator's slice: the ACTIVE (focused) in-use slice, or
// the lowest-indexed in-use slice when none is flagged active. EVERYTHING the
// user does — freq/mode display, RX DSP, tuning, mode changes, spot clicks —
// follows this slice, so a second independent slice (e.g. monitoring another
// band) never hijacks the main frequency. Returns (-1, nil) when no slice is in
// use. Caller holds f.mu.
func (f *Flex) mainSliceLocked() (int, *flexSlice) {
best, bestS := 1<<30, (*flexSlice)(nil)
for idx, s := range f.slices {
if !s.inUse {
continue
}
if s.active {
return idx
return idx, s
}
if idx < best {
best, found = idx, true
best, bestS = idx, s
}
}
if found {
return best
if bestS != nil {
return best, bestS
}
return -1, nil
}
// activeSliceIndexLocked returns the slice index to send commands to (the main
// slice, else 0). Caller holds f.mu.
func (f *Flex) activeSliceIndexLocked() int {
if idx, _ := f.mainSliceLocked(); idx >= 0 {
return idx
}
return 0
}
// sliceLetter maps a slice index to its SmartSDR letter (0→A, 1→B, …).
func sliceLetter(idx int) string {
if idx < 0 || idx > 25 {
return fmt.Sprintf("%d", idx)
}
return string(rune('A' + idx))
}
// txSliceLocked returns the slice flagged as the transmitter (tx=1), or nil.
// Caller holds f.mu.
func (f *Flex) txSliceLocked() *flexSlice {
for _, s := range f.slices {
if s.inUse && s.tx {
return s
}
}
return nil
}
// operatingLocked resolves the operator's slices: the MAIN (active) slice for the
// mode/display, and — ONLY for a GENUINE split — the RX and TX slices. Split is
// the tx-flagged slice PLUS a distinct in-use slice on the SAME band (different
// freq) — detected from the pair itself, NOT from which slice is active (the TX
// slice often steals focus right after "slice create", which must NOT read as
// "no split"). A slice on another band is an independent receiver, ignored.
// Caller holds f.mu.
func (f *Flex) operatingLocked() (main, rx, tx *flexSlice) {
_, main = f.mainSliceLocked()
txS := f.txSliceLocked()
if txS == nil {
return main, nil, nil
}
bt := BandFromHz(txS.freqHz)
if bt == "" {
return main, nil, nil
}
// RX = the active slice when it's a distinct same-band slice, else the first
// other in-use same-band slice.
if main != nil && main != txS && main.freqHz != txS.freqHz && BandFromHz(main.freqHz) == bt {
rx = main
} else {
for _, s := range f.slices {
if s.inUse && s != txS && s.freqHz != txS.freqHz && BandFromHz(s.freqHz) == bt {
rx = s
break
}
}
}
if rx == nil {
return main, nil, nil // tx slice alone (simplex) → not split
}
return main, rx, txS
}
// SetActiveSlice focuses slice idx on the radio so every subsequent command
// (freq / mode / DSP / spot click) targets it. Lets the operator pick the
// operating slice from OpsLog (like SliceLogger's A/B selector).
func (f *Flex) SetActiveSlice(idx int) error {
f.mu.Lock()
_, exists := f.slices[idx]
connected := f.conn != nil
f.mu.Unlock()
if !connected {
return fmt.Errorf("flex: not connected")
}
if !exists {
return fmt.Errorf("flex: no slice %d", idx)
}
f.send(fmt.Sprintf("slice s %d active=1", idx))
return nil
}
// SetTXSlice makes slice idx the transmitter (tx=1) — e.g. "put TX on the active
// slice" so you transmit where you're listening. Only one slice can be TX; the
// radio clears the flag on the others.
func (f *Flex) SetTXSlice(idx int) error {
f.mu.Lock()
_, exists := f.slices[idx]
connected := f.conn != nil
f.mu.Unlock()
if !connected {
return fmt.Errorf("flex: not connected")
}
if !exists {
return fmt.Errorf("flex: no slice %d", idx)
}
f.send(fmt.Sprintf("slice s %d tx=1", idx))
return nil
}
func (f *Flex) SetFrequency(hz int64) error {
if hz <= 0 {
return fmt.Errorf("flex: invalid frequency")
@@ -1048,19 +1100,10 @@ func clampLevel(v int) int {
return v
}
// rxSliceLocked returns the active RX slice and its index (-1 when none), using
// the same RX-selection rule as ReadState. Caller holds f.mu.
// rxSliceLocked returns the operator's (main/active) slice and its index — the
// slice every RX-DSP control and read targets. Caller holds f.mu.
func (f *Flex) rxSliceLocked() (int, *flexSlice) {
rx, _ := f.pickSlicesLocked()
if rx == nil {
return -1, nil
}
for i, s := range f.slices {
if s == rx {
return i, rx
}
}
return -1, rx
return f.mainSliceLocked()
}
// FlexState returns a snapshot of the radio's transmit/ATU state plus the active
@@ -1093,10 +1136,31 @@ func (f *Flex) FlexState() FlexTXState {
CWSidetone: f.tx.cwSidetone,
CWMonLevel: f.tx.cwMonLevel,
}
if rx, tx := f.pickSlicesLocked(); rx != nil && tx != nil && rx != tx && rx.freqHz != tx.freqHz {
if _, rxS, txSplit := f.operatingLocked(); rxS != nil && txSplit != nil {
st.Split = true
st.RXFreqHz = rx.freqHz
st.TXFreqHz = tx.freqHz
st.RXFreqHz = rxS.freqHz
st.TXFreqHz = txSplit.freqHz
}
// Every in-use slice (A/B/C/D…) so the panel shows them all and highlights the
// active/TX one — the active slice drives everything the operator does.
sidx := make([]int, 0, len(f.slices))
for i, s := range f.slices {
if s.inUse {
sidx = append(sidx, i)
}
}
sort.Ints(sidx)
for _, i := range sidx {
s := f.slices[i]
st.Slices = append(st.Slices, FlexSliceInfo{
Index: i,
Letter: sliceLetter(i),
FreqHz: s.freqHz,
Mode: flexModeToADIF(s.mode),
Band: BandFromHz(s.freqHz),
Active: s.active,
TX: s.tx,
})
}
if _, rx := f.rxSliceLocked(); rx != nil {
st.RXAvail = true
@@ -1229,35 +1293,40 @@ func (f *Flex) SetSplit(on bool) error {
f.mu.Unlock()
return fmt.Errorf("flex: not connected")
}
rx, tx := f.pickSlicesLocked()
// Split is built AROUND THE ACTIVE slice, and "already split" uses the SAME
// same-band-pair rule as the button state (operatingLocked) — otherwise two
// INDEPENDENT slices on different bands look "already split" and SPLIT ON does
// nothing (the bug the user hit: A on 20m + B on 80m).
main, rxS, txS := f.operatingLocked()
rxIdx, txIdx := -1, -1
for i, s := range f.slices {
if s == rx {
if s == rxS {
rxIdx = i
}
if s == tx {
if s == txS {
txIdx = i
}
}
alreadySplit := rx != nil && tx != nil && rx != tx
var rxFreq int64
var rxMode, rxAnt string
if rx != nil {
rxFreq, rxMode, rxAnt = rx.freqHz, rx.mode, rx.rxAnt
alreadySplit := rxS != nil && txS != nil
var baseFreq int64
var baseMode, baseAnt string
if main != nil {
baseFreq, baseMode, baseAnt = main.freqHz, main.mode, main.rxAnt
}
f.mu.Unlock()
if on {
if alreadySplit || rx == nil {
return nil // already split (or no slice)
if alreadySplit || main == nil {
return nil // already split, or no active slice
}
offset := int64(5000)
if strings.HasPrefix(strings.ToUpper(rxMode), "CW") {
if strings.HasPrefix(strings.ToUpper(baseMode), "CW") {
offset = 1000
}
cmd := fmt.Sprintf("slice create freq=%.6f mode=%s", float64(rxFreq+offset)/1e6, rxMode)
if rxAnt != "" {
cmd += " ant=" + rxAnt
// Create the TX slice at the ACTIVE slice's freq + offset (same band).
cmd := fmt.Sprintf("slice create freq=%.6f mode=%s", float64(baseFreq+offset)/1e6, baseMode)
if baseAnt != "" {
cmd += " ant=" + baseAnt
}
if seq := f.send(cmd); seq > 0 {
f.mu.Lock()
@@ -1270,10 +1339,10 @@ func (f *Flex) SetSplit(on bool) error {
return nil
}
if txIdx >= 0 {
f.send(fmt.Sprintf("slice remove %d", txIdx))
f.send(fmt.Sprintf("slice remove %d", txIdx)) // drop the extra TX slice
}
if rxIdx >= 0 {
f.send(fmt.Sprintf("slice s %d tx=1", rxIdx))
f.send(fmt.Sprintf("slice s %d tx=1", rxIdx)) // RX slice transmits again
}
return nil
}