Skip to content

feat(tnc): isolate AX.25 shim on dedicated QThread#3473

Merged
jensenpat merged 2 commits into
aethersdr:mainfrom
K5PTB:feat/ax25-worker-thread
Jun 8, 2026
Merged

feat(tnc): isolate AX.25 shim on dedicated QThread#3473
jensenpat merged 2 commits into
aethersdr:mainfrom
K5PTB:feat/ax25-worker-thread

Conversation

@K5PTB

@K5PTB K5PTB commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

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

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 #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 against main.

K5PTB and others added 2 commits June 8, 2026 15:02
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>
@K5PTB K5PTB requested a review from a team as a code owner June 8, 2026 20:06
@K5PTB

K5PTB commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Post-open addendum: ran the full AX.25/TNC test suite on feat/ax25-hdlc-codec (which includes all commits from this PR) on MacBook Air M2:

ax25_frame_formatter_test  PASSED   0.32s
ax25_libmodem_shim_test    PASSED  11.14s
hdlc_codec_test            PASSED   0.19s
tnc_terminal_test          PASSED   0.18s
4/4 passed

These tests were not called out explicitly in the original test plan — adding them here for the record.

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) are Q_DECLARE_METATYPE'd and qRegisterMetaType'd in the shim ctor (on the GUI thread, before moveToThread) — the classic queued-connection trap is handled.
  • No residual cross-thread state access: every m_shim-> call is gone except construction-time moveToThread; GUI reads now hit the m_shimConfig cache set synchronously in setModemProfile; all signal connections pass this as context so they correctly degrade to queued.
  • Ownership/teardown is right: shim is correctly unparented (required for moveToThread) and reclaimed via finished → deleteLater, with quit()/wait() driving the final event-loop pass. No double-free (QPointer entries).
  • Extracted free functions are pure over cfg — TX-build on the GUI thread never touches the shim thread's m_impl. Good separation.
  • The ~MainWindow UAF fix is correct: explicit delete of the dialog before the AudioEngine deleteLater block, so setTncRxTapEnabled(false) runs while AudioEngine is alive. Mirrors the existing m_networkDiagnosticsDialog pattern; the RPi 5 ASAN confirmation backs it up.

Non-blocking notes for a future pass:

  1. The GUI status/log silently loses the ", N lane(s)" suffix — it switched from the member demodDescription() (keeps lanes) to the free ax25DemodDescription() (omits them). Reasonable since lane count is shim-thread state, but consider surfacing it via diagnosticsUpdated if it's diagnostically useful.
  2. Two explanatory comments (the < 15-byte minimum-frame note and the "host omits FCS; TNC appends it" note) were dropped during the ax25BuildTransmitAudioFromFrame extraction — easy to restore.
  3. feedAudio posts 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

@jensenpat jensenpat merged commit b0bd8ff into aethersdr:main Jun 8, 2026
5 checks passed
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>
@K5PTB K5PTB deleted the feat/ax25-worker-thread branch June 10, 2026 05:14
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>
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