Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions docs/HERMES.md
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,7 @@ Effort is rough: **XS** under an hour, **S** a session, **M** a few sessions,
| 4 | Pipeline reset `0x39[7:4]=0x8` after an NCO move | A2 §B2 | Decimation state smears a transient across band-scale jumps — which `a1cbe154` made routine | XS |
| 5 | Normalize by `2^23-1`, not `2^23` | A1 §A2 | dBFS parity with piHPSDR. Numerically trivial, but parity is the point | XS |
| 6 | `RXASetNC` / `RXASetMP` after `OpenChannel` | A3 §7 | Selectivity vs latency; matters to CW operators. We silently take defaults | XS |
| 6a | Rate-limit the ADC-overload warning | §15.7 | Edge-gated, but the value chatters: **~133 warnings/second** on MW, which flushes the log ring and hides everything else | XS |
| ~~6a~~ | ~~Rate-limit the ADC-overload warning~~ **DONE** | §15.7 | The edge gate stays and a 10 s rate limit sits behind it, carrying the count of transitions the window swallowed. Note the severity here was already overstated when this row was written — see §15.7 | — |

### Tier 2 — correctness gaps

Expand Down Expand Up @@ -1751,11 +1751,26 @@ value the assertion depends on, even when it looks like a constant.

### 15.7 Noticed, not fixed

- **ADC overload chatter.** On the MW broadcast band with the default +20 dB LNA
the overload flag dithers, and the warning in `publishTelemetry` — although
edge-gated — fires **~133 times/second**, flushing the log ring. The gate is on
the value changing, but the value genuinely chatters. It also buries every
other log line, which is how it obstructed the diagnosis in §15.5.
- **ADC overload chatter — fixed, and the figure below was already stale.** On
the MW broadcast band with the default +20 dB LNA the overload flag dithers,
and the warning in `publishTelemetry` — although edge-gated — was measured at
**~133 times/second**, flushing the log ring and burying every other line,
which is how it obstructed the diagnosis in §15.5. The gate is on the value
changing; the value genuinely chatters.

**That rate has not been reachable since #4449.** `MetisClient` coalesces
`telemetryUpdated` to 10 Hz (`kTelemetryMinIntervalMs`), with no
change-bypass, so `publishTelemetry` cannot run faster than 10 Hz however hard
the comparator chatters — which capped this at ~10/s and ended the
ring-flushing without anyone recording that it had. **Kept rather than
rewritten**, because a symptom that stops being reproducible for a reason
nobody wrote down is worth more as a corrected entry than as a deleted one.

