feat(tnc): Direwolf-derived AFSK demodulator for VHF 1200-baud AX.25#3527
Conversation
There was a problem hiding this comment.
Thanks @K5PTB — this is a well-executed contribution. The 3-way comparator data is exactly the kind of evidence we like to see, the Dire Wolf attribution (root THIRD_PARTY_LICENSES + README.aethersdr.md, GPL-2.0-or-later → GPL-3.0-or-later) is done right, and the code follows project conventions: no QSettings, RAII throughout (unique_ptr lanes, std::vector buffers), idiomatic C++20 (std::numbers, std::popcount, std::call_once). The sampleGap absolute-difference fix in shouldEmitFrame correctly anticipates the non-monotonic sample indices introduced by lane-major block processing — good catch. AetherAFSKDemod.cpp compiles clean standalone with -Wall -Wextra -std=c++20. No Copilot findings to address.
A few items, roughly in priority order:
1. FCS reject-display threshold changed 17 → 18 (recordReject) — lastRejectActualFcs/lastRejectExpectedFcs are now blanked for frameBytesSize < 18, but the comment four lines below (and the < 17 too-short check) still says 17 bytes is the minimum valid frame — a no-PID U-frame sits exactly at 17 and has a real FCS in bytes 15–16. As written, a 17-byte bad-FCS reject loses its FCS diagnostics. If this was intentional, the adjacent comment should say why; otherwise it looks like an off-by-one and should revert to >= 17.
2. The publishFrameMqtt change is a refactor, not a restoration. On the merge-base (f39746a3), appendFrame() already calls publishFrameMqtt(frame) under #ifdef HAVE_MQTT — MQTT publishing is not broken on main. This PR moves the call to a separate frameDecoded connection, which is behaviorally equivalent and fine to keep, but the PR description ("wiring was lost during cherry-pick… Restored") describes the author's fork, not upstream. Flagging so maintainers don't think main needs a hotfix.
3. Scope: a few unrelated edits in Ax25HfPacketDecodeDialog. The removal of controls->addStretch(2) is a visible layout change not mentioned in the PR — please either revert it or note the intent. The three invokeMethod PMF→lambda rewrites, the startTransmit declaration reorder in the header, and the hdlc_codec_test block relocation in CMakeLists are all functionally inert churn that inflates the diff; not blocking, but worth trimming.
4. Efficiency (non-blocking follow-up): the 9 A+ slicers each run a full independent front-end. Prefilter → IQ mix → RRC → envelope → AGC are computed identically in all 9 lanes (identical inputs, identical state); only the step-5 decision (spaceGain) and DPLL state actually differ. Dire Wolf shares one demodulated signal across its slicers for this reason. That's roughly 9× redundant DSP (~4k MACs/sample at 24 kHz). Your RPi 5 testing shows it's affordable, so fine to land as-is, but sharing the front-end across slicers would be a nice follow-up and would also shrink per-lane state.
5. Minor, diagnostics-only: the totalHdlcFrameStarts dedupe window (+ 10 samples) is half a symbol at 24 kHz/1200 baud; independent DPLLs commonly land flag detection a symbol or more apart, so cross-lane duplicates will still be counted — and with lane-major ordering, a later lane processing earlier samples fails the > comparison asymmetrically. Consider widening to a symbol-or-two window, or just documenting that the counter is approximate.
6. Trivia: the kPrefilterLenSym comment says "~8.3 symbol-times" but 383 × 1200⁄44100 ≈ 10.4; and the __restrict__ MSVC shim in AetherAFSKDemod.h is unused (nothing in the file uses __restrict__) and defines a reserved identifier — suggest dropping it.
Two FYIs, no action needed: the branch's actual merge-base is f39746a3 (two commits behind main at open — contradicts the "rebased immediately before opening" note, but there are no conflicts), and VhfMode::Off produces zero lanes so processMonoFloat early-returns with frozen diagnostics — fine today since nothing selects Off, just confirm that's the intended semantic.
None of the above is structural — item 1 is the only one I'd want resolved before merge. Nice work, and 73.
🤖 aethersdr-agent · cost: $19.8929 · model: claude-fable-5
|
@AetherClaude — thanks for the thorough review. Addressing each item: #1 — FCS display threshold (>= 18 → >= 17) #2 — #3 — Inadvertent churn in #4 — Independent front-end per slicer #5 — #6 — FYIs |
There was a problem hiding this comment.
Thanks @K5PTB — this is a well-prepared PR. The vendoring follows the repo's third_party conventions (attribution in THIRD_PARTY_LICENSES, refresh notes, GPL-2.0-or-later → GPL-3.0-or-later is fine), the new code is clean RAII/C++20 (std::numbers, std::popcount, std::call_once, unique_ptr lanes), and the comparator data is appreciated. I verified two claims from the description against main:
- MQTT rewiring: confirmed equivalent.
appendFrame()'s only early return isif (!frame.fcsOk), the shim setsfcsOk = trueon every frame it emits, andappendFramehas no callers other than theframeDecodedconnection. The directframeDecoded → publishFrameMqttconnection preserves behavior. - Settings: only whitespace changes to the grandfathered flat keys; no new AppSettings keys. ✓
Findings, none blocking:
1. Nine slicer lanes redundantly recompute an identical front end (perf, suggest follow-up). Each A+ lane runs the full ~209-tap BPF + IQ mix + 4× RRC convolutions on the same input samples; the lanes differ only at the slicer decision (m_spaceGain) and DPLL. Direwolf computes the filter chain once per channel and fans out only the per-slicer comparison — that structure would be both more faithful and ~9× cheaper on the VHF path (~94M MACs/s currently). Your RPi 5 results show it's workable as-is, so I'd take this as a follow-up refactor (shared front end producing mAmp/sAmp, per-slicer decision + DPLL state), not a blocker for this PR.
2. pllAlpha is accepted but silently ignored — kVhf1200PllAlpha is now dead. AetherAFSKDemod's constructor discards pllAlpha (and the other libmodem tuning params) in favor of the internal Direwolf inertia constants — documented in the header, but the shim still defines kVhf1200PllAlpha = 0.010 and passes it through addLaneA, so a maintainer tuning that constant would see no effect. Suggest dropping the constant and the parameter from addLaneA (or at minimum a comment at the call site noting the value is unused on the VHF path).
3. Gate-open no longer resets lane bitstream state — please confirm intentional. resetBitstreamStates() was removed along with its call in updateReceiveGate, so HDLC/demod state now persists across squelch closures. FCS protects against splicing, it matches Direwolf's behavior, and your test plan covers gate recovery — so this looks deliberate, but it isn't mentioned in the PR description's change list, and it's a behavior change independent of the new demodulator. Worth a sentence in the description for future archaeology.
4. Doc nits.
README.aethersdr.mdsays the header provides asinc_corr_afsk_demodulatoralias — it doesn't; the shim adapts viaAfskDemodWrapper. Please update.- The CMake comment/status message labels the library "GPL-3"; the vendored code is GPL-2.0-or-later (GPL-3 only as combined work). Minor, but the THIRD_PARTY_LICENSES entry gets it right — worth matching.
5. Minor. The HDLC frame-start dedup window is hardcoded to 20 samples (one VHF symbol), but processBit is shared with the HF 300 path where a symbol is 80 samples — the counter is diagnostics-only and commented as approximate, so fine, but deriving it from config.sampleRate / config.baud would make it correct for both. Also, AetherAFSKDemod's filter design divides by bitrate/sampleRate unguarded — fine with today's hardcoded 24000/1200 callers, but a cheap assert would protect future ones.
Nice touches I want to call out: the quint64 cast fixing potential overflow in the duplicate-window computation, and the absolute-gap handling in shouldEmitFrame correctly accounting for the now non-monotonic currentDecodeSampleIndex across per-lane block processing.
🤖 aethersdr-agent · cost: $7.1404 · model: claude-fable-5
…cy (aethersdr#3527 review) Maintainer fix-up addressing the three small items from the second aethersdr-agent review pass that were left open / deferred: - CMakeLists.txt: the vendored Dire Wolf source is GPL-2.0-or-later, not "GPL-3" — the build comment and STATUS message now state that, with the combined-work GPL-3.0-or-later note (matching THIRD_PARTY_LICENSES, which was already correct). No license-version mislabel ships. - AetherAx25LibmodemShim: drop the dead `kVhf1200PllAlpha` constant and the unused `pllAlpha` parameter from `addLaneA`. AetherAFSKDemod ignores pll_alpha (uses Direwolf's internal DPLL inertia — confirmed by the `/*pllAlpha*/` unnamed ctor param), so the named constant was a false tunable. The 0.010 positional value is preserved inline + documented, so behavior is byte-identical; the HF libmodem lane keeps its real pll_alpha. - README.aethersdr.md: the header does not provide a `sinc_corr_afsk_demodulator` alias — it's adapted via `AfskDemodWrapper<>`. Corrected. Verified: clean build; ax25_libmodem_shim_test (Profile A+ loopback) + ax25_frame_formatter_test + hdlc_codec_test + tnc_terminal_test all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@K5PTB — pushed a small maintainer fix-up (
Verified locally: clean build, and all four AX.25 tests pass ( The 9-slicer shared-front-end de-duplication remains a good follow-up (not a blocker), as you noted. With these in, this is on my list to approve once CI re-greens. 73. |
There was a problem hiding this comment.
Re-reviewed at head c8f11129 — the new AetherAFSKDemod, the shim block-processing rewrite, the GUI/MQTT rewiring, CMake, and the test. The findings from the previous round all look correctly resolved, and I didn't turn up anything blocking. 🎉 Nice work, @K5PTB — the 3-way comparator evidence is genuinely impressive.
Confirmed resolved from the prior pass
- CMake
STATUSmessage + comment now correctly state the vendored source is GPL-2.0-or-later (combined work GPL-3.0).THIRD_PARTY_LICENSESand the README agree. - Dead
kVhf1200PllAlphaconstant and the falsepllAlphatunable removed;addLaneApasses0.010inline with a comment explainingAetherAFSKDemodignores it (uses Direwolf's internal DPLL inertia). HF lane keeps its realpllAlpha. - MQTT is now a direct
frameDecoded → publishFrameMqttconnection guarded by#ifdef HAVE_MQTT, and the duplicate call insideappendFrame()is gone — behaviorally equivalent sinceframeDecodedonly fires for FCS-valid frames. - Cross-lane dedup window widened to 20 samples (one symbol) with the "approximate counter" comment.
Things I checked that are correct
- Build linkage is complete. All four targets that compile
AetherAx25LibmodemShim.cppnow linkaether_afskdemod: the mainAetherSDRtarget (viaCORE_SOURCES→ALL_SOURCES), plusax25_libmodem_shim_test,ax25_replay, andax25_session_analyze. No orphaned consumer that would hit a link error. - The non-monotonic
currentDecodeSampleIndex. The new per-lane block loop setscurrentDecodeSampleIndexfrom each bit's absolute index and then re-walks it per lane, so it's no longer globally monotonic within aprocessMonoFloatcall. The switch inshouldEmitFrameto an absolute-differencesampleGap()correctly handles that — good defensive change. (ThelastHdlcFrameStartSampleIndexgating is technically subject to the same non-monotonicity, but it only feeds the approximatetotalHdlcFrameStartsdiagnostic, so it's harmless and already documented.) s_cosTableinit iscall_once-guarded; reads after init are fine across instances/threads.- AGC/DPLL/confidence paths and the single-vs-multi-slicer decision look faithful to the documented profile-A algorithm.
Non-blocking observations (no action needed for this PR)
- The shared-front-end-per-slicer de-dup is the right follow-up, as already noted — each of the 9 A+ lanes currently re-runs the prefilter + 4 LP convolutions independently. Fine to defer.
VhfModehas no UI/AppSettings exposure and is hard-coded toAPlusvia the config default. That matches the PR's stated scope; just flagging that "A vs A+" is currently a compile-time-only choice.
CI was still in progress when I looked (build / check-macos / check-windows / CodeQL all running, none failed). Worth letting it green before merge, as @ten9876 noted. Thanks again for the thorough test plan and the careful attribution handling on the vendored code.
🤖 aethersdr-agent · cost: $3.2236 · model: claude-opus-4-8
Adds AetherAFSKDemod — Profile A+ (9 slicers, Direwolf default) AFSK
demodulator derived from Dire Wolf demod_afsk.c / dsp.c (WB2OSZ,
GPL-2.0-or-later → GPL-3.0-or-later). Replaces libmodem's
sinc_corr_afsk_demodulator on the VHF 1200-baud path; libmodem is
unchanged as the HF 300-baud backend.
Performance — 1875-packet overnight comparator run, RPi 5, live APRS
traffic (Principle VIII — evidence over assertion):
AetherSDR A+ 93% avg confidence 60
Graywolf 90% avg confidence 52
Direwolf 87% avg level 58
137 packets (7.3%) decoded exclusively by AetherSDR
New files:
third_party/direwolf_afsk/AetherAFSKDemod.h/.cpp — vendored per
Technology Constraints (third-party); THIRD_PARTY_LICENSES updated
third_party/direwolf_afsk/THIRD_PARTY_LICENSES
Modified:
src/core/tnc/AetherAx25LibmodemShim.cpp/.h — AfskDemodWrapper<>
template; VhfMode enum (Off/A/APlus); HF path unchanged
src/gui/Ax25HfPacketDecodeDialog.cpp/.h — profile persistence
tests/ax25_libmodem_shim_test.cpp — synthetic Profile A+ loopback
Also restores publishFrameMqtt wiring to frameDecoded signal, lost
during cherry-pick from aethersdr#3460. Demonstrated by functional test
confirming frames on aethersdr/ax25/rx. Principle XI.
No new AppSettings keys added. kPacketDecoderProfileSetting and
kPacketDecoderDebugSetting are flat keys grandfathered from e0618ee
(2026-05-16), one day before Constitution v1.1.0. Principle V.
AppSettings writes use single setValue + save(). Principle XIV.
Pre-submission: max-effort code review pass run; all blocking findings
addressed. Branch rebased against current upstream/main. Principle X.
Member naming and π constants comply with AGENTS.md §147/§144.
Principle VIII.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ethersdr#6 on feat/ax25-direwolf-demod aethersdr#1 — Revert FCS display threshold from >= 18 back to >= 17. Off-by-one in the initial commit; a 17-byte no-PID U-frame has a real FCS at bytes [15-16]. The adjacent comment already said 17 was the minimum; the threshold contradicted it without rationale. aethersdr#3 — Revert inadvertent churn in Ax25HfPacketDecodeDialog. Four changes were cherry-pick/split artifacts from the profiles branch that were not restored when PR3 was separated: the controls->addStretch(2) removal (visible layout regression), three invokeMethod PMF->lambda rewrites (functionally inert), the startTransmit declaration reorder in the header, and the hdlc_codec_test block relocation in CMakeLists. All reverted to merge-base state. aethersdr#5 — Widen totalHdlcFrameStarts dedup window from 10 to 20 samples. One symbol = 20 samples at 24 kHz/1200 baud. The previous 10-sample window was half a symbol, allowing cross-lane frame-start duplicates to increment the counter when DPLLs on different lanes land detection slightly apart. Added comment documenting the counter as approximate. aethersdr#6 — Fix kPrefilterLenSym comment (8.3 -> 10.4 symbol-times) and drop the __restrict__ MSVC shim in AetherAFSKDemod.h. The shim was unused (nothing in the file uses __restrict__) and defined a reserved identifier. FYI/VhfMode::Off — added confirming comment at the lanes.empty() early-return in processMonoFloat: frozen diagnostics when Off is intentional. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cy (aethersdr#3527 review) Maintainer fix-up addressing the three small items from the second aethersdr-agent review pass that were left open / deferred: - CMakeLists.txt: the vendored Dire Wolf source is GPL-2.0-or-later, not "GPL-3" — the build comment and STATUS message now state that, with the combined-work GPL-3.0-or-later note (matching THIRD_PARTY_LICENSES, which was already correct). No license-version mislabel ships. - AetherAx25LibmodemShim: drop the dead `kVhf1200PllAlpha` constant and the unused `pllAlpha` parameter from `addLaneA`. AetherAFSKDemod ignores pll_alpha (uses Direwolf's internal DPLL inertia — confirmed by the `/*pllAlpha*/` unnamed ctor param), so the named constant was a false tunable. The 0.010 positional value is preserved inline + documented, so behavior is byte-identical; the HF libmodem lane keeps its real pll_alpha. - README.aethersdr.md: the header does not provide a `sinc_corr_afsk_demodulator` alias — it's adapted via `AfskDemodWrapper<>`. Corrected. Verified: clean build; ax25_libmodem_shim_test (Profile A+ loopback) + ax25_frame_formatter_test + hdlc_codec_test + tnc_terminal_test all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c8f1112 to
4008280
Compare
|
@K5PTB — rebased onto current Good news: the rebase was conflict-free. This PR doesn't touch Verified on the rebased head: clean build, all four AX.25 ctests pass ( Once CI re-greens this is ready for maintainer approval — it needs mine specifically since it touches |
… settings (aethersdr#3565 review) Maintainer fix-up addressing the two should-fix items from the aethersdr#3565 review (folded in on top of a rebase onto current main): - THIRD_PARTY_LICENSES: add entry aethersdr#16 for the vendored QGeoView (LGPL-3.0-or-later, (C) 2018-2025 Andrey Yaroshenko, https://github.com/AmonRaNet/QGeoView). The PR vendored ~7k lines of LGPL code but the root attribution registry — which lists all 10 other bundled libs and is itself maintainer-gated — didn't list it. The bundled LICENSE + AETHERSDR-PATCHES.md remain; this is the canonical registry entry, matching the Dire Wolf (aethersdr#3527) precedent. - PskReporterMapDialog: consolidate the two new flat AppSettings keys (PskReporterShowPaths, PskReporterUpdateIntervalMs) into one nested-JSON blob under a single "PskReporter" key (Constitution Principle V), via read-modify- write helpers. These keys are brand-new in this PR, so no migration is needed. (PskReporterMapGeometry stays — that's PersistentDialog window geometry, a separate established convention.) Builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## 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>
…ethersdr#3527) Direwolf profile-A AFSK demodulator (IQ-mix + RRC + DPLL, 9-slicer A+) replacing libmodem's VHF 1200-baud path; HF 300-baud stays on libmodem. Vendored Dire Wolf source is GPL-2.0-or-later (combined work GPL-3.0-or-later, see THIRD_PARTY_LICENSES). 3-way comparator: AetherSDR A+ 93% vs Graywolf 90% vs Direwolf 87% over 1875 packets. Authored by @K5PTB with a maintainer review fix-up (GPL label, dead-constant cleanup, README). Co-authored-by: K5PTB <k5ptb@users.noreply.github.com>
Summary
Adds `AetherAFSKDemod`, a Direwolf-derived AFSK demodulator for VHF 1200-baud AX.25, replacing `libmodem`'s `sinc_corr_afsk_demodulator` on the VHF path. libmodem is unchanged as the HF 300-baud backend. Profile A+ (9 slicers, the Direwolf default) is the active configuration.
3-way comparator — 1875 packets, 6-hour overnight run (RPi 5, live APRS traffic). Principle VIII — evidence over assertion:
AetherSDR captured 137 packets (7.3%) that neither Graywolf nor Direwolf decoded.
What changed
New files
Modified files
Fix also in this branch
`publishFrameMqtt` wiring was lost on this branch during cherry-pick from #3460. Rather than restoring the original `#ifdef HAVE_MQTT` block inside `appendFrame()`, the call is now a direct `frameDecoded` → `publishFrameMqtt` signal connection — behaviorally equivalent since `frameDecoded` is only emitted for FCS-valid frames. Confirmed by functional test. Principle XI.
Pre-submission
Branch rebased against `upstream/main` on 2026-06-11; two commits landed upstream in the interval before the PR was opened, no conflicts. Principle X.
A `max`-effort code review pass was run. Principle VIII. Three findings were intentionally left unaddressed:
Test plan
Tested on MacBook Air M2 (macOS), RPi 5 Debian trixie (Debug and Release builds), and Windows 11 Intel i3 / integrated GPU.
Unit tests — 4/4 pass on all three platforms:
Functional — all three platforms:
Built and smoke tested on: MacBook Air M2 / RPi 5 Debian trixie / Win 11 i3