Skip to content

[codex] Harden SmartMTR meter fast-follows#3757

Closed
rfoust wants to merge 1 commit into
aethersdr:mainfrom
rfoust:codex/issue-3750-smartmtr
Closed

[codex] Harden SmartMTR meter fast-follows#3757
rfoust wants to merge 1 commit into
aethersdr:mainfrom
rfoust:codex/issue-3750-smartmtr

Conversation

@rfoust

@rfoust rfoust commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

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

  • Local build passes (cmake --build build --target AetherSDR -- -j4)
  • Behavior verified on a real radio if applicable
  • Existing tests pass (focused ctest --test-dir build -R '^meter_extremes_test$' --output-on-failure; full CI pending)
  • Reproduction steps documented if user-reported bug
  • git diff --check
  • python3 tools/check_a11y.py exits 0 with existing warning-only findings

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)
  • Documentation updated if user-visible behavior changed
  • Security-sensitive changes reference a GHSA if applicable

@rfoust rfoust marked this pull request as ready for review June 23, 2026 02:39
@rfoust rfoust requested a review from a team as a code owner June 23, 2026 02:39
Copilot AI review requested due to automatic review settings June 23, 2026 02:39
@rfoust rfoust requested review from a team as code owners June 23, 2026 02:39

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

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 MeterExtremes against non-finite inputs and improve external-peak (mic) animation termination behavior.
  • Add focused regression coverage for MeterExtremes edge 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.
@rfoust rfoust force-pushed the codex/issue-3750-smartmtr branch from 50b1b6b to e5c9aa0 Compare June 23, 2026 02:49
@rfoust rfoust self-assigned this Jun 23, 2026
@rfoust

rfoust commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased this branch onto current upstream/main (1252d311) and force-pushed e5c9aa04.

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 (SmartMtrWidget::changeEvent duplicate), and kept the focused MeterExtremes regression coverage.

GitHub now reports mergeable: MERGEABLE; the remaining BLOCKED state is required review/checks, not merge conflicts.

Local validation after the rebase:

  • ctest --test-dir build -R '^meter_extremes_test$' --output-on-failure
  • cmake --build build --target AetherSDR -- -j4
  • git diff --check
  • python3 tools/check_a11y.py exits 0 with warning-only existing findings

@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 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 centralized sanitizedInput() correctly close the "NaN survives std::clamp and poisons the running sum" path. Moving the sanitize ahead of m_kind/m_input assignment (so kindChanged and the extremes feed both see the cleaned input) is the right ordering, and routing every downstream read through m_input rather than the raw input is consistent.
  • No-data parking: hasValue == false now resets extremes, snaps the smoother, stops the anim timer, and early-returns in drawIndicator(). 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 both extremesChanged/txMeterChanged through it via this-context lambdas is correct — the 3-arg connect form auto-disconnects on widget destruction, so no dangling-receiver risk. No stale references to the removed syncSmartMtrSettingsControls remain.
  • 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 needle and zero-dt tick keeps pending slew alive tests exercise exactly the maxTgt/pendingExternalSlew edges 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

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

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() calls m_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() sets min=0/max=1 when the range is invalid, but hasValue is forced false in that branch so those values are never consumed — harmless, just slightly dead. Fine to leave.
  • The DisplaySettings.h / MainWindow.cpp comment 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

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

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 gates in.hasValue on the calibrated/experimental Kiwi meter capability + hasDbm, and SmartMtrWidget::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 feeds updateSignalMeterTarget(), so the S-meter and SmartMTR paths agree.
  • M1 (external-peak timer settle) — the pendingExternalSlew condition 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 (via m_extremes.hasData() in setMeterInput). The testExternalPeak* cases cover settle, zero-dt keep-alive, and the below-needle collapse.
  • M2 (non-finite)sanitizedInput() is a solid consolidation; the min < max range guard and the cascade to hasValue=false/hasPeak=false are all sensible defensive defaults.
  • M3 (cross-flag desync) — folding the re-seed into syncSmartMtrOptionControls() and calling it at construction is cleaner, and the selectData helper's idx >= 0 guard is a real improvement over the old setCurrentIndex(findData(...)) which could pass -1.
  • Blank-row fixeffect->setEnabled(!fullStrength) to skip the identity opacity effect is the correct call; a disabled QGraphicsOpacityEffect is 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

@ten9876

ten9876 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

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 MeterExtremes/VfoWidget code (a parallel implementation of the same fixes), which isn't worth the regression surface for no functional gain over main.

The one genuinely net-new fix here — disabling the identity QGraphicsOpacityEffect so SmartMTR option rows don't render blank under GPU-composited flags (#3695) — has been extracted to #3760 (cherry-picked onto current main, builds clean).

The regression test from this branch wasn't separable: it asserts this branch's MeterExtremes architecture (e.g. a non-finite guard inside record()), which differs from #3752's caller-level guard in setMeterInput, so 3/5 cases fail against main — it can't land without bringing the rework. main already guards those cases, just at a different layer. If we want record()-level defense-in-depth + coverage later, that's a clean small follow-up on its own.

Thanks @rfoust — the blank-row catch is a real one and lands via #3760.

@ten9876 ten9876 closed this Jun 23, 2026
ten9876 added a commit that referenced this pull request Jun 23, 2026
…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>
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.

SmartMTR meter view (#3723) — consolidated fast-follow fixes

3 participants