Skip to content

feat(smartmtr): selectable SmartMTR meter view for the VFO flag#3723

Merged
ten9876 merged 20 commits into
aethersdr:mainfrom
dsocha:smartmtr
Jun 23, 2026
Merged

feat(smartmtr): selectable SmartMTR meter view for the VFO flag#3723
ten9876 merged 20 commits into
aethersdr:mainfrom
dsocha:smartmtr

Conversation

@dsocha

@dsocha dsocha commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

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:

  1. Open a VFO flag and click the meter strip (the S-meter / dBm area). An inline selector drops down under the tab row.
  2. Click SmartMTR. The choice is global (applies to every open flag) and persists across restarts.
  3. The SmartMTR-only options — Show extremes, Extremes speed (1s/3s/5s window), Show values, TX meter — appear in the same panel.

Click the meter strip again to close the selector; click S-Meter to switch back.

What it adds

  • Separate, render-only SmartMtrWidget (UNITS→pixel geometry, static-layer pixmap caching, repaint gating tuned for the GPU panadapter).
  • Analog d'Arsonval bar ballistics (reuses MeterSmoother); separate constant-slew min/max "extremes" markers over a sliding window (Fast 1s / Medium 3s / Slow 5s).
  • Optional numeric value labels (s-unit + dBm), drawn in the spectrum overlay so they sit above the slice.
  • TX mic-level scale (radio MICPEAK-driven peak) swapped in on TX when opted in.
  • Global, persisted choice via MeterViewController + DisplaySettings nested-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)
  • VfoWidget large but additive; the S-meter path is preserved.

Full app builds & links clean. Settings persist via AppSettings nested-JSON (Principle V). All commits signed.

Touches src/gui/ + CMakeLists.txt → maintainer review (@ten9876) required per CODEOWNERS.

🤖 Generated with Claude Code

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

  1. Rebase onto main + resolve the 6-file conflicts (VfoWidget first).
  2. Push → CI green.
  3. (In parallel) the #3450 scope decision.
  4. 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.

@ten9876

ten9876 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

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:

  1. Rebase onto current main + resolve the integration conflicts (the VfoWidget collision with the since-merged [codex] Add Kiwi diversity receive interlock #3706 / fix(cat): rigctld dialect — gray out the meaningless VFO B selector #3698 is the real work).
  2. CI green (it'll dispatch once the conflicts are cleared).

One small thing for the linkage: the PR body says "Implements #3450", which isn't a GitHub auto-close keyword — please change it to Closes #3450 so the RFC closes itself when this merges (otherwise I'll add it to the squash-merge body at merge time).

Ping me once it's rebased and green and I'll do the full architecture + integration review.

@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, @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. DisplaySettings persists through AppSettings nested-JSON (readObj/write), not QSettings, 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. SmartMtrWidget is reparented into m_meterStack via addWidget; the QGraphicsOpacityEffects and menu widgets all get Qt parents; the micMetersChanged/moxChanged connections pass vfo as the context object so they auto-disconnect on flag teardown. No leaks or dangling lambdas spotted.
  • Separation of concerns. SmartMtrWidget is render-only and radio-agnostic; the dBm/dBFS domain knowledge lives in SmartMtrConfig, and the parent translates DisplaySettings enums 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) and TransmitModel::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:

  1. 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 at SmartMtrUnits::kIndicatorFraction — no signal processing yet." The widget now does full ballistics + extremes tracking, and kIndicatorFraction no longer exists anywhere in the tree. This header comment reads as describing an earlier WIP.
  2. 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 dBm regardless 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

dsocha and others added 17 commits June 22, 2026 22:23
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>
@dsocha

dsocha commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@ten9876 / @aethersdr-agent — rebased and ready for the full review.

Rebased onto current main (f190643c, #3722). The integration conflict was in VfoWidget.cpp against #3706 (Kiwi diversity interlock): #3706 replaced the post-collapse adjustSize() with relayoutToCurrentContent(), and the SmartMTR work added a pushSmartMtrOptions() call on the same expand path — resolved to keep both. #3698 didn't touch VfoWidget. Verified #3706's interlock methods + TX-inhibit logic are intact post-rebase.

One regression caught and fixed during integration (70e28f8f): #3706 changed VfoWidget's layout model to pin the flag height via setFixedHeight() (min==max). The SmartMTR paths still called a bare adjustSize(), which can't grow past that clamp — so opening the meter selector left no room for the menu row, and switching to the taller SmartMTR page mis-sized the meter. Routed setMeterMenuOpen() and applyMeterView() through relayoutToCurrentContent() (clears the clamp, recomputes, re-pins). Verified locally: meter sizes correctly and the selector menu opens.

Comment cleanup (352ef63a): fixed the two stale comments you flagged — the SmartMtrWidget.h WIP/kIndicatorFraction paragraph and the DisplaySettings.h "wiring lands in a follow-up" note.

PR body updated to Closes #3450.

Full build links clean locally (macOS). CI re-dispatched on 70e28f8f but the runs are sitting in action_required (fork-PR workflow-approval gate) — could you approve the workflow runs when you get a chance? Should go green; same set passed pre-rebase. Thanks!

@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, @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 DisplaySettings nested-JSON blob (Principle V), reusing the established readObj()/write() helpers and the "True"/"False" string convention already in that file. 👍
  • RAII / ownership — every widget is parented or reparented via addWidget; the QGraphicsOpacityEffects are parented to their rows; <QGraphicsOpacityEffect> is correctly included (header only forward-declares it).
  • Signal wiring is correctMeterModel::micMetersChanged(float,float,float,float) and TransmitModel::moxChanged(bool) both match the new slots, and the connections use vfo as the context object so they auto-disconnect on flag destruction. The mic feed correctly gates on m_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 full Display JSON blob from AppSettings on every call. pushSmartMtrInput() calls txMeter() per meter update. m_smartMtr is 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 on m_smartMtr above 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

dsocha and others added 2 commits June 22, 2026 23:20
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>
@dsocha

dsocha commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @ten9876 — both addressed in 0835a632 (substantive) and 42d0e144 (the cache).

The hidden widget running by default — gated pushSmartMtrInput() on m_smartMtr so meter packets no longer reach the hidden SmartMtrWidget while the default S-Meter view is selected; its ballistics and the 120 Hz m_animTimer now stay idle. Added a single pushSmartMtrInput() re-seed in applyMeterView() on switch-in so the meter shows the current level immediately (your first suggested option).

Redundant JSON re-parse — cached showExtremes/extremesSpeed/showValues/txMeter in MeterViewController (same pattern as m_smartMtr): seeded in the ctor, returned from the getters, refreshed in the setters. txMeter() is now off the per-packet path entirely.

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 action_required again, so they'll need another approval when you get a sec. 🙏

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

@ten9876 ten9876 merged commit f3b9a80 into aethersdr:main Jun 23, 2026
6 checks passed
ten9876 added a commit that referenced this pull request Jun 23, 2026
…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>
ten9876 added a commit that referenced this pull request Jun 23, 2026
…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>
ten9876 added a commit that referenced this pull request Jun 23, 2026
…#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>
@dsocha dsocha deleted the smartmtr branch June 23, 2026 05:42
ten9876 added a commit that referenced this pull request Jun 23, 2026
…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>
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.

[RFC] Redesign slice S-meter: optional extremes markers + improved indicator dynamics

2 participants