fix(gui): show panadapter split-pair for externally-initiated split (#3726)#3794
Conversation
The panadapter's split-pair styling — red SPLIT on the RX VFO, SWAP on the TX VFO, and the spectrum split-pair marker — only appeared when split was enabled from the in-app SPLIT button. When split was established by an external controller (rigctld / CAT / TCI / front panel) or restored on reconnect, the panadapter showed nothing, even though TX was correctly on the split slice. Root cause: MainWindow::updateSplitState() gated the pair styling on the GUI-only flag m_splitActive (plus m_splitTxSliceId/m_splitRxSliceId), which is set true only by the three GUI-initiated split sites. An external split sets the model's TX slice (SliceModel::setTxSlice(true)) but never touches m_splitActive, so the renderer saw no split. The TX badge was correct only because it derives from model state (isTxSlice()). Fix (RFC aethersdr#3715 — consumers derive from model truth, not per-consumer state): - updateSplitState() now resolves, per panadapter, the TX slice and a distinct RX slice on the same pan; the TX slice renders SWAP, the RX slice renders red SPLIT, and SpectrumWidget::setSplitPair() is fed from the same derivation. The GUI flags still drive the SWAP/teardown *actions* (which need the GUI-created TX slice id), but no longer gate rendering. When the GUI tracks an RX slice it is preferred as the partner, so the SPLIT-button pairing is unchanged. - updateSplitState() is now re-triggered from model changes: each slice's SliceModel::txSliceChanged, plus the tail of onSliceAdded/onSliceRemoved, so an externally-initiated split (or one present on reconnect) repaints the panadapter without a GUI action. Proven with the agent automation bridge against the fixed build (RX-only, no TX), isolated on a unique bridge socket. Reading the "Split mode" badge text via dumpTree with m_splitActive == false throughout: - single slice -> badge SPLIT (ghosted); after a model-only `slice add`, badges become SWAP on the TX slice and SPLIT on the RX slice. - on reconnect with two slices already present (TX on A, RX on B), the panadapter immediately shows SWAP on A and red SPLIT on B — the reported externally-established-split scenario, now rendered. Refs aethersdr#3715. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks for this, @jensenpat — this is a clean, well-reasoned fix and the writeup (root cause, RFC #3715 alignment, and the bridge-proof tables) made review easy. The core change is right: gating the pair rendering on the GUI-only m_splitActive/m_splitTxSliceId/m_splitRxSliceId flags was the bug, and deriving from model truth (isTxSlice() + a distinct RX on the same pan) is the correct shape. CI is green across all six checks.
Things I checked and liked:
- Null-safety improved. The old loop did
s->sliceId()with no guard; the new code guardsif (!s ...)everywhere. Good. - Multi-pan correctness. Old
updateSplitState()only touchedspectrum(); the newm_panStack->allApplets()/spectrum()fallback mirrors the existingsyncTxWaterfallSliceToSpectrums()pattern exactly, so it now repaints every pan. Nice. - RX-partner preference order (GUI-tracked RX → active → first-found) is order-independent because
m_splitRxSliceIdalways wins, so the SPLIT-button pairing stays byte-for-byte as claimed. - Connection lifetime. The per-slice
connect(s, &SliceModel::txSliceChanged, this, ...)is safe — sender-destroyed and receiver-destroyed both auto-disconnect; no dangling.
One thing worth flagging (low severity, your call):
Transient pair lingering on GUI split teardown. SliceModel::setTxSlice() (SliceModel.cpp:451) is async — it only sends slice set N tx=… and isTxSlice() doesn't flip until the radio echoes back. In disableSplit() (MainWindow.cpp:6126), after setSplitPair(-1,-1) you now call updateSplitState(), which re-derives from the current model — where the old TX slice is still isTxSlice()==true and both slices are still present on the pan. So the new derivation will momentarily re-assert the SWAP/SPLIT badges and spectrum pair until the async slice remove completes and onSliceRemoved re-derives to clear them. Previously, m_splitActive=false cleared everything instantly. The result self-corrects within a radio round-trip, and it's arguably consistent with your own "two slices on a pan with TX → show the pair" behavioral note — so this may be acceptable as-is. Just calling it out since it's a (cosmetic) change on the well-trodden GUI teardown path; if it's visible in practice, a m_splitTeardownInProgress guard or suppressing the pair while the TX slice is pending-removal would address it.
Not a blocker — the fix is sound and addresses #3726 correctly. The two bridge-gap proposals (get pan exposing splitPartnerId, and a slice tx <id> verb) are good follow-ups; worth filing as their own issues so they don't get lost.
🤖 aethersdr-agent · cost: $2.3212 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Traced the derivation and wiring — correct and well-aligned with RFC #3715.
updateSplitState() now derives the split pair from model truth: txByPan (the radio-wide TX slice's pan) + a per-pan RX partner chosen by priority GUI-tracked RX → active → first found, so the SPLIT-button pairing is preserved byte-for-byte while external splits resolve a partner. Verified the auto*& chosen = rxByPan[...] reference is re-fetched each iteration with only its own in-iteration insert, so no rehash-invalidation. Multi-pan aware and self-clearing — a pan with no TX+RX pair gets updateSplitBadge(false,false) + setSplitPair(-1,-1), so teardown resets stale badges.
Re-trigger wiring is loop-free: updateSplitState() only renders (no model mutation), so the new txSliceChanged connection and the onSliceAdded/onSliceRemoved tail calls can't re-emit txSliceChanged or re-enter add/remove — worst case a harmless idempotent double-call.
The flagged behavioral change (any pan with TX + a distinct RX slice renders SWAP/SPLIT, not just SPLIT-button splits) is functionally accurate and the maintainer-endorsed #3715 derivation. And the slice tx verb this PR proposed was just landed in #3819, so the external-split transition is now drivable literally rather than approximated.
Clean fix, RX-only bridge-proven (m_splitActive==false throughout), no TX/safety surface. Thanks @jensenpat.
Problem
The panadapter's split-pair visualization — red SPLIT on the RX VFO, SWAP on the TX VFO, and the spectrum split-pair marker — only appeared when split was enabled from the in-app SPLIT button. When split was established by an external controller (rigctld / CAT / TCI / front panel), or was already present on reconnect, the panadapter showed nothing, even though TX was correctly on the split slice.
Fixes #3726.
Root cause
MainWindow::updateSplitState()gated the split-pair styling on the GUI-only flagm_splitActive(plusm_splitTxSliceId/m_splitRxSliceId):m_splitActiveis set true only by the three GUI-initiated split sites. An external split sets the model's TX slice viaSliceModel::setTxSlice(true)but never touches the flag, so the renderer saw no split. The TX badge was correct only because it derives from model state (isTxSlice()) — theSPLIT/SWAPpair badges and spectrum pairing were flag-driven, so every non-GUI split was invisible to the panadapter.Two compounding gaps: the rendering read GUI state instead of model truth, and nothing re-ran
updateSplitState()when the model's TX slice changed out-of-band.Fix
Per RFC #3715 (consumers derive from model truth, not per-consumer state):
updateSplitState()now resolves, per panadapter, the TX slice and a distinct RX slice on the same pan; the TX slice renders SWAP, the RX slice renders red SPLIT, andSpectrumWidget::setSplitPair()is fed from the same derivation (multi-pan aware). The GUI flags still drive the SWAP/teardown actions (which need the GUI-created TX slice id) but no longer gate rendering. When the GUI tracks an RX slice it's preferred as the partner, so the SPLIT-button pairing is byte-for-byte unchanged.updateSplitState()now also runs on each slice'sSliceModel::txSliceChanged, and at the tail ofonSliceAdded/onSliceRemoved— so an externally-initiated split (or one present on reconnect) repaints the panadapter without a GUI action.How the agent automation bridge proved it
RX-only (no TX keying), against the fixed build, isolated on a unique bridge socket (
AETHER_AUTOMATION_SOCKET) so the parallel apps sharing the Flex couldn't be mistaken for it. TheSplit modebadge text is read viadumpTree(aQPushButton's text is exposed as itsvalue).m_splitActive == falsethroughout — no SPLIT button was ever pressed.Run A — model-only
slice add(exercises thesliceAddedtrigger):[(B, SPLIT)]— ghosted, no pairslice add[(B, SWAP), (A, SPLIT)]— pair renderedRun B — reconnect into an externally-present split (the literal bug): on connect the model already had two slices (TX on A, RX on B), i.e. a split set up outside this GUI session:
[(B, SPLIT), (A, SWAP)]— panadapter shows the pair immediatelyOn the old build, with
m_splitActive == false, both runs would leave every badge at the ghosted defaultSPLIT— noSWAP, no red, no pairing. (Old behavior confirmed by source inspection and the independent triage verification on the issue.)Agent automation bridge — gaps found
SWAP/SPLITbadges are real widgets and read cleanly viadumpTree, but the spectrum's paired-overlay marker (SliceOverlay::splitPartnerId) is drawn in theSpectrumWidgetGPU layer and isn't reflected indumpTree. It can only be checked by pixel-grabbingSpectrumWidget— proposed: exposesplitPartnerId(and similar overlay-derived state) in aget pan/get slicesnapshot so split pairing is assertable without image diffing.sliceverb supports add/remove/select but not "make slice N the TX slice". The most literal rigctld repro (set_split_vfo 1 VFOB— move TX to an existing slice) had to be approximated byslice add(new TX-adjacent slice) and reconnect-into-split. Proposed: aslice tx <id>verb so the bridge can drive the exact external-split transition end-to-end.Behavioral note for review
By design the pair now renders whenever a pan has a TX slice and a distinct RX slice — which is exactly the maintainer-endorsed derivation in the issue. This means two slices on one panadapter with TX on one will now show SWAP/SPLIT even if the user didn't think of it as "split". That's functionally accurate (you RX on one, TX on the other) and matches #3715, but flagging it explicitly per the triage's open question.
💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat