Repository navigation
Fix Flex noise-floor pop after dBm scale release - #6242
Conversation
check-windows failure on
|
| Check | Conclusion |
|---|---|
| check-windows | failure |
| Static checks | success |
| Sanitizer option configures | success |
| build (Linux) | in progress at time of writing |
| check-macos | in progress at time of writing |
So this is Windows-only — Linux and macOS hadn't finished yet, and both static-check jobs are green.
Inside check-windows, the failed step is:
Setup hidapi (HID encoder support) → failure
Setup PortAudio → skipped
Configure → skipped
Build → skipped
...
sccache stats → failure
Setup hidapi is .\scripts\setup\setup-hidapi.ps1, wired in at .github/workflows/ci.yml:626. It downloads the hidapi 0.14.0 source tarball from GitHub, SHA256-verifies it, and builds it with CMake + MSVC. It is pure build-environment provisioning — no AetherSDR source is involved, and it runs before Configure. Everything after it, including the entire compile, was skipped.
The trailing sccache stats failure is downstream noise, not a second problem: nothing compiled, so sccache had no stats to report.
One honest caveat: the CI log tool gives me per-step conclusions, not the step's stderr, and I don't have API credentials to pull the raw log. So I can't tell you which sub-step of the script failed — the tarball download, the SHA256 check, the CMake configure, or the hidapi build itself. All four are infrastructure, so the verdict holds either way; it only changes who has to act if it recurs (see below).
Why I'm confident it isn't you
- The parent commit on this same branch was green.
7725ebe2(Fix Flex FFT headroom and range reply convergence #6241's commit, which this branch sits on) passed check-windows, check-macos, build and both static jobs — run 37553836896. Same branch, same script, one commit earlier, no problem. - main was green hours before.
25de97c3passed check-windows — run 37553887930. - Your diff doesn't touch CI. The changed files are
src/core/SpectrumDecodeScale.h,PanadapterStream.{h,cpp},DbmRangeTransition.h,SpectrumPreviewLogic.h,SpectrumWidget.{h,cpp},MainWindow_{Session,Wiring}.cpp,RadioModel.{h,cpp}, three tests,tests/tests.cmakeand four docs files. No workflow, no setup script, nothird_partypinning.
What to do
Re-run the failed job (Actions → run 37568103426 → Re-run failed jobs). An empty commit works too, but a plain re-run is cheaper. No rebase, no code edit.
If it fails at the same step again, it stops being transient and becomes a maintainer item — flag it rather than chasing it, since the cause would be in scripts/setup/setup-hidapi.ps1 or the runner image. Worth knowing for that conversation: hidapi and PortAudio are the only two Windows dependencies not behind actions/cache — Qt, FFTW3, DeepFilterNet3 and sccache all have Restore/Save pairs, but hidapi re-downloads and rebuilds from source on every Windows run. That makes it a standing single point of transient failure. (The script also already carries a CMAKE_POLICY_VERSION_MINIMUM=3.5 workaround for CMake 4.x on windows-latest, so it has drifted with the runner image before.)
One thing this does mean
Because the job died before Configure, MSVC has not yet seen this change at all. A green check-windows re-run is required, not just tidiness, before anyone can say the new headers are clean under cl.exe — SpectrumPreviewLogic.h adds <span> and std::span, and DbmRangeTransition.h adds <limits> and new std::numeric_limits use. Both look fine for C++20, and your cmake --build build -j22 on macOS is good signal, but MSVC is the arm that tends to find the surprises in new DSP/geometry headers. Nothing to pre-emptively fix — just don't read the current red as a verdict on the code, and don't read a future red at Build as the same problem as this one.
No Copilot or reviewer comments have been posted on this PR yet, so there's nothing on that side to correlate with the failure.
Thanks for the unusually careful writeup on this one — the per-layer before/after table with the limits called out explicitly (deterministic replay vs. hardware vs. render proof kept separate) makes this much easier to review than it would otherwise be. The infrastructure flake is on us, not you.
🤖 aethersdr-agent · cost: $6.9473 · model: claude-opus-5
There was a problem hiding this comment.
Issue fit
Partially. The mechanism named in #6238 R7 — "a frame decoded before release could arrive afterward and prematurely complete the correction guard" — is the right root cause, and the design (immutable per-observation aperture + generation, bounded candidate reprojection ahead of smoothing/floor/waterfall/3D) addresses it correctly for a pan whose stream has already taken at least one range write. It does not apply to a pan whose stream has never had setDbmRange() called, which is a reachable startup state on Flex; see Blocker 1. The PR's own evidence configuration (midpoint −100 dBm, ±37.55 dB) never enters that state, which is consistent with the measurements being clean.
The body is accurate about what it does not claim (R5/R6, Auto Floor ON, Windows, KN7K's firmware). One staleness: "Hosted CI is pending" — all five checks are now green on c7c94dce (build, check-macos, check-windows, Static checks, Sanitizer option configures). Note that ci.yml's ctest steps are all -R-filtered to a handful of unrelated tests, so green CI does not mean panadapter_dbm_range_test ran.
Scope
| Files / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
Commit 7725ebe (19 files: SpectrumPreviewLogic.h, automation-bridge.md, tests.cmake, the clippedPeakRecoveryRange/NoiseFloorReacquisition/reply-convergence work, …) |
#6241 verbatim, carried as the stacked base | Yes — body says "contains its existing 7725ebe2 commit", with rebase-after-merge promised |
In scope as a stacked base; those findings belong to #6241's review, not here |
SpectrumDecodeScale.h, PanadapterStream.{h,cpp} (generation map, setDbmRange return, spectrumReady 4th arg) |
Per-observation aperture + generation on the decode side | Yes | In scope |
DbmRangeTransition.h (FrameGuard) |
Bounded candidate history + reprojection | Yes | In scope |
SpectrumWidget.{h,cpp}, MainWindow_Session.cpp, MainWindow_Wiring.cpp |
Carry the scale through the deferred feed; guard consulted before smoothing; RF-gain / geometry invalidation | Yes | In scope |
RadioModel.{h,cpp} |
qRegisterMetaType, signal 4th arg with a default so the non-Flex 3-arg emit at RadioModel.cpp:6778 still compiles |
Yes | In scope |
docs/architecture/vita49-format.md, touchpoint manifest 240→241 |
One new header (core/SpectrumDecodeScale.h, tagged universal) |
Yes | In scope; Static checks green confirms the generator agrees, so no manifest staleness |
tests/panadapter_dbm_range_test.cpp, tests/spectrum_confirmed_geometry_test.cpp |
New regressions | Yes | In scope |
No new public surface: no bridge verb, no capability field, no settings key, no protocol verb, no CHANGELOG.md entry. No deleted guard — the legacy DbmRangeTransition::evaluate path is retained, not removed (which is itself the subject of Blocker 1). No sibling call site left behind: SpectrumWidget::updateSpectrum has exactly two production callers (MainWindow_Session.cpp:1826 and :1840) and both pass the scale. Nothing in the diff is unexplained by R7 or by the disclosed #6241 base.
Socket-test disclosure: neither new/modified test opens, binds, listens or connects. panadapter_dbm_range_test drives PanadapterStream::decodeFFT directly through a friend struct (PanadapterDbmRangeTestAccess) with a hand-built 48-byte buffer and a Qt::QueuedConnection to a plain QObject; spectrum_confirmed_geometry_test is an offscreen real-widget test with no transport. No fake radio/firmware peer is added, and none is removed.
Blockers
1. src/core/SpectrumDecodeScale.h:18 — a Flex stream that never took a range write produces generation == 0, so valid() is false and the whole repair is inert; the first drag then falls into the superseded path, which prematurely completes and destroys the frame guard.
return generation != 0 && dbmRangeLooksPlausible(minDbm, maxDbm);The chain, all in the head checkout:
- Generations are assigned only inside
PanadapterStream::setDbmRange'sapplyScale().registerPanStream(PanadapterStream.cpp:420-425) assigns none. RadioModel.cpp:916-923calls the stream'ssetDbmRangeonly when the model actually changed:if (pan->setRange(minDbm, maxDbm)) { if (m_panStream) m_panStream->setDbmRange(pan->panStreamId(), pan->minDbm(), pan->maxDbm()); }
PanadapterModeldefaults tom_minDbm{-130.0f} / m_maxDbm{-40.0f}(PanadapterModel.h:279-280) — the same pairdecodeFFTuses as its fallback (PanadapterStream.cpp:974). A Flex pan whose stored range is-130/-40therefore produces nosetRange()change, no stream write, and no generation.- The other two candidate seeds don't close it:
MainWindow_Wiring.cpp:2727is insidereacquireNoiseFloorLocksAfterProfileLoad()(profile load only), andrestoreClientOwnedDbmRange(:3193-3204) is gated onclientOwnsPanDbmRange()and only writes the model. - Meanwhile #6241's seed at
MainWindow_Wiring.cpp:3698-3700callssw->setEncoderDbmRange(pan->minDbm(), pan->maxDbm())directly, so the widget has a valid encoder range and armsFrameGuardfrom it while the stream is still at generation 0. The two halves disagree silently. - First release in that state:
beginDbmRangeTransitionarms the guard,setStreamDbmRange(..., true)assigns generation 1 — but the already-decoded, still-queued pre-release frames carry generation 0. AtSpectrumWidget.cpp:8511decodeScale.valid()is false, so control falls toelse if (m_dbmReleaseRebaseUntilMs > 0)and the legacyDbmRangeTransition::evaluateruns. Those frames were decoded at the old aperture, so they matchm_binsdirectly:directMedian ≈ 0,rebasedMedian ≈ 37 dB→newEncodingObserved = true→clearDbmReleaseRebase()with the defaultforgetFrames = true, which callsm_dbmFrameGuard.clear(). Post-release old-wire frames then arrive with generation 1,FrameGuard::evaluatereturns early onm_candidates.isEmpty(), and nothing is corrected. That is R7 reproduced on the first scale release of the session.
Neither new test can see this: panadapter_dbm_range_test calls stream.setDbmRange(kStreamId, -165, -35) before its first fft(), and spectrum_confirmed_geometry_test hands the widget generation 1 directly without going through PanadapterStream.
Also worth tightening: the header comment at SpectrumDecodeScale.h:10 ("An empty scale denotes absolute-level samples with no pixel-range encoding") conflates "absolute-level producer" with "Flex stream not yet configured". Only the first is intended; the second is this defect.
Smallest fix — mint on first use in decodeFFT, still under m_streamMutex, replacing PanadapterStream.cpp:974-975:
dbmRange = m_dbmRanges.value(streamId, {-130.0f, -40.0f});
quint64& generation = m_dbmRangeGenerations[streamId];
if (generation == 0) {
generation = ++m_nextDbmRangeGeneration;
}
decodeScale = {dbmRange.first, dbmRange.second, generation};unregisterPanStream / clearRegisteredStreams already drop the entry, so teardown is unchanged. Seeding in registerPanStream instead would work equally well. Either way please add the regression: registerPanStream → PanadapterDbmRangeTestAccess::fft() → assert the queued scale is valid() and that a later setDbmRange yields a strictly greater generation. That is a deterministic socket-free seam, so the coverage ask is merge-gating rather than a nit.
2. src/gui/MainWindow_Wiring.cpp:3706-3715 — when the stream rejects a range as a stale echo, the caller pairs the rejected endpoints with the accepted range's generation, leaving the widget's encoder cache describing an aperture the decoder never used.
SpectrumDecodeScale decodeScale;
if (auto* pan = m_radioModel.panadapter(applet->panId())) {
sw->setEncoderYPixels(pan->fftYPixels());
if (pan->panStreamId() && m_radioModel.panStream()) {
decodeScale = m_radioModel.panStream()->setDbmRange(
pan->panStreamId(), minDbm, maxDbm, waitForEcho);
}
}
sw->setEncoderDbmRange(minDbm, maxDbm, decodeScale.generation);PanadapterStream::setDbmRange's stale-echo branch (PanadapterStream.cpp:533-543) returns currentScale() — the pending range and its generation — without applying minDbm/maxDbm. The new test documents exactly this: staleEcho = stream.setDbmRange(kStreamId, -165, -35) after a pending -127.5/2.5 asserts staleEcho.generation == requested.generation. At the call site above, the returned generation is then paired with the refused minDbm/maxDbm.
Reachable via the levelChanged handler's fallthrough at MainWindow_Wiring.cpp:4107 (setStreamDbmRange(minDbm, maxDbm); on HandshakeAction::Ignore) while the stream still holds a pending echo. Consequence: m_encoderMinDbm/m_encoderMaxDbm describe a range the decoder refused, so the next beginDbmRangeTransition arms FrameGuard with {m_encoderMinDbm, m_encoderMaxDbm} and the actual old wire aperture is never a candidate — the release pop is uncorrectable for that transition. It also runs prepareForFftScaleChange() (fresh settle window, noise-floor reacquisition) for a decoder change that did not happen, and flexInputFloorLooksClipped() / requestFlexRadioHeadroom() compute rowStepDb from the wrong aperture.
The honest shape is to use what the stream reports rather than what was asked for, e.g. only update the widget when the write was adopted:
if (decodeScale.valid()) {
sw->setEncoderDbmRange(decodeScale.minDbm, decodeScale.maxDbm,
decodeScale.generation);
} else {
sw->setEncoderDbmRange(minDbm, maxDbm);
}I'd rather you pick the shape than take that verbatim — the valid() predicate is itself Blocker 1's subject, and the non-Flex / no-stream arm needs to keep working. Either way, please state in the body which of minDbm/maxDbm and decodeScale is authoritative for the widget cache.
Nits (non-blocking)
src/gui/SpectrumWidget.h:556-560—setRfGainandsetFrequencyRangeInternaldiscard the candidate history, but an antenna change does not:reacquireNoiseFloorLock()→prepareForFftScaleChange()touches neitherclearDbmReleaseRebase()norm_dbmFrameGuard.vita49-format.mdlists "RF-gain or accepted RF geometry changes"; #6241's bridge doc fornoiseFloorReacquiringlists antenna alongside them. Low probability inside the 2 s window, but the invalidation set should be one list.src/gui/SpectrumWidget.cpp:3133—setEncoderDbmRange(min, max, 0)reachessetTargetScale({min, max, 0}), which can reset an already-correctm_targetGenerationback to 0 whensetStreamDbmRangeruns with no pan or no stream (teardown, pan removal). Today the only effect is that the transition completes by timeout instead of on the first target-aperture frame, but adecodeGeneration != 0guard insetTargetScalewould make that explicit.src/gui/SpectrumWidget.cpp:7769—clearDbmReleaseRebase()runs before the small-nudge "already animating toward this exact target" early return at:7838, so a redundant radio echo discards the history for a geometry change already in flight. Harmless (the earlier call already cleared it) but it means the clear is not strictly "accepted changes only" asvita49-format.mdsays.MainWindow_Session.cpp:1852-1863— the otherpanFeedSpectrumReadyconsumers (incl. the spectrogram/CNN feed) still receive uncorrected bins. R7 is about the visible floor so this is out of scope; naming it so the next reader doesn't assume the correction is global.- The widget-side wiring (
clearDbmReleaseRebase(false),preserveFrameTransition, theupdateSpectrumbranch) is covered only byspectrum_confirmed_geometry_test, which is behindoption(AETHER_BUILD_SPECTRUM_GESTURE_TEST ... OFF). The cmake comment attests/tests.cmake:2209-2212gives a real reason and the body discloses it, so not a blocker — but it does mean the only default-graph coverage of this PR ispanadapter_dbm_range_test, and that test's widget-side assertions live in the opt-in file.
What I tried to break
- Thread safety of the new generation map.
m_dbmRangeGenerationsis written insetDbmRange(GUI thread) and read indecodeFFT(network thread); both are insideQMutexLocker lock(&m_streamMutex), and the read is bundled with them_dbmRangesread in the same critical section, so the aperture and its generation cannot be torn apart.m_nextDbmRangeGenerationis only touched under the same lock. Clean.unregisterPanStreamandclearRegisteredStreamsboth drop the new entry, so a recreated stream can't inherit a stale generation, and the counter stays monotone across recreation. - Division by zero / NaN in the reprojection.
decodeScale.valid()requiresdbmRangeLooksPlausible, which enforcesrangeDb >= 10, sodecodeSpancan never be 0;errorForguards both operands withstd::isfinite.rowStepDb = (max-min)/(m_encoderYPixels-1)inflexInputFloorLooksClipped/requestFlexRadioHeadroomlooked like a divide-by-zero until I readsetEncoderYPixels(SpectrumWidget.h:292-296), which refusesyPixels <= 1and defaults to 700.PanadapterStream::setDbmRangeclampsminDbmtokMinSpectrumDbm = -180.0f, exactly the plausibility floor, so the clamp cannot produce a range its own predicate rejects. - The queued-signal path. Added a 4th argument to a signal crossing a thread boundary;
Q_DECLARE_METATYPE(SpectrumDecodeScale)is present andRadioModelregisters it at construction (RadioModel.cpp:2003). The default argument onpanFeedSpectrumReadykeeps the 3-arg non-Flex emit atRadioModel.cpp:6778compiling and deliberately yields an empty scale, which routes those backends to the unchanged legacy behaviour — that part is correct, and it's also what made Blocker 1 easy to miss. - Whether the tests would pass against the unfixed code. The generation comparison in
FrameGuard::evaluateis load-bearing for exactly one assertion — the A→B→A case (frames.arm(middle, first, …)with a gen-1 decode against target gen 3, andspectrum_confirmed_geometry_test's "cannot complete using an earlier A decode generation"). The "a queued pre-release decode cannot complete a new range transition" assertion passes without the generation check too, because the range mismatch alone defeatsnewEncodingObserved. So the reversal case is the one that proves the new mechanism; the rest would survive a weaker implementation. Worth knowing if you mutate further. - Sibling surfaces and dead additions.
core/SpectrumDecodeScale.his referenced byPanadapterStream.h,RadioModel.handDbmRangeTransition.h, and reaches the touchpoint manifest — not a dead file. Both productionupdateSpectrumcallers pass the scale.pan_frame_guard_testalready existed onmain(tests.cmake:2234), so the PR body's reference to it is to pre-existing coverage, not an addition. - Could a candidate win spuriously and rescale a correct frame?
errorFortakes a per-bin median over up to 256 samples andkImprovementDb = 0.75must be beaten, so a 37 dB wrong aperture is never competitive with the true one. For a small requested shift (a few dB) the candidates sit close together and a genuine scene change of similar magnitude could flip the selection — but the resulting error is bounded by the shift itself, so this is a real-but-minor sharp edge rather than a finding.
Everything above is reasoned from the head checkout at /tmp/aetherclaude/pr-6242. I have no build, no test run and no way to drive the GUI, so nothing here was reproduced at runtime; the two blockers are code-path arguments, and Blocker 1 in particular is the one I'd most like you to disprove with a targeted test before dismissing. I could not check the mutation results, the replay figures, the 5,446-FFT parity cross-check, or the native capture — the raw data is local by design, which is fine, but it does mean the live evidence is unverifiable from here.
Recommendation
Request changes, narrowly. The architecture is right and the diff is genuinely careful — the immutable-per-observation aperture is the correct seam, the candidate expansion over intermediate endpoint combinations is a real insight, and the invalidation hooks for RF-gain/geometry are the right instinct. But Blocker 1 means a Flex pan sitting at the shared -130/-40 default gets none of it and still pops on the first release, and Blocker 2 leaves the widget's encoder cache able to describe an aperture the decoder refused — which disarms the guard for the next transition. Both are small, local changes with a deterministic socket-free test seam.
Next step: seed the generation (or drop the generation != 0 sentinel), decide which of the requested range and the returned scale is authoritative at MainWindow_Wiring.cpp:3714, and add the registerPanStream → fft() → valid() assertion. #6241 must land first and this branch rebased, as the body already commits to; commit 7725ebe's findings belong to that review.
Thanks for the unusually honest limits section — "this PR does not claim to resolve every symptom", the explicit separation of evidence layers, and the note that rendered telemetry describes draw submission rather than scanout all made this much faster to review.
🤖 aethersdr-agent · cost: $14.6161 · model: claude-opus-5
|
Antares Detector — candidate vulnerable file(s)
The dbmRangeLooksPlausible() function computes rangeDb = maxDbm - minDbm and then checks bounds. If minDbm is near float::max and maxDbm is finite, the subtraction can underflow/wrap, producing an implausible range that bypasses the intended checks (overflow/out-of-bounds in a bounded range check). 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: $14.8335 · model: claude-opus-5 |
NF0T
left a comment
There was a problem hiding this comment.
Independent review of the R7 commit c7c94dce. 7725ebe2 is #6241's commit and is reviewed there, in #6241's CHANGES_REQUESTED. I formed this before comparing against the @aethersdr-agent review, and I note below where I agree and where I don't.
What's right
- The diagnosis is correct, and the seam is the right one. Command acceptance says nothing about which aperture an in-flight FFT used. Capturing the aperture and generation under
m_streamMutexindecodeFFT, in the same critical section as them_dbmRangesread, makes them untearable. Carrying them with the observation through the queued signal anddeferReceivePresentationis exactly how the provenance survives the thread hop.qRegisterMetaType<SpectrumDecodeScale>()is present, and the defaulted fourth argument keeps the non-Flex 3-arg emit routing to the legacy path. - Reprojecting before smoothing, the floor tracker, the waterfall fallback and 3D history is the only placement that keeps a bad frame out of the EMA. Correcting after smoothing could not undo it.
- The intermediate-endpoint candidates are a real insight.
{old.min, new.max}and{new.min, old.max}are what the recorded affine error points at. Retaining the original aperture when the 32-entry cap evicts (removeAt(1)) is the right choice. - The A→B→A generation check.
newEncodingObservedrequiresdecoded.generation == m_targetGeneration, so an older A decode cannot retire a newer A request. - Scene invalidation on RF gain and accepted geometry changes stops a genuine level change from being reprojected away.
Inherited from #6241
This branch still carries if (!sendDbmRangeCommand(minDbm, maxDbm)) { return; } at MainWindow_Wiring.cpp:4320 and :4397, so it inherits the Hermes-Lite 2 dBm-range persistence blocker from my #6241 review. Nothing more is needed here: once #6241 is fixed and this branch is rebased as the body plans, it goes away.
Blocker: a Flex stream that never took a range write decodes with generation 0, and the repair is inert on it
I agree with the bot's Blocker 1 and checked it independently against the head:
- Generations are minted only in
PanadapterStream::setDbmRange()'sapplyScale(). - That stream write is reached from
RadioModel.cpp:921only whenpan->setRange()reports a change. It is also reached from profile load (MainWindow_Wiring.cpp:2727), the Kiwi→Flex restore (MainWindow_KiwiSdr.cpp:446) and the wiring lambda.PanadapterModelstarts at-130/-40, the same pairdecodeFFTfalls back to. - So a pan whose status is
-130/-40never mints a generation. Every queued FFT then carriesvalid() == false. - I checked a wider trigger and ruled it out:
panStreamId()is derived from the pan id, so a stream id arriving late is not a second path.
On the first release in that state, the pre-release frames take the legacy evaluate branch, match the old interpretation and call clearDbmReleaseRebase() with forgetFrames = true. That clears m_dbmFrameGuard before the post-release old-wire frames arrive with generation 1, so R7 reproduces on the session's first scale release.
The new test can't see this because it calls setDbmRange(kStreamId, -165, -35) before its first fft(). Suggested fix inline: mint on first decode, still under the mutex. unregisterPanStream/clearRegisteredStreams already drop the entry. A socket-free regression on the existing friend seam would pin it: registerPanStream → fft() → assert the queued scale is valid(), and that a later setDbmRange yields a strictly greater generation.
Questions (non-blocking for me)
1. Should the widget's encoder cache take the stream's answer rather than the request? The bot's Blocker 2 is real, but I'd scope it narrower.
- Origin: the request-wins cache write is #6241's. Its
setStreamDbmRangealready set*encoderDbmRangeandsetEncoderDbmRange(minDbm, maxDbm)regardless of whether the stream adopted the range. - What this commit adds: a generation alongside.
setTargetScale()already refuses a mismatched range, so the generation can't mis-target the guard; the stale cache is what then feedsFrameGuard::arm. - Reachability: the bot's route is not right.
observeRadioRangereturnsApplyRadioRange, neverIgnore, when inactive. The routes I found are narrow ones where a non-echo write meets a live stream pending range:- the profile-load-settling auto-floor branch;
- a rejected reply whose prior range differs from the model.
Using the returned decodeScale for the cache when it is valid() would close it at the root. Your call whether it goes here or into #6241.
2. Is the recovery-axis ordering in the new accepted-reply branch intended? At MainWindow_Wiring.cpp:3839, guard->setDbmRange() runs before the *encoderRecoveryPending check. applyAuthoritativeDbmRange deliberately does the reverse, so that an encoder-only recovery never moves the presentation axis. I believe it is unreachable today: it needs pan->setRange() to report no change on an accepted reply, and a matching status would already have cleared the handshake. Mirroring the existing order would make it unreachable by construction.
3. Windows. The same question as on #6241: all live evidence is from macOS/Metal. check-windows is now green, so MSVC has compiled the new headers (the bot's 03:48 note predates the re-run). Could the release-pop recipe be run on a Windows host before #5525/#6238 R7 are treated as addressed?
Other review comments
- Antares advisory on
dbmRangeLooksPlausible: refuted. Both inputs must passstd::isfinite. IEEE float subtraction overflows to+infand does not wrap, which the<= 180bound rejects, andminDbm >= -180rejects an extreme minimum on its own. It namesdBmRangePlausibility.h(wrong case), and this PR doesn't change that file. - Bot nits I agree with: antenna changes don't invalidate the candidate history while RF gain does;
setTargetScalecan reset a correct target generation to 0 on a no-stream write; the new widget wiring is only covered by the opt-inspectrum_confirmed_geometry_test. The PR discloses the last one. - Body staleness: "Hosted CI is pending" — all five checks are now green on
c7c94dce.
Scope
| Group | Claimed | Verdict |
|---|---|---|
7725ebe2 (#6241) |
Yes, stacked base | Reviewed on #6241 |
SpectrumDecodeScale.h, PanadapterStream.{h,cpp} |
Per-observation aperture/generation | In scope |
DbmRangeTransition.h FrameGuard |
Bounded candidate reprojection | In scope |
SpectrumWidget.{h,cpp}, MainWindow_{Session,Wiring}.cpp |
Carry and consult the scale; invalidation | In scope |
RadioModel.{h,cpp} |
Metatype + signal argument | In scope; #5554 gate: no family branch, wire text, cast or capability |
docs/architecture/* |
New universal touchpoint (241), VITA notes | In scope; Static checks agree |
tests/* |
New regressions | In scope; no socket/peer constructs |
Preference vs fix: a fix for the reported #6238 R7 pop. No default or UI behaviour change, and nothing new leaves the machine: this commit adds no radio writes. No CHANGELOG.md entry, which is correct.
What I tried to break
- Generation 0 at startup. Traced every stream
setDbmRangecaller. Confirmed above; the widget gets a valid encoder range from #6241's direct seed while the stream has none. - Stale-echo pairing. Traced reachability through each handshake exit (match, timeout retire/reconcile, cancel for authority, accepted/rejected reply). Only the narrow routes in Question 1 reach it.
- Thread tearing. Aperture and generation are read in one locked section, and teardown drops both maps.
- Division/NaN in the reprojection.
valid()impliesrangeDb >= 10, anderrorForguards both operands. - Candidate eviction under rapid reversals. The original aperture is retained, intermediates are deduplicated and implausible ones are refused.
- Not run. The automation bridge was unavailable this session and I built nothing, so every finding here is reasoned from code at
c7c94dce, not reproduced. I couldn't check the replay figures, the 5,446-FFT parity cross-check or the native captures, since the raw data is local by design. I also didn't verify the bot's claim that only the A→B→A assertion depends on the generation check.
CI: all five checks green. Commits are signed and GitHub-verified. All files are Tier 3.
Verdict: request changes, for the generation-0 seed and its regression test, plus the inherited #6241 blocker, which the planned rebase resolves. The design is sound, and the evidence layering in the body (replay vs. pipeline vs. native draw, each with its limits) is the kind of honesty that makes a change like this reviewable.
73,
Ryan NF0T
👨🏼💻 Co-authored by Claude Opus 5.5
There was a problem hiding this comment.
This is the disclosed R7 follow-up to #6238, stacked on #6241. Carrying the decoder aperture/generation corrects the tested queued-frame/reversal cases, but it misses the first scale transition when the producer has never received a changed dBm range. The full head also retains #6241's already-reviewed HL2 regression. Reviewed c7c94dcebad13b1026eb680baba163ca3378ebaa: request changes.
| File/group | Claimed change | Scope verdict |
|---|---|---|
Inherited commit 7725ebe2 |
#6241 R1–R4 range/reply repair | Explicit stack; prior blocker remains |
SpectrumDecodeScale.h, PanadapterStream.cpp/.h |
Aperture/generation attached to each decoded FFT | Direct R7 fix; initial-generation blocker |
DbmRangeTransition.h, SpectrumPreviewLogic.h |
Bounded in-flight aperture candidates | Direct fix |
SpectrumWidget.cpp/.h, MainWindow session/wiring |
Correct before smoothing/history and preserve guard | Direct fix |
RadioModel.cpp/.h |
Propagate typed observation/metatype | In scope |
| Three changed tests, test registration | Producer/helper/real-widget replay coverage | In scope; real-widget lane is explicit opt-in |
| VITA/bridge documentation and touchpoint/tag manifests | Describe scale ownership and generation | In scope; new universal record, no vendor declassification |
The stack and limits are disclosed; no unrelated default, dependency, theme, changelog or extra radio-write feature. This is a partial repair, not closure of every report in #6238.
Blockers
-
src/core/backends/flex/PanadapterStream.cpp:974–975;src/gui/SpectrumWidget.cpp:8521–8542— the first default-aperture FFT has generation zero, so a queued frame destroys the new guard. Registration does not seedm_dbmRangeGenerations.decodeFFT()uses a fallback −130/−40 aperture but a default generation of zero.SpectrumDecodeScale::valid()rejects it. This is reachable when the pan's status matches its built-in −130/−40 range:RadioModel.cpp:917updates the stream only ifpan->setRange()changes the model, and initial GUI seeding does not seed the stream.The actual socket-free producer seam, with a registered stream and no prior
setDbmRange(), returned:first FFT aperture=-130,-40 generation=0 valid=0 FAIL: decoded.valid()A focused replay using the real production SpectrumWidget then reproduced the consequence: seed physical −104 dBm at that default aperture, release to −100/−10, deliver the queued pre-release generation-zero frame, then an old-wire frame decoded under generation one.
queued generation=0 pending=0 physical=-104 corrected=-74.000000The invalid observation takes the legacy branch and calls
clearDbmReleaseRebase()with its defaultforgetFrames=true, so the next old-wire frame enters at a false level. The same replay with initial generation one keepspending=1andcorrected=-104; its two checks pass. This is a reproduced production-widget input correction failure, not a claimed offscreen GPU/pixel observation.Mint a nonzero generation on first decode under the existing mutex. Replace the current decoder metadata assignment with:
quint64& generation = m_dbmRangeGenerations[streamId]; if (generation == 0) { generation = ++m_nextDbmRangeGeneration; } decodeScale = {dbmRange.first, dbmRange.second, generation};I compiled this replacement against the producer reproducer: it returned
generation=1 valid=1and passed. Source restored afterwards. Add the register→first FFT→release regression without explicitly callingsetDbmRange()before the first observation; the current producer/widget tests preseed nonzero generations and miss this case. -
src/gui/MainWindow_Wiring.cpp:4320,4397— the stack still prevents HL2 dBm gestures from adopting/persisting client-owned range. This is inherited unchanged from #6241, not a new allegation about the generation commit. Both handlers return when the Flex command dispatch fails, beforeadoptClientOwnedDbmRange(). HL2 declares local range persistence but has no Flex command plane; its radio-scale flag does not route it into the fixed-scale arm.The exact full head again fails
client_display_pan_settings_test:FAIL: the send comes before the mark is read 1 check(s) failedThat source-order assertion is not the whole evidence: our earlier #6241 review on the identical inherited commit independently probed the real unconnected HL2 backend (
clientPersistsDbmRange=1, no command plane, dispatch false) and both handler early returns, and ran this test successfully on the same merge base30882701a. The source pin and actual capability/dispatch behavior agree. No live HL2/restart claim is made here.Preserve client-owned adoption independently of the Flex send result. For both three-line early-return blocks, the minimal replacement from that review is:
sendDbmRangeCommand(minDbm, maxDbm);The following ownership-shaped adoption already refuses non-owners/invalid ranges. Resolve this in #6241 and rebase the stack, or carry the fix here; don't merge the full head while it retains the regression.
Non-blocking notes:
src/gui/MainWindow_Wiring.cpp:3706–3715: the stream can return its retained scale when refusing stale endpoints, while the caller caches the requested endpoints. Prefer the returned aperture for the encoder cache. I verified the return contract and caller, but did not reproduce a full profile-settling/rejected-reply GUI sequence. The bot's claimedHandshakeAction::Ignoreroute is incorrect: inactiveobserveRadioRange()returns ApplyRadioRange. This is a narrowly scoped hardening/completeness note, not a second independently reproduced release-pop blocker.tests/tests.cmake:2206: preserve the documented-DAETHER_BUILD_SPECTRUM_GESTURE_TEST=ONopt-in lane; do not expand the frozen per-PR gate. The default CI does not execute this production-widget replay.docs/architecture/vita49-format.md:36: Windows/8600, firmware-specific status/reply timing, second owned pan, resize/y_pixels and post-TX stress remain unverified locally. Demo does not establish physical Flex encoder convergence or settle those reports.
Verification/attacks: Built exact-head app and four focused targets. panadapter_dbm_range_test, spectrum_preview_logic_test, dbm_range_plausibility_test passed; the client-display test failed as above. Enabled the opt-in registration and compiled the unchanged real-widget test against the exact app's production desktop objects to avoid duplicate desktop compilation; its CTest passed 1/1. The additional focused producer/widget fixtures failed at the first generation and passed with nonzero seeding. Removing the generation equality from the new FrameGuard made the PR's rapid-reversal assertion fail. All temporary scratch source changes were restored; the three core focused tests passed again.
Drove owned pr-6242-review with fresh isolated settings, explicit socket, offscreen and TX disabled; connected only DEMO-0001, tuned 14.1 MHz, dragged the actual right-hand dBm scale 30 pixels, queried model/trace diagnostics, disconnected/reconnected and exited via Quit. The accepted GUI range converged:
{"refLevelDbm":-29.7727279663,"dynamicRangeDb":90,
"encoderMinDbm":-119.7727279663,"encoderMaxDbm":-29.7727279663}The demo retained one owned slice/pan, 1024 finite input bins and transmitting:false. Offscreen renderedTrace.valid was false, so no screenshot/scanout claim is made. The first-generation defect was tested at the real decoder and widget seams; Demo cannot supply the Flex encoded-FFT lag that causes it.
Head/base engine, command-plane, capability, registration, settings, timeout and frozen-gate checks passed without per-file growth; manifests reproduce and colour/accessibility counts did not regress. Hosted required checks passed on an older merge of main. Commits are GitHub-verified. No new socket-owning modified test or synthetic firmware peer. Separate automated-review skill unavailable; existing bot/human leads were independently checked, including rejection of the non-finite advisory because the validator checks both inputs with isfinite.
Recommendation: Request changes. Seed the first decoder generation and protect the default-aperture replay; resolve the inherited HL2 adoption regression and rebase the stack before approval. The valid-generation replay coverage is useful, but it does not yet cover the session's first release.
Final CI recheck: required checks still pass and the reviewed head is unchanged. Current main is 14ef3e78841009d73ddf2e7de22b9a5a83d5ee0a (2026-10-08 08:23:34 UTC), newer than this head's earliest pull-request run creation. With non-strict branch protection, the green checks do not prove current-main integration.
Carry each observation's decoder aperture and generation through the existing queued spectrum feed. Reproject in-flight wire apertures before smoothing, including partial endpoint updates and rapid range reversals. Retain a bounded candidate history after target encoding first appears; command acceptance does not identify the aperture of an FFT already in flight. Discard comparisons when RF gain or accepted frequency geometry changes so real reception changes retain their levels. Matching replies preserve frame context. Refs aethersdr#6238 (R7). Follow-up dependent on aethersdr#6241. Validation: full macOS build, eight selected offscreen tests, mutation checks, recorded-frame C++ replay, and RX-only FLEX-8400M bridge/native capture. Twenty held releases stayed within 1.53 dB at the native rendered floor; ten rapid reversal pairs stayed within 1.94 dB at the smoothed-bin median. These are separate evidence layers. Raw VITA and logs remain local.
…a valid generation. Principle II. A pan whose status matches the -130/-40 default never reaches PanadapterStream::setDbmRange(), so its frames decoded with generation 0, SpectrumDecodeScale::valid() rejected them, and the first scale release took the legacy path and cleared the release frame guard. decodeFFT() now mints a generation on first decode, under the same mutex, and panadapter_dbm_range_test pins it: register, decode, assert valid, then a real write supersedes it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
c7c94dc to
c983a78
Compare
Both requested changes are resolved. (1) Generation 0 on a never-written stream: fixed in c983a78 with your suggested seed in decodeFFT() under the mutex. The new panadapter_dbm_range_test regression (register, decode, valid(), then a strictly greater generation after a real write) fails without the seed (first.valid() at line 202) and passes with it. (2) The inherited HL2 blocker: the branch is now Pat's R7 commit replayed onto main after #6241's squash ac2f0ce, which carries the HL2 adopt fix. client_display_pan_settings_test passes on the rebased head. Your questions 1-3 and the bot nits are answered in the PR conversation; under the maintainer's rule they are non-blocking and are not changed here.
|
Landing pass at Blockers, both resolved:
The maintainer accepted the new Not blocking, so not changed in this pass (the maintainer's rule is that nits don't hold a landing):
|
ten9876
left a comment
There was a problem hiding this comment.
Approving at c983a7884.
What changed in this pass:
5f3568d5c: Pat's R7 commit replayed onto main after #6241 (ac2f0ce3b), with six one-hunk conflicts resolved by hand (maintainer choice). This also carries #6241's HL2 adopt fix, so the inherited blocker is gone.c983a7884: a stream with no written range mints a decode generation on its first FFT (NF0T and maintainer-review blocker). Pinned by a newpanadapter_dbm_range_testblock, which fails without the seed.
Maintainer decisions:
- replay onto main rather than merge;
- accept the
core/SpectrumDecodeScale.htouchpoint; - non-blocking findings are answered in the conversation, not fixed.
Verified:
- 7/7 focused tests on Linux/Qt 6.12, including the opt-in widget test and
client_display_pan_settings_test. - Engine-boundary and command-plane findings identical to main; capability records at 70; manifest clean.
- Demo arrow clicks converged the model and encoder (-130/-50, -130/-60; generations 2 and 3), with
transmitting: false. - Required CI green on this head, built against current main
ac2f0ce3b.
Not verified:
- live Flex release-pop timing (the demo has no encoded-FFT lag);
- Windows.
tci_server_review_test was not run (socket-owning).
Follow-up to #6241 (merged as
ac2f0ce3b), rebased onto main so the diff holds only this repair. It addresses R7 in #6238: the noise floor popping immediately after release of the right dBm scale.With FFT Floor Auto OFF, MW AM, normal 2D and default averaging/FPS, shifting the entire right-side range by about 37.55 dB could produce a roughly 27 dB false floor excursion after mouse-up. A frame decoded before release could arrive afterward and prematurely complete the correction guard; subsequent old-wire frames then entered averaging under the new decoder scale. Command acceptance did not identify the aperture of an FFT already in flight.
The repair carries immutable decode aperture/generation with each observation through the existing queued/deferred spectrum feed, then reprojects plausible in-flight apertures before smoothing, floor tracking, waterfall fallback and 3D history. It retains at most 32 candidate ranges for the existing two-second window, including rapid reversals and intermediate min/max combinations. Recorded samples matched a temporary old-min/new-max aperture across all 2,696 bins; mixed UDP fragments did not explain that affine error. The endpoint timing is inferred from those samples, not a new firmware guarantee.
Matching replies preserve frame context. Older decode generations cannot complete an A→B→A request. Cancellation, authoritative reconciliation, RF-gain changes and accepted frequency/span changes discard the comparison history so genuine reception changes retain their levels. Absolute-level producers keep an empty context. The new engine/UI payload contains three scalars, no vendor types or commands, and follows the existing route without another thread.
Before / after results
The raw/native capture used temporary record-only probes around the same repair. Those probes are absent from this PR and the production app. The later RF-scene invalidation hooks have direct production-widget tests and a separate uninstrumented RX bridge run. The complete final production source at
c7c94dcebad13b1026eb680baba163ca3378ebaabuilt successfully withcmake --build build -j22; its eight selected tests passed. A read-only merge-tree check against refreshed main25de97c3was clean; integration tests on that merged tree have not been run.Proof
Per-frame native rendered floor, two captured failing releases → 20 held releases with the repair:
Native after screenshot from our RX-only FLEX-8400M run (static appearance; timing proof is the chart above):
Validation and remaining scope
Selected tests:
panadapter_dbm_range_test,spectrum_preview_logic_test,dbm_range_plausibility_test,spectrum_confirmed_geometry_test,noise_floor_auto_adjust_gate_test,pan_frame_guard_test,backend_family_switch_test,rtl_model_acceptance_test. Run withQT_QPA_PLATFORM=offscreen ctest --test-dir build -j22 --no-tests=errorand the explicit eight-test selection. The opt-in real-widget cases are not part of the default CI graph.A stream whose range was never written (its status matches the -130/-40 default) mints a decode generation on its first FFT, so the session's first release is guarded as well.
panadapter_dbm_range_testpins this: register a stream, decode, assert the scale isvalid(), then check that a real write supersedes it. The test fails without the seed.On the rebased head (
c983a788, Linux/Qt 6.12, with-DAETHER_BUILD_SPECTRUM_GESTURE_TEST=ON), 7/7 tests passed:panadapter_dbm_range_test,spectrum_preview_logic_test,dbm_range_plausibility_test,noise_floor_auto_adjust_gate_test,pan_frame_guard_test,client_display_pan_settings_testandspectrum_confirmed_geometry_test. On the demo radio, arrow clicks converged the model and encoder to -130/-50 and then -130/-60, with encoder generations 2 and 3, 1024 finite bins andtransmitting: false.Engine-boundary findings match the merge-base baseline per file (102 tracked findings, no additions); capability count stays 70; raw command-plane count stays 369 across 32 files. Generated touchpoints, frozen CI gate, bridge documentation and whitespace checks passed. No new vendor include, raw command, capability, thread, setting, bridge verb or transmit behavior was added.
The agent automation bridge drove phaseful native gestures, captured snapshots/screenshots and confirmed positive RX control convergence. All sampled MOX/tune/transmitting states were false; touched slice/pan/filter/step and global line-out state were restored, the owned app stopped, and the shared reservation released. No TX testing was performed.
Refs #6238, #6241 and #5525. The six KN7K display reports remain linked in #6241: #5685, #6219, #5204, #4221, #3970, #2371. This follow-up does not claim to resolve every symptom in those reports. R5/R6 band/Go-to/band-stack and preamp bounce remain open investigation cases. Windows, KN7K's radio/firmware, Auto Floor ON timing and TX recovery are not live-proven here.
Future RadioCert gap: existing gestures can reach this path, but sparse
tracedebugpolling cannot certify an approximately 100 ms non-event. Bounded per-frame VITA/decode-generation/transition/render correlation, recorded-frame replay and capture-gap handling remain future work in #6238; this PR adds deterministic ordering regressions without introducing that testing feature.Generated with OpenAI Codex (GPT-6 Astra)