Skip to content

Three surfaces that report confidently and wrongly: get radio.txPower, get meters.sLevel, and the noise-blanker comment in Hl2RxDsp::setAudioMuted #5499

Description

@on8st

Report preparation

  • I used the AI-assisted bug report tool (Help → Support → File an Issue)
  • I have attached a support bundle or log file

What happened?

Three surfaces in the app report confidently and wrongly, with no failure
visible from outside. Each is the documented, obvious thing an operator or an
instrument would reach for. Each answers plausibly. None of them answers.

They are filed together because they are one class and because a maintainer
fixing one will already be in the files the next one lives in.

The class has a name in this codebase already. Two lines above one of the fields
below, metersSnapshot says:

Null rather than a stale ratio, matching the SWR entry in all and the
fwdPower/reflectedPower pair above — a client reading this scalar must not
get a different answer from the one reading the array
(#4533).

That is exactly the rule all three break. swr got the treatment; the field on
the next line did not.

All three were found on a Hermes-Lite 2 during a measurement run on the
operator's own radio, keyed into a dummy load at 10% drive
(of order 0.15 W
forward, uncalibrated). None of them required transmitting to establish; two of
them are visible from a receiving radio and one from source alone. The keying is
mentioned because it is how the first one was caught in the act.


1. get radio.txPower is a constant 0.0 whatever the drive is

RadioModel::m_txPower is declared

float       m_txPower{0.0f};

in RadioModel.h and is assigned nowhere in the tree. The only writes to a
symbol of that name are SMeterWidget's own, unrelated member. RadioModel::txPower()
returns it, AutomationServer's radioSnapshot publishes it as txPower, and
the property declaration is

Q_PROPERTY(float   txPower     READ txPower     NOTIFY metersChanged)

— so it advertises a freshness it cannot have. metersChanged will fire; the
value behind it never moves.

Proved live, not only from source. With get transmit.rfPower reading 10,
the tone modulating, the relay thrown and 0.153–0.184 W measurably going into the
dummy load on all three keyed windows, get radio.txPower read 0 on every
sample.

The tree already believes this field is live. Hl2Backend's own drive
telemetry block — the one added under #4912 precisely to stop automation trusting
a readback that shares the failure it is checking — says:

Nothing anywhere reported the APPLIED drive. get transmit has rfPower and
get radio has txPower, but both read TransmitModel — the operator's
request — which is exactly the readback-shares-the-failure problem this section
exists to solve.

Only the first half is true. get radio.txPower reads RadioModel, not
TransmitModel, and reads a field nothing writes. A comment written to warn
about an over-trusted readback names a field that is worse than the one it warns
about.

What it cost here. An earlier run of ours gated on this field: it asked for
a drive and read it back before keying, which is the right shape of gate. The
gate was incapable of failing — it compared 0 against 0. A later run asked
for drive 10, read back 0, and correctly refused before the first key; it was
only in working out why that the field turned out to be dead. So one of our own
published runs carries a "drive set and read back" sentence that is a
coincidence, and has been withdrawn.

The live field is TransmitModel::m_rfPower, written by
TransmitModel::setRfPower and also assigned from the radio's own status in
TransmitModel::applyStatus, published as get transmit.rfPower.

Suggested remedy: populate m_txPower from the value TransmitModel already
carries, or drop the field from radioSnapshot entirely so a client cannot
read it. Either is fine. Two readings in one app disagreeing is not.


2. get meters.sLevel is dead on this backend — a constant −130 dBm

MeterModel::m_sLevel is declared {-130.0f} and assigned in exactly one
place
: MeterModel::clear, to -130.0f. The meter-packet path never touches
it. A per-slice LEVEL meter is recognised through m_sLevelIdxBySlice, and that
branch stores the value in m_values[idx] and emits sLevelChanged(slice, v)
which is what MainWindow::wireMeters carries to SMeterWidget::setLevel. The
scalar is not on that path.

metersSnapshot publishes {"sLevel", m->sLevel()} regardless.

Measured. One 14-second pass, six legs, radio receiving normally throughout:
1304 samples of get meters.sLevel, every one of them exactly −130 dBm,
zero distinct values other than that. On the control leg alone that is 212
identical samples over 14.03 s. The live level in the same session, read from the
SLC:LEVEL row of the same reply's all array, sat at a median of
−83.9 dBm (214 samples).

So the working path exists, in the same reply, two fields away. This named field
is simply not on it.

Suggested remedy: populate the scalar from the same per-slice value the array
carries, or publish it as null rather than −130, so a client can tell "the
radio never told us"
from "the signal is at the bottom of scale". That is the
distinction metersSnapshot already draws for swr, and which #4533 settled for
that field.

Related, and reported separately because it is a different quantity: the
SLC:LEVEL row itself is published outside its own declared range.
Hl2Backend::defineMeters declares it −140…0 dBm, and during a transmission the
smoothed value reaches −252 dBm — Hl2Backend::addReceiver's EMA runs on the raw
dBm with no clamp. Noted here only so a maintainer touching this file sees both.


3. Hl2RxDsp::setAudioMuted's comment inverts the code it points at — and the mitigation it describes is inert with the noise blanker off

Hl2RxDsp::setAudioMuted ends:

// ... Holding it freezes the average instead; WdspChannel flushes it on release.

WdspChannel::processIq's hold-release branch says the opposite, in terms:

// DELIBERATELY NOT FLUSHED on the way out of a hold, and this is
// the whole point of holding rather than muting. The stage was
// SKIPPED while held, not fed, so its running average still holds
// the pre-transmit signal level and it is already armed for the
// first receive sample.

and then argues it at length with measured numbers: a flush there would leave the
blanker unarmed for ~200 ms at backtau 0.05 s, giving a blanked/unblanked
impulse peak ratio of 1.000 — bit-identical to the blanker being switched off
— against 0.43 once settled, and 0.52 without the flush.

The behaviour is the deliberate, well-argued one. The comment upstream of it is
stale and inverts it.
The only flush_anbEXT call in the file is in
WdspChannel::setNoiseBlanker, on enable, not on hold release.

WdspChannel::setNoiseBlankerHold's own comment carries the same wrong belief a
second time: "Both stores are atomic and the flush they schedule happens inside
processIq() itself."
No flush is scheduled and none happens.

And with the blanker off, none of it runs at all. The entire hold mechanism
sits inside

if (m_nbActive.load(std::memory_order_relaxed)) {
    if (m_nbHold.load(std::memory_order_relaxed)) {

so with the blanker disabled, m_nbHold gates a stage that is not running and
the mitigation is dead code. On the station where this was found, get hostnb
read {"on": false, "requestedOn": false, "level": 50, "threshold": 20} — the
blanker is off, which is the default and is the configuration the unkey
transient in #5497 was reported and measured on. Whatever that mitigation is
worth, it is not in the path there.

Suggested remedy: correct the sentence, change no behaviour. Say what
WdspChannel actually does — the stage is skipped while held, so its running
average is preserved and it is already armed on release, and nothing is flushed.
A comment that inverts the code it points at is worse than no comment: this one
sent a bench looking through the source for a mechanism that does not exist.


What did you expect?

That a published field either carries a value or says it has none; that a
Q_PROPERTY declaring NOTIFY metersChanged is attached to something that
changes; and that a comment describing a neighbouring class's behaviour matches
that behaviour.

Steps to reproduce

1 — Connect a Hermes-Lite 2. Set a transmit drive: set transmit rfpower=10.
Read both fields:

get transmit   ->  "rfPower": 10
get radio      ->  "txPower": 0

The second stays 0 at every drive, before and during a transmission. (Confirming
that the drive is really applied needs a physical witness — here, forward power
present on the load while keyed.)

2 — With the radio receiving normally, poll get meters and compare the two
places the same quantity is published:

get meters   ->  "sLevel": -130          (constant, always)
             ->  "all":  { source "SLC", name "LEVEL" }  ~ -84 dBm, moving

3 — Source-only, no radio needed: read the last sentence of
Hl2RxDsp::setAudioMuted, then read the hold-release branch of
WdspChannel::processIq and grep flush_anbEXT over WdspChannel.cpp.

AetherSDR version

26.9.1 (built from source at 63a4a2e9, stock and uninstrumented)

Radio model & firmware

Hermes-Lite 2, gateware 74.2 (20231230_74p2_883a338)

Operating system

macOS

OS version and hardware

macOS 26.6.2, Apple M2, Qt 6.8.3


What was NOT measured

  • HL2 only. sLevel and the Hl2RxDsp/WdspChannel pair are backend-scoped
    by construction. RadioModel::m_txPower is not — it is a model field with
    no backend in it, so if it is dead here it is dead for every radio family, but
    that is a claim about the source and it was checked on no other hardware.
  • macOS only. Not built or run on Linux or Windows. Nothing above is platform
    conditioned in the source, but that is again a claim about the source.
  • Whether anything else reads these fields. The consequences described are
    the ones this bench hit. No survey of consumers was done.
  • Whether the noise blanker's hold works correctly when the blanker IS on.
    It was off throughout. Only the comment and the m_nbActive gate are
    established here; the argued-for behaviour is taken at its own word.
  • The dummy load's power rating could not be established from any record
    available here, and the operator was not asked.
    Forward power is uncalibrated
    and reads only while keyed.
  • No far-side observer. Nothing here says what any other station heard.

Citations

By file and symbol; no line numbers.

  • src/models/RadioModel.h — the m_txPower member declaration, RadioModel::txPower,
    and the Q_PROPERTY(float txPower READ txPower NOTIFY metersChanged)
  • src/core/AutomationServer.cpp radioSnapshot — the txPower entry
  • src/models/TransmitModel.cpp TransmitModel::setRfPower and
    TransmitModel::applyStatus — the live field
  • src/core/backends/hl2/Hl2Backend.cpp — the drive-telemetry comment block that
    names txPower as reading TransmitModel; and Hl2Backend::defineMeters for
    the SLC:LEVEL −140…0 dBm range
  • src/models/MeterModel.cpp MeterModel::clear (the only assignment to
    m_sLevel) and the meter-packet loop that recognises m_sLevelIdxBySlice and
    emits rather than stores
  • src/models/MeterModel.h — the m_sLevel declaration and MeterModel::sLevel
  • src/gui/MainWindow_Wiring.cpp MainWindow::wireMeters
    MeterModel::sLevelChangedSMeterWidget::setLevel, the path the scalar is
    not on
  • src/core/AutomationServer.cpp metersSnapshot — the sLevel entry and the
    #4533 comment two lines above it
  • src/core/backends/hl2/Hl2RxDsp.cpp Hl2RxDsp::setAudioMuted — the wrong
    sentence
  • src/core/dsp/WdspChannel.cpp WdspChannel::processIq (the hold-release branch
    that does not flush, and the m_nbActive gate above it),
    WdspChannel::setNoiseBlankerHold (the second wrong sentence),
    WdspChannel::setNoiseBlanker (the only flush_anbEXT, on enable)

Related: #4533 (the swr scalar, the precedent this cites), #5370 (HL2 TX Delay
— the same "the control is present, editable and looks functional" shape on the
GUI side rather than the automation side), #4957 (a stale comment naming a path
that has not existed since #4545).

🤖 Generated with Claude Code

https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    audioAudio engine and streamingbugSomething isn't workingmaintainer-reviewRequires maintainer review before any action is takenpriority: mediumMedium priorityprotocolSmartSDR protocol

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions