Repository navigation
fix(pgxl): fit the docked panel's controls and readings. Principle XI. - #6036
Conversation
…reading on radio fallback. Principle VIII. The PGXL reports the PA heatsink and Harmonic Load heatsink temperatures (PowerGeniusXL User Guide v3.9.8, p. 55). Rename the setters and members after them and cite the manual in place of the old guess. A FlexRadio relays only the PA heatsink temperature, as the radio's TEMP meter. Stop reading HL keys on the relay path, and clear HL when the direct connection drops so a frozen value is not shown as live. Verified on a FLEX-8600 (SmartSDR 4.2.20.41343) with a PowerGeniusXL on firmware 3.9.8. Fixes aethersdr#6029 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…. Principle VIII. The Harmonic Load heatsink temperature comes only over the direct connection. Guard its setter like Vdd and Vac, so a write that arrives after the connection drops cannot put a stale value back on screen, and test it. Label the packed "PA/HL" form of `temp` and the `tempb` name as never seen in a capture. Firmware 3.8.9 and 3.9.8 both send separate `temp` and `hltemp` keys. Addresses the triage on aethersdr#6029. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a FLEX-8600 (SmartSDR 4.2.20.41343) with a PGXL on firmware 3.9.8, the radio's amplifier status carries only ip, model, serial_num, ant and state. The PGXL readings it relays arrive as meters: FWD, RL, DRV, ID and TEMP. Decode ID, feed drain current and PA heatsink temperature from the radio's meters while fresh, and remove the amplifier-status handler, whose keys never arrive. Measured rates and method are in docs/pgxl-telemetry-source-evidence.md. Fixes aethersdr#6032 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Firmware 3.9.8 with authorization enabled returns the code in `setup read` and refuses any `setup` that carries `authcode=<code>` with 50000013, so every fan-mode change and MEffA toggle was refused. Probed on the hardware: the same group without `authcode` is accepted and the stored code is unchanged, as is `setup fanmode=` alone. With no code set the key is still sent empty, as the vendor utility does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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>
|
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 (
2. The same clipping at another moment, as reported in #5903:
|
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>
There was a problem hiding this comment.
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
- The before-report OPERATE placeholder reuses a real state word (
AmpApplet.cpp:1576, inline). With no state, the button readsOPERATEinkBtnStyle— the same caption the operating state shows, and the same style theSTANDBYstate shows. Disabled-ness is the only thing distinguishing "not reported" from either. The same commit gives the fan comboFan: —/ "not reported yet" and both temperatures "not reported" accessible names, so this is inconsistent with the PR's own pattern; andm_operateBtnhas no accessible name at all, so a screen reader announces "OPERATE, dimmed". Suggested in the inline comment. - 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).leftJustifiedputs the padding after the unit, soVdd 0.0 V/Vdd 100.4 VplaceVtwo columns apart, andPA 9.9 C/PA 106.8 Clikewise. The oldpad()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.testVoltagesStayAlignedWithTheTemperaturespins 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 onlyfont-family: monospace. Where no realmonospacefamily exists the two can resolve differently, and if the measured face is the narrower onesetColumnMinimumWidthunder-reserves and Vac/Vdd shift again — on Windows, not on the CI host where the new test runs.m_tempBtn->fontMetrics()afterapplyStyleSheetwould 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 leavesm_hasTempB/m_tempBalone — 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. controlsFitdereferencesmeffabefore the null check (tests/amp_applet_test.cpp:810, inline):b->parentWidget() == meffa->parentWidget()runs inside the discovery loop, whilebool fits = meffa && fan && operate;is line 814. IfampMeffaButtonis ever renamed, the Principle XI test segfaults instead of failing. (The object name does exist today atAmpApplet.cpp:511, so this is latent.)- The
#4885guard does not actually measure the fan caption (tests/amp_applet_test.cpp:818, inline). WithAdjustToMinimumContentsLengthWithIcon,QComboBox::sizeHint()skips the item texts and usesminimumContentsLength * advance('x'), sowidth() >= sizeHint().width()is a 14-character proxy, not "Fan: Broadcastfits".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. TheQPushButtonhalves (MEffA, OPERATE) are genuinely covered, since theirsizeHintderives 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, notbuildUI" — the obvious break is an empty readout grid on first show.buildUI()ends withapplyDensityAtScale(1.0)atAmpApplet.cpp:588, soplaceReadings()always runs before the applet is visible. Survived. - Reparenting
m_sourceLabelbetween the grid and the column on every density pass.placeReadings()callsremoveWidgeton both containers (one is always a no-op), then re-appends tom_vboxafterm_bottomStretchin 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. !knownused before its declaration in the rewrittenapplyStateToControls—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 plainQPushButtonselectors (AmpApplet.cpp:126-157) and none setsfont-size, so there is no conflict and no ordering dependence. Survived. - Token existence for the re-pointed
kOperateStyle, including the:hovervariant — all fourcolor.accessory.key.meffa.active.*keys are present in both bundled themes.kOperateStylehas no:disabledrule (unlikekPanelKeyMeffaActiveStyle, which has one precisely because a hard-set background beats the disabled palette), butsetEnabled(known)means that sheet is never applied to a disabled button. Survived. kValueFieldCharsoverflow: it is 5, and the widest real temperature (106.8,-88.8) is exactly 5, soleftJustifiednever 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 insideQSignalBlockerscopes, and the test assertsfanModeChangedcount stays 0 across a connect/disconnect cycle. Survived. - Dangling references to the renamed/removed API:
applyTelemetryLayout,m_telemetryInRowandvoltsReadouthave zero remaining hits anywhere insrc/ortests/; every newobjectNameis referenced only by this applet and its two tests. Survived. - Build hazards from the new code —
std::max/std::make_pairwithout<algorithm>/<utility>(both arrive transitively via QtCore containers;pgxl_panel_test.cppdoes add<algorithm>explicitly), and noM_PIorwindows.h/std::minpairing.check-windowsandcheck-macosare green on this head. Survived. - CodeGuard's six CG-PATH-001 advisories — refuted.
src/core/TgxlConnection.h:19is a protocol comment (// S0|state key=val ...);MainWindow.cpp:9549is the NR2 wisdom dialog's label text and:9780-9782is the macOS bundle-rootQDir::cdUp()walk. None is a path operation, none is touched byb4dcd8c9(MainWindow.cppis 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
-Rgate.
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)
|
Cisco CodeGuard — static analysis of this PR (6 finding(s))
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 |
|
Antares Detector — candidate vulnerable file(s)
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 |
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>
…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>
|
Thanks for the review. Pushed Blocker 1: the telemetry grid has an objectName. The test measures the widest reading in the polished Nits (all in
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 ( Heads-up: |
|
@ten9876 the updates from your review are pushed ( |
… Principle VII. The radio-reported address is now tried as an alternate attempt: the reconnect timer keeps aiming at the saved manual host, and a missing saved code for the alternate address fails that try without blocking reconnects. unreachable() is sent only for automatic attempts (discovery, presence, startup and their reconnects) whose socket never connected, and never after a deliberate disconnect, so an explicit Setup -> Connect is not redirected. The wiring receives it queued, after the failed socket's error handling. The setup-write comment states the current behaviour in the condensed form. peripheral_auth_handshake_test pins each guard; removing any of them fails it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… 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>
…cked layout. Principle XI.
… 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.
…the docked layout. Principle XI.
**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
left a comment
There was a problem hiding this comment.
Approving at bb8a14c1.
Fixed (each one proved):
- Blocker 1, column-pin test couldn't observe the pin:
b12f8f51. WithsetColumnMinimumWidth(0, widest + 4)removed,amp_applet_testfails "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.cppreports 0 findings. - N2: stale comments,
b12f8f51ande2b49b51; history narration is trimmed. - N3, N4, N6:
b12f8f51. - N5:
b12f8f51. Ine2b49b51, the temperature readouts no longer post a manualNameChanged, because on Qt 6.11.2setAccessibleNamealready 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.
…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>




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):
How to review: this PR's own work is
b4dcd8c9(the layout change) and4cc897c2(the review findings: a neutral OPERATE placeholder with an accessible name, the temperature column measured in the drawn font, and two test fixes).1b0de46emerges #6030 in, and3827ffea,4ef42eaeand2186ef8emerge #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:
Changes
label value unit) with its own screen-reader name.9.9and106.8differ in width.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.HL 89.8, notHL 89.8). The temperature readouts have no left padding, so they line up with the controls above them.Fan: Standard,Fan: Contest,Fan: Broadcast. The pull-down reserves room for the longest.#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.—and is grey and disabled (with the accessible name "Amplifier state not reported"), and Fan showsFan: —, 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: Contestlabel 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.handtests/amp_applet_test.cpp: #6030 renames the temperature setters and members (setTemp→setPaHeatsinkTemp,m_hasTempA→m_hasPaHeatsinkTemp, …) and changesupdateTempLabelandsetDirectConnected, 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 andAmpApplet::setMeff, and addssetRadioPaHeatsinkTempandsetRadioDrainCurrentbeside 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_testlays the panel out at the rail's real width (260 px) and checks that no control or readout is narrower than its text needs, andtestDockedCaptionsFitTheRailrepeats 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
Fan: Broadcast, the longest label, fits beside MEffA and OPERATE.pgxl_panel_test,amp_applet_test,pgxl_direct_protocol_test,amp_model_test,peripheral_auth_handshake_test,health_applet_testpgxl_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: newtestDockedCaptionsFitTheRailshows 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. NewtestVoltagesStayAlignedWithTheTemperaturesruns 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.testDualSensorTogglechecks 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.Checklist
docs/COMMIT-SIGNING.md)AppSettingscallsMeterSmoother(no meter changed)docs/page describes this panel)🤖 Generated with Claude Code
Review follow-up (2026-10-03)
setColumnMinimumWidth(0, ...)line removed it fails.label + rightJustified(value, 5) + unit), so neither the unit nor the number's right edge moves as the value changes.setAccessibleNamepostsNameChangeditself); 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.pgxl_panel_testfails at:553-554(source->height() == vdd->height()) in my sandbox, where the theme resources fail to load. It fails identically at the previous headf5eafa06, so it predates this change. It passes for you per your review.