Skip to content

[hlth] Fix false HLTH SWR warnings during transmit transitions#4268

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:fix/4243-health-swr-transient
Jul 19, 2026
Merged

[hlth] Fix false HLTH SWR warnings during transmit transitions#4268
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:fix/4243-health-swr-transient

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Summary

  • qualify HLTH SWR scoring with fresh instantaneous forward power instead of the slowly decaying display-power value
  • require a full five-update SWR admission window and use its lower quartile, rejecting isolated reflected-power-floor spikes while preserving sustained mismatch warnings
  • add a deterministic health_applet_test covering key-up, unkey, intermittent low-power outliers, healthy load, and sustained high SWR

Root cause

HLTH combined MeterModel's smoothed forward-power value with unsmoothed SWR reports, then retained each report in its 3.5-second variance history. Near key/unkey—and especially at low forward power—the radio can emit isolated upward SWR samples even though the current main meter is normal. Those samples poisoned HLTH's baseline and variance score, producing a stale WATCH or GROUND? state.

The bridge trace on the ANT2 dummy load at a 2 W setpoint observed instantaneous SWR reports as high as 1.79 among predominantly 1.00 readings. The fix admits only distinct, power-qualified reports and requires four of the latest five reports to be elevated before scoring the rise.

Verification

  • cmake --build build --target health_applet_test AetherSDR -j8
  • ctest --test-dir build -R 'health_applet_test|meter_model_test' --output-on-failure — 2/2 passed
  • python3 tools/check_a11y.py — no new findings (existing baseline warnings only)
  • python3 tools/check_engine_boundary.py --strict — 0 blockers
  • git diff --check

Agent automation bridge proof on the rebuilt app:

  • live TX antenna readback: ANT2
  • hard automation power ceiling: 2 W; Tune power and RF power both read back as 2 W
  • before: transmitter off, HLTH IDLE, SWR 1.00
  • keyed steady state: 1.34 W instantaneous forward power, live SWR 1.27, HLTH OK
  • after unkey: transmitter off, HLTH returned to IDLE
  • restored afterward: ANT1, Tune power 10 W, RF power 40 W, ATU memory recall successful, HLTH hidden

HLTH remains healthy during the ANT2 2 W dummy-load proof

Fixes #4243

👨🏼‍💻 Generated with OpenAI Codex (GPT-5.6 Sol) and tested by @jensenpat

@jensenpat jensenpat self-assigned this Jul 16, 2026
@jensenpat jensenpat changed the title Fix false HLTH SWR warnings during transmit transitions [hlth] Fix false HLTH SWR warnings during transmit transitions Jul 16, 2026
@jensenpat
jensenpat marked this pull request as ready for review July 16, 2026 16:11
@jensenpat
jensenpat requested review from a team as code owners July 16, 2026 16:11

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

Nicely scoped fix with a clear root-cause writeup. I traced the data flow and the design holds: directionalPowerMetersChanged carries m_fwdPowerInstant (raw instantaneous forward power) and is emitted right after txMetersChanged from the same MeterModel, so updateRadioDirectionalMeters reliably overwrites swrQualifyingPowerWatts with the instantaneous value. That makes the unkey case behave — m_fwdPower is still decaying ≥0.75 W while m_fwdPowerInstant≈0, so the SWR sample fails kActivePowerWatts qualification and never poisons the baseline. The admittedSwr lower-quartile (index (n-1)/4 = 1 at n=5) does require 4 of 5 elevated samples as claimed, and severity is correctly forced to 0 until swrTrusted. No null-deref, leak, QSettings, or RAII concerns, and all 7 checks are green. The health_applet_test covering key-up / unkey / outliers / sustained-fault is a welcome addition.

Polish (non-blocking)

  • The new tests wait on wall-clock elapsed time and the 50 ms GUI tick timer — inherently timing-sensitive under CI load. It passed here, so this is just a heads-up on the pattern, not a request to change it.

Non-blocking notes

  • admittedSwr's comment hardcodes "four of five" / "250 ms"; those numbers are only exact at the current kSwrAdmissionWindow/kSwrSettleFrames/kTickMs. Consider phrasing it in terms of the constants so it doesn't drift if they're tuned later.

Thanks for the thorough live-bridge proof and the deterministic test — looks good to me.


🤖 aethersdr-agent · cost: $7.0732 · model: claude-opus-4-8

Comment thread tests/health_applet_test.cpp
@jensenpat jensenpat added help wanted Extra attention needed priority: low Low priority labels Jul 17, 2026
NF0T
NF0T previously requested changes Jul 17, 2026

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

