fix: Bug showing split for flex while in different mode on same band

This commit is contained in:
2026-07-10 15:59:32 +02:00
parent 96390110f8
commit 6ab606fa54
3 changed files with 130 additions and 9 deletions
+38 -9
View File
@@ -919,9 +919,11 @@ func (f *Flex) txSliceLocked() *flexSlice {
// 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.
// freq) AND the SAME split class (both PHONE, or both CW) — 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; so is a same-band slice in a
// different mode (SSB + FT8) or a data mode (FT8/FT4/RTTY never split).
// Caller holds f.mu.
func (f *Flex) operatingLocked() (main, rx, tx *flexSlice) {
_, main = f.mainSliceLocked()
@@ -933,21 +935,31 @@ func (f *Flex) operatingLocked() (main, rx, tx *flexSlice) {
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 {
// Split only applies to PHONE/CW; a data-mode TX slice never splits.
ct := flexSplitClass(txS.mode)
if ct == "" {
return main, nil, nil
}
// A split partner is an in-use, same-band slice at a different freq in the
// SAME split class (so SSB + FT8 on one band is NOT a split).
sameSplit := func(s *flexSlice) bool {
return s != nil && s.inUse && s != txS && s.freqHz != txS.freqHz &&
BandFromHz(s.freqHz) == bt && flexSplitClass(s.mode) == ct
}
// RX = the active slice when it qualifies, else the first other qualifying
// same-band slice.
if sameSplit(main) {
rx = main
} else {
for _, idx := range f.sortedSliceIdxLocked() {
s := f.slices[idx]
if s.inUse && s != txS && s.freqHz != txS.freqHz && BandFromHz(s.freqHz) == bt {
if s := f.slices[idx]; sameSplit(s) {
rx = s
break
}
}
}
if rx == nil {
return main, nil, nil // tx slice alone (simplex) → not split
return main, nil, nil // tx slice alone (simplex) or no same-mode partner → not split
}
return main, rx, txS
}
@@ -1921,6 +1933,23 @@ func parseFloatDefault(s string, def float64) float64 {
}
// flexModeToADIF maps a Flex slice mode to a generic ADIF mode.
// flexSplitClass groups a raw Flex slice mode into a split-capable class. A
// genuine split (one RX freq, one TX freq, SAME mode) only makes sense for PHONE
// and CW pileups. Data modes (FT8/FT4/RTTY all report as DIGU/DIGL/RTTY on a
// Flex) and digital voice never operate split, so they return "" (not
// split-capable). Two same-band slices whose classes differ (e.g. SSB + FT8) are
// independent operations, not a split.
func flexSplitClass(rawMode string) string {
switch flexModeToADIF(rawMode) {
case "SSB", "AM", "FM":
return "PHONE"
case "CW":
return "CW"
default: // DATA, RTTY, DIGITALVOICE, "" → never split
return ""
}
}
func flexModeToADIF(m string) string {
switch strings.ToUpper(strings.TrimSpace(m)) {
case "USB", "LSB":
+49
View File
@@ -0,0 +1,49 @@
package cat
import "testing"
// A genuine split is two same-band slices in the SAME split class (both PHONE or
// both CW). Two same-band slices in different modes (SSB + FT8), or in a data
// mode (FT8/FT4/RTTY = DIGU/DIGL), must NOT read as split.
func TestOperatingSplitMode(t *testing.T) {
mk := func(hz int64, mode string, active, tx bool) *flexSlice {
return &flexSlice{freqHz: hz, mode: mode, active: active, tx: tx, inUse: true}
}
cases := []struct {
name string
slices map[int]*flexSlice
wantSplit bool
}{
{"phone split (both USB)", map[int]*flexSlice{
0: mk(14_100_000, "USB", true, false),
1: mk(14_200_000, "USB", false, true),
}, true},
{"CW split (both CW)", map[int]*flexSlice{
0: mk(7_010_000, "CW", true, false),
1: mk(7_025_000, "CW", false, true),
}, true},
{"SSB + FT8 same band → not split", map[int]*flexSlice{
0: mk(14_100_000, "USB", true, false),
1: mk(14_074_000, "DIGU", false, true),
}, false},
{"both FT8 (DIGU) same band → not split", map[int]*flexSlice{
0: mk(14_074_000, "DIGU", true, false),
1: mk(14_080_000, "DIGU", false, true),
}, false},
{"different bands → not split", map[int]*flexSlice{
0: mk(14_100_000, "USB", true, false),
1: mk(7_100_000, "LSB", false, true),
}, false},
{"simplex (single slice) → not split", map[int]*flexSlice{
0: mk(14_100_000, "USB", true, true),
}, false},
}
for _, c := range cases {
f := &Flex{slices: c.slices}
_, rx, tx := f.operatingLocked()
gotSplit := rx != nil && tx != nil
if gotSplit != c.wantSplit {
t.Errorf("%s: split=%v, want %v", c.name, gotSplit, c.wantSplit)
}
}
}