Skip to content

Fix flat 3D FFT traces after dBm scale changes#4279

Open
rfoust wants to merge 1 commit into
aethersdr:mainfrom
rfoust:codex/fix-dbm-band-handshake
Open

Fix flat 3D FFT traces after dBm scale changes#4279
rfoust wants to merge 1 commit into
aethersdr:mainfrom
rfoust:codex/fix-dbm-band-handshake

Conversation

@rfoust

@rfoust rfoust commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

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

  • Local build passes (cmake --build build)
    • Completed the full 854-target build successfully; the final incremental build reported no work remaining.
    • ctest --test-dir build -R '^(dss_renderer_test|panadapter_dbm_range_test|automation_drag_at_test)$' --output-on-failure
    • ctest --test-dir build -R '^automation_probe_field_mapping$' --output-on-failure
    • python3 tools/gen_bridge_docs.py --check
    • python3 tools/check_engine_boundary.py --strict
    • python3 tools/check_a11y.py
  • Behavior verified on a real radio if applicable
    • Used dragAt to exercise the right-side 3D dBm-floor drag through the real widget event path.
    • One completed drag emitted exactly one display pan set request (min_dbm=-117.50 max_dbm=-22.50); no per-move command stream was produced.
    • After the 96-row history window refreshed, all 96 visible 3D rows were non-flat and the largest localized equal-value run was five bins.
    • The radio remained connected and receiving throughout; no transmit action was used.
  • Existing tests pass (CI)
    • Pending the upstream CI run for the updated commit.
  • Reproduction steps documented if user-reported bug
    1. Enable the 3D FFT display.
    2. Drag the right-side dBm floor vertically; optionally Command-drag the scale into a tight range.
    3. Optionally change bands immediately afterward.
    4. Before this change, the 3D history could collapse into uniform straight lines or retain localized clipped plateaus after the floor moved. After this change, the scale follows the local drag smoothly, the radio encoding aperture updates once on release, and newly arriving rows retain spectral variation.

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls — use nested-JSON-under-one-key
    (Principle V)
  • Code is clean-room — not decompiled, disassembled, or
    reverse-engineered from a proprietary binary (Principle IV)
  • All meter UI uses MeterSmoother (AGENTS.md convention) — no meter UI changed
  • Documentation updated if user-visible behavior changed
  • Security-sensitive changes reference a GHSA if applicable — not applicable

Generated with OpenAI Codex.

@rfoust
rfoust requested review from a team as code owners July 17, 2026 04:07
Copilot AI review requested due to automatic review settings July 17, 2026 04:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 dragAt automation (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.

@rfoust
rfoust marked this pull request as draft July 17, 2026 04:18
@rfoust
rfoust force-pushed the codex/fix-dbm-band-handshake branch from 8662612 to a9c481e Compare July 17, 2026 15:53
@rfoust rfoust self-assigned this Jul 17, 2026
@rfoust
rfoust force-pushed the codex/fix-dbm-band-handshake branch from a9c481e to 275b32e Compare July 17, 2026 19:37
@rfoust
rfoust force-pushed the codex/fix-dbm-band-handshake branch from 275b32e to d849061 Compare July 18, 2026 01:22
@jensenpat
jensenpat marked this pull request as ready for review July 19, 2026 04:28

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

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_dynamicRange from 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/memory broaden 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 behind main (base 28f0a47) 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;

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.

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

@ten9876

ten9876 commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

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 (DbmRangeTransition::manualRequestRangedssFloorDbm + displaySpanDb) instead of the hidden 2D ref level, so the radio's FFT encoder aperture matches what's drawn and bins no longer collapse to one endpoint at deep zoom. Spot-checks that would normally be blockers came back clean:

  • Div-by-zero / NaN: evaluate() guards oldRange<=0 || newRange<=0 and empty/size-mismatched bins; manualRequestRange guards !isfinite(floor) || floor<=-500 || span<=0; rowStats uses isfinite filters. No unguarded log/dBm math.
  • Handshake retire vs. reconcile is generation-stamped and the timeout QTimer::singleShot is bound to sw as context (auto-cancels on destroy); a matched echo clear()s and bumps the generation so the stale timer no-ops. panadapter_dbm_range_test exercises the hold/timeout/no-echo/band-restore/latest-wins branches.
  • Namespace: both SpectrumWidget.cpp and MainWindow_Wiring.cpp are inside namespace AetherSDR, so the unqualified DbmRangeTransition:: resolves — no build break there.
  • automationDssSnapshot ring index (headRing()+age)%rows() matches rowAt(age) = (m_head+age)%kRows, so per-age stats are consistent.

Secondary notes (non-blocking, worth a maintainer eye)

  • Scope: this PR bundles the display/DSP fix with a substantial new automation-bridge surface — dragAt, targettune, memory verbs, the JSON args positional field, and probe token attachment (~216 LOC in AutomationServer.cpp + docs). They're the harness used to prove the fix, but they're orthogonal user-reachable API and would benefit from their own review pass (or a split). The new tune/memory verbs route through commanded-target/memory-recall and carry the VFO-lock + SWR-sweep guards, which is the right call.
  • dssSpanDb() lower clamp drops 45→10 dB (displaySpanDb, SpectrumWidget.cpp). That's a global change to 3D surface steepness for all users, not just the bug scenario — worth an explicit UX sign-off beyond the single-radio verification in the test plan.
  • The 3D floor-drag release path now rewrites m_refLevel/m_dynamicRange (the 2D scale state) from floor+span (SpectrumWidget.cpp mouseReleaseEvent). Likely intended (radio-authoritative), but it means a 3D-only floor move changes what the 2D panadapter shows on mode switch — please confirm that coupling is desired.
  • handleLine args path: findVerb(cmd) silently no-ops on an unknown cmd, so {"cmd":"bogus","args":"…"} falls through to the later unknown-command handling rather than erroring on the args — harmless, just noting.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants