fix(rade): deactivate RADE in closeEvent before radio disconnect#2968
Conversation
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>
330f7d1 to
f7838c2
Compare
There was a problem hiding this comment.
Thanks @NF0T — really clear write-up. I verified both claims against the tree:
MainWindow::deactivateRADE()clearsm_radeSliceId = -1(MainWindow.cpp:14059), so the destructor'sif (m_radeSliceId >= 0)guard correctly no-ops on the normal close path. Idempotency is intact.RadioConnection::writeCommand()already callsm_socket->flush()(RadioConnection.cpp:218), so the WAN path was the asymmetric one — the newm_socket.flush()inWanConnection::sendCommandbrings it in line.- The
#ifdef HAVE_RADEguard around the newcloseEvent()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
|
Merged — thanks @NF0T! The two-layered diagnosis (sequencing in Shipping in the next release. 73, |
…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>
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 inm_radePrevMute.deactivateRADE()restores it.The problem:
closeEvent()callsdisconnectFromRadio()to close the TCP socket, and then the destructor callsdeactivateRADE(). By the timedeactivateRADE()tries to send the restore command (slice set N audio_mute=0), the socket is already closed. The command is silently dropped.Fix: Call
deactivateRADE()incloseEvent()beforedisconnectFromRadio(), inside an#ifdef HAVE_RADEguard. The destructor guard is unchanged — it is already idempotent becausedeactivateRADE()setsm_radeSliceId = -1.Root cause 2 —
WanConnection::sendCommand()missingflush()Testing on a WAN (SmartLink) connection showed the fix from RC1 was not effective:
audio_mute=1still persisted after exit. Diagnostic logging confirmeddeactivateRADE()was being reached correctly (connected=true, correct slice ID and prev-mute value). The failure was one layer deeper, inWanConnection::sendCommand():QSslSocket::write()queues data in Qt's SSL plaintext buffer. Withoutflush(), the SSL layer never encrypts and sends those bytes until the Qt event loop runs. SincecloseEvent()callsdisconnectFromRadio()immediately afterdeactivateRADE()without yielding to the event loop, theaudio_mute=0bytes 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 calledflush()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::invokeMethodto a dedicated connection thread that explicitly flushes, so the timing is structurally different.Files changed
src/gui/MainWindow.cppdeactivateRADE()incloseEvent()beforedisconnectFromRadio()src/core/WanConnection.cppm_socket.flush()afterwrite()insendCommand()Test plan
"MainWindow: RADE mode deactivated"exactly once per session (destructor guard is no-op on normal close path)Principle XIV.
🤖 Generated with Claude Code