Skip to content

feat(smartmtr): add SWR, Power, and Compression TX meters (#3774)#3776

Merged
ten9876 merged 5 commits into
aethersdr:mainfrom
dsocha:feat/smartmtr-tx-meters
Jun 27, 2026
Merged

feat(smartmtr): add SWR, Power, and Compression TX meters (#3774)#3776
ten9876 merged 5 commits into
aethersdr:mainfrom
dsocha:feat/smartmtr-tx-meters

Conversation

@dsocha

@dsocha dsocha commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

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.

⚠️ Opened for review while #3774 still carries maintainer-review ("gated on approval before any PR"). Not requesting merge until the design is signed off. The visual/UX decisions below are flagged for the maintainer per the Autonomous-Agent-Boundaries rule in AGENTS.md.

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:

  • Compression — implemented as a reversed −25..0 gain-reduction meter mirroring the Phone/CW gauge (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.
  • Power — radio-aware exciter scale (barefoot 0–120 W / Aurora 0–600 W, red at rated) via a shared kPowerHeadroom = 1.2, mirroring TxApplet::setPowerScale (exciter only — no 2 kW amplifier scale, since TxApplet shows 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.
  • SWR — 1.0–3.0, nonlinear (knee at 2.0 pinned at ~70% of the scale so the operating-critical 1.0–2.0 band gets most of the room), red zone ≥ 2.0.
  • "render-only — no widget changes" was inaccurate. SmartMtrWidget needed: generalizing the MicLevel/Signal guards (→ hasPeak / != Signal), a reversed fill/peak/ballistics mode for compression, and a reversed mode in MeterExtremes. These are minimal generalizations, not parallel code paths.
  • Shipped as one combined PR (the RFC offered SWR-first sequencing or a combined PR).

Constitution principle honored

  • Principle V — the TX-meter selection and the new showTxMeterType flag persist as nested JSON under the single DisplaySettings key (token-serialized; unknown tokens fall back to None/False). No new flat-key AppSettings calls.
  • Principle IV — clean-room; not derived from any proprietary binary.

Test plan

  • Local build passes (cmake --build build)
  • Behavior verified on a real radio — not yet done. Visuals were iterated live in dev, but the TX behavior (esp. compression with the speech processor on/off, and the radio-aware power scale on an Aurora/AU- rig) wants a hardware/sim pass.
  • Existing tests pass (CI)
  • Backward-compatible settings (appended enum ordinals + token serialization; old configs and downgrades fall back to None)

Checklist

  • Commits are signed (SSH)
  • No new flat-key AppSettings calls — nested JSON under one key (Principle V)
  • Code is clean-room (Principle IV)

🤖 Generated with Claude Code

dsocha and others added 2 commits June 23, 2026 18:50
…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>
dsocha and others added 3 commits June 24, 2026 14:40
…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>

@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 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) matches TxApplet::setPowerScale exactly, and the AU- bump mirrors the maxW <= 100 rule in MainWindow_Wiring.
  • Null guards on m_txModel / m_radioModel are present; cache invalidation correctly gains m_cacheMin/Max so 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:

  1. Undisclosed scope: the scale-label rendering rewrite. Three of the five commits (ff8e69f, 811a211, ce45808) replace the marker-label positioning (AlignVCenter box → declared capHeight baseline) and add a Windows QFont::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.

  2. Compression bar source. The bar value uses compPeak (negated), while the mic meter uses the instantaneous micLevel for the bar and reserves micPeak for the marker. compLevel (the 2nd micMetersChanged arg) is the instantaneous analog. Was compPeak-for-the-bar deliberate? Using compLevel for the bar + compPeak as an external peak marker would mirror the mic meter exactly; as written, the bar and the window-derived peak both track the peak.

  3. Power scale for non-AU rigs >100 W (minor). TxApplet treats any maxWatts > 100 as the 600/500 Aurora scale, whereas txPowerFullScaleW() returns ratedW × 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 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.

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; txMetersChangedsetTxPower(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.

@dsocha

dsocha commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

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 (ff8e69f, 811a211, ce45808) re-anchor all drawMarkers() labels on declared capHeight() for backend-consistent centering, plus a Windows-only weight bump. I made these changes specifically after testing the meter on all three platforms — not just macOS, but Linux and Windows too — and verified the Signal + Mic tick labels render correctly on each.

2 — Compression bar source. Deliberate. compPeak (negated) gives a steadier gain-reduction needle — instantaneous compLevel was too jittery to read as a GR meter. Peak-for-bar is the intended behavior here, which is why it differs from the mic path.

3 — Power scale edge (minor). Intentional — ratedW × 1.2 is the general rule; the >100 W ⇒ 600/500 shortcut in TxApplet only happens to coincide for AU rigs. No non-AU rig reports >100 W today, so no divergence in practice. Will soften the "mirrors TxApplet" wording.

@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 @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 txMeter and the new showTxMeterType persist as token-serialized nested JSON under the single DisplaySettings key, with unknown tokens falling back to None/False. No new flat-key AppSettings calls. Appended enum ordinals keep old configs/downgrades safe.
  • Signal wiring matches. txMetersChanged(fwdPower, swr)setTxPower(fwd)/setTxSwr(swr) (correct order), and micMetersChanged(micLevel, compLevel, micPeak, compPeak) → the lambda grabs micLevel and compPeak. Receiver context is vfo, so the connections tear down with the widget.
  • Null safety. m_txModel/m_radioModel are both guarded in txPowerFullScaleW() and the compression gate.
  • Power-scale parity. txPowerFullScaleW()'s maxPowerLevel() + AU-/<=100 → 500 bump mirrors MainWindow_Wiring::updatePowerScale, and kPowerHeadroom = 1.2 reproduces TxApplet's barefoot 100→120 (red@100) and Aurora 500→600 (red@500). One minor divergence worth noting (not a fix request): TxApplet keys off maxWatts > 100 to pick the 600 W arc, whereas this scales rated × 1.2 continuously — 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/max to 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_activeCfg seeded in the ctor before the first applyBallistics — 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 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.

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.

@ten9876 ten9876 merged commit bb5e575 into aethersdr:main Jun 27, 2026
6 checks passed
@dsocha dsocha deleted the feat/smartmtr-tx-meters branch June 27, 2026 06:24
ten9876 added a commit that referenced this pull request Jun 28, 2026
## 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>
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] Additional TX meters for SmartMTR (SWR, Power, Compression)

2 participants