feat(smartmtr): selectable SmartMTR meter view for the VFO flag#3723
Conversation
ten9876
left a comment
There was a problem hiding this comment.
Thanks @dsocha — this is substantial, well-structured work, and the transparent scope-diff in the #3450 implementation-update comment is exactly the right way to surface a broadened scope. The architecture looks sound from the file layout: a render-only SmartMtrWidget, a MeterViewController + DisplaySettings for the global persisted choice, separate geometry/style/config headers, and the existing S-meter kept pixel-identical and opt-in. I want to give this a full review — but there are three things blocking that right now.
1. Stale base — rebase needed (blocking)
The branch is based on 0ba821b0 (#3676), which is 26 commits behind current main, and it now conflicts in every integration file it touches: CMakeLists.txt, MainWindow_Wiring.cpp, SpectrumWidget.{cpp,h}, and especially VfoWidget.{cpp,h}. VfoWidget collides directly with two changes that landed since you branched:
- #3706 (Kiwi diversity ESC controls — added ~136 lines to
VfoWidget.cpp) - #3698 (CAT VFO-B dialect capability — also reworked VfoWidget)
The "+743/−10, minimal/additive original-file impact" is accurate against your base, but that integration has to be re-done against current main. Until it's rebased, the diff is against a 26-commit-old tree, so I can't review the integration (how it wires into VfoWidget/SpectrumWidget) for correctness. Please rebase smartmtr onto current main and resolve the conflicts — the VfoWidget one (vs #3706/#3698) is the real work.
2. CI hasn't run (blocking)
Checks are empty — they can't run while the PR is CONFLICTING. The rebase will unblock this; I'll want all six green before merge.
3. RFC #3450 authorization for the broadened scope (blocking — governance)
#3450 is still open and maintainer-review-labeled, and it was never formally authorized — the triage said "authorize once Q1 (dynamics) is pinned," you pinned it, but no explicit maintainer go-ahead followed. On top of that, the implementation is materially broader than the RFC proposed: a separate selectable SmartMTR view (vs redesign-in-place), a new TX mic-level meter, numeric value labels, and an inline selector panel. To be clear, I think the broadened shape is defensible and arguably safer than the original (opt-in, old meter untouched) — but a feature this size needs an explicit maintainer decision recorded on #3450 before it can land. A maintainer will weigh in there.
Suggested order
- Rebase onto
main+ resolve the 6-file conflicts (VfoWidget first). - Push → CI green.
- (In parallel) the #3450 scope decision.
- Then I'll do the full architecture + integration review.
None of this is a knock on the work — it's a big, clean feature that just needs to catch up to a fast-moving main and clear the RFC gate. Happy to review thoroughly once it's rebased.
|
Update — blocker #3 (RFC authorization) is now resolved. RFC #3450 has been authorized with the broadened scope explicitly approved on the record: #3450 (comment) — the as-built design (separate opt-in SmartMTR view, S-meter pixel-identical by default, extremes + numeric labels, TX MICPEAK meter, inline selector) is approved as a lower-risk shape than the original in-place redesign. So you don't need to do anything on the governance side. That leaves only the two mechanical blockers before the full review:
One small thing for the linkage: the PR body says "Implements #3450", which isn't a GitHub auto-close keyword — please change it to Ping me once it's rebased and green and I'll do the full architecture + integration review. |
There was a problem hiding this comment.
Thanks for this, @dsocha — this is a genuinely high-quality contribution. The architecture is clean and the original-file impact really is minimal as advertised: the standard S-meter path is preserved and the new view is cleanly isolated behind MeterViewController. A few notes below, but nothing blocking from my read.
What's good
- Settings layer follows convention.
DisplaySettingspersists throughAppSettingsnested-JSON (readObj/write), notQSettings, and mirrors the existing"True"/"False"string + typed-enum-token style. The new keys default safely (.toString("False")/"None"/"Medium"), so no migration step is needed. - Ownership / RAII is correct.
SmartMtrWidgetis reparented intom_meterStackviaaddWidget; theQGraphicsOpacityEffects and menu widgets all get Qt parents; themicMetersChanged/moxChangedconnections passvfoas the context object so they auto-disconnect on flag teardown. No leaks or dangling lambdas spotted. - Separation of concerns.
SmartMtrWidgetis render-only and radio-agnostic; the dBm/dBFS domain knowledge lives inSmartMtrConfig, and the parent translatesDisplaySettingsenums to widget-local enums so the widget stays free of the settings layer. The UNITS→pixel geometry and static-layer pixmap caching are nicely done and the repaint-gating rationale (GPU panadapter recomposite cost) is well-justified. - Signal wiring matches.
MeterModel::micMetersChanged(float,float,float,float)andTransmitModel::moxChanged(bool)line up with the connect lambdas, and the VFO correctly self-filters to its own TX slice before swapping to the mic scale.
Minor — stale comments contradict the shipped code
These are non-functional but worth fixing since they'll mislead a future maintainer:
SmartMtrWidget.h:22-24— "This is the first step of a multi-step build. For now it paints only the static control body, the recessed hole, and a single indicator bar pinned atSmartMtrUnits::kIndicatorFraction— no signal processing yet." The widget now does full ballistics + extremes tracking, andkIndicatorFractionno longer exists anywhere in the tree. This header comment reads as describing an earlier WIP.DisplaySettings.h:45— "consumed by the SmartMTR rendering layer (wiring lands in a follow-up)." The wiring (pushSmartMtrOptions/pushSmartMtrInput) is present in this same PR, so the "follow-up" note is stale.
Minor observations (no action needed)
pushSmartMtrInput()hardcodes the signal scale at-127..-13 dBmregardless of source;indicatorPosition()clamps, so KiwiSDR readings outside that band degrade gracefully — just flagging the assumption.- The value-label overlay refresh is throttled to ~20 Hz while the in-widget markers repaint at up to 60 Hz, so the text can lag the triangle by a frame or two during a fast glide. You've documented this as intentional and it's a reasonable cost tradeoff.
CI is green across build / check-macos / check-windows / CodeQL on the head commit. Nice work — the comment cleanup is the only thing I'd ask for before merge.
(Automated review — COMMENT only. A maintainer (@ten9876) still needs to sign off per CODEOWNERS.)
🤖 aethersdr-agent · cost: $3.4638 · model: claude-opus-4-8
Click the signal meter on the VFO flag to switch between the standard S-meter and a new SmartMTR view (placeholder for now). The choice is global and persists across restart. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the SmartMTR placeholder label with a painted control built on a resolution-independent UNITS coordinate system. - SmartMtrStyle.h: design tokens (colors + UNIT geometry) in one place. - SmartMtrGeometry.h: the single UNITS->pixel mapping (fit-and-center, uniform scale, aspect preserved). - SmartMtrWidget: thin paintEvent orchestrating per-element draw methods (control body, recessed rounded hole, indicator bar with bright end marker, inset shadow rim). Fills available parent width via heightForWidth, keeping the design aspect ratio. - VfoWidget/TabStack: forward heightForWidth from the current page so SmartMTR drives the strip height; the S-meter page is unaffected. No signal processing yet — indicator hard-pinned at 50%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a render-only meter model so the SmartMTR control can show different measurements per radio state, and wire it to live data. - SmartMtrConfig.h/.cpp: MeterInput (kind/value/min/max) pushed by the parent, plus a per-kind registry. Signal uses a piecewise map with S9 pinned at the scale midpoint; mic level maps linearly. Scale markers (size/color/optional label) are static per kind; indicatorPosition() handles null -> scale-min and clamps to the band. - SmartMtrWidget: setMeterInput() + marker/label rendering (symmetric pair stuck to the hole, labels above top ticks in the marker colour). The indicator bar starts at hole-local 0 so a min/blank value still shows a 0..10 stub; value end-line marks the reading. - VfoWidget/MainWindow_Wiring: feed received signal (dBm) on RX and mic level (dBFS) on the TX slice while transmitting, switching on MOX. - Style: marker normal/high colours; scale-band, marker-size and label tokens. Fix foreground/high reds (Qt reads 8-digit hex as #AARRGGBB). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add ScaleMarker.labelOffset to shift a label horizontally from its tick (+ right / - left), scaled through the UNITS mapping. Set +20/+40/+60 to -4 units so the tick falls between the digits instead of under the "+". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Soft gradient inset shadow (3-unit depth) replacing the flat rim, so the hole reads as a real recess. - Two label styles via ScaleMarker.labelStyle: Strong (full size, regular weight) and Normal (slightly smaller, light weight); S9 is the only strong S-meter label. - Small ticks drawn at reduced opacity (kMarkerSmallOpacity) so they read as secondary to the labeled ones. - kControl set to #161620 to match the slice flag background (base + its 5% white overlay). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add SmartMTR-only display options below the S-Meter/SmartMTR selector buttons, shown vertically: a "Show extremes" checkbox, an "Extremes speed" select (Slow/Medium/Fast), and a "Show values" select (None/Signal/Extremes), separated by rule lines. - Persisted via DisplaySettings (typed ExtremesSpeed/MeterValues enums); defaults: extremes off, speed Medium, values None. Not yet consumed by the rendering layer. - Options are disabled while the standard S-meter is selected (they tune the SmartMTR view only). Disabled select rows dim via a 0.45 opacity effect to match the disabled-checkbox label. - "Extremes speed" is further gated on "Show extremes" being checked. - With "Show extremes" off, the "Extremes" item is hidden from the "Show values" dropdown and the selection snaps back to None. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the SmartMTR macOS app's asymmetric exponential ballistics to the in-app SmartMTR bar: fast ~18.2 ms attack, slow ~269 ms lazy decay, for the d'Arsonval "jumps up, sags down" envelope-follower feel. Drives a per-instance MeterSmoother on the normalised scale fraction (filtering the mapped position, so the piecewise S0-S9 / S9+ slope is respected), ticked by an 8 ms PreciseTimer that stops itself once settled. Snaps across the RX<->TX scale change instead of gliding. Isolated to the SmartMTR widget; the standard S-meters are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SmartMTR canvas was 40 units tall but its content only reaches 35 (hole at 20–30 + 5-unit bottom ticks), so the widget reserved a 5-unit empty band below the meter — a visibly larger gap to the DSP/MODE/XRIT/ DAX row than the S-meter leaves. Drop kControlH 40→35 to hug the content extent; the scale is width-driven so the bar/hole/ticks render at the same size and only the empty bottom band is removed. Also nudge inset-shadow alpha 90→100 and small-tick opacity 0.55→0.6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the min/max peak-hold "extremes" to the SmartMTR meter and surface the configured value labels. - MeterExtremes: sliding-window min/max/avg envelope with a constant linear slew, ticked alongside the bar's MeterSmoother (settles when idle). - Markers: apex-up triangles stuck inside the hole's top edge — min+max for RX (signal), peak-only for mic (TX), where the bar tracks the windowed avg. - Fades: proximity (min/max too close) x signal (near-floor) for RX; mic marker always shown. Signal-value label never fades. - Value labels (Show values = Signal/Extremes): two lines (S-unit over dBm, units dimmed to emphasise the number), drawn in the spectrum overlay just below the flag with a dimmed connector line, on top of the slice. - S-unit uses floor (e.g. -86 dBm reads s6). - Options broadcast to every open flag via MeterViewController. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The min/max extremes markers returned toward the needle in visible steps instead of the original SmartMTR's smooth, slow glide. The motion law and slew speed already matched the original (~30 deg/s); the defect was the repaint/tick cadence during the return: - Lean mode gates meter repaints to 12 Hz. The marker glide piggybacked the bar's gate, but the bar is settled during the glide, so the markers were drawn at 12 Hz (~2 deg/frame hops). - The animation timer settled aggressively and relied on the irregular ~60 Hz meter packet feed to restart it; the window only prunes inside tick(), so slow returns advanced at the bursty feed rate. Match the original's steady-loop return without changing the speed: - MeterExtremes::tick() stays active while the markers are still standing off the needle (not only while mid-slew), so the window prunes and the markers slew at the timer rate; it settles once min ~= max ~= needle. - A returning marker repaints on its own kExtremesRepaintHz (60 Hz) gate, bypassing the bar's lean gate; the bar repaint is gated on barMoving so the timer staying alive through a hold doesn't cause needless repaints. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the meter paint into cached static layers and the moving parts. The control body + recessed hole (below the bar) and the inset shadow + scale markers/labels (above the bar) are rendered once into two pixmaps and blitted each frame, rebuilt only when size/kind/DPR changes. The per-frame paint during animation then skips the gradient fills and per-tick font work, keeping the hot repaint cheap over the GPU panadapter — only the indicator bar and the extremes markers are drawn live. Also fix the value label: a signal value just above S9 (-73 to ~-72.5 dBm) took the overshoot branch but rounded to "+0dB", a meaningless zero overshoot. Only use the "+NdB" form once the rounded overshoot is at least +1; sub-1 dB overshoots fall through to the S-unit branch, which reads s9 near -73. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… & physics Add a "TX meter" option (None / Mic Level, default None) below "Show values": the SmartMTR meter only swaps to the mic (dBFS) scale on TX when opted in; otherwise it stays on the RX signal scale. Persisted in DisplaySettings and broadcast live to every flag via MeterViewController::txMeterChanged. Mic meter reworked to read like an audio meter rather than an S-meter: - bar tracks the live mic level with snappy PPM ballistics (~2 ms attack / ~45 ms release), distinct from the signal meter's analog d'Arsonval sag; ballistics now switch per MeterKind (applyBallistics). - peak marker is driven by the radio's separate MICPEAK stat over UDP (MeterInput.peak; MeterExtremes external-peak override) instead of a locally-synthesized window max, and slews fast (kPeakSlewUnitsPerSec) to track it. The previously-dropped micPeak arg is now plumbed through setMicLevel(). - linear -40..0 dBFS scale with markers at -40/-30/-20/-10 (blue) and the -5/0 clip-warning zone (red); per-tick label offsets center the digits. Label rendering: small labeled ticks share the large-tick baseline and fade with their tick; all labels vertical-center on the regular-label line so a larger strong label stays aligned (kLabelHeight bumped to 14). Numeric value labels are now suppressed for non-signal kinds (mic shows the bar + peak marker, no floating dB text). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The curved underline only needs ~3px of clearance below the meter for its 2px round-capped stroke; 8px pushed the tab row down more than necessary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In SmartMTR view the meter-strip underline anchored at g.bottom()+1, but its upward-hooked ends rise above the baseline into the SmartMtrWidget child rect. That child is opaque and repaints over the parent, clipping the hooks so the blue underline read as "cut". Drop the SmartMTR baseline below the meter child (tips clear g.bottom() by 1px) and grow the underline-room spacer to 5px in SmartMTR mode only; the standard S-meter (transparent page, 3px spacer) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SmartMtrWidget.h described an earlier single-step WIP (static body + a pinned bar at the long-gone kIndicatorFraction); it now renders full ballistics + extremes. DisplaySettings.h said the options wiring "lands in a follow-up", but pushSmartMtrOptions() ships in this same PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aethersdr#3706 changed VfoWidget's layout model: relayoutToCurrentContent() now pins the flag height with setFixedHeight() (min==max). The SmartMTR paths still called a bare adjustSize(), which can't grow the flag past that clamp — so opening the meter selector left no room for the menu row (menu invisible) and switching to the taller SmartMTR page mis-sized the meter. Route both setMeterMenuOpen() and applyMeterView() through relayoutToCurrentContent(), which clears the min/max clamp before recomputing and re-pinning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@ten9876 / @aethersdr-agent — rebased and ready for the full review. Rebased onto current One regression caught and fixed during integration ( Comment cleanup ( PR body updated to Full build links clean locally (macOS). CI re-dispatched on |
There was a problem hiding this comment.
Thanks for this, @dsocha — this is an unusually well-engineered contribution. The separation is clean (render-only SmartMtrWidget, radio-agnostic SmartMtrConfig, design tokens isolated in SmartMtrStyle.h, geometry in SmartMtrGeometry.h), the conventions are spot-on, and the comments explain the why (GPU-panadapter repaint gating, the dBm-vs-UNITS domain split, the d'Arsonval vs PPM ballistics) rather than the what. A few specific things I checked and liked:
- AppSettings, not QSettings — persistence goes through the existing
DisplaySettingsnested-JSON blob (Principle V), reusing the establishedreadObj()/write()helpers and the"True"/"False"string convention already in that file. 👍 - RAII / ownership — every widget is parented or reparented via
addWidget; theQGraphicsOpacityEffects are parented to their rows;<QGraphicsOpacityEffect>is correctly included (header only forward-declares it). - Signal wiring is correct —
MeterModel::micMetersChanged(float,float,float,float)andTransmitModel::moxChanged(bool)both match the new slots, and the connections usevfoas the context object so they auto-disconnect on flag destruction. The mic feed correctly gates onm_transmitting && m_slice->isTxSlice(), so only the TX slice's flag shows mic level. - Original-file impact really is minimal and additive; the S-meter path is preserved.
One substantive concern: the hidden widget runs by default
pushSmartMtrInput() is called unconditionally on every meter update (setSignalLevel, setReceiveMeterReading) and only null-checks m_smartMtrWidget — it does not check m_smartMtr. So even with the default S-Meter view selected (the off-by-default majority case), every meter packet flows into SmartMtrWidget::setMeterInput(), which runs the ballistics and starts the 120 Hz m_animTimer (advance() ticking m_smooth + m_extremes every ~8 ms). The widget is on the hidden stack page so it never paints, but the per-tick math + timer churn runs on every VFO flag for users who never enable SmartMTR.
Suggested fix — short-circuit when the view isn't active. Either:
void VfoWidget::pushSmartMtrInput()
{
if (!m_smartMtrWidget || !m_smartMtr) // skip while the S-meter is shown
return;
...
}and call pushSmartMtrInput() once from applyMeterView() when switching to SmartMTR so it re-seeds immediately; or guard inside setMeterInput() with an if (!isVisible()) return; (the hidden stack page reports isVisible() == false). The first is cleaner and also avoids the redundant work below.
Minor
- Redundant JSON re-parse on the hot path.
MeterViewController::txMeter()/showExtremes()/ etc. each re-read and re-parse the fullDisplayJSON blob fromAppSettingson every call.pushSmartMtrInput()callstxMeter()per meter update.m_smartMtris already cached in the controller — caching the extremes/tx options the same way (and refreshing them in the setters) would keep these off the per-packet path. Gating onm_smartMtrabove mostly mitigates it, but caching is the tidier fix.
These are both efficiency notes, not correctness bugs — the feature itself looks solid and the default-off S-meter behavior is genuinely pixel-preserved. Nice work.
(Note: no CI check runs were reported for the head SHA at review time, so I couldn't confirm the build there — worth a glance once they land.)
🤖 aethersdr-agent · cost: $3.9620 · model: claude-opus-4-8
pushSmartMtrInput() only null-checked m_smartMtrWidget, so every meter packet flowed into the hidden SmartMtrWidget even with the default S-Meter view selected — running its ballistics and restarting the 120 Hz animation timer on every flag, for users who never enable SmartMTR (the page never paints). Gate the feed on m_smartMtr, and re-seed once from applyMeterView() on switch-in so the meter shows the current level immediately. Raised in review of aethersdr#3723. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MeterViewController::txMeter()/showExtremes()/extremesSpeed()/showValues() each re-parsed the full Display JSON blob from AppSettings on every call; txMeter() is read per meter packet. Cache all four as members (same pattern as m_smartMtr): seeded in the ctor, returned from the getters, refreshed in the setters. Raised in review of aethersdr#3723. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @ten9876 — both addressed in The hidden widget running by default — gated Redundant JSON re-parse — cached Builds clean locally and I re-verified the GUI (default S-meter unchanged; switch-in seeds + animates; options apply live). Pushed to the same branch — CI runs are sitting in |
ten9876
left a comment
There was a problem hiding this comment.
Approving — full architecture + integration review done, all three earlier blockers cleared (RFC #3450 authorized, rebased onto current main, CI green ×6). The rebase resolution and the #3706 ESC/diversity interlock are intact, and both perf fixes (0835a632, 42d0e144) are correct and complete.
Merging now. The review findings are non-blocking and tracked as a consolidated fast-follow in #3750 — H1 (the SmartMTR needle showing a false S0 for KiwiSDR sources with no calibrated meter) is on us: it interacts with our KiwiSDR public-receiver feature (#3679), which you couldn't have known about. Thanks for an unusually clean, well-documented contribution, @dsocha.
This review supersedes my earlier CHANGES_REQUESTED (those blockers are resolved).
…3750 H1) (#3751) Fixes the **H1** item from #3750 (SmartMTR fast-follow), introduced with the SmartMTR meter view in #3723. ## Problem `VfoWidget::pushSmartMtrInput()` hardcodes the FLEX dBm scale (`-127` S0 .. `-13` S9+60) for **every** source. When a KiwiSDR slice has no calibrated meter, the receive path sets the sentinel `m_signalDbm = -130` and the dBm label correctly shows **"Meter ---"** — but the SmartMTR needle clamped `-130` to S0 and rendered a confident **S0**, and the numeric value labels printed the bogus sub-S0 dBm. The PR author wasn't aware of our KiwiSDR public-receiver feed (#3679), so this interaction was on us (maintainers) to fix. ## Fix Track whether the current signal reading is a real calibrated dBm: - new `m_signalHasDbm` — `true` for FLEX (`setSignalLevel`), set from the KiwiSDR meter capability in `setReceiveMeterReading`; - feed it into `MeterInput::hasValue` for the `Signal` kind. The widget **already** parks/fades the indicator and suppresses the value labels when `hasValue == false` (see `indicatorPosition()` → `kScaleMin`, the `kSignalFadeLoDbm` needle target, and the `if (m_input.hasValue)` value-label guard). So no widget change is needed — the SmartMTR view now shows no-data, matching the "Meter ---" label, instead of a false S0. The mic (TX) path keeps `hasValue = true`. ## Scope 3 small edits in `VfoWidget.{cpp,h}`, behavior-neutral for FLEX (the flag is always `true` there). No change to the S-meter path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…3750 M1–M4) (#3752) Addresses the four **MEDIUM** items from #3750 (SmartMTR fast-follow), all introduced with #3723. Each is independent; grouped into one PR per the follow-up plan. ### M1 — TX/mic mode never lets the 120 Hz timer settle `MeterExtremes.h`: in external-peak (mic) mode the MAX marker is a held radio stat that sits above the needle by design, so the `standingOff` keep-alive kept `tick()` returning `true` for the entire TX, pinning ~120 Hz repaints over the GPU panadapter. Each mic packet re-arms the timer via `setMeterInput` (`setExternalPeak` keeps `hasData()` true), so we only need to keep animating while a marker is actually slewing — drop the standing-off keep-alive in that mode. Bounded before (PTT release resets), but this restores the idle-settle the perf work intended. ### M2 — NaN/Inf permanently poisons the meter `SmartMtrWidget::setMeterInput`: a non-finite value survived `std::clamp` (both comparisons false), stuck in the bar smoother (`needsAnimation()` never cleared → timer never stopped), and poisoned `MeterExtremes::m_sumRaw` permanently (corrupting `avgRaw()` → value labels). Drop non-finite `value`/`peak` at entry; the last good frame stays until a finite reading arrives. ### M3 — Cross-flag option-control desync `VfoWidget`: the global `extremesChanged`/`txMeterChanged` signals only re-pushed each flag's *rendering*, not its option *controls*, which are seeded once at build. With the selector open on two flags, changing a setting on one left the other's checkbox/combos stale. New `syncSmartMtrSettingsControls()` re-seeds the four controls from the (cached, singleton) `MeterViewController`, `QSignalBlocker`-guarded to avoid a re-broadcast loop, then re-runs `syncSmartMtrSettingsState()`. Wired to both global signals. ### M4 — Font change doesn't invalidate the cached marker layer `SmartMtrWidget`: the static-layer cache key is only `{size, kind, dpr}`, but the cached marker glyphs are built from `font()`. Override `changeEvent` to invalidate on `FontChange`/`ApplicationFontChange`/`StyleChange` so a theme/font-size change doesn't leave stale glyphs until the next resize. **Scope:** +75 lines across `MeterExtremes.h`, `SmartMtrWidget.{cpp,h}`, `VfoWidget.{cpp,h}`. No change to the S-meter path; FLEX behavior unaffected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#3750 L1–L3) (#3753) Addresses the three **LOW / nit** items from #3750 (SmartMTR fast-follow), all from #3723. Low-risk cleanup; grouped into one PR. ### L1 — dead no-op seed call `VfoWidget::buildUI()` had `pushSmartMtrInput(); // seed the at-rest display`, but `m_smartMtr` is still `false` during `buildUI()`, so the gate added in #3723 early-returns — the call does nothing. The real seed is `applyMeterView()`, run from the constructor when the persisted choice is SmartMTR. Replaced the dead call with a comment pointing at where seeding happens. ### L2 — misleading migration comment `DisplaySettings.h` claimed the legacy flat `"LeanMode"` key is "migrated into this blob **on first read**." It isn't — migration is an explicit `migrateLegacy()` call at startup; `readObj()` does no migration. Reworded so a future maintainer doesn't assume read-time migration safety. ### L3 — stale raw min/max on empty extremes window `MeterExtremes::tick()` left `m_minRaw`/`m_maxRaw` at their previous values when the sliding window emptied (only `m_hasData` was cleared). Harmless today — every reader (`drawExtremes`, `extremeLabels`, `extremesOpacity`) gates on `hasData()`/`extremesActive()` first — but a latent trap for any future reader of `minRaw()`/`maxRaw()`. Clear them to `0.0`, matching `reset()` and the member initializers. **Scope:** comment-only + 2 trivial lines, no behavior change. No S-meter path impact. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…rites (rebased #3765) (#3771) Closes #3762. **Rebased + conflict-resolved version of #3765** by @dsocha (original authorship preserved on all three commits). Opened from a maintainer branch because the original is on a fork; #3765 should be closed in favor of this. ## What it fixes (unchanged from #3765) Two GPU-flag-sprite rendering bugs in the SmartMTR meter view (#3723): 1. **Extremes value labels never drew (#3762)** — `drawSmartMtrLabels()` gated on `m_smartMtrWidget->isVisible()`, false in GPU-sprite mode. Now gates on `!size().isEmpty()` (the same condition the sprite is drawn under), so labels track the markers in both GPU and software paths. 2. **Meter selector vanished from the sprite** — `QWidget::render()` can't rasterize a `QGraphicsEffect`. The selector now gates its underline on an explicit `m_meterMenuOpen` flag (not the hidden row's `isVisible()`), and the option rows drop the `QGraphicsOpacityEffect`s in favor of `setEnabled(false)` + `:disabled` stylesheet dimming (new `QComboBox:disabled` rule in `ComboStyle.h`), so they stay visible **and** dimmed in the sprite. ## Rebase / conflict resolution (the reason this is a new branch) The original #3765 conflicted with **#3760** ("disable identity opacity effect…"), which merged into `main` after #3765 was branched. Both target the same blank-rows-in-sprite bug, but: - **#3760** only neutralized the effect at *identity* (opacity 1.0) — i.e. for *enabled* rows. A *disabled* row still carried an active 0.45 effect, so it would **still render blank** in a sprite. - **#3765** removes the effects entirely, so both enabled and disabled rows render. **#3765 ⊇ #3760.** Resolved `syncSmartMtrSettingsState` in favor of #3765's approach and **dropped #3760's now-dead `setRowOpacity` lambda + the `m_*Fade` members**. Verified: no dangling `m_*Fade`/`kDisabledOpacity` references; the `QGraphicsOpacityEffect` on the DSP filter-level row (meant to vanish in sprites) is untouched. ## Verification - Rebased onto `main` (`437f7469`); conflict resolved; commits signed. - Composes with the merged #3752 (M3 `syncSmartMtrSettingsControls` re-seeds combos, then `syncSmartMtrSettingsState` disables the rows) and #3758 (a11y — `QAccessibleInterface` reference preserved; a11y linter passes 0 findings). - Original hardware test (macOS / FlexRadio 6000) per #3765. Credit: @dsocha. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Dariusz Socha <dariusz.socha.pl@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #3450.
Adds an opt-in SmartMTR meter view selectable from the VFO flag's meter menu, alongside the existing S-meter (unchanged, pixel-identical when not selected). The implemented scope is broader than the original RFC text — see the implementation-update comment on #3450 for the point-by-point diff and rationale.
How to try it
The new view is off by default. To switch:
Click the meter strip again to close the selector; click S-Meter to switch back.
What it adds
SmartMtrWidget(UNITS→pixel geometry, static-layer pixmap caching, repaint gating tuned for the GPU panadapter).MeterSmoother); separate constant-slew min/max "extremes" markers over a sliding window (Fast 1s / Medium 3s / Slow 5s).MeterViewController+DisplaySettingsnested-JSON blob; live-broadcast to every open flag.Original-file impact (minimal)
CMakeLists.txt+3 (source registration)MainWindow_Wiring.cpp+8 (two signal connections)SpectrumWidget+19 (overlay hook)VfoWidgetlarge but additive; the S-meter path is preserved.Full app builds & links clean. Settings persist via
AppSettingsnested-JSON (Principle V). All commits signed.🤖 Generated with Claude Code