Skip to content

fix(rade): deactivate RADE in closeEvent before radio disconnect#2968

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
NF0T:fix/rade-shutdown-cleanup
May 23, 2026
Merged

fix(rade): deactivate RADE in closeEvent before radio disconnect#2968
ten9876 merged 2 commits into
aethersdr:mainfrom
NF0T:fix/rade-shutdown-cleanup

Conversation

@NF0T

@NF0T NF0T commented May 22, 2026

Copy link
Copy Markdown
Collaborator

Problem

When the user exits AetherSDR with RADE mode active (File → Exit or the window close button), the slice that was RADE-assigned ends up muted on the next session even though RADE is not running.


Root cause 1 — sequencing error in closeEvent()

activateRADE() mutes the slice (slice set N audio_mute=1) and saves the pre-RADE mute state in m_radePrevMute. deactivateRADE() restores it.

The problem: closeEvent() calls disconnectFromRadio() to close the TCP socket, and then the destructor calls deactivateRADE(). By the time deactivateRADE() tries to send the restore command (slice set N audio_mute=0), the socket is already closed. The command is silently dropped.

closeEvent() — original broken order:
  1. preparePanadapterUiForShutdown()
  2. s.save()
  3. m_radioModel.disconnectFromRadio()   ← socket closed HERE
  ...
~MainWindow() — too late:
  4. if (m_radeSliceId >= 0) deactivateRADE()  ← audio_mute=0 dropped

Fix: Call deactivateRADE() in closeEvent() before disconnectFromRadio(), inside an #ifdef HAVE_RADE guard. The destructor guard is unchanged — it is already idempotent because deactivateRADE() sets m_radeSliceId = -1.


Root cause 2 — WanConnection::sendCommand() missing flush()

Testing on a WAN (SmartLink) connection showed the fix from RC1 was not effective: audio_mute=1 still persisted after exit. Diagnostic logging confirmed deactivateRADE() was being reached correctly (connected=true, correct slice ID and prev-mute value). The failure was one layer deeper, in WanConnection::sendCommand():

// Before — data placed in SSL plaintext buffer but never pushed to OS:
m_socket.write(data);
return seq;

// After — matches RadioConnection::writeCommand() behaviour:
m_socket.write(data);
m_socket.flush();   // push SSL plaintext buffer to OS TCP buffer immediately
return seq;

QSslSocket::write() queues data in Qt's SSL plaintext buffer. Without flush(), the SSL layer never encrypts and sends those bytes until the Qt event loop runs. Since closeEvent() calls disconnectFromRadio() immediately after deactivateRADE() without yielding to the event loop, the audio_mute=0 bytes were sitting in the SSL buffer when the socket closed.

flush() forces the SSL layer to encrypt and push the bytes to the OS kernel TCP buffer synchronously — TCP then guarantees their delivery before the FIN. The LAN path (RadioConnection::writeCommand()) already called flush() for exactly this reason; the WAN path was simply missing it.

Note: on a direct LAN connection the RC1 fix alone would have been sufficient — the LAN command path dispatches via QMetaObject::invokeMethod to a dedicated connection thread that explicitly flushes, so the timing is structurally different.


Files changed

File Change
src/gui/MainWindow.cpp Call deactivateRADE() in closeEvent() before disconnectFromRadio()
src/core/WanConnection.cpp Add m_socket.flush() after write() in sendCommand()

Test plan

  • WAN (SmartLink): Enable RADE, exit via File → Exit, reconnect — slice unmuted ✓
  • LAN (direct): Same test on a direct local connection
  • Exit via title-bar X button — slice unmuted on reconnect
  • Exit without RADE active — no crash, no behaviour change
  • Enable RADE, manually deactivate via mode selector, then exit — mute state correct on reconnect
  • Log shows "MainWindow: RADE mode deactivated" exactly once per session (destructor guard is no-op on normal close path)

Principle XIV.

🤖 Generated with Claude Code

@NF0T NF0T requested review from jensenpat and ten9876 as code owners May 22, 2026 14:19
@NF0T NF0T marked this pull request as draft May 22, 2026 14:31
@NF0T NF0T marked this pull request as ready for review May 22, 2026 14:52
NF0T and others added 2 commits May 22, 2026 12:27
When the user exits via File→Exit or the window close button with
RADE active, deactivateRADE() was called in the destructor — after
disconnectFromRadio() had already closed the TCP socket. The mute-
restore command (slice set N audio_mute=0) was sent into a dead socket
and silently dropped. On the next session the radio still had
audio_mute=1 set for that slice, so VfoWidget showed the slice muted
with no RADE running.

Fix: call deactivateRADE() in closeEvent() before disconnectFromRadio()
so the restore command reaches the radio while the connection is live.
The destructor guard (if (m_radeSliceId >= 0)) is unchanged; it is
already idempotent because deactivateRADE() sets m_radeSliceId = -1,
so the destructor becomes a no-op on the normal close path and remains
a safety net for programmatic destruction without closeEvent.

Principle XIV.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without flush(), QSslSocket::write() places the audio_mute=0 restore
command in Qt's SSL plaintext buffer but never pushes it to the OS TCP
stack. Since closeEvent() calls disconnectFromRadio() immediately after
deactivateRADE() without yielding to the Qt event loop, the buffered
bytes were discarded when the WAN socket closed — leaving audio_mute=1
stranded on the radio for the next session.

flush() forces the SSL layer to encrypt and write synchronously to the
OS kernel TCP buffer, where TCP guarantees delivery before the FIN.
RadioConnection::writeCommand() (the LAN path) already called flush()
for exactly this reason; the WAN path was simply missing it.

Diagnosed via qWarning instrumentation: the RC1 closeEvent sequencing
fix was being reached correctly (connected=true, correct slice/mute
values) but the command was lost in the unflushed SSL buffer on WAN.
On a direct LAN connection, RC1 alone would have been sufficient.

Principle XIV.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@NF0T NF0T force-pushed the fix/rade-shutdown-cleanup branch from 330f7d1 to f7838c2 Compare May 22, 2026 17:27

@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 — really clear write-up. I verified both claims against the tree:

  • MainWindow::deactivateRADE() clears m_radeSliceId = -1 (MainWindow.cpp:14059), so the destructor's if (m_radeSliceId >= 0) guard correctly no-ops on the normal close path. Idempotency is intact.
  • RadioConnection::writeCommand() already calls m_socket->flush() (RadioConnection.cpp:218), so the WAN path was the asymmetric one — the new m_socket.flush() in WanConnection::sendCommand brings it in line.
  • The #ifdef HAVE_RADE guard around the new closeEvent() block matches how the rest of the RADE call sites are gated.

Worth flagging that the flush() addition helps every WAN command that's followed by an immediate socket close or teardown — not just the RADE mute-restore. That's a net positive; the comment's (#rade-shutdown) tag slightly undersells the scope, but the rationale is accurate.

LGTM as a focused fix. No unit tests, but the bug is a socket-timing/event-loop interaction that's hard to cover without an integration harness — the manual matrix in the test plan (WAN + title-bar X + no-RADE + reactivated-then-exit) covers the relevant paths.


🤖 aethersdr-agent · cost: $7.1552 · model: claude-opus-4-7

@ten9876 ten9876 merged commit 04affba into aethersdr:main May 23, 2026
5 checks passed
@ten9876

ten9876 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Merged — thanks @NF0T! The two-layered diagnosis (sequencing in closeEvent plus the missing m_socket.flush() in WanConnection::sendCommand) is exactly the kind of root-cause work that's hard to do from a stack trace alone. The flush() symmetry with RadioConnection::writeCommand is a structural improvement that'll quietly help any future WAN command that needs to land before a teardown.

Shipping in the next release.

