Skip to content

fix(pgxl): read Id from the radio's ID meter. Principle VIII. - #6033

Merged
ten9876 merged 33 commits into
aethersdr:mainfrom
w5jwp:pgxl-relayed-status-fields
Oct 3, 2026
Merged

ten9876 merged 33 commits into
aethersdr:mainfrom
w5jwp:pgxl-relayed-status-fields

Conversation

@w5jwp

@w5jwp w5jwp commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

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 commits are ec0d432a and 590a798e. d473b183 merges #6008 in (one conflict in AmpApplet.cpp, resolved as described under "Merging with #6008") and b41ed522 merges #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). 92fd1aa5 merges #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 main and 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 ID meter, 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

  • Available only from the radio: use the radio (drive).
  • Available only from the PGXL: use the PGXL (Harmonic Load heatsink temperature, Vdd, Vac, fan mode, MEffA).
  • Available from both: use the faster source. The radio is faster for forward power and SWR. Drain current and PA heatsink temperature are a tie, and a tie goes to the radio. In every case the radio's value is used while fresh (1500 ms), and the PGXL's value otherwise.

Changes

  • MeterModel decodes the PGXL's ID meter. RadioModel passes the amplifier's handle to MeterModel::setAmpHandle; once it is known, an AMP meter (FWD, RL, DRV, TEMP, ID) reaches the PGXL only when its source index matches, as FlexLib Radio.FindMetersByAmplifier does, 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 new ampVitalsChanged signal carries drain current and PA heatsink temperature, each with a valid flag.
  • AmpApplet gains setRadioDrainCurrent and setRadioPaHeatsinkTemp. The radio's values win while fresh, the same rule forward power and SWR already use.
  • Removed the radio amplifier-status handler in MainWindow_Wiring. None of the keys it read (temp, id, vdd, vac, meffa) arrive. Also removed AmpApplet::setMeff, which only that handler called, and its test.
  • Corrected the applyMeters comment: it said the PGXL is polled at 5 Hz, and now cites the measured rates.
  • Added tools/probe_pgxl_telemetry_sources.py, the read-only probe that produced the evidence, and docs/pgxl-telemetry-source-evidence.md.

Merging with #6008: src/gui/AmpApplet.cpp conflicts in one place. #6008 adds setRadioConnected, updateSourceIndicator, setDirectFailureReason and hasRadioRelay just before setMeff, which this PR removes. Keep #6008's four functions and drop setMeff. 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

  • Local build passes
  • Behavior verified on a real radio: FLEX-8600 (SmartSDR 4.2.20.41343) with a PowerGeniusXL on firmware 3.9.8
  • Tests pass: amp_applet_test, meter_model_test, health_applet_test, pgxl_panel_test, amp_model_test
  • New tests: testAmplifierVitalsAreRouted and testTunerDrainCurrentIsNotRoutedToTheAmplifier (MeterModel), testRadioVitalsWinOverTheDeviceWhileFresh (AmpApplet)
  • Reproduction steps documented in the linked issue

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls
  • Protocol behavior comes from captures of the radio and the PGXL; the meter-to-amplifier match follows FlexLib Radio.FindMetersByAmplifier (Principle I)
  • All meter UI uses MeterSmoother (no meter UI changed)
  • Documentation updated: docs/pgxl-telemetry-source-evidence.md
  • Security-sensitive changes reference a GHSA if applicable (not applicable; the probe reads the authorization code without echoing it and never records it)

🤖 Generated with Claude Code

Review follow-up (2026-10-03)

  • ampVitalsChanged carries a per-reading "updated in this packet" flag, so an ID packet no longer restamps TEMP's freshness and the reverse. A new meter_model_test case sends only ID and checks TEMP is not marked updated.
  • testAmpAndTunerMetersSplitByHandleAfterALateHandle now includes a tuner ID/TEMP, pinning the rescan after a late handle.
  • AMP meters match the amplifier's handle (maintainer ruling on N3). testOnlyTheAmplifiersOwnMetersReachIt and radiomodel_amp_meter_routing_test cover it.
  • Merged feat(peripherals): authenticate direct TGXL, PGXL, and Antenna Genius connections #6008's branch.

