Skip to content

feat(gui): FFT Smooth and FFT De-comb shape the panadapter trace across frequency. Principle VIII. - #6371

Merged
ten9876 merged 2 commits into
mainfrom
feat/fft-trace-smoothing
Oct 11, 2026
Merged

ten9876 merged 2 commits into
mainfrom
feat/fft-trace-smoothing

Conversation

@ten9876

@ten9876 ten9876 commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds two sliders to Display → PANADAPTER, below FFT FPS. Both are off by default and change only the spectrum trace: the main pan's and the mini-pan's, which magnifies it. The waterfall, the noise-floor readout, FFT Floor Auto and auto-squelch keep the raw bins. Both run in the client, so FLEX, HL2, ANAN, RTL and Kiwi behave the same.

FFT Smooth (0–16 px, 0 = off). This is the panadapter version of the Smoothing setting in the AetherRX/AetherTX EQ analyzer. Each point becomes the mean of its neighbors, averaged in linear power and converted back to dB, so a dB average doesn't drag the noise floor down. The smoothed floor reads its true average power, about 1–1.5 dB above the middle of the unsmoothed noise; the tooltip and docs say so. The EQ's window is a fraction of an octave because its axis is logarithmic. The panadapter's axis is linear, so here the window is set in the main trace's logical pixels.

FFT De-comb (0–600 Hz, 0 = off). Zoomed in to about 15 Hz per point, an SSB voice shows up as a comb of its pitch harmonics. I measured a 40 m screenshot: three stations combed at about 105, 180 and 180 Hz, each matching that speaker's own pitch. So the comb is real signal, not a receiver artifact. A mean wide enough to hide it drops the peaks by several dB. De-comb instead replaces the trace with its cepstral true envelope (Röbel & Rodet, core/dsp/SpectralEnvelope), a smooth curve over the harmonic peaks that keeps the signal's outline and peak level:

  • Width in Hz: the comb has a fixed spacing in Hz at any zoom. De-comb runs only when the width spans at least two source bins, so the GPU and CPU paths and the device pixel ratio always agree.
  • Noise floor: before enveloping, the input is clipped from below at 12 dB over the trace's 20th percentile, and the envelope blends in only where it rises clear of that clip. The noise floor is left as drawn, and voices about 8 dB or more above the noise are filled. No-data (non-finite) bins stay no-data.
  • Edges: it rounds a signal's steep edges over about half the width.
  • UI thread and FFTW: FFTW r2r runs on 2ᵃ3ᵇ5ᶜ-padded lengths, and the four most recent lengths' plans are cached. Plans are created and freed only with a try_lock on the FFTW planner lock (Two independent mutexes guard one process-global FFTW planner: SpectralNR never takes WdspChannel::fftwSetupLock() #5895), so the UI thread never waits on another thread's planning. At an uncached length, frames draw without De-comb until the lock is free, and each frame retries.

Both shape the source bins before the trace is resampled for drawing (De-comb, then Smooth), after the existing plateau-gated 5-tap blend. At 0 the trace is exactly what it is today.

Docs: added rows to docs/user/docs/panadapter-controls.md and updated the get display example in docs/automation-bridge.md.

Known, unchanged diagnostic: the bridge's rendered.floorDbm is measured from the drawn trace on the GPU path and from the raw bins on the CPU path. That predates this PR. With either slider on, the GPU figure reports the floor of the shaped trace.

Constitution principle honored

  • Principle V / XIV: both settings live in one owned object per pan, DisplayFftTraceShaping = {version, smoothPx, decombHz}, written in a single save and skipped when unchanged. No flat keys were added.
  • Principle VIII: each algorithm has a dedicated test, and each test was shown to fail with its fix reverted.

Test plan

  • Local build passes (lean RelWithDebInfo, shared core).
  • fft_trace_smoothing_test: off is bit-identical; the mean is in linear power; an impulse spreads to 2N+1 points; the window clamps at the edges; a floor point beside a +10 dBm carrier stays exact.
  • fft_trace_decomb_test:
    • 105 and 180 Hz voice combs, 18 dB deep, over noise: the outline holds within −1.28 to +2.25 dB, the skirts don't widen, and the halfway point stays within one window of the true edge.
    • A weak voice about 20 dB over the noise: filled to −0.76 dB.
    • Plain noise, 50 seeds: the median moves +0.10 dB (worst +0.28).
    • Other cases: non-finite bins are preserved, the source-bin eligibility boundary holds, and the plan cache works across many lengths.
    • Revert checks: with the noise clip disabled, the noise checks fail at +6.4 dB; with eligibility keyed to drawn density, the eligibility checks fail.
  • Existing tests pass: spectrum_overlay_fft_fps_bounds_test, waterfall_manual_black_test, waterfall_impulse_blanker_test, mini_pan_widget_test.
  • Every static check CI runs passes locally. The aetherd gates match the merge base, apart from one new ui-support touchpoint row (core/dsp/SpectralEnvelope.h, one includer), which the maintainer approved.
  • Automation bridge, isolated settings store: a fresh store loads both sliders as 0. Setting them reads back as fftSmoothing / fftDecombHz and persists as one DisplayFftTraceShaping object.
  • On a real radio: De-comb on a zoomed-in 40 m SSB pan (about 5 kHz span) at 300 Hz, on both the main pan and the mini-pan. The harmonic comb should become a smooth outline at peak level, with the noise floor unchanged.

Checklist

  • Commits are signed
  • No new flat-key AppSettings calls; one nested object per pan (Principle V)
  • Code is clean-room (the true envelope follows the published Röbel & Rodet method, DAFx 2005)
  • All meter UI uses MeterSmoother (no meter UI touched)
  • Documentation updated; no CHANGELOG entry
  • Security-sensitive changes reference a GHSA (none)

🤖 Generated with Claude Code

…ss frequency. Principle VIII.

