diff --git a/app.go b/app.go
index 84037f5..755ac9d 100644
--- a/app.go
+++ b/app.go
@@ -8270,6 +8270,22 @@ func (a *App) FlexSetSplit(on bool) error {
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetSplit(on) })
}
+// FlexSetActiveSlice focuses a slice (A/B/C/D…) so all commands target it.
+func (a *App) FlexSetActiveSlice(idx int) error {
+ if a.cat == nil {
+ return fmt.Errorf("cat not initialized")
+ }
+ return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetActiveSlice(idx) })
+}
+
+// FlexSetTXSlice makes a slice the transmitter (e.g. TX on the active slice).
+func (a *App) FlexSetTXSlice(idx int) error {
+ if a.cat == nil {
+ return fmt.Errorf("cat not initialized")
+ }
+ return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetTXSlice(idx) })
+}
+
// keyFlexBandAnt stores the per-band RX/TX antenna map (global, machine-local).
const keyFlexBandAnt = "flex.band_antennas"
diff --git a/frontend/src/components/FlexPanel.tsx b/frontend/src/components/FlexPanel.tsx
index e03015a..1512caa 100644
--- a/frontend/src/components/FlexPanel.tsx
+++ b/frontend/src/components/FlexPanel.tsx
@@ -5,7 +5,7 @@ import {
FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic,
FlexMox, FlexAmpOperate,
GetPGXLStatus, PGXLSetFanMode,
- FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna, FlexSetSplit,
+ FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna, FlexSetSplit, FlexSetActiveSlice, FlexSetTXSlice,
FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel,
FlexSetAPF, FlexSetAPFLevel, FlexSetCWSpeed, FlexSetCWPitch, FlexSetCWBreakInDelay,
FlexSetCWSidetone, FlexSetSidetoneLevel, FlexSetCWFilter, FlexSetFilter,
@@ -30,8 +30,10 @@ type FlexState = {
apf: boolean; apf_level: number; filter_lo: number; filter_hi: number;
amp_available: boolean; amp_model?: string; amp_operate: boolean; amp_fault?: string;
meters?: Meter[];
+ slices?: FlexSlice[];
};
+type FlexSlice = { index: number; letter: string; freq_hz: number; mode?: string; band?: string; active: boolean; tx: boolean };
type Meter = { id: number; src?: string; name?: string; unit?: string; value: number; lo: number; hi: number };
const ZERO: FlexState = {
@@ -280,6 +282,37 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
+ {/* Slices A/B/C/D — every in-use receiver. Click one to make it the
+ ACTIVE slice: the main frequency, mode, DSP and spot-clicks all follow
+ it. The active slice is highlighted; the TX slice is flagged. */}
+ {!!st.slices && st.slices.length > 0 && (
+
+ {st.slices.map((sl) => (
+
+ ))}
+
+ )}
+
{off && (
{t('flxp.waiting')}
diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx
index ba048f3..eec41ea 100644
--- a/frontend/src/lib/i18n.tsx
+++ b/frontend/src/lib/i18n.tsx
@@ -191,7 +191,7 @@ const en: Dict = {
'wkp.autoCallHint': 'Click a CQ macro (one whose text contains CQ) to resend it on a loop — message, gap, repeat — until you send another macro (e.g. a report), press Stop, or hit ESC. Non-CQ macros send once.', 'wkp.autoCall': 'Auto-call', 'wkp.gap': 'gap', 'wkp.gapHint': 'Seconds to wait after the message before resending', 'wkp.loopHint': 'click a CQ macro to loop it', 'wkp.macroN': 'Macro {n}',
'dvkp.voiceKeyer': 'Voice keyer', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Disable voice keyer', 'dvkp.noMsgPre': 'No messages recorded yet. Open', 'dvkp.settingsPath': 'Settings → Audio devices & voice keyer', 'dvkp.noMsgPost': 'to record F1–F6.', 'dvkp.transmit': 'Transmit F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — empty', 'dvkp.message': 'message',
'agp.portDeselect': 'Port {letter} — click to deselect', 'agp.portSelect': 'Select on port {letter}', 'agp.online': 'online', 'agp.offline': 'offline', 'agp.close': 'Close', 'agp.connecting': 'Connecting…', 'agp.noAntennas': 'No antennas configured.',
- 'flxp.smartsdrRemote': 'SmartSDR remote control', 'flxp.offline': 'OFFLINE', 'flxp.waiting': 'Waiting for the FlexRadio… (set CAT to FlexRadio and connect)', 'flxp.transmit': 'Transmit', 'flxp.rfPower': 'RF Power', 'flxp.tunePwr': 'Tune Pwr', 'flxp.splitHint': 'Split: RX/TX on separate slices. ON creates a TX slice +1 kHz (CW) / +5 kHz (SSB) up, like SmartSDR.', 'flxp.voxDly': 'VOX Dly', 'flxp.speed': 'Speed', 'flxp.pitch': 'Pitch', 'flxp.delay': 'Delay',
+ 'flxp.smartsdrRemote': 'SmartSDR remote control', 'flxp.offline': 'OFFLINE', 'flxp.waiting': 'Waiting for the FlexRadio… (set CAT to FlexRadio and connect)', 'flxp.transmit': 'Transmit', 'flxp.rfPower': 'RF Power', 'flxp.tunePwr': 'Tune Pwr', 'flxp.splitHint': 'Split: RX/TX on separate slices. ON creates a TX slice +1 kHz (CW) / +5 kHz (SSB) up, like SmartSDR.', 'flxp.sliceHint': 'Click to make this the active slice — frequency, mode, DSP and spot-clicks all follow it.', 'flxp.txSlice': 'This slice transmits', 'flxp.setTxSlice': 'Move TX to this slice (transmit here)', 'flxp.voxDly': 'VOX Dly', 'flxp.speed': 'Speed', 'flxp.pitch': 'Pitch', 'flxp.delay': 'Delay',
'flxp.receiveActive': 'Receive (active slice)', 'flxp.muted': 'Muted — click to unmute', 'flxp.mute': 'Mute RX audio', 'flxp.filter': 'Filter', 'flxp.amplifier': 'Amplifier', 'flxp.ampInLine': 'Amplifier is in line (transmitting through PA).', 'flxp.ampBypassed': 'Amplifier bypassed (standby).', 'flxp.pgConnected': 'PowerGenius connected', 'flxp.pgOffline': 'PowerGenius offline', 'flxp.fan': 'Fan', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Broadcast', 'flxp.fault': 'FAULT', 'flxp.meters': 'Meters', 'flxp.noMeters': "No meters yet — waiting for the radio's UDP stream…", 'flxp.amplifierHdr': 'AMPLIFIER',
'icmp.spectrum': 'Spectrum', 'icmp.scopeFixed': 'Fixed — double-click / wheel to tune', 'icmp.scopeCenter': 'Center — follows VFO', 'icmp.scopeOff': 'Scope off', 'icmp.scopePanDown': 'Shift scope −50 kHz', 'icmp.scopePanUp': 'Shift scope +50 kHz', 'icmp.scopeCenterVfo': 'Center scope on the current frequency (±50 kHz)', 'icmp.notConnected': "Icom not connected. Enable the Icom CI-V backend in Settings → CAT and connect the radio's USB port.", 'icmp.refresh': 'Refresh', 'icmp.meters': 'Meters', 'icmp.transmit': 'Transmit', 'icmp.power': 'Power', 'icmp.mic': 'Mic', 'icmp.receive': 'Receive', 'icmp.preamp': 'Preamp', 'icmp.filter': 'Filter', 'icmp.noiseNotch': 'Noise / Notch', 'icmp.autoNotch': 'Auto notch filter', 'icmp.apf': 'Audio peak filter (CW)', 'icmp.clarifiers': 'RIT / ΔTX', 'icmp.ritHint': 'Wheel or ± to shift · Ctrl+←/→ shifts RIT when active', 'icmp.bandsAntenna': 'Bands & Antenna', 'icmp.antenna': 'Antenna', 'icmp.passband': 'Passband / Notch', 'icmp.pbtCenter': 'Center PBT', 'icmp.manualNotch': 'Manual notch — MN on, then set position', 'icmp.squelch': 'Squelch', 'icmp.powerOnHint': 'Power the radio ON (boots ~15 s)', 'icmp.powerOffHint': 'Power the radio OFF', 'icmp.powerOffConfirm': 'Switch the radio OFF?',
'rst.clickToFill': 'Click to set RST tx from the signal',
@@ -377,7 +377,7 @@ const fr: Dict = {
'wkp.autoCallHint': "Clique une macro CQ (dont le texte contient CQ) pour la réémettre en boucle — message, pause, répétition — jusqu'à envoyer une autre macro (ex. un report), appuyer sur Stop ou ESC. Les macros non-CQ ne sont émises qu'une fois.", 'wkp.autoCall': 'Appel auto', 'wkp.gap': 'pause', 'wkp.gapHint': 'Secondes à attendre après le message avant de réémettre', 'wkp.loopHint': 'clique une macro CQ pour la boucler', 'wkp.macroN': 'Macro {n}',
'dvkp.voiceKeyer': 'Manipulateur vocal', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Désactiver le manipulateur vocal', 'dvkp.noMsgPre': 'Aucun message enregistré. Ouvre', 'dvkp.settingsPath': 'Réglages → Périphériques audio & manipulateur vocal', 'dvkp.noMsgPost': 'pour enregistrer F1–F6.', 'dvkp.transmit': 'Émettre F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — vide', 'dvkp.message': 'message',
'agp.portDeselect': 'Port {letter} — clic pour désélectionner', 'agp.portSelect': 'Sélectionner sur le port {letter}', 'agp.online': 'en ligne', 'agp.offline': 'hors ligne', 'agp.close': 'Fermer', 'agp.connecting': 'Connexion…', 'agp.noAntennas': 'Aucune antenne configurée.',
- 'flxp.smartsdrRemote': 'Contrôle à distance SmartSDR', 'flxp.offline': 'HORS LIGNE', 'flxp.waiting': 'En attente du FlexRadio… (règle le CAT sur FlexRadio et connecte)', 'flxp.transmit': 'Émission', 'flxp.rfPower': 'Puissance RF', 'flxp.tunePwr': 'Puiss. TUNE', 'flxp.splitHint': 'Split : RX/TX sur des slices séparées. ON crée une slice TX +1 kHz (CW) / +5 kHz (SSB) au-dessus, comme SmartSDR.', 'flxp.voxDly': 'Délai VOX', 'flxp.speed': 'Vitesse', 'flxp.pitch': 'Tonalité', 'flxp.delay': 'Délai',
+ 'flxp.smartsdrRemote': 'Contrôle à distance SmartSDR', 'flxp.offline': 'HORS LIGNE', 'flxp.waiting': 'En attente du FlexRadio… (règle le CAT sur FlexRadio et connecte)', 'flxp.transmit': 'Émission', 'flxp.rfPower': 'Puissance RF', 'flxp.tunePwr': 'Puiss. TUNE', 'flxp.splitHint': 'Split : RX/TX sur des slices séparées. ON crée une slice TX +1 kHz (CW) / +5 kHz (SSB) au-dessus, comme SmartSDR.', 'flxp.sliceHint': 'Cliquer pour rendre cette slice active — fréquence, mode, DSP et clics de spot la suivent tous.', 'flxp.txSlice': 'Cette slice émet', 'flxp.setTxSlice': 'Déplacer le TX sur cette slice (émettre ici)', 'flxp.voxDly': 'Délai VOX', 'flxp.speed': 'Vitesse', 'flxp.pitch': 'Tonalité', 'flxp.delay': 'Délai',
'flxp.receiveActive': 'Réception (slice active)', 'flxp.muted': 'Coupé — clic pour rétablir', 'flxp.mute': "Couper l'audio RX", 'flxp.filter': 'Filtre', 'flxp.amplifier': 'Amplificateur', 'flxp.ampInLine': 'Amplificateur en ligne (émission via le PA).', 'flxp.ampBypassed': 'Amplificateur en bypass (standby).', 'flxp.pgConnected': 'PowerGenius connecté', 'flxp.pgOffline': 'PowerGenius hors ligne', 'flxp.fan': 'Ventilo', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Diffusion', 'flxp.fault': 'DÉFAUT', 'flxp.meters': 'Mesures', 'flxp.noMeters': 'Aucune mesure — en attente du flux UDP de la radio…', 'flxp.amplifierHdr': 'AMPLIFICATEUR',
'icmp.spectrum': 'Spectre', 'icmp.scopeFixed': 'Fixe — double-clic / molette pour accorder', 'icmp.scopeCenter': 'Centré — suit le VFO', 'icmp.scopeOff': 'Scope éteint', 'icmp.scopePanDown': 'Décaler le scope −50 kHz', 'icmp.scopePanUp': 'Décaler le scope +50 kHz', 'icmp.scopeCenterVfo': 'Centrer le scope sur la fréquence actuelle (±50 kHz)', 'icmp.notConnected': 'Icom non connecté. Active le backend CI-V Icom dans Réglages → CAT et connecte le port USB de la radio.', 'icmp.refresh': 'Rafraîchir', 'icmp.meters': 'Mesures', 'icmp.transmit': 'Émission', 'icmp.power': 'Puissance', 'icmp.mic': 'Micro', 'icmp.receive': 'Réception', 'icmp.preamp': 'Préampli', 'icmp.filter': 'Filtre', 'icmp.noiseNotch': 'Bruit / Notch', 'icmp.autoNotch': 'Filtre notch auto', 'icmp.apf': 'Filtre de pic audio (CW)', 'icmp.clarifiers': 'RIT / ΔTX', 'icmp.ritHint': 'Molette ou ± pour décaler · Ctrl+←/→ décale le RIT si actif', 'icmp.bandsAntenna': 'Bandes & Antenne', 'icmp.antenna': 'Antenne', 'icmp.passband': 'Passe-bande / Notch', 'icmp.pbtCenter': 'Centrer PBT', 'icmp.manualNotch': 'Notch manuel — active MN, puis règle la position', 'icmp.squelch': 'Squelch', 'icmp.powerOnHint': 'Allumer la radio (démarre en ~15 s)', 'icmp.powerOffHint': 'Éteindre la radio', 'icmp.powerOffConfirm': 'Éteindre la radio ?',
'rst.clickToFill': 'Clic pour remplir le RST tx depuis le signal',
diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts
index 77cda9d..35c68c7 100644
--- a/frontend/wailsjs/go/main/App.d.ts
+++ b/frontend/wailsjs/go/main/App.d.ts
@@ -172,6 +172,8 @@ export function FlexSetAPFLevel(arg1:number):Promise;
export function FlexSetATUMemories(arg1:boolean):Promise;
+export function FlexSetActiveSlice(arg1:number):Promise;
+
export function FlexSetAudioLevel(arg1:number):Promise;
export function FlexSetCWBreakInDelay(arg1:number):Promise;
@@ -216,6 +218,8 @@ export function FlexSetSplit(arg1:boolean):Promise;
export function FlexSetTXAntenna(arg1:string):Promise;
+export function FlexSetTXSlice(arg1:number):Promise;
+
export function FlexSetTunePower(arg1:number):Promise;
export function FlexSetVox(arg1:boolean):Promise;
diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js
index 93bdceb..abf20e8 100644
--- a/frontend/wailsjs/go/main/App.js
+++ b/frontend/wailsjs/go/main/App.js
@@ -306,6 +306,10 @@ export function FlexSetATUMemories(arg1) {
return window['go']['main']['App']['FlexSetATUMemories'](arg1);
}
+export function FlexSetActiveSlice(arg1) {
+ return window['go']['main']['App']['FlexSetActiveSlice'](arg1);
+}
+
export function FlexSetAudioLevel(arg1) {
return window['go']['main']['App']['FlexSetAudioLevel'](arg1);
}
@@ -394,6 +398,10 @@ export function FlexSetTXAntenna(arg1) {
return window['go']['main']['App']['FlexSetTXAntenna'](arg1);
}
+export function FlexSetTXSlice(arg1) {
+ return window['go']['main']['App']['FlexSetTXSlice'](arg1);
+}
+
export function FlexSetTunePower(arg1) {
return window['go']['main']['App']['FlexSetTunePower'](arg1);
}
diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts
index 59c6aa8..716eb17 100644
--- a/frontend/wailsjs/go/models.ts
+++ b/frontend/wailsjs/go/models.ts
@@ -541,9 +541,34 @@ export namespace cat {
this.callsign = source["callsign"];
}
}
+ export class FlexSliceInfo {
+ index: number;
+ letter: string;
+ freq_hz: number;
+ mode?: string;
+ band?: string;
+ active: boolean;
+ tx: boolean;
+
+ static createFrom(source: any = {}) {
+ return new FlexSliceInfo(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.index = source["index"];
+ this.letter = source["letter"];
+ this.freq_hz = source["freq_hz"];
+ this.mode = source["mode"];
+ this.band = source["band"];
+ this.active = source["active"];
+ this.tx = source["tx"];
+ }
+ }
export class FlexTXState {
available: boolean;
model?: string;
+ slices?: FlexSliceInfo[];
rf_power: number;
tune_power: number;
tune: boolean;
@@ -600,6 +625,7 @@ export namespace cat {
if ('string' === typeof source) source = JSON.parse(source);
this.available = source["available"];
this.model = source["model"];
+ this.slices = this.convertValues(source["slices"], FlexSliceInfo);
this.rf_power = source["rf_power"];
this.tune_power = source["tune_power"];
this.tune = source["tune"];
diff --git a/internal/cat/cat.go b/internal/cat/cat.go
index e380643..9cec63c 100644
--- a/internal/cat/cat.go
+++ b/internal/cat/cat.go
@@ -245,9 +245,24 @@ func (m *Manager) SendSpot(s SpotInfo) {
// FlexTXState is the FlexRadio transmit/ATU state surfaced to the dedicated
// FlexRadio control tab. Levels are 0-100. (Phase 1: controls + state pushed by
// the radio over TCP; live meters arrive over a separate UDP stream later.)
+// FlexSliceInfo identifies one FlexRadio receiver slice (A/B/C/D…) for the
+// panel, so the operator sees every slice and which one is active/TX.
+type FlexSliceInfo struct {
+ Index int `json:"index"` // 0-based slice index
+ Letter string `json:"letter"` // A, B, C, D…
+ FreqHz int64 `json:"freq_hz"`
+ Mode string `json:"mode,omitempty"` // ADIF mode
+ Band string `json:"band,omitempty"`
+ Active bool `json:"active"` // the focused/operating slice
+ TX bool `json:"tx"` // this slice transmits
+}
+
type FlexTXState struct {
Available bool `json:"available"` // backend is Flex and handshaked
Model string `json:"model,omitempty"`
+ // Slices lists every in-use receiver slice (A/B/C/D…) so the panel can show
+ // them all and highlight the active one. The active slice drives everything.
+ Slices []FlexSliceInfo `json:"slices,omitempty"`
RFPower int `json:"rf_power"`
TunePower int `json:"tune_power"`
Tune bool `json:"tune"` // tune carrier active
@@ -339,6 +354,8 @@ type FlexController interface {
SetMute(bool) error
SetRXAntenna(string) error
SetTXAntenna(string) error
+ SetActiveSlice(int) error // focus slice idx so commands target it
+ SetTXSlice(int) error // make slice idx the transmitter (tx=1)
SetSplit(bool) error
SetNB(bool) error
SetNBLevel(int) error
diff --git a/internal/cat/flex.go b/internal/cat/flex.go
index 9b55eae..26a7c10 100644
--- a/internal/cat/flex.go
+++ b/internal/cat/flex.go
@@ -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
}