Fix flat 3D FFT traces after dBm scale changes#4279
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a Flex panadapter dBm-range transition edge case that could flatten 3D FFT history after aggressive dBm-scale zooming (and sometimes after a subsequent band change). It aligns the radio-requested min_dbm/max_dbm with the range actually displayed in 3D mode, adds a more robust “pending range” handshake/timeout model to avoid stale-echo blocking of radio-authoritative ranges, and introduces regression tests plus an automation verb to reproduce the gesture reliably.
Changes:
- Make 3D dBm-scale drags request the visible (floor-anchored) range and rebase interim FFT frames still encoded with the old range until the new encoding is observed or a timeout expires.
- Add explicit cancellation of in-flight client dBm-range requests so radio-authoritative band-stack ranges can apply immediately (avoiding stale-echo wedging).
- Add
dragAtautomation (TX-guarded) and focused regression coverage for the handshake/rebase logic.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/panadapter_dbm_range_test.cpp | Adds regression coverage for pending-range cancellation, stale-encoding rebasing, and 3D-visible-range request logic. |
| src/gui/SpectrumWidget.h | Exposes cancellation and transition helpers; replaces frame-count hold with a time-based rebase window. |
| src/gui/SpectrumWidget.cpp | Implements 3D-aware range requests, rebase evaluation via shared helper, and explicit pending-transition cancellation. |
| src/gui/MainWindow_Wiring.cpp | Refines the dBm-range handshake lifecycle (generation + timeout) and cancels pending requests before band-stack restores. |
| src/gui/DbmRangeTransition.h | Introduces shared helper logic for 3D range selection and stale-encoding detection/rebasing. |
| src/core/PanadapterStream.h | Adds API to cancel pending dBm-range requests per stream. |
| src/core/PanadapterStream.cpp | Implements pending dBm-range cancellation under stream mutex. |
| src/core/AutomationServer.h | Documents and declares the new dragAt automation verb. |
| src/core/AutomationServer.cpp | Implements dragAt with optional modifiers and TX-safety guarding. |
| docs/automation-bridge.md | Documents dragAt usage and adds it to the verb table. |
| CMakeLists.txt | Adds panadapter_dbm_range_test to the build/test suite. |
8662612 to
a9c481e
Compare
a9c481e to
275b32e
Compare
275b32e to
d849061
Compare
There was a problem hiding this comment.
Nice work — this is a careful, well-scoped fix. Extracting the range-transition state machine into DbmRangeTransition::Handshake (pure, header-only, no Qt-widget deps) and covering it with panadapter_dbm_range_test is exactly the right shape: the trickiest logic (arm → observe → finish → cancel, generation-guarded stale timers, no-echo retirement) is now unit-testable in isolation, and the wiring lambdas read cleanly against it. The float-widening of dssFloorDepth for smooth fractional drag preview + deferred single AppSettings write on release is a tidy solution, and the emit-guard on the rounded integer avoids menu-signal spam. Conventions look good (nested-JSON AppSettings, RAII, std::optional/std::clamp), and all 7 CI checks pass on the head commit.
I have no change-requests — just one design question and two logistics notes.
Non-blocking notes
- 3D floor-drag release now writes
m_refLevel/m_dynamicRangefrom the floor-anchored 3D range (SpectrumWidget.cpp:8134). Confirm this doesn't leak into the live 2D scale when toggling 3D→2D — see inline question. - New automation verbs
dragAt/targettune/memorybroaden the bridge surface, but each is justified as the test path for this fix (modifier-drag through the real event path; radio-authoritative band restores) and is documented + covered. Fine to keep bundled. - GitHub reports the PR as
mergeable_state: dirty— the branch is behindmain(base28f0a47) and will need a rebase before merge; nothing wrong with the diff itself.
🤖 aethersdr-agent · cost: $6.0098 · model: claude-opus-4-8
| beginDbmRangeTransition( | ||
| oldRange.minDbm, oldRange.maxDbm, | ||
| requestedRange.minDbm, requestedRange.maxDbm); | ||
| m_refLevel = requestedRange.maxDbm; |
There was a problem hiding this comment.
The 3D floor-drag release overwrites m_refLevel/m_dynamicRange with the floor-anchored 3D range so the radio command matches what's displayed in 3D — that part makes sense. My only question: m_refLevel/m_dynamicRange are the shared 2D reference-level/span state. After a floor drag in 3D mode, do these get restored to the user's prior 2D scale when they switch back to 2D, or does the 3D-derived range now become the 2D scale too? If there's no restore on mode-switch, a floor drag would silently rewrite the 2D dBm scale. Not blocking — just want to confirm the coupling is intended (and covered).
|
First-pass triage review (not exhaustive — surfacing blockers to keep this moving): Blockers None spotted in a first pass. The core fix looks like it targets the root cause rather than a bandaid: in 3D the request range is now derived from the visible floor-anchored axis (
Secondary notes (non-blocking, worth a maintainer eye)
Nice work — the handshake abstraction + focused tests read well. Blocker-wise this looks clear to proceed; the secondary items are mostly scope/UX confirmations for a deeper reviewer. |
Summary
Fixes flat 3D FFT traces triggered by aggressive dBm-scale zooming and sometimes exposed by a subsequent band change. FlexRadio FFT bins are vertical pixel positions encoded against the radio's current
min_dbm/max_dbm, not absolute dBm samples. In 3D mode, AetherSDR displays a noise-floor-anchored range but could send the radio the unrelated hidden 2D range; at deep zoom, the RF energy fell outside that encoding aperture and every bin was clipped to approximately the same endpoint, producing straight 3D lines. The range-transition handshake also assumed the radio would echo every accepted range change, allowing a stale client request to reject a radio-authoritative band-stack range.The fix sends the range actually displayed in 3D, temporarily rebases frames still encoded with the previous range, retires the handshake when the new encoding is observed or the no-echo timeout expires, and explicitly cancels pending client ranges before band-stack restores. A 3D floor drag now remains a smooth, fractional local preview while the pointer moves, then sends exactly one radio/decoder range update on release using the visible floor and current 3D span. Previously clipped history is left intact and naturally scrolls away as newly encoded rows arrive. The agent automation bridge now reports localized minimum-value counts and longest flat-run lengths, which detect clipped plateaus that whole-row flatness checks miss. Focused regression coverage exercises the range math, handshake behavior, drag path, and plateau diagnostics.
Constitution principle honored
Principle II — Radio-Authoritative Live State. Band-stack dBm ranges reported by the radio take precedence over an unfinished client-side range transition. The client now abandons its pending guard before requesting a radio-authoritative band restore, while preserving a manually requested range when the radio accepts it without sending a status echo.
Test plan
cmake --build build)ctest --test-dir build -R '^(dss_renderer_test|panadapter_dbm_range_test|automation_drag_at_test)$' --output-on-failurectest --test-dir build -R '^automation_probe_field_mapping$' --output-on-failurepython3 tools/gen_bridge_docs.py --checkpython3 tools/check_engine_boundary.py --strictpython3 tools/check_a11y.pydragAtto exercise the right-side 3D dBm-floor drag through the real widget event path.display pan setrequest (min_dbm=-117.50 max_dbm=-22.50); no per-move command stream was produced.Checklist
docs/COMMIT-SIGNING.md)AppSettingscalls — use nested-JSON-under-one-key(Principle V)
reverse-engineered from a proprietary binary (Principle IV)
MeterSmoother(AGENTS.md convention) — no meter UI changedGenerated with OpenAI Codex.