The remainder — one message repeating up to ten times a second for as long as
the band stays strong — is fixed: the edge gate stays and a 10 s rate limit
sits behind it, reporting the count of transitions the window swallowed.
That count is transitions *seen*, at the 10 Hz telemetry cadence, not
comparator edges, which are sampled far below their true rate and always were.
- **The HL2 LNA gain is only settable at connect time** (`lnaGainDb` param).
There is no seam verb for RF gain, so an operator on a strong band cannot back
it off without reconnecting. This is why the overload above could not simply be
Expand Down
48 changes: 48 additions & 0 deletions src/core/backends/hl2/Hl2Backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4869,7 +4869,55 @@ void Hl2Backend::publishTelemetry(const Hl2Telemetry& t)
if (t.adcOverload && *t.adcOverload != m_adcOverload) {
m_adcOverload = *t.adcOverload;
if (m_adcOverload)
++m_adcOverloadEdges;

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.

Nit (non-blocking): this counts only false-to-true overload assertions, not both transitions. Consider m_adcOverloadRises / m_adcOverloadAssertions and matching prose so the eventual N times summary has one precise meaning.

}
// Rate-limited, not merely edge-gated. The edge gate above is necessary and
// was never sufficient: the comparator genuinely chatters on a strong band,
// so nearly every telemetry sample is an edge and one message repeats at the
// full telemetry cadence (see the members' comment in the header for the
// rate, and for why the historical figure there is not repeated as a
// current one).
//
// Deliberately OUTSIDE the edge test, and this is the whole reason the two
// are separate: a burst that stops must still report its tally. Flushing
// only on the next edge would hold the count until the band goes loud
// again, which could be hours away or never. publishTelemetry runs on every
// telemetry update, so the window closes on time whether or not the
// condition is still happening.
//
// Reported rather than dropped because the rate IS the severity here — a
// flag that sets once is a hint, one that sets on every sample for a minute
// is a front end being slammed.
if (m_adcOverloadEdges > 0

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.

Blocker: this introduces a deterministic timing/state policy without regression coverage. It does not require a fake Metis peer: extract the (overload sample, elapsed time) -> warning decision/count state machine into a pure socket-free helper (the existing Hl2TxLevelPolicy.h is a close precedent) and register a CTest in tests/tests.cmake. Cover first assertion, repeated chatter inside 10 s, tally flush on a later unchanged telemetry sample, and an assertion after a quiet interval; mutation-check the rate gate and the placement of the flush outside the edge test.

&& (!m_adcOverloadClock.isValid()
|| m_adcOverloadClock.hasExpired(kAdcOverloadWarnIntervalMs))) {
// Why reading elapsed() here is safe: more than one transition implies
// the clock is valid. This flush is unconditional on every telemetry
// update and the counter rises by at most one per update, so the first
// transition after an invalid clock always flushes in the same call and
// resets the count. The count can only exceed one against a running
// window.
//
// What the single-transition branch does NOT mean. It is not "this is
// the first overload ever" — it is "exactly one transition was seen in
// this window". That lone transition may have arrived at any point since
// the window opened, so a bare message can lag the event by up to
// kAdcOverloadWarnIntervalMs. Accepted deliberately: it is the cost of
// the rate limit, one transition is a hint rather than an emergency, and
// an isolated overload after a quiet period still reports immediately
// because the clock is long expired by then.
if (m_adcOverloadEdges > 1)
// noquote + one composed string: streaming "(" as its own item makes
// QDebug insert a space after it and print "( 51 times in 10000 ms)".
qWarning().noquote()
<< "Hl2Backend: ADC OVERLOAD — reduce LNA gain or attenuate"
<< QStringLiteral("(%1 times in %2 ms)")
.arg(m_adcOverloadEdges)
.arg(m_adcOverloadClock.elapsed());
else
qWarning() << "Hl2Backend: ADC OVERLOAD — reduce LNA gain or attenuate";
m_adcOverloadClock.restart();

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.

Blocker: m_adcOverloadClock is default-constructed invalid, and the first overload reaches this line through !m_adcOverloadClock.isValid(). Qt explicitly documents restart() on an invalid QElapsedTimer as undefined behavior: https://doc.qt.io/qt-6/qelapsedtimer.html#restart. That makes the call intended to arm the limiter platform-dependent. Since the return value is unused, start() is the safe reset for both invalid and valid clocks.

Suggested change
m_adcOverloadClock.restart();
m_adcOverloadClock.start();

m_adcOverloadEdges = 0;
}
}

Expand Down
21 changes: 21 additions & 0 deletions src/core/backends/hl2/Hl2Backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,27 @@ class Hl2Backend : public IRadioBackend {
quint64 m_linkRxPacketsAtLastTick = 0;
static constexpr int kLinkStatsIntervalMs = 1000;
bool m_adcOverload = false;
// The overload bit is a per-frame sample of a level comparator, not an
// event: on a strong band it dithers, so the edge gate in publishTelemetry
// sees an edge nearly every time it looks. docs/HERMES.md 15.7 recorded

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.

This comment is careful to mark the ~133/s figure as stale — but docs/HERMES.md, the source it cites, still asserts it in the present tense and still lists the fix as outstanding:

  • :930 — backlog row | 6a | Rate-limit the ADC-overload warning | §15.7 | …**~133 warnings/second**… | XS |, unstruck
  • :1755-1758 — §15.7 "Noticed, not fixed" → "the warning in publishTelemetry — although edge-gated — fires ~133 times/second, flushing the log ring"

The table's convention for a landed item is right above it at :942: ~~12a~~ … **DONE**. Suggest striking 6a and moving/annotating the §15.7 bullet in this PR — non-blocking, but leaving the doc asserting the symptom this PR removes is the kind of drift the header comment is otherwise guarding against.

// ~133 warnings/second on the MW broadcast band, flushing the log ring.
//
// That figure is stale and deliberately not repeated as a present-tense
// claim: MetisClient has since coalesced telemetryUpdated to 10 Hz (#4449),
// which caps this at ~10/s however hard the comparator chatters. What
// remains is one message repeating ten times a second for as long as the
// band stays strong — no longer ring-flushing, still enough to bury the
// lines around it over a session.
//
// So the edge gate stays and a rate limit sits behind it: warn on the first
// transition, then at most once per window, carrying the count of
// transitions the window swallowed. Note what that count is and is not — it
// counts the transitions SEEN, at the 10 Hz telemetry cadence, not
// comparator edges, which are sampled far below their true rate and always
// were.
QElapsedTimer m_adcOverloadClock;
int m_adcOverloadEdges = 0;
static constexpr qint64 kAdcOverloadWarnIntervalMs = 10000;
bool m_keyed = false;
bool m_tuning = false;
bool m_cwAutoKeyed = false;
Expand Down