feat(tnc): isolate AX.25 shim on dedicated QThread#3473
Merged
Conversation
Move AetherAx25LibmodemShim to its own QThread so audio processing (feedAudio, HDLC decode) runs off the GUI thread. All GUI-side shim calls use QMetaObject::invokeMethod with Qt::QueuedConnection; config is mirrored in m_shimConfig so the GUI thread never reads across the thread boundary. Extract ax25DemodDescription, ax25BuildTransmitAudio, and ax25BuildTransmitAudioFromFrame as free functions taking a const Ax25DemodConfig& so TX audio and status display work without crossing the thread boundary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ax25HfPacketDecodeDialog::~Ax25HfPacketDecodeDialog calls m_audio->setTncRxTapEnabled(false) via a raw AudioEngine pointer. AudioEngine is freed via deleteLater() during MainWindow teardown; Qt's deleteChildren() then runs the dialog destructor against already- freed memory — a heap-use-after-free caught by ASAN on shutdown. Fix: explicitly delete m_ax25HfPacketDecodeDialog before the AudioEngine teardown block, matching the existing pattern for m_networkDiagnosticsDialog and m_tciServer in the same destructor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contributor
Author
|
Post-open addendum: ran the full AX.25/TNC test suite on These tests were not called out explicitly in the original test plan — adding them here for the record. |
jensenpat
approved these changes
Jun 8, 2026
jensenpat
left a comment
Collaborator
There was a problem hiding this comment.
Approved ✅
Reviewed read-only end-to-end. This is a clean, well-scoped refactor that correctly isolates the AX.25 shim on its own QThread, and the threading model is implemented carefully.
Correctness — verified:
- Cross-thread signal args (
frameDecoded,diagnosticsUpdated) areQ_DECLARE_METATYPE'd andqRegisterMetaType'd in the shim ctor (on the GUI thread, beforemoveToThread) — the classic queued-connection trap is handled. - No residual cross-thread state access: every
m_shim->call is gone except construction-timemoveToThread; GUI reads now hit them_shimConfigcache set synchronously insetModemProfile; all signal connections passthisas context so they correctly degrade to queued. - Ownership/teardown is right: shim is correctly unparented (required for
moveToThread) and reclaimed viafinished → deleteLater, withquit()/wait()driving the final event-loop pass. No double-free (QPointerentries). - Extracted free functions are pure over
cfg— TX-build on the GUI thread never touches the shim thread'sm_impl. Good separation. - The
~MainWindowUAF fix is correct: explicit delete of the dialog before the AudioEnginedeleteLaterblock, sosetTncRxTapEnabled(false)runs while AudioEngine is alive. Mirrors the existingm_networkDiagnosticsDialogpattern; the RPi 5 ASAN confirmation backs it up.
Non-blocking notes for a future pass:
- The GUI status/log silently loses the
", N lane(s)"suffix — it switched from the memberdemodDescription()(keeps lanes) to the freeax25DemodDescription()(omits them). Reasonable since lane count is shim-thread state, but consider surfacing it viadiagnosticsUpdatedif it's diagnostically useful. - Two explanatory comments (the
< 15-byte minimum-frame note and the "host omits FCS; TNC appends it" note) were dropped during theax25BuildTransmitAudioFromFrameextraction — easy to restore. feedAudioposts one queued event per buffer with no backpressure; fine for libmodem, worth watching when the heavier demodulator lands in PR #3 of this series.
Thorough test plan (3 platforms, profile-switch-under-load, soak, ASAN) with honest caveats. Merging.
💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat
2 tasks
K5PTB
added a commit
to K5PTB/AetherSDR
that referenced
this pull request
Jun 8, 2026
…mments Addresses non-blocking review notes 1 and 2 from aethersdr#3473: 1. ax25DemodDescription() now appends ", N lane(s)" via a new ax25DemodLaneCount() free function computed from config — restores the suffix that was lost when the free function was extracted from the member demodDescription() during the threading refactor. 2. Restore two explanatory comments dropped during ax25BuildTransmitAudioFromFrame extraction: the minimum-frame-size invariant (14 addr + 1 ctrl = 15 bytes, no FCS) and the "host omits FCS; TNC appends it" note. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
K5PTB
added a commit
to K5PTB/AetherSDR
that referenced
this pull request
Jun 8, 2026
…mments Addresses non-blocking review notes 1 and 2 from aethersdr#3473: 1. ax25DemodDescription() now appends ", N lane(s)" via a new ax25DemodLaneCount() free function computed from config — restores the suffix that was lost when the free function was extracted from the member demodDescription() during the threading refactor. 2. Restore two explanatory comments dropped during ax25BuildTransmitAudioFromFrame extraction: the minimum-frame-size invariant (14 addr + 1 ctrl = 15 bytes, no FCS) and the "host omits FCS; TNC appends it" note. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
K5PTB
added a commit
to K5PTB/AetherSDR
that referenced
this pull request
Jun 9, 2026
- Drop dead selectedTonePolarity() declaration from Ax25HfPacketDecodeDialog.h — leftover from conflict resolution during rebase onto main after aethersdr#3473 merged - handleMqttMessage in Ax25HfPacketDecodeDialog now guards with isMqttTopicEnabled, consistent with the CW transmit handler in MainWindow - cw/decode TX payloads (rx:false) now pull pitch_hz/speed_wpm from transmitModel().cwPitch()/cwSpeed() rather than the last RX measurement - radio/state publish coalesced through a 150 ms single-shot timer to prevent flooding on VFO drag/scan (frequencyChanged fires on every intermediate step) - setMqttTopicEnabled no longer saves to disk per call; saveSettings() does one AppSettings::save() after the full loop - Add comment near kCwDecodeTopic/kCwTransmitTopic noting relay scripts should filter on the aethersdr/ namespace to avoid cw/decode→cw/transmit feedback loops — the full topic path serves as the origin identifier. @aethersdr-agent: was an explicit origin field your intent, or is the namespace sufficient? Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jensenpat
pushed a commit
that referenced
this pull request
Jun 9, 2026
… HDLC framer (#3475) ## Summary Second of three focused PRs extracted from #3466 per @jensenpat's review guidance, implementing part of the design laid out in #2701 (Native Soft-TNC + APRS Integration). Introduces `HdlcCodec`, a self-contained pure-C++ HDLC framer with CRC-16-CCITT, and wires it into the VHF 1200-baud decode lane in place of libmodem's `bitstream_state`. libmodem is retained for HF demodulation and AX.25 frame parsing. Also addresses non-blocking review notes 1 and 2 from #3473: lane count restored in `ax25DemodDescription()` via `ax25DemodLaneCount()`, and two explanatory comments restored in `ax25BuildTransmitAudioFromFrame`. ## Motivation libmodem's `bitstream_state` is an opaque internal struct — not unit-testable in isolation and not replaceable without forking libmodem. Extracting HDLC framing into `HdlcCodec` makes the state machine independently testable, and establishes the boundary where the Direwolf demodulator (PR 3) will plug in. ## Changes - **New:** `src/core/tnc/HdlcCodec.h` / `HdlcCodec.cpp` — pure C++ HDLC bit-unstuffing and CRC-16-CCITT framer - **New:** `tests/hdlc_codec_test.cpp` — 27 tests covering state transitions, FCS validation cross-checked against libmodem, two-frame decode, long payload, and reset - `CMakeLists.txt`: added `HdlcCodec.cpp` to main build and shim test; added `hdlc_codec_test` target - `AetherAx25LibmodemShim`: `DecodeLane` replaces `bitstreamState` with `HdlcCodec hdlcCodec` - `ax25DemodLaneCount()` free function added; `ax25DemodDescription()` now includes lane count suffix - Two explanatory comments restored in `ax25BuildTransmitAudioFromFrame` (minimum-frame-size invariant; host-omits-FCS note) Squashed-from: #3475 Co-authored-by: Paul <274291579+K5PTB@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: jensenpat <84298137+jensenpat@users.noreply.github.com>
ten9876
added a commit
that referenced
this pull request
Jun 14, 2026
## Release prep for v26.6.3 Bumps the version and refreshes documentation for the **58 commits since v26.6.2** (2026-06-07). No tag is created — tagging is the release trigger (the #3567 MS Store automation fires on `v*` tags), so that step is left to the maintainer after this merges. ### Version - `CMakeLists.txt`: `VERSION 26.6.2 → 26.6.3` (drives the MSIX `Identity.Version`). - README.md + AGENTS.md "current version" strings updated to match. ### CHANGELOG.md — v26.6.3 entry Covers: WFM software demodulator, APRS client, PSK Reporter reception map + reusable Qt mapping engine (QGeoView), Direwolf-derived VHF AFSK demodulator, DAX-IQ made fully usable, profile-load recovery hardening, DEXP protocol fix, Multi-Flex ping-watchdog grace, MQTT enhancements, the #3351 MainWindow decomposition + `RadioSession`, the MS Store CI auto-stage, and the fix roll-up. ### Doc refresh (reviewed all repo-root + `docs/` files) - **README.md** — Highlights: added WFM, AetherModem packet/APRS, PSK Reporter map. - **ROADMAP.md** — cycle → `post-v26.6.3`; moved **AetherModem Phase 1** from Queued to **Recently shipped** alongside WFM / AFSK / PSK-Reporter map / DAX-IQ / the decomposition. - **AGENTS.md** — version, added `RadioSession` to Key classes, threads **11 → 12**. - **CONTRIBUTING.md** — thread count 11 → 12. - **docs/architecture/pipelines.md** — added the **AX.25 TNC thread** (#3473) to the list + table; **11 → 12**. - **docs/MODEM.md** — VHF 1200 baud now documents the **Direwolf-derived Profile A+ demodulator (9 slicers)** that replaced the libmodem lane bank (#3527); the APRS client + map are no longer "out of scope" (HF 300-baud section unchanged). Reviewed (via two parallel doc-review agents) and found **no changes needed** in: CONSTITUTION, GOVERNANCE, SUPPORT, SECURITY, CODE_OF_CONDUCT, CLAUDE/GEMINI (pointer files), and the rest of `docs/` (audio-pipeline, vita49-format, tci-*, style/, theming/, qa/, etc.). ### Verification - Builds clean with `VERSION 26.6.3`; full suite **38/38 pass**. ### To roll the release (maintainer) After merge: `git tag -s v26.6.3 && git push origin v26.6.3` → triggers the Windows installer / release workflows (and the MS Store draft-stage once its credentials are set). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
## Summary First of three focused PRs extracted from aethersdr#3466 per @jensenpat's review guidance. Lays the threading groundwork for aethersdr#2701 (Native Soft-TNC + APRS Integration) — the shim thread boundary is required before a more capable demodulator can be dropped in without blocking the GUI. ## Motivation `AetherAx25LibmodemShim` ran on the GUI thread. Audio callbacks, HDLC decode, and profile configuration all competed with paint events and user interaction, making it impossible to later add a CPU-heavier demodulator without degrading responsiveness. ## Changes - `AetherAx25LibmodemShim` moved to a dedicated `QThread` (`m_shimThread`) - All GUI→shim calls use `QMetaObject::invokeMethod(..., Qt::QueuedConnection)` - `m_shimConfig` cache added to dialog — GUI thread never reads `m_impl->config` directly - Free functions extracted: `ax25DemodDescription`, `ax25BuildTransmitAudio`, `ax25BuildTransmitAudioFromFrame` - `startTransmit(const QString&)` extracted from `startTransmitFromUi` - Destructor: `m_shimThread.quit(); m_shimThread.wait()` - **ASAN fix (`b495dac9`):** `MainWindow::~MainWindow()` now explicitly deletes `m_ax25HfPacketDecodeDialog` before AudioEngine teardown. Pre-existing UAF: dialog destructor called `m_audio->setTncRxTapEnabled(false)` via raw pointer after AudioEngine was freed via `deleteLater()`. Same pattern as the existing `m_networkDiagnosticsDialog` fix in that destructor. Found on RPi 5 with ASAN; confirmed clean after fix. ## Test plan Tested on all three target platforms. 1. **Basic decode** — enabled modem on 144.390 MHz APRS; frames appeared in the log within seconds; disable/re-enable confirmed clean reset with no stale state. 2. **Profile switching under load** — switched HF↔VHF while audio was flowing, including rapid successive switches; no crash, new profile description appeared correctly each time. 3. **TX path** — transmitted a UI frame; confirmed key, send, and unkey with no crash or hang. KISS frame sent via `kiss_test.py` also keyed cleanly, exercising `ax25BuildTransmitAudioFromFrame` as a free function. **Hardware note:** transverter attenuated to prevent RF output — full software TX path exercised but no RF generated; iGate/APRS-IS verification deferred pending hardware modification. 4. **Clean shutdown** — closed dialog while modem was enabled; no hang or crash; reopened and re-initialized correctly. 5. **Extended run** — modem left running 10+ minutes on a busy APRS channel; no crashes, no memory growth, GUI remained responsive. 6. **ASAN (RPi 5)** — Debug+ASAN build clean after fix; no new leaks or UAF reported. **RX sensitivity note:** A single decode was achieved with an HT at close range. Consistent decode is limited by libmodem's single-slicer VHF sensitivity — a known pre-existing limitation addressed in the third PR in this series (Direwolf demodulator). This PR does not regress decode behavior. **300-baud HF decode** — not tested on-air; no 300-baud packet infrastructure available in the test environment. --- Built and smoke tested on: MacBook Air M2 / RPi 5 Debian trixie / Win 11 i3 Extracted from aethersdr#3466 (closed). **Note:** The follow-on PR introducing `HdlcCodec` ([feat/ax25-hdlc-codec](https://github.com/K5PTB/AetherSDR/tree/feat/ax25-hdlc-codec)) is ready but will be held until this PR merges, so it can be opened cleanly against `main`. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
… HDLC framer (aethersdr#3475) ## Summary Second of three focused PRs extracted from aethersdr#3466 per @jensenpat's review guidance, implementing part of the design laid out in aethersdr#2701 (Native Soft-TNC + APRS Integration). Introduces `HdlcCodec`, a self-contained pure-C++ HDLC framer with CRC-16-CCITT, and wires it into the VHF 1200-baud decode lane in place of libmodem's `bitstream_state`. libmodem is retained for HF demodulation and AX.25 frame parsing. Also addresses non-blocking review notes 1 and 2 from aethersdr#3473: lane count restored in `ax25DemodDescription()` via `ax25DemodLaneCount()`, and two explanatory comments restored in `ax25BuildTransmitAudioFromFrame`. ## Motivation libmodem's `bitstream_state` is an opaque internal struct — not unit-testable in isolation and not replaceable without forking libmodem. Extracting HDLC framing into `HdlcCodec` makes the state machine independently testable, and establishes the boundary where the Direwolf demodulator (PR 3) will plug in. ## Changes - **New:** `src/core/tnc/HdlcCodec.h` / `HdlcCodec.cpp` — pure C++ HDLC bit-unstuffing and CRC-16-CCITT framer - **New:** `tests/hdlc_codec_test.cpp` — 27 tests covering state transitions, FCS validation cross-checked against libmodem, two-frame decode, long payload, and reset - `CMakeLists.txt`: added `HdlcCodec.cpp` to main build and shim test; added `hdlc_codec_test` target - `AetherAx25LibmodemShim`: `DecodeLane` replaces `bitstreamState` with `HdlcCodec hdlcCodec` - `ax25DemodLaneCount()` free function added; `ax25DemodDescription()` now includes lane count suffix - Two explanatory comments restored in `ax25BuildTransmitAudioFromFrame` (minimum-frame-size invariant; host-omits-FCS note) Squashed-from: aethersdr#3475 Co-authored-by: Paul <274291579+K5PTB@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: jensenpat <84298137+jensenpat@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First of three focused PRs extracted from #3466 per @jensenpat's review guidance. Lays the threading groundwork for #2701 (Native Soft-TNC + APRS Integration) — the shim thread boundary is required before a more capable demodulator can be dropped in without blocking the GUI.
Motivation
AetherAx25LibmodemShimran on the GUI thread. Audio callbacks, HDLC decode, and profile configuration all competed with paint events and user interaction, making it impossible to later add a CPU-heavier demodulator without degrading responsiveness.Changes
AetherAx25LibmodemShimmoved to a dedicatedQThread(m_shimThread)QMetaObject::invokeMethod(..., Qt::QueuedConnection)m_shimConfigcache added to dialog — GUI thread never readsm_impl->configdirectlyax25DemodDescription,ax25BuildTransmitAudio,ax25BuildTransmitAudioFromFramestartTransmit(const QString&)extracted fromstartTransmitFromUim_shimThread.quit(); m_shimThread.wait()b495dac9):MainWindow::~MainWindow()now explicitly deletesm_ax25HfPacketDecodeDialogbefore AudioEngine teardown. Pre-existing UAF: dialog destructor calledm_audio->setTncRxTapEnabled(false)via raw pointer after AudioEngine was freed viadeleteLater(). Same pattern as the existingm_networkDiagnosticsDialogfix in that destructor. Found on RPi 5 with ASAN; confirmed clean after fix.Test plan
Tested on all three target platforms.
kiss_test.pyalso keyed cleanly, exercisingax25BuildTransmitAudioFromFrameas a free function. Hardware note: transverter attenuated to prevent RF output — full software TX path exercised but no RF generated; iGate/APRS-IS verification deferred pending hardware modification.RX sensitivity note: A single decode was achieved with an HT at close range. Consistent decode is limited by libmodem's single-slicer VHF sensitivity — a known pre-existing limitation addressed in the third PR in this series (Direwolf demodulator). This PR does not regress decode behavior.
300-baud HF decode — not tested on-air; no 300-baud packet infrastructure available in the test environment.
Built and smoke tested on: MacBook Air M2 / RPi 5 Debian trixie / Win 11 i3
Extracted from #3466 (closed).
Note: The follow-on PR introducing
HdlcCodec(feat/ax25-hdlc-codec) is ready but will be held until this PR merges, so it can be opened cleanly againstmain.