[codex] Harden SmartMTR meter fast-follows#3757
Conversation
There was a problem hiding this comment.
Pull request overview
Hardens the SmartMTR fast-follow path in the GUI so “no meter data” and invalid meter values don’t produce misleading readings or poison smoothing/extremes state, while keeping multi-flag option controls in sync and avoiding a GPU-composition blanking edge case.
Changes:
- Add input sanitization and no-data rendering behavior to SmartMTR (including cache invalidation on font/style changes).
- Harden
MeterExtremesagainst non-finite inputs and improve external-peak (mic) animation termination behavior. - Add focused regression coverage for
MeterExtremesedge cases and wire it into CTest.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/meter_extremes_test.cpp | Adds regression tests for non-finite rejection, window expiry clearing, and external-peak animation edge cases. |
| src/gui/VfoWidget.h | Declares syncSmartMtrOptionControls() to keep open option controls consistent across VFO flags. |
| src/gui/VfoWidget.cpp | Resyncs SmartMTR option controls on global controller broadcasts; fixes no-data propagation; avoids identity opacity-effect blanking. |
| src/gui/SmartMtrWidget.h | Adds changeEvent() override to invalidate cached static layers on font/style changes. |
| src/gui/SmartMtrWidget.cpp | Sanitizes meter input (range/value/peak), stops animation on no-data, and skips indicator rendering when no value is present. |
| src/gui/MeterExtremes.h | Rejects non-finite samples/peaks, clears raw bounds when the window empties, and refines external-peak settling behavior. |
| src/gui/MainWindow.cpp | Corrects comment to match actual DisplaySettings legacy migration timing. |
| src/gui/DisplaySettings.h | Clarifies migration behavior in the header comment (performed by migrateLegacy() at startup). |
| CMakeLists.txt | Adds meter_extremes_test executable and registers it with CTest. |
Fixes aethersdr#3750. Handle unavailable/non-finite SmartMTR inputs, keep multi-flag option controls synchronized, avoid identity opacity effects blanking selector rows, and add focused MeterExtremes regression coverage.
50b1b6b to
e5c9aa0
Compare
|
Rebased this branch onto current The conflicts were from the overlapping SmartMTR fixes that landed in #3751, #3752, and #3753. I kept the current upstream behavior where it was newer, retained the remaining hardening from this branch, removed one merge artifact ( GitHub now reports Local validation after the rebase:
|
There was a problem hiding this comment.
Reviewed the full diff against the surrounding SmartMtrWidget / VfoWidget / MeterExtremes code. This is a clean, well-targeted set of fast-follows — thanks for the focused scope and the new regression test. All six CI checks are green (build / macOS / Windows / CodeQL / a11y).
What I verified holds up:
- Non-finite guards in
MeterExtremes::record()/setExternalPeak()and the centralizedsanitizedInput()correctly close the "NaN survivesstd::clampand poisons the running sum" path. Moving the sanitize ahead ofm_kind/m_inputassignment (sokindChangedand the extremes feed both see the cleaned input) is the right ordering, and routing every downstream read throughm_inputrather than the rawinputis consistent. - No-data parking:
hasValue == falsenow resets extremes, snaps the smoother, stops the anim timer, and early-returns indrawIndicator(). That matches the "Meter ---" dBm label state and avoids pegging a confident S0 on a Kiwi slice without a calibrated meter. - Option-control resync: folding the per-flag control re-seed + re-push into
syncSmartMtrOptionControls()and wiring bothextremesChanged/txMeterChangedthrough it viathis-context lambdas is correct — the 3-argconnectform auto-disconnects on widget destruction, so no dangling-receiver risk. No stale references to the removedsyncSmartMtrSettingsControlsremain. - Opacity effect:
setEnabled(!fullStrength)so the identity (1.0) effect is detached rather than left installed is a sound fix for the blank-row GPU-composite case, and it preserves the 0.45 dim path. - The new
external peak below needle settles at needleandzero-dt tick keeps pending slew alivetests exercise exactly themaxTgt/pendingExternalSlewedges the diff introduces.
One non-blocking observation:
pushSmartMtrInput() now derives the RX hasValue from usesUnavailableSignalMeter(), while the dBm-label path still maintains m_signalHasDbm separately (setSignalLevel / setReceiveMeterReading). I traced both and they agree in every reachable case (the new predicate is actually stricter — it also gates on reading.valid), so this is not a bug. But there are now two parallel sources of truth for "is the signal meter available"; a future edit to one could silently diverge from the other. Worth a brief comment cross-referencing them, or consolidating, at some later point — not for this PR.
Nice work — LGTM as a COMMENT.
🤖 aethersdr-agent · cost: $4.7970 · model: claude-opus-4-8
There was a problem hiding this comment.
Thanks @rfoust — this is a tidy, well-scoped fast-follow on #3750 and it lands the fixes cleanly. I traced each change against the issue and the surrounding code; everything checks out and all 6 CI jobs are green (build/macos/windows/CodeQL/a11y).
What I verified
H1 — no-data render for uncalibrated Kiwi/non-FLEX meters. pushSmartMtrInput() now sets in.hasValue = !usesUnavailableSignalMeter() for the Signal kind, which reuses the exact state the dBm "Meter ---" label already keys off (VfoWidget::usesUnavailableSignalMeter()), rather than inventing a parallel notion of "unavailable." The blank then propagates correctly through all three render paths:
drawIndicator()early-returns on!m_input.hasValue(no S0 bar stub),- the value labels already gate on
m_input.hasValue(SmartMtrWidget.cpp:592), so the raw sub-S0 dBm no longer prints, setMeterInput()callsm_extremes.reset()and stops the anim timer on!hasValue, so no stale markers and no idle repaint.
That's the right fix and it's consistent with the S-meter path.
M2 (refined) — non-finite hardening. Good defense-in-depth: sanitizedInput() at the widget boundary plus isfinite guards inside MeterExtremes::record()/setExternalPeak() (the latter reset()-ing on a poisoned peak). Belt-and-suspenders, but cheap and correct — a NaN can no longer wedge the smoother or poison m_sumRaw.
M1 (refined) — external-peak timer settle. Replacing the blanket return moving with pendingExternalSlew is a real improvement: the marker keeps animating until it reaches the (clamped-to-needle) peak target, then the timer settles between packets. The maxTgt < needlePosUnits → needlePosUnits clamp is what makes the below-needle case converge, and testExternalPeakStaysAliveOnZeroElapsedTick correctly guards against a zero-dt tick prematurely stopping the timer.
M3 — cross-flag control sync via syncSmartMtrOptionControls() with QSignalBlocker (no re-broadcast loop), and the identity-opacity bypass (effect->setEnabled(!fullStrength)) is a sensible workaround for the blank-row GPU compositing artifact.
Conventions are all honored — no QSettings/flat AppSettings, RAII signal blockers, and the new meter_extremes_test is registered and focused on the edge cases that mattered.
Minor (non-blocking)
sanitizedInput()setsmin=0/max=1when the range is invalid, buthasValueis forced false in that branch so those values are never consumed — harmless, just slightly dead. Fine to leave.- The
DisplaySettings.h/MainWindow.cppcomment rewordings overlap with the L2 nit already addressed in #3753 (this PR's base). Pure churn on those two lines, no behavior change.
Nice work — LGTM from my read.
🤖 aethersdr-agent · cost: $7.1474 · model: claude-opus-4-8
There was a problem hiding this comment.
Thanks for this, @rfoust — clean, well-scoped fast-follow on #3750, and it's nice to see the regression test land alongside the fixes (Principle XI). I checked it out, read through each change against the issue findings, and confirmed all 6 CI checks are green (build on macOS/Windows/Linux, CodeQL, a11y static analysis). No blocking concerns.
What I verified:
- H1 (KiwiSDR/non-FLEX no-data) —
usesUnavailableSignalMeter()correctly gatesin.hasValueon the calibrated/experimental Kiwi meter capability +hasDbm, andSmartMtrWidget::drawIndicator()now early-returns when!m_input.hasValue, so the needle no longer pegs a confident S0 over the "Meter ---" label. The same gate already feedsupdateSignalMeterTarget(), so the S-meter and SmartMTR paths agree. - M1 (external-peak timer settle) — the
pendingExternalSlewcondition replacing the unconditional standing-off keep-alive looks right: in mic mode the timer now stops once both markers reach target and re-arms on the next packet (viam_extremes.hasData()insetMeterInput). ThetestExternalPeak*cases cover settle, zero-dt keep-alive, and the below-needle collapse. - M2 (non-finite) —
sanitizedInput()is a solid consolidation; themin < maxrange guard and the cascade tohasValue=false/hasPeak=falseare all sensible defensive defaults. - M3 (cross-flag desync) — folding the re-seed into
syncSmartMtrOptionControls()and calling it at construction is cleaner, and theselectDatahelper'sidx >= 0guard is a real improvement over the oldsetCurrentIndex(findData(...))which could pass-1. - Blank-row fix —
effect->setEnabled(!fullStrength)to skip the identity opacity effect is the correct call; a disabledQGraphicsOpacityEffectis a no-op and the row renders directly.
One note (not a blocker): M2 is a deliberate behavior change worth confirming is intended. The previous guard dropped a non-finite frame and held the last good frame on screen; sanitizedInput() instead flips hasValue=false, so a transient NaN now blanks the needle for that frame rather than holding. That's consistent with H1's new no-data path and the gap is sub-frame in practice, so I think it's the right choice — just calling it out since the comment that documented the old "last good frame stays on screen" contract was removed.
Nice work — LGTM.
🤖 aethersdr-agent · cost: $6.5177 · model: claude-opus-4-8
|
Closing in favor of #3760. On review, ~all of this PR's substance re-implemented #3750 hardening that already landed via #3751 (H1), #3752 (M1–M4), and #3753 (L1–L3) — after rebasing past those merges, the diff was mostly re-working already-merged The one genuinely net-new fix here — disabling the identity The regression test from this branch wasn't separable: it asserts this branch's Thanks @rfoust — the blank-row catch is a real one and lands via #3760. |
…ender blank (#3760) ## Summary Disables the identity `QGraphicsOpacityEffect` on fully-enabled SmartMTR option rows so they don't render blank when multiple VFO flags are GPU-composited. A fully-enabled option row keeps its `QGraphicsOpacityEffect` at opacity `1.0` (identity). With the GPU-composited slice flags from #3695, an identity opacity effect can render the row's contents blank. The fix disables the effect entirely while it is at full strength (a disabled `QGraphicsOpacityEffect` is a no-op, so the row paints directly) and keeps it installed only while it is actually dimming the row. ## Provenance This is the net-new fix extracted from #3757. The remainder of that PR re-implemented SmartMTR `#3750` hardening that already landed via #3751 (H1), #3752 (M1–M4), and #3753 (L1–L3); #3757 is being closed in favor of this focused change to avoid re-churning already-merged code. Part of #3750. ## Test plan - [x] Local build passes (`cmake --build build --target AetherSDR`) - [ ] Behavior verified on a real radio if applicable — visual (open the SmartMTR option panel with multiple flags up; rows render, not blank) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Fixes #3750. This hardens the SmartMTR meter view fast-follow path: unavailable Kiwi/non-FLEX meter data now renders as no data instead of a confident S0, non-finite meter values are rejected before they can poison smoothing/extremes state, open SmartMTR option controls resync across VFO flags, and fully visible option rows bypass the identity opacity effect that could render them blank when multiple GPU-composited flags were active.
Constitution principle honored
Principle XI — Fixes Are Demonstrated. The patch adds focused MeterExtremes regression coverage for the timer/no-data edge cases and was validated with a local app build plus focused tests.
Test plan
cmake --build build --target AetherSDR -- -j4)ctest --test-dir build -R '^meter_extremes_test$' --output-on-failure; full CI pending)git diff --checkpython3 tools/check_a11y.pyexits 0 with existing warning-only findingsChecklist
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)