Skip to content

feat(gui): add PEP peak-hold tick to TX FWDPWR gauge (#2561)#2596

Merged
ten9876 merged 3 commits into
mainfrom
aetherclaude/issue-2561
May 12, 2026
Merged

feat(gui): add PEP peak-hold tick to TX FWDPWR gauge (#2561)#2596
ten9876 merged 3 commits into
mainfrom
aetherclaude/issue-2561

Conversation

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Summary

Fixes #2561

What was changed

feat(gui): add PEP peak-hold tick to TX FWDPWR gauge (#2561)

Files modified

  • src/gui/MainWindow.cpp
  • src/gui/TxApplet.cpp
  • src/gui/TxApplet.h
  • src/models/MeterModel.cpp
  • src/models/MeterModel.h
 src/gui/TxApplet.cpp      | 56 +++++++++++++++++++++++++++++++++++++++++++++++
 src/gui/TxApplet.h        | 19 ++++++++++++++++
 src/models/MeterModel.cpp |  7 ++++++
 src/models/MeterModel.h   |  6 +++++
 5 files changed, 94 insertions(+)

Generated by AetherClaude (automated agent for AetherSDR)

SmartSDR shows both a fast-moving needle and a slow peak-hold bar on
the TX forward power gauge; AetherSDR's double-smoothed needle alone
undershoots SSB PEP by 1-2 dB.  Add a peak-hold tick that captures the
raw pre-smoothed FWDPWR sample, holds for 2 s, then decays linearly
toward the current smoothed reading at 50 W/s.

- MeterModel: add txPeakChanged(float) signal carrying the existing
  m_fwdPowerInstant; emitted only on FWDPWR samples so SWR-only ticks
  don't re-fire.
- TxApplet: new updatePeakPower / setTransmitting slots track the
  peak, run the hold + decay timer, and reset on un-key.  Mirrors the
  SMeterWidget RX peak-hold pattern.
- MainWindow: wire txPeakChanged and TransmitModel::moxChanged into
  TxApplet.

Existing txMetersChanged signature is unchanged, so TciServer and the
TX-mode S-meter path are untouched.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for tackling this — the implementation is small, focused, and the SMeterWidget peak-hold pattern is a good model to follow. The wiring (new txPeakChanged carrying the pre-smoothed sample, moxChangedsetTransmitting for un-key reset, HGauge::setPeakValue for rendering) all looks correct, and I verified the dependencies (HGauge::setPeakValue, TransmitModel::moxChanged, m_fwdPowerInstant) already exist on main.

A couple of things worth considering:

1. Decay rate doesn't scale with the gauge maximum (src/gui/TxApplet.cpp:73)

kDecayWattsPerSec = 50.0f was tuned for a 120 W barefoot scale ("empties in ~2 s"). On a 1500 W amp scale, a 1500 W peak will take ~30 s to bleed off after the 2 s hold — much longer than the operator-feedback "PEP tick" intent and noticeably out of step with the SMeterWidget ballistic (which is scale-invariant because it works in dB).

Suggestion: derive the decay from the gauge max so it always empties in ~2 s of decay regardless of scale, e.g.:

const float kDecayWattsPerSec = static_cast<HGauge*>(m_fwdGauge)->max() / 2.0f;

(or capture the value set in setPowerScale into a member). Even just keying off m_powerScale would do it.

2. Minor: post-MOX FWDPWR straggler can re-arm the tick

updatePeakPower doesn't check a "transmitting" flag, so any FWDPWR sample that arrives between moxChanged(false) and the radio's final 0 W tick can momentarily raise the tick back above zero after the reset. Probably a 1-frame artifact in practice — but a if (!m_transmitting) return; guard in updatePeakPower (with m_transmitting tracked in setTransmitting) would make the un-key reset clean.

Both are minor — the PR does what #2561 asks for, the comments are great, and the data flow is solid. Nice work.

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

Claude here, reviewing on Jeremy's behalf.

Verdict

Clean feature addition from AetherClaude. Correctly architected, uses existing infrastructure (HGauge::setPeakValue), matches SmartSDR's behavior. Recommend merge with one parameter question to think about.

Verified

  • Correct architectural split. MeterModel exposes the new txPeakChanged(float fwdPowerInstant) signal carrying the pre-smoothed raw watts — this is the key insight, since the existing double-smoothing (model EMA + HGauge MeterSmoother) is exactly what's attenuating SSB peaks by 1-2 dB. TxApplet decides display ballistics.
  • Mirrors existing patterns. Comment correctly references SMeterWidget.cpp's RX peak-hold as prior art. Uses HGauge::setPeakValue for the rendering — no reinvention.
  • Lifecycle correct. Timer starts on first peak, stops when decay completes or TX ends. setTransmitting(false) resets to zero on un-key so a held peak doesn't linger across overs.
  • Thread safety fine. MeterModel and TxApplet both live on the main thread (per CLAUDE.md's "RadioModel owns all sub-models on the main thread"); direct connection is correct.
  • Stale-code audit: new signal/members all consistent, no orphans. m_fwdPowerInstant already existed in MeterModel — this PR just adds the emission gate. Backwards compatible — existing txMetersChanged consumers see no change.
  • Built locally clean (only pre-existing unrelated warnings). All 5 CI checks green. Auto-merges cleanly against current main.

One concern worth thinking about — decay rate scaling

The decay rate is hardcoded at kDecayWattsPerSec = 50.0f. Comment says "empties a barefoot (120 W) gauge in roughly 2 s." That's right for the typical 100 W/120 W setup, but doesn't scale for the extremes that AetherSDR supports:

Setup Max watts 50 W/s decay behavior
QRP rig (5W max) 5 W Peak from 5 W → 0 in 0.1 s — tick effectively never visible
Standard 100 W 120 W ~2.4 s — matches comment, feels right
500 W FLEX-8600 500 W ~10 s — slightly long
PGXL amplifier 1500 W ~30 s — way too long; tick hangs around stale

Suggested fix: scale the decay rate to the current power-scale max. setPowerScale(int maxWatts, ...) is already called when scale changes, and could set m_peakDecayWattsPerSec = maxWatts / 2.5f (gives ~2.5s decay from full-scale for any rig).

Not blocking — the issue author specifies 100W/120W scenarios and the current behavior matches SmartSDR there. But operators running PGXL will see a peak tick that hangs around longer than they expect. Worth either:

  • Adding the scaling now (one-line change in setPowerScale), or
  • Filing as a follow-up issue specifically for amplifier setups

Smaller observations (non-blocking)

  • if (fwdPowerInstant > m_peakPower) — correct peak-hold semantics. New samples only update the peak if they exceed the current (possibly decaying) value. Matches SmartSDR.
  • First-sample-of-TX as peak — if the very first sample after key-down is a glitch-high value, that gets held for 2 s. Matches SmartSDR's behavior per the issue description, so probably fine.
  • m_smoothedPower driver of decay terminus — when the decay reaches m_smoothedPower, the timer stops. Good — peak doesn't decay below the current display value, so the user always sees the tick at-or-above the bar.
  • No unit tests — defensible for visual UI features but a small unit test of the decay math (mock timer, feed samples, assert peak values at known time points) would be cheap. Not blocking.

Recommendation

Merge after Jeremy spot-tests on the FLEX-8600 with both:

  1. Barefoot SSB — speech peaks should show the tick rising rapidly, holding ~2 s, decaying back to the smoothed bar.
  2. High-power if available — verify decay feel at 500 W. If the 10-second decay seems too long, the scaling fix above is the right follow-up.

Thanks @aethersdr-agent — the architectural call to put the ballistics in TxApplet rather than MeterModel is correct (model exposes raw data, view decides presentation), and the pattern matches the existing SMeterWidget peak-hold. Clean implementation.

73, Jeremy KK7GWY & Claude (AI dev partner)

The constexpr 50 W/s decay rate was tuned for the barefoot 120 W
gauge (~2.4 s peak-to-zero decay), but the same constant gives the
wrong visual feel on the Aurora 500 W exciter gauge (0-600 W full
scale), where 50 W/s stretches the decay to ~12 s and leaves the
tick lingering long after the operator's voice peak has passed.

Promotes the rate to a member `m_peakDecayWattsPerSec` and recomputes
it in setPowerScale() against the gauge full-scale.  Target is ~2.5 s
from peak to floor regardless of rig class:

  Barefoot:        120 W /  2.5 s →  48 W/s  (matches prior behavior)
  Aurora exciter:  600 W /  2.5 s → 240 W/s  (was 50 W/s — 5x too slow)

Default member value (48 W/s) matches barefoot for the pre-connect
case before setPowerScale() runs.

Co-Authored-By: aethersdr-agent <aethersdr-agent@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Pushed `1193e8a7` with the decay-rate scaling fix discussed in the review.

What changed

`kDecayWattsPerSec = 50.0f` constexpr → `m_peakDecayWattsPerSec` member, recomputed in `setPowerScale()` against the gauge full-scale to give a consistent ~2.5 s peak-to-zero decay regardless of rig class:

Rig Gauge full-scale Old rate New rate Old decay New decay
Barefoot 120 W 50 W/s 48 W/s ~2.4 s ~2.5 s
Aurora 500 W exciter 600 W 50 W/s 240 W/s ~12 s ~2.5 s

The Aurora case was the visible bug — peak tick lingered ~10 seconds past the operator's actual peak. Now both rig classes feel the same.

Default member value (48 W/s) matches the barefoot path so the pre-connect state — before any `setPowerScale()` call lands — has sensible behavior.

Co-Authored-By tag preserves the original AetherClaude attribution.

Build clean locally. CI will re-run on the push.

73, Jeremy KK7GWY & Claude (AI dev partner)

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

Claude here, follow-up review after the decay-scaling push.

Status

Pushed merge commit `7264afda` syncing in main (which now includes #2591 Profile Manager non-modal and `docs/dialog-patterns.md`). Clean auto-merge.

Final state of the PR

Two commits:

  1. `ddcefbf1` — AetherClaude's original PEP peak-hold tick implementation
  2. `1193e8a7` — my follow-up: scale the decay rate to gauge full-scale so the ~2.5 s visual feel stays consistent across barefoot (120 W) and Aurora exciter (600 W)

Plus the merge commit.

Post-merge audit — all main work preserved

PR Marker Count
#2572 TxMicChannelNormalizer `TxMicChannelNormalizer` in AudioEngine.cpp 13
#2583 AudioDeviceChangeDialog `AudioDeviceChangeDialog`/`m_audioDeviceMonitor` 5
#2586 interlock notifications `interlock`/`Interlock` in SpectrumWidget.cpp 34
#2591 ProfileManagerDialog non-modal `m_profileManagerDialog` in MainWindow.cpp 7
This PR — PEP peak-hold `updatePeakPower`/`m_peakDecayWattsPerSec` in TxApplet 10 across .h+.cpp
This PR — MeterModel signal `txPeakChanged` 2
`docs/dialog-patterns.md` (from #2591) present 10 KB

Built locally clean (only pre-existing unrelated warnings). All 5 CI checks green on the pre-merge HEAD.

Architectural review unchanged

The feature is well-architected and the decay-scaling fix from my earlier push closes the only substantive concern from the original review:

  • Model exposes raw data: `MeterModel::txPeakChanged(float fwdPowerInstant)` carries the pre-smoothed FWDPWR sample, not the double-smoothed value
  • View owns presentation: `TxApplet` decides display ballistics (2 s hold + decay to floor), separated by concern
  • Reuses existing infrastructure: `HGauge::setPeakValue` already renders the static tick; nothing reinvented
  • Mirrors `SMeterWidget` RX peak-hold pattern: comment correctly references the prior art
  • Lifecycle clean: `setTransmitting(false)` resets the peak to zero on un-key so a held PEP doesn't linger across overs
  • Decay scales with gauge: `m_peakDecayWattsPerSec = gaugeFullScaleW / 2.5f` ensures consistent ~2.5 s decay visual feel across barefoot (48 W/s) and Aurora exciter (240 W/s)
  • Threading correct: `MeterModel` and `TxApplet` both on the main thread; direct connection is right

Recommendation

Merge after Jeremy spot-tests on the FLEX-8600 with both:

  1. Barefoot SSB — speech peaks should show the tick rising rapidly, holding ~2 s, decaying back to the smoothed bar over ~2.5 s
  2. Aurora 500 W exciter (if available) — verify the new decay scaling feels right; peak shouldn't linger 10+ seconds

The decay-rate fix specifically addresses the Aurora case; the barefoot case behavior should be effectively identical to AetherClaude's original `50 W/s` (new value is `48 W/s` = barefoot-equivalent within rounding).

Thanks @aethersdr-agent for the original implementation — model→view split was the right call. 73, Jeremy KK7GWY & Claude (AI dev partner)

@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 — PEP peak-hold with gauge-scaled decay.

@ten9876 ten9876 merged commit a012dfc into main May 12, 2026
4 checks passed
@ten9876 ten9876 deleted the aetherclaude/issue-2561 branch May 12, 2026 07:01
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.

Add PEP peak hold indicator to TX power gauge (SmartSDR parity)

1 participant