feat: Management of multiple slices on Flexradio
This commit is contained in:
+169
-100
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user