w5jwp and others added 6 commits September 28, 2026 13:07
…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>
@w5jwp
w5jwp requested a review from a team as a code owner September 29, 2026 23:45
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>

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

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 — TEMP is left name-matched while ID gains handle-matching. The new ID branch cites DRV's rationale ("which device is this meter about … the day one appears"), but TEMP, the other half of the signal this PR introduces, still has no handle guard in either defineMeter or setTgxlHandle. I found no evidence a TGXL publishes TEMP — 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: because setRadioPaHeatsinkTemp holds the PGXL off for kRelayMeterFreshnessMs, a misrouted tuner TEMP would not merely show the wrong number, it would suppress the PGXL's correct PA reading on a DIRECT station. Reasoned from code, not reproduced. One if would close it.
  • amp_applet_test.cpp:393 — a withdrawn ID meter 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, and setDrivePower(w, valid) next door hides its whole row on valid=false. It is probably invisible in practice (the ID meter goes away when the amplifier does, and applyMeffaControls-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 to setDrivePower will assume one of the two is wrong.
  • AmpApplet.cpp:1213 — the tooltip explains HL even when no HL value is on the button. On a RADIO-only station the button reads PA 47.9 C and the tooltip still defines a label that is not there.
  • AmpApplet.cpp:413 — setAccessibleDescription is not wrapped in tr(). Pre-existing on that line and widespread in the tree, but docs/a11y.md:62,73 shows both accessor examples inside tr(), 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::telemetryUpdated to its single emit site (AmpModel.cpp:371, the appliesToAmp block) and confirmed the deleted handler was its only GUI consumer and setMeff its only caller — so nothing else silently depended on the deleted reads. I then checked the one field whose loss would be user-visible, meffa, through applyMeffaControls's known gate: hidden before, hidden after. No behavior lost.
  • Unit and scaling. convertRaw (MeterModel.cpp:456) divides Amps by 256 and degC by 64, citing FlexLib Meter.cs — matching the raw values the tests construct and the doc's table. 70 A hi against qint16/256 = 127.99 A ceiling, no overflow.
  • The new test's index collisions. defineAmpManifest uses 12,13,14,16,17,18; the new ID takes 15, so defineMeter's ID-reuse purge never fires and index 16 really is TEMP. Both new meter tests fail to compile against unfixed code (the signal does not exist), so neither is self-asserting.
  • testReadoutWidthIsStable. I suspected the new m_directConnected gate on setHarmonicLoadHeatsinkTemp had 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. ampVitalsChanged carries both readings on every sample of either, so a station with only TEMP invalidates the drain-current timer each sample (PGXL owns Id) and vice versa; removeMeter clears index, value and flag together for both, and emits. setTgxlHandle's rescan clears m_ampDrainCurrent/its flag but not m_ampTemp — correct, because TEMP is unconditionally re-found in the same loop while ID may not be. clear() does not emit ampVitalsChanged, which costs at most one 1500 ms window before the PGXL resumes.
  • Late writes and ordering. setHarmonicLoadHeatsinkTemp's !m_directConnected early return follows the setDrainVoltage/setMainsVoltage precedent exactly, and setDirectConnected(false) arrives from PgxlConnection::disconnected before any late status can land. Repeated setDirectConnected(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 its ctest steps are -R-filtered, so green does not tell me amp_applet_test or meter_model_test ran. 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

Comment thread src/models/MeterModel.cpp Outdated
Comment thread tests/amp_applet_test.cpp
Comment thread src/gui/AmpApplet.cpp Outdated
Comment thread src/gui/AmpApplet.cpp Outdated
w5jwp and others added 3 commits September 29, 2026 19:52
… 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>
@w5jwp

w5jwp commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 590a798, plus e21d21a (#6030's latest commit, cherry-picked), covering the review:

  • TEMP routed by handle, in defineMeter and the setTgxlHandle rescan, with a new test that fails without the guard.
  • Withdrawn ID keeps its last value: the reason is now in a comment at setRadioDrainCurrent.
  • Tooltip and tr(): fixed in fix(pgxl): define the heatsink temperatures and clear stale HL. Principle VIII. #6030 and brought in here.
  • The rate comment now says the device figure came from polling at 50 ms, twice this client's rate.

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 main. I've made that explicit at the top of the body.

amp_applet_test, meter_model_test, health_applet_test, pgxl_panel_test and amp_model_test pass.

@w5jwp

w5jwp commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

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 ID meter; before this PR it did not move in this mode. PA temperature comes from the TEMP meter; HL, Vdd and Vac have no radio source and read blank.

radio-mode-transmit-id-19A

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.

direct-mode-transmit-with-6008

w5jwp and others added 2 commits September 29, 2026 21:42
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>
w5jwp added a commit to w5jwp/AetherSDR that referenced this pull request Sep 30, 2026
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>
w5jwp added a commit to w5jwp/AetherSDR that referenced this pull request Sep 30, 2026
…inciple XI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

# Conflicts:
#	tests/amp_applet_test.cpp
@w5jwp
w5jwp requested a review from a team as a code owner September 30, 2026 02:50
w5jwp and others added 2 commits September 29, 2026 22:03
…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>
w5jwp added a commit to w5jwp/AetherSDR that referenced this pull request Sep 30, 2026
…) into the docked layout. Principle XI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
w5jwp and others added 7 commits October 1, 2026 11:16
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>
…Principle XI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…Principle XI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rge. Principle XI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

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

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:

  • TEMP is now handle-routed at both sites.
  • The withdrawn-ID behaviour 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.
    • MeterModel emits ampVitalsChanged(id, idValid, temp, tempValid) when either meter lands (MeterModel.cpp:1196-1200, :1280-1281). The lambda at MainWindow_Wiring.cpp:7301 calls both setRadioDrainCurrent and setRadioPaHeatsinkTemp every time.
    • So an ID packet restamps TEMP'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 the setTgxlHandle rescan. testAmpAndTunerMetersSplitByHandleAfterALateHandle covers FWD/RL only, and the new tuner tests all set the handle first. Adding a tuner ID/TEMP to 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/TEMP wins. FlexLib matches positively (Radio.cs FindMetersByAmplifier compares SourceIndex with 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 only AmpApplet.* and two tests), and ran meter_model_test, amp_applet_test, amp_model_test, health_applet_test and pgxl_panel_test: all pass. pgxl_panel_test binds a loopback stub PGXL; the operator approved it for this review.
  • Mutation: with the TEMP handle guard removed at both sites (MeterModel.cpp:121, :247), meter_model_test fails. 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 RadioModel on main, so ampVitalsChanged adds 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. state handling still lives in AmpModel, and setMeff and the deleted reader had no other consumers.
    • Merge d473b183. It keeps #6008's four functions and drops only the claimed code.
  • 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 -R filter.

w5jwp and others added 6 commits October 3, 2026 08:35
…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>
@w5jwp

w5jwp commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed 5fb8b154 (this branch merges #6008, no force-push).

  • N1 (c485cdf5): each reading's freshness is its own. ampVitalsChanged carries per-reading "updated in this packet" flags, both false when a meter is withdrawn, and the MainWindow_Wiring.cpp lambda calls only the setter for the reading that arrived. New meter_model_test cases: only ID arriving does not mark TEMP updated, and the reverse. Re-coupling the flags makes those fail.
  • N2 (c485cdf5): testAmpAndTunerMetersSplitByHandleAfterALateHandle now has a tuner ID/TEMP in the manifest before setTgxlHandle, and asserts neither reaches the amplifier after the rescan.
  • N3: not done. MeterModel is only told the tuner's handle (setTgxlHandle), not the amplifier's. A positive FindMetersByAmplifier-style match needs a new setAmpHandle from RadioModel. FWD/SWR/DRV use the same "not the tuner" rule, so it predates this PR. Happy to do it as a follow-up if you want.

The merge conflicts with #6008's side in AmpApplet.cpp / MainWindow_Wiring.cpp were resolved by keeping this PR's removal of the relay telemetry reader and AmpApplet::setMeff. Please look at that hunk. Focused targets pass locally; the gates are clean. The relay/direct switching still rests on your hardware captures.

@w5jwp

w5jwp commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

@ten9876 the updates from your review are pushed (5fb8b154). The itemized reply is above. Ready for another look when you have time.

ten9876 and others added 3 commits October 3, 2026 12:06
… 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>
ten9876 added a commit to w5jwp/AetherSDR that referenced this pull request Oct 3, 2026
@ten9876

ten9876 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

Follow-up on the review at fccf8e47, now on 255553eb:

  • N1: per-reading freshness, c485cdf5. Re-coupling the *Updated flags makes meter_model_test fail ("a packet carrying only ID marks ID updated and TEMP not", and the reverse).
  • N2: the late-handle rescan covers tuner ID/TEMP, c485cdf5.
  • N3: maintainer ruling: fold it in. e372d423:
    • RadioModel passes the amplifier's handle to MeterModel::setAmpHandle. Once that's known, an AMP meter reaches the PGXL only when its source index matches, as Radio.FindMetersByAmplifier does.
    • A handle change re-routes the meters and withdraws Id/TEMP until the next sample.
    • Removing the handle match, the RadioModel call, or the withdrawal fails meter_model_test or the new radiomodel_amp_meter_routing_test.
  • e372d423 also restores the condensed fan-mode/OPERATE wiring comment that a merge had reverted to its pre-docs: cut review latency — comment policy, AGENTS.md split, condensed comments, Constitution 2.1.0, body-only reviews. Principle VIII. #6072 text.

The 14 focused targets pass. The four static gates show the same findings as main.

ten9876 added a commit that referenced this pull request Oct 3, 2026
… 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 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.

Approving at 972f991f.

Fixed (each one proved):

  • N1, per-reading freshness: c485cdf5. Re-coupling the *Updated flags fails meter_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 to MeterModel::setAmpHandle, and AMP meters then match it as FlexLib Radio.FindMetersByAmplifier does. 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.
  • Comment policy: e372d423 restores the condensed fan-mode/OPERATE wiring comment.
  • Stack: 972f991f merges main with #6008's head 071ddd0b as the base. The squash differs from that head only by main's own commits (the changed lines are byte-identical), and this branch's net diff against main is 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.

@ten9876
ten9876 merged commit d2e85fa into aethersdr:main Oct 3, 2026
5 checks passed
ten9876 added a commit to w5jwp/AetherSDR that referenced this pull request Oct 3, 2026
ten9876 added a commit to w5jwp/AetherSDR that referenced this pull request Oct 3, 2026
…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.
ten9876 added a commit that referenced this pull request Oct 3, 2026
#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>
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.

PGXL: Id doesn't update when relayed through a FlexRadio

2 participants