Skip to content

feat(gui): bring Aether-gate's device controls into the app - #5372

Open
crypticpy wants to merge 3 commits into
aethersdr:mainfrom
crypticpy:pr/aether-gate-applet
Open

feat(gui): bring Aether-gate's device controls into the app#5372
crypticpy wants to merge 3 commits into
aethersdr:mainfrom
crypticpy:pr/aether-gate-applet

Conversation

@crypticpy

Copy link
Copy Markdown

Summary

Fixes #5371.

Aether-gate presents non-Flex hardware to AetherSDR as a FLEX-6600, so everything
it can say has to fit a Flex verb. Plenty does not: an RSPdx's antenna port,
bias-T, MW/DAB notches, HDR mode and AGC set-point have no Flex equivalent, and
neither does the panadapter's bin count. Reaching them meant leaving the app for
the gate's web panel — mid-operation.

GATE is a normal applet tile that talks to the gate's control port:

  • Controls are built from what the gate reports the attached device offers,
    not from a fixed list. An RSP1a, an RSPdx and an RTL stick share almost no
    settings, and hardcoding one device's set is the same mistake as hardcoding its
    sample rates.
  • Presence is the applet's own discovery: it probes the control port at the
    radio's address rather than sniffing the serial, which is operator-settable
    (--serial) and would make a renamed gate vanish from the UI. Because
    AppletPanel keeps the button out of the bar until presence is confirmed, the
    probe has to run while hidden — it stops itself once the question is answered
    either way, and showEvent restarts it.
  • The second commit fixes the probe firing at an empty address: the applet
    reaches the gate at the radio's address, but ip() is filled in by the
    reply to info, which RadioModel only sends after it emits
    connectionStateChanged(true). A probe with nowhere to go returns without
    counting a failure, so the applet stopped its timer and gave up having never
    actually asked. It now watches infoChanged and re-probes on address change.
    This is the same ordering trap the TX power-scale wiring hit in Power Meter #4813.

The diff is purely additive (603 insertions, 0 deletions) across one new
applet plus its AppletPanel / MainWindow_Wiring registration.

Relationship to #4944

#4944 asks the maintainer to settle where an undiscoverable peripheral gets
configured and when its applet button is visible, after #4905 and #4919 landed
with opposite answers. This is a third data point, not an attempt to pre-empt
that decision.
It lands on the middle path #4944 itself proposes — hidden until
the device is confirmed present, shown thereafter — and it can do so because a
gate is discoverable: it answers on a known port at the radio's address, which
is precisely what #4944 observes was missing for Everyware. Happy to re-shape
this to whatever convention #4944 settles on.

On RFC scope

Per GOVERNANCE.md this needs no RFC: "new applets or dialogs that don't change
existing UX"
is explicitly on the no-RFC list, no existing control, shortcut or
default changes behaviour, and nothing appears in the applet bar for users
without a gate. A waterfall colour-scheme commit that originally sat on this
branch has been deliberately split out and held back
, because changing theme
colours is RFC-gated and "do not open a PR until the RFC issue is approved."

Constitution principle honored

Principle VIII — Evidence Over Assertion. The third commit exists because I
ran the repo's own gates locally instead of assuming the applet was clean: the
hardcoded-colour ratchet in static-checks.yml is a delta against the PR base,
and this applet raised two of three counters (total_references +1 from a
#8090a0 literal, setstylesheet +5 from the call sites) — it would have failed
CI. Row labels now resolve through ThemeManager::applyStyleSheet() with a
{{color.text.secondary}} token, which also makes them restylable by a user
theme and repaint on a live theme switch, neither of which a bare
setStyleSheet() did. Re-verified with the exact command CI runs, against the
merge base: unique_colours 613 (+0), total_references 2724 (+0), setstylesheet 1090 (+0).

Principle II — The Radio Is Authoritative On Live State. The control set is
whatever the gate reports its device offers; the applet asserts no device shape
of its own.

Test plan

  • Local build passes (cmake --build build) — clean, links exit 0
  • Behavior verified on a real radio if applicable — yes: driven against a
    live Aether-gate 0.4.3 fronting an SDRplay RSPdx-R2, exercising antenna
    port, bias-T, notches, HDR, AGC set-point and the span/bins resolution
    controls. The antenna-port control in particular was used to run an
    A-vs-B disconnect measurement (25 dB difference between a connected and an
    empty port), so the writes demonstrably reach the hardware
  • Existing tests pass (CI) — full local suite: only the two failures that
    reproduce unchanged on clean main (bridge_docs_check,
    hl2_state_restore_test). Repo gates run locally: colour ratchet +0 on
    all three counters
    ; check_engine_boundary.py → 0 findings would block;
    check_a11y.py → no findings in the new file
  • This PR adds no automated test. The applet's behaviour is HTTP probe +
    widget construction against a live gate, and there is no existing fixture
    for a fake gate endpoint to build on. Flagging it rather than claiming
    coverage — happy to add one if a reviewer wants a particular shape

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md) — GPG, GitHub reports
    verified: true
  • No new flat-key AppSettings calls — the applet persists nothing; device
    state lives on the gate, which is authoritative for it
  • Code is clean-room — AetherSDR's own widget code against public Qt API and
    the gate's own documented control port (Principle IV)
  • All meter UI uses MeterSmootherN/A, the applet has no meter; its
    readouts are discrete device settings, not continuous levels
  • Documentation updated if user-visible behavior changed — the applet is
    self-describing (every control is built from the gate's own reported
    shape, with tooltips), and no existing document describes the applet bar's
    contents as a fixed list. CHANGELOG.md deliberately untouched
  • Security-sensitive changes reference a GHSA if applicable — N/A. Worth
    a reviewer's eye regardless: the applet issues HTTP GETs to the radio's
    address on a fixed control port, and renders the returned setting shape.
    It stores no credentials and sends none

Note on the claim protocol (AGENTS.md §Issue / PR Claim Protocol): assignee
changes are rejected for an account without write access to this repo, so the
Fixes #5371 link is the visible claim on the issue timeline instead.

crypticpy and others added 3 commits August 31, 2026 22:51
Aether-gate presents non-Flex hardware to AetherSDR as a FLEX-6600, so
everything it can say has to fit a Flex verb. Plenty does not: an RSPdx's
antenna port, bias-T, MW/DAB notches, HDR mode and AGC setpoint have no
Flex equivalent, and neither does the panadapter's bin count. Reaching
them meant leaving the app for the gate's web panel.

GATE is a normal applet tile that talks to the gate's control port. The
controls are built from what the gate reports the attached device
offers, not from a fixed list — an RSP1a, an RSPdx and an RTL stick share
almost no settings, and hardcoding one device's is the same mistake as
hardcoding its sample rates.

Presence is the applet's own discovery: it probes the control port at the
radio's address rather than sniffing the serial, which is operator-settable
(--serial) and would make a renamed gate vanish from the UI. Because
AppletPanel keeps the button out of the bar until presence is confirmed,
the probe has to run while hidden — it stops itself once the question is
answered either way, and showEvent restarts it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The GATE button never appeared. The applet reaches the gate at the
RADIO's address, but ip() is filled in by the reply to "info", which
RadioModel only sends after it emits connectionStateChanged(true) — so
the probe fired at an empty address. A probe with nowhere to go returns
without counting a failure, so the applet stopped its timer and gave up
having never actually asked.

Watch infoChanged instead, and re-probe on address change rather than on
every emission. Clearing the remembered address on disconnect keeps a
reconnect honest: the gate may have been restarted, or replaced by a
real Flex at the same address.

Same ordering trap the TX power-scale wiring hit in aethersdr#4813, documented a
hundred lines above where this went in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nciple VIII.

The applet styled its five row labels with a literal
"QLabel { color: #8090a0; font-size: 10px; font-weight: bold; }" pushed through
setStyleSheet(). Two problems, both caught by running the repo's own gate rather
than assuming the file was clean:

  * static-checks.yml's hardcoded-colour ratchet is a DELTA against the PR base,
    and this applet raised it on two of three counters — total_references +1
    (the literal) and setstylesheet +5 (the call sites). It would have failed CI.
  * A bare setStyleSheet() resolves once. The colour could not be restyled by a
    user theme and did not repaint on a live theme switch.

Route the labels through ThemeManager::applyStyleSheet() with a {{token}}
placeholder instead — the same shape MainWindow already uses. applyStyleSheet
resolves scope-aware against the widget's container chain and re-resolves every
widget it tracks when the theme changes, so this is a behaviour gain, not only
ratchet appeasement. Its own setStyleSheet call lives in ThemeManager.cpp, which
the ratchet allow-lists as the file that DEFINES the token values.

color.text.secondary (#8ea8c0), not color.text.label (#506070): #8090a0 is a
light mid-grey, so the semantically-named label token would have visibly
darkened these against every sibling applet. Preserving the designed appearance
wins over the tidier name here.

Verified with the exact command CI runs, against the merge base:
  unique_colours 613 (+0), total_references 2724 (+0), setstylesheet 1090 (+0)

TunerApplet and ProfileSwitcherApplet still hold their own copies of the old
literal. Converging all three is a follow-up — out of scope for a PR that should
only carry the new applet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant