Skip to content

Fix Flex FFT headroom and range reply convergence - #6241

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:aether/6238-panadapter-range
Oct 9, 2026
Merged

ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:aether/6238-panadapter-range

Conversation

@jensenpat

@jensenpat jensenpat commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Flex FFT headroom recovery can lower the peak ceiling while trying to unclip the noise floor. Ordinary radio status also leaves the GUI encoder cache stale, and accepted reply-only range writes do not converge the canonical pan model. This repair preserves the opposite encoder endpoint, validates before decoder mutation, and completes range writes on the current radio reply.

Refs #6238. This is a partial repair: complete band/Go To/preamp trace-bounce removal is not claimed, and the issue remains open.

Linked display reports

The six open KN7K panadapter display reports are linked below, together with the related Auto Floor OFF report #5525. These are reference links, not closing claims; the PR's implementation and validation scope remain the partial range/controller repair described below.

Issue Relationship to this PR
#5685 — FFT peaks clipping Primary clipping report addressed by the bounded headroom/range repair; KN7K's exact Windows/8600 environment remains unverified.
#6219 — FFT peaks clipping #5685 — reminder KN7K confirms the same clipping report persists in 26.10.1; linked with #5685.
#5204 — Inconsistent CW signal on waterfall Its RX flat-topped FFT screenshot is relevant to clipping. Its CW TX/waterfall-width symptom is not tested by this PR.
#4221 — Waterfall a little jumpy/not smooth Related waterfall cadence report, linked for tracking; not reproduced or claimed fixed by this range repair.
#3970 — Keep S and Noise Floor readings in minimized Panadapter applet Related display request, linked for tracking; minimized-readout behavior is unchanged and untested here.
#2371 — Two receivers do not center when switching between them Related panadapter centering report, linked for tracking; centering behavior is unchanged and untested here.
#5525 — Reference level rises on frequency changes with Auto Floor OFF Related report by @aaldocampana-art. The current auto-floor-ON proof does not establish a fix for the OFF case.

Earlier KN7K reports #3483 — band-to-band floor positioning, #3731 — FFT artifacts, and #3804 — post-TX floor recovery remain historical context. Those closed reports are not reopened or reclassified as fixed by this PR.