Good root-cause diagnosis and a well-designed core idea — decoupling instantaneous forward power (for SWR gating) from the smoothed display power is the right direction, and the four new tests demonstrate the false-positive fix works for the reported case. CI is green and the commit is signed.

Two issues that make this riskier than the bug it fixes:

1. The admission-window reset can permanently trap HLTH in "never trusted," masking a real sustained fault. m_swrAdmissionWindow clears on any single inactive tick or source switch — unlike every other piece of state in this class, which tolerates up to 24 frames (~1.2s) before resetting. bestSnapshot() recomputes the winning meter source every tick from two freshness thresholds (900ms/1700ms) with no hysteresis; if an amplifier or tuner's meter cadence straddles that boundary, bestSnapshot() can oscillate between sources, clearing the 5-sample window each time. Once trapped in that oscillation, swrTrusted never becomes true and sample.severity stays forced to 0.0f — even during a genuine antenna fault. That's the opposite of what #4243 needs. None of the four new tests exercise more than one meter source, so this wouldn't be caught. Would suggest either not clearing the window on a bare source switch (only on genuine inactivity), or giving it the same dropout tolerance the rest of the class already has.

2. The fix's correctness depends on an undocumented, and not-always-true, cross-signal assumption. directionalPowerMetersChanged and txMetersChanged both emit from MeterModel::updateValues(), but directionalChanged is gated by three branches (FWDPWR/REFPWR/SWR) while txChanged is gated by two (FWDPWR/SWR) — so a REFPWR-only packet fires directionalPowerMetersChanged alone, with no txMetersChanged in the same cycle, on any radio that reports REFPWR. It's harmless today only because the timestamps happen to reflect staleness correctly in that case, but nothing in HealthApplet.cpp comments this dependency — a future MeterModel.cpp refactor (splitting FWDPWR/SWR into independently-gated signals, a plausible cleanup) could silently reintroduce the exact bug this PR fixes, with no compile error and no guaranteed test failure. Worth at least a comment at the connection site explaining why emission order matters here.

Also worth addressing, lower priority:

  • The swrTrusted gate (4-frame settle + 5-sample quorum, ~200-250ms+) applies uniformly to all three meter sources, but the root cause — smoothed power gating unsmoothed SWR — only exists for Radio (MeterModel.cpp confirms Tuner/Amplifier forward power is never smoothed). This adds an unnecessary detection delay for a genuine Tuner/Amplifier fault that previously flagged immediately. Untested by the new suite (Radio-source meters only).
  • pushRecent()/baseline-averaging is now gated behind swrTrusted, not just active — health telemetry freezes for the first ~250ms+ of every TX over, so a fast real fault confined to that window is invisible once per over, systematically.

Non-blocking:

  • admittedSwr() re-implements a sort-based order-statistic pattern that exists as nth_element-based helpers in 4+ other files (AdaptiveFilterEngine.cpp even documents a prior review making this exact tradeoff) — worth reusing one of those instead.
  • cacheMeters()'s writes to the new "qualifying" fields are dead code for Radio (immediately overwritten) and redundant for Tuner/Amp (duplicate the already-instantaneous value) — a comment explaining the asymmetry would help the next reader.

Happy to re-review once the admission-window fragility and the cross-signal dependency are addressed — the core approach is sound, it just needs to fail safer.