73,
Jeremy KK7GWY & Claude (AI dev partner)

G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…hersdr#2968)

## Problem

When the user exits AetherSDR with RADE mode active (File → Exit or the
window close button), the slice that was RADE-assigned ends up **muted
on the next session** even though RADE is not running.

---

### Root cause 1 — sequencing error in `closeEvent()`

`activateRADE()` mutes the slice (`slice set N audio_mute=1`) and saves
the pre-RADE mute state in `m_radePrevMute`. `deactivateRADE()` restores
it.

The problem: `closeEvent()` calls `disconnectFromRadio()` to close the
TCP socket, and _then_ the destructor calls `deactivateRADE()`. By the
time `deactivateRADE()` tries to send the restore command (`slice set N
audio_mute=0`), the socket is already closed. The command is silently
dropped.

```
closeEvent() — original broken order:
  1. preparePanadapterUiForShutdown()
  2. s.save()
  3. m_radioModel.disconnectFromRadio()   ← socket closed HERE
  ...
~MainWindow() — too late:
  4. if (m_radeSliceId >= 0) deactivateRADE()  ← audio_mute=0 dropped
```

**Fix:** Call `deactivateRADE()` in `closeEvent()` before
`disconnectFromRadio()`, inside an `#ifdef HAVE_RADE` guard. The
destructor guard is unchanged — it is already idempotent because
`deactivateRADE()` sets `m_radeSliceId = -1`.

---

### Root cause 2 — `WanConnection::sendCommand()` missing `flush()`

Testing on a WAN (SmartLink) connection showed the fix from RC1 was not
effective: `audio_mute=1` still persisted after exit. Diagnostic logging
confirmed `deactivateRADE()` was being reached correctly
(`connected=true`, correct slice ID and prev-mute value). The failure
was one layer deeper, in `WanConnection::sendCommand()`:

```cpp
// Before — data placed in SSL plaintext buffer but never pushed to OS:
m_socket.write(data);
return seq;

// After — matches RadioConnection::writeCommand() behaviour:
m_socket.write(data);
m_socket.flush();   // push SSL plaintext buffer to OS TCP buffer immediately
return seq;
```

`QSslSocket::write()` queues data in Qt's SSL plaintext buffer. Without
`flush()`, the SSL layer never encrypts and sends those bytes until the
Qt event loop runs. Since `closeEvent()` calls `disconnectFromRadio()`
immediately after `deactivateRADE()` without yielding to the event loop,
the `audio_mute=0` bytes were sitting in the SSL buffer when the socket
closed.

`flush()` forces the SSL layer to encrypt and push the bytes to the OS
kernel TCP buffer synchronously — TCP then guarantees their delivery
before the FIN. The LAN path (`RadioConnection::writeCommand()`) already
called `flush()` for exactly this reason; the WAN path was simply
missing it.

Note: on a direct LAN connection the RC1 fix alone would have been
sufficient — the LAN command path dispatches via
`QMetaObject::invokeMethod` to a dedicated connection thread that
explicitly flushes, so the timing is structurally different.

---

## Files changed

| File | Change |
|---|---|
| `src/gui/MainWindow.cpp` | Call `deactivateRADE()` in `closeEvent()`
before `disconnectFromRadio()` |
| `src/core/WanConnection.cpp` | Add `m_socket.flush()` after `write()`
in `sendCommand()` |

## Test plan

- [x] **WAN (SmartLink)**: Enable RADE, exit via File → Exit, reconnect
— slice unmuted ✓
- [ ] **LAN (direct)**: Same test on a direct local connection
- [x] Exit via title-bar X button — slice unmuted on reconnect
- [x] Exit without RADE active — no crash, no behaviour change
- [x] Enable RADE, manually deactivate via mode selector, then exit —
mute state correct on reconnect
- [x] Log shows `"MainWindow: RADE mode deactivated"` exactly once per
session (destructor guard is no-op on normal close path)

Principle XIV.

🤖 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