fix(spectrum): reset FFT trace EMA on TX→RX so the floor doesn't linger (#3804)#3831
Conversation
…er (aethersdr#3804) After TX→RX the displayed FFT noise floor stayed elevated and decayed slowly. setTransmitting(false) already resets every other TX→RX display state (waterfall blanker, native-tile wait, dBm-range freeze) but never the FFT trace's client-side EMA (m_smoothed). During the UNKEY_REQUESTED window the radio keeps streaming TX-contaminated FFT frames (aethersdr#1927); those poison m_smoothed, and at SMOOTH_ALPHA=0.35 the EMA takes ~5-7 frames (~200-300 ms at 25 fps) to wash out — leaving the trace floor visibly elevated after key-up. Reset the trace EMA on the first clean RX frame via the existing m_resetFftSmoothingOnNextFrame mechanism (the same pattern used by the lock / retune / dBm-range paths) so it is taken raw instead of weighted against TX-era history. Scope: this is the client-side contributor only. The radio firmware's own FFT averaging buffer (display pan average / weighted_average) is the dominant slow-decay source and is not client-fixable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the one-line change against the codebase — it's correct and well-scoped.
Verified:
- The new
m_resetFftSmoothingOnNextFrame = truesits in the(!tx && m_transmitting)branch ofsetTransmitting(), alongside the other TX→RX resets (m_txEndMs, native-tile wait,endTxDbmRangeFreeze()), so it only fires on a genuine key-up edge — not on everysetTransmitting(false)call. - It reuses the existing, proven flag exactly as the lock (
SpectrumWidget.cpp:4878), retune (6348), dBm-range (4125), and FFT-scale (1441) paths do. The flag is consumed once inupdateSpectrum()(4566):m_smoothed = *spectrumBins, i.e. the next frame is taken raw instead of EMA-weighted against TX-era history. So this genuinely completes a reset block that was missing this one member. - No convention concerns: no settings, no allocations, no lifetime/RAII issues, no new system boundaries to guard. The flag is default-initialized (
SpectrumWidget.h:802).
One thing worth noting (no change requested): because the radio keeps streaming TX-contaminated frames through the UNKEY_REQUESTED window, the first post-key-up frame the flag lands on may itself still be slightly contaminated — m_smoothed then re-anchors from that frame rather than a fully clean one. That's strictly better than the multi-frame EMA tail this replaces, and the waterfall blanker (m_txEndMs) covers that window separately, so it's fine as-is. I mainly call it out because it's the same subtlety your honesty note already gestures at.
The honesty note about the bench is appreciated and accurate — a clean dummy-load RX can't produce the contaminated frames needed to measure the tail shrink, so "builds, stays level, no regression, TX-safe" is the right claim to make. The change stands on its own as a low-risk correctness completion regardless.
Thanks for the thorough write-up and the careful scoping (client-side only, firmware averaging buffer explicitly out of scope). LGTM.
🤖 aethersdr-agent · cost: $3.7830 · model: claude-opus-4-8
|
Single-file, one line of code, nine lines total including comment. One of the cleaner fixes in the queue. Root cause and fix are correct. The inline comment is the right shape. It explains the WHY, quantifies the impact (~200–300 ms, 5–7 frames, SMOOTH_ALPHA), and explicitly bounds what this does not fix (firmware averaging buffer). That last sentence is especially good — it tells a future reader the exact right place to look for the remaining effect. The honesty note in the PR body is Principle VIII applied correctly. Explicitly calling out that the dummy-load bench can't reproduce the TX-contaminated frames needed to measure the tail shrink, and making only the claim the evidence supports ("builds, stays level, no regression, TX-safe"), is exactly right. The fix stands on its own as a correctness completion regardless of what the bench can prove. CI: All six checks green. The live-RF limitation (CI can't produce TX-contaminated frames from a dummy load) is inherent and honestly disclosed — no concern there. Constitution: Principle VIII (Evidence Over Assertion) and Principle XI (Fixes Are Demonstrated) both honored. Principle II respected by explicitly scoping to the client-side contributor only and leaving the firmware averaging buffer out of scope. For future reference (not blocking): No Tier 3 only ( |
NF0T
left a comment
There was a problem hiding this comment.
One-line fix in the correct branch, reusing the existing m_resetFftSmoothingOnNextFrame mechanism already used by lock/retune/dBm-range paths — completes a reset block that was missing this one member. Inline comment is WHY-correct and bounds the firmware-side contributor explicitly. Honesty note about dummy-load bench limitations is Principle VIII applied correctly. All six CI checks pass. Tier 3 only.
Squash subject: fix(spectrum): reset FFT trace EMA on TX→RX so the floor doesn't linger — Principle VIII. (#3831)
… CWX, floors, pan mgmt (#3646) (#3832) Expands the agent automation bridge with the verbs and read-fields a TX/spectrum behavioural test actually needs. Every one was exercised live against a FLEX radio while reproducing and verifying the [#3804](#3804) FFT-floor work (companion fix PR #3831). No behaviour changes outside `AETHER_AUTOMATION`. ## New verbs | Verb | Purpose | TX-gated | |---|---|---| | `slice txant <ANT>` / `slice rxant <ANT>` | Set TX/RX antenna deterministically — the GUI controls are blocking `QMenu::exec()` popups `invoke()` can't drive. Routes through `SliceModel::setTx/RxAntenna`. | no | | `floors` | Lightweight per-pan FFT floor read (`displayFloorDbm`/`noiseFloorDbm`/`panIndex`), ~15 ms vs `dumpTree`'s ~250 ms — fast enough to sample a sub-second post-TX transient. | no | | `cwx send <text>` / `cwx speed <wpm>` / `cwx stop` | Drive the CWX keyer (CW's rapid TX→RX edges are the easy floor-recovery repro). | `send` only | | `txwaterfall on\|off` | Toggle the radio's `show_tx_in_waterfall` so a test can confirm keyed TX paints the waterfall. | no | | `pan create` / `pan remove <id>` / `pan center <mhz>` | Add/remove a panadapter and recenter it (band-change lever — plain `tune` uses `autopan=0` and clamps to the pan's range). Multi-band coverage. | no | ## New read-fields / properties - **`txAntenna`** on the slice snapshot (`get slice`) — previously unreadable, so the live TX antenna couldn't be asserted before keying. This is what makes a dummy-load TX-safety gate enforceable. - **`showTxInWaterfall`** on the transmit snapshot (`get transmit`). - **`noiseFloorDbm` / `displayFloorDbm` / `panIndex`** Q_PROPERTYs on `SpectrumWidget`, surfaced generically via `QObject::property()` in `describeWidget`/`floors` — no core→gui coupling. `displayFloorDbm` reflects the smoothed trace (`m_smoothed`); `noiseFloorDbm` tracks the raw frame. ## Safety `cwx send` rides the existing `AETHER_AUTOMATION_ALLOW_TX` rail and arms the force-unkey watchdog, same as `txtest`/`key`. Antenna/floor/pan verbs don't key. ## Known follow-ups (not in this PR) - `RadioModel::removePanadapter` sends `display pan close`, which doesn't close a *panafall*-created pan — the second pan had to be torn down via slice removal. - `grab` returns the *first* `SpectrumWidget` in a multi-pan layout — a `grab pan <index>` selector (keyed on the new `panIndex`) would let a driver capture a specific pan. 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
Issue
#3804: after TX the FFT noise floor takes a long time to drop back to its pre-TX level.
Root cause
SpectrumWidget::setTransmitting(false)resets every other TX→RX display state — the waterfall blanker (m_txEndMs, #2117), the native-tile wait, the dBm-range freeze — but never the FFT trace EMA,m_smoothed.During the radio's UNKEY_REQUESTED window the panadapter keeps streaming TX-contaminated FFT frames (the same behaviour called out for the waterfall in #1927). Those frames feed
updateSpectrum()and poisonm_smoothed; atSMOOTH_ALPHA = 0.35the EMA then needs ~5–7 frames (~200–300 ms at 25 fps) to wash back out, so the displayed floor stays visibly elevated right after key-up.Fix
One line in
setTransmitting(false): setm_resetFftSmoothingOnNextFrame = trueso the first clean RX frame is taken raw instead of being weighted against TX-era history. This reuses the exact mechanism already used on the lock / retune / dBm-range-change paths.Scope
This addresses the client-side contributor only. The triage on the issue identifies the radio firmware's own FFT averaging buffer (
display pan average/weighted_average) as the dominant slow-decay source — that is firmware-side and not client-fixable (workaround: lower Average + Wt Avg off).Testing
Verified live on a FLEX radio (fw 4.1.5) using the agent automation bridge. To do this at all, the bridge first had to gain numeric FFT-floor observability and deterministic antenna/CWX/pan control — that work is the companion PR #3832. The fix was A/B'd by building it behind a temporary env-gate (since stripped) so baseline-vs-fixed could be measured on the same binary.
Harness. A
floorsbridge verb samples the panadapter's measured display floor (displayFloorDbm, offm_smoothed) at ~20–25 Hz — fast enough to resolve the ~300 ms client-EMA transient (dumpTreeat ~3.5 Hz is not). Every keying was gated on a livetxAntenna == ANT2readback (dummy load) and confirmedtransmitting == falseafterward.Findings
peak during TX +0.00 dB,peak after unkey +0.11 dBThis exercised the change across two bands, two panadapters, two-tone + CW, and multi-slice TX routing, with a hard ANT2 safety gate and post-run
transmitting=falseconfirmation on every pass. No regression, no stuck/elevated floor, no TX-safety violation.Honesty note — what this does not prove. On a clean dummy-load RX, no TX-contaminated frames reach the client, so the displayed floor stays flat on both the unfixed and fixed builds — there was nothing to wash out to measure a delta against. The original report is from a live-RF environment that this bench (dummy load) does not reproduce. So the bench result is "builds, stays level, no regression, TX-safe", not a measured shrink of the multi-second tail. The change remains a low-risk correctness completion of the existing TX→RX reset block, matching the lock/retune/dBm-range paths.
Build
Builds clean standalone (Ninja, RelWithDebInfo) off
origin/main; the only file touched isSpectrumWidget.cpp(+9 incl. comment).💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat