Repository navigation
fix(pgxl): read Id from the radio's ID meter. Principle VIII. - #6033
Conversation
…reading on radio fallback. Principle VIII. The PGXL reports the PA heatsink and Harmonic Load heatsink temperatures (PowerGeniusXL User Guide v3.9.8, p. 55). Rename the setters and members after them and cite the manual in place of the old guess. A FlexRadio relays only the PA heatsink temperature, as the radio's TEMP meter. Stop reading HL keys on the relay path, and clear HL when the direct connection drops so a frozen value is not shown as live. Verified on a FLEX-8600 (SmartSDR 4.2.20.41343) with a PowerGeniusXL on firmware 3.9.8. Fixes aethersdr#6029 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…. Principle VIII. The Harmonic Load heatsink temperature comes only over the direct connection. Guard its setter like Vdd and Vac, so a write that arrives after the connection drops cannot put a stale value back on screen, and test it. Label the packed "PA/HL" form of `temp` and the `tempb` name as never seen in a capture. Firmware 3.8.9 and 3.9.8 both send separate `temp` and `hltemp` keys. Addresses the triage on aethersdr#6029. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a FLEX-8600 (SmartSDR 4.2.20.41343) with a PGXL on firmware 3.9.8, the radio's amplifier status carries only ip, model, serial_num, ant and state. The PGXL readings it relays arrive as meters: FWD, RL, DRV, ID and TEMP. Decode ID, feed drain current and PA heatsink temperature from the radio's meters while fresh, and remove the amplifier-status handler, whose keys never arrive. Measured rates and method are in docs/pgxl-telemetry-source-evidence.md. Fixes aethersdr#6032 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Firmware 3.9.8 with authorization enabled returns the code in `setup read` and refuses any `setup` that carries `authcode=<code>` with 50000013, so every fan-mode change and MEffA toggle was refused. Probed on the hardware: the same group without `authcode` is accepted and the stored code is unchanged, as is `setup fanmode=` alone. With no code set the key is still sent empty, as the vendor utility does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
1. Issue fit
Yes, for #6032, and the diff also carries #6029's fix (the first two commits are PR #6030's). Mapping the issue's three asks:
| Ask | Where |
|---|---|
Id must move on RADIO, from the radio's ID meter |
MeterModel.cpp:118,240 (route, handle-matched), :1187-1190 (sample), ampVitalsChanged → MainWindow_Wiring.cpp:7155 |
| Read each reading from where the radio actually sends it; stop waiting on status keys that never arrive | the whole AmpModel::telemetryUpdated reader is deleted from MainWindow_Wiring.cpp |
| Where both sources exist, use the faster one | setDrainCurrent/setPaHeatsinkTemp drop-while-fresh, kRelayMeterFreshnessMs |
I checked the one thing the deletion could silently cost: meffa. AmpModel::telemetryUpdated is emitted only from the radio-status path (AmpModel.cpp:371), the deleted handler was its only GUI consumer, and setMeff had no other caller. So on a relay-only station MEffA now goes through applyMeffaControls's known = !m_meffaState.isEmpty() gate and the control is hidden — which is exactly what already happened, because the radio never sent meffa for it to display. The deleted comment claiming "this is the only place the state appears at all" was describing a path that never fired. Clean removal, and the evidence doc covers it. AmpModel::telemetryUpdated is now GUI-dead (still exercised by amp_model_test) — worth noting, not worth changing here.
One process item, not a code problem: PR #6030 is still open carrying commits 029d7e44 and a9a635d6 verbatim. This PR supersedes it. Please say so in the body / close #6030 so the two don't both land.
2. Scope
| File(s) | Change | Claimed? | Verdict |
|---|---|---|---|
src/models/MeterModel.{h,cpp} |
decode AMP ID, new ampVitalsChanged, m_hasAmpTempValue |
yes (#6032) | In scope |
src/gui/MainWindow_Wiring.cpp |
wire ampVitalsChanged; delete the whole amplifier-status telemetry reader |
yes (#6032 names id/temp/vdd/vac/meffa as the wrong source) |
In scope |
src/gui/AmpApplet.{h,cpp} |
PA/HL rename, HL cleared on relay, radio-vs-device arbitration for Id and PA temp | #6029 + #6032 | In scope, but arrives from #6030 — disclose |
docs/pgxl-telemetry-source-evidence.md |
new evidence doc | Principle VIII | In scope |
tools/probe_pgxl_telemetry_sources.py |
new read-only capture tool, referenced from the doc | Principle VIII | In scope. Not dead — the doc links it, and it is a dev tool, not a build target |
tests/amp_applet_test.cpp, tests/meter_model_test.cpp |
coverage | yes | In scope |
No CHANGELOG.md edit. No new settings key, capability, protocol verb or public wire surface. No socket test added, modified or removed — I read the added test bodies in /tmp/aetherclaude/pr-6033, not the working tree, and neither touches QTcpServer/QUdpSocket/bind/connectToHost or a fake peer. No preference smuggled in: every user-visible change (PA/HL labels, HL cleared on relay, tooltip and accessible-name wording) traces to #6029's citation of the PowerGeniusXL User Guide v3.9.8 p. 55 or to the captured meter manifest. The PR body already flags the user-visible text for maintainer review, which is the right call.
3. Blockers
None. Nothing here breaks users, violates canon or fails on current main.
4. Nits (all non-blocking)
MeterModel.cpp:240—TEMPis left name-matched whileIDgains handle-matching. The newIDbranch cites DRV's rationale ("which device is this meter about … the day one appears"), butTEMP, the other half of the signal this PR introduces, still has no handle guard in eitherdefineMeterorsetTgxlHandle. I found no evidence a TGXL publishesTEMP— the capture in the doc lists only the PGXL's five meters — so this is hypothetical today. But it now costs more than it did: becausesetRadioPaHeatsinkTempholds the PGXL off forkRelayMeterFreshnessMs, a misrouted tunerTEMPwould not merely show the wrong number, it would suppress the PGXL's correct PA reading on a DIRECT station. Reasoned from code, not reproduced. Oneifwould close it.amp_applet_test.cpp:393— a withdrawnIDmeter leaves the last drain current on the gauge, and the test pins that. It is the same shape as the stale-HL defect this PR fixes, andsetDrivePower(w, valid)next door hides its whole row onvalid=false. It is probably invisible in practice (theIDmeter goes away when the amplifier does, andapplyMeffaControls-style visibility gating takes the panel with it) — but the asymmetry is deliberate enough to be worth a sentence of justification in the comment, since a reader comparing it tosetDrivePowerwill assume one of the two is wrong.AmpApplet.cpp:1213— the tooltip explainsHLeven when no HL value is on the button. On a RADIO-only station the button readsPA 47.9 Cand the tooltip still defines a label that is not there.AmpApplet.cpp:413—setAccessibleDescriptionis not wrapped intr(). Pre-existing on that line and widespread in the tree, butdocs/a11y.md:62,73shows both accessor examples insidetr(), and you are already editing the string. The accessible name three hundred lines down is translated; the description is not.- The header comment now reads "the device's own 2 to 11 times [a second]", sourced from the probe polling the PGXL at 50 ms — twice the app's own transmit poll rate. The real device-path rate is lower, so the conclusion ("the relay wins") only gets stronger; the doc discloses the 50 ms, the header does not.
5. What I tried to break
- The claim that the radio relays no PGXL telemetry in amplifier status. I traced
AmpModel::telemetryUpdatedto its single emit site (AmpModel.cpp:371, theappliesToAmpblock) and confirmed the deleted handler was its only GUI consumer andsetMeffits only caller — so nothing else silently depended on the deleted reads. I then checked the one field whose loss would be user-visible,meffa, throughapplyMeffaControls'sknowngate: hidden before, hidden after. No behavior lost. - Unit and scaling.
convertRaw(MeterModel.cpp:456) dividesAmpsby 256 anddegCby 64, citing FlexLibMeter.cs— matching the raw values the tests construct and the doc's table. 70 A hi againstqint16/256 = 127.99 A ceiling, no overflow. - The new test's index collisions.
defineAmpManifestuses 12,13,14,16,17,18; the newIDtakes 15, sodefineMeter's ID-reuse purge never fires and index 16 really isTEMP. Both new meter tests fail to compile against unfixed code (the signal does not exist), so neither is self-asserting. testReadoutWidthIsStable. I suspected the newm_directConnectedgate onsetHarmonicLoadHeatsinkTemphad silently turned the pair-width assertion into a single-sensor one. It has not —setDirectConnected(true)is at the top of the test (line 598), so the pair is still exercised. Withdrawn.- Partial-manifest and withdrawal paths.
ampVitalsChangedcarries both readings on every sample of either, so a station with onlyTEMPinvalidates the drain-current timer each sample (PGXL owns Id) and vice versa;removeMeterclears index, value and flag together for both, and emits.setTgxlHandle's rescan clearsm_ampDrainCurrent/its flag but notm_ampTemp— correct, becauseTEMPis unconditionally re-found in the same loop whileIDmay not be.clear()does not emitampVitalsChanged, which costs at most one 1500 ms window before the PGXL resumes. - Late writes and ordering.
setHarmonicLoadHeatsinkTemp's!m_directConnectedearly return follows thesetDrainVoltage/setMainsVoltageprecedent exactly, andsetDirectConnected(false)arrives fromPgxlConnection::disconnectedbefore any late status can land. RepeatedsetDirectConnected(false)is idempotent. - Couldn't check: anything at runtime. I have no build, no test run and no way to drive the GUI, so every statement above is read off the code in the PR head checkout, not reproduced. CI is green on
ec0d432a(build, check-macos, check-windows, Static checks, Sanitizer configure) but itscteststeps are-R-filtered, so green does not tell meamp_applet_testormeter_model_testran. The hardware claims — the FLEX-8600 capture, the refresh-rate table, the in-app DIRECT/RADIO verification — I cannot test at all; the probe script and the doc make them auditable by someone who can, which is the right substitute.
6. Recommendation
Approve with nits. The framing is right: the previous code read PGXL telemetry from a status message that never carries it, and this reads it from the meter the radio actually publishes. The arbitration rule is stated once, applied identically to both shared readings, and backed by a measurement rather than an assertion — which is the part I most expected to find hand-waved and did not. Nothing I found rises above a comment or a defensive if. Next step for the maintainer: rule on the tooltip/accessible-name wording (the author flagged it), and close or retarget #6030 so its two commits don't land twice.
🤖 aethersdr-agent · cost: $9.7106 · model: claude-opus-5
… Principle VIII. The radio's amplifier status carries no temperature at all, so remove the last `temp` read from the relay handler and its field list; the PA heatsink temperature arrives as the AMP TEMP meter. Name HL in the tooltip only while an HL value is showing, wrap the accessible description in tr(), and speak the placeholder as "not reported" instead of a dash. Addresses the review on this PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Principle VIII. The radio's amplifier status carries no temperature at all, so remove the last `temp` read from the relay handler and its field list; the PA heatsink temperature arrives as the AMP TEMP meter. Name HL in the tooltip only while an HL value is showing, wrap the accessible description in tr(), and speak the placeholder as "not reported" instead of a dash. Addresses the review on this PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ciple VIII. Match the AMP TEMP meter by handle, as ID and DRV are: a tuner's TEMP on the amplifier would now also hold off the PGXL's own PA reading. Say why a withdrawn ID meter leaves the last value while drive hides its row, and that the device's refresh rate was measured polling faster than the app. Addresses the review on this PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed 590a798, plus e21d21a (#6030's latest commit, cherry-picked), covering the review:
On #6030: this PR is stacked on it, not a replacement, so #6030 should stay open and land first. Its commits appear here only until then; after it merges this branch is rebased onto
|
|
Verified on a FLEX-8600 (SmartSDR 4.2.20.41343) with a PGXL on firmware 3.9.8, both during a TUNE. 1. Relayed through the FlexRadio (RADIO). Id reads 19 A and follows the transmit, from the radio's
2. Direct connection (DIRECT), built together with #6008 (this PGXL firmware needs #6008's authentication for a direct connection). Id 19 A, Vdd 52.0 V, Vac 117 V and both heatsink temperatures; Id and PA temperature did not flicker between the two sources. The clipped buttons in this shot are the separate docked-layout bug #5903, fixed in #6036.
|
aethersdr#6033 deletes AmpApplet::setMeff next to the source-indicator functions aethersdr#6008 adds; keep those and drop setMeff, as this branch does. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This branch carried aethersdr#6030's last commit as a cherry-pick (e21d21a), which conflicted with the real one. Merging aethersdr#6030 makes it an ancestor; the tree is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Keep the two-readout layout and drop the single-button tooltip checks it replaces. The PGXL's own PA reading keeps the grey-to-live style change when it is the first reading, as the radio's reading does. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…inciple XI. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> # Conflicts: # tests/amp_applet_test.cpp
…rinciple VII. A saved authorization code was bound to the connected peer IP and port, so an operator reaching a device through a DDNS name had to retype it every time their residential IP changed. That is the main remote use case of aethersdr#2313. Bind to the lowercased hostname and port when the operator entered a name, and keep the connected peer IP and port when they entered a literal address. Nothing binds before a socket has a real peer address. A different name, port or a literal IP is a different identity, so a code saved for one is not offered to another. The cost is that whatever the name resolves to receives the saved code, which the header and the evidence doc now say. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…inciple VII. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…) into the docked layout. Principle XI. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ethersdr#6030's squash. Principle VIII. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…relayed-status-fields
…Principle XI. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…Principle XI. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts: # tests/tests.cmake
…rge. Principle XI. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ields # Conflicts: # tests/tests.cmake
…relayed-status-fields
ten9876
left a comment
There was a problem hiding this comment.
Issue fit
Yes. #6032 asks for three things, and each maps to the diff:
| #6032 asks | Where in the diff |
|---|---|
Id moves on RADIO, from the radio's ID meter |
MeterModel decodes AMP/ID, matched by handle, and emits ampVitalsChanged |
| Stop waiting on amplifier-status keys that never arrive | the relay handler is deleted from MainWindow_Wiring.cpp |
| Use the faster source where both exist | setRadioDrainCurrent / setRadioPaHeatsinkTemp with the 1500 ms freshness rule |
FlexLib confirms the premise: Amplifier.StatusUpdate parses only model, serial_num, ant, state and ip (reference/FlexLib_*/Amplifier.cs:236-300). ID conversion matches Meter.cs (Amps ÷ 256). The previous review's nits are addressed in 590a798e:
TEMPis now handle-routed at both sites.- The withdrawn-
IDbehaviour is commented. - The 50 ms probe rate is disclosed.
Reviewed as the increment over #6008 (git diff 71d5bcd7 fccf8e47, 9 files).
Scope
| File | Change | Claimed? | Verdict |
|---|---|---|---|
src/models/MeterModel.{h,cpp} |
decode ID; handle-route ID and TEMP; ampVitalsChanged |
yes | in scope |
src/gui/MainWindow_Wiring.cpp |
wire ampVitalsChanged; delete the dead amplifier-status reader |
yes | in scope |
src/gui/AmpApplet.{h,cpp} |
radio-vs-device arbitration for Id and PA temp; setMeff removed |
yes | in scope |
docs/pgxl-telemetry-source-evidence.md, tools/probe_pgxl_telemetry_sources.py |
evidence + read-only probe | yes | in scope |
tests/amp_applet_test.cpp, tests/meter_model_test.cpp |
coverage | yes | in scope |
Everything in the increment is explained by the issue. CHANGELOG.md is untouched. The probe sends only client udpport, sub amplifier all, sub meter all, auth code= and status. That is no TX and no state change (Principle VI), and it never logs the code. It opens UDP/TCP to a radio but is a dev tool, not a registered test.
Blockers
None. The branch now conflicts with main in AmpApplet.cpp, MainWindow_Wiring.cpp, TunerApplet.h and AmpModel.cpp, three of them inherited from #6008 (see #6008's review for the AmpModel.cpp trap). The planned post-#6008 rebase resolves that.
Nits (non-blocking)
- N1. Each reading's freshness is renewed by the other reading's samples.
MeterModelemitsampVitalsChanged(id, idValid, temp, tempValid)when either meter lands (MeterModel.cpp:1196-1200,:1280-1281). The lambda atMainWindow_Wiring.cpp:7301calls bothsetRadioDrainCurrentandsetRadioPaHeatsinkTempevery time.- So an
IDpacket restampsTEMP's freshness and vice versa. If the radio ever stopped sending one meter while the other continued, the applet would hold a stale relayed value and drop the PGXL's live one indefinitely. - This is the class of problem #4805 fixed for FWD. Low risk today, since both run at about 20/s per your capture. A per-reading "updated in this packet" flag would close it.
- N2. No test covers the real boot order for
ID/TEMP. The order is manifest before the tuner handle, then thesetTgxlHandlerescan.testAmpAndTunerMetersSplitByHandleAfterALateHandlecovers FWD/RL only, and the new tuner tests all set the handle first. Adding a tunerID/TEMPto that test's manifest would pin the rescan. - N3. The routing is "not the tuner", not "is this amplifier", so with two amplifiers the last-defined
ID/TEMPwins. FlexLib matches positively (Radio.csFindMetersByAmplifiercomparesSourceIndexwith the amp handle). This pattern predates the PR; the PR extends it.
What I verified vs. read
- Built #6036's head
f5eafa06, which contains this branch unchanged (#6036's increment touches onlyAmpApplet.*and two tests), and ranmeter_model_test,amp_applet_test,amp_model_test,health_applet_testandpgxl_panel_test: all pass.pgxl_panel_testbinds a loopback stub PGXL; the operator approved it for this review. - Mutation: with the
TEMPhandle guard removed at both sites (MeterModel.cpp:121,:247),meter_model_testfails. The new test pins the guard. - Gates: engine-boundary, command-plane and capability-record checks are identical to #6008's, and the manifest check passes.
- Attacks that held (reasoned from code, read-only second pass):
- Threads. The meter path is unchanged: the network thread queues to
RadioModelon main, soampVitalsChangedadds no new thread crossing. - No TGXL handle (0). All AMP meters go to the amp, and the rescan corrects this once the tuner reports.
- Lost behaviour.
statehandling still lives inAmpModel, andsetMeffand the deleted reader had no other consumers. - Merge
d473b183. It keeps #6008's four functions and drops only the claimed code.
- Threads. The meter path is unchanged: the network thread queues to
- Could not drive it: the demo backend advertises
hasAmplifier = false, so the relay/direct switching is evidenced only by your FLEX-8600 + PGXL 3.9.8 captures and screenshots, not reproduced here. - CI is stale: the earliest run was created 2026-10-01 22:26Z, before main's 2026-10-02 19:07Z tip, and the touched tests aren't in a
ci.yml -Rfilter.
…ciple XI. ampVitalsChanged now says which reading arrived in the packet, so the wiring restamps only that reading and a TEMP that stopped arriving is not kept fresh by a live ID. Adds a meter_model_test case where only ID arrives and pins the tuner ID/TEMP rescan after a late handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…. Principle XI. setAuthCodeForAttempt passes blockReconnect=false when the credential store is unavailable, in the TGXL, PGXL and Antenna Genius paths. The handshake test asserts the reconnect timer stays allowed after an "unavailable" failure and still stops for an empty code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y. Principle XI. AmpModel tracks whether setup read contained the authcode key and appends authcode= only when it was present and empty. The setup comment states the firmware fact and version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… change. Principle XI. - Add the firmware 3.9.8 R4|50000013| probe result to the evidence doc. - Log the PGXL status key/values with authcode removed. - Move the authcode-omission assertion into the socket-free handshake test through a command-writer seam on PgxlConnection. - When a connect to a saved manual TGXL/PGXL address fails at the socket level, retry once at the radio-reported address on the default port; no retry when auth is pending or blocked. The pure address choice is unit tested and the MainWindow wiring has a manual verification section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review. Pushed
The merge conflicts with #6008's side in |
|
@ten9876 the updates from your review are pushed ( |
… Principle VII. The radio-reported address is now tried as an alternate attempt: the reconnect timer keeps aiming at the saved manual host, and a missing saved code for the alternate address fails that try without blocking reconnects. unreachable() is sent only for automatic attempts (discovery, presence, startup and their reconnects) whose socket never connected, and never after a deliberate disconnect, so an explicit Setup -> Connect is not redirected. The wiring receives it queued, after the failed socket's error handling. The setup-write comment states the current behaviour in the condensed form. peripheral_auth_handshake_test pins each guard; removing any of them fails it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
RadioModel passes the power amplifier's handle to MeterModel. Once it is known, an AMP meter reaches the PGXL only when its source index matches it, as FlexLib's Radio.FindMetersByAmplifier does; until then any AMP meter that is not the tuner's goes to the PGXL, as before. A handle change re-routes meters already defined and withdraws Id and PA temperature until the next sample, so the applet falls back to the PGXL's own readings meanwhile. The fan-mode/OPERATE wiring comment is back to its condensed form. meter_model_test and the new radiomodel_amp_meter_routing_test fail with the handle match, the RadioModel call or the withdrawal removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cked layout. Principle XI.
|
Follow-up on the review at
The 14 focused targets pass. The four static gates show the same findings as |
… connections (#6008) **Merge order for the PGXL / 4O3A PRs** (the same map is in each of them; #6030 has merged): ``` Stage 1: #6008 authenticate TGXL, PGXL and Antenna Genius Stage 2 (either order, after what each needs): #6027 Peripherals settings UI needs #6008 #6033 PGXL drain current (ID meter) needs #6008 Stage 3: #6036 PGXL docked layout needs #6008 and #6033 ``` ## Summary Fixes #2313. Authenticate TGXL, PGXL, and Antenna Genius direct TCP sessions when a device's greeting requests `AUTH`; keep ordinary commands gated until the device accepts the code. Use the existing manual host/port fields in **Setup → Peripherals** and restore distinct, endpoint-bound codes from QtKeychain on reconnect. Builds without QtKeychain retain a code for the current session only. Keep TGXL/PGXL radio-relay fallback available when the direct path cannot authenticate, and show the actual connection path in the accessory applets. Target `main` at `v26.9.5`. This branch started at the preceding commit; the sole overlapping release file is `CMakeLists.txt`, and this branch's CMake change applies cleanly to the release version. `docs/PR-WORKFLOW.md` does not require a rebase for a stale PR branch. The auth command and success-reply forms are device-specific. A live probe confirmed TGXL and PGXL forms on the firmware listed in `docs/4o3a-remote-auth-hardware-evidence.md`; the AG syntax follows 4O3A's public API documentation and uses CRLF, which the contributor capture confirms reaches the AG auth parser. New code is saved only after an AUTH success reply. A code typed for another target, rejected, or never challenged cannot silently replace a working saved code. ### Implementation details - Gate the TGXL, PGXL, and AG initialization/status paths on AUTH completion; reject malformed/oversized lines and invalid code characters before constructing a line-oriented command. - Keep one Keychain record per device type with the accepted IP/port and code together. Never write a new plaintext password field into AppSettings; retain the existing manual IP/port settings. A Keychain read failure stays visible, and a failed Clear operation does not claim the code was deleted. - Bound authentication attempts and suppress automatic retries after a blocking failure. Manual AG/ShackSwitch retries reset only the selected target's failure budget. An attempt token ignores late Keychain replies from an abandoned connection. - Redact credentials from PGXL setup logging, sensitive automation text extraction, and UI grabs. Register the new auth logging category and add focused socket-free credential, handshake, automation, and applet coverage. - Regenerate the aetherd touchpoint manifest for the new peripheral validation header; add the credential-store source to the desktop-support target. The new GUI→engine validator touchpoint keeps one input rule in the Keychain adapter and TCP clients before a line-oriented command is built. The manifest's AppSettings includer count grows by one for the session-only credential fallback in the Keychain adapter. ThemeManager grows by one because the extracted dialog connect-flow header uses the same token-based error style as the dialog; that header exposes a socket-free regression seam for target-switch ordering. - Add `tools/probe_4o3a_auth.py`, a permanent one-shot, non-TX hardware handshake probe; it prompts for the code without echo and records no credentials. ## Constitution principle honored - **Principle IV — Every Contribution Is Clean-Room:** the TGXL and PGXL handshake code is grounded in a redacted live probe and published 4O3A documents; AG follows the published 4O3A API plus the contributor capture cited below. No proprietary binary was disassembled or translated. - **Principle V — Each Feature Owns Its Configuration As A Single Object:** no new flat AppSettings credential writes were added. Existing manual connection settings remain as they were; each stored credential is a single Keychain record for its device type, bound to the hostname and port the operator typed, or to the connected peer IP and port for a literal address. - **Principle VII — Untrusted Input Is Validated At The Boundary:** codes are checked before being interpolated into commands, incoming line buffers are bounded, and malformed/failed AUTH replies do not make a direct connection available. - **Principles VIII and XI — Evidence Over Assertion / Fixes Are Demonstrated:** the probe output, focused tests, and the pre-change Tune routing record are in `docs/4o3a-remote-auth-hardware-evidence.md`. The report separates observed hardware behavior from test-only and still-unverified behavior; CI and operator confirmation remain gates after submission. ## UI Changes - **Setup → Peripherals, TGXL/PGXL/AG rows:** added separate masked Authorization code fields, Show/Hide buttons, Clear code buttons, endpoint-specific reuse/session-only placeholder text, and screen-reader names and descriptions. A blank field reuses the matching saved code when an AUTH greeting arrives; a new accepted code replaces that device type's saved record. - **Setup status cells:** show connecting, rejected/required code, Keychain deletion failure, and session-only save warnings. They distinguish an unchallenged connection (code not saved) from a code discarded before verification, and clear stale errors after a recovered connection. The AG and ShackSwitch rows display their own connection errors. - **TGXL applet:** added a bottom-right `● DIRECT` / `● RADIO` / `● OFFLINE` indicator in docked and floating layouts. DIRECT uses the theme success color, RADIO the warning color, and OFFLINE the disabled-text color. A blocked direct-connection reason appears in the tooltip and accessible description, and clears on manual retry or successful direct connection. - **PGXL applet:** moved its connection indicator below the controls to the same bottom-right placement and control-to-indicator spacing as TGXL. It uses the same three states, colors, tooltip, and accessibility behavior; RADIO requires a connected radio and PGXL relay handle. - **Antenna Genius applet:** added a bottom-right `● DIRECT` / `● OFFLINE` indicator with success/disabled theme colors and accessible text. It remains OFFLINE before a direct AG connection; the shared ShackSwitch model does not make it display DIRECT. - **Setup layout:** each device keeps address/port/connect on its first line, authorization controls on the next, and wrapped status across the available width. Clear code and errors fit at the default 960×680 size without horizontal scrolling. ShackSwitch Web UI occupies its own line; its authentication errors are named as ShackSwitch in the main status bar. - **Automation-visible UI:** masked the code field's value and its grab output so remote automation does not expose a shown code. **Maintainer rulings:** the central Peripherals tab (rather than per-applet fields), the compact Setup layout, the applet source indicators, and credential binding (configured host+port for names, peer IP+port for literal addresses) are accepted as built. So is a saved manual host taking precedence over the radio-reported endpoint, with the fallback described under **Automatic connection behavior**. ## Test plan - [x] Local build passes (`cmake --build build --parallel 6`) on macOS 27.0/arm64 using the branch's 26.9.4 source snapshot. - [ ] Behavior verified on a real radio if applicable — a prior operator Tune action showed the FlexRadio accepting a relayed TGXL autotune command; a full in-app saved-code restore has not yet been observed. - [ ] Existing tests pass (CI) — rerun required for the latest review-fix commit. - [x] Reproduction steps documented for the pre-change direct-TGXL Tune routing problem in `docs/4o3a-remote-auth-hardware-evidence.md`. Earlier submission verification: **37 passed, 1 skipped** across the peripheral auth dialog, handshake, and Keychain tests; automation redaction tests; TGXL/PGXL applet tests; and AppSettings safety scenarios. `pgxl_panel_test` skipped because this sandbox could not bind loopback; its result is not a pass claim. PR-REVIEW preflight identified that the earlier dialog regression clicked Connect against reserved IPs and opened outbound `QTcpSocket`s. That check now uses an injected callback at the production connect-ordering seam and its focused test passes socket-free. A mutation that kept the old pending flag set during the callback made the revised test fail; restoring the fix made it pass. The modified `pgxl_panel_test` is an existing default-graph loopback `QTcpServer` test; it was not rerun during this review after operator notification. Its bind failure returns 77. `git diff --check`, test registration, the aetherd touchpoint manifest, engine-boundary, capability-record, and command-plane baselines completed with no new blocker; the accessibility scan reports existing warning-only findings. An isolated, TX-disabled offscreen app launched, but its automation bridge did not expose a socket, so no demo GUI claim is based on bridge output. **Live evidence and limits:** the one-command TGXL and PGXL probes received accepted AUTH replies on firmware 1.2.17 and 3.9.1, respectively. A later app build established direct TCP sockets and showed bidirectional traffic, but that socket snapshot did not capture the in-app AUTH exchange. AG hardware AUTH and the full in-app Keychain restore/clear cycle remain unverified. ## Checklist - [x] Commits are signed (`docs/COMMIT-SIGNING.md`) — feature commit `120ed439` has a locally verified SSH signature registered to `w5jwp`. - [x] No new flat-key `AppSettings` calls — existing manual IP/port keys are reused and credentials use endpoint-bound Keychain records (Principle V). - [x] Code is clean-room — vendor/public API documents and observed wire behavior, with no decompiled or disassembled proprietary code (Principle IV). - [x] All meter UI uses `MeterSmoother` — not applicable to this PR; it changes connection labels/layout, with no new meter data or smoothing path. - [x] Documentation updated for user-visible behavior in `docs/4o3a-remote-auth-hardware-evidence.md`; `CHANGELOG.md` remains untouched. - [x] Security-sensitive changes reference a GHSA if applicable — no GHSA applies to this feature request. **Review routing:** source/tests/docs need Tier-3 code-owner review; `CMakeLists.txt` needs Tier-2 infrastructure review. The issue carries `maintainer-review`, and the UI/UX decisions above need the project maintainer's approval. The required issue-claim attempt was rejected by GitHub because `w5jwp` lacks upstream assignment permission; no placeholder assignee was created. ## Current review dispositions - TGXL/PGXL socket-free regression coverage now enables automatic reconnect at a nonempty target and asserts that the named reconnect timers stop after explicit AUTH rejection or three timeouts/handshake disconnects. Late disconnect/error delivery must not re-arm them. - AG requires sequence 1, zero result, and an omitted/empty or exactly `OK` body. Unknown/rejection bodies and extra fields cannot accept a credential. The test writer is private test-access plumbing; the production constructor accepts only its QObject parent. - [The contributor capture on #2313](#2313 (comment)) reports `V<version> AG AUTH\n`, LF from the vendor utility, and CRLF from AetherSDR reaching the auth parser. AUTH now uses CRLF. The report also says the utility silently truncates passwords to eight characters; operators must enter the actual device code. AetherSDR does not silently truncate passwords. This is attributed contributor evidence, not a new successful AG hardware test; the report names no firmware version. - Failed Keychain deletion retains the cache; a completed older deletion cannot erase a newer save. AG retry history ignores empty targets and is bounded to 128 targets without evicting existing blocks. - The earlier six CodeGuard advisories concern a protocol comment and unchanged MainWindow code; they do not identify a new path operation requiring a fix. **Automatic connection behavior:** saved TGXL/PGXL manual host/port settings take precedence over radio-discovered endpoints. Loss of radio-reported accessory presence does not disconnect the direct session while a manual override is configured. This also affects LAN users with a saved override; clearing it restores discovered-address selection. When an automatic connect (discovery, presence, startup, or their reconnects) to the saved address never reaches the device over TCP, the app tries the radio-reported address once, on the default port, as an alternate attempt. Reconnects keep aiming at the saved host, a code saved for the saved host is never sent to the alternate address, and a missing code there fails that try without blocking reconnects. An explicit Setup → Connect, a deliberate disconnect, or an attempt whose socket connected never triggers it. `checkUnreachableAndAlternate` in `peripheral_auth_handshake_test` pins each rule. **Credential identity (`907a88e8`):** a saved code is bound to the name the operator typed. A hostname (typically DDNS) binds the lowercased name and port, so a change of the residential IP behind it no longer asks for the code again, which is the main remote use case of #2313. A literal IP binds the connected peer IP and port, as before. Nothing binds until a socket has a real peer address, and a different name, port, or a literal IP is a different identity, so a code saved for one is never offered to another. The trade is deliberate: whatever the name resolves to receives the saved code, so a code should only be saved for a name the operator controls; the store header and `docs/4o3a-remote-auth-hardware-evidence.md` say so. A code saved earlier for a hostname was stored in the IP form and is not reused, so those operators enter it once more. Tests cover name equality across IPs, case and whitespace, the empty-peer and zero-port refusals, and that another name, port or literal IP does not match. This was raised as a question on the review; if the maintainer prefers peer-IP-only binding, it reverts by dropping this one commit. Central-tab placement, the compact layout, and applet indicator design still await maintainer direction. **Latest local verification (2026-09-29):** the desktop app builds, and all eight focused auth, automation-redaction, TGXL and amp applet tests passed. Mutation checks removed TGXL/PGXL auth-block reconnect guards, reverted AG AUTH to bare CR, and restored the wide Setup table: each failed at its intended regression assertion. All fixes were restored and the eight tests passed again. Test registration, touchpoint manifest, engine-boundary, capability-record, command-plane, colour ratchet, and whitespace checks found no new blocker. No new synthetic device peer, live-hardware, OS-vault, or TX test was introduced or run. The existing loopback `pgxl_panel_test` was not rerun. **Verification of the credential-identity commit `907a88e8` (2026-09-30):** the desktop app builds, and the ten focused tests (peripheral auth dialog, keychain and handshake; automation redaction; TGXL and amp applet) pass. The new handshake-test assertions cover name equality across IPs, case and whitespace, the empty-peer and zero-port refusals, and that another name, port or literal IP does not match. I did not run a mutation check against it, and it has not been tried against a live device or a real DDNS name. Self-assignment was attempted again; GitHub denied it because `w5jwp` lacks upstream assignment permission. Review threads are left for the reviewers to resolve. ## Review follow-up (2026-10-03) - A keychain outage no longer blocks auto-reconnect: `setAuthCodeForAttempt` passes `blockReconnect=false` when the credential store is unavailable (TGXL, PGXL, Antenna Genius). A rejected or missing code still blocks. - `setup` appends ` authcode=` only when `setup read` contained the `authcode` key and it was empty. The firmware 3.9.8 `R4|50000013|` probe result is in `docs/4o3a-remote-auth-hardware-evidence.md`. - The PGXL status debug log omits `authcode`. - The authcode-omission assertion is a socket-free case in `peripheral_auth_handshake_test`, through a command-writer seam on `PgxlConnection`. - Manual-address fallback: see **Automatic connection behavior** above. - Merged `origin/main` (`732b99a9`). --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
…rinciple XI. Merged with aethersdr#6008's head 071ddd0 as the base: this branch already carries it, and the squash differs from it only by main's own commits, so aethersdr#6008's lines are taken as they are here.
ten9876
left a comment
There was a problem hiding this comment.
Approving at 972f991f.
Fixed (each one proved):
- N1, per-reading freshness:
c485cdf5. Re-coupling the*Updatedflags failsmeter_model_test(only-ID / only-TEMP cases). - N2, rescan of tuner ID/TEMP after a late handle:
c485cdf5. - N3, positive amplifier match (maintainer ruling: fold in):
e372d423. RadioModel passes the amplifier's handle toMeterModel::setAmpHandle, and AMP meters then match it as FlexLibRadio.FindMetersByAmplifierdoes. A handle change re-routes the meters and withdraws Id/TEMP until the next sample. Each of these breaks fails a test:- removing the match:
testOnlyTheAmplifiersOwnMetersReachIt,radiomodel_amp_meter_routing_test; - removing the RadioModel call:
radiomodel_amp_meter_routing_test; - removing the withdrawal: the re-route check.
- removing the match:
- Comment policy:
e372d423restores the condensed fan-mode/OPERATE wiring comment. - Stack:
972f991fmergesmainwith #6008's head071ddd0bas the base. The squash differs from that head only bymain's own commits (the changed lines are byte-identical), and this branch's net diff againstmainis exactly its increment over #6008.
Verified empirically: Linux build. 14 focused targets pass on this merged head, including pgxl_panel_test on loopback. Mutations as listed. The four static gates match main. CI is green on this head, created after main's tip.
Not verified: relay/direct switching with real hardware. The demo backend has no amplifier, so that rests on the author's FLEX-8600 + PGXL 3.9.8 captures.
…the docked layout. Principle XI.
…nciple XI. Merged with aethersdr#6033's head 972f991 as the base: this branch already carries it, and the squash differs from it only by main's own commits.
#6036) **Merge order: #6008 and #6033 must be merged before this PR.** This branch is built on #6008 and merges in #6033 (with the conflicts resolved), so it carries their commits until they merge; it will then be rebased onto `main`. #6030 has merged. **Merge order for the PGXL / 4O3A PRs** (the same map is in each of them; #6030 has merged): ``` Stage 1: #6008 authenticate TGXL, PGXL and Antenna Genius Stage 2 (either order, after what each needs): #6027 Peripherals settings UI needs #6008 #6033 PGXL drain current (ID meter) needs #6008 Stage 3: #6036 PGXL docked layout needs #6008 and #6033 ``` **How to review:** this PR's own work is [`b4dcd8c9`](b4dcd8c) (the layout change) and [`4cc897c2`](4cc897c2) (the review findings: a neutral OPERATE placeholder with an accessible name, the temperature column measured in the drawn font, and two test fixes). `1b0de46e` merges #6030 in, and `3827ffea`, `4ef42eae` and `2186ef8e` merge #6033 in (the last also brings #6008's hostname-bound credential); open them only to check the resolutions. Everything else in the diff is carried from #6008, #6030 and #6033, which are reviewed in their own PRs. ## Summary Fixes #5903. Fixes #4885. #6008 puts the connection indicator on its own line and provides the direct connection that shows all of the readings and controls. In the docked (rail) view, the PGXL panel clipped its controls and readings: the 260 px rail had one row holding a stacked column of readings plus the MEffA, Fan and OPERATE controls. This PR gives the controls and the readings their own lines, and keeps the panel's shape whether the amplifier is offline or connected. Docked, the panel now reads: ``` [ MEffA ] [ Fan: Standard ] [ OPERATE ] PA 89.9 F Vac 122 V HL 89.2 F Vdd 51.9 V ● DIRECT ``` **Changes** - **Controls.** MEffA, Fan and OPERATE have the row to themselves and share its width, at one height (22 px). MEffA keeps its three state colours but takes OPERATE's 10 px bold lettering. - **Readings in a 2×2 grid**, in both presentations: PA over HL, and Vac over Vdd in a second column 10 px to the right. Docked, the connection indicator ends the HL/Vdd row; expanded, it keeps its own line below. Each is its own readout (`label value unit`) with its own screen-reader name. - **Vac and Vdd never move.** A grid column is as wide as its widest cell, so they start at one x; and the temperature column is held at the width of its widest possible reading, measured in the face it is drawn in, so they also stay put as the temperatures change. Fixed-length text alone was not enough: where no true fixed-width face is installed the fallback is proportional, and `9.9` and `106.8` differ in width. - **Temperature units.** PA and HL are both clickable, and a click on either switches both between Celsius and Fahrenheit and saves the choice, as the single readout did. HL shows `HL —` in grey until the direct connection sends a value, rather than disappearing, so the rows never move. The hover outline is gone (the pointing-hand cursor already says it can be clicked); the keyboard-focus outline stays. - **The connection indicator shares Vdd's line exactly.** Docked, it takes Vdd's height, so both centre the same way. At one pixel shorter, centring it needed a half-pixel offset that widget geometry rounds away, which lifted its text above Vdd's. - **Padding.** Every readout keeps a fixed length, but the spare spaces go at the end instead of in front of the number (`HL 89.8`, not `HL 89.8`). The temperature readouts have no left padding, so they line up with the controls above them. - **Spacing.** Docked, an 8 px pad separates the controls from the Id gauge, and the readings sit with equal space above and below. - **Fan labels** are spelled out: `Fan: Standard`, `Fan: Contest`, `Fan: Broadcast`. The pull-down reserves room for the longest. - **OPERATE's colours come from the theme.** It used a hardcoded green (`#6030`) under the theme's primary text colour, which is near-black under Default Light and unreadable there (#5903, item 4). It now uses the lit MEffA key's tokens, so the dark theme looks as before and the light theme gets its own green with white text. - **Before the amplifier reports.** The docked controls stay in place instead of hiding: MEffA is grey and disabled, OPERATE reads `—` and is grey and disabled (with the accessible name "Amplifier state not reported"), and Fan shows `Fan: —`, disabled. None of them claims a state. Clearing the fan selection is done with signals blocked, so it never sends a fan-mode command. PA and HL are grey until a reading arrives, like Vac and Vdd. The expanded panel's keys still wait for a state before appearing. **Maintainer rulings:** accepted as built. That covers the expanded (floating) panel's 2×2 grid, the removed hover outline, the right-justified readouts, and showing the controls disabled before the amplifier reports instead of hiding them. On a station without a direct PGXL connection, MEffA and fan stay dimmed, and their descriptions say they need a direct PGXL connection. **Against #5903's triage:** items 1 (controls on their own line), 2 (the rail MEffA button at 10 px) and 4 (OPERATE contrast) are done as proposed. Item 3 (the fan pull-down too wide when docked) is solved the other way: the controls share the full row, so the full labels fit, and they are spelled out rather than shortened to Std/Bcast. The docked-width test it asks for is added (below). **#4885** was left open until the docked `Fan: Contest` label could be checked on a current build: it reads in full here, and the new test checks all three fan labels at the rail's width. **Merging with #6030, #6033 and #5984:** #6030 is merged into this branch (commit 1b0de46), because the two conflicted in 10 places across `AmpApplet.cpp`, `AmpApplet.h` and `tests/amp_applet_test.cpp`: #6030 renames the temperature setters and members (`setTemp` → `setPaHeatsinkTemp`, `m_hasTempA` → `m_hasPaHeatsinkTemp`, …) and changes `updateTempLabel` and `setDirectConnected`, all code this PR restructures into two readouts. The resolution keeps this PR's structure with #6030's names and behaviour: HL clears (and turns grey) when the direct connection drops, a late HL write is ignored, and each readout's tooltip names only its own label. #6030's fallback test is rewritten for the two readouts and now also checks the grey tone. #6033 is merged in too (`3827ffea`, `4ef42eae`, `2186ef8e`). It deletes the amplifier-status relay handler and `AmpApplet::setMeff`, and adds `setRadioPaHeatsinkTemp` and `setRadioDrainCurrent` beside the PA setter this PR restructures, so it conflicted here in three files. The resolution keeps this PR's two-readout layout and tooltips, takes #6033's deletions and evidence comment, and keeps the grey-to-live style change on the PGXL's own first PA reading. Its single-button tooltip assertions are dropped in favour of this PR's per-readout ones. #5984 (spelling out MEffA on the expanded panel's key) edits panel-key code this PR leaves alone. ## Constitution principle honored Principle XI (Fixes Are Demonstrated). `pgxl_panel_test` lays the panel out at the rail's real width (260 px) and checks that no control or readout is narrower than its text needs, and `testDockedCaptionsFitTheRail` repeats that for every caption the controls can show. Both fail against the old layout. The layout was also checked in the running app on the hardware below, offline and connected. ## Test plan - [x] Local build passes - [x] Behavior verified on a real radio: FLEX-8600 (SmartSDR 4.2.20.41343) with a PowerGeniusXL on firmware 3.9.8. Docked, offline and connected with the direct connection up; `Fan: Broadcast`, the longest label, fits beside MEffA and OPERATE. - [x] Tests pass: `pgxl_panel_test`, `amp_applet_test`, `pgxl_direct_protocol_test`, `amp_model_test`, `peripheral_auth_handshake_test`, `health_applet_test` - [x] Test changes: - `pgxl_panel_test`: in both presentations, PA sits over HL and Vac over Vdd, with Vac and Vdd at one x. Docked at 260 px, the indicator ends the HL/Vdd row at the right edge, at Vdd's height and top edge; nothing is narrower than its text; the controls share one height; the space above and below the readings matches; the controls clear the Id gauge. Expanded keeps the indicator on its own line below, and the layout survives docked → expanded → docked. - `amp_applet_test`: new `testDockedCaptionsFitTheRail` shows every caption the rail's controls can take (each operate state word, all three fan labels, all three MEffA states, and the before-report placeholders) at 260 and 300 px, and checks each control is at least as wide as its caption needs. It compares against the control's own size hint, so a CI host that falls back to other fonts cannot fail it on a caption that fits. Against the old layout it fails at both widths: the fan pull-down got 70 px of the 130 its label needs. New `testVoltagesStayAlignedWithTheTemperatures` runs PA and HL through 9.9, 106.8, 100.4, 120.0 and -5.0 and checks that Vac and Vdd stay at one x and never move; without the pinned column width they moved by up to 7 px. `testDualSensorToggle` checks that a click on either temperature switches both and that the unit chosen from HL is saved. Also the new readout text; fixed length across digit changes; the before-state controls are shown but disabled with no mode; clearing the fan selection sends no command; the PA placeholder tone. - [x] Reproduction steps documented in the linked issues ## Checklist - [x] Commits are signed (`docs/COMMIT-SIGNING.md`) - [x] No new flat-key `AppSettings` calls - [x] Code is clean-room - [x] All meter UI uses `MeterSmoother` (no meter changed) - [x] Documentation updated if user-visible behavior changed (no `docs/` page describes this panel) - [x] Security-sensitive changes reference a GHSA if applicable (not applicable) 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Review follow-up (2026-10-03) - The column-pin test forces a proportional face on the temperature buttons, drives 9.9 → 106.8, and asserts the column minimum covers the widest reading plus the border. With the `setColumnMinimumWidth(0, ...)` line removed it fails. - **Justification (see #5983):** only the numeric field is right-justified (`label + rightJustified(value, 5) + unit`), so neither the unit nor the number's right edge moves as the value changes. - When there is no direct connection, the MEffA and fan descriptions say they need a direct PGXL connection. - OPERATE gets an accessible description while disabled; Vac/Vdd say "not reported" before the first reading; each readout's name change is announced once (`setAccessibleName` posts `NameChanged` itself); with a direct connection and no fan mode yet, the fan pull-down says the amplifier has not reported one; `placeReadings()` skips re-flow when nothing changed. - Comments describe current behaviour; the caption check uses the combo's edit-field rect; placeholder-tone checks compare against the resolved disabled colour. - **Known:** `pgxl_panel_test` fails at `:553-554` (`source->height() == vdd->height()`) in my sandbox, where the theme resources fail to load. It fails identically at the previous head `f5eafa06`, so it predates this change. It passes for you per your review. - Merged #6033's branch. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>


Merge order for the PGXL / 4O3A PRs (the same map is in each of them; #6030 has merged):
How to review: this PR's own commits are
ec0d432aand590a798e.d473b183merges #6008 in (one conflict inAmpApplet.cpp, resolved as described under "Merging with #6008") andb41ed522merges #6030 in (no change to the tree; this branch used to carry #6030's last commit as a cherry-pick, which conflicted with the original).92fd1aa5merges #6008 in again for its hostname-bound credential (907a88e8). Everything else is carried from #6008 and #6030, which are reviewed in their own PRs.Summary
Fixes #6032.
Stacked on #6008 and #6030, not a replacement for either. It merges both in until they merge; this PR will then be rebased onto
mainand show only its own.When PGXL status is relayed through a FlexRadio, the Id gauge did not move. The radio sends drain current as its
IDmeter, which AetherSDR never decoded, and the relay path instead waited for amplifier-status keys the radio never sends. This PR reads each PGXL reading from where the radio actually sends it, and picks between the radio and the PGXL by measured refresh rate.Verified on a FLEX-8600 (SmartSDR 4.2.20.41343) and a PowerGeniusXL running firmware 3.9.8. The captures, rates and method are in
docs/pgxl-telemetry-source-evidence.md.The rule for each reading
Changes
MeterModeldecodes the PGXL'sIDmeter.RadioModelpasses the amplifier's handle toMeterModel::setAmpHandle; once it is known, an AMP meter (FWD, RL, DRV, TEMP, ID) reaches the PGXL only when its source index matches, as FlexLibRadio.FindMetersByAmplifierdoes, so neither a tuner's nor a second amplifier's meters land on it. Before the handle is known, any AMP meter that is not the tuner's goes to the PGXL, and a handle change re-routes the meters and withdraws Id and PA temperature until the next sample. A newampVitalsChangedsignal carries drain current and PA heatsink temperature, each with a valid flag.AmpAppletgainssetRadioDrainCurrentandsetRadioPaHeatsinkTemp. The radio's values win while fresh, the same rule forward power and SWR already use.MainWindow_Wiring. None of the keys it read (temp,id,vdd,vac,meffa) arrive. Also removedAmpApplet::setMeff, which only that handler called, and its test.applyMeterscomment: it said the PGXL is polled at 5 Hz, and now cites the measured rates.tools/probe_pgxl_telemetry_sources.py, the read-only probe that produced the evidence, anddocs/pgxl-telemetry-source-evidence.md.Merging with #6008:
src/gui/AmpApplet.cppconflicts in one place. #6008 addssetRadioConnected,updateSourceIndicator,setDirectFailureReasonandhasRadioRelayjust beforesetMeff, which this PR removes. Keep #6008's four functions and dropsetMeff. Tested that way (see below).Constitution principle honored
Principle VIII (Evidence Over Assertion). The relay handler rested on an untested claim about what the radio sends (#2444). Every choice here is taken from captures of the radio and the PGXL, and the evidence page records how to repeat them.
Test plan
amp_applet_test,meter_model_test,health_applet_test,pgxl_panel_test,amp_model_testtestAmplifierVitalsAreRoutedandtestTunerDrainCurrentIsNotRoutedToTheAmplifier(MeterModel),testRadioVitalsWinOverTheDeviceWhileFresh(AmpApplet)Checklist
docs/COMMIT-SIGNING.md)AppSettingscallsRadio.FindMetersByAmplifier(Principle I)MeterSmoother(no meter UI changed)docs/pgxl-telemetry-source-evidence.md🤖 Generated with Claude Code
Review follow-up (2026-10-03)
ampVitalsChangedcarries a per-reading "updated in this packet" flag, so anIDpacket no longer restampsTEMP's freshness and the reverse. A newmeter_model_testcase sends onlyIDand checksTEMPis not marked updated.testAmpAndTunerMetersSplitByHandleAfterALateHandlenow includes a tunerID/TEMP, pinning the rescan after a late handle.testOnlyTheAmplifiersOwnMetersReachItandradiomodel_amp_meter_routing_testcover it.