feat(ag): probe device with info get to fix Port B visibility#3900
Conversation
|
@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
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
1c35b0c to
4325f4f
Compare
|
@jensenpat Thanks for the feedback. Working on the automation |
Automation Bridge Test SummaryPer @jensenpat's request, I inventoried the bridge against this PR's Test goals considered
DetailGoal (a) - initial visibility. Achievable with Goal (b) - signal-driven re-render. Not achievable today. Goal (c) - protocol flow. This is the right shape for a QTest Goal (d) - watchdog timing. The watchdog is added by this PR What this PR was actually tested with
Proposed new verbsTwo verbs would close the gaps for goals (a) and (b), enabling Verb 1: Estimated cost: ~40 LOC in Verb 2: The handler would call the existing Both verbs would make goal (a) trivially testable and goal (b) If these verbs are of interest I am happy to follow up with a Goals (c) and (d) remain unit-test territory and outside the |
ten9876
left a comment
There was a problem hiding this comment.
Re-reviewed at 4325f4f5 — every point from the prior review is addressed, cleanly. This supersedes my earlier changes-requested.
m_seqInfowrap-hijack (blocker) — now reset to-1in bothprocessResponsepaths, so a wrapped sequence number can't re-enter the info-get handler. ✓- Init gated on
info getwith no fallback (the decision) — resolved the right way: a 5 s single-shotm_initWatchdog, started wheninfo getis sent and stopped the moment it answers (body or error), falls back torunInitSequence()if the device never responds. Traced the paths — exactly onerunInitSequence()fires in every case (response-first stops the timer; watchdog-first resetsm_seqInfoso 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()-handlersetVisibleis gone;deviceInfoChangedis 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.
#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>
Summary
Sends
C<seq>|info getafter the TCP prologue to retrieve theauthoritative
ports=andantennas=fields from a real AntennaGenius, and parses the response into
m_device. A newdeviceInfoChanged()signal letsAntennaGeniusAppletre-evaluatePort 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.radioPortsdefaulted to 1 via the ShackSwitch-versionheuristic. 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.cpphas a ShackSwitch-specific fallback thatfires for every device when no UDP beacon has arrived:
For a real AG (firmware version like 4.1.16), neither branch matches
and
radioPortsdefaults to 1. Manual-IP connections never receivea UDP beacon, so this fallback is the sole source of
radioPortsfor them.
AntennaGeniusAppletthen hides Port B at:A pcap of 4O3A's own Antenna Genius Utility connecting to the same
AG shows it issues
info getimmediately after the prologue. TheAG responds on a single line:
Documented at https://github.com/4o3a/genius-api-docs/wiki/Antenna-Genius-API
What this PR does
C<seq>|info getimmediately after the prologue is accepted.parseKeyValues()helper and updates
m_device.radioPorts,m_device.antennaPorts,m_device.serial,m_device.name,m_device.version,m_device.mode.deviceInfoChanged()signal soAntennaGeniusAppletre-evaluates Port B visibility (which was previously only set
inside the
connected()handler, beforeinfo getwould haveresponded).
runInitSequence()from theinfo getresponse handlerin 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.
info getoverrides when available.deviceInfoChanged()fromonDiscoveryDatagram()after a beacon updates
radioPorts/antennaPorts, so late-arriving beacons re-trigger Port B re-evaluation on auto-discovery
connections too.
Parser detail
info getreturns its data on a single non-empty body line with noterminating empty-body line, unlike
antenna listandband listwhich accumulate multiple lines and end with an empty body. The
m_seqInfo dispatch in
processResponsetherefore has two parsepaths:
the normal firmware behavior.
pure error responses (e.g.
R|FF|) and defensively handles ahypothetical 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:
No crash, no stall, clean cleanup via
onTcpDisconnected. TheShackSwitch 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 getparsescorrectly,
m_device.radioPortsbecomes 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 anymaintainer-only path.
Open questions for the maintainer
No timeout for an unresponsive
info get. If the deviceaccepts the TCP connection, sends a prologue, but then stalls
without responding to
info get,runInitSequence()is nevercalled.
onTcpDisconnectedwould eventually clean up, but theconnection 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.
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?> -> runInitSequenceto<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 betrivial; 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
https://github.com/4o3a/genius-api-docs/wiki/Antenna-Genius-API
claude-activelabel that should be cleared onmerge or earlier; AetherClaude did not produce a draft for this
issue.