Skip to content

fix(adapters): promote device-lost signalling into the base class (#41) - #42

Merged
nigelfenton merged 1 commit into
mainfrom
fix/device-lost-in-base-class
Sep 2, 2026
Merged

fix(adapters): promote device-lost signalling into the base class (#41)#42
nigelfenton merged 1 commit into
mainfrom
fix/device-lost-in-base-class

Conversation

@nigelfenton

Copy link
Copy Markdown
Owner

Fixes #41.

The failure

With a Radioberry powered off, the gate came up anyway, advertised a
FLEX-6700, and AE connected and sat on "Connecting to radio…" with a black
waterfall, an S-meter at −127 dBm and a full TX surface — for hardware that was
not powered on.

Two causes, both fixed

1. device_lost was Soapy-only. It was declared on RadioAdapter but set
in exactly one file. core/engine.py's two guards — refuse an AE connection
when the radio is gone (:1068) and drop AE rather than serve a dead stream
(:2193) — both read it with getattr(..., False), so for every other adapter
they silently did nothing. The comments at both sites describe a guarantee that
only ever held for one adapter.

2. --radio-ip short-circuited the board check. ip = self.radio_ip or self._discover(s) means the RuntimeError("no HPSDR device found…") is
unreachable whenever an address is supplied — which every systemd unit in
deploy/ does. The follow-up _discover() ran but its result was discarded, so
board=0x00 (the or 0 fallback) printed as though it were a reading.

Promoted, not copied

base.py gains note_device_alive() and note_device_silent(reason), so an
adapter signals health from its read loop instead of re-implementing threshold
logic. Design points worth reviewing:

  • The clock runs from the last evidence of life, not from the first silent
    call. A source alternating one good read with a burst of failures is not
    healthy; resetting on each good read would hide that forever.
  • note_device_silent() returns True only on the transition, so a hot loop
    can log exactly once.
  • An adapter never seen alive starts its clock rather than firing
    "it was never there" is open()'s job, and it has a better error.
  • device_lost_after_s defaults to Soapy's measured 3 s and is overridable.

Soapy is deliberately left alone. Its detection is richer than the helper
(it also watches for unchanged buffers while the driver reports success) and it
works. This PR makes the contract available to everyone else rather than
rewriting the one adapter that already had it.

The HPSDR adapter now uses the helpers in its EP6 read loop, verifies the board
even when --radio-ip is given, and prints the board id as a reading.

Verified

The original failure, reproduced fixed on the same hardware. Radioberry
still powered off, Pi 5, this branch:

18:18:16 adapter open failed: no HPSDR device answered at 10.0.0.224 (is it powered on?)
18:18:16 cleaned up (sent disconnect). NOT retrying — let the radio settle…

Ports 7993/8797 never opened — nothing was advertised, so there is no
phantom radio for AE to find.

Tests. test_device_lost.py locks the contract; stdlib-only, so it joins the
existing CI job rather than needing a new one. Full suite 193 passed on
Windows; the CI list 20/20 on both x86-64 and the Pi's ARM64.

Both mutations run rather than assumed — making note_device_silent() never
fire, and resetting the clock on silence instead of tracking last-alive. Each
fails the suite; neither is a green re-run.

Not covered

Only the HPSDR adapter was audited against a powered-off radio. The same
self.<ip> or self._discover(...) shape may exist in other adapters — I have
not checked. Nothing here was tested against a live Radioberry, so the healthy
path is exercised only by the unit tests and by the fact that the failure path
now refuses correctly.

`device_lost` was a bare attribute on RadioAdapter that only soapy.py ever
set, so core/engine.py's two guards — refuse an AE connection when the radio
is gone (:1068), drop AE rather than serve a dead stream (:2193) — were dead
code for every other adapter. Both read it with getattr(..., False), so they
silently did nothing.

The visible result, with a Radioberry powered off: the gate came up, logged
"board=0x00", advertised a FLEX-6700, and AE connected and sat on "Connecting
to radio..." with a black waterfall and a full TX surface — the exact failure
soapy.py's own comment says the guard exists to prevent.

Promoted rather than copied. base.py gains note_device_alive() and
note_device_silent(reason), so an adapter signals health from its read loop
instead of re-implementing the threshold logic. The clock runs from the last
evidence of LIFE, not from the first silent call: a source alternating one
good read with a burst of failures is not healthy, and resetting on each good
read would hide that forever. note_device_silent() returns True only on the
transition, so a hot loop can log exactly once.

The HPSDR adapter now uses them, and its open() no longer trusts --radio-ip
blindly. `ip = self.radio_ip or self._discover(s)` short-circuits the
discovery check whenever an address is supplied — which every systemd unit in
deploy/ does — so the "no HPSDR device found" RuntimeError was unreachable in
the shipped configuration, and the follow-up _discover() result was discarded.
It is now checked, and the board id prints as a reading rather than an
`or 0` fallback that looked like one.

Soapy is left alone: its own detection is richer than the helper (it also
watches for unchanged buffers while the driver reports success) and it works.

test_device_lost.py locks the contract and is registered in tests.yml — it is
stdlib-only, so it joins the existing job rather than needing a new one. Both
mutations were run: making note_device_silent() never fire, and resetting the
clock on silence instead of tracking last-alive. Each fails the suite.

Not covered: only the HPSDR adapter was audited against a powered-off radio.
The same `self.<ip> or self._discover(...)` shape may exist elsewhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nigelfenton
nigelfenton force-pushed the fix/device-lost-in-base-class branch from e74c70a to 355c9a0 Compare September 2, 2026 04:32
@nigelfenton
nigelfenton merged commit 5fa3d0c into main Sep 2, 2026
11 checks passed
nigelfenton added a commit that referenced this pull request Sep 6, 2026
0.5.0 was tagged but never published as a GitHub release. Its changelog
entry says the device_lost base-class promotion (#41/#42) "is still an
open PR" — it has since merged, so this release carries it and supersedes
that note.

Co-Authored-By: Claude Opus 5 <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.

HPSDR adapter never sets device_lost, and --radio-ip skips the board check — the gate presents a phantom FLEX-6700

1 participant