Skip to content

fix(gui): show panadapter split-pair for externally-initiated split (#3726)#3794

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:fix/3726-external-split
Jun 26, 2026
Merged

fix(gui): show panadapter split-pair for externally-initiated split (#3726)#3794
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:fix/3726-external-split

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

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 flag m_splitActive (plus m_splitTxSliceId / m_splitRxSliceId):

bool isTxSlice = (m_splitActive && s->sliceId() == m_splitTxSliceId);
bool isRxSplit = (m_splitActive && s->sliceId() == m_splitRxSliceId);

m_splitActive is set true only by the three GUI-initiated split sites. An external split sets the model's TX slice via SliceModel::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()) — the SPLIT/SWAP pair 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):

  1. Derive from the model. 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 (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.
  2. Re-trigger from the model. updateSplitState() now also runs on each slice's SliceModel::txSliceChanged, and at the tail of onSliceAdded/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. The Split mode badge text is read via dumpTree (a QPushButton's text is exposed as its value). m_splitActive == false throughout — no SPLIT button was ever pressed.

Run A — model-only slice add (exercises the sliceAdded trigger):

Step TX slice Split badges (letter, value)
Before (1 slice) A [(B, SPLIT)] — ghosted, no pair
After bridge slice add A [(B, SWAP), (A, SPLIT)] — pair rendered

Run 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:

TX slice Split badges
A [(B, SPLIT), (A, SWAP)] — panadapter shows the pair immediately

On the old build, with m_splitActive == false, both runs would leave every badge at the ghosted default SPLIT — no SWAP, no red, no pairing. (Old behavior confirmed by source inspection and the independent triage verification on the issue.)

Agent automation bridge — gaps found

  • Spectrum split-pair marker is GPU-only. The VFO SWAP/SPLIT badges are real widgets and read cleanly via dumpTree, but the spectrum's paired-overlay marker (SliceOverlay::splitPartnerId) is drawn in the SpectrumWidget GPU layer and isn't reflected in dumpTree. It can only be checked by pixel-grabbing SpectrumWidget — proposed: expose splitPartnerId (and similar overlay-derived state) in a get pan/get slice snapshot so split pairing is assertable without image diffing.
  • No bridge verb to set a slice's TX role. The slice verb 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 by slice add (new TX-adjacent slice) and reconnect-into-split. Proposed: a slice 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

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>
@jensenpat jensenpat marked this pull request as ready for review June 25, 2026 20:10
@jensenpat jensenpat requested review from a team as code owners June 25, 2026 20:10

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 guards if (!s ...) everywhere. Good.
  • Multi-pan correctness. Old updateSplitState() only touched spectrum(); the new m_panStack->allApplets() / spectrum() fallback mirrors the existing syncTxWaterfallSliceToSpectrums() pattern exactly, so it now repaints every pan. Nice.
  • RX-partner preference order (GUI-tracked RX → active → first-found) is order-independent because m_splitRxSliceId always 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 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ten9876 ten9876 merged commit 6340741 into aethersdr:main Jun 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Panadapter shows no split state when split is initiated externally (rigctld / CAT / TCI / front panel)

2 participants