The Display panel's FFT AVG and Wt Avg average over time. Two new
sliders, both off by default, work across frequency on the trace only;
the waterfall, noise-floor readout and auto-squelch keep the raw bins.
Both are client-side, so every radio gets them.

FFT Smooth (0-16 px, 0 = off) is the panadapter's counterpart of the
AetherRX/AetherTX EQ analyzer's Smoothing: each trace point becomes the
mean of its neighbors in linear power, then back to dB, so the noise
floor does not drop the way a dB mean would. The EQ's window is a
fraction of an octave on a log axis; the panadapter's axis is linear, so
here it is a half-width in logical pixels, scaled to the GPU's
device-pixel and the CPU path's oversampled point density.

FFT De-comb (0-600 Hz, 0 = off) is for a zoomed-in SSB voice, which at
~15 Hz per point resolves into a comb of pitch harmonics. Measured off a
40 m pan, three stations combed at ~105, ~180 and ~180 Hz -- each
station's own pitch, so the comb is real signal, not a receiver
artifact. A mean wide enough to hide it drops the peaks several dB, so
De-comb replaces the trace with its cepstral true envelope (Röbel &
Rodet, core/dsp/SpectralEnvelope): a DCT of the dB trace, coefficients
finer than the width tapered out, iterated with over-relaxation (alpha
3, at most 12 passes; plain iteration still sat ~4 dB under the peaks
after 6). The width is in Hz because the comb is a fixed spacing in Hz
at any zoom; under two points per width the span cannot resolve a comb
and it does nothing. Being a smooth curve it rounds a signal's steep
edges over about half the width; the tooltip and docs say so.