Change

  • Expand only a clipped encoder endpoint, bounded by FlexLib's -180/+20 dBm endpoints and the client's 180 dB aperture. Detect sparse ceiling clipping using the actual decoder row scale. Saturated ranges become no-ops, and a recovery helper never moves an endpoint inward.
  • Headroom recovery (floor and ceiling) rewrites min_dbm/max_dbm only while Auto Floor is on, and only on a pan this session owns. Flex stores the range per band, so with Auto Floor off the client never changes the operator's range. This was a maintainer ruling. It also changes the floor recovery main ran with Auto Floor off.
  • Refresh the per-pan encoder cache from ordinary status. Rejected, superseded, rewired or foreign-pan callbacks cannot complete a newer request. A radio status received after the write wins over the request, accepted or not (Principle II; FlexLib's SetLowDbmReply/SetHighDbmReply ignore an accepted reply). With no such status, an accepted reply-only write converges the model, and a rejection falls back to the range the radio last confirmed.
  • Validate and confirm dispatch before committing decoder/handshake state. Both existing RadioModel command entry points retain confirmed foreign-pan refusal. On a backend with no command plane (HL2), both dBm gestures still adopt and persist the operator's client-owned range (fix(gui): FFT FPS and the dBm range survive a restart. Principle VIII. #6140).
  • Keep the native auto-floor presentation axis separate from encoder headroom. Reacquire once after the existing 750 ms settling interval plus three fresh floors within 1 dB over at least 100 ms; restore ordinary transient rejection afterward. Hold headroom requests during settling. A clipped floor blocks baseline acquisition only while a recovery can still come: on a foreign pan, with Auto Floor off, or with the floor at -180 dBm, the baseline acquires instead of stalling.
  • Extend existing trace diagnostics with decoder metadata, pre-EMA input statistics and the last native 2D draw's floor/axis. These local counters do not identify VITA wire encoding generations.

The generated touchpoint change records reuse of the existing core dBm validator by the GUI range helper. No new engine dependency, raw command-plane site, capability or bridge verb is added; the frozen raw-site count remains 369. Host absolute-bin and Kiwi paths retain their existing acquisition behavior.

Proof

Agent automation bridge, native Cocoa/Qt 6.12/Metal, RX-only FLEX-8400M, firmware 4.2.18.41174. MW broadcast signals, native band buttons, keyboard Go To, local band-stack bookmarks, and RF-gain controls were exercised. These are maintainer 8400M screenshots, not KN7K's. They are separate ambient RX captures, not a controlled RF amplitude comparison.

Regression Observed repair / remaining result
R1: encoder ceiling Starting at -178/-88, automatic upper recovery reached -178/-40, keeping the minimum fixed. The after image shows intact AM peaks around -42 dBm instead of the old -88 plateau.
R2: floor recovery -100/-16 recovered to -124/-16; the maximum stayed -16. Presentation span stayed 84 dB while the encoder aperture expanded.
R3: status cache / reply-only completion After ordinary status -80/-10, recovery requested -104/-10, then -128/-10, with successful replies and no matching self-status range echo in the captured bracket. The model and decoder metadata both converged to -128/-10.
R4: invalid / refused writes Tests reject an invalid -202/-112 range, NaN, dispatch refusal and a confirmed foreign pan before state commit. Invalid ranges were not sent to hardware.
R5: band / keyboard / bookmarks The controller guards are demonstrated, but trace transients persist: 40m→80m reached 35.26% before settling near 25%; a keyboard transition reached 63.31%; bookmarks reached 29.52%.
R6: preamp 0→32 dB retained encoder -124/-16, preventing the intermediate candidate's false expansion to -148/-16. The rendered trace still moved (24.77–36.30%, ending 25.49% at an 84 dB presentation span). Reverse and 86% target cases also retained transients.

Before: earlier unfixed maintainer 8400M reproduction, abc5b26, with encoder ceiling -88 dBm.

Before: clipped encoder ceiling

After: current production changes, automatic ceiling recovery and intact MW signal peaks.

After: recovered AM signal peaks

Native captures were made before committing as 30882701-dirty; only a source comment and stronger test assertions changed before signed commit 7725ebe2 was rebuilt and tested. Rendered telemetry describes draw submission, not scanout; the after image was inspected separately. Raw logs and the full evidence archive remain local at the operator's request.

Validation and limits

  • Full build with cmake --build <build-dir> -j22 passed at signed commit 7725ebe2.
  • QT_QPA_PLATFORM=offscreen ctest --test-dir <build-dir> --no-tests=error: 7/7 selected tests passed at 793ab77c on Linux/Qt 6.12: 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_test (the fix(gui): FFT FPS and the dBm range survive a restart. Principle VIII. #6140 HL2 pin), and opt-in spectrum_confirmed_geometry_test. The last one needs -DAETHER_BUILD_SPECTRUM_GESTURE_TEST=ON at configure. Offscreen widget checks are logic/gesture proof, not GPU rendering proof.
  • Each landing-pass fix goes red when reverted: the HL2 adopt (client_display_pan_settings_test), radio-status-wins (panadapter_dbm_range_test), the Auto Floor gate, the ownership gate, and the -180 dBm limit (spectrum_confirmed_geometry_test).
  • Demo bridge (Linux, DEMO-0001, transmitting: false): clicking the dBm strip's up arrow moved the pan model and encoder to -130/-50 and then -130/-60, with the display axis following. This held before and after a disconnect and reconnect, which exercises the rewire epoch.
  • Scratch mutation builds fail when validation, endpoint preservation, accepted-reply completion, settle timing or clipped-baseline protection is removed. The widget mutant fails three relevant assertions; production binaries were not replaced.
  • Strict engine boundary, generated touchpoints, strict capability records, strict command plane, strict test registration, frozen CI test gate and generated bridge docs passed.
  • RX session cleanup confirmed TX/tune idle, owned app stopped, shared RF gain and line-out mute restored, front-panel client retained, and radio reservation released.
  • CI, KN7K's Windows/FLEX-8600/firmware-4.1.5 environment, resize/y_pixels stress, a second owned pan and post-TX behavior remain unverified. Auto Floor OFF report Panadapter reference level rises when changing frequency with FFT Floor Auto off #5525 is outside this proof.

The remaining bounce needs raw VITA frame/pixel correlation with decoding range and TCP status timing. The agent automation bridge has no raw VITA capture/replay surface; the noninteractive packet-capture privilege check required a password, and no pcap was captured. A trace hold/blanking policy or automatic display-span change would require maintainer UX direction. This PR introduces neither and should not close the complete bounce investigation.

Generated with OpenAI Codex (GPT-6 Astra)

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

Reviewed at head 7725ebe2 against the PR checkout. All five checks are green (build, check-windows, check-macos, Static checks, Sanitizer option configures). Findings below are reasoned from code — I have no build, no test run, and no way to drive the GUI here, so nothing in this review was reproduced at runtime.

1. Issue fit

Partially — and the PR says so, which I checked and agree with. This is Refs #6238, not Fixes, and the body is explicit that the band/Go-To/preamp bounce (R5/R6) is not claimed. Mapping #6238's acceptance list to the diff:

#6238 case Addressed?
R1 — bounded upper encoder headroom, sparse peak detection, explicitly not reusing the floor detector's occupancy threshold Yes — fftPeakTouchesEncoderCeiling (src/gui/SpectrumPreviewLogic.h:421) is a per-bin test with no occupancy term, exactly as the issue directed
R2 — add lower headroom without sacrificing upper room, within min/max/span limits, terminate at the bound Yes — clippedFloorRecoveryRange now pins the ceiling (src/gui/DbmRangeTransition.h:200-213), bounded at −180 and a 180 dB aperture; materiallyDifferent gives the no-op termination
R3 — sync the applied range on ordinary status; reply-only completion; supersession/rejection Yes — setStreamDbmRange moved out of the encoderRecoveryPending branch (src/gui/MainWindow_Wiring.cpp:4097), plus Handshake::completeReply
R4 — validate the whole candidate before mutating handshake/cache/decoder/pending Yes — dispatchValidatedRange orders validate → dispatch → commit
R5/R6 — settled floor acquisition Partial, as disclosed: NoiseFloorReacquisition is added and the PR's own R5/R6 rows report the transients persist

The core of R3 is a real pre-existing defect and the fix is the right shape: on main, an ordinary levelChanged reached sw->setDbmRange() but never PanadapterStream::setDbmRange(), so a foreign-driven range change left the VITA decoder on the old aperture. That's worth having.

2. Scope

File / group What it changes Claimed? Verdict
src/gui/DbmRangeTransition.h dispatchValidatedRange, completeReply, endpoint-preserving recovery helpers Yes (R1–R4) In scope
src/gui/SpectrumWidget.{h,cpp} encoder-range cache, peak-clip detector, reacquisition gate, applyDbmRangeImmediate ref policy, rendered-trace diagnostics Yes In scope
src/gui/SpectrumPreviewLogic.h NoiseFloorReacquisition, fftPeakTouchesEncoderCeiling Yes In scope
src/gui/MainWindow_Wiring.cpp range dispatch rewrite, rewire epoch, status cache refresh Yes In scope — but see Blocker 1 for a non-Flex side effect
src/gui/MainWindow_Session.cpp one setEncoderYPixels call Yes In scope
src/models/RadioModel.{h,cpp} rejectsForeignPanWrite extraction; sendCmdPublic void→bool + the pan gate Yes In scope. I verified the extraction is equivalence-preserving: panadapter() is m_panadapters.value(panId, nullptr) (RadioModel.cpp:5789) and ourClientHandle() is literally return clientHandle(); (RadioModel.cpp:10235)
docs/automation-bridge.md adds encoderMinDbm/encoderMaxDbm/encoderRangeGeneration, noiseFloorReacquiring, nativeInputBins, renderedTrace to the trace snapshot Yes Needs maintainer decision — no new verb, but new keys in an existing bridge reply are surface third parties bind to. Caveats are honestly written ("local diagnostics, not VITA wire tags", "does not certify scanout"), so this is a "is this the right reply to grow" call, not a defect
docs/architecture/vita49-format.md new paragraphs; also deletes the file's trailing blank line Partly Nit — unrelated whitespace, harmless
docs/architecture/aetherd-touchpoints.md DbmRangePlausibility.h count 1→2 Yes In scope; the new #include "core/DbmRangePlausibility.h" in DbmRangeTransition.h explains it
tests/*, tests/tests.cmake new assertions; panadapter_dbm_range_test joins AETHER_SETTINGS_CONSUMERS Yes In scope. The CMake contract is satisfied: the target now instantiates RadioModel and uses TestSettingsProfile.h, and it is registered in AETHER_SETTINGS_CONSUMERS (tests/tests.cmake:7765)

No CHANGELOG.md entry (correct). No new capability field, no new protocol verb, no settings key, no raw command-plane site. No removed guard that I could find — I read the - lines and the #3977 ownership comment block that was deleted from sendDbmRangeCommand is replaced by a stronger check in the same function (pan && isConnected() && ownedByClient, MainWindow_Wiring.cpp:3800-3804), so the protection is not lost.

3. Blockers

1. src/gui/MainWindow_Wiring.cpp:4306 — a Flex-scoped early return silently drops dBm-range persistence on the Hermes-Lite 2.

        if (!sendDbmRangeCommand(minDbm, maxDbm)) {
            return;
        }

sendDbmRangeCommand returns false at its very first gate when the backend does not own the dBm scale (MainWindow_Wiring.cpp:3790):

        if (!m_radioModel.backendCapabilities().radioOwnsDbmScale) {
            return false;
        }

On main the same code path read armDbmRangeHandshake(...); setStreamDbmRange(...); sendDbmRangeCommand(...); … adoptClientOwnedDbmRange(...) — sendDbmRangeCommand was void and simply no-op'd on a fixed-scale backend, so the other three still ran. After this PR the early return skips both setStreamDbmRange and adoptClientOwnedDbmRange.

That matters on exactly one family today: Hl2Backend.cpp:1799 sets amplitude.clientPersistsDbmRange = true (ANAN and RTL explicitly set it false, Icom and Sim declare no panAmplitude record at all), and clientPersistsDbmRange() additionally requires binsAbsolute, which HL2 has. So on an HL2:

  • Operator clicks the dBm strip's ±10 dB arrow. SpectrumWidget.cpp:9996 mutates m_refLevel/m_dynamicRange directly and emits dbmRangeChangeRequested, so the display does move.
  • autoFloorChange is false (this is an operator gesture, not auto floor), profileLoadHeld is false, so localOnly is false and control reaches 4306.
  • sendDbmRangeCommand returns false → adoptClientOwnedDbmRange() never runs → pan->setRange() is never called and ClientDisplaySettings::saveDbmRange() is never scheduled. The operator's new range is lost at the next profile restore / restart.

The sibling handler shows the intended shape — dbmRangeDragFinished has its own explicit branch for this case at MainWindow_Wiring.cpp:4375-4381:

        if (!m_radioModel.backendCapabilities().radioOwnsDbmScale) {
            sw->setDbmRange(minDbm, maxDbm);
            setStreamDbmRange(minDbm, maxDbm, true);
            adoptClientOwnedDbmRange(applet->panId(), sw->panIndex(),
                                     minDbm, maxDbm);
            return;
        }

so the drag gesture still persists correctly; only the click/arrow gesture regressed. Suggested fix — add the same branch immediately before line 4306, keeping the existing emittingDssZoomFloorDbmRange() guard on the adopt:

        if (!m_radioModel.backendCapabilities().radioOwnsDbmScale) {
            setStreamDbmRange(minDbm, maxDbm, true);
            if (!sw->emittingDssZoomFloorDbmRange()) {
                adoptClientOwnedDbmRange(applet->panId(), sw->panIndex(),
                                         minDbm, maxDbm);
            }
            return;
        }
        if (!sendDbmRangeCommand(minDbm, maxDbm)) {
            return;
        }

Two notes so this is scored fairly. First, the handshake half of the same change is an improvement: main armed a 2 s handshake on a radio that can never echo, which is the hazard the 4375 comment names, and this PR stops doing that. Second, nothing in the suite would have caught the persistence loss — tests/client_display_pan_settings_test.cpp is the only test touching dbmRangeChangeRequested, and it does so via a source-text followsWithin() grep and direct ClientDisplaySettings calls, never through the handler. A socket-free check that an HL2-shaped capability set still reaches saveDbmRange on this gesture would be the natural guard, and it's a deterministic seam.

4. Needs maintainer decision

src/gui/SpectrumWidget.cpp:3690-3717 + SpectrumPreviewLogic.h:421 — the ceiling half of recovery can ratchet the operator's radio-owned max_dbm to the +20 dBm cap and never give it back.

fftPeakTouchesEncoderCeiling fires when any single bin is within 1.5 × rowStepDb of m_encoderMaxDbm. On a −130/−40 pan at y_pixels=700 that threshold is ≈ 0.19 dB, i.e. VITA pixel row 0 or 1 — genuinely saturated, so detection is correct. The response is what I'd want a ruling on:

    candidate = DbmRangeTransition::clippedPeakRecoveryRange(
        candidate.minDbm, candidate.maxDbm, peakClipped ? kRecoveryHeadroomDb : 0.0f);

kRecoveryHeadroomDb is 24.0, rate-limited to one request per kRecoveryRequestIntervalMs (1000 ms), and the post-write settle gate is kFrequencyRangeSettleMs = 300 ms. The call site at SpectrumWidget.cpp:8545 is unconditional in updateSpectrum — pre-existing on main, and its comment says so ("even when client-side automatic floor placement is disabled"). So the scenario: operator deliberately sets −130/−40 to work a weak signal next to a local S9+40 broadcaster. The strong peak saturates row 0 every frame, and the client issues max_dbm=−16, then +8, then +20 over about three seconds, where clippedPeakRecoveryRange's std::min({… , 20.0f, …}) terminates it. Because the request is sent with encoderOnly = true, the presentation axis is deliberately preserved, so the operator sees no range change in the UI while the radio-owned max_dbm has moved 60 dB. Pulling it back by hand re-triggers the ratchet on the next frame.

I'm not calling this a Constitution violation, because #6238's own R1 acceptance text explicitly directs "do not reuse the floor detector's broad occupancy threshold as the only peak detector" — the absence of an occupancy term is the requested design, not an oversight. But an automatic, repeated, invisible-to-the-operator write to radio-owned state (Principle II/III territory) with no persistence requirement and an irreversible terminal state is a policy call, and the floor half has always had a run-length gate (dssFrameFloorLooksClipped) that bounds its false-positive rate while this one has nothing. Options that stay inside the issue's guidance: require the ceiling-touching peak to persist across N consecutive frames (persistence, not occupancy); stop expanding at the operator's stored/adopted ceiling; or gate the ceiling half on m_noiseFloorEnable so it cannot fire in the Auto-Floor-OFF regime the PR declares out of scope (tests/spectrum_confirmed_geometry_test.cpp asserts the opposite today — "a sparse clipped peak requests upper headroom even with auto floor disabled").

A second-order consequence worth stating in the body either way: both halves now move one endpoint, so every recovery event permanently widens the aperture and nothing ever narrows it. On main floor recovery was a translation, so the span — and therefore the ~0.13 dB/row amplitude LSB — was conserved. The new behaviour trades vertical resolution for headroom monotonically. That is the correct fix for R1/R2 as specified; it is not a free trade and the PR body doesn't name the cost.

5. Nits (non-blocking)

  • src/gui/SpectrumWidget.cpp:3978-3982 — the new if (m_radioOwnsDbmScale && flexInputFloorLooksClipped()) return false; in updateNoiseFloorBaseline is unconditional on whether recovery can actually succeed. On a MultiFlex-shared pan owned by another client whose min_dbm sits above the real noise floor, sendDbmRangeCommand refuses (:3800) and this early return then blocks baseline acquisition for the whole session, so auto floor never converges. main acquired a (wrong) baseline from the clipped frame. Pairing this with "…and a recovery request is eligible" would keep the intent without the deadlock.
  • src/gui/SpectrumWidget.cpp:3707 / :3717 — requestFlexRadioHeadroom returns floorClipped even on the saturated no-op path, so once min_dbm is pinned at −180 with a still-clipped floor, applyNoiseFloorAutoAdjust returns early every frame forever. Same shape on main, so not a regression, but the new :3978 return makes it a two-sided freeze instead of one.
  • src/gui/MainWindow_Wiring.cpp:3677-3680 — the rewire guard rides a dynamic "_aetherDbmRangeWireEpoch" QObject property. It works, but a stringly-typed property on SpectrumWidget is invisible to the compiler and to anyone grepping the widget's header; a quint64 m_dbmRangeWireEpoch with an accessor would be greppable and is the same amount of code.
  • src/gui/MainWindow_Wiring.cpp:4097 — setStreamDbmRange() on every ordinary levelChanged now reaches setEncoderDbmRange() → prepareForFftScaleChange(), which arms a fresh 750 ms settle plus reacquisition. While a foreign client drags its own range, our auto floor stays in perpetual reacquisition. setEncoderDbmRange's equality guard bounds the no-change case, so this only bites during sustained foreign motion.
  • src/gui/DbmRangeTransition.h:206-213 — clippedPeakRecoveryRange's std::min({currentMaxDbm + step, 20.0f, …}) lowers the ceiling if the radio ever reported max_dbm > +20, which dbmRangeLooksPlausible permits up to +80. FlexLib caps at +20, so this is unreachable today; a std::max(currentMaxDbm, …) floor on the result would make it unreachable by construction.
  • src/gui/SpectrumWidget.h:288-296 — setEncoderYPixels bumps m_encoderRangeGeneration and m_encoderRangeChangedMs but, unlike setEncoderDbmRange, does not call prepareForFftScaleChange(). The asymmetry is probably deliberate (a pixel-count change has its own settle path via prepareForFftPixelScaleChange), but it isn't commented and the two setters sit adjacent.
  • m_encoderYPixels defaults to 700. If setEncoderYPixels never runs (pan absent at wire time) the derived rowStepDb is wrong by the ratio of the real y_pixels to 700. Only a 1.5-row tolerance rides on it, so the practical effect is small.
  • Coverage placement: spectrum_confirmed_geometry_test — which carries all of the widget-level proof (reacquisition integration, the ceiling detector's wiring, the new applyDbmRangeImmediate ref policy) — is behind if(AETHER_BUILD_SPECTRUM_GESTURE_TEST) at tests/tests.cmake:2213, so CI never builds or runs it. The PR discloses this. The pure helpers in spectrum_preview_logic_test and panadapter_dbm_range_test are default-graph targets, which is the right home for them. Also note the geometry test's settled-sequence assertions depend on wall clock (wait(60) ×2 against the gate's nowMs - m_firstMs < 100), a ~20 ms margin; waits only lengthen under load so it should hold, but it is timing-coupled.

6. What I tried to break and could not

  • Lambda lifetime in dispatchValidatedRange. The [&] captures looked alarming for a function that registers an async callback. They're safe: dispatchValidatedRange (DbmRangeTransition.h:21-29) invokes both dispatch and commit synchronously before returning, and the reply callback handed to sendCmdPublic captures by value (this, guard, pan, pendingDbm, generation, previousRange, …). No dangling reference.
  • *generation read before it's written. The reply callback reads *generation, which the commit lambda sets after dispatch. If sendCmd ever invoked the callback inline on a drop, completeReply(0, …) would run against an inactive handshake and return Ignore — and in fact sendCmdPublic returns sendCmd(...) != 0, so a drop returns false and commit never runs at all. Both orderings are safe.
  • Teardown / rewire / foreign-pan races on the reply path. The callback guard at MainWindow_Wiring.cpp:3815-3821 checks QPointer<SpectrumWidget>, QPointer<PanadapterModel>, isConnected(), the wire epoch, pan-identity re-lookup, and re-confirms ownership. I tried widget deletion, pan recreation under the same id, disconnect mid-flight, and a double wirePanadapter() — each is covered by one of those five.
  • Stuck m_pendingDbmRangeEcho after the early returns. I expected the dropped armDbmRangeHandshake to orphan the widget's pending guard. It self-expires: setDbmRange at SpectrumWidget.cpp:7917-7921 clears it once nowMs - m_pendingDbmRangeEchoStartMs > kDbmRangeHandshakeTimeoutMs, and updateNoiseFloorBaseline:3952-3958 does the same. Bounded at 2 s, same as the handshake it replaced.
  • A drag snapping back when the reply lands. The new applyDbmRangeImmediate branch recomputes ref from m_noiseFloorBaselineDbm + m_noiseFloorTargetFrac * dyn, which looked like it would discard the operator's drag once pan->setRange() → levelChanged → setDbmRange() came back. Both emitters call refreshNoiseFloorTarget(true, true) synchronously before emitting (SpectrumWidget.cpp:11404 and :11449, and :9994 on the click path), so m_noiseFloorTargetFrac already describes the operator's new placement and the formula reproduces their ref.
  • Div-by-zero / empty-bin crashes in the new math. rowStepDb divides by m_encoderYPixels - 1 — the setter guards yPixels > 1 and the default is 700. *std::min_element(m_bins...) is behind m_bins.isEmpty() in both flexInputFloorLooksClipped and requestFlexRadioHeadroom. fftPeakTouchesEncoderCeiling refuses non-finite inputs and rowStepDb <= 0.
  • An aperture-shrinking clippedFloorRecoveryRange. max{min - step, −180, max − 180} raises the floor when the span already exceeds 180 dB. Unreachable: dbmRangeLooksPlausible caps rangeDb at 180 (src/core/DbmRangePlausibility.h:27) and both recovery helpers use the same bound.
  • Widening sendCmdPublic's pan gate onto unrelated callers. 26 call sites across rigctl, TCI, firmware/waveform/DVK upload, memories, XVTR, profiles. None emits a display pan set command, so the new refusal can only fire on the two raw pass-throughs (RigctlProtocol.cpp:2107, TciProtocol.cpp:1652), where it strictly adds the #3977 protection. The void→bool change is source-compatible for all of them.
  • Per-frame cost of the new diagnostics. recordRenderedTrace adds estimateNoiseFloorDbm to both render paths. It's stride-sampled to ~512 reads, branch-only, zero allocation (src/gui/NoiseFloorEstimator.h:15-40), so this is noise on a 30 Hz pan. Withdrawn as a finding.
  • Tests that would pass unfixed. The recovery-range assertions genuinely discriminate: CHECK(std::abs(clippedRecoveryRange.maxDbm - -15.5f) < 0.01f) demands the preserved ceiling where main produced −21.5. The completeReply and NoiseFloorReacquisition suites exercise API that doesn't exist on main, so they can't be self-confirming. fftPeakTouchesEncoderCeiling's negative cases (visible axis, zero row step, NaN) are real.

Could not check: any runtime behaviour; the FlexLib claim in docs/architecture/vita49-format.md ("FlexLib 4.2.18's LowDbm/HighDbm reply handlers also accept this") — this is a clean-room repo and FlexLib is not in the checkout, so that Principle I citation is untestable from here and only a maintainer with the reference can settle it; and KN7K's Windows/FLEX-8600/firmware-4.1.5 environment, which the PR already lists as unverified.

One behaviour I'd flag for the maintainer rather than the author: Handshake::completeReply (src/gui/DbmRangeTransition.h:115-128) returns m_requestedRange on an accepted reply even when an intervening radio status reported a different range. The PR's own test documents it — arm −128/−10, observeRadioRange(-130, -20), then an accepted reply yields −128/−10. That is the client's own write winning over a radio status it had already received, which is the Principle II seam. It may well be the right call for a firmware that doesn't echo its own accepted range (which is R3's whole premise), but it is a judgment the maintainer owns, not something I'd want settled implicitly by a test assertion.

7. Recommendation

Request changes, for Blocker 1 only — a one-branch fix, and the HL2 persistence loss is a user-visible regression in a family this Flex-scoped PR shouldn't be touching. Everything else here is good work: the R3 decoder-staleness fix is a genuine pre-existing defect, the validate-before-commit ordering is the right seam, the callback guard is unusually thorough, and the limits section is honest about what the proof does and doesn't cover. Next step: add the !radioOwnsDbmScale branch before MainWindow_Wiring.cpp:4306 (ideally with a socket-free check that an HL2-shaped capability set still reaches saveDbmRange on the strip-click gesture), then get a maintainer ruling on the ceiling-ratchet policy in §4 and on the new renderedTrace/encoder keys in the bridge reply before merge.

Thanks for the detailed body — the per-regression table and the explicit "not verified" list made this much faster to review against #6238's acceptance criteria.


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

@NF0T NF0T self-assigned this Oct 7, 2026
NF0T
NF0T previously requested changes Oct 7, 2026

@NF0T NF0T left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Independent review at head 7725ebe2. I formed this before comparing against the @aethersdr-agent review. I agree with its outcome on the one blocker, but its mechanism is wrong, which changes the fix. Details are below.

What's right

  • R3 is a real pre-existing defect, and the fix has the right shape. On main an ordinary levelChanged reached the widget but never PanadapterStream::setDbmRange(), so the GUI's encoderDbmRange cache went stale. Moving setStreamDbmRange() out of the encoderRecoveryPending branch (MainWindow_Wiring.cpp:4096) fixes exactly what #6238's live C317 capture shows: a translation of the stale -130/-40 instead of the applied -80/-10.
  • R4: validate, then dispatch, then commit, is the right seam. dispatchValidatedRange runs both lambdas synchronously, so the [&] captures are safe. The reply callback captures by value. A drop that invokes the callback inline (sequence 0) reaches completeReply(0, …) against an inactive handshake and returns Ignore. PanadapterModel::setRange() is a pure setter, so reply-driven convergence cannot loop back to the radio.
  • The endpoint bounds hold up against FlexLib 4.2.20.41343. Panadapter.LowDbm clamps at -180.0 and HighDbm at 20.0 (Panadapter.cs:448, :480), matching clippedFloorRecoveryRange and clippedPeakRecoveryRange.
  • The foreign-pan gate on sendCmdPublic is equivalence-preserving. I agree with the bot. It strictly adds #3977 protection to the two raw pass-through callers.
  • The partial-repair framing is honest. Refs #6238, not Fixes, plus the explicit R5/R6 "transients persist" rows, matches what the code does.

Blocker: Hermes-Lite 2 dBm-range persistence (#6140) is lost on both operator gestures

MainWindow_Wiring.cpp:4306 (scale-strip arrows) and :4383 (drag finished) now return before adoptClientOwnedDbmRange() whenever sendDbmRangeCommand() returns false.

The mechanism differs from the earlier review, and that matters for the fix. The HL2 does not fail the radioOwnsDbmScale gate. It leaves that capability at its default true (RadioCapabilities.h:439; Hl2Backend.cpp:1802 says so explicitly). It passes plausibility and pan ownership: ownedByClient() fails open on an unknown owner, and clientHandle() is 0 without a RadioConnection. The failure comes one step later. RadioModel::sendCmd() hits !hasCommandPlane() and returns 0, so sendCmdPublic() returns false, dispatchValidatedRange() returns false, and the handler returns before the adopt.

Two consequences follow:

  1. The drag path is broken too, not only the click path. The !radioOwnsDbmScale branch at :4375 that the earlier review cited as "still persists correctly" never runs on an HL2, because the flag is true there. Drag falls through to the same early return at :4383.
  2. That review's suggested fix would not help the HL2, for the same reason: a new !radioOwnsDbmScale branch before :4306 is unreachable on the one family it targets.

Only the HL2 declares clientPersistsDbmRange (ANAN and RTL set it false; Icom and Sim declare no panAmplitude), so this is the only affected family. It is the persistence #6140 shipped two commits before this PR's base (2e3d7ae7f, "the dBm range survive[s] a restart on the Hermes-Lite 2").

The existing guard catches it, but CI doesn't run that guard per PR. client_display_pan_settings_test pins precedes(wiring, "sendDbmRangeCommand(minDbm, maxDbm);\n", "if (!sw->emittingDssZoomFloorDbmRange()) {") ("the send comes before the mark is read"). I evaluated every dBm-range pin in that test with its exact precedes/followsWithin/count semantics against the git blobs. At the base (30882701) all 9 pass. At this head, that one fails: the statement literal no longer exists, because both sites are now if (!sendDbmRangeCommand(...)). The test is in the default graph but not on the frozen per-PR allow-list, so it would first go red in full-suite.yml on main after merge. The PR's "6/6 selected tests" didn't include it.

Suggested fix (inline below). In both handlers, call sendDbmRangeCommand() as a statement and let the adopt run unconditionally:

  • adoptClientOwnedDbmRange() already returns early unless clientOwnsPanDbmRange(), and it re-checks dbmRangeLooksPlausible(). On a Flex the adopt is a no-op, so the early return does no work there.
  • On the HL2 this restores main's behaviour exactly. It also keeps this PR's improvement: no 2 s handshake is armed on a radio that can never echo, because the arm now lives inside the commit lambda.
  • The #6140 pin passes again unchanged.

Questions (non-blocking for me)

1. Should automatic, radio-persisted ceiling writes reach every Flex user? The ceiling half of recovery runs every frame from updateSpectrum (SpectrumWidget.cpp:8545), including with Auto Floor OFF, and there is no opt-out. It sends display pan set … max_dbm=, which is radio state the Flex saves per band: KN7K's -88 came back from the band stack (#5685). So this is a default-behaviour change for the whole Flex population, and part of it leaves the machine. An Auto-Floor-OFF operator's manual range can come back widened on the next band recall, and nothing on screen shows that it changed. The aperture also only ever widens, so vertical resolution drops monotonically. I read this as following #6238's R1 design, not as a defect. Is that reach intended, though? Would you be open to gating the ceiling half on Auto Floor, or on a persistence/run-length condition, so an Auto-Floor-OFF operator's stored range is left alone?

2. Is requested-wins on an accepted reply deliberate against Principle II? completeReply(accepted) lets the client's request supersede a radio status received after the command (DbmRangeTransition.h:123, pinned by the -128/-10 over -130/-20 assertion in panadapter_dbm_range_test). Principle II: "Where a command has no status echo the client may update optimistically, but the radio's subsequent status is the truth and supersedes the optimistic value if they differ."

  • FlexLib's SetLowDbmReply/SetHighDbmReply return immediately on resp_val == 0 (Panadapter.cs:460, :492). In FlexLib, a status that arrives before the reply therefore stands.
  • finish() in the same class already prefers m_authoritativeRange, so the timeout and reply paths now disagree about the same intervening status.
  • I see the counter-case: a status in flight from before the radio processed the command lands in the same bracket, and last-received-wins would then keep a stale value. FlexLib has that mirror flaw. Either answer is defensible. Whichever you choose, it should be stated in the code against Principle II rather than settled only by a test assertion. If you keep requested-wins, a one-line comment naming the in-flight-status case would do it. If not, m_authoritativeRange.value_or(m_requestedRange) is the change.

3. Can we get Windows evidence before this closes KN7K's report? All of the live proof is one 8400M on macOS/Metal. The reporter is on Windows 11, a FLEX-8600 and firmware 4.1.5, and Windows/D3D and Linux have no evidence at all. The decoder and recovery logic is platform-neutral, but recordRenderedTrace sits on both the QRhi and software paint paths. Could the R1/R2 recipe (MW, -178/-88 → recovery) be run on a Windows host before #5685/#6219 are treated as addressed?

Non-blocking / forward guidance

  • I verified and agree with these earlier-review nits: the stringly-typed _aetherDbmRangeWireEpoch property (a quint64 member would be greppable), the trailing blank line removed from vita49-format.md, and spectrum_confirmed_geometry_test sitting behind AETHER_BUILD_SPECTRUM_GESTURE_TEST so CI never builds the widget-level proof. The PR discloses that last one.
  • When a PR touches the dBm handlers, add client_display_pan_settings_test to the local selection. It is the only guard on the client-owned path.

Scope

File / group Claimed Verdict
DbmRangeTransition.h, SpectrumPreviewLogic.h, SpectrumWidget.{h,cpp} R1–R4 + reacquisition In scope
MainWindow_Wiring.cpp, MainWindow_Session.cpp dispatch/reply/cache In scope; HL2 side-effect above
RadioModel.{h,cpp} foreign-pan gate on both entry points In scope. #5554 gate: no family branch, no new wire text (Static checks: 369 raw sites unchanged)
docs/automation-bridge.md new traceDebugSnapshot keys In scope; diagnostic-only and honestly caveated
docs/architecture/* touchpoint count, VITA notes In scope (+ whitespace nit)
tests/* new assertions In scope; no socket/peer constructs

Preference vs fix: every behavioural change traces to #6238's R1–R4 acceptance text. No preference changes are bundled. No CHANGELOG.md entry, which is correct.

What I tried to break

  • HL2 gestures. I traced click and drag through sendDbmRangeCommand → dispatchValidatedRange → sendCmdPublic → sendCmd's !hasCommandPlane() drop. Both now skip the adopt (blocker above).
  • #6140's source pins. All 9 dBm pins evaluated at the base and at the head, with exact helper semantics on the git blobs: 9/9 at the base, 8/9 at the head. Running the local MinGW binary directly was not a valid control: this checkout has core.autocrlf=true, so its \n literals fail on main too. I didn't use those results.
  • Inline-drop reply ordering. The callback fires with generation 0 before commit and is ignored. On a dispatch failure, commit never runs.
  • Accepted-reply equality. If pan->setRange() returns false (no change), it falls back to applyAuthoritativeDbmRange, so the decoder still converges.
  • Rejected reply with no status. The previous encoder range is restored through the same path.
  • Reply-loop risk. None: PanadapterModel::setRange() emits levelChanged only.
  • FlexLib bounds and reply semantics. Checked against the 4.2.20.41343 C# sources.
  • Not driven. The automation bridge was unavailable in this session. Neither finding is reachable in demo mode anyway: Sim declares no client-owned range and has no Flex FFT encoder. Both findings are reasoned from code, plus the deterministic pin evaluation, not reproduced at runtime. No build was run here.

CI: all five checks green (build, check-macos, check-windows, Static checks, Sanitizer option configures). Commit is signed and GitHub-verified.

Verdict: request changes, for the HL2 persistence regression only. It's a two-site, two-line fix, and it restores the #6140 pin. The rest is careful work. The callback guard covers every teardown/rewire/foreign-pan race I tried, and the R3 cache fix is worth landing on its own. I'm happy to re-review promptly once the two call sites are changed, since #6242 is stacked on this.

73,
Ryan NF0T


👨🏼‍💻 Co-authored by Claude Opus 5.5

Comment thread src/gui/MainWindow_Wiring.cpp Outdated
Comment thread src/gui/MainWindow_Wiring.cpp Outdated
@NF0T NF0T removed their assignment Oct 7, 2026
@ten9876 ten9876 self-assigned this Oct 7, 2026
ten9876
ten9876 previously requested changes Oct 7, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Issue fit

Partially — request changes. Reviewed 7725ebe251b7119a4e87236230423e6474a89ee2 against #6238 and the primary clipping reports #5685/#6219. R1/R2 map to endpoint-aware detection and bounded expansion; R3 maps to the ordinary-status cache update and reply completion; R4 maps to validate/dispatch/commit ordering. The partial-repair framing is appropriate: R5/R6 bounce and the separately tracked R7 repair are not claimed solved. However, the dispatch-success requirement now prevents both HL2 operator gestures from adopting and persisting their client-owned dBm range, regressing #6140 outside the intended Flex repair.

Scope

File/group Change Claimed? Verdict
src/gui/DbmRangeTransition.h Validate-before-commit, reply completion, bounded endpoint expansion Yes In scope
src/gui/SpectrumPreviewLogic.h, SpectrumWidget.{cpp,h} Sparse ceiling detection, settled reacquisition, separate encoder metadata, draw diagnostics Yes In scope
src/gui/MainWindow_Wiring.cpp, MainWindow_Session.cpp Per-pan cache/dimensions, guarded replies and gesture dispatch Yes In scope; HL2 regression below
src/models/RadioModel.{cpp,h} Dispatch result and shared confirmed foreign-pan refusal Yes In scope
Three changed tests, tests/tests.cmake Range/recovery/reacquisition tests and settings isolation registration Yes In scope
docs/architecture/vita49-format.md, docs/automation-bridge.md Range contract and additional diagnostic fields Yes In scope
docs/architecture/aetherd-touchpoints.md Existing validator includers 1→2 Yes Disclosed and reproducible

All 15 files are explained by the stated repair. There is one signed, GitHub-verified commit, authored October 7 at 00:40:25 UTC, with a matching subject. No unrelated dependency, default, theme, settings schema, CHANGELOG entry, or new bridge verb is bundled. The added diagnostic fields are an additive bridge response surface and are documented. The automatic ceiling recovery, including Auto Floor OFF, follows #6238's proposed endpoint repair; I do not classify it as an unrelated preference. Its radio-persisted effect deserves the maintainer attention noted below.

Blocker

  1. src/gui/MainWindow_Wiring.cpp:4306 and :4383 — both HL2 operator gestures now return before adoption/persistence. These two new branches make adoptClientOwnedDbmRange() conditional on successfully dispatching a Flex command. HL2 declares client-owned range persistence but leaves radioOwnsDbmScale at its permissive default, so neither the send helper's initial gate nor the drag's fixed-scale branch handles it. RadioModel::sendCmd() drops the command because HL2 has no command plane; the new false result causes both handlers to skip the model update and deferred save. The changed scale can therefore be lost when the model reseeds it or the app restarts.

    A socket-free cold probe installed the real HL2 backend through RadioModel::setBackendForTest() without connecting hardware and called the real dispatch method:

    HL2 cold declarations: radioOwnsDbmScale=1 clientPersistsDbmRange=1 hasCommandPlane=0 dispatched=0
    

    The existing client_display_pan_settings_test fails on the head:

    FAIL: the send comes before the mark is read
    1 check(s) failed
    

    The same test passes on merge base 30882701a. The source pin's missing literal alone would be only a refactor-sensitive assertion. Here the actual backend declaration/drop plus both early-return paths independently establish the behavioral regression. This test is registered by default but outside the frozen per-PR test gate, explaining why hosted green does not settle it. I did not connect an HL2 or claim a full-app HL2 restart reproduction; the evidence combines the real backend/transport probe, the actual handler control flow, and the head/base test comparison.

    Restore adoption for a declared client owner independently of Flex dispatch. The minimal change is to replace each new three-line if (!sendDbmRangeCommand(...)) { return; } block with the existing statement:

         sendDbmRangeCommand(minDbm, maxDbm);

    adoptClientOwnedDbmRange() already refuses non-owners and invalid ranges, so this is a no-op on Flex; the new send helper still commits no decoder/handshake state on refusal. Alternatively use an explicit capability-shaped local-owner branch, with the source pin updated to the intended ordering. Adding only a !radioOwnsDbmScale branch would not fix HL2. Run the client-display guard and retain refusal/foreign-pan tests. This finding agrees with @NF0T's mechanism; the earlier bot's explanation that HL2 fails the radio-scale gate is incorrect.

Nits and maintainer questions — non-blocking

  • src/gui/DbmRangeTransition.h:123; tests/panadapter_dbm_range_test.cpp:159 — make the accepted-reply/status ordering decision explicit. The real helper probe reports status -130/-20 then accepted reply => -128/-10. finish() instead preserves the intervening authoritative range. Constitution II says, “Whenever the client's model and the radio's reported state disagree about what is live ... the radio wins and the client reconciles to it.” A status already in flight before this write is a plausible reason to prefer the accepted request, but arrival alone cannot distinguish that from a newer radio change. Jeremy: settle/document the intended contract and its evidence rather than treating a zero-code reply as a range-generation tag. I have reproduced the helper decision, not a live status-ordering failure, so I am not asserting an additional confirmed hardware defect.
  • src/gui/SpectrumWidget.cpp:8545; tests/spectrum_confirmed_geometry_test.cpp:165 — explicitly describe Auto Floor OFF reach in the Change section. The new ceiling half runs on every native frame, even when auto floor is disabled; the widget test intentionally pins that. This can widen a manually selected encoder range that Flex persists. The PR already lists #5525 as unproven, but that is a different limit from saying this new behavior is active with Auto Floor OFF. This is consistent with the linked repair proposal, not a request to invent an opt-out.
  • docs/architecture/vita49-format.md:36 — the exact FlexLib 4.2.18 reply-handler authority remains unverified here. There is no local reference/FlexLib_* source. @NF0T cites a 4.2.20 source check; that is useful corroboration but not my independent verification of 4.2.18. Keep the evidence version clear. Windows/8600/4.1.5, second owned pan, resize/y_pixels stress and post-TX remain honest disclosed limits; demo results cannot close those reports.
  • tests/tests.cmake:2212 — keep the opt-in real-widget test invocation reproducible. It requires -DAETHER_BUILD_SPECTRUM_GESTURE_TEST=ON and runs in neither default full-suite nor default sanitizer configurations. The body discloses opt-in, but giving this configure flag beside the test command makes the advertised six-test run directly repeatable. Do not expand the frozen per-PR gate to fix this.

Verification and attacks

  • Build/tests: built named targets only in isolated head and base worktrees, using Linux, Qt 6.12, AETHER_SHARED_CORE=ON, -O2 -g1, and the explicit opt-in widget flag. Head AetherSDR and all seven selected test targets build. The author's six named tests pass, including spectrum_confirmed_geometry_test; the additional client_display_pan_settings_test fails as above and passes in a full merge-base build. No broad suite or local Windows/macOS run was attempted. The shared-core review link mode differs from the shipped static configuration.

  • Mutation checks: separate /tmp include overlays restored the bad moving ceiling (five failing assertions), replaced accepted completion with the old range (two), bypassed validation (five), and removed the settle deadline (one). Each mutant exits 1 while the original focused tests pass. These mutations validate the two helper suites; I did not independently repeat the author's production-widget mutation. PR sources remained unchanged.

  • Lifecycle/refusal attacks: inspected invalid/NaN and dispatch-refused ordering, synchronous drop callbacks before generation commit, superseded replies, rejection with/without intervening status, matching status, timeout, disconnect, widget/pan destruction, same-ID replacement and wiring epoch changes. The synchronous dispatch lambdas do not escape; asynchronous reply captures are by value and guarded by object lifetime, connection, epoch, identity and ownership. The relevant socket-free range tests pass. This is code/test evidence, not a sanitizer churn run or a live multi-pan proof.

  • Demo bridge: drove only the scratch binaries offscreen with fresh settings, explicit per-instance sockets under /tmp/pr6241-demo, identities pr6241-review-head and pr6241-review-base*, and AETHER_AUTOMATION_NO_TX=1. connect local serial DEMO-0001, connect wait, get radio, get pans, get tracedebug, dumpTree, persistent-connection gesture begin/move/end, tune, disconnect and reconnect exercised the real GUI. Both builds retain one slice at 1.050 MHz and one trace pane after reconnect; sampled serial/model are DEMO-0001 / AetherSDR Demo, transmitting=false.

    The same settled 30-pixel scale drag demonstrates R3's model behavior on the demo's acknowledgement-without-range-status path:

    {"baseWidgetRefBefore":-40,"baseWidgetRefAfter":-29.772727966308594,"baseWidgetSpan":90,"baseModelMin":-130,"baseModelMax":-40}
    {"headWidgetRef":-29.772727966308594,"headWidgetSpan":90,"headModelMin":-119.7727279663086,"headModelMax":-29.772727966308594,"headEncoderMin":-119.7727279663086,"headEncoderMax":-29.772727966308594}

    Head diagnostics also show 1024 finite input bins and increasing FFT sequence, with the added encoder metadata absent from the base response. All owned instances were disconnected and closed via close MainWindow; none of the operator's instances was driven. No live radio was connected or TX keyed. The simulator acknowledges otherwise unmodeled commands, so this proves GUI reply convergence, not physical Flex encoder headroom or entitlement to infer radio wire generations. Offscreen GPU rendering produced renderedTrace.valid=false; no pixel/scanout claim is made. Demo cannot represent HL2's declared client-owned range or the exact Windows/8600 clipping report.

  • Governance/gates: engine, command-plane and capability findings are byte-identical on base/head: 102 tracked boundary warnings, 369 raw sites, 70 capability booleans. Both generated manifests reproduce; the validator includer growth is disclosed. Registration, frozen CI test gate and git diff --check pass. No new family-string branch, concrete backend cast, raw command template, capability, dependency or keying path was added. A separately model-invocable code-review skill was unavailable; existing bot/human findings were checked rather than treated as evidence by themselves.

CI and recommendation

Request changes for the HL2 regression. All four required checks plus the sanitizer configure check passed, and the commit is signed. Their pull_request runs were created at 2026-10-07 00:47:53/54 UTC; current main 121340c08 is dated 16:14:44 UTC. With strict: false, that green does not establish compatibility with current main. Restore both client-owned gesture paths, run the focused tests including client_display_pan_settings_test, and refresh CI. The bounded headroom and validation work is useful; it should land without undoing #6140.

… and headroom recovery writes only for Auto Floor on an owned pan. Principle II.

- Both dBm-range gestures adopt the operator's range again on a backend
  with no command plane (HL2, aethersdr#6140): a dropped dispatch no longer returns
  before adoptClientOwnedDbmRange(), which is a no-op on a radio-owned range.
- An accepted range reply keeps a radio status received after the write
  (Principle II; FlexLib's handlers ignore an accepted reply). A rejection
  falls back to the range the radio last confirmed, not an earlier request
  still in flight.
- Headroom recovery, floor and ceiling, rewrites min_dbm/max_dbm only with
  Auto Floor on and only on a pan this session owns (maintainer ruling).
  A clipped floor holds baseline acquisition only while a recovery can
  still come, so a foreign pan or a floor at -180 dBm no longer stalls
  auto floor. Recovery helpers never move an endpoint inward.
- The rewire epoch is a SpectrumWidget member, not a dynamic property, and
  one ownership predicate serves the widget and the dispatch path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876
ten9876 dismissed stale reviews from NF0T and themself October 9, 2026 17:24

Your blocker is fixed in 793ab77. Both HL2 gestures (MainWindow_Wiring.cpp arrow and drag-finished handlers) call sendDbmRangeCommand() as a statement followed by the unconditional adopt, as you suggested. client_display_pan_settings_test passes ('the send comes before the mark is read'), and reverting the fix makes it fail again. Your questions were settled by maintainer rulings, also implemented in 793ab77. Q1: both halves of headroom recovery rewrite min/max_dbm only with Auto Floor on and only on an owned pan. Q2: a radio status received after the write wins over an accepted reply (Principle II, matching FlexLib); this is documented in DbmRangeTransition.h. Q3: the PR stays 'Refs', and #5685/#6219 stay open until Windows evidence exists. Your nits are also in 793ab77: the rewire epoch is now a SpectrumWidget member, vita49-format.md's trailing blank line is restored, and the PR body gives the opt-in -DAETHER_BUILD_SPECTRUM_GESTURE_TEST=ON configure flag beside the test run.

@ten9876

ten9876 commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Landing pass at 793ab77cd. The PR body describes the current change; this comment covers the findings that didn't live in a review thread.

Maintainer rulings, implemented here:

  • Headroom recovery: both floor and ceiling run only with Auto Floor on and only on a pan this session owns.
  • Reply ordering: a radio status received after the write wins over an accepted reply (Principle II).
  • Bridge keys: the new get tracedebug keys stay.
  • Clipped floor: baseline acquisition is held only while a recovery can still come.

Earlier bot review

  • The :3978 clipped-floor hold can deadlock on a foreign pan. Fixed: flexFloorClipAwaitsRecovery() holds only when recovery is allowed (Auto Floor on, owned pan) and the -180 dBm / 180 dB limits still leave room. Pinned by "a clipped floor on a pan another client owns still acquires a baseline"; dropping the ownership gate makes it fail.
  • A saturated no-op freezes auto floor. Fixed the same way: requestFlexRadioHeadroom returns false when the candidate equals the current range. Pinned by "a floor already at -180 dBm is not held for a recovery that cannot come", which fails without the limit check.
  • clippedPeakRecoveryRange can lower a ceiling above +20. Fixed: both recovery helpers now clamp so an endpoint never moves inward.
  • The _aetherDbmRangeWireEpoch dynamic property. Fixed: it's now a SpectrumWidget member (advanceDbmRangeWireEpoch() / dbmRangeWireEpoch()).
  • The setEncoderYPixels asymmetry. Fixed: a comment now says the pixel-scale path owns that settle.
  • Perpetual reacquisition during foreign range motion. Not changed. While the encoder range keeps moving, reacquiring is the correct response, and setEncoderDbmRange's equality guard stops it the moment the range is still.
  • The m_encoderYPixels 700 default. Not changed: wirePanadapter seeds it from pan->fftYPixels(), and both y_pixels paths update it.

Automated review of this pass (code-review, medium)

  • Fixed in 793ab77cd:
    • auto floor could still stall on a foreign pan through requestFlexRadioHeadroom;
    • a rejection could fall back to an unconfirmed request (it now uses the radio-confirmed pan->minDbm()/maxDbm());
    • a floor at its limit stalled auto floor;
    • the ownership predicate was duplicated (now one sessionCommandsPan);
    • the docs over-claimed FlexLib reply handling. vita49-format.md now says FlexLib's per-endpoint handlers adopt a rejection's reply body, while this client's combined min_dbm/max_dbm write has no documented rejection-body shape, so it isn't parsed.
  • Not changed:
    • An accepted reply can yield to a stale echo of an earlier request. That is the known cost of the radio-status-wins ruling, which FlexLib shares; the next status corrects it.
    • A failed dispatch on a Flex gives no reconcile. It behaves exactly as the PR head did: the adopt is a no-op on a radio-owned range, and a dispatch can only fail when disconnected or on an implausible range the UI can't produce.
    • The clip statistics run up to three times per frame. That's an O(n) pass over at most 4096 floats at ~30 fps, negligible next to the render.

Not verified:

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at 793ab77cd.

Fixed in this pass (793ab77cd):

  1. HL2 gestures (NF0T, ten9876 blocker). Both handlers adopt the operator's range whether or not dispatch succeeds. client_display_pan_settings_test passes, and fails when the fix is reverted.
  2. Accepted-reply ordering (maintainer ruling: radio status wins). A rejection falls back to the radio-confirmed range. Pinned in panadapter_dbm_range_test, which fails when reverted.
  3. Headroom recovery (maintainer ruling). Floor and ceiling recovery run only with Auto Floor on and only on an owned pan. A clipped floor holds baseline acquisition only while a recovery can still come (foreign pan, -180 dBm limit). Pinned by four spectrum_confirmed_geometry_test cases, each red under its mutation.
  4. Recovery helpers never move an endpoint inward. The rewire epoch is a member, and one ownership predicate is shared. The docs cite the in-repo FlexLib 4.1.5.39794 reply handlers.

Verified empirically:

  • 7/7 focused tests on Linux/Qt 6.12, including client_display_pan_settings_test and the opt-in widget test.
  • Engine-boundary and command-plane findings identical to the merge base; capability records at the frozen 70; manifest clean.
  • Demo bridge: dBm arrow clicks converged the model and encoder (-130/-50, then -130/-60) before and after a reconnect, with transmitting: false.
  • Required CI green on this head.

Reasoned, not run:

  • the HL2 runtime path (the demo has no client-owned range);
  • the rejection-fallback path (there's no reply-injection seam);
  • KN7K's Windows/FLEX-8600 environment, so #5685 and #6219 stay open.

tci_server_review_test was not run (socket-owning).

@ten9876
ten9876 merged commit ac2f0ce into aethersdr:main Oct 9, 2026
5 checks passed
ten9876 pushed a commit to jensenpat/AetherSDR that referenced this pull request Oct 9, 2026
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.
ten9876 added a commit that referenced this pull request Oct 9, 2026
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

| Case and evidence layer | Before | After / result | Coverage and limit
|
| --- | --- | --- | --- |
| Held right-scale release, live native Cocoa/Metal | Two of ten
captured separated releases on #6241 showed approximately ±27 dB floor
departures, about 116 ms after release. | **20 held releases**, both
directions: maximum rendered-floor departure **1.53 dB** in the first
second after release. | FLEX-8400M 4.2.18.41174; MW AM; Auto Floor OFF;
2D; average 50, weighted OFF, 25 FPS; midpoint −100 dBm; ±37.55 dB
shifts. Measured relative to the last pre-release draw. |
| Same captured failing frames, production C++ replay | Smoothed-bin
median departures **26.89 / 27.13 dB**; guard completed on the first
queued old decode. | **1.16 / 0.31 dB**; completion waits for the
current decode generation and target interpretation. | Same vectors,
ordering and smoothing inputs. Deterministic replay is separate from
hardware/render proof. |
| Intermediate endpoint aperture, captured-frame C++ replay | Omitting
partial endpoint candidates left median errors **14.11 / 14.32 dB** and
falsely completed the transition. | Both recorded frames reprojected;
residual median errors **0.72 / 0.62 dB** and transition stayed pending.
| Both endpoint combinations covered by socket-free helper and
real-widget regressions. |
| Rapid A→B→A, live decoder/widget pipeline | Two-aperture assumptions
discard older in-flight candidates; queued older A could retire a newer
A request. | **10 rapid reversal pairs (20 releases)**: maximum
smoothed-bin median departure **1.94 dB** in the first second after each
pair. | Per-frame CPU/widget measurements. Native draw records paused
during this portion, so it is not rendered-trace timing proof. |
| Late old-wire frame after target appears; matching/rounded reply |
Correction could be discarded by completion/reconciliation. | Target
completion retains bounded candidates; old frames still reproject;
matching replies preserve the guard. | Deterministic production-widget
tests; explicit cancellation and expiry also covered. |
| Genuine RF-gain / frequency change during candidate window | Retaining
old-scene comparisons could incorrectly reinterpret a real level change.
| History invalidates; injected −104→−72 dBm RF scene remains −72 dBm.
Production bridge also observed gain 0→32→0 and center 1.000→1.050→1.000
MHz. | Unit test proves preservation of true levels; live readback
proves control convergence. Broader band/preamp bounce remains separate.
|
| Raw VITA decode cross-check, live capture | Aperture assumptions were
previously ambiguous. | **5,446 complete decoded FFTs** independently
reconstructed from raw pixel samples: **zero parity discrepancies**
above 20 µdB. Five pre-release decodes arrived after release; 50 inputs
with >10 dB median error were corrected before averaging. | Raw data,
identifiers and logs remain local; only aggregate results and images are
published. |
| Regression protection | Removing the metadata-aware correction
reproduces the ordering failure. | **8/8 selected offscreen tests
passed**; disabling the widget metadata branch caused 21 assertions to
fail; removing endpoint candidates restored the recorded errors above. |
The real-widget test is opt-in, enabled locally. No new socket peer or
CI gate additions. Hosted CI is pending. |

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 `c7c94dcebad13b1026eb680baba163ca3378ebaa` built
successfully with `cmake --build build -j22`; its eight selected tests
passed. A read-only merge-tree check against refreshed main `25de97c3`
was 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:

![Release-pop before and
after](https://raw.githubusercontent.com/jensenpat/AetherSDR/aether-6238-panadapter-proof/.pr-assets/6238-release-validation.png)

Native after screenshot from our RX-only FLEX-8400M run (static
appearance; timing proof is the chart above):

![MW AM after the range-release
repair](https://raw.githubusercontent.com/jensenpat/AetherSDR/aether-6238-panadapter-proof/.pr-assets/6238-release-after.png)

## 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 with `QT_QPA_PLATFORM=offscreen ctest
--test-dir build -j22 --no-tests=error` and 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_test` pins this:
register a stream, decode, assert the scale is `valid()`, 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_test` and
`spectrum_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 and `transmitting: 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
`tracedebug` polling 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)

---------

Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
rfoust pushed a commit that referenced this pull request Oct 12, 2026
#6255)

Refs #5678 (HL2 parity rows), #5263 (the drop notice). No `Fixes` line:
I found no issue for this.

## Limits first

- **Nothing here was seen in the running app or on a radio.** The defect
is a source reading of `main`; the fix is tested on `RadioModel` with an
injected backend, and the four `MainWindow` call sites are pinned as
text only.
- **This is not an M4 conversion.** The `display panafall set` text is
not replaced by a typed intent. It moves out of `MainWindow_Wiring.cpp`
into the two wire sites `RadioModel` already has.
`tools/check_command_plane.py`: `MainWindow_Wiring.cpp` 20 to 14,
`RadioModel.cpp` unchanged at 135.
- **One design choice is yours: the Black Level gate.** It reads
`RadioCapabilities::panBinsAbsolute()`, the same proxy for the row unit
the renderer reads (`radio-capabilities-map.md` calls it a proxy). If
the row unit gets its own signal later, this gate should follow it. An
undeclared `panAmplitude` reads as not absolute, so a backend that
computes dB rows and forgets the declaration gets a false notice, not a
missing one.
- **On Icom, Black Level keeps its notice, also when it comes from Clone
to all Pans or Reset display defaults.** Reset turns auto black on, so
the value it writes is not in use at that moment; the notice is still
raised there, as it is on `main`. I did not make that case quieter.
- **ANAN and RTL-SDR are reached** (both declare `binsAbsolute`), and
the test does not run with their capabilities: only Hermes-Lite 2, Icom
and Flex.
- **Two other false notices on a Hermes-Lite 2 are not touched:** the
dBm scale (`sendDbmRangeCommand`, beside the dBm-range work of my #6140,
now merged) and `cw break_in` (left open by #6111).
- **The test ends in a source-text pin** (six checks). It names its
claim.
- The branch is rebased onto `121340c0`, with no conflict; head
`182bb99c` adds two review commits on `1935624d`. Two open PRs edit the
same files: #6241 merges with this branch without conflict; #6199
conflicts with it only on adjacent new lines in the `tests/tests.cmake`
test list (it also conflicts with `main` itself in `RadioModel.cpp`,
independently of this PR). Neither touches the four sites, the two
setters, or the gain and black text. Neither merged tree was built.

## What was wrong on main

On a backend with no Flex command plane, four Display-panel controls set
their value on the `SpectrumWidget`, which draws it, and then also sent
Flex wire text:

| control | site in `src/gui/MainWindow_Wiring.cpp` | text |
|---|---|---|
| WtrFall Gain | `wirePanadapter`,
`SpectrumOverlayMenu::wfColorGainChanged` handler | `display panafall
set <wf> color_gain=N` |
| Black Level | `wirePanadapter`,
`SpectrumOverlayMenu::wfBlackLevelChanged` handler | `display panafall
set <wf> black_level=N` |
| Clone to all Pans | `MainWindow::cloneDisplaySettingsToAllPans` |
both, per target pan |
| Reset display defaults | `wirePanadapter`,
`SpectrumOverlayMenu::displaySettingsReset` handler | `color_gain=50`,
`black_level=15` |

Each went through `RadioModel::sendCommand()` to `sendCmd()`, which has
no command plane to write to, logs the drop and emits `commandDropped`.
`MainWindow` shows that once per session as "nothing was sent to the
radio". So the notice was raised for a control that had acted, and the
one-shot was spent.

`RadioModel::setWaterfallColorGain()` and `setWaterfallBlackLevel()`
already return early without a command plane. The four sites did not use
them: those setters address the active pan, the sites address a named
one.

Manual Black Level acts on a Hermes-Lite 2 since #6115
(`WaterfallLevelMap::manualBlackThreshold` on absolute-dB rows).

## The change

- `RadioModel` gains `setWaterfallColorGainFor(panId, gain, autoBlack)`
and `setWaterfallBlackLevelFor(panId, level)`. All four setters go
through two private senders, `sendWaterfallColorGain()` and
`sendWaterfallBlackLevel()`, which hold the only wire site and the only
command-plane gate for each key.
- The four sites call those setters after the widget call, and pass the
pan's `wfAutoBlack()` with the gain. They no longer build wire text.
- **WtrFall Gain** is withheld without a command plane where the client
draws it: auto black on, or `panBinsAbsolute()`. With manual black on
any other row (Icom), every row is black whatever the gain, so it goes
to `sendCmd()` and is reported.
- **Black Level** is withheld without a command plane only where
`panBinsAbsolute()` is true (Hermes-Lite 2, ANAN, RTL-SDR). Elsewhere it
goes to `sendCmd()` and is reported. On Icom the manual black point is
still in tile units against a dB row, so every position draws black: the
slider does nothing there and says so.
- The connect-time replay (`setWaterfallBlackLevel()` from
`panadapterInfoChanged`) stays quiet on every backend with no command
plane, as on `main`. No operator moved anything.
- No capability is added and no family is named.
- `docs/architecture/radio-capabilities-map.md` records the flag's new
consumer.

**A Flex is sent what it is sent today**: same text, same pan, same
order. The sites used `sendCommand()` and the setters use `sendCmd()`;
the only thing `sendCommand()` adds is the pan-ownership gate, which
matches `display pan set ` and not `display panafall set `.

## How this sits with the #5919 review

That review kept the notice for an operator-moved control the backend
cannot apply, because nothing happened and the notice says so. It was
about receive controls; nobody has ruled on display text, and I am not
reading it as covering this.

The rule used here is narrower than "no command plane, no notice": the
send is withheld only where the control has acted in the client. Where
it has not (Black Level on Icom, and WtrFall Gain on Icom with manual
black), the notice stays and the test pins it. If you would rather keep
the notice on these four until pan commands get a typed intent, close
this and I will leave them.

## Tests

`pan_display_drop_notice_test` (new, socket-free, 41 checks). The
injected backend carries `capabilities()` read off a real `Hl2Backend`,
`IcomCivBackend` and `FlexBackend`; panes are made the way a backend
without Flex status makes them.

- **Hermes-Lite 2 capabilities, no command plane (12 checks):** the
renderer's law changes with gain (auto and manual black) and with the
manual black level for a dB row; none of the four raises
`commandDropped`; the same gain text sent raw is still reported, so the
alarm is live in the fixture.
- **Icom capabilities, no command plane (13 checks):** with auto black
gain raises nothing; with manual black no gain changes a dB row and gain
raises `commandDropped`, with the exact text; every Black Level position
draws a dB row black and the slider still raises `commandDropped`; Clone
reports the black level only with auto black, and both keys with manual
black; Reset reports the black level only; the connect-time replay
raises nothing.
- **A command plane (10 checks):** the wire text compared whole through
a command sink on an unopened `RadioConnection`, for each control, for
the target pan and not the active one, gain before black. With
absolute-dB rows and a command plane the black level is still sent.
- **Source-text pin (6 checks).** The claim: each of the four sites sets
the widget and then calls the model setter, and `MainWindow_Wiring.cpp`
builds no gain or black wire text of its own. No behavioural seam
reaches it: the sites are `MainWindow` lambdas and no test target
constructs a `MainWindow`. It shows how the sites are written, not that
they run.

**Mutations**, each as a scratch commit on `182bb99c` through the full
suite:

| mutation | failing checks of 41 | other tests |
|---|---|---|
| both gates removed, the four sites as on `main` | 15 |
`icom_family_test` |
| Black Level gated on `hasCommandPlane()` alone | 4 | none |
| colour gain ignores auto black (row unit only) | 4 | none |
| colour gain ignores the row unit (auto black only) | 4 | none |
| colour gain always withheld (the `1935624d` rule) | 2 | none |
| the connect-time replay reports its drop | 1 | `icom_family_test` |

The legs share one capture of dropped commands, so a wrong drop is also
counted by the checks after it in that leg.

Full ctest on macOS (Qt 6.12.0, Ninja, RelWithDebInfo) at `182bb99c`:
654 of 658 pass, at -j8 and on the serial rerun. The four failures are
known on clean `main` on this machine, each with only its known check
lines: `control_receive_test` ("RTL adjustable filter qualification
excludes legacy WFM", #6248), `vkamp_connection_test` ("commands after a
disconnect are dropped, not queued", #5254),
`peripheral_auth_dialog_test` (#6008, a serial-port name of this host)
and `unified_title_bar_test` (its two menu checks, #6224). This PR
changes `MainWindow_Wiring.cpp`, but only the four display sites; the
same two title-bar checks fail identically on two other branches at the
same base that do not touch it. Nothing else this PR touches is built by
those tests.

Every script step of the `Static checks` job was run locally at
`182bb99c` with stdin from `/dev/null` and is clean: colour ratchet +0,
`gen_touchpoint_manifest.py --check` up to date with no change,
`check_a11y.py` on the one changed GUI file with no findings. The
`Sanitizer option configures` job needs the CI image and was not run.

## Not verified

- Nothing in the running app; nothing on a Hermes-Lite 2, an Icom or a
Flex.
- Whether each of the four controls is enabled and reachable on a
Hermes-Lite 2 today.
- That the Icom Black Level slider is dead, and WtrFall Gain with it
under manual black, is the renderer's arithmetic plus `IcomCivBackend`
sending its scope rows through `toDbm`; I have no Icom.
- That the three sites pass the pan's own `wfAutoBlack()` is pinned by
source text only.
- ANAN and RTL-SDR, beyond their `binsAbsolute` declaration.
- Linux and Windows builds: CI will say.

73, ON8ST

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: high High priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants