Skip to content

feat(aetherd): IRadioBackend seam + RadioCapabilities (RFC step 2.1) (#3849)#4054

Merged
ten9876 merged 3 commits into
mainfrom
aetherd-step2-iradiobackend
Jul 5, 2026
Merged

feat(aetherd): IRadioBackend seam + RadioCapabilities (RFC step 2.1) (#3849)#4054
ten9876 merged 3 commits into
mainfrom
aetherd-step2-iradiobackend

Conversation

@ten9876

@ten9876 ten9876 commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

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 — abstract QObject seam: capability query, connection lifecycle, canonical core-profile verbs (setSliceFrequency/Mode/Filter, setKeying), a namespaced invokeExtension escape hatch, and normalized state + data-plane signals emitted UP. AUTOMOC-listed so the Q_OBJECT is moc'd into libaethercore. 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 extensions bag; the honest self-description that feeds the protocol welcome (§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)

  1. Capabilities shape → typed struct + QVariantMap extension bag.
  2. Sub-model ownershipRadioModel keeps owning SliceModel et al.; the backend drives them via signals. Minimal churn.
  3. One interface vs RX/TX split → one interface; capabilities().canTransmit == false + setKeying() guarded no-op for receive-only families. The TX guard lives above the seam (§6).
  4. Directory layoutsrc/core/backends/, families under src/core/backends/<family>/ (FlexBackend → flex/).

Verification

Clean build; IRadioBackend moc'd into libaethercore; engine-boundary --strict green (the new backends/ dir has no gui includes / no QtWidgets).

Next

2.2 — FlexBackend skeleton wrapping the existing SmartSDR stack (RadioConnection/PanadapterStream/RadioDiscovery) behind RadioModel, zero behavior change, verified with verify_slice0_rx.py. AGENTS.md staging Block 2 lands with it (routing rules become actionable once a backend exists).

🤖 Generated with Claude Code

…— 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>

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

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 AetherSDR matches the core-wide convention (314 files under src/core/).
  • #include "core/backends/RadioCapabilities.h" resolves via aethercore's src/ PUBLIC include dir (CMakeLists.txt:1200) — consistent with the core/-prefixed include style used elsewhere.
  • The AUTOMOC-via-target_sources line mirrors the existing UlanziDialBackend.h pattern, and the abstract Q_OBJECT with signals genuinely needs moc, so listing it is correct.
  • CI is fully green — build/macOS/Windows/CodeQL plus the Engine/UI dependency direction gate you cited all pass, confirming backends/ 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

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

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 throughout src/core) and the #include "core/backends/..." prefixed-quote style (consistent with core/aprs/... etc.).
  • No symbol collisions — IRadioBackend/RadioConnectRequest/RadioCapabilities are all new.
  • RadioCapabilities doesn't duplicate the existing ModelCapabilities; 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's connect/disconnect) is a good deliberate choice — avoids shadowing QObject::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

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

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 AetherSDR matches 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_OBJECT header (IRadioBackend.h) is added to aethercore target_sources for AUTOMOC; RadioCapabilities.h is a plain struct and correctly left off. The comment-alignment cleanup on the UlanziDialBackend.h line 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 new backends/ dir stays free of gui/QtWidgets includes, as the PR body claims).

One minor, optional nit:

  • IRadioBackend.h uses QByteArray in 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

ten9876 and others added 2 commits July 5, 2026 09:18
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>
@ten9876

ten9876 commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

Review resolved

Thanks for the three passes — all COMMENTED/non-blocking, and the sequencing/design notes are appreciated.

  • invokeExtension async (the substantive point): @ten9876 pushed the redesign in eb755a69 — fire-and-forget void with a caller-minted requestId and extensionResult/extensionError reply signals. That's the better call than my callee-returned id: it keeps every DOWN verb uniformly void and matches §4.1's client-minted correlation id. Adopted as the base.
  • <QByteArray> explicit include — done in the same commit.
  • RadioCapabilities vs ModelCapabilities naming (raised twice): added a comment spelling out the distinction (model-string-derived truth vs backend-reported self-description) in 4bddb27b.
  • Q_DECLARE_METATYPE / cross-thread metatype: intentionally deferred to 2.2. Nothing carries RadioCapabilities across a queued connection yet (capabilitiesChanged() is payload-free), so the declaration + the runtime qRegisterMetaType land together with the first cross-thread emitter — adding half of it now would be premature.

Interface builds clean, Q_OBJECT moc'd into libaethercore, boundary --strict green. Ready for the FlexBackend skeleton (2.2).

@ten9876 ten9876 merged commit 2bfed9f into main Jul 5, 2026
6 checks passed
@ten9876 ten9876 deleted the aetherd-step2-iradiobackend branch July 5, 2026 17:06
ten9876 added a commit that referenced this pull request Jul 5, 2026
… (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>
@nigelfenton

Copy link
Copy Markdown
Contributor

One follow-up on RadioCapabilities now that 2.1 is in. The doc's "capabilities are declared, not assumed" principle names #4027 directly — and radio-declared bands are the one capability #4027 already carries end-to-end (a bands= key off the discovery/status wire, parsed + validated into a band list). But the merged struct doesn't have a band field yet. Deliberate deferral, or open for the FlexBackend touchpoint?

If it's wanted, I'm happy to take it. #4027 already has the parser — parseDeclaredBands(csv) → validated QStringList in src/models/DeclaredBands.h (drops unknown names, empty list = real-Flex path unchanged). So FlexBackend::capabilities() could populate a band field on RadioCapabilities straight from that same parse — one source of truth, no new decode. The struct side is just a field (QStringList declaredBands, or a typed BandId set if you'd prefer) plus wiring it in the Flex backend.

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.

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.

2 participants