FFTW r2r runs on 2^a3^b5^c-padded lengths (prime lengths are ~5x
slower). Plans are cached per length and created or freed only on a
try_lock of the FFTW planner lock, since other threads hold it for
seconds (#5895): a first frame at a new length that cannot plan draws
raw, and closing a pan never waits. The envelope would lift a noise
floor toward its own peaks, so a gate blends back to the raw trace
within 8-14 dB of the estimated floor.

Both apply last in buildFftDisplayTrace, De-comb then Smooth, on top of
the existing plateau-gated 5-tap blend, so at 0 the trace is today's
bit for bit. Settings are one owned object per pan (Principle V/XIV):
DisplayFftTraceShaping = {version, smoothPx, decombHz}, written in one
save; carried by Clone to all pans, reset by Reset, and reported as
fftSmoothing / fftDecombHz by the bridge. SpectralEnvelope.h is tagged
ui-support in the aetherd touchpoint manifest.

fft_trace_smoothing_test pins off-is-identity, the linear-power mean,
2N+1 impulse spread, edge clamping, and an exact floor beside a +10 dBm
carrier (why it sums directly rather than through a prefix sum).
fft_trace_decomb_test drives a synthetic 2.7 kHz voice with 105 and
180 Hz combs, 18 dB deep, over unaveraged noise: the outline holds
within -1.32..+2.42 dB, the skirts do not widen, the half-way point
stays within one window of the true edge, and the floor median does
not rise.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876
ten9876 requested review from a team as code owners October 11, 2026 03:18
@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 6 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 2
Total findings : 6

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@ten9876 ten9876 self-assigned this Oct 11, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review of 874126715f1bfc5a3bbc7e60103caaee1ed3633d: Approve with nits. No linked issue or RFC; I reviewed against the stated intent. The two optional, off-by-default frequency-shaping controls, display-only processing and owned settings document are implemented and survived the focused attacks below. Native rendered traces and real SSB appearance remain unverified. Jeremy should confirm the new control direction through normal maintainer/CODEOWNERS review; I found no demonstrated architectural change requiring an RFC.

Scope audit

File/group Claimed? Verdict
src/core/dsp/SpectralEnvelope.{cpp,h}, src/gui/FftTraceSmoothing.h, FftTraceDecomb.h Yes: envelope, smoothing, floor gate In scope
SpectrumWidget.{cpp,h}, SpectrumOverlayMenu.{cpp,h}, MainWindow_Wiring.cpp Yes: rendering, sliders, persistence, clone/reset, bridge state In scope
CMakeLists.txt, tests/tests.cmake, both new FFT trace tests Yes: build and socket-free coverage In scope
docs/architecture/aetherd-touchpoint-tags.json, aetherd-touchpoints.md Yes: classify new display-support include and regenerate manifest In scope
docs/automation-bridge.md, docs/user/docs/panadapter-controls.md Yes: controls and state documentation In scope

All 17 changed files are explained by the body. The single relevant commit has a verified good GPG signature. No unrelated hunks, changed existing defaults, new external dependency, protocol verb, or enforcement weakening found.

Blockers

None.

Non-blocking nits

  1. De-comb eligibility depends on interpolated drawing density. src/gui/SpectrumWidget.cpp:9379–9382 uses the resampled point count for the two-points-per-width gate. At the same 1000 logical-pixel width, 200 kHz span and 300 Hz setting, the DPR-1 GPU path has about 1.50 points per width and skips De-comb; the CPU two-times sampling / DPR-2 GPU path has about 3.00 and enables it. Interpolation changes eligibility without adding source FFT resolution.

    I reproduced this by calling the actual head's production trace builder on identical 512-bin input (alternating −130/−135 dB with a three-bin carrier), copying each result before its scratch buffer could be reused:

    points=1000 maxDeltaDb=0.000000 changed=0 rawCarrier=-40.00
    points=2000 maxDeltaDb=0.385841 changed=14 rawCarrier=-40.00
    

    This is a measured small inconsistency, not a demonstrated blocker. Please choose a consistent resolution criterion for eligibility (for example, source-bin resolution), retain the appropriate resampled spacing for the envelope calculation, and add a socket-free density-boundary test. The numerical reproduction uses production code; assigning these counts to CPU/GPU paths comes from source inspection, not native GPU pixel capture.

  2. The one-frame planner-contention claim is too strong. src/gui/SpectrumWidget.cpp:9389–9391 and the PR body's “first frame” worst-case description imply that the next frame necessarily gets a plan. An uncached length returns false on every attempt while another thread holds the planner mutex; cached lengths can still compute. The contention probe confirmed the fresh-length failure leaves output untouched. Replace the comment with:

                    // At an uncached length, frames draw without De-comb until
                    // the planner lock becomes available; each frame retries.

    Please make the body equally precise. FFT Smooth can still run on those frames, so “without De-comb” describes the fallback more accurately than “raw.”

What I tried to break

  • Exact-head build and focused tests: lean RelWithDebInfo, Qt 6.12, shared-core review configuration; built AetherSDR and the five named tests only. All five passed: fft_trace_smoothing_test, fft_trace_decomb_test, spectrum_overlay_fft_fps_bounds_test, waterfall_manual_black_test, waterfall_impulse_blanker_test. Test-boundary preflight found no socket ownership in these targets. No broad suite was run.
  • Mutation and numerical attacks: replacing linear-power smoothing with a dB mean made four assertions fail. Reducing envelope iterations from 12 to 1 failed both comb-outline fixtures by approximately 10.6 dB. Unmodified fixtures stayed within −0.76/+1.91 dB (105 Hz) and −1.32/+2.42 dB (180 Hz), with 0.00 dB median floor rise. Flat input remained flat; cached-plan computation survived concurrent planner-lock ownership; fresh-length fallback preserved output. The public-method reference was checked against Röbel & Rodet, DAFx 2005; this implementation has its own iteration/taper choices.
  • Demo GUI and persistence: drove only isolated, offscreen DEMO-0001 instances on explicit socket aethersdr-pr6371-head, with TX disabled. get display reported defaults 0/0, extrema 16/600, then Reset 0/0. Setting 7/300 wrote one whole object, {"decombHz":300,"smoothPx":7,"version":1}, and both values returned after a process restart. The Display panel screenshot showed both controls fitting without label overlap; accessible names/descriptions and slider ranges were present. Both review-owned instances were closed; the operator's instance was untouched. Separate actual-widget probes showed malformed JSON/object type falls back to off without destroying the stored row, and out-of-range values clamp to 0/600.
  • Source and boundary attacks: raw waterfall, noise-floor, FFT Floor Auto and autosquelch consumers retain their unshaped data; scratch-buffer mutation does not overwrite source bins. Clone wiring and reset/menu synchronization include both controls. Clone/second-pan isolation was inspected, not exercised in the single-pan demo. Base/head gate comparisons retain 102 existing engine-boundary findings, 70 capability records and 32 command-plane files / 368 commands; the touchpoint manifest grows from 251 to 252 for the disclosed display-support include. Manifest and generated reference/bridge checks pass. All six accessibility-bot warnings predate this PR. A model-invocable automated code-review skill was unavailable; this was a manual review with independent code/governance audits.

CI: nine applicable checks passed; release-only PDF attachment was skipped. Earliest head workflow creation was 2026-10-11 03:18:07 UTC, after current main e56cb0ca664db9b5c4c12a36d8810faa8e40c8fd at 02:02:48 UTC, so the reviewed green runs were fresh at the final check.

Verification limit: offscreen QRhi cannot render SpectrumWidget; its capture returned grab produced an empty image for SpectrumWidget. Control pixels, state, restart persistence and production trace-builder numerics were verified, but native GPU/CPU trace pixels, multiple pans, hardware-family equivalence and real SSB visual quality were not. A native rendering capture would settle the visual claim; real firmware work needs a separately authorized review session.

Recommendation: Approve with nits. No reproduced correctness or canon blocker. Address the density consistency and contention wording, and have the maintainer confirm the additive control direction through the normal review. This is a COMMENT review; formal approval remains the operator's call.

@ten9876 ten9876 assigned ten9876 and unassigned ten9876 Oct 11, 2026

@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.

1. Issue fit

No linked issue and no RFC. I searched (FFT smoothing spectrum display, panadapter trace smoothing comb harmonics) and found no issue this closes, so I reviewed against the commit message's own stated intent. Against that, it delivers what it claims: two off-by-default client-side controls that shape the drawn trace across frequency, a cepstral envelope in src/core/dsp/, per-pan persistence, Clone/Reset/bridge coverage, and two tests.

On canon: I read GOVERNANCE.md §"What requires an RFC" and it does not cleanly cover this — two off-by-default rows added to an existing Display panel, no change to default UX behaviour, no new external dependency (FFTW3 is already a hard requirement at CMakeLists.txt:500). "New applets or dialogs that don't change existing UX" is explicitly on the not required list. So I am not calling this an RFC violation. What does need a maintainer's eye is the new public surface (below) and the adjacency to open RFC #5782.

#5782 (open, maintainer-review) is adjacent context, not a rule this breaks. Its §5 Option C — rejected — is about temporal averaging above the backend seam, on three grounds: wrong domain (dB), wrong frames (post-fps-cap), doesn't serve the waterfall. This PR is spatial, display-only, trace-only, and smoothFftTraceLinearPower averages in linear power, so the domain objection does not apply and #5980 ("backend-side, as #5782 ruled") is about a different axis. But #5782 §8 third bullet explicitly flags SpectrumWidget's existing client-side shaping as something a maintainer should rule on, and this adds two more stages to that same chain. Worth reconciling before merge; not a blocker.

2. Scope

The harness handed me a wider audit list than the diff. For the record, git show --stat on 8741267 touches 17 files; src/gui/MainWindow.cpp, src/gui/MainWindow_Menus.cpp and docs/user/screens.json are not among them, which is where most of the CodeGuard findings live (see §4).

File / group What it changes Claimed? Verdict
src/core/dsp/SpectralEnvelope.{h,cpp} New cepstral true-envelope DSP Yes In scope
src/gui/FftTraceSmoothing.h, FftTraceDecomb.h New header-only smoothing + floor gate Yes In scope
src/gui/SpectrumWidget.{h,cpp} Two members, setters, DisplayFftTraceShaping load/save, shaping in buildFftDisplayTrace, two snapshot keys Yes In scope
src/gui/SpectrumOverlayMenu.{h,cpp} Two slider rows, two signals, syncExtraDisplaySettings params Yes In scope
src/gui/MainWindow_Wiring.cpp Clone / Reset / Defaults / two connects Yes In scope
CMakeLists.txt One line: SpectralEnvelope.cpp into CORE_SOURCES Yes In scope
tests/fft_trace_{smoothing,decomb}_test.cpp, tests/tests.cmake Two new socket-free CTest targets Yes In scope
docs/architecture/aetherd-touchpoint{s.md,-tags.json} Tags core/dsp/SpectralEnvelope.h ui-support, 251→252 / 212→213 Yes In scope (Static checks green, so the count check agrees)
docs/automation-bridge.md, docs/user/docs/panadapter-controls.md Bridge example + two user-doc rows Yes In scope

Nothing in the diff is unexplained by the stated intent. No CHANGELOG.md entry (correct — please keep it that way). No deleted guard, confirmation or early return: the only - lines are the two syncExtraDisplaySettings signatures, the buildFftDisplayTrace signature, and the two return displayBins / return trace statements that became applyFftSmoothing(...). No preference change smuggled in — both controls default to Off and applyFftSmoothing is a straight pass-through at 0, so the existing trace is untouched for every existing user.

New public surface → maintainer call (per the review table, not a defect): fftSmoothing / fftDecombHz in both panstatsSnapshot and automationDisplaySettingsSnapshot, the DisplayFftTraceShaping settings document, and two new menu signals. The bridge keys are documented at docs/automation-bridge.md:1375; the panstats pair is not, but that snapshot has no documented key inventory, so there is nothing to diverge from.

Socket tests: none added, none removed, no QTcpServer/QUdpSocket/QLocalServer/bind/listen/connectToHost/Fake* peer in either new test. Both are pure in-process computation.

3. Blockers

None. The findings below are worth fixing but none of them breaks an existing user or fails on current main.

4. Findings (non-blocking)

4.1 The De-comb floor gate is calibrated against a different statistic than its comment reasons about, and the test fixture can't see the gap

src/gui/FftTraceDecomb.h:22-26:

The envelope rides peaks, so over noise it would lift the floor toward its
own noise peaks. [...] A single unaveraged frame's noise peaks reach ~8-10 dB
over the median, so the ramp starts at 8 dB above floorDbm and is fully on
at 14 dB.

But the floorDbm actually passed in is estimateNoiseFloorDbm (SpectrumWidget.cpp:9394), which is the mean of the bins at or below the dB mean (src/gui/NoiseFloorEstimator.h:32-39) — not the median. On exponentially-distributed per-bin power (a single unaveraged periodogram) that statistic sits ~5.5 dB below the median, and that offset is what moves the gate out of its intended regime.

Arithmetic on the gate formula, 600-point trace, 20-point window (= 300 Hz at the 15 Hz/point the test uses), envelope modelled as the local max over one window (what a peak-riding true envelope converges to):

floor model estimateNoiseFloorDbm median local peak peak − estimator gate w floor rise
true unaveraged periodogram (noise only) −136.6 −131.1 −123.7 12.9 dB 0.82 +6.1 dB
the test's floor (const −130 + noise) −128.6 −127.5 −122.8 5.9 dB 0.00 +0.00 dB

The reason the fixture lands in the safe row is tests/fft_trace_decomb_test.cpp:46-49 — voiceComb() returns the constant kFloor outside the signal, and main() at :84-89 then power-sums noiseFloor() on top. Summing a constant pedestal with equal-power noise makes the floor ~3 dB hotter and roughly halves its skew, so its local peak is 5.9 dB over the estimator, just below the 8 dB ramp start — w == 0 across the whole floor region and "noise floor kept" passes with a rise of exactly 0.00 dB.

To be fair to the test: it is not vacuous. Delete the gate and out == env, the floor median rises ~+4.7 dB in that same fixture and the rise < 1.0 assertion fails. The gate is covered; what is not covered is the gate's calibration against the floor distribution the comment names.

Fix is one of two, author's choice:

// in tests/fft_trace_decomb_test.cpp main(), floor region = noise only
            for (int i = 0; i < kPoints; ++i) {
                auto& x = in[static_cast<std::size_t>(i)];
                if (x == kFloor) {
                    x = noise[static_cast<std::size_t>(i)];
                    continue;
                }
                x = 10.0f * std::log10(std::pow(10.0f, x / 10.0f)
                                       + std::pow(10.0f, noise[static_cast<std::size_t>(i)] / 10.0f));
            }

and then raise the ramp so the fixture passes honestly (kRampStartDb = 14.0f, kRampSpanDb = 6.0f puts "fully on" above the ~13 dB local-peak level), or keep 8/14 and express them relative to a median rather than estimateNoiseFloorDbm. Either way the comment should name the statistic the code actually uses.

Reasoned from code plus a Python model of the gate formula with local-max as the envelope proxy — I did not run SpectralEnvelope. Severity is medium-to-nit rather than blocker because on a Flex the frames arrive radio-averaged and m_smoothed is itself a client EMA (#5782 §8), so the true-periodogram row needs FFT AVG at 0 on a raw-spectrum backend.

4.2 renderedTrace.floorDbm now means different things in the two renderers

src/gui/SpectrumWidget.cpp:15233 (GPU):

                renderedTraceFloorDbm = estimateNoiseFloorDbm(trace);

trace is now the shaped output. SpectrumWidget.cpp:15708 (CPU) still records estimateNoiseFloorDbm(displaySpectrumBins()) — raw bins. Because FFT Smooth collapses the floor's scatter toward its mean power, that estimator moves a long way: on an exponential floor it reads −7.5 dB (relative to mean power) raw, −3.0 at halfWidth 1, −1.4 at 4, and −0.6 at 16 — i.e. up to ~7 dB higher on the GPU path than the CPU path for the same frame. A bridge or radiocert consumer comparing panstats.renderedTrace.floorDbm across render modes, or across an FFT Smooth change, sees a step that isn't a radio change. Worth either measuring both paths on the same array or noting the shaping dependency in docs/automation-bridge.md.

(The user-facing readouts are fine: noiseFloorAutoLevelBins() returns raw bins, so FFT Floor Auto and measuredNoiseFloorDbm are genuinely unaffected, as the tooltip claims.)

5. Nits

  • src/gui/SpectrumOverlayMenu.cpp:1838-1840 and docs/user/docs/panadapter-controls.md:203 — "Averages in linear power, so the noise floor stays put" / "so the noise floor stays put". True of mean power (exact to 0.01 dB in my model at every width), but the floor the eye reads is the bottom of the grass, and collapsing the scatter raises the drawn median ~1.5 dB and the estimator-measured floor by up to ~7 dB (§4.2). Something like "the floor's average level stays put — the scatter collapses toward it, so the bottom of the grass rises" would survive a user measuring it.
  • src/gui/SpectrumWidget.cpp:9388-9390 — "False only on the first frame at a new trace length while another thread holds FFTW's planner lock". FftwPlannerLock.h says holds are "tens of seconds for a cold WdspChannel::open()". So resizing a pan while a slice opens leaves De-comb silently off for that whole window and then snaps on, which is more than "the first frame". The behaviour is right; the comment undersells it.
  • src/gui/SpectrumWidget.cpp:4426-4434 — each setter calls saveFftTraceShaping() → setValue + save(), and the slider is wired on QSlider::valueChanged (SpectrumOverlayMenu.cpp:1845), so a drag commits a SQLite transaction per tick, and cloneDisplaySettingsToAllPans does two per pan. setFftLineWidth on the same panel has exactly this shape (:1913), so this is precedent rather than a new defect — but a single combined write (or sliderReleased) would be cheaper.
  • tests/ — nothing covers the Hz→points mapping at SpectrumWidget.cpp:9374-9379 (the only non-header logic in the feature) or the DisplayFftTraceShaping clamp on an out-of-range stored value. Both are small; the clamp in particular is a two-line test against TestSettingsProfile.h. Non-gating.

6. What I tried to break (and didn't)

  • "At 0 the trace is today's bit for bit." Holds. applyFftSmoothing returns points untouched when both are 0, with no allocation and no extra pass, and fft_trace_smoothing_test pins out[i] != in[i] as a failure at halfWidth 0. Verified both return sites at SpectrumWidget.cpp:9414 / :9439.
  • A stale syncExtraDisplaySettings caller snapping both sliders to Off (defaulted params + QSignalBlocker) while the widget members stayed live. All four call sites are updated: MainWindow_Wiring.cpp:3564, :5023, :5140, SpectrumWidget.cpp:2618. No stale caller in the head checkout.
  • Reset / Clone / Defaults asymmetry. displaySettingsReset sets both to 0 and syncs the panel with 0, 0; cloneDisplaySettingsToAllPans copies both and re-syncs the destination menu; the Defaults lambda's ..., 8, 0, 0) is freqScaleFontPt=8 + the two new zeros, which is correct against the header's parameter order.
  • "The waterfall and the 3D surface keep the raw bins." Holds. buildFftDisplayTrace has exactly two callers (:15229, :16032), both the 2D trace; the waterfall and DSS go through croppedBinsForDisplay directly (:9078, :12766).
  • hzPerPoint against the ANAN edge crop. effectiveBandwidthMhz() already narrows via panDisplayBandwidthMhz(.., panEdgeCropActive()), matching the cropped bin array, so De-comb's width-in-Hz stays honest with the crop on. Not a bug.
  • Logical-vs-device pixel mismatch between renderers for FFT Smooth. Both pass logical width (specContentW is specRect.width() - DBM_STRIP_W; the CPU path passes r.width()), and pointsPerPx is derived from the actual n, so the on-screen window is the same at either sampling density.
  • The narrow-span degenerate case — a nonzero De-comb flattening the whole trace when the window approaches the span. Only the lower guard (windowPoints >= 2.0) exists, but at the 10 kHz default minimum (m_minBwMhz{0.010}) a 600 Hz width leaves ~33 DCT terms and at a 5 kHz Flex minimum ~17, so I could not reach a flat trace through the UI. Not a finding.
  • Memory safety and FFTW lifetime in SpectralEnvelope. m >= n always (smoothLength starts at n), every buffer is resized to M before use, !(cutoffPoints >= 2.0) rejects NaN, kc > 0 strictly, in/out aliasing is safe because out is written only at :191-193, and FFTW_UNALIGNED + new-array execute makes the plan-time local vectors going out of scope legal. The try_to_lock takes the double-precision mutex matching its fftw_plan_r2r_1d, and that mutex has real contenders, so it serializes against something. The security-audit subagent (CodeGuard) reached the same conclusion independently.
  • Attacking the tests. fft_trace_smoothing_test fails if the implementation is replaced by a dB-domain mean (-84.6 vs -93.3 at :49), by a prefix sum (the exact -140.0 beside a +10 dBm carrier at :92), or by edge padding instead of clamping (:83). fft_trace_decomb_test fails if compute returns false, if the gate is removed (§4.1), or if the envelope sits under the comb peaks. These are real assertions, not the implementation echoed back. The one gap is §4.1's fixture distribution.
  • CMake / linkage. aethercore gets ${FFTW3_INCLUDE_DIRS} PUBLIC and links ${FFTW3_LIBRARIES} (CMakeLists.txt:3016, :3019), and fft_trace_decomb_test's aethercore Qt6::Core ${FFTW3_LIBRARIES} is the same pattern as hl2_spectrum_test:1466 and hl2_pan_averaging_test:1484 — not a new linkage shape. Neither new target touches AppSettings.cpp, so the AETHER_SETTINGS_SOURCES / AETHER_SETTINGS_CONSUMERS contract doesn't apply.
  • Settings canon. docs/agents/settings.md puts "client-only display appearance preferences" in flat AppSettings keys, and DisplayFftTraceShaping is one versioned JSON document written in a single setValue + save — the same shape as saveDisplay3DSettings at SpectrumWidget.cpp:2909, Principle V satisfied, no migration needed for a new key. The "check the write result" rule in that doc attaches to radio_settings' setFeature(), not AppSettings::setValue(), so not applicable.
  • a11y. Both new sliders are GuardedSlider via makeRow (wheel guard inherited), carry setAccessibleName + setAccessibleDescription and object names, and the value column already reserves width for "-260 dBm", so "Off"/"600" cannot clip.

CodeGuard (§4 of the brief): all 98 findings refuted. src/gui/FftTraceSmoothing.h:22 CMDI-BACKTICK is a lexical hit on Markdown-quoted parameter names in a // comment; the MainWindow.cpp and docs/user/screens.json findings are in files this PR does not touch (RFC 5737 documentation addresses and screenshot alt-text); the homoglyph hits are ±, Röbel and em-dashes in comments. No shell, SQL or path sink exists in the new code. Credited to security-audit (CodeGuard), which returned no confirmed findings.

On green CI: build, check-macos, check-windows, Static checks and the sanitizer configure all pass on 8741267. But every ctest step in ci.yml is -R-filtered (lines 819-820, 832, 1174, 1189) and neither fft_trace_smoothing_test nor fft_trace_decomb_test is in any of those filters — green proves they compile and link on all three platforms, not that they pass. I could not run them.

Everything above is reasoned from the head checkout at 8741267 plus arithmetic; I am headless, so nothing here was reproduced at runtime, and §4.1 in particular is a model of the gate formula rather than a run of the real envelope.

7. Recommendation

Needs maintainer decision — on the new public surface, not on correctness. The code is genuinely careful: the planner-lock discipline, the no-prefix-sum justification, the linear-power domain, the honest "rounds steep edges by about half this width" in the tooltip, and two tests that would actually catch regressions are all better than this change needed to be. I found no blocker.

What a maintainer should rule on: (a) the two new bridge keys, the new settings document and two new panel rows arriving without an issue; (b) whether a third and fourth client-side shaping stage in the SpectrumWidget chain should wait on #5782, which explicitly asks for a ruling on the stages already there.

Concrete next step for @ten9876, independent of that ruling: fix §4.1 (make the test's floor region noise-only and re-tune or re-reference the 8/14 dB ramp — right now the ramp's own justification cites the median while the code gates on estimateNoiseFloorDbm), decide §4.2, and soften the "noise floor stays put" wording. Thanks for the unusually well-documented commit message — the alpha=3 / 12-pass justification and the 5-smooth-length note made this much faster to review.


🤖 aethersdr-agent · cost: $16.5949 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (103 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6371/docs/architecture/aetherd-touchpoint-tags.json:10
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6371/docs/architecture/aetherd-touchpoint-tags.json:1145
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6371/docs/user/screens.json:2579
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6371/docs/user/screens.json:4219
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in docs/user/screens.json /tmp/aetherclaude/pr-6371/docs/user/screens.json:3877
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6371/docs/user/screens.json:1122
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6371/docs/user/screens.json:2579
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6371/docs/user/screens.json:2625
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6371/docs/user/screens.json:2659
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6371/docs/user/screens.json:2756
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6371/docs/user/screens.json:2796
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6371/docs/user/screens.json:4219
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/FftTraceSmoothing.h /tmp/aetherclaude/pr-6371/src/gui/FftTraceSmoothing.h:22
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:9686
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:9798
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:9920
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:9921
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:9922
  • [HIGH] CS-VLN-PATH-PROC — Vulnerability: CS-VLN-PATH-PROC in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:6217
  • [HIGH] CS-VLN-PATH-PROC — Vulnerability: CS-VLN-PATH-PROC in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:6296
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:526
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:556
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:559
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:581
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:3660
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:1280
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:1804
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:1896
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:1973
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:2015
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:2050
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:2345
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:3602
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:3693
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:6159
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:7965
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:8030
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:8104
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:8189
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6371/src/gui/MainWindow.cpp:8370

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $16.7845 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Antares Detector — candidate vulnerable file(s)

  • src/core/dsp/SpectralEnvelope.cpp

The SpectralEnvelope implementation creates FFTW plans with sizes derived from the input size via std::size_t conversion (lines 94-99). This can silently truncate or wrap values, potentially allocating insufficient memory if the input size is not an exact power of two. The fftw_plan_r2r_1d call then uses these buffers, which could lead to buffer-overflow or memory-safety issues.

Localized by Cisco Foundation AI Antares-1B running locally in the AetherClaude sandbox, seeded by the Cartographer security map. Advisory only — please verify before acting.


🤖 aethersdr-agent · cost: $16.7845 · model: claude-opus-5

…n magnifies the shaped trace. Principle VIII.

De-comb's envelope rides peaks, and keyed on its own level against a
floor estimate that reads ~6 dB under the noise median, it rode the noise
peaks too: over plain unaveraged noise it lifted the median floor 4.8 dB
on average (5.7 worst of 50 seeds). The input is now clipped from below
at 12 dB over the trace's 20th percentile before enveloping -- unaveraged
noise crosses that ~0.1% of the time, and the percentile holds while a
signal fills up to ~80% of the span -- and the envelope blends in only
where it rises 1-6 dB clear of the clip. Over noise the median now moves
+0.10 dB (worst +0.28); voices ~20 dB over the noise still fill to
-0.76 dB of their outline. Non-finite (no-data) points are left as they
are rather than painted with envelope.

Both controls now shape the source bins before the resample, in one
shapeFftTrace(): whether De-comb runs is decided by the source
resolution (the width must span two bins), so the GPU path, the
oversampled CPU path and the device pixel ratio cannot disagree, and the
UI thread shapes srcCount points instead of up to 4x as many. The mini-pan
-- the main pan's magnifier -- shapes its source frame the same way
before reslicing, so it no longer draws the raw comb under a de-combed
main trace.

The FFTW plan cache keeps the four most recent lengths, evicting older
plans under the lock it already holds, so a drag-resize no longer
accumulates plans. The setters skip unchanged values, so a slider drag
or Clone no longer rewrites the settings object for nothing, and both
labels read back the clamped slider value through one helper. The FFT
Smooth tooltip and docs now say what the linear-power mean does to the
floor: it reads its true average power, ~1-1.5 dB over the unsmoothed
middle.

fft_trace_decomb_test adds 50-seed pure noise (median and per-point
change), a weak ~20 dB voice, preserved non-finite points, the source-bin
eligibility boundary, and the plan cache across many lengths. With the
clip disabled the noise checks fail at +6.4 dB.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 6 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 8
Files with findings: 2
Total findings : 6

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@ten9876

ten9876 commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator Author

Remediation pushed in b67e7728c, on top of 874126715. Each item is listed with its proof.

Review nits (review 5481750048)

  1. De-comb eligibility depended on drawn density: fixed. Both controls now shape the source bins before the resample, in one SpectrumWidget::shapeFftTrace(). Eligibility is decombWindowPoints(hzPerBin, widthHz) in src/gui/FftTraceDecomb.h, which requires the width to span at least 2 source bins. So the GPU path, the CPU path and DPR can't disagree. fft_trace_decomb_test pins it: 512 bins over 200 kHz is off at 300 Hz, the width is reported in source bins, and exactly 2 bins per width runs while 1.99 doesn't. With eligibility keyed to the drawn density instead, those checks fail.
  2. Contention wording: fixed. The comment at shapeFftTrace() now reads "At an uncached length, frames draw without De-comb until the planner lock becomes available; each frame retries." The PR body says the same.

Automated code-review pass (medium), each finding verified before acting

  • Floor gate let the envelope ride noise: confirmed. It caused a +4.8 dB median rise over 50 seeds of unaveraged noise (worst +5.7). Fixed in decombTrace(): the input is clipped from below at 20th percentile + 12 dB, and the envelope blends in only 1–6 dB clear of the clip. The noise median now moves +0.10 dB (worst +0.28), and a ~20 dB voice still fills to −0.76 dB. Disabling the clip makes the new noise checks fail at +6.4 dB.
  • FFT Smooth "floor stays put": confirmed, a rise of +1.0 to +1.5 dB. That's the linear-power mean reading the true average power, which is the intended behavior, so the tooltip and docs now say so.
  • FFTW plan cache never shrank: confirmed. It's now capped at 4 lengths, and older plans are destroyed under the lock already held for the new one. A test walks many lengths twice.
  • Shaping ran on the up-to-4× oversampled trace: confirmed. It now shapes srcCount source bins.
  • Non-finite bins got painted: confirmed. They're now left as they are, and a test covers −inf runs and NaN.
  • Setters saved on every tick: confirmed. They now skip unchanged values.
  • Duplicated label code, and the Smooth label showed the unclamped value: confirmed. Both now use one offOrValueText() helper and read the clamped slider value.
  • Mini-pan didn't get the controls: confirmed. By maintainer decision it now shapes its source frame with shapeFftTraceForMirror() before reslicing, so it magnifies the same trace.
  • rendered.floorDbm differs between GPU and CPU: left as is by maintainer decision. The asymmetry predates this PR; the value is a bridge diagnostic only (FFT Floor Auto reads raw bins), and the PR body notes it.

Accessibility bot (6 warnings): none are in this diff. They flag m_zoomSegBtn, m_zoomBandBtn, m_loopABtn, m_loopBBtn, a disabled btn and m_dssRowSpanSlider, and git diff b3e0302ea..HEAD -- src/gui touches none of them. The new sliders have accessible names and descriptions.

Verification at b67e7728c:

  • Tests: the lean build passes; fft_trace_decomb_test, fft_trace_smoothing_test, spectrum_overlay_fft_fps_bounds_test, waterfall_manual_black_test, waterfall_impulse_blanker_test and mini_pan_widget_test all pass (6/6).
  • Static gates: all of CI's static checks pass. Engine-boundary findings are identical to the merge base apart from shifted line numbers, and capability (70) and command-plane (368) counts are unchanged.
  • Touchpoint: exactly one new ui-support row (core/dsp/SpectralEnvelope.h, one includer), kept in core by maintainer decision.

@ten9876
ten9876 merged commit a4a39e7 into main Oct 11, 2026
10 checks passed
@ten9876
ten9876 deleted the feat/fft-trace-smoothing branch October 11, 2026 05:40
ten9876 added a commit that referenced this pull request Oct 12, 2026
… and re-take the Stable snapshot. Principle VIII. (#6396)

## Summary

This brings the user guide (docs.aethersdr.com) up to date with
everything in v26.10.2 and re-takes the Stable snapshot so the default
site shows it. Docs only, apart from the TCI page prose in
`tools/docs/gen_reference.py`; no application source, test or CI
changes.

Every page was audited against the 106 PRs in the release. Every fact
that was added or kept was checked against the source at `f753dbfc4`,
the v26.10.2 release commit, not taken from the PR bodies alone.

## What changed

- **Menu paths.**
- AetherModem, Callsign Lookup, AetherMap and FreeDV Reporter are on
**Tools**. They are created on View and then moved to Tools
(`MainWindow_Menus.cpp:1519-1520`, `:1569-1575`); the style guide's
example says **Tools → AetherMap...**.
  - **Show live CW contacts** is on **View**.
  - Waveforms is on **Tools**, not File.
  - The VUDU applet button was called TXDSP.
  - Labels now match the source's `...` vs `…`.
- **Gaps from this cycle.**
- **Windows and Network Diagnostics:** Windows Firewall rules and the
Network Diagnostics Windows Firewall page (#6289); the `aether.network`
and `aether.nvafx` log categories (#6289, #6308).
  - **Remote access:** Tailscale shim revocation semantics (#6325).
  - **Sleep, App Nap and power throttling (#6195):** per-OS notes.
- **Hermes-Lite 2:** state restore, RX audio at unity, CW at TUNE level,
multi-interface discovery and a discovery troubleshooting entry (#6259,
#6254, #5975, #6009, #6293).
- **ANAN-G2:** AGC-T range, a "What is remembered" section, and supply
voltage and link status (#6282, #6129, #6137, #6211).
  - **NB2** on the HL2 and ANAN-G2 (#5966).
- **DVK:** WAV import requirements, the slot menu without Delete, status
text and licensing (#6247, #6262, #6251).
  - **Copy Assist** from unprocessed audio (#6216).
  - **Live CW contacts** replace the decoder's contact card (#6268).
  - **Status-bar indicators** work from the keyboard (#6267).
- **Volume:** controllers, hotkeys and TCI `volume` follow the active
output (#6129, #6260).
- **Peripherals:** the KPA1500 (#5905), CTR2 link extensions and the
AetherKnob (#6338), TMate 2 udev (#6250), MIDI RADE Modem and FDV modes
(#6031, #6038).
  - **RTL-SDR:** live PPM (#6082).
  - **Icom:** COMP and the MK2 squelch scale (#6174, #6184).
- **Known issues.** Entries whose GitHub issues are closed are removed
(each checked with `gh issue view`). As decided, this includes three
closed today without a demonstrated fix: #5107, #4824 and #5665.
- **US spelling.** `docs-style-guide.md` asked for British spelling; it
now asks for US, by the maintainer's decision, and every page is
converted. The generated TCI pages are converted at their source, the
prose strings in `tools/docs/gen_reference.py`. Code spans, links and
URLs were left alone. UI labels were checked against the source strings,
and none of them are British. Two in-page anchors and two Log Analyzer
rule anchors (`#bnr-is-grayed-out-…`) follow the renamed headings.
- **Stable snapshot.** `snapshot_stable.py 26.10.2` was re-run on the
release tree with these corrections, before `main` was merged in, so
Stable carries no post-release docs. `docs/` also has `main`'s
post-release pages, in US spelling. Stable alone keeps the IC-9700
waterfall-rate entry (#5996): #6402 fixed it after the 26.10.2 tag, so
26.10.2 still has it. The PDF manual already attached to the v26.10.2
release keeps the tag's docs.

## Validation

- `npm ci && npm run build` in `docs/user`: success. This is the
broken-link and broken-anchor check.
- `node tools/docs/test_log_rules.mjs`: 707 checks passed for 45 rules.
- `python3 tools/docs/gen_reference.py --check`: up to date (8 pages).
- `git diff --check`: clean.
- Menu paths on a build of this branch (offscreen, isolated settings, no
radio), `menu list`: `Tools → AetherModem...`, `Tools → Callsign
Lookup...`, `Tools → AetherMap...`, `Tools → FreeDV Reporter...`, `Tools
→ Waveforms...`, `View → Show live CW contacts`.

## Screenshots now out of date (not recaptured)

These need a recapture pass on the radio:
- **Old panadapter title strip (#6386):** `panadapter`, `main-window`,
`first-session-*`, `multi-slice-two-slices`, `split-pair`,
`tnf-markers`, `offscreen-vfo-indicator`, `pan-3d-stacked-trace`,
`default-light-theme`.
- **Pre-canon windows (#6334, #6339, #6341, #6372):** `aetherrx*`,
`aethertx*`, `aethermodem-aprs`, `bnr-panel`, `connect-dialog`,
`connect-by-ip-card`, `connect-icom-rows`, `connect-smartlink-card`,
`rx-agc-calibration`.
- **Other:**
  - `title-bar` (lowercase tab states, #6281);
  - `menu-help` (#6294);
  - `display-panel` (#6371);
  - `support-diagnostics-dialog`;
  - `waveforms-dialog` and `tailnet-remote-access` (show shim 0.4.0);
  - `about-window` (v26.10.1);
  - `radio-setup-qrz`.

## Found during the audit, not fixed here

- **App string bug:** `TailnetShimDialog.cpp:628` tells the user to
"Connect remotely with Connect → Manual"; the real path is **File →
Connect to Radio... → Connect by IP**.
- **ANAN AGC-T tooltips:** the RX applet and VFO tooltips show the
slider position as dB, which is wrong on ANAN. #6282 lists this as a
follow-up.
- **MIDI Master Volume on non-Flex radios:** it is refused while PC
Audio is off, whereas hotkeys, FlexControl and HID drive the line out.
The docs describe the code as it is.
- **RTL mode names:** `rtl-sdr.md` writes "FM-N" and "CW-R", but the
source strings are `FMN` and `CWR`. Left as is until someone confirms
what the mode menu displays.
- **IC-705 COMP readback:** inferred from the MK2 (#6174) and not
verified on an IC-705.
- **Not yet documented:** the #6230 Radio Health row "Forward responses
skipped", and the Store/MSIX package.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <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.

1 participant