Skip to content

fix(rx-applet): restore unconditional radeActivated(false) emit#2745

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
NF0T:fix/rxapplet-rade-deactivate-guard
May 16, 2026
Merged

fix(rx-applet): restore unconditional radeActivated(false) emit#2745
ten9876 merged 1 commit into
aethersdr:mainfrom
NF0T:fix/rxapplet-rade-deactivate-guard

Conversation

@NF0T

@NF0T NF0T commented May 16, 2026

Copy link
Copy Markdown
Collaborator

Closes #2734

Why

RxApplet.cpp was patched in #2376 so its mode-combo lambda only emits radeActivated(false, ...) when m_slice->mode() == "RADE". That guard is never true in steady state.

"RADE" is a client-side-only concept. When the user selects RADE, setMode("RADE") sends slice set N mode=RADE to the radio, which ignores the unknown mode string and echoes back the real mode (DIGL for 40m, DIGU for 60m/WSPR). SliceModel::handleSliceStatus immediately overwrites m_mode from that echo. By the time the user switches away from RADE, m_slice->mode() is already "DIGL" — the guard always evaluates false, radeActivated(false) is never emitted, deactivateRADE() never runs, and audio_mute=1 is stranded on the slice.

The radio's per-band stack saves that muted state. Returning to the band (via the band-stack, on reconnect, or after a restart) reloads audio_mute=1, making RX audio silently dead on that band until manually toggled. All other bands are unaffected because they were saved before RADE muted the slice.

What changes

src/gui/RxApplet.cpp — remove the mode() == "RADE" guard; emit radeActivated(false, ...) unconditionally, matching pre-#2376 behavior:

// "RADE" is client-side only — the radio echoes back the real mode
// (DIGL/DIGU) immediately, so mode() == "RADE" is never true in
// steady state. Emit unconditionally; MainWindow's
// sliceId == m_radeSliceId check is the authoritative filter that
// prevents spurious deactivations from non-RADE slices (#2026).
emit radeActivated(false, m_slice ? m_slice->sliceId() : -1);

Net diff: +6 / −14 in one file.

Why the unconditional emit is safe

MainWindow's radeActivated handler (wired in MainWindow.cpp) already guards:

else if (sliceId == m_radeSliceId) deactivateRADE();

A spurious radeActivated(false, sliceId) from a non-RADE slice is silently dropped there. That check is the safety #2026 actually relied on for the multi-pan case — not the mode string.

RxApplet is also architecturally different from VfoWidget: a single RxApplet instance cycles through whichever slice the user selects via the tab row (N:1), whereas each VfoWidget is permanently bound to one slice (1:1). A mode-string or boolean flag check in RxApplet is inherently unreliable across slice rebinds; MainWindow::m_radeSliceId is the right authority.

Behavior

  • Steady state / multi-pan: identical to today. A USB→LSB swap on a non-RADE slice fires radeActivated(false, sliceB); MainWindow drops it because sliceB != m_radeSliceId. RADE on slice A is unaffected.
  • The fixed case: user enables RADE on 40m, then switches to USB via the RxApplet combo. radeActivated(false, 0) now fires; MainWindow calls deactivateRADE(); audio_mute is restored; band-stack saves the clean state.

Verification

Tested on FLEX-8400, firmware v4.1.5.39794:

  • Enable RADE on 40m via RxApplet combo → switch to USB → slice unmutes, RADE engine stops ✓
  • Enable RADE on 40m via VfoWidget combo → switch to USB via RxApplet combo → same result ✓
  • Multi-pan: RADE on slice 0 (Pan A), swap slice 1 (Pan B) USB→LSB via RxApplet → RADE on Pan A remains active ✓
  • Close + reopen AetherSDR, tune to 40m → audio present ✓

Follow-on

A separate PR will add a modeChanged listener in MainWindow::activateRADE to auto-deactivate RADE when the slice mode changes via external paths (TCI client, profile load, remote SmartSDR client). That closes the same class of bug regardless of which code path drives the mode change.

🤖 Generated with Claude Code

The mode() == "RADE" guard introduced in aethersdr#2376 is never true in steady
state: "RADE" is a client-side-only mode. When the user selects RADE,
setMode("RADE") sends slice set N mode=RADE to the radio, which ignores
it and echoes back the real mode (DIGL/DIGU). SliceModel overwrites
m_mode from that echo, so by the time the user switches away from RADE,
m_slice->mode() is already "DIGL" — the guard always suppresses the
emit, deactivateRADE() never runs, and audio_mute=1 is stranded on the
slice. The radio's per-band stack then saves that muted state, making
40m (or whichever band RADE was used on) silently dead until manually
unmuted.

Fixes aethersdr#2734.

Restore pre-aethersdr#2376 behavior: emit radeActivated(false, ...) unconditionally.
MainWindow's existing sliceId == m_radeSliceId check is the authoritative
filter for spurious deactivations from non-RADE slices — that is the
safety aethersdr#2026 actually relied on, not the mode string.

RxApplet is N:1 with slices (one applet, multiple selectable slice tabs)
so a mode-string or boolean flag check here is inherently unreliable;
the right authority is MainWindow's m_radeSliceId.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@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 fix with a thorough write-up. The diagnosis lines up with the code:

  • SliceModel::handleSliceStatus (SliceModel.cpp:543) overwrites m_mode from the radio echo, so m_mode == "RADE" is indeed unreachable in steady state — confirmed by the connect at RxApplet.cpp:1759 which then drives the combo back to DIGL/DIGU via QSignalBlocker, so the user wouldn't even see "RADE" in the dropdown for long.
  • MainWindow's sliceId == m_radeSliceId guard (MainWindow.cpp:2818, mirrored at 11247 for VfoWidget) is the authoritative filter, and m_radeSliceId defaults to -1 (MainWindow.h:702), so spurious emits from non-RADE slices are dropped correctly.
  • The N:1 vs 1:1 architectural point about RxApplet (cycling slices) vs VfoWidget (bound) is a good observation — the mode-string guard was always the wrong authority for RxApplet.

Minor edge case worth noting (not a blocker): when m_slice is null, the new code emits radeActivated(false, -1). If RADE is inactive, m_radeSliceId == -1 and the handler will invoke deactivateRADE() — which internally guards the mute-restore (if (m_radeSliceId >= 0) at MainWindow.cpp:13647) but still does some cleanup work (setRadeMode(false), clearTxAccumulators(), etc.). Idempotent on an inactive engine, and the lambda fires only from user combo interaction, so a null slice is unrealistic in practice. Could be tightened to if (m_slice) emit radeActivated(false, m_slice->sliceId()); if you want a belt-and-braces guard, but happy to leave as-is.

The follow-on plan to listen for SliceModel::modeChanged in MainWindow::activateRADE sounds like the right structural fix — TCI/profile/SmartSDR mode changes would still bypass this.

Verified on FLEX-8400 looks great. Nice work tracking this one down to the band-stack persistence.

@ten9876 ten9876 merged commit b652c52 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:17 UTC. Thanks Ryan, that's a model bug fix. 🎉

The root-cause analysis is exactly what we needed: spotting that "RADE" is client-side-only, that the radio's mode echo overwrites m_mode before the user ever switches away, and that the mode-string guard in #2376 was therefore always false in steady state. Combined with the multi-scenario hardware verification on FLEX-8400 (single-applet, cross-applet, multi-pan, and the persistence case from #2734) this PR effectively writes its own audit trail.

The architectural point about RxApplet being N:1 with slices (vs VfoWidget's 1:1) and therefore needing to rely on MainWindow::m_radeSliceId as the authority rather than any RxApplet-local state — that's worth capturing somewhere durable, probably in CLAUDE.md's Key Implementation Patterns section. I'll file a docs follow-up so the next person who touches RADE code doesn't reinvent the same broken guard.

Looking forward to the planned modeChanged listener PR for the external-mode-change paths (TCI / profile load / SmartSDR remote). That'll close the same class of bug regardless of which code path drives the mode swap.

73, Jeremy KK7GWY & Claude (AI dev partner)

ten9876 pushed a commit that referenced this pull request May 16, 2026
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:

```cpp
// 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](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@NF0T NF0T deleted the fix/rxapplet-rade-deactivate-guard branch May 17, 2026 15:43
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.

Audio fails after RADE

2 participants