Skip to content

fix(pgxl): fit the docked panel's controls and readings. Principle XI. - #6036

Merged
ten9876 merged 48 commits into
aethersdr:mainfrom
w5jwp:pgxl-minimized-applet-button-fix
Oct 3, 2026
Merged

ten9876 merged 48 commits into
aethersdr:mainfrom
w5jwp:pgxl-minimized-applet-button-fix

Conversation

@w5jwp

@w5jwp w5jwp commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

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 (the layout change) and 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 (#006030) under the theme's primary text colour, which is near-black under Default Light and unreadable there ([bug] 26.9.4 PGXL Applet cosmetically broken #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

  • Local build passes
  • 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.
  • Tests pass: pgxl_panel_test, amp_applet_test, pgxl_direct_protocol_test, amp_model_test, peripheral_auth_handshake_test, health_applet_test
  • 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.
  • Reproduction steps documented in the linked issues

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls
  • Code is clean-room
  • All meter UI uses MeterSmoother (no meter changed)
  • Documentation updated if user-visible behavior changed (no docs/ page describes this panel)
  • Security-sensitive changes reference a GHSA if applicable (not applicable)

🤖 Generated with 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 PGXL panel (popped out): the MEffA, fan and STBY keys are squeezed below their size while the port strips are shown, and their captions clip #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 fix(pgxl): read Id from the radio's ID meter. Principle VIII. #6033's branch.

w5jwp and others added 11 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>
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>
… 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>
In the 260 px rail one row held the stacked readings and the MEffA, fan
and operate controls, and everything clipped. Give the controls their own
row at one height, set PA, HL, Vac and Vdd in a two-column grid whose
voltage column never moves, spell out the fan modes, take OPERATE's
colours from the theme, and keep the docked controls in place, disabled,
before the amplifier reports.

Fixes aethersdr#5903
Fixes aethersdr#4885

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@w5jwp
w5jwp requested review from a team as code owners September 30, 2026 01:38
@w5jwp

w5jwp commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Before. Both taken docked in the 260 px rail, on a FLEX-8600 (SmartSDR 4.2.20.41343) with a PGXL on firmware 3.9.8 connected directly (#6008).

1. With the amplifier operating. The temperature is cut off (PA 106.7 /, no HL value visible), and all three controls are squeezed below their captions: MEffA is clipped at both ends, the fan pull-down reads Fan: C… and OPERATE reads PERAT. The readings are stacked in a narrow column beside the controls.

before-docked-clipped-with-6008

2. The same clipping at another moment, as reported in #5903: PA 35.7 / H, Fan: St… and PERAT, with Vdd and Vac crammed against the left edge.

before-docked-clipped

@w5jwp

w5jwp commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

After. Same station, docked in the 260 px rail.

1. Connected, direct connection up. MEffA, Fan: Standard and OPERATE share the row at one height, with nothing clipped. PA over HL and Vac over Vdd form a two-column grid; Vac and Vdd start at one x and do not move as the temperatures change. The connection indicator shares Vdd's line. (Vdd reads 0.0 V because, on the Standard fan profile, the PGXL turns its supply off when idle.)

after-docked-grid-final

2. Before a connection. The docked controls hold their place instead of hiding: MEffA, Fan: — and OPERATE are grey and disabled, so none of them claims a state the amplifier has not reported. PA, HL, Vac and Vdd keep the same grid in grey with dashes, and ● OFFLINE sits on the Vdd line. The panel keeps one shape whether the amplifier is offline or connected.

after-docked-grid-offline

aethersdr#6030 renames the temperature setters and members and changes
updateTempLabel and setDirectConnected, all code this branch restructures
into two readouts. Keep this branch's structure with aethersdr#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. The fallback test is rewritten for the two readouts and checks the
grey tone.

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 both, with one deviation the body already names. #5903's reporter (dark-theme variant, Windows 11, 236 px content width) and @G6PWY-Chris's Linux confirmation are both the docked 260 px rail clipping MEffA / Fan: St… / STANDBY. Giving the three controls their own full-width row at one height removes the ~300-px-of-minimums-in-228-px squeeze the triage measured, and the geometry guards at 260 px are the thing that was missing (the triage explicitly asked for the TGXL parity-test equivalent; tests/pgxl_panel_test.cpp now lays out at 260×380 and asserts width() >= sizeHint().width() for every visible control and readout). #4885 ("Fan: Contes" in the Panel) is met by spelling the labels out instead of shortening them to Std/Bcast — the opposite of triage item 3, disclosed as such, and the better answer to an issue whose complaint is the truncated word. Triage item 4 (light-theme kOperateStyle contrast) is fixed by moving to color.accessory.key.meffa.active.*; both tokens exist in default-dark.json:120-124 and default-light.json:121-125, so this resolves rather than silently dropping through. Note item 4 is not what the reporter saw — they answered "a variation of Dark" — so it is a ride-along fix the triage proposed, not part of the reported symptom.

One requirement neither issue asked for: the docked controls now stay in place, disabled, before the amplifier reports. The body flags it for maintainer review, which is the right call — see Needs-maintainer-decision below.

2. Scope

File(s) Change Claimed? Verdict
src/gui/AmpApplet.cpp, AmpApplet.h controls row, 2×2 readout grid, pinned temperature column, theme-resolved OPERATE, spelled-out fan labels, before-report placeholders Yes In scope
tests/amp_applet_test.cpp testDockedCaptionsFitTheRail, testVoltagesStayAlignedWithTheTemperatures, dual-readout toggle, before-report assertions Yes In scope
tests/pgxl_panel_test.cpp 260 px docked geometry, grid order, indicator on the Vdd line Yes In scope
26 other files (src/core/Pgxl*, Tgxl*, PeripheralAuth*, src/gui/PeripheralAuth*, RadioSetupDialog, MainWindow*, docs/, CMakeLists.txt, .gitignore, touchpoint manifest) The peripheral-auth feature Yes — first line of the body Inherited, not this PR's

I verified the stacking claim rather than taking it: git fetch origin pull/6008/head and this checkout share 120ed439 → 80f40fef → 601b4b41 → 00d6f79e exactly, and b4dcd8c9 is the only commit this branch adds (4 files, 563 insertions). So the 26-file tail is byte-identical to #6008's head — no extra change smuggled in under the stack, and nothing here for me to re-review that #6008's thread does not already own. The "Merging" section's conflict inventory against #6030 / #6033 / #5984 matches what I see in the tree.

Expanded-panel readouts also change from one row of three to the same 2×2 grid. Neither issue is about the expanded panel — but splitting one PA x / HL y label into two readouts makes four cells, and four abreast in the panel is worse than 2×2, so I read this as a consequence rather than smuggled restyling. Worth the maintainer's eye since it alters a presentation nobody reported as broken.

No CHANGELOG.md edit. CI on b4dcd8c9 is 5/5 green (build, check-windows, check-macos, Static checks, Sanitizer configures) — note that is the frozen -R gate, so neither amp_applet_test nor pgxl_panel_test actually ran there; both are full-suite.yml.

Socket-test disclosure (§2): this PR modifies tests/pgxl_panel_test.cpp, an existing registered default-graph target that binds a loopback QTcpServer on QHostAddress::LocalHost, 0 and answers as a simulated PGXL peer (tests/pgxl_panel_test.cpp:123-145), skipping with 77 when it cannot bind. The changes here are UI-geometry assertions only — no new bind, listen, connect or peer behaviour, and no new fake peer. It is not in the PR -R gate. tests/amp_applet_test.cpp is socket-free. Recording this per the reporting half of the socket-test rule; it is not a finding.

3. Blockers

None. Everything I could construct that breaks users either did not reproduce in the code or predates this commit.

4. Needs maintainer decision

  1. The before-report OPERATE placeholder reuses a real state word (AmpApplet.cpp:1576, inline). With no state, the button reads OPERATE in kBtnStyle — the same caption the operating state shows, and the same style the STANDBY state shows. Disabled-ness is the only thing distinguishing "not reported" from either. The same commit gives the fan combo Fan: — / "not reported yet" and both temperatures "not reported" accessible names, so this is inconsistent with the PR's own pattern; and m_operateBtn has no accessible name at all, so a screen reader announces "OPERATE, dimmed". Suggested in the inline comment.
  2. The before-report rule itself. This replaces a deliberate rule whose comment was deleted: "Nothing before the amplifier has reported a state, on the same rule the fan controls follow: a control that cannot say what it is set to is worse than none." The new rule (present, disabled, asserting nothing) is defensible and keeps the row's shape, but it is a behaviour change to working UI that neither #5903 nor #4885 asks for. The body already routes it here, which is the right handling under GOVERNANCE.md §AI Contributors — no split needed, just a ruling. Fixing (1) would make the new rule considerably easier to accept.

5. Nits (non-blocking)

  • The unit letter now moves instead of the number (AmpApplet.cpp:234, inline). leftJustified puts the padding after the unit, so Vdd 0.0 V / Vdd 100.4 V place V two columns apart, and PA 9.9 C / PA 106.8 C likewise. The old pad() right-justified the value, which pinned the unit and moved the number's left edge instead — the gap the comment rejects. Both jiggle; you traded one for the other in a change whose headline is stable readings. testVoltagesStayAlignedWithTheTemperatures pins the column, not the inside of the readout.
  • Column width is measured from a font the sheet does not ask for (AmpApplet.cpp:981, inline): QFont("monospace") + setStyleHint(QFont::Monospace), while the sheet says only font-family: monospace. Where no real monospace family exists the two can resolve differently, and if the measured face is the narrower one setColumnMinimumWidth under-reserves and Vac/Vdd shift again — on Windows, not on the CI host where the new test runs. m_tempBtn->fontMetrics() after applyStyleSheet would measure what is actually drawn.
  • HL keeps a stale, live-toned reading when the direct connection drops (AmpApplet.cpp:1608, inline). setDirectConnected(false) clears Vdd/Vac to — and greys them, and drops fan mode, but leaves m_hasTempB/m_tempB alone — and the radio does not relay HL. Pre-existing (the old combined label froze the same value), so not a regression, but this commit promotes it to its own readout beside a correctly-greyed Vdd, and the body says the #6030 merge resolution is what fixes it. If #6036 lands first, that stale HL ships alone.
  • controlsFit dereferences meffa before the null check (tests/amp_applet_test.cpp:810, inline): b->parentWidget() == meffa->parentWidget() runs inside the discovery loop, while bool fits = meffa && fan && operate; is line 814. If ampMeffaButton is ever renamed, the Principle XI test segfaults instead of failing. (The object name does exist today at AmpApplet.cpp:511, so this is latent.)
  • The #4885 guard does not actually measure the fan caption (tests/amp_applet_test.cpp:818, inline). With AdjustToMinimumContentsLengthWithIcon, QComboBox::sizeHint() skips the item texts and uses minimumContentsLength * advance('x'), so width() >= sizeHint().width() is a 14-character proxy, not "Fan: Broadcast fits". minimumContentsLength(14) happens to equal that label's length, and the drop-down width is added on top, so it is fine today — but the test would stay green if a future label outgrew the combo, which is exactly the #4885 failure mode. The QPushButton halves (MEffA, OPERATE) are genuinely covered, since their sizeHint derives from the text.

6. What I tried to break (and could not)

Findings below are reasoned from the head checkout at /tmp/aetherclaude/pr-6036; I have no build, no test run and no way to drive the GUI, so nothing here was reproduced at runtime.

  • "The grid is placed from applyDensityAtScale, not buildUI" — the obvious break is an empty readout grid on first show. buildUI() ends with applyDensityAtScale(1.0) at AmpApplet.cpp:588, so placeReadings() always runs before the applet is visible. Survived.
  • Reparenting m_sourceLabel between the grid and the column on every density pass. placeReadings() calls removeWidget on both containers (one is always a no-op), then re-appends to m_vbox after m_bottomStretch in the floating branch — so docked → floating → docked cannot leave the indicator above the bottom pad, and cannot leave it orphaned (show() at the end). Walked the order by hand in both directions. Survived.
  • !known used before its declaration in the rewritten applyStateToControls — const bool known = !m_stateWord.isEmpty(); is at line 1560, ahead of every use. Survived.
  • Appending QPushButton { font-size: 10px; } to the panel-key sheets for the rail MEffA button: if those sheets used a class selector (PanelKey {...}) the concatenation would be the only matching rule and MEffA would lose its three state colours. They are all plain QPushButton selectors (AmpApplet.cpp:126-157) and none sets font-size, so there is no conflict and no ordering dependence. Survived.
  • Token existence for the re-pointed kOperateStyle, including the :hover variant — all four color.accessory.key.meffa.active.* keys are present in both bundled themes. kOperateStyle has no :disabled rule (unlike kPanelKeyMeffaActiveStyle, which has one precisely because a hard-set background beats the disabled palette), but setEnabled(known) means that sheet is never applied to a disabled button. Survived.
  • kValueFieldChars overflow: it is 5, and the widest real temperature (106.8, -88.8) is exactly 5, so leftJustified never has to truncate; and even when a value did overrun, the pinned column min-width absorbs it rather than shifting the voltages. Survived.
  • Clearing the fan selection firing a fan-mode command — both setCurrentIndex(-1) sites are inside QSignalBlocker scopes, and the test asserts fanModeChanged count stays 0 across a connect/disconnect cycle. Survived.
  • Dangling references to the renamed/removed API: applyTelemetryLayout, m_telemetryInRow and voltsReadout have zero remaining hits anywhere in src/ or tests/; every new objectName is referenced only by this applet and its two tests. Survived.
  • Build hazards from the new code — std::max / std::make_pair without <algorithm> / <utility> (both arrive transitively via QtCore containers; pgxl_panel_test.cpp does add <algorithm> explicitly), and no M_PI or windows.h/std::min pairing. check-windows and check-macos are green on this head. Survived.
  • CodeGuard's six CG-PATH-001 advisories — refuted. src/core/TgxlConnection.h:19 is a protocol comment (// S0|state key=val ...); MainWindow.cpp:9549 is the NR2 wisdom dialog's label text and :9780-9782 is the macOS bundle-root QDir::cdUp() walk. None is a path operation, none is touched by b4dcd8c9 (MainWindow.cpp is not even in that commit), and #6008's thread has already dispositioned the same six. No action.
  • Could not check: anything requiring a run — the actual rendered widths on Windows/Linux with the real font fallbacks (the font-resolution nit above is the specific place that matters), and whether the new 260 px geometry assertions pass on a CI host, since neither modified test is in the PR's -R gate.

7. Recommendation

Needs maintainer decision — on the before-report control states (item 1 is a concrete, cheap fix; item 2 is a ruling), and on the expanded panel picking up the 2×2 grid. The layout work itself is the right fix for both issues and the evidence is unusually good: the geometry guards run at the rail's real 260 px, they are claimed to fail against the old layout, and the scope claim about the inherited #6008 stack checks out commit-for-commit. Next step: settle the OPERATE placeholder caption and accessible name, then land behind #6008 (and fold in the #6030 resolution for the stale-HL nit, whichever order the two take).

Thanks for the merge-order and conflict inventory up front — verifying the stack took one git fetch instead of a guess.


🤖 aethersdr-agent · cost: $11.7145 · model: claude-opus-5


GitHub rejected the inline anchors (GitHub API 422: Unprocessable Entity); findings below:

src/gui/AmpApplet.cpp:1576
With no state reported the caption is OPERATE in kBtnStyle — the same word the operating state shows, and the same style the STANDBY state shows. Only setEnabled(false) separates "not reported" from either. m_operateBtn also has no setAccessibleName/setAccessibleDescription anywhere in this file, so a screen reader gets "OPERATE, dimmed".

The same commit is careful about exactly this elsewhere — Fan: — plus "Fan speed: not reported yet", and "PA heatsink not reported" / "Harmonic Load heatsink not reported". A neutral caption plus a name would make the new before-report rule much easier to accept:

    m_operateBtn->setText(!known ? QStringLiteral("—")

…paired with m_operateBtn->setAccessibleName(known ? m_operateBtn->text() : tr("Amplifier state not reported")); just below the setToolTip call. Caption wording is the maintainer's call — the point is that the placeholder should not be a state word.

src/gui/AmpApplet.cpp:234
leftJustified puts the spare columns after the unit, so the unit letter is what moves now: Vdd 0.0 V vs Vdd 100.4 V place V two columns apart, and PA 9.9 C vs PA 106.8 C likewise. The old pad() pinned the unit and moved the number's left edge instead — the HL 89.8 gap the comment rejects.

Both jiggle, and you chose consciously, so this is an observation rather than a defect: in a change whose headline is that readings do not move, the readout's own interior still does. testVoltagesStayAlignedWithTheTemperatures pins the column boundary, not the inside of the cell. If it matters, right-justify only the numeric field and keep the fixed total (label + rightJustified(value, kValueFieldChars) + unit) — that pins both edges at the cost of the leading gap.

src/gui/AmpApplet.cpp:981
The column width is measured in a font the style sheet never asks for: here it is QFont("monospace") with setStyleHint(QFont::Monospace), while the sheet applied to these buttons says only font-family: monospace. Where no family literally named monospace is installed, the sheet's resolution and this one can land on different faces — and if the measured face is the narrower one, setColumnMinimumWidth(0, widest + 4) under-reserves and Vac/Vdd shift again, which is the whole bug this hunk exists to prevent.

testVoltagesStayAlignedWithTheTemperatures cannot catch that: it measures in the same process as the sheet, on the CI host. A style sheet's font-size/font-family is pushed onto the widget's own font by QStyleSheetStyle::polish, so measuring m_tempBtn->fontMetrics() after theme.applyStyleSheet(...) would measure what is actually drawn and remove the divergence entirely.

src/gui/AmpApplet.cpp:1608
This clears Vdd/Vac to — and greys them, and drops fan mode just below — but m_hasTempB / m_tempB are le

(truncated)

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (6 finding(s))

  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/core/TgxlConnection.h /tmp/aetherclaude/pr-6036/src/core/TgxlConnection.h:19
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6036/src/gui/MainWindow.cpp:9549
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6036/src/gui/MainWindow.cpp:9661
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6036/src/gui/MainWindow.cpp:9780
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6036/src/gui/MainWindow.cpp:9781
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6036/src/gui/MainWindow.cpp:9782

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $12.1437 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Antares Detector — candidate vulnerable file(s)

  • src/core/PgxlConnection.cpp

The TcpSocket connection reads all incoming data via m_socket.readAll() without size limiting, allowing untrusted external input to force large memory allocations and potential DoS or resource exhaustion.

Localized by Cisco Foundation AI Antares-1B running locally in the AetherClaude sandbox, seeded by the Cartographer security map. Advisory only — please verify before acting.


🤖 aethersdr-agent · cost: $12.1437 · model: claude-opus-5

w5jwp and others added 10 commits September 29, 2026 21:35
Before the amplifier reports, the rail OPERATE button reads a dash with the
accessible name 'Amplifier state not reported' instead of borrowing a state
word. The temperature column is measured in the button's own polished font.
The fan caption is measured against the combo in the docked-fit test, and the
test no longer dereferences a missing MEffA button.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
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>
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>
…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>
w5jwp and others added 8 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>
…le III.

- Pin the temperature column and test it against a proportional face: the
  test now fails with the setColumnMinimumWidth(0, ...) line removed.
- Right-justify only the numeric field of each reading so the unit and the
  number's right edge stay put.
- Describe the disabled OPERATE button; name Vac/Vdd "not reported" before
  the first report and send NameChanged only when the name changes.
- When relayed, MEffA and fan speed say they need a direct PGXL connection.
- placeReadings() skips re-flow when the presentation is unchanged.
- Compare the fan caption with the combo's edit-field rect; placeholder-tone
  assertions check the resolved disabled colour.
- Rewrite stale comments to state what is true now.

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>
…button-fix

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 c30e153e, which merges #6033's branch (no force-push). The fixes are all in b12f8f51.

Blocker 1: the telemetry grid has an objectName. The test measures the widest reading in the polished m_tempBtn font, forces a proportional family on the temperature buttons, drives 9.9 → 106.8, and asserts columnMinimumWidth(0) is at least that width plus the 4 px border. With m_telemetryGrid->setColumnMinimumWidth(0, widest + 4) commented out, amp_applet_test fails twice; restored, it passes.

Nits (all in b12f8f51)

  • N1: m_operateBtn->setAccessibleDescription(...) while disabled. tools/check_a11y.py src/gui/AmpApplet.cpp reports no issues.
  • N2: rewrote the stale comments (AmpApplet.h, the two AmpApplet.cpp spots, pgxl_panel_test.cpp) to say what is true now and dropped the history narration.
  • N3: the caption check compares against the SC_ComboBoxEditField rect.
  • N4: placeholder-tone assertions check the sheet contains the resolved disabled colour (and a live reading does not).
  • N5: Vac/Vdd say "not reported" before the first reading, and NameChanged is sent only when the name changes.
  • N6: placeReadings() skips re-flow when the presentation is unchanged.

M2 mitigation: with no direct connection, the MEffA and fan descriptions say they need a direct PGXL connection. M1 mitigation (justification): only the numeric field is right-justified (label + rightJustified(value, 5) + unit), so neither the unit nor the number's right edge moves. Cited #5983 in the body. Left for your ruling: the expanded-panel grid, the hover/justification direction, and the before-report rule; I did not change them.

Heads-up: pgxl_panel_test fails at :553-554 (source->height() == vdd->height()) in my environment, where theme resources fail to load. It fails the same way at the previous head f5eafa06, so it predates this change, and it passed for you. The other focused targets pass; I could not drive the panel on the bridge (no amplifier in demo).

@w5jwp

w5jwp commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

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

ten9876 and others added 5 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>
… Principle XI.

setAccessibleName already sends NameChanged when the name changes, so the
temperature readouts no longer post a second event per change. With a
direct connection and no fan mode reported yet, the pull-down's description
says the amplifier has not reported it. Comments state the current
constraint rather than the bug that led to 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 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 added a commit that referenced this pull request Oct 3, 2026
**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`](ec0d432)
and
[`590a798e`](590a798).
`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

- [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
- When PGXL status is relayed through a FlexRadio: Id read 19 A during a
TUNE and dropped after unkey; PA temperature updated; HL, Vdd and Vac
blank.
- Direct connection (built together with #6008, which this PGXL firmware
needs for authentication): Id 19 A, Vdd 52.0 V, Vac 117 V, both heatsink
temperatures; no flicker between sources.
- [x] Tests pass: `amp_applet_test`, `meter_model_test`,
`health_applet_test`, `pgxl_panel_test`, `amp_model_test`
- [x] New tests: `testAmplifierVitalsAreRouted` and
`testTunerDrainCurrentIsNotRoutedToTheAmplifier` (`MeterModel`),
`testRadioVitalsWinOverTheDeviceWhileFresh` (`AmpApplet`)
- [x] Reproduction steps documented in the linked issue

## Checklist

- [x] Commits are signed (`docs/COMMIT-SIGNING.md`)
- [x] No new flat-key `AppSettings` calls
- [x] Protocol behavior comes from captures of the radio and the PGXL;
the meter-to-amplifier match follows FlexLib
`Radio.FindMetersByAmplifier` (Principle I)
- [x] All meter UI uses `MeterSmoother` (no meter UI changed)
- [x] Documentation updated: `docs/pgxl-telemetry-source-evidence.md`
- [x] 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](https://claude.com/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 #6008's branch.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
…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 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 bb8a14c1.

Fixed (each one proved):

  • Blocker 1, column-pin test couldn't observe the pin: b12f8f51. With setColumnMinimumWidth(0, widest + 4) removed, amp_applet_test fails "column 0 is pinned to the widest reading (0 < 64)" and "the voltages do not move as the temperatures change". Restored, it passes.
  • N1: OPERATE's accessible description, b12f8f51. tools/check_a11y.py src/gui/AmpApplet.cpp reports 0 findings.
  • N2: stale comments, b12f8f51 and e2b49b51; history narration is trimmed.
  • N3, N4, N6: b12f8f51.
  • N5: b12f8f51. In e2b49b51, the temperature readouts no longer post a manual NameChanged, because on Qt 6.11.2 setAccessibleName already posts it once per change (checked with a test harness). The fan pull-down now says the amplifier hasn't reported a mode when a direct connection is up.

Maintainer rulings: accepted as built:

  • the expanded-panel 2×2 grid;
  • the removed hover outline;
  • the right-justified readouts;
  • dim-not-hide before the amplifier reports;
  • MEffA and fan dimmed, with "needs a direct PGXL connection", on relay-only stations.

Stack: bb8a14c1 merges main with #6033's head as the base. The squash differs from that head only by main's own commits, and this branch's net diff against main is exactly its 4-file increment.

Verified empirically: Linux build. 14 focused targets pass on this head, including pgxl_panel_test on loopback. The :553-554 failure the author saw doesn't reproduce here. The mutation is as listed. CI is green on this head, created after main's tip.

Not verified: the panel on the bridge, because the demo backend has no amplifier. Layout rests on the unit tests and the author's screenshots.

@ten9876
ten9876 merged commit 0374952 into aethersdr:main Oct 3, 2026
5 checks passed
ten9876 added a commit that referenced this pull request Oct 3, 2026
…ple XI. (#6133)

`main` went red after #6036. The Full Suite on `03749527` failed two
tests:

- `amp_applet_test`: "every rail caption fits at 260 px"
- `pgxl_panel_test.cpp:570`: `width >= sizeHint`

The rail's fan pull-down sized itself to 14 average character widths
(`AdjustToMinimumContentsLengthWithIcon` +
`setMinimumContentsLength(14)`). In DejaVu Sans, the usual Ubuntu
default and the face on the CI image, that comes to 158 px. The 260 px
rail gives the control 151 px, so the row's minimum overflows. The real
captions fit: the caption-vs-edit-field check passes. Only the estimate
was too wide.

## Change

- `AmpApplet`: the pull-down uses `QComboBox::AdjustToContents`, so its
width comes from the widest caption ("Fan: Broadcast") in the font that
draws it. `ElideNone` stays.
- `amp_applet_test`: the #4731 guard now checks that property (policy
`AdjustToContents`, no character-count minimum, size hint at least the
widest caption's advance) instead of the old mechanism.

## Proof

- Reproduced locally with a DejaVu-only fontconfig: both tests failed
before this change and pass after it. They also pass with this machine's
default fonts.
- Restoring the character-count hint makes `amp_applet_test` fail ("fan
combo sizes to its widest item, not a character estimate") in any font.
- The focused targets pass: `amp_applet_test`, `pgxl_panel_test`,
`pgxl_direct_protocol_test`, `amp_model_test`, `meter_model_test`,
`health_applet_test`, the peripheral-auth and automation-redaction
tests, and `radiomodel_amp_meter_routing_test`.

## Constitution principle honored

- [x] Principle XI (Fixes Are Demonstrated): the failing Full Suite
tests on `main` are reproduced and shown green, and the guard is shown
to fail without the fix.
- [x] Commits are signed.

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

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants