Skip to content

feat(ag): probe device with info get to fix Port B visibility#3900

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
TnxQSO-Admin:pr/3314-port-b-info-get
Jun 30, 2026
Merged

feat(ag): probe device with info get to fix Port B visibility#3900
ten9876 merged 1 commit into
aethersdr:mainfrom
TnxQSO-Admin:pr/3314-port-b-info-get

Conversation

@TnxQSO-Admin

Copy link
Copy Markdown
Contributor

Summary

Sends C<seq>|info get after the TCP prologue to retrieve the
authoritative ports= and antennas= fields from a real Antenna
Genius, and parses the response into m_device. A new
deviceInfoChanged() signal lets AntennaGeniusApplet re-evaluate
Port B visibility once those values are known.

Fixes #3314.

Framing: this is a parity bugfix, not a UX change

The existing UDP-discovery path already shows Port B correctly for
2-radio-input Antenna Genius hardware (e.g. AG 8x2). The manual-IP
path silently disagreed because no UDP beacon ever arrives, so
m_device.radioPorts defaulted to 1 via the ShackSwitch-version
heuristic. This PR restores parity between the two connection
paths. It does not introduce a new visual element, change defaults,
or alter behavior for users on currently-working connections.

Problem

AntennaGeniusModel.cpp has a ShackSwitch-specific fallback that
fires for every device when no UDP beacon has arrived:

// Uno Q sends V2.0, R4 sends V1.0.
m_device.radioPorts = (m_device.version == "2.0") ? 2 : 1;

For a real AG (firmware version like 4.1.16), neither branch matches
and radioPorts defaults to 1. Manual-IP connections never receive
a UDP beacon, so this fallback is the sole source of radioPorts
for them. AntennaGeniusApplet then hides Port B at:

m_portBSection->setVisible(m_model->connectedDevice().radioPorts >= 2);

A pcap of 4O3A's own Antenna Genius Utility connecting to the same
AG shows it issues info get immediately after the prologue. The
AG responds on a single line:

R2|0|info v=4.1.16 date=2024-10-18 btl=1.6 hw=4.0 serial=18-3A-D0 name=Antenna_Genius ports=2 antennas=8 mode=master uptime=22159

Documented at https://github.com/4o3a/genius-api-docs/wiki/Antenna-Genius-API

What this PR does

  1. Sends C<seq>|info get immediately after the prologue is accepted.
  2. Parses the response body with the existing parseKeyValues()
    helper and updates m_device.radioPorts, m_device.antennaPorts,
    m_device.serial, m_device.name, m_device.version,
    m_device.mode.
  3. Emits a new deviceInfoChanged() signal so AntennaGeniusApplet
    re-evaluates Port B visibility (which was previously only set
    inside the connected() handler, before info get would have
    responded).
  4. Calls runInitSequence() from the info get response handler
    in both the success path (inline body parse) and the
    error/terminator fallback path, so the connection does not stall
    even if a device returns an error code.
  5. Keeps the existing ShackSwitch fallback intact as a baseline that
    info get overrides when available.
  6. Also emits deviceInfoChanged() from onDiscoveryDatagram()
    after a beacon updates radioPorts/antennaPorts, so late-
    arriving beacons re-trigger Port B re-evaluation on auto-discovery
    connections too.

Parser detail

info get returns its data on a single non-empty body line with no
terminating empty-body line, unlike antenna list and band list
which accumulate multiple lines and end with an empty body. The
m_seqInfo dispatch in processResponse therefore has two parse
paths:

  • A non-empty-body inline parse in the accumulate block, which is
    the normal firmware behavior.
  • A terminator/error fallback in the dispatch block, which handles
    pure error responses (e.g. R|FF|) and defensively handles a
    hypothetical firmware variant that does send a terminator.

Both paths call runInitSequence().

Testing

Tested against a real FlexRadio AU-520 (firmware 4.2.18) with an
Antenna Genius 8x2 (firmware 4.1.16) attached, on Linux (CachyOS).

The PR branch in isolation does not include AG auth credentials,
so a fresh connection from this branch to an AG that requires auth
will fail at the auth stage. The error path was exercised and the
fallback behavior verified:

WRN aether.tuner: AntennaGenius: info get error code 0x ff , keeping
                  heuristic radioPorts= 1
WRN aether.tuner: AntennaGenius: TCP error: "The remote host closed
                  the connection"

No crash, no stall, clean cleanup via onTcpDisconnected. The
ShackSwitch fallback is preserved as designed.

The same logic, with a separate locally-applied auth step that is
not part of this PR, was used to verify the success path against
the real AG over the past several weeks: info get parses
correctly, m_device.radioPorts becomes 2, Port B becomes visible,
antenna list loads cleanly. Building this branch with the auth step
re-applied reproduces that working setup.

Scope

Changes are confined to:

  • src/models/AntennaGeniusModel.h (+2 -0)
  • src/models/AntennaGeniusModel.cpp (+55 -3)
  • src/gui/AntennaGeniusApplet.cpp (+6 -0)

No changes to MainWindow, RadioModel, CMakeLists.txt, or any
maintainer-only path.

Open questions for the maintainer

  1. No timeout for an unresponsive info get. If the device
    accepts the TCP connection, sends a prologue, but then stalls
    without responding to info get, runInitSequence() is never
    called. onTcpDisconnected would eventually clean up, but the
    connection state sits idle until then. I chose not to add a
    QTimer fallback because the failure mode (silent TCP stall after
    a successful prologue) seems unlikely on real AG firmware. Happy
    to add a 5-second timer if you would prefer the defensive
    posture.

  2. Independence from Auth Code / password field for remote TGXL, PGXL and Antenna Genius connections without VPN #2313. This fix is independent of Auth Code / password field for remote TGXL, PGXL and Antenna Genius connections without VPN #2313
    (Auth Code support). It triggers for any manual-IP connection to
    a real AG, with or without auth required. The only coordination
    point is that the post-prologue command order changes from
    <auth?> -> runInitSequence to <auth?> -> info get -> runInitSequence. If Auth Code / password field for remote TGXL, PGXL and Antenna Genius connections without VPN #2313 lands first the merge should be
    trivial; if this PR lands first then Auth Code / password field for remote TGXL, PGXL and Antenna Genius connections without VPN #2313 builds on the new
    structure cleanly.

Principle citation

Most load-bearing principle: Evidence Over Assertion (Principle
VIII). The fix replaces a static-version-based heuristic with the
device's own authoritative response to a documented protocol query.

Related

@jensenpat

Copy link
Copy Markdown
Collaborator

@TnxQSO-Admin Thanks for the contribution! Can you do me a favor on this one can you ask your agent: "Please use the agent automation bridge for testing, and include a test summary in the PR. Include any potential new verbs in the test summary.".

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: changes requested

Nice, well-targeted fix for the #3314 manual-IP parity gap — probing info get and re-evaluating Port B on deviceInfoChanged() is the right shape, and the m_pending.contains(seq) guard correctly prevents a double-init. CI is green. One real bug to fix and one design decision to make before merge; the rest are non-blocking nits.

Blocker (one-liner): m_seqInfo is set once and never reset, but sequence numbers wrap at 255 in sendCommand, so after a long session a command that reuses the info-get seq has its response hijacked as info get and re-runs the whole init sequence mid-session. Inline suggestions add the reset.

Decision needed: runInitSequence() is now fully gated on the info get response. A device that accepts the TCP connection but silently ignores info get (no body, no error terminator) never initializes — there's no init watchdog (the existing QTimers are discovery/reconnect). Error responses are handled; silent non-response isn't. Either add a short init-timeout fallback to runInitSequence(), or confirm every supported AG/ShackSwitch firmware answers info get (even with an error code). Flagged inline.

Non-blocking: the duplicated parse block and the redundant double Port-B update.

Thanks @TnxQSO-Admin — the pcap-backed framing and the #3314 parity write-up made this easy to follow.

Comment thread src/models/AntennaGeniusModel.cpp
Comment thread src/models/AntennaGeniusModel.cpp
Comment thread src/models/AntennaGeniusModel.cpp
Comment thread src/models/AntennaGeniusModel.cpp Outdated
Comment thread src/gui/AntennaGeniusApplet.cpp Outdated

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

Thanks for this, @TnxQSO-Admin — the framing is excellent. Replacing a static version heuristic with the device's own authoritative info get response is the right call (Evidence Over Assertion), the scope is tight (3 files, all in-scope), and the writeup with the pcap + the candid "Open questions" section made this easy to review. The dual parse paths (inline single-line body vs. terminator/error fallback) are a correct read of the protocol's two response shapes.

A few things before merge.

1. [blocker] m_seqInfo is never reset — confirmed. I verified ten9876's finding against the source. sendCommand cycles m_nextSeq through 1..255 and wraps (AntennaGeniusModel.cpp:388-389), and processResponse only gates on m_pending.contains(seq) (:432). m_seqInfo keeps its value for the whole session, so once the counter wraps (~255 commands — keep-alive pings alone get there on a long session) and an unrelated command reuses that seq, both the inline path and the terminator path match seq == m_seqInfo. That re-parses the unrelated body into m_device and fires a spurious full runInitSequence() (re-sending antenna list / band list / port get) mid-session. Clear it once handled, on both paths:

m_seqInfo = -1;  // clear so a wrapped-seq reuse can't re-enter the info-get path
runInitSequence();

2. [recommend] No watchdog for a silent info get non-response. This is the more consequential of your two open questions. Init is now fully gated on the info get reply. The code != 0 terminator path covers an error response, but a device that accepts TCP, sends the prologue, then silently drops info get (no body, no error terminator) never reaches runInitSequence(), and the connection sits connected-but-empty until onTcpDisconnected eventually fires. This isn't purely hypothetical: previously runInitSequence() ran unconditionally right after the prologue, so this changes behavior for every device — including the ShackSwitch hardware the existing version heuristic was written for, where we don't actually know that info get is implemented. I'd take the 5-second single-shot fallback you offered; it's cheap insurance on exactly the manual-IP path this PR targets. (A timer that's cancelled in the response handler and falls back to runInitSequence() on timeout.)

3. [nit] Factor out the duplicated key/value apply. The 6-line kvs.contains(...) block is verbatim in both the inline and terminator paths, so a new field has to be added in two places. A small applyDeviceInfo(const QString& line) helper removes the drift risk. Non-blocking.

On the other Copilot notes: the dangling-pr reference (a reference invalidated by m_pending.remove(seq) but not read afterward) is harmless as written — fine to leave, just don't add code reading pr below it. The double Port B update from both connected() and deviceInfoChanged is also harmless; collapsing it is optional cleanup.

#1 is the one I'd treat as required; #2 is a strong recommendation given it now gates init for all device types.


🤖 aethersdr-agent · cost: $3.0897 · model: claude-opus-4-8

Send `C<seq>|info get` after the TCP prologue and parse the
response (ports, antennas, serial, name, version, mode) into
m_device. The new deviceInfoChanged signal lets AntennaGeniusApplet
re-evaluate Port B visibility once authoritative radioPorts is
known.

This fixes Port B being hidden when connecting to a 2-radio-input
Antenna Genius (e.g. 8x2) via manual IP, where no UDP discovery
beacon ever arrives and the existing ShackSwitch-version heuristic
falls back to radioPorts=1.

The ShackSwitch fallback at the prologue stage is preserved as a
baseline; info get overrides it when the device responds. The
beacon path in onDiscoveryDatagram also emits deviceInfoChanged
so late-arriving beacons re-trigger visibility checks.

runInitSequence() is deferred to the info get response handler so
radioPorts is correct before any antenna/band list arrives. Both
the inline body-parse path and the terminator/error fallback
path call runInitSequence(), so the connection cannot stall as
long as the device responds with anything.

Fixes aethersdr#3314
@TnxQSO-Admin TnxQSO-Admin force-pushed the pr/3314-port-b-info-get branch from 1c35b0c to 4325f4f Compare June 30, 2026 07:05
@TnxQSO-Admin

Copy link
Copy Markdown
Contributor Author

@jensenpat Thanks for the feedback. Working on the automation
bridge test summary now -- will post in a follow-up comment.
Force-pushed an updated commit addressing @ten9876's review:
m_seqInfo reset on both paths, 5-second init watchdog, helper
extracted, vestigial Port B update removed.

@TnxQSO-Admin

Copy link
Copy Markdown
Contributor Author

Automation Bridge Test Summary

Per @jensenpat's request, I inventoried the bridge against this PR's
test goals. Honest finding: the bridge in its current form cannot
meaningfully exercise what this PR changes, because the PR operates
at the protocol layer (info get send/parse, signal emission, init
sequencing) and the bridge is designed for widget-level interaction.
Below are the goals I considered, the gap analysis, and proposed
new verbs.

Test goals considered

Goal Achievable today?
(a) Verify Port B section is initially hidden when no AG connected Partial - see below
(b) Verify deviceInfoChanged -> Port B becomes visible No
(c) Verify info get protocol flow (send, parse, emit) end-to-end No - belongs in QTest, not bridge
(d) Verify init watchdog fires after 5s on silent non-response No - requires mock device

Detail

Goal (a) - initial visibility. Achievable with dumpTree, but
m_portBSection has no setObjectName/setAccessibleName call, so
addressing it requires either parsing structural position (fragile)
or adding an annotation. I considered adding
m_portBSection->setObjectName("agPortBSection") in this PR but
chose not to, since per docs/a11y.md the right path is
setAccessibleName and that belongs in an a11y-focused change, not
folded into a protocol fix. Happy to add it here if you would
prefer; one line.

Goal (b) - signal-driven re-render. Not achievable today.
deviceInfoChanged is emitted by model code in response to either
a TCP info get response or a UDP beacon. The bridge has no verb
to fire a named signal on a model, and AntennaGeniusModel is not
exposed via the get model registry, so even reading the post-state
radioPorts is not possible.

Goal (c) - protocol flow. This is the right shape for a QTest
unit test against a mock QTcpSocket, not for the bridge. The
bridge sits above the model and sees only resulting widget state;
it cannot observe wire bytes or signal emission.

Goal (d) - watchdog timing. The watchdog is added by this PR
(force-pushed earlier today as 4325f4f). Verifying it fires
correctly on a silent non-response requires a mock AG that completes
the TCP prologue but withholds the info get response. That mock
does not exist in the codebase today.

What this PR was actually tested with

  • Real FlexRadio AU-520 (fw 4.2.18) + real Antenna Genius 8x2 (fw
    4.1.16), on Linux (CachyOS), with a separately-applied local auth
    step that is not part of this PR.
  • Success path: info get parses, m_device.radioPorts becomes 2,
    deviceInfoChanged fires, Port B section becomes visible, antenna
    list loads. Watchdog stops before firing.
  • Error path: real AG returns code 0xff without auth, terminator
    handler logs the warning, watchdog stops, runInitSequence()
    runs, fallback radioPorts=1 heuristic remains. No crash, no
    stall.
  • Silent-stall path (goal d): not tested empirically. Verified only
    by code-path inspection. A mock would be needed for empirical
    coverage.

Proposed new verbs

Two verbs would close the gaps for goals (a) and (b), enabling
genuine offline bridge testing of AG-applet behavior:

Verb 1: get antennaGenius - parallel to get radio.

Request:  {"cmd":"get","model":"antennaGenius"}
Response: {"ok":true, "model":"antennaGenius",
           "connected":true,
           "name":"Antenna Genius Uno Q",
           "serial":"AG-00042",
           "version":"4.0.22",
           "radioPorts":2,
           "antennaPorts":6,
           "mode":"normal"}

Estimated cost: ~40 LOC in AutomationServer.cpp, same pattern as
radioSnapshot(). No model changes required - reads existing public
accessors on AntennaGeniusModel.

Verb 2: inject antennaGenius deviceInfo - synthesize an
info get response into the model without a real device.

Request:  {"cmd":"inject","target":"antennaGenius",
           "action":"deviceInfo",
           "value":"ports=2 antennas=6 serial=AG-TEST"}
Response: {"ok":true,"inject":"antennaGenius",
           "radioPorts":2,"signalEmitted":"deviceInfoChanged"}

The handler would call the existing parseKeyValues path, update
m_device, and emit deviceInfoChanged - exactly what a real
response does. Estimated cost: ~30 LOC plus a public
AntennaGeniusModel::applyDeviceInfoForTest(const QString&) entry
point (the applyDeviceInfo helper introduced in this PR at
AntennaGeniusModel.cpp:425 is already the right shape - the test
entry point would just be a public wrapper that also emits the
signal).

Both verbs would make goal (a) trivially testable and goal (b)
testable in a fully offline / CI-friendly way:

# b: signal-driven re-render, no hardware
assert get("antennaGenius")["radioPorts"] == 1
assert dumpTree.find(objectName="agPortBSection")["visible"] == False

inject("antennaGenius", "deviceInfo", "ports=2 antennas=6")

assert get("antennaGenius")["radioPorts"] == 2
assert dumpTree.find(objectName="agPortBSection")["visible"] == True

If these verbs are of interest I am happy to follow up with a
separate PR that adds them, plus the objectName annotation on
m_portBSection (or setAccessibleName if that is preferred per
docs/a11y.md). I would scope it as a small, focused bridge-only PR
rather than expanding this one.

Goals (c) and (d) remain unit-test territory and outside the
bridge's scope.

@TnxQSO-Admin TnxQSO-Admin requested a review from ten9876 June 30, 2026 08:02

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 4325f4f5 — every point from the prior review is addressed, cleanly. This supersedes my earlier changes-requested.

  • m_seqInfo wrap-hijack (blocker) — now reset to -1 in both processResponse paths, so a wrapped sequence number can't re-enter the info-get handler. ✓
  • Init gated on info get with no fallback (the decision) — resolved the right way: a 5 s single-shot m_initWatchdog, started when info get is sent and stopped the moment it answers (body or error), falls back to runInitSequence() if the device never responds. Traced the paths — exactly one runInitSequence() fires in every case (response-first stops the timer; watchdog-first resets m_seqInfo so a late response is ignored), and the timer is parented for cleanup. ✓
  • Dedup — the parse block is now one applyDeviceInfo() helper used by both paths. ✓
  • Redundant Port-B update — the connected()-handler setVisible is gone; deviceInfoChanged is the single authority. ✓

Nicely done, and the watchdog is a better answer than my suggestion since it also recovers a genuinely-stalled device. CI green on all six checks. Approving — good to merge.

Thanks @TnxQSO-Admin.

@ten9876 ten9876 merged commit d760c5e into aethersdr:main Jun 30, 2026
6 checks passed
@TnxQSO-Admin TnxQSO-Admin deleted the pr/3314-port-b-info-get branch July 1, 2026 05:39
ten9876 added a commit that referenced this pull request Jul 2, 2026
#3961)

## Release documentation prep for **v26.7.1** (2026-07-02)

29 commits since v26.6.5. Docs-only + version bump — no code behavior
change.

### Version bump 26.6.5 → 26.7.1
The three canonical locations (per `AGENTS.md`):
- `CMakeLists.txt:2` — `project(AetherSDR VERSION 26.7.1)` →
`AETHERSDR_VERSION` (the app's version string)
- `README.md` — Current version line
- `AGENTS.md` — Current version line

### `CHANGELOG.md`
Promoted `[Unreleased]` → **`## [v26.7.1] — 2026-07-02`** in house style
(v-prefix, em-dash, "N commits since…" opener + headline), authored from
all merged PRs and categorized:
- **Added:** 3D stacked-trace spectrum (#3899), 3D dBm scale (#3937),
in-process NVIDIA BNR + AFX download-on-demand (#3902), TX meter
mouse-over readouts (#3936), SWR manual sweep range (#3885), CW sidetone
capture (#3895), KiwiSDR metadata scaffolding (#3898), bridge verbs
(#3920)
- **Changed:** BNR container/NIM backend removed, 60 fps + per-pixel GPU
FFT trace (#3958), FlexLib-sourced model capabilities (#3954/#2177), RX
speaker-latency cut (#3897), Display pane sections (#3935)
- **Fixed:** #3922, #3926, #3941, #3940, #3942, #3921, #3903, #3892,
#3924, #3891, #3890, #3900, #3947
- Fresh empty `[Unreleased]` restored above the release block.
Internal/CI/docs/self-fix PRs (#3931/#3916/#3934/#3929) omitted per
house style.

### `ROADMAP.md`
- Cycle header → "post-v26.7.1"
- NVIDIA BNR removed from **In flight** (shipped this cycle)
- Five v26.7.1 highlights prepended to **Recently shipped**

### `README.md`
- Highlights: GPU spectrum bullet now notes the **per-pixel FFT trace at
up to 60 fps** and the **3D stacked-trace** spectrum mode

### Reviewer notes
- Touches protected paths (`*.md` + `AGENTS.md` Tier-1 +
`CMakeLists.txt`) — needs `@aethersdr/infrastructure` sign-off.
- Tagging `v26.7.1` and cutting the release build are **not** part of
this PR (docs prep only).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <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.

Antenna Genius Port B section is hidden when connecting via manual IP

3 participants