Skip to content

fix: RADE deactivated by mode change on unrelated slice in multi-pan setup#2026

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
NF0T:fix/rade-multipan-deactivation
Apr 26, 2026
Merged

fix: RADE deactivated by mode change on unrelated slice in multi-pan setup#2026
ten9876 merged 1 commit into
aethersdr:mainfrom
NF0T:fix/rade-multipan-deactivation

Conversation

@NF0T

@NF0T NF0T commented Apr 26, 2026

Copy link
Copy Markdown
Collaborator

Background

This fix was identified during post-merge testing of PR #1953 (dual-buffer RADE
RX architecture). Multi-pan verification of the new RADE audio path uncovered
two independent bugs that together caused RADE to be torn down whenever the mode
was changed on a completely unrelated slice.

Problem

In a two-panadapter setup, switching Slice B to DIGU (or any non-RADE mode via
the mode combo box) killed RADE on Slice A:

  • RADE status indicators (icon, sync light, SNR, freq offset) disappeared from
    Pan A's VFO
  • RADE decoded audio stopped
  • The radio's RADE waveform stream was torn down

Switching Slice B mode again did not restore RADE — a full manual re-activation
was required.

Root Cause (three independent bugs)

Bug 1 — Unconditional radeActivated(false) emit in VfoWidget:
The mode combo-box currentTextChanged handler emitted radeActivated(false, sliceId) for every non-RADE mode selection, regardless of whether RADE was
active on that widget. The quick-mode buttons directly below in the same file
already had the correct if (m_radeActive) guard; the combo handler was missing
it.

Bug 2 — Missing sliceId guard in MainWindow handlers:
Both radeActivated signal handlers in MainWindow (one wired to RxApplet, one
to VfoWidget) called deactivateRADE() unconditionally on on=false. A
spurious radeActivated(false, sliceB) from Bug 1 was enough to tear down RADE
even when m_radeSliceId pointed to Slice A.

Bug 3 — Wrong spectrum lookup in activateRADE/deactivateRADE:
Both methods called spectrum() (returns the currently focused pan's
SpectrumWidget) instead of spectrumForSlice() (looks up by pan ID). RADE
status indicators would appear/disappear on the wrong pan if a different pan
was focused at the time of activation.

Fix

  • VfoWidget.cpp: add if (m_radeActive) guard before
    emit radeActivated(false, ...) in the combo handler, matching the existing
    guard on the quick-mode buttons
  • MainWindow.cpp (both handlers): change else deactivateRADE()
    else if (sliceId == m_radeSliceId) deactivateRADE() so only the owning
    slice can trigger deactivation
  • MainWindow.cpp (activateRADE / deactivateRADE): replace spectrum()
    with spectrumForSlice(m_radioModel.slice(...)) for correct indicator
    placement in any pan layout

Testing

Tested on FLEX-8400, firmware v4.1.5.39794, two-panadapter layout:

  • Pan A: Slice 0 in RADE/FreeDV, RADE active
  • Pan B: Slice 1 — cycled through DIGU, DIGL, USB, LSB, AM, FM repeatedly
  • RADE on Pan A remained active throughout; status indicators stayed on Pan A
  • RADE activate/deactivate from Pan A VFO still works correctly
  • No regressions on single-pan operation

🤖 Generated with Claude Code

@NF0T NF0T requested a review from ten9876 as a code owner April 26, 2026 14:34

@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.

Nice work tracking down all three bugs, @NF0T — the root cause analysis in the PR description is excellent, and the fixes are well-scoped.

Review

All three fixes look correct:

  1. VfoWidget guard (if (m_radeActive)) — matches the existing pattern on the quick-mode buttons. Clean fix.

  2. MainWindow sliceId guard (sliceId == m_radeSliceId) — correctly applied to both handler lambdas (RxApplet at ~L2270, VfoWidget at ~L9049). This is the key defense against cross-slice teardown.

  3. spectrum()spectrumForSlice() — correct in both activateRADE and deactivateRADE. The null-check on the return value is already in place via if (auto* sw = ...), so this is safe even if the slice's pan is somehow missing.

No issues found with: conventions (AppSettings used correctly), null safety, resource management, or scope creep — changes are strictly within the stated scope.

One observation (non-blocking)

RxApplet::radeActivated at RxApplet.cpp:396 has the same unconditional emit radeActivated(false, ...) pattern that VfoWidget had before this PR:

// RxApplet.cpp ~L396
emit radeActivated(false, m_slice ? m_slice->sliceId() : -1);

This is now harmless because the MainWindow handler (Bug 2 fix) will reject it when sliceId != m_radeSliceId. But for consistency with the VfoWidget fix, you may want to add the same if (m_radeActive) guard in RxApplet's combo handler too. Not a blocker — the MainWindow guard is sufficient protection — but it would eliminate the spurious signal at the source.

Overall this is a clean, minimal fix. Thanks for catching this and for the thorough multi-pan testing on real hardware.

…etup

Two bugs found via user testing (FLEX-8400 v4.1.5, two-pan RADE+SSB layout):

Bug 1: Switching Slice B (Pan B) to DIGU silently deactivated RADE on
Slice A (Pan A) and unmuted its audio.

Root cause: VfoWidget's mode combo handler unconditionally emitted
radeActivated(false, sliceId) for any non-RADE mode selection — even on
a slice that never had RADE active. Every VfoWidget is wired to the same
MainWindow deactivateRADE() handler, so Slice B's combo change fired
deactivateRADE(), which restored m_radePrevMute (false) on Slice A and
tore down the RADE engine.

The quick-mode button handler already had the correct guard:
  if (m_radeActive) emit radeActivated(false, ...)
The combo handler was simply missing it. Fix applied to VfoWidget (source
guard using m_radeActive). The RxApplet combo has the same missing guard
but has no m_radeActive member, so both MainWindow radeActivated handlers
now also check sliceId == m_radeSliceId before calling deactivateRADE() —
belt-and-suspenders that covers both widgets correctly.

Bug 2: After the spurious deactivation, re-selecting RADE on Slice A
re-muted the slice but the RADE status label / sync indicator / SNR meter
did not appear.

Root cause: activateRADE() and deactivateRADE() used spectrum() (returns
the *active* pan's SpectrumWidget) to find the RADE slice's VfoWidget. In
multi-pan, if Pan B had focus, spectrum() returned Pan B's widget and
vfoWidget(sliceA_id) returned null — setRadeActive(true) was never called,
leaving the VfoWidget in m_radeActive=false with no status label shown.
Additionally the syncChanged/snrChanged/freqOffsetChanged connections to
the VfoWidget were silently skipped, so those indicators never updated.

Fix: use spectrumForSlice(m_radioModel.slice(sliceId)) which resolves the
correct pan by panId regardless of which pan is currently focused.
spectrumForSlice() falls back to spectrum() when the slice is null, so
behaviour is identical for the single-pan case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@NF0T NF0T force-pushed the fix/rade-multipan-deactivation branch from 3bc68a6 to 200e841 Compare April 26, 2026 18:54
@NF0T NF0T requested a review from jensenpat as a code owner April 26, 2026 18:54
@ten9876 ten9876 merged commit 500d538 into aethersdr:main Apr 26, 2026
5 checks passed
@NF0T NF0T deleted the fix/rade-multipan-deactivation branch April 26, 2026 23:36
@aethersdr-agent aethersdr-agent Bot mentioned this pull request May 8, 2026
2 tasks
NF0T added a commit to NF0T/AetherSDR that referenced this pull request May 8, 2026
…t pattern

PR aethersdr#2026 added an `if (m_radeActive)` guard to VfoWidget's unconditional
`emit radeActivated(false, ...)` to prevent spurious deactivation of RADE
on a different slice when any slice changes mode. RxApplet had the identical
pattern but was not updated in the same PR.

Add `m_radeActive{false}` and `setRadeActive(bool)` to RxApplet (mirroring
VfoWidget and PhoneCwApplet), guard the false emit, and wire the setter in
MainWindow::activateRADE / deactivateRADE so the state tracks correctly.

The MainWindow `sliceId == m_radeSliceId` handler guard remains load-bearing
and prevents any behavioral regression; this is a defense-in-depth cleanup
for code clarity and future-proofing.

Closes aethersdr#2062

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request May 8, 2026
…2376)

* fix(rx-applet): add matching radeActivated guard to mirror VfoWidget

Closes #2062

PR #2026 patched VfoWidget so its mode-switch lambda only emits
`radeActivated(false, ...)` when RADE was actually active on that
widget. The MainWindow handler `(else if (sliceId == m_radeSliceId)
deactivateRADE())` makes the matching unguarded emit at
`RxApplet.cpp:396` harmless today, but the asymmetry is exactly the
shape of the multi-pan regression #2026 fixed: it would re-tear-down
RADE on a different slice if the MainWindow guard ever softened.

This applies the same defense-in-depth guard:

- Add `bool m_radeActive{false};` to `RxApplet.h` (private), mirroring
  `VfoWidget`.
- In the mode-combo lambda, set `m_radeActive = true` after emitting
  the activate signal.
- Wrap the deactivate emit in `if (m_radeActive) { ... m_radeActive =
  false; }` so a USB->LSB swap on a non-RADE slice no longer fires a
  spurious `radeActivated(false)`.

Comment cites #2026 inline so future readers see the parallel.
No behavior change in the steady state: MainWindow still owns the
authoritative `m_radeSliceId` check; this just stops shouting `false`
from a slice that never said `true`.

* fix(rx-applet): use slice mode pre-change instead of local m_radeActive flag

The local-flag pattern from VfoWidget doesn't transfer cleanly to
RxApplet because the lifecycle is different:

- VfoWidget is one instance per slice; m_radeActive is set externally
  by MainWindow::setRadeActive() so it tracks the authoritative state.
- RxApplet is a single instance rebound across slices via setSlice();
  the local flag isn't synced from MainWindow and isn't reset on
  setSlice(), so it gets out of sync in two real scenarios:

    1. Externally activated RADE (VfoWidget combo, profile load on
       startup, MainWindow::activateRADE direct path) leaves
       m_radeActive=false in RxApplet; subsequent mode change in
       RxApplet would suppress the deactivate emit and orphan the
       RADE engine.

    2. Slice 0 in RADE → slice switch to slice 1 (USB) doesn't reset
       the flag → user picks LSB on slice 1 → flag clears (because
       the lambda runs even when MainWindow's authoritative guard
       silently filters the spurious emit) → user switches back to
       slice 0 (still RADE) → guard now incorrectly suppresses.

Both reachable in normal multi-pan workflows, same class of bug as
docs/multi-pan-pitfalls.md catalogues.

The simpler fix uses what's already authoritative: the slice's
pre-change mode.  m_slice->setMode(mode) runs at the END of the
lambda, so m_slice->mode() inside the lambda still holds the prior
mode.  Read it directly; emit deactivate only when the slice was
actually in RADE.  No new state, no sync hazard, correct across:

  - single-instance setSlice() rebind
  - externally activated RADE
  - the multi-pan case the original PR was targeting

Co-Authored-By: Matt Van Horn <mvanhorn@nibbcorp.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.com>
Co-authored-by: Matt Van Horn <mvanhorn@nibbcorp.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ten9876 pushed a commit that referenced this pull request May 16, 2026
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:

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

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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