feat(smartmtr): add SWR, Power, and Compression TX meters (#3774)#3776
Conversation
…3774) Extend the SmartMTR TX-meter selector beyond Mic Level to SWR, forward Power, and Compression, following the existing mic-level meter end to end. - DisplaySettings::TxMeter gains SWR/Power/Compression (token-serialized, appended ordinals, unknown -> None: old configs and downgrades are safe). - SmartMtrConfig: new MeterKind values, a shared linear mapper (now also used by mic), a nonlinear SWR map (knee at 2.0), static SWR/Compression configs, and a radio-aware buildPowerConfig() injected at push time. - VfoWidget: combo entries, cached values + setters, a per-TxMeter switch in pushSmartMtrInput(), and txPowerFullScaleW() mirroring TxApplet's exciter scale (barefoot 100->120, Aurora 500->600; red at rated). - MainWindow_Wiring: route txMetersChanged (power/SWR) to the VFO and capture the previously-dropped compression arg; no amp-operate gate. Compression is a reversed gain-reduction meter (-25..0) matching the Phone/CW gauge: the bar fills from the hole's far edge toward -25, the peak marker and ballistics are inverted accordingly, and it is gated on the speech processor (parked at 0 otherwise) to avoid pegged quiescent readings. Peak markers use the sliding-window envelope so they decay slowly like the other meters. The widget changes generalize the existing MicLevel/Signal guards rather than adding per-kind branches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Show meter type" checkbox to the SmartMTR settings menu that draws a short label (MIC / SWR / PWR / COMP) inside the indicator hole telling the operator which TX meter is active. - DisplaySettings::showTxMeterType + MeterViewController cache/setter (broadcast via the existing extremesChanged overlay-options signal): persisted and live-synced across open flags like the other choices. - VfoWidget: the checkbox (sharing a styleMeterCheck helper with "Show extremes"), wired to re-seed/enable through the existing sync paths. Disabled when the TX meter is None. - SmartMtrWidget: setShowTypeLabel + drawTypeLabel. TX meters only (never the RX signal scale); right-aligned in the hole, left for the reversed compression meter; bounded to the marker span; white at 60%; sized to the hole height and clipped. Baked into the below-bar cached layer so it sits above the background but below the indicator bar (zero per-frame cost; re-rasters on kind/size/toggle). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…indows S9 (the only strong/emphasized S-meter label) rendered top-aligned on Linux and Windows while centered on macOS. The labels were placed with Qt::AlignVCenter over a box of fm.height() (the font line box = ascent + descent + leading), which centers the line box, not the glyph ink. The ascent/descent split differs across Core Text / FreeType / DirectWrite, so digits — which sit high in the line box with no descenders — landed centered on macOS but high elsewhere; the larger strong font made S9 the most visible case. Center the actual glyph ink (tightBoundingRect) on the shared center line via baseline-anchored drawText instead, which is backend-independent and uniform across strong/normal labels. Also bump label font weight one step on Windows only (Light->Normal, Normal->Medium) where DirectWrite renders these small pixel sizes too thin; macOS/Linux keep their original weights. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…seline
The prior ink-centering measured tightBoundingRect per label, so each
label centered on its own glyphs ("+20" vs "9" have different ink),
letting baselines drift apart — the row read as ragged.
Measure a representative digit ("0") ink ONCE per style and anchor every
label of that style off it. All same-style labels now share one baseline,
while strong (S9) and normal labels stay co-centered on the shared center
line. Still ink-based, so it remains backend-independent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed ink tightBoundingRect is *measured* ink — FreeType can report it inconsistently with hinting/AA, which left S9 still reading high on Linux. Switch the per-style vertical anchor to QFontMetricsF::capHeight(), a declared font metric that is identical across backends. Digits span [baseline-capHeight .. baseline], so anchoring baseline = centerY + capHeight/2 centres the cap band on the shared line, co-centring strong (S9) and normal labels deterministically on every platform. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks for the thorough write-up, @dsocha — the "as-built vs. RFC" section and the explicit flagging of UX decisions for the maintainer made this much easier to review. The end-to-end extension (selector → persistence → config → wiring) is clean and faithfully follows the existing MicLevel path, Principle V is honored (token-serialized nested JSON, graceful fallback to None), and all 6 CI checks are green (build / check-windows / check-macos / CodeQL / accessibility).
I verified the parts most prone to silent breakage:
- Signal wiring is correct.
micMetersChanged(micLevel, compLevel, micPeak, compPeak)— the lambda picks the 4th arg (compPeak) for compression and the 3rd (micPeak) for mic, both right.txMetersChanged(fwdPower, swr)—setTxPower(fwd)/setTxSwr(swr)order matches. - Power scale parity holds.
txPowerFullScaleW()→ barefoot 100×1.2 = 120 (red @100) and Aurora 500×1.2 = 600 (red @500) matchesTxApplet::setPowerScaleexactly, and the AU- bump mirrors themaxW <= 100rule inMainWindow_Wiring. - Null guards on
m_txModel/m_radioModelare present; cache invalidation correctly gainsm_cacheMin/Maxso a radio swap re-rasters the radio-aware power markers even when the kind is unchanged.
A few things worth a maintainer's eye — mostly questions, not blockers:
-
Undisclosed scope: the scale-label rendering rewrite. Three of the five commits (
ff8e69f,811a211,ce45808) replace the marker-label positioning (AlignVCenter box → declaredcapHeightbaseline) and add a WindowsQFont::Medium/Normal#ifdef.drawMarkers()runs for every kind, so this changes how the existing Signal and Mic meter labels render on all three platforms — yet it isn't in the PR's divergences list. The rationale (backend-independent metric) is sound, but please call it out explicitly and eyeball the Signal/Mic labels on macOS/Linux/Windows so a regression there doesn't ride in under a TX-meter PR. -
Compression bar source. The bar value uses
compPeak(negated), while the mic meter uses the instantaneousmicLevelfor the bar and reservesmicPeakfor the marker.compLevel(the 2ndmicMetersChangedarg) is the instantaneous analog. WascompPeak-for-the-bar deliberate? UsingcompLevelfor the bar +compPeakas an external peak marker would mirror the mic meter exactly; as written, the bar and the window-derived peak both track the peak. -
Power scale for non-AU rigs >100 W (minor).
TxApplettreats anymaxWatts > 100as the 600/500 Aurora scale, whereastxPowerFullScaleW()returnsratedW × 1.2. For a hypothetical non-AU rig reporting, say, 200 W these diverge — your version is arguably more correct, but the "mirroring TxApplet" claim isn't exact at that edge. Probably fine; just noting it.
Nice work, and good call opening this for review while #3774 is still gated rather than pushing for merge.
🤖 aethersdr-agent · cost: $2.7112 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Re-reviewed after the 4 follow-up commits (now +574/-77, CI green). The end-to-end extension remains clean and faithful to the existing MicLevel path, Principle V is honored (token-serialized nested JSON, graceful fallback to None), and I re-verified the load-bearing wiring: micMetersChanged 4th arg → compPeak, 3rd → micPeak; txMetersChanged → setTxPower(fwd)/setTxSwr(swr); txPowerFullScaleW() parity (barefoot 100×1.2=120, Aurora 500×1.2=600); and the m_cacheMin/Max invalidation that re-rasters the radio-aware power markers on a radio swap. Thanks for the thorough as-built write-up, @dsocha.
I read the 3 scale-label commits (ff8e69f, 811a211, ce45808) in full. The change is sound — anchoring the baseline on the declared capHeight() instead of AlignVCenter over measured ink is the right way to get backend-consistent vertical centering (FreeType/Core Text/DirectWrite report tight-ink inconsistently, which is what left S9 top-aligned on Linux/Windows), and horizontal centering is preserved. Two asks and one design question before this is sign-off-ready:
1. Disclose the label-rendering change + confirm no regression on the existing meters. drawMarkers() runs for every MeterKind, so the cap-height re-anchoring and the Windows QFont::Medium/Normal #ifdef change how the existing Signal and Mic labels render on all three platforms — not just the new TX meters. It's an improvement, not a likely regression, but it's currently absent from the "as-built vs. RFC" list. Please add it there and eyeball the Signal/Mic tick labels on macOS/Linux/Windows so a regression can't ride in under a TX-meter PR.
2. Compression bar source — intended? The bar uses compPeak (negated), while the mic meter uses the instantaneous micLevel for the bar and reserves micPeak for the marker. The 2nd micMetersChanged arg (compLevel) is the instantaneous analog. As written, the compression bar and its window-derived peak both track the held peak; exact mic-mirroring would use compLevel for the bar + compPeak as the external peak marker. Was the peak-for-the-bar choice deliberate (it does read as a steadier gain-reduction needle), or should the bar track compLevel?
3. (Minor) txPowerFullScaleW() returns ratedW × 1.2 whereas TxApplet treats any maxWatts > 100 as the fixed 600/500 Aurora scale — these diverge only for a hypothetical non-AU rig reporting >100 W, where your version is arguably more correct. Non-blocking; just noting the "mirrors TxApplet" claim isn't exact at that edge.
On the design itself (compression-as-reversed-gain-reduction mirroring PhoneCwApplet, radio-aware exciter power, nonlinear SWR, the optional meter-type label): I read these as improvements over the RFC, grounded in existing in-app conventions. Final design sign-off + the #3774 maintainer-review gate are the maintainer's call — flagging item 2 as the one new technical decision to fold into that. Good call keeping this open for review rather than pushing for merge while gated.
|
Thanks for the careful reviews, @aethersdr-agent and @ten9876 — responses to the three items: 1 — Label-rendering rewrite (disclosure + regression). Good catch — adding to the as-built list now. The 3 commits ( 2 — Compression bar source. Deliberate. 3 — Power scale edge (minor). Intentional — |
There was a problem hiding this comment.
Thanks @dsocha — this is a careful, well-documented PR. The "as-built vs RFC" writeup and the inline comments make the design intent easy to follow, and the as-built honesty about the visual/UX divergences is exactly right given the maintainer-review gate. Nothing here blocks; a few notes below, plus one question for the maintainer's design sign-off. CI is green across all six checks (build, macOS, Windows, CodeQL, accessibility).
Convention / correctness — verified, looks good:
- Principle V honored as claimed. Both
txMeterand the newshowTxMeterTypepersist as token-serialized nested JSON under the singleDisplaySettingskey, with unknown tokens falling back toNone/False. No new flat-keyAppSettingscalls. Appended enum ordinals keep old configs/downgrades safe. - Signal wiring matches.
txMetersChanged(fwdPower, swr)→setTxPower(fwd)/setTxSwr(swr)(correct order), andmicMetersChanged(micLevel, compLevel, micPeak, compPeak)→ the lambda grabsmicLevelandcompPeak. Receiver context isvfo, so the connections tear down with the widget. - Null safety.
m_txModel/m_radioModelare both guarded intxPowerFullScaleW()and the compression gate. - Power-scale parity.
txPowerFullScaleW()'smaxPowerLevel()+AU-/<=100 → 500bump mirrorsMainWindow_Wiring::updatePowerScale, andkPowerHeadroom = 1.2reproduces TxApplet's barefoot 100→120 (red@100) and Aurora 500→600 (red@500). One minor divergence worth noting (not a fix request): TxApplet keys offmaxWatts > 100to pick the 600 W arc, whereas this scalesrated × 1.2continuously — so a rig reporting, say, 200 W rated lands on a 240 W scale here vs TxApplet's 600 W. The continuous version is arguably more correct; just flagging that the two power displays won't be pixel-identical for non-standard rated values. - Cache invalidation. Adding
m_input.min/maxto the rebuild key correctly handles a Power radio-swap re-raster, and the Signal branch uses constant min/max (−127/−13) so no spurious rebuilds.m_activeCfgseeded in the ctor before the firstapplyBallistics— good catch on the ordering.
One question for design sign-off (Compression): the compression bar value is driven by compPeak (the 4th micMetersChanged arg), and compLevel (the 2nd arg) goes unused. By analogy with the mic meter — which uses micLevel for the bar and micPeak only for the peak marker — I'd have expected compLevel to drive the bar and compPeak the held marker. Here the bar tracks peak gain-reduction directly while the window envelope synthesizes the marker. That's a defensible choice for a GR meter (peak GR is the conventional read), but since it's one of the UX decisions you flagged for review, worth confirming it's intentional rather than an oversight — and the compLevel-unused detail is easy to miss.
Net: clean generalizations (the != Signal / hasPeak guards read well, not parallel code paths), solid persistence and null handling. The remaining open items are the hardware/sim behavioral pass you already called out and the maintainer's UX sign-off on the flagged divergences.
🤖 aethersdr-agent · cost: $3.0048 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Approved. Reviewed the full diff (incl. the shared drawMarkers() label-rendering rework) and the changes are confined to the VFO-flag meter subsystem — no applets touched, no keying/invoke() path, so Principle VI doesn't apply. Principle V honored (token-serialized nested JSON under DisplaySettings, appended ordinals, safe fallback). Maintainer design sign-off on the four RFC divergences: Compression (reversed −25→0 GR, compPeak-driven, speech-processor gated), Power (radio-aware exciter rated×1.2), SWR (nonlinear knee@2.0), and the "Show meter type" in-hole label — all approved as built. Tested locally on a FLEX-8600. Thanks @dsocha — clean, well-documented work.
## Release prep for v26.6.5 Docs + version-string updates for the **50 merges** since v26.6.4. No code change — safe to merge without a rebuild. ### Changes - **CHANGELOG.md** — new `[v26.6.5] — 2026-06-28` section, headlined by **KiwiSDR receive sync** (#3872), **SmartMTR TX meters** (#3776), the **PROF profile-switcher applet** (#3829), and a large **agent automation bridge** expansion (#3851/#3856/#3883/#3842/#3832/#3819); categorized Added/Changed/Fixed/Performance/Removed compiled from the full merge list. Folds in the prior `[Unreleased]` entries and resets `[Unreleased]` to empty. - **CMakeLists.txt / README.md / AGENTS.md** — `26.6.4` → `26.6.5`. - **ROADMAP.md** — current cycle → `post-v26.6.5`; four v26.6.5 highlights added atop *Recently shipped*. ### Not included - Tagging `v26.6.5` is a separate, explicit step (not done here). - The untracked `docs/aetherd-headless-engine-design.md` RFC and `prototypes/` are intentionally left out. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Closes #3774.
Implements the additional SmartMTR TX meters end-to-end — SWR, forward Power, and Compression — extending the TX-meter selector beyond Mic Level, following the existing mic-level meter through selector → persistence → render config → wiring.
Also includes a small follow-on: an optional "Show meter type" in-hole label (
MIC/SWR/PWR/COMP) toggled by a new SmartMTR menu checkbox. This label is an addition beyond the RFC — included here for review.As-built vs. the RFC (design points needing maintainer sign-off)
The RFC body was left unchanged; this PR is the source of truth for what was actually built. Notable divergences:
PhoneCwApplet): COMPPEAK source (negated), bar anchored at the hole's far edge growing toward −25, a reversed peak marker + inverted attack/release ballistics, and gated on the speech processor (parked at 0 otherwise, to avoid the pegged quiescent COMP meters some radios publish). The RFC proposed a plain 0–25 dB scale.kPowerHeadroom = 1.2, mirroringTxApplet::setPowerScale(exciter only — no 2 kW amplifier scale, sinceTxAppletshows exciter power). The peak marker uses the sliding-window envelope (slow decay, like the signal meter) rather than the instantaneous sample the RFC mentioned — so it falls at the same rate as the other meters' peaks.SmartMtrWidgetneeded: generalizing theMicLevel/Signalguards (→hasPeak/!= Signal), a reversed fill/peak/ballistics mode for compression, and a reversed mode inMeterExtremes. These are minimal generalizations, not parallel code paths.Constitution principle honored
showTxMeterTypeflag persist as nested JSON under the singleDisplaySettingskey (token-serialized; unknown tokens fall back toNone/False). No new flat-keyAppSettingscalls.Test plan
cmake --build build)None)Checklist
AppSettingscalls — nested JSON under one key (Principle V)🤖 Generated with Claude Code