@NF0T NF0T self-assigned this Jul 17, 2026
…le (aethersdr#4268)

Addresses @NF0T's review. The core decoupling of instantaneous forward power
from smoothed display power is kept; these changes make it fail SAFE and cover
the source/mode cases the original missed.

- Admission-window fragility (NF0T #1, the blocker): the window cleared on any
  single inactive tick or bestSnapshot() source flip. bestSnapshot() has no
  hysteresis at its 900ms/1700ms freshness boundary, so a second meter whose
  cadence straddles it oscillates the source and could trap HLTH in "never
  trusted" — masking a real fault. Now the window tolerates kSwrDropoutFrames
  (~1.2s, matching the class's idle-baseline tolerance) of inactivity or source
  mismatch before resetting, admits only samples from the tracked source, and
  still acquires the first source immediately. Net direction is fail-safe: the
  window only ever persists longer, never shorter.

- Radio-only settle (NF0T #3): only Radio forward power is smoothed, so only
  Radio SWR carries the key/unkey transient. Tuner/Amplifier now trust their raw
  SWR immediately instead of eating the ~250ms settle delay before a genuine
  amp/tuner fault can flag.

- Cross-signal dependency (NF0T #2): documented at the connect site why
  directionalPowerMetersChanged and txMetersChanged must stay co-emitted.

- Telemetry gate (NF0T aethersdr#4): documented the intentional Radio settle blind spot
  (Tuner/Amp have none now).

- admittedSwr() uses std::nth_element (O(n)) instead of a full sort; cacheMeters
  qualifying-field asymmetry documented; unkey test margin widened for CI
  headroom.

health_applet_test 4/4 across 10 runs; full app builds clean. (During
development an initial version of the tolerance delayed first-source acquisition
by 24 frames and MASKED the sustained-fault test — caught and fixed; the suite
has teeth.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

Maintainer approval (Tier 1) — @NF0T's issues fixed, fail-safe

Full high-effort pass. I independently confirmed NF0T's #1 is a real fail-unsafe trap: bestSnapshot() has no hysteresis at its 900ms/1700ms boundary (verified in code), so a second meter cadence straddling it oscillates the source and the old m_swrAdmissionWindow.clear() on every flip could pin swrTrusted false and force severity to 0 during a genuine fault. Pushed 7d4bdbf5 to resolve all of it:

  • #1 (admission-window fragility): the window now tolerates kSwrDropoutFrames (~1.2s, matching the class's idle-baseline tolerance) of inactivity or source mismatch before resetting, admits only samples from the tracked source, and still acquires the first source immediately. The change is fail-safe by construction — it can only make the window persist longer (more trust → more detection), never shorter.
  • #3 (Tuner/Amp settle delay): only Radio forward power is smoothed, so Tuner/Amplifier now trust raw SWR immediately — no ~250ms delay before a real amp/tuner fault flags.
  • #2 / #4: documented the directionalPowerMetersChanged/txMetersChanged co-emission dependency at the connect site, and the intentional Radio settle blind spot.
  • Non-blocking: admittedSwr()std::nth_element (O(n)); cacheMeters asymmetry documented; unkey-test margin widened for CI headroom.

Verification: health_applet_test 4/4 across 10 runs, full app builds clean. Notably, my first cut of the tolerance delayed first-source acquisition by 24 frames and masked the sustained-fault test — I caught and fixed it, so the suite genuinely has teeth against the fail-unsafe direction.

Honest coverage note: I did not add a dedicated multi-source oscillation test — reproducing the 900ms-straddle deterministically in the current wall-clock harness would be flaky (the exact concern behind NF0T's test note), and a flaky test is worse than none. The fix is instead validated by the fail-safe reasoning above + the regression I caught. A clock-injection test seam is a reasonable follow-up.

Approving. Thanks for the sharp fail-unsafe catch, @NF0T, and the solid root-cause work, @jensenpat.

@ten9876
ten9876 enabled auto-merge (squash) July 19, 2026 00:33
@ten9876
ten9876 dismissed NF0T’s stale review July 19, 2026 00:37

Requested changes have been made

@ten9876
ten9876 merged commit 4820327 into aethersdr:main Jul 19, 2026
7 checks passed
jensenpat added a commit that referenced this pull request Jul 19, 2026
## Summary

- bump the CMake and README versions from 26.7.2 to 26.7.3
- add the v26.7.3 CHANGELOG entry for 25 commits since v26.7.2
- credit 7 contributors plus Dependabot, including two squash commits
and one direct docs asset commit recovered during the attribution audit

## Release facts

- content cutoff: `48203274` — PR #4268
- range: `v26.7.2..4820327` (25 commits)
- signed prep commit: `9634c35f`
- release theme: cross-needle metering, radio-state fidelity, and
operating polish

The v26.7.3 tag and published release will point directly at the signed
prep commit so tag-triggered platform builds carry the correct embedded
version while this PR awaits the required code-owner review.

## Verification

- [x] fetched upstream tags and confirmed the cutoff still matches
`upstream/main`
- [x] confirmed no existing v26.7.3 tag or GitHub release
- [x] verified exactly three tracked files changed
- [x] `git diff --check`
- [x] CMake and README both report 26.7.3
- [x] commit signature reports `Good "git" signature`
- [x] cross-checked contributor counts against `git shortlog`
- [x] audited missed PRs #4283 and #4284 plus the direct README
screenshot commit
- [ ] tag-triggered Linux, macOS, Windows, signing, and Stream Deck
workflows (start after tag push)

Screenshots: not applicable — this PR changes release metadata only;
screenshots for the shipped features live on their originating PRs.

👨🏼‍💻 Generated with OpenAI Codex (GPT-5.6 Sol) and tested by @jensenpat

Co-authored-by: Codex <noreply@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

help wanted Extra attention needed priority: low Low priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

health meter swr reporting high swr into dummy load upon keying and unkeying while the main meter reports normal swr.

3 participants