-
-
Notifications
You must be signed in to change notification settings - Fork 118
feat(aetherd): add observe-only typed resources #5391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ten9876
merged 6 commits into
aethersdr:main
from
rfoust:codex/aetherd-stage3-read-resources
Sep 4, 2026
Merged
Changes from 5 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
fed24d3
feat(aetherd): add observe-only resources. Principle VII.
rfoust 08f36cc
fix(aetherd): refresh reclaimed resources. Principle VIII.
rfoust c69d7ed
fix(aetherd): preserve resource invariants. Principle XI.
rfoust 0e601ea
Merge remote-tracking branch 'upstream/main' into codex/pr5391-latest…
rfoust 22d42ea
fix(aetherd): publish reported pan state. Principle II.
rfoust 08da8be
fix(aetherd): keep resources canonical under replaced audio. Principl…
ten9876 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| # AetherD control protocol v1 — observe-only resource catalogue | ||
|
|
||
| This catalogue fixes the schema implemented by the first read-only Stage 3 | ||
| slice of RFC #3849. It supplements | ||
| [`aetherd-control-protocol-v1-design.md`](aetherd-control-protocol-v1-design.md); | ||
| the envelope, limits, errors, authentication, and TX rules in that document | ||
| remain normative. | ||
|
|
||
| Only the four resource types below exist in this slice. `meter` and | ||
| `transmitState` remain unimplemented. No method in this catalogue mutates a | ||
| model or can reach a radio backend intent. | ||
|
|
||
| ## Resource identities and selectors | ||
|
|
||
| An exact identity has one of these shapes: | ||
|
|
||
| ```json | ||
| {"type":"server"} | ||
| {"type":"radioSession","id":"radio-1"} | ||
| {"type":"slice","radioSession":"radio-1","id":"0"} | ||
| {"type":"panadapter","radioSession":"radio-1","id":"0x40000000"} | ||
| ``` | ||
|
|
||
| `resource.get` requires an exact identity. `resource.subscribe` also accepts | ||
| an omitted `id` as an all-current-and-future selector for `radioSession`, | ||
| `slice`, or `panadapter`; `slice` and `panadapter` still require | ||
| `radioSession`. Unknown fields and unsupported resource types are rejected. | ||
|
|
||
| ## Methods | ||
|
|
||
| All methods require the negotiated session ID. The initial current-user local | ||
| endpoint grants `observe` to every negotiated session; because no other session | ||
| type exists yet, this slice has no separate per-request grant branch. Explicit | ||
| per-session grant mapping and checks arrive with authentication before another | ||
| grant or remote session is exposed. | ||
|
|
||
| ### `resource.get` | ||
|
|
||
| Parameters: | ||
|
|
||
| ```json | ||
| {"resource":{"type":"slice","radioSession":"radio-1","id":"0"}} | ||
| ``` | ||
|
|
||
| Result: | ||
|
|
||
| ```json | ||
| { | ||
| "resource":{"type":"slice","radioSession":"radio-1","id":"0"}, | ||
| "revision":3, | ||
| "value":{} | ||
| } | ||
| ``` | ||
|
|
||
| The complete typed value occupies `value`. A missing exact identity returns | ||
| `resource.not_found`. | ||
|
|
||
| ### `resource.subscribe` | ||
|
|
||
| Parameters contain 1–64 selectors: | ||
|
|
||
| ```json | ||
| {"resources":[{"type":"slice","radioSession":"radio-1"}]} | ||
| ``` | ||
|
|
||
| The result contains a session-local subscription ID, the last session event | ||
| sequence already drained to the transport, and the complete baseline matching | ||
| those selectors. Registration and snapshot capture execute as one main-thread | ||
| operation. Events still pending for existing subscriptions retain sequences | ||
| greater than the returned boundary, and newly generated events advance beyond | ||
| them, so an event delivered after the baseline cannot leave a snapshot/event | ||
| gap or reuse the baseline sequence. | ||
|
|
||
| ### `resource.unsubscribe` | ||
|
|
||
| Parameters are `{"subscription":"sub-1"}`. Success returns the same ID and | ||
| `"removed":true`. An unknown ID returns `resource.not_found`. | ||
|
|
||
| ## Events, revisions, and resync | ||
|
|
||
| `resource.changed` carries the complete new value. `resource.removed` carries | ||
| the identity and its next revision but no value. Revisions come from one | ||
| store-wide monotonic counter. They are therefore monotonic per identity and | ||
| survive removal/recreation. A revision is consumed only when a canonical value | ||
| changes or a live identity is removed, but an identity's revisions need not be | ||
| consecutive or begin at one. | ||
|
|
||
| Event `sequence` is monotonic within one protocol session. Pending events for | ||
| the same resource coalesce to the newest sequence, revision, and complete value. | ||
| Sequences may therefore have gaps; they never move backward. | ||
|
|
||
| If a session's bounded event queue cannot retain its subscribed state, the | ||
| service clears that session's subscriptions and emits: | ||
|
|
||
| ```json | ||
| { | ||
| "v":1, | ||
| "sessionId":"...", | ||
| "event":"resource.resyncRequired", | ||
| "sequence":42, | ||
| "subscriptionsInvalidated":true | ||
| } | ||
| ``` | ||
|
|
||
| The client must call `resource.subscribe` again and replace its cache from the | ||
| fresh baseline. The current-user local transport also has an independent hard | ||
| socket-output cap. A client whose operating-system socket buffer is already at | ||
|
ten9876 marked this conversation as resolved.
Outdated
|
||
| that cap can be disconnected before a queued resync notice is written; after | ||
| reconnecting it must establish a new session and baseline. | ||
|
|
||
| ## Resource values | ||
|
|
||
| ### `server` | ||
|
|
||
| - `name`: server product name. | ||
| - `buildVersion`: AetherSDR build version. | ||
| - `protocolVersions`: supported protocol versions. | ||
| - `health`: bounded service health token. | ||
| - `localTransport`: `idle`, `listening`, or `stopped`. `idle` and `stopped` | ||
| describe in-process lifecycle state before or after socket availability; a | ||
| protocol client can query this resource only while the value is `listening`. | ||
|
|
||
| No endpoint path, process environment, hostname, or filesystem value is | ||
| exported. | ||
|
|
||
| ### `radioSession` | ||
|
|
||
| - `id`, `connected`, `family`. | ||
| - `identity`: `name`, `model`, `serial`, `version`, `manufacturer`. | ||
| - `capabilities`: | ||
| - `maxSlices`, `maxPanadapters`, `sampleRatesHz`; | ||
| - `tuningRangeHz` with `minimum` and `maximum`; | ||
| - `declaredBands`, each with `name`, `lowHz`, and `highHz`; | ||
| - `canTransmit`, `maximumTransmitWatts`, `hasTuner`, `hasAmplifier`; | ||
| - `extensions`, containing namespace names only, never extension payloads. | ||
|
|
||
| `canTransmit` is observation only. It does not advertise a protocol TX method | ||
| or grant and cannot key a radio. | ||
|
|
||
| ### `slice` | ||
|
|
||
| - `id`, `letter`, `panadapterId`, `owned`. | ||
| - `frequencyHz`, `mode`, `filter.lowHz`, `filter.highHz`. | ||
| - `active`, `txSlice`, `locked`. | ||
| - `audio.gain`, `audio.pan`, `audio.muted`. | ||
| - `receive.antenna`, `receive.rfGain`. | ||
| - `receive.agc.mode`, `receive.agc.threshold`, `receive.agc.offLevel`. | ||
| - `receive.squelch.enabled`, `receive.squelch.level`. | ||
|
|
||
| Values come from `SliceModel`; radio/backend status remains authoritative. | ||
|
|
||
| ### `panadapter` | ||
|
|
||
| - `id`. | ||
| - `centerHz`, `centerKnown`, `bandwidthHz`. | ||
| - `dbmRange.minimum`, `dbmRange.maximum`. | ||
| - `bandwidthLimitsHz.minimum`, `bandwidthLimitsHz.maximum`; zero means the | ||
| backend has not reported a limit. | ||
| - `receive.antenna`, `receive.rfGain`. | ||
| - `displayCadence.fps`, `displayCadence.averageFrames`. | ||
| - `displayCadence.weightedAverage`, `weightedAverageKnown`. | ||
| - `displayCadence.waterfallRate`; `-1` means the backend has not reported a | ||
| value, otherwise this is the normalized 1–100 rate, not milliseconds. | ||
|
|
||
| FFT bins, waterfall rows, audio, and other high-rate data never enter these | ||
| JSON resources; they belong to the later bounded binary data plane. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| #include "ControlResourceStore.h" | ||
|
|
||
| namespace AetherSDR::control { | ||
|
|
||
| QString ResourceAddress::key() const | ||
| { | ||
| return type + QChar(0x1f) + radioSession + QChar(0x1f) + id; | ||
| } | ||
|
|
||
| QJsonObject ResourceAddress::toJson() const | ||
| { | ||
| QJsonObject object{{QStringLiteral("type"), type}}; | ||
| if (!radioSession.isEmpty()) { | ||
| object.insert(QStringLiteral("radioSession"), radioSession); | ||
| } | ||
| if (!id.isEmpty()) { | ||
| object.insert(QStringLiteral("id"), id); | ||
| } | ||
| return object; | ||
| } | ||
|
|
||
| bool ResourceSelector::matches(const ResourceAddress& address) const | ||
| { | ||
| return type == address.type | ||
| && (radioSession.isEmpty() || radioSession == address.radioSession) | ||
| && (id.isEmpty() || id == address.id); | ||
| } | ||
|
|
||
| QJsonObject ResourceSnapshot::toJson() const | ||
| { | ||
| return {{QStringLiteral("resource"), resource.toJson()}, | ||
| {QStringLiteral("revision"), static_cast<qint64>(revision)}, | ||
| {QStringLiteral("value"), value}}; | ||
| } | ||
|
|
||
| ControlResourceStore::ControlResourceStore(QObject* parent) | ||
| : QObject(parent) | ||
| { | ||
| } | ||
|
|
||
| bool ControlResourceStore::upsert( | ||
| const ResourceAddress& address, const QJsonObject& value) | ||
| { | ||
| const QString resourceKey = address.key(); | ||
| const auto current = m_resources.constFind(resourceKey); | ||
| if (current != m_resources.constEnd() && current->value == value) { | ||
| return false; | ||
| } | ||
|
|
||
| const quint64 revision = ++m_lastRevision; | ||
| const ResourceSnapshot next{address, revision, value}; | ||
| m_resources.insert(resourceKey, next); | ||
| emit resourceChanged(next); | ||
| return true; | ||
| } | ||
|
|
||
| bool ControlResourceStore::remove(const ResourceAddress& address) | ||
| { | ||
| const QString resourceKey = address.key(); | ||
| if (m_resources.remove(resourceKey) == 0) { | ||
| return false; | ||
| } | ||
| const quint64 revision = ++m_lastRevision; | ||
| emit resourceRemoved(address, revision); | ||
| return true; | ||
| } | ||
|
|
||
| std::optional<ResourceSnapshot> ControlResourceStore::get( | ||
| const ResourceAddress& address) const | ||
| { | ||
| const auto found = m_resources.constFind(address.key()); | ||
| if (found == m_resources.constEnd()) { | ||
| return std::nullopt; | ||
| } | ||
| return *found; | ||
| } | ||
|
|
||
| QList<ResourceSnapshot> ControlResourceStore::snapshot( | ||
| const QList<ResourceSelector>& selectors) const | ||
| { | ||
| QList<ResourceSnapshot> result; | ||
| for (auto it = m_resources.constBegin(); it != m_resources.constEnd(); ++it) { | ||
| for (const ResourceSelector& selector : selectors) { | ||
| if (selector.matches(it->resource)) { | ||
| result.append(*it); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| } // namespace AetherSDR::control |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.