feat(aetherd): IRadioBackend seam + RadioCapabilities (RFC step 2.1) (#3849)#4054
Conversation
…— Principle I. First sub-step of RFC §10 step 2 (§5.5 pluggable radio backends). Pure addition — a header-only interface with no implementor wired, zero behavior change. Establishes the radio-facing seam's contract for review before the FlexBackend wiring (2.2) builds on it. - src/core/backends/IRadioBackend.h — abstract QObject seam: capability query, connection lifecycle, canonical core-profile verbs (grow per the touchpoint burndown), a namespaced extension escape hatch, and normalized state/data-plane signals the backend emits UP. AUTOMOC-listed so the Q_OBJECT is moc'd into libaethercore. - src/core/backends/RadioCapabilities.h — typed core-profile capability struct + QVariantMap extension bag; the honest self-description that feeds the protocol `welcome` (§4.1) and replaces model-impersonation (§1). - docs/architecture/aetherd-iradiobackend-design.md — the step-2 design, with the four §5.5 open questions resolved (typed caps + bag; RadioModel keeps sub-models, backend drives them; one interface with canTransmit gating keying; src/core/backends/<family>/). DSP location is invisible above the seam (protocol-decoder vs engine-side DSP chain both emit the same signals). TX keying is only translated here; the guard/arbitration lives above the seam (§6). SmartSDR becomes the first implementor (FlexBackend, 2.2) with zero behavior change. Verified: clean build; IRadioBackend moc'd into libaethercore; boundary --strict green (backends/ dir clean — no gui includes, no QtWidgets). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the two headers, the CMake wiring, and the design doc against the tree. This is a clean, tightly-scoped pure addition — no implementor wired, so there's no behavior to regress, and the four files are exactly what the description claims (no scope creep). Thanks for keeping the "core seed" small rather than dumping all 140 touchpoints at once; that makes it genuinely reviewable.
Conventions — all good:
namespace AetherSDRmatches the core-wide convention (314 files undersrc/core/).#include "core/backends/RadioCapabilities.h"resolves via aethercore'ssrc/PUBLIC include dir (CMakeLists.txt:1200) — consistent with thecore/-prefixed include style used elsewhere.- The AUTOMOC-via-
target_sourcesline mirrors the existingUlanziDialBackend.hpattern, and the abstractQ_OBJECTwith signals genuinely needs moc, so listing it is correct. - CI is fully green — build/macOS/Windows/CodeQL plus the
Engine/UI dependency directiongate you cited all pass, confirmingbackends/stays clean of GUI/QtWidgets includes.
One forward-looking note (non-blocking, for 2.2+):
capabilities() returns RadioCapabilities by value, which is fine for the in-process direct-call path this step targets. Once the daemon/protocol lands and any signal starts carrying the struct across a queued (cross-thread) connection, it'll need Q_DECLARE_METATYPE + qRegisterMetaType. Nothing to change now — the current capabilitiesChanged() is payload-free — just flagging it so it isn't a surprise when the boundary goes cross-thread. Same eventual consideration applies to invokeExtension() returning QVariant synchronously if it's ever invoked across the seam rather than in-process.
Design-wise the resolved §5.5 questions read sensibly: keeping RadioModel as sub-model owner and having the backend drive via signals is the minimal-churn choice, and the single-interface + canTransmit/guarded-setKeying() approach keeps the TX guard above the seam where §6 wants it. No concerns to hold up the interface contract. Nice work — looking forward to the FlexBackend skeleton in 2.2.
🤖 aethersdr-agent · cost: $5.7774 · model: claude-opus-4-8
There was a problem hiding this comment.
Reviewed the full diff — a header-only, no-implementor pure addition, exactly as described. This is clean, well-documented work and lands the seam's contract in a reviewable state. Thanks @ten9876.
Verified
- Conventions match the codebase:
namespace AetherSDR(used throughoutsrc/core) and the#include "core/backends/..."prefixed-quote style (consistent withcore/aprs/...etc.). - No symbol collisions —
IRadioBackend/RadioConnectRequest/RadioCapabilitiesare all new. RadioCapabilitiesdoesn't duplicate the existingModelCapabilities; they're distinct concepts (radio-reported self-description vs. model-string-derived truth), which is the right split and consistent with the §1 anti-pattern the doc calls out.connectRadio()/disconnectRadio()(vs. the doc'sconnect/disconnect) is a good deliberate choice — avoids shadowingQObject::connect. No change needed.- All 7 checks green, including
Engine/UI dependency direction(the boundary gate) and CodeQL.
One design point worth settling now, while there's no implementor:
invokeExtension(ns, verb, arg) returns QVariant synchronously, whereas every other verb (setSliceFrequency, setSliceMode, setKeying, connectRadio) is fire-and-forget void with results arriving via signals. For a backend speaking an async wire protocol (FlexBackend over SmartSDR TCP), a synchronous return can't carry a real device response — it would have to be a local/echoed value or block the caller. That's an inconsistency in the seam's async model. Since a Flex extension verb (e.g. an ATU/amp command) is exactly the kind of thing whose result comes back later on the wire, it may be worth deciding whether extension results should return via a signal + request-id (or a QFuture/callback) rather than a blocking QVariant. Cheap to change now, expensive once 2.2 wires a real backend against it.
Minor (optional): given the near-identical names, a one-line comment in RadioCapabilities.h distinguishing it from ModelCapabilities would save a future reader the double-take.
Neither blocks — the sequencing is sound and the interface is easy to revise while it has no callers. Nice groundwork for 2.2.
🤖 aethersdr-agent · cost: $6.8494 · model: claude-opus-4-8
There was a problem hiding this comment.
Reviewed the full diff against the tree. This is a clean, well-scoped RFC step — a header-only interface + capability struct + design doc, no implementor wired, genuinely zero behavior change. Nice work, @ten9876.
Conventions & correctness — all good:
namespace AetherSDRmatches the core convention;#include "core/backends/RadioCapabilities.h"uses the src-rooted include style the rest of the tree uses.- The CMake change is right: only the
Q_OBJECTheader (IRadioBackend.h) is added toaethercoretarget_sourcesfor AUTOMOC;RadioCapabilities.his a plain struct and correctly left off. The comment-alignment cleanup on theUlanziDialBackend.hline is a tidy touch. - Design-doc links resolve (
../aetherd-headless-engine-design.md,aetherd-touchpoints.md). - QObject-parent ctor gives RAII ownership;
connectionError(QString)is the right boundary error channel for the future implementor. No AppSettings/persistence surface, so nothing to flag there. - All 7 CI checks green, including
Engine/UI dependency direction(confirms the newbackends/dir stays free of gui/QtWidgets includes, as the PR body claims).
One minor, optional nit:
IRadioBackend.husesQByteArrayin three data-plane signals (spectrumFrameReady,waterfallRowReady,audioFrameReady) but doesn't#include <QByteArray>. It compiles transitively via<QVariant>today (hence CI passing), but for a foundational interface header that many TUs will include, an explicit#include <QByteArray>would be more robust against future include-graph churn. Not a blocker.
The resolved §5.5 decisions (typed struct + extension bag, RadioModel keeps sub-model ownership, single interface with canTransmit guard above the seam, src/core/backends/ layout) all read as sound defaults, and keeping them revisable while there's no implementor is the right call. LGTM — thanks for the careful sequencing and the design doc.
🤖 aethersdr-agent · cost: $5.0420 · model: claude-opus-4-8
Every other DOWN verb on IRadioBackend is fire-and-forget void with results arriving via signals, but invokeExtension returned QVariant synchronously — a backend speaking an async wire protocol (FlexBackend/SmartSDR TCP) can't carry a real device response (ATU tune, amp state) synchronously; it would have to block or fabricate a local value. Make it fire-and-forget: invokeExtension(ns, verb, requestId, arg) with the reply arriving via new extensionResult(requestId, QVariant) / extensionError( requestId, reason) signals, keyed by a caller-minted requestId (0 = no reply expected). Cheap to settle now while the seam has no implementor; expensive once 2.2 wires FlexBackend against it. Also add the explicit <QByteArray> include the data-plane signals rely on (was transitive via <QVariant>). Design doc updated to match. Still header-only, no implementor, zero behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…2.1 review nit) — Principle VIII. Two reviews of #4054 flagged the near-identical names. Add a comment clarifying the distinct concepts: models/ModelCapabilities is model-string-derived truth (static FlexLib platform table, Principle I); RadioCapabilities is the backend's *reported* self-description surfaced to clients. A FlexBackend may seed one from the other. (Q_DECLARE_METATYPE deferred to 2.2, where the first cross-thread signal actually carries the struct — adding it now, with no carrier, is premature.) Adopts the maintainer's async invokeExtension redesign (eb755a6) as the base — caller-minted requestId is the better fit for §4.1's correlation id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review resolvedThanks for the three passes — all COMMENTED/non-blocking, and the sequencing/design notes are appreciated.
Interface builds clean, |
… (RFC step 2.2) — Principle I. Introduces the SmartSDR/FlexRadio backend behind the §5.5 seam, wired into the real RadioModel lifecycle. Purely ADDITIVE and behavior-neutral: the wire objects (RadioConnection/PanadapterStream + threads) and the command hot path are NOT rerouted yet — that ownership move (the #502 teardown risk) and the per-touchpoint conversions are the following increments (2.2b–2.4). - src/core/backends/flex/FlexBackend.{h,cpp} — implements IRadioBackend. Observes the connection's connected/disconnected/errorOccurred and re-emits them as the interface signals (so the backend is live in the wire path). capabilities() seeds from the FlexLib ModelCapabilities table (Principle I). Core verbs (setSliceFrequency/Mode/Filter, setKeying) build the exact SmartSDR strings ("slice tune", "slice set … mode=", "filt …", "xmit …") into RadioModel's command sink — correct but not yet on the live path (the burndown routes each model through them in 2.3). - RadioModel owns it via std::unique_ptr<IRadioBackend>, constructed after the connection is wired and reset FIRST in ~RadioModel (before the connection teardown) so its observation tears down cleanly. The risky connection/panStream/thread teardown is untouched. - AGENTS.md: In-flight status advanced to the step-2 seam; added the "where radio-facing code goes" routing table (backend code → src/core/backends/<family>/). The touchpoint-conversion claim/recipe + autonomous-conversion carve-out are deferred to 2.3 (staging doc updated). Verified: clean build; FlexBackend moc'd into libaethercore; boundary --strict green; 61/61 tests; verify_slice0_rx.py FULL PASS against a live FLEX-8600 (connect → slice-0 RX, TX-guard refusal); clean connect+teardown (no ASAN/segfault — the thread teardown is unchanged). Stacked on the 2.1 interface branch (#4054) — a genuine hard dependency (2.2 implements the 2.1 interface); rebases onto main when 2.1 merges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
One follow-up on If it's wanted, I'm happy to take it. #4027 already has the parser — Not trying to front-run your 2.2 sequencing — just flagging that the parser's already there and tested if you'd like the band set to land with FlexBackend. Whatever shape you want it in, I can match. |
First sub-step of the accepted aetherd RFC (#3849, §10 step 2 / §5.5 pluggable radio backends). Pure addition — a header-only interface with no implementor wired, zero behavior change. Lands the radio-facing seam's contract so it can be reviewed before the FlexBackend wiring (2.2) builds on it.
What's here
src/core/backends/IRadioBackend.h— abstractQObjectseam: capability query, connection lifecycle, canonical core-profile verbs (setSliceFrequency/Mode/Filter,setKeying), a namespacedinvokeExtensionescape hatch, and normalized state + data-plane signals emitted UP. AUTOMOC-listed so theQ_OBJECTis moc'd intolibaethercore. Marked as the core seed that grows one verb at a time per the touchpoint burndown — not all 140 touchpoints at once.src/core/backends/RadioCapabilities.h— typed core-profile capability struct +QVariantMap extensionsbag; the honest self-description that feeds the protocolwelcome(§4.1) and structurally replaces the model-impersonation anti-pattern (§1, PR feat(radio): radio-declared band capability via optional bands= discovery/status key #4027).docs/architecture/aetherd-iradiobackend-design.md— the step-2 design doc.Resolved §5.5 open questions (proceeding as recommended; revisable in review while there's no implementor)
QVariantMapextension bag.RadioModelkeeps owningSliceModelet al.; the backend drives them via signals. Minimal churn.capabilities().canTransmit == false+setKeying()guarded no-op for receive-only families. The TX guard lives above the seam (§6).src/core/backends/, families undersrc/core/backends/<family>/(FlexBackend →flex/).Verification
Clean build;
IRadioBackendmoc'd intolibaethercore;engine-boundary --strictgreen (the newbackends/dir has no gui includes / no QtWidgets).Next
2.2 —
FlexBackendskeleton wrapping the existing SmartSDR stack (RadioConnection/PanadapterStream/RadioDiscovery) behindRadioModel, zero behavior change, verified withverify_slice0_rx.py. AGENTS.md staging Block 2 lands with it (routing rules become actionable once a backend exists).🤖 Generated with Claude Code