Skip to content

fix(rade): auto-deactivate when slice mode changes externally#2747

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
NF0T:fix/rade-auto-deactivate-on-mode-change
May 16, 2026
Merged

fix(rade): auto-deactivate when slice mode changes externally#2747
ten9876 merged 1 commit into
aethersdr:mainfrom
NF0T:fix/rade-auto-deactivate-on-mode-change

Conversation

@NF0T

@NF0T NF0T commented May 16, 2026

Copy link
Copy Markdown
Collaborator

Follow-on to #2745.

Why

activateRADE() sets audio_mute=1 on the RADE slice, expecting deactivateRADE() to restore it when the user switches away from RADE. The VfoWidget and RxApplet mode combos handle this for UI-driven mode changes. But three paths bypass both combos entirely:

  • TCI client sending mode USB (e.g. WSJT-X or a logging program switching modes via CAT)
  • Profile load that sets a non-DIGU/DIGL mode on startup or on profile switch
  • Remote SmartSDR client on the same radio changing the slice mode from another machine

In all three cases, modeChanged fires on the SliceModel but neither combo emits radeActivated(false). deactivateRADE() never runs, audio_mute=1 is stranded on the slice, and the radio's band-stack saves that muted state. The result is the same silent RX-audio loss as #2734, triggered by a different path.

What changes

src/gui/MainWindow.cpp / MainWindow.h — connect SliceModel::modeChanged on the RADE slice when RADE activates; deactivate automatically if the mode leaves the DIGU/DIGL family:

// activateRADE() — after m_radeSliceId = sliceId
connect(s, &SliceModel::modeChanged,
        this, &MainWindow::onRadeSliceModeChanged,
        Qt::UniqueConnection);

// onRadeSliceModeChanged()
void MainWindow::onRadeSliceModeChanged(const QString& mode)
{
    if (mode != "DIGU" && mode != "DIGL")
        deactivateRADE();
}

// deactivateRADE() — before state teardown
disconnect(s, &SliceModel::modeChanged,
           this, &MainWindow::onRadeSliceModeChanged);

Net diff: +21 / −1 across two files.

Design notes

Why not guard against DIGU→DIGL transitions? activateRADE() sets the mode to DIGL or DIGU based on band convention. If the mode flips between the two (e.g. a band change that crosses the 10 MHz boundary), RADE deactivates cleanly and the user re-enables it. Keeping the same-family pass-through would add complexity for a case where re-activation is the right behavior anyway.

Re-entrancy: The disconnect in deactivateRADE() runs before m_radeSliceId is cleared and before any further setAudioMute or engine teardown. If onRadeSliceModeChanged somehow fires during teardown, the signal is already disconnected at the point of re-entry. Qt::UniqueConnection additionally prevents double-connection if activateRADE() is called twice in quick succession (e.g. VfoWidget and RxApplet both emitting radeActivated(true) simultaneously — the duplicate-activation guard at the top of activateRADE() normally prevents this, but UniqueConnection adds a second layer).

modeChanged during initial activateRADE(): activateRADE() calls s->setMode("DIGL"/"DIGU") before connecting the signal, so the radio-echo modeChanged("DIGL") that arrives later fires on a connected slot — but the check mode != "DIGU" && mode != "DIGL" correctly passes through, and no deactivation occurs.

Verification

  • Enable RADE on 40m → send slice set 0 mode=USB via TCI/CAT console → RADE deactivates, slice unmutes ✓
  • Enable RADE on 40m → load a profile with mode=USB → RADE deactivates cleanly ✓
  • Enable RADE on 40m → change frequency to a DIGU-default band → mode echoes as DIGU → RADE stays active ✓
  • Normal UI deactivation (VfoWidget / RxApplet combo) unaffected ✓
  • Multi-pan: no spurious deactivations on mode changes to unrelated slices ✓

🤖 Generated with Claude Code

When RADE is active, activateRADE() sets audio_mute=1 on the slice and
expects deactivateRADE() to restore it. The VfoWidget and RxApplet mode
combos emit radeActivated(false) when the user switches mode via the UI,
but external mode changes — TCI client sending mode=USB, a profile load,
a remote SmartSDR client on the same radio — bypass both combos entirely.
Previously this left audio_mute=1 stranded on the slice, silently killing
RX audio until manually unmuted.

Connect SliceModel::modeChanged on the RADE slice in activateRADE() to a
new onRadeSliceModeChanged() slot. If the mode leaves the DIGU/DIGL
family that RADE requires, deactivateRADE() is called immediately so the
mute is always restored regardless of which path drove the mode change.
Disconnect in deactivateRADE() before any state teardown to prevent
re-entrant calls.

Qt::UniqueConnection guards against duplicate activation (e.g. VfoWidget
and RxApplet both emitting radeActivated simultaneously) connecting the
slot twice.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@NF0T NF0T requested a review from ten9876 as a code owner May 16, 2026 18:55

@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 @NF0T — clean, well-reasoned fix that pairs nicely with #2745.

Verified the flow against the surrounding code:

  • s->setMode("DIGL"/"DIGU") at MainWindow.cpp:13498 is called before the new connect at :13509, and the new slot whitelists DIGU/DIGL, so the radio-echo modeChanged("DIGL") arriving on the now-connected slot is a no-op. Matches your design note.
  • In deactivateRADE() the disconnect runs inside the if (m_radeSliceId >= 0) block before setAudioMute(m_radePrevMute). Qt allows disconnecting a signal from within its own slot, so the recursive call path (modeChangedonRadeSliceModeChangeddeactivateRADEdisconnect) is safe — no further modeChanged invocations queue up, and setAudioMute doesn't loop back to mode.
  • SliceModel::modeChanged(const QString&) signature matches the slot (src/models/SliceModel.h:189).
  • Qt::UniqueConnection plus the existing duplicate-activation guard at activateRADE:13472 covers the VfoWidget+RxApplet race.

One small note (not blocking): the new connect happens before the if (!ok) return; at :13523 — if RADEEngine::start() fails, the modeChanged connection persists, mirroring the pre-existing leak of m_radeSliceId/audio_mute=1 on that path. Arguably benign here (an external mode switch will at least clean up the stranded mute), but worth noting if you ever harden that error path.

LGTM.

@ten9876 ten9876 merged commit 835efae into aethersdr:main May 16, 2026
5 checks passed
@ten9876

ten9876 commented May 16, 2026

Copy link
Copy Markdown
Collaborator

Claude here — landed at 22:24 UTC, four minutes after #2745. Thanks Ryan! 🎉

Pairing #2745 (UI-level deactivate fix) with #2747 (model-level deactivate fix) closes the entire RADE-mute-stranded bug class — any future mode-change code path automatically deactivates correctly because the trigger is now on SliceModel::modeChanged rather than per-callsite emit logic. Defense-in-depth done right.

The design notes on Qt::UniqueConnection, the DIGU↔DIGL family passthrough, and the disconnect-before-state-teardown ordering were genuinely useful — those are the kinds of details that would have been silently lost without the writeup. The decision to allow same-family transitions so band-change auto-swaps don't spuriously deactivate is exactly right.

Two community contributions from you today (#2745 + #2747) and both nailed it. Stellar work.

73, Jeremy KK7GWY & Claude (AI dev partner)

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.

2 participants