Repository navigation
fix(audio): the title-bar mute reaches the radio's line out only where it has one. Principle VIII. - #6260
Conversation
There was a problem hiding this comment.
Reviewed at the PR head 123ad88 (checkout at /tmp/aetherclaude/pr-6260). Everything below is reasoned from source — I have no build, no test run and no radio, so nothing here was reproduced at runtime.
1. Issue fit
No Fixes line; the PR body is the report, and GOVERNANCE.md does not want an RFC for a bug fix with a named root cause, so that's fine. The symptom is real and the root cause is correctly identified: setLineoutMute() sends mixer lineout mute N, sendCmd() drops it at hasCommandPlane() and burns the once-per-session notice.
I checked the cited authority. The #4665 ruling exists — ten9876, 2026-10-03: "The speaker click keeps doing what it does today; muting both paths is harmless on the inactive one." Credit for citing it accurately.
But the body's mechanism claim does not hold on the radio in the report:
"Without a command plane,
sendCmd()drops the text and raisescommandDropped, even though the seam call already carried the request."
AnanBackend is the only backend that overrides IRadioBackend::setLineoutGain/setLineoutMute (src/core/backends/IRadioBackend.h:432-433, defaults Q_UNUSED). Hl2Backend overrides neither, so on the reporter's HL2 the seam call is a no-op and nothing carried the request. The mute the operator hears is m_audio->setMuted(muted) on the line below in src/gui/MainWindow.cpp:2047. on8st said the same thing in the #4665 thread from an HL2: "there is no radio-side mixer at all… It still does not reach any hardware."
So: Partially. The user-visible outcome on an HL2 is right (PC Audio is pinned on there by setPcAudioLocked, so the line out genuinely is the inactive path), but the gate was placed on the wrong predicate and reaches further than the claim.
2. Scope
| File | What it changes | Claimed? | Verdict |
|---|---|---|---|
src/models/RadioModel.cpp:11910-11916 |
new sendsLineoutWireText() |
yes | in scope |
src/models/RadioModel.cpp:11927-11929, 11951-11953 |
guards the two mixer lineout sends |
yes | in scope |
src/models/RadioModel.h:597-598 |
declares the predicate (private helper, no public/protocol surface) | yes | in scope |
tests/transmit_seam_drop_notice_test.cpp |
RecordingBackend lineout overrides + one case |
yes | in scope, but see Blocker 2 |
Everything in the diff is explained by the stated fix. No new settings key, capability field, protocol verb or config surface. No CHANGELOG.md entry — correct. No guard or symptom-naming comment deleted. Socket tests: none added, modified or removed; the touched test links aethercore + Qt6::Core only and opens nothing.
CI is 5/5 green on 123ad88, but note transmit_seam_drop_notice_test is registered at tests/tests.cmake:573-576 and is not named in any ctest -R step in ci.yml — the green badge did not run the new case.
3. Blockers
1. src/models/RadioModel.cpp:11910-11916 — the guard asks "no command plane?" when the question is "does this backend apply the lineout seam?". Only ANAN does; on every other family the request now vanishes with no notice.
bool RadioModel::sendsLineoutWireText() const
{
return !m_backend || usesFlexCommandPlane() || hasCommandPlane();
}AnanBackend::setLineoutMute (AnanBackend.cpp:1029-1032) stores m_lineoutMuted, and it is genuinely consumed at AnanBackend.cpp:1674 — on ANAN the PR's framing is exactly right. Hl2Backend, IcomCivBackend, the RTL backend and the simulator all inherit the Q_UNUSED defaults, so after this change the operator's request is discarded at the seam and the drop notice that said so is gone. That is the #5263 dead-control shape with the alarm removed.
This cuts against three things already in the tree:
- the convention pinned two functions above the new case,
tests/transmit_seam_drop_notice_test.cpp:514-529: "The HL2 implements neither setVox nor setTxMonitor and declares neither record" → notice kept. Same at:501-511for PROC. Lineout declares no record either, and gets the opposite treatment. src/gui/MixerControlAvailability.h, which states the project's position directly: "A connected radio with no command plane has no such mixer", and prescribes the shape — the headphone pair is dimmed with its reason, not silently accepted (MainWindow_Session.cpp:3239).src/gui/MainWindow_Controllers.cpp:2317-2328: the MIDIglobal.masterVolumeknob on exactly this path logs"refused: no radio mixer and PC Audio is off"and raises the unsupported-control notice. After this PR the title-bar slider and the Radio Setup slider take the same request silently.
Worst instance, and the one with no PC-sink companion to make it harmless: the Radio Setup → Radio Audio Outputs → Line Out slider and Mute button, src/gui/RadioSetupDialog.cpp:4692-4714.
connect(lineoutSlider, &QSlider::valueChanged, this, [this, lineoutValue](int v) {
lineoutValue->setText(QString::number(v));
m_model->setLineoutGain(v);
});
connect(lineoutMute, &QPushButton::toggled, m_model, &RadioModel::setLineoutMute);outGroup is created at :4683 and added at :4781 with no availability or hasCommandPlane gate anywhere in 4600-4790. On an HL2 those two widgets are live and enabled, reach the base-class no-op, and after this PR say nothing at all.
The capability-shaped fix is the record precedent this file already uses for VOX/MON/PROC: an std::optional<RadioCapabilities::LineoutControl> that AnanBackend declares and the others don't, gating both the wire text and the seam call — an optional record needs no FROZEN_BOOL_COUNT ruling. Alternatively, extend MixerControlAvailability.h to the lineout pair and dim it with its reason the way the headphone pair already is. Either keeps #4665's ruling intact (the click still mutes both paths on a radio that has both) without silencing a radio that has neither.
2. tests/transmit_seam_drop_notice_test.cpp:95-96, 534-548 — the new case's fixture gives the HL2 a lineout seam the real Hl2Backend does not have, which is why Blocker 1 is invisible.
void setLineoutGain(int percent) override { lineoutGains << percent; }
void setLineoutMute(bool mute) override { lineoutMutes << mute; }RecordingBackend overrides both, and the case runs it under hostModulatingTransmitter(), documented at :100-102 as "the Hermes-Lite 2's answers". So the test asserts the ANAN arm and labels it HL2 — the one configuration where the PR's premise is true. It proves the seam is called, never that anything applies it. The arm that matters is the one voxAndMonitorWithoutRecordsKeepDropNotice covers for VOX: a backend that does not override the setters, where the notice should be kept (or where the maintainer rules explicitly that an inactive-path request is withheld silently).
Secondary: the case is one-sided. Replacing sendsLineoutWireText() with return false passes every check in it, so nothing pins the Flex-sends-text half. #6255 shows the local pattern — a command sink on an unopened RadioConnection and the text compared whole.
4. Nits (non-blocking)
src/models/RadioModel.cpp:11915—hasCommandPlane()is redundant next tousesFlexCommandPlane().m_connectionis assigned only inside thedynamic_cast<FlexBackend*>block (RadioModel.cpp:882) andm_wanConnonly on the SmartLink path (:3802), so no non-Flex backend can make it true, and any built Flex already satisfies the first term. The predicate reads as three cases where there are two.src/models/RadioModel.cpp:11942-11947— the doc comment abovesetLineoutMute()is now stale: "Unlike the gain setters, the command is always sent: a mute is a request, and suppressing it on an unchanged model would make model/radio drift unrecoverable from the UI." The new in-function comment says "Sent unconditionally" about the seam call, so a reader gets two contradictory statements about the same function.tests/transmit_seam_drop_notice_test.cpp:531-533— the "Flex now has its VOX, monitor and PROC setters called…" block is now orphaned above the new case instead of aboveflexSeamSettersWriteNothing().src/gui/RadioSetupDialog.cpp:4743— the dialog's headphone Mute is still ungated on a radio with no headphone jack, while the title-bar pair is dimmed. Pre-existing, correctly out of scope here ("Headphone gain and mute are not touched"), and route-separately rather than fold in.
5. What I tried to break, and could not
- Is the test honest about failing on main? Yes. Without the guard,
sendCmd("mixer lineout mute 1")reaches the!hasCommandPlane()branch (RadioModel.cpp:~9834-9845) and emitscommandDroppedwith that exact text, sodroppedStartingWith("mixer lineout")is true and the case goes red. The author's statedreturn truemutation is the right one. - Does the gain check self-satisfy? No.
m_lineoutGain{50}(RadioModel.h:1923), sosetLineoutGain(30)passes the equality early-return and reaches both paths. - Is the Flex path really untouched? Yes, two ways:
usesFlexCommandPlane()ism_family == "flex", andm_connection = flex->connection()makeshasCommandPlane()true for any built Flex. The seam call stays behindm_backend && !usesFlexCommandPlane(), so text and seam remain mutually exclusive and never both-off while a backend exists. - Does the
!m_backendarm leak a false notice? It sends text on a bare model, whichradiomodel_audio_mute_testdepends on, and with no backend nothing carried the request, so a drop notice there is honest. Correct as written. - Master volume with PC Audio off on an HL2 — I expected a silently dead slider via
applyMasterVolume→setLineoutGain(MainWindow.cpp:7517), butapplyTxAudioCapabilitiespins PC Audio on for a host-modulating transmitter (MainWindow_Session.cpp:3200-3206), and the MIDI knob refuses before reaching the model. Not reachable on the reporter's radio; it is reachable on an Icom/RTL profile with PC Audio off, which is folded into Blocker 1. - Could not check: anything at runtime, on a Flex, or on hardware — the PR already discloses it was not re-run on hardware after the fix. I also did not verify that
transmit_seam_drop_notice_testpasses; CI never ran it.
6. Recommendation
Needs maintainer decision, with Blocker 2 to fix either way.
The title-bar symptom is real and the HL2 outcome the author wants is defensible — but the seam chosen is wider than the claim. "No command plane" is not "the backend applies this request", and the only backend that applies it is ANAN. Whether an operator request that reaches no hardware should go silent is the question #5919 and MixerControlAvailability.h already answered one way (keep the notice, or dim the control with its reason), and this PR answers the other way for a whole class of radios on the strength of a premise that is false on the reporter's own.
Concrete next step: narrow the gate to a declared capability (an optional LineoutControl record on RadioCapabilities, or the MixerControlAvailability.h lineout pair), and add the test arm where the backend does not override the two setters so the HL2/Icom/RTL case is pinned rather than assumed. If a maintainer prefers the broad "inactive path, no notice" rule, that is a legitimate call — but it wants saying out loud in the body, because it reverses the convention this test file pins fifteen lines above the new case.
Thanks for the careful mutation note and for naming what wasn't re-tested on hardware — that is what made the premise checkable.
🤖 aethersdr-agent · cost: $9.6891 · model: claude-opus-5
|
Thanks. Both blockers were right, and my body's premise was wrong. Reworked in
Still not done: no hardware click after this revision, no Flex run, and these two tests are outside CI's |
NF0T
left a comment
There was a problem hiding this comment.
Reviewed at ae5f874b. I built it locally (Windows, MinGW 13.1, Qt 6.12.0), ran the two tests, mutated the code myself, and drove main and this head against the demo radio through the automation bridge.
What's right
The revision gets the model right. lineoutThroughSeam() puts the request on exactly one route: typed through the seam for a backend that declares lineoutControl, Flex wire text otherwise. The undeclared radios (HL2, Icom, RTL) keep their mixer lineout drop notice inside RadioModel, which matches the VOX/MON/PROC convention this test file already pins. Both of @aethersdr-agent's blockers are fixed as asked:
- Blocker 1. The record route replaces the "no command plane" gate. ANAN is the only non-Flex backend that declares the record, and it is the only one with real
setLineoutGain/setLineoutMuteoverrides. - Blocker 2. The no-record arm now asserts no seam call and keeps the notice for both
muteandgain.
The stale-comment nit and the orphaned Flex comment block are also fixed. The optional record means the frozen-70 bool count stays put.
One blocker: the title-bar speaker goes silently dead on Icom/RTL with PC Audio off
src/gui/MainWindow.cpp:2049-2054. The new gate skips setLineoutMute() whenever lineoutControlsAvailable() is false, then mutes this computer's output. That is right on the HL2, because applyTxAudioCapabilities() pins PC Audio on there (pcAudioRequired = takesTxAudioOverSeam && canTransmit && hostModulates). It doesn't hold on Icom (hostModulates = false) or RTL (canTransmit false). On both, PC Audio can be off, and then:
- The click sets the button checked and emits
lineoutMuteChanged(true). - The gate is false, so
setLineoutMute()is skipped andm_lineoutMutestaysfalse. m_audio->setMuted(true)emitsmutedChanged, which callssyncTitleBarOutput().- With PC Audio off,
syncTitleBarOutput()callsm_titleBar->setLineoutMuted(m_radioModel.lineoutMute()), which isfalse, so the icon snaps back to unmuted. - There's no notice, and
PcAudioMuted=Truehas been saved without the operator seeing it. That part is pre-existing, but it is now the only effect of the click.
On main the same click sends the text, it's dropped, the notice fires, and the icon stays muted. The body says "an undeclared radio keeps its drop notice". That's true in RadioModel, but this path never reaches RadioModel. It's the #5263 dead-control-with-the-alarm-removed shape again, one layer up.
The tree already has the answer for this exact state. The MIDI global.masterVolume handler (MainWindow_Controllers.cpp:2317-2327) refuses with showUnsupportedControlNotice() when there is "no radio mixer and PC Audio is off". The inline suggestion does the same for the speaker: refuse and resync the icon when there is neither a line out nor PC Audio, and otherwise behave exactly as this revision does.
Also asked for in the same push (non-blocking on their own)
- Radio Setup's Line Out gate is evaluated once.
src/gui/RadioSetupDialog.cpp:4716-4731runs when the Audio page is lazily built. The dialog is a non-modalPersistentDialogthat can stay open across a disconnect and reconnect. Open it on an HL2, switch to a Flex, and the Flex's Line Out row stays dimmed until the dialog is closed. The other gates in this dialog re-evaluate live:rebootBtnat:1406-1407, the radio-side record button at:5170-5185, and the calibration item onconnectionStateChanged. Re-applying it onconnectionStateChanged/capabilitiesChanged, and re-enabling with the tooltip cleared, would match them. - The capabilities-map row is wrong about the Sim. The row says "Absent (HL2, Icom, RTL, Sim): the title-bar speaker mutes only this computer's output, Radio Setup's Line Out row is dimmed". The Sim owns a
RadioConnection(RadioModel.cpp, theSimBackendbranch ofsetupBackend,m_connection = sim->connection()), sohasCommandPlane()is true and neither change applies there. I confirmed this on the demo: see below. Suggested wording: "Sim: absent, but its synthetic command plane takes the text, so it behaves as a Flex here." - User docs. The docs site (#6286) merged on 10-08, the day after your last push, and AGENTS.md now asks every user-visible change to update
docs/user/in the same PR.docs/user/docs/audio-settings.mdalready says controls for a missing output are dimmed with a reason, and this PR finally makes that true for Line Out.docs/user/docs/menu-reference.md's "Speaker and master slider" table has no row for a radio without a line out. One sentence there covers it once the speaker fix lands.
Scope
| Change | Claimed | Verdict |
|---|---|---|
RadioCapabilities::lineoutControl + six backend declarations |
yes | in scope; optional record, no bool-ratchet growth |
RadioModel::lineoutThroughSeam() and the two setters |
yes | in scope; net one fewer usesFlexCommandPlane() branch, no new wire text (#5554 checklist clean) |
lineoutControlsAvailable() in MixerControlAvailability.h |
yes | in scope |
Title-bar speaker gate (MainWindow.cpp) |
yes | in scope; blocker above |
| Radio Setup Line Out dimming | yes | in scope; see the staleness note |
radio-capabilities-map.md row |
yes | in scope; Sim wording is wrong |
| Two tests | yes | in scope; no sockets (Step-0 clean) |
Preference vs fix: a fix. The Radio Setup dimming applies an existing documented rule ("a control the radio lacks is dimmed with a stated reason, never hidden"), and the speaker change follows the #4665 ruling. No default changes. All files are Tier 3. Commits are signed. No CHANGELOG.md entry, which is correct.
What I tried to break
- Ran both tests at the PR head:
transmit_seam_drop_notice_testreported0 failure(s)andmixer_control_availability_testreportedPASS. You're right that CI never runs either of them. - My own mutation, different from yours: I restored
main'ssetLineoutMute()(text plus an unconditional seam call). Two rows went red:[FAIL] lineout: no commandDropped where the backend applied itand[FAIL] lineout: no seam call to a backend that declares no line out. The tests pin the routing. - Bridge,
main(10-06 build) and this head, both againstDEMO-0001: I used an isolated settings dir,AETHER_AUTOMATION_NO_TX=1, and connected explicitly to the demo with no autoconnect. I clickedpcAudioBtnoff, then clickedLine out mute. Both builds read back{"lineoutMuted":true,"pcAudioEnabled":false}from thetitlebarmodel and logged no drop. That proves no regression on a command-plane radio, and it's how I found the Sim has one. The blocker above can't be reached in demo mode for the same reason. It is reasoned from code: steps 1–5 above, each line cited. I didn't have an Icom or RTL to click it on. - Checked and fine: the Flex path (
usesFlexCommandPlane()is true, so text only, as onmain); the!m_backendarm, which still sends text soradiomodel_audio_mute_test's contract holds; and ANAN, which now gets the seam only with no false notice. The other mute entry points (master_mute_toggleshortcut, MIDIglobal.masterMute) were and still are PC-only, so they're untouched.
For future reference
masterKnobDrivesLocalOutput()still refuses the MIDI master knob on ANAN with PC Audio off, while the title-bar slider now reaches ANAN's line out through the seam. Teaching it the new record would be a natural small follow-up, not this PR.- The Line Out tooltip says "Its audio plays on this computer". On an Icom with PC Audio off it plays on the rig's own speaker. Something like "this radio has no line out AetherSDR can control" stays true on every family.
Thanks for taking the record route cleanly and for the mutation table. It made the routing quick to check, and the remaining gap is one lambda.
73, Ryan NF0T
👨🏼💻 Co-authored by Claude Opus 5.5
…rough the seam. Principle VIII. The title-bar speaker mutes the radio's line out and the PC sink together (#4665 ruling: muting the inactive path is harmless). On a backend without a command plane (HL2, ANAN, ...) setLineoutMute() and setLineoutGain() still sent the Flex `mixer lineout` text as well as the typed seam call, so every click raised "the radio does not support this" although the seam had carried the request. The text now goes only to a radio with a command plane; the seam call is unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ng command plane. Principle VIII. Review of #6260: only ANAN applies setLineoutGain/Mute, so gating the wire text on "no command plane" silenced the drop notice on HL2, Icom and RTL for a request nothing applied. - RadioCapabilities::lineoutControl, an optional record: Flex and ANAN declare it, every other family states its absence. - RadioModel::lineoutThroughSeam(): a declaring non-Flex backend takes the request typed and no text is sent; otherwise the text goes out as on main, so an undeclared radio keeps its notice. - The title-bar speaker mutes the line out beside this computer's output only where lineoutControlsAvailable() (#4665); Radio Setup's Line Out row is dimmed with its reason elsewhere. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…o has no line out and PC Audio is off. Principle VIII. Review of #6260 (NF0T): on Icom or RTL with PC Audio off, the click skipped setLineoutMute(), and syncTitleBarOutput() then read the unchanged lineoutMute() and snapped the icon back, with no notice. Neither output is AetherSDR's to mute there, so the click now refuses the way the MIDI master knob does (showUnsupportedControlNotice) and resyncs the icon. Every other case is unchanged. Also: Radio Setup's Line Out gate is re-applied on connectionStateChanged and capabilitiesChanged, since the dialog can stay open across a reconnect, and its tooltip no longer claims the audio plays on this computer; the capabilities map says the Sim takes the text through its command plane; and menu-reference.md gains the no-line-out sentence. Rebased onto main (ca5e063). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ae5f874 to
e4feb42
Compare
|
Thanks. The blocker was right: the gate stopped at Addressed in
Built on macOS 26, Qt 6.12.0. The |
NF0T
left a comment
There was a problem hiding this comment.
Re-reviewed at e4feb42c (rebased onto ca5e0634). Everything from my review at ae5f874b is addressed, and nothing new turned up.
What changed, checked against the diff
git range-diff between the two heads shows commit 2 changed by the rebase only in context lines (the DocsLinks.h include and an updated speechProcessorControl row from main). Commit 3 is new and covers every item:
- Blocker, title-bar speaker on Icom/RTL with PC Audio off: the suggestion applied as written (
MainWindow.cpp:2055-2074). With neither a line out nor PC Audio, the click callsshowUnsupportedControlNotice(), thensyncTitleBarOutput()puts the icon back, and it returns. You're right that no extra connected guard is needed:lineoutControlsAvailable()is true while disconnected, so!lineoutalready implies connected. - Radio Setup gate evaluated once: now
applyLineoutAvailability, run at build time and again onconnectionStateChangedandcapabilitiesChanged, withlineoutMuteas the connection context, so it dies with the page. It re-enables the row and clears the tooltip and accessibleDescription when a line out exists. That is safe: these four widgets set neither on their own, so clearing them loses nothing. - Capabilities map: the Sim now reads as "behaves as a Flex here", and the refusal is documented.
- User docs: the
menu-reference.mdsentence covers the no-line-out row and the refusal. "Build docs site" is green. - Tooltip: now "this radio has no line out AetherSDR can control", which stays true on every family.
What I ran
- Rebuilt at
e4feb42c(Windows, MinGW 13.1, Qt 6.12.0), because CI's-Rfilters still don't run these tests and the rebase touchedRadioModelcontext.transmit_seam_drop_notice_testreported 77 PASS,0 failure(s).mixer_control_availability_testreported 36 PASS. All six line-out rows are green. - The routing mutation from my last review still applies unchanged to
RadioModel.cpp, which this push didn't touch, so its result carries over. - Not reproduced: the refusal path itself. It needs an Icom or RTL, because the demo radio has a command plane, as we both found. It is three lines that copy the MIDI master-knob precedent, and I checked them against
syncTitleBarOutput()'sQSignalBlockerpath.
CI is 6/6 green on the head, and all three commits are signed.
Verdict
Approved. All files are Tier 3. I'll squash-merge as:
fix(audio): the title-bar mute reaches the radio's line out only where it has one. Principle VIII. (#6260)
The masterKnobDrivesLocalOutput()/ANAN follow-up stays out of scope, as agreed.
Thanks for a clean turnaround: suggestion taken, every side item handled, and the untested path named.
73, Ryan NF0T
👨🏼💻 Co-authored by Claude Opus 5.5
Seven conflicts, all one cause: aethersdr#6260 (eb9a1fb) added the lineoutControl record on the line where this branch adds txWaterfallClientFlag. Both sides are kept in every file, main's line first, this branch's after it. - src/core/backends/anan/AnanBackend.cpp: lineoutControl engaged (main), then txWaterfallClientFlag absent. - src/core/backends/flex/FlexBackend.cpp: lineoutControl engaged (main), then txWaterfallClientFlag absent. - src/core/backends/hl2/Hl2Backend.cpp: lineoutControl absent (main), then txWaterfallClientFlag engaged with clientPersistsFlag true. - src/core/backends/icom/IcomCivBackend.cpp: lineoutControl absent (main), then txWaterfallClientFlag absent. - src/core/backends/rtl/RtlSdrBackend.cpp: same. - src/core/backends/sim/SimBackend.cpp: same. - docs/architecture/radio-capabilities-map.md: main's lineoutControl row, then this branch's txWaterfallClientFlag row. Everything else merged without conflict. aetherd-touchpoints.md was regenerated and did not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Radio Setup -> Audio -> Radio Audio Outputs: the Headphone row (caption, slider, value, mute) is now dimmed, never hidden, on a connected radio with no command plane (Hermes-Lite 2, ANAN, Icom), using the title bar's own predicate (headphoneControlsAvailable) and its two reason sentences on tooltip and accessibleDescription. - Re-evaluated on connectionStateChanged; neither direction writes the level or the mute. The Line Out row (#6260) is untouched and gated separately. - New socket-free offscreen test pins HL2 and ANAN (real capabilities) dimmed, ANAN Line Out live, and a Demo command-plane positive control. - Refs #5482. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#6388) Refs #5482, #6353 This is the follow-up to the two forward notes in the approving review of #6353. ## Limits, first - **macOS, offscreen platform only** (Qt 6.12.0). The pixels are from `QWidget::grab()` in a test. Nobody looked at the dialog on a screen, and there is no screenshot. - **Two themes rendered: Default Dark and Default Light.** No user theme. - **The Line Out row's styling is touched; its logic is not.** That row is @jcmerg's (#6260). His predicate, his reason text and his `applyLineoutAvailability` lambda are unchanged. Only the three stylesheets of the row gain a `:disabled` rule, the same one the Headphone row gets, so the two rows keep agreeing. - **The Front Speaker row is not touched.** It is never disabled. - **A dimmed Mute looks the same engaged or not** (from this PR's review, `c0bfd883`). An engaged Mute on an unavailable row took the unavailable text on the engaged red background. It now takes the row's own unchecked background (`color.background.1`), so the engaged state is not shown while the row is unavailable. §4a names no look for a control that is unavailable and also has state, so whether the state should stay visible is the maintainer's call. The checked state is still on the widget for accessibility clients. - **Contrast is what the token gives.** `color.control.unavailable` is 2.09:1 (dark) and 1.77:1 (light) by the style guide's own table (§4a). This PR uses the token and does not change it. - **The Line Out reason sentence is not hoisted.** It exists in one place only (`src/gui/RadioSetupDialog.cpp:4774-4775` at `9fabcbf8`); no other source file and no test repeats it. - `Refs`, not `Fixes`: closing #5482 stays your call. ## What was wrong At `9fabcbf8`, in Radio Setup → Audio → "Radio Audio Outputs", a disabled row shows it on the slider only. The other three widgets of each row carry their own stylesheet, and none has a `:disabled` rule: | widget | stylesheet | applied at (Line Out / Headphone) | |---|---|---| | caption label | `kLabelStyleTemplate`, `QLabel { color: {{color.text.primary}}; … }` (`src/gui/RadioSetupDialogCommon.h:31-32`) | `src/gui/RadioSetupDialog.cpp:4740` / `:4794` | | value label | `kValueStyleTemplate`, `QLabel { color: {{color.accent.bright}}; … }` (`RadioSetupDialog.cpp:129-130`) | `:4745` / `:4799` | | Mute button | inline `QPushButton { … color: {{color.text.primary}}; … }` plus `:checked` | `:4751-4753` / `:4805-4807` | | slider | none of its own | `:4742` / `:4796` | An explicit `color` with no `:disabled` rule also applies when the widget is disabled, so the label, the value and the Mute button render the same in both states. The review of #6353 measured 0 differing pixels on them; the test here reproduces that (below). The two headphone reason sentences were written out three times: `src/gui/TitleBar.cpp:1297-1302`, `src/gui/RadioSetupDialog.cpp:4826-4834`, and `tests/radio_setup_headphone_output_gate_test.cpp:134-139`. ## The change **The look.** A disabled label, value or Mute button of the two rows takes `color.control.unavailable`, the token §4a of `docs/style/theme-style-guide.md` names for the unavailable state: ``` QLabel:disabled { color: {{color.control.unavailable}}; } QPushButton:disabled { color: {{color.control.unavailable}}; } ``` This is the pattern already in the tree for the TX timing fields (#6166, `RadioSetupDialog.cpp:2565-2567`, `QLineEdit:disabled`) and for `WfmApplet.cpp:58`. The rules go through `ThemeManager::applyStyleSheet`, so they follow a theme switch like their neighbours. No colour literal is added. The enabled look is unchanged: the rule is appended to the existing template, and only for these six widgets. `makeValueLabel` takes an optional extra rule so `kValueStyleTemplate` keeps its single reference. The shared `applyLabelStyle` is not changed, so no other page of the dialog moves. **The wording.** `src/gui/MixerControlAvailability.h` now holds the two sentences next to the predicate, as `kHeadphoneLevelUnavailableReason` and `kHeadphoneMuteUnavailableReason`. They are `constexpr char[]`, so the header stays free of Qt. `TitleBar::setHeadphoneAvailable`, the Radio Setup lambda and both tests read them from there. The text itself is unchanged, character for character. No predicate, no capability field, no default and no user-visible string changes. `docs/user/docs/audio-settings.md` already says the controls "are dimmed with the reason in the tooltip", which stays true. ## Tests `tests/radio_setup_headphone_output_gate_test.cpp` gains `dimmedRowsLookUnavailableInBothThemes` (offscreen, no socket). For each of Default Dark and Default Light it builds the dialog on the HL2 capabilities (both rows dimmed) and on the Demo (both rows live), and for the label, value and Mute button of each row: - renders the widget's own region with `grab()` and counts differing pixels, exactly, no tolerance. It requires more than 0 between dimmed and live; - first asserts the two renders are comparable: same size, same value text, same mute state. So the enabled state is the only difference; - control 1: the same widget rendered twice differs in 0 pixels; - control 2: after a disconnect the same HL2 widgets are live again and render pixel-identical to the Demo's, and different from their own dimmed render; - requires the resolved stylesheet to carry the colour `color.control.unavailable` has in the active theme. With the change, differing pixels (measured at `6c83ac5a`, the same diff before it was rebased onto current `main`; the test that asserts them passes at the head), dimmed vs live, identical in both themes: | region | Headphone | Line Out | |---|---|---| | label | 1329 / 6840 | 852 / 6840 | | value | 410 / 2280 | 410 / 2280 | | Mute | 512 / 3800 | 512 / 3800 | **Fail-before.** Named mutation: this commit with both `:disabled` rule strings emptied, which is main's styling. Full build and ctest. All twelve regions read 0 differing pixels, as in the review: ``` PIXELS Default Dark | Headphone: label: dimmed vs live 0 / 6840 (same widget twice: 0) PIXELS Default Dark | Headphone: value: dimmed vs live 0 / 2280 (same widget twice: 0) PIXELS Default Dark | Headphone: mute: dimmed vs live 0 / 3800 (same widget twice: 0) PIXELS Default Dark | Line Out: label: dimmed vs live 0 / 6840 (same widget twice: 0) PIXELS Default Dark | Line Out: value: dimmed vs live 0 / 2280 (same widget twice: 0) PIXELS Default Dark | Line Out: mute: dimmed vs live 0 / 3800 (same widget twice: 0) (the same six lines for Default Light) FAIL! : RadioSetupHeadphoneOutputGateTest::dimmedRowsLookUnavailableInBothThemes() 'failures.isEmpty()' returned FALSE. (Default Dark | Headphone: label: dimmed renders as live Default Dark | Headphone: label: not the unavailable token #3a4a5a … Totals: 5 passed, 1 failed, 0 skipped, 0 blacklisted ``` The test could not be run on unmodified main as it stands, because it reads the sentences from the header. That is why the fail-before is a mutation and not "the test alone". **The wording.** `radio_setup_headphone_output_gate_test` compares the row against the header's two constants. `tests/mixer_control_availability_test.cpp` gains two rows: the title bar's mute and slider descriptions equal the header's sentences. Its existing rows still check the content independently ("Unavailable:", "no headphone output", "speaker button", "master volume"). ## ctest macOS (Apple Silicon), Qt 6.12.0, `ENABLE_ASR=OFF`, full suite at `-j8`, failures rerun serially, on `c0bfd883` (this PR's head): - **765 tests, 763 passed, 2 failed**, both also on the serial rerun: the dialog test below, and `vkamp_connection_test` with only its two known checks (#5254). - `peripheral_auth_dialog_interaction_test` fails, at `-j8` and serially: "Clear code or Status is clipped at the default dialog size", `dialog 960x680, viewport 658, page minimum 784`. This is the host-dependent failure of #6008 (diagnostic in #6134): the Peripherals page is as wide as the longest serial-port name on this Mac. The page minimum is 784, the same figure as on clean main here. This PR does not touch the Peripherals page. - `radio_setup_headphone_output_gate_test` (6 passed), `mixer_control_availability_test`, `titlebar_headphone_mute_test`, `unified_title_bar_test` and `radio_setup_label_theme_token_test` pass. Static checks, run locally: `tools/check_a11y.py` on `RadioSetupDialog.cpp` 9 findings before and 9 after, `TitleBar.cpp` 0; `tools/audit_colours.py` summary identical before and after; `gen_touchpoint_manifest.py --check` and `docs/gen_reference.py --check` up to date; the `tools/check_*.py --strict` set passes (`check_codeql_coverage.py` needs a build directory and was not run). 73, ON8ST 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… and re-take the Stable snapshot. Principle VIII. (#6396) ## Summary This brings the user guide (docs.aethersdr.com) up to date with everything in v26.10.2 and re-takes the Stable snapshot so the default site shows it. Docs only, apart from the TCI page prose in `tools/docs/gen_reference.py`; no application source, test or CI changes. Every page was audited against the 106 PRs in the release. Every fact that was added or kept was checked against the source at `f753dbfc4`, the v26.10.2 release commit, not taken from the PR bodies alone. ## What changed - **Menu paths.** - AetherModem, Callsign Lookup, AetherMap and FreeDV Reporter are on **Tools**. They are created on View and then moved to Tools (`MainWindow_Menus.cpp:1519-1520`, `:1569-1575`); the style guide's example says **Tools → AetherMap...**. - **Show live CW contacts** is on **View**. - Waveforms is on **Tools**, not File. - The VUDU applet button was called TXDSP. - Labels now match the source's `...` vs `…`. - **Gaps from this cycle.** - **Windows and Network Diagnostics:** Windows Firewall rules and the Network Diagnostics Windows Firewall page (#6289); the `aether.network` and `aether.nvafx` log categories (#6289, #6308). - **Remote access:** Tailscale shim revocation semantics (#6325). - **Sleep, App Nap and power throttling (#6195):** per-OS notes. - **Hermes-Lite 2:** state restore, RX audio at unity, CW at TUNE level, multi-interface discovery and a discovery troubleshooting entry (#6259, #6254, #5975, #6009, #6293). - **ANAN-G2:** AGC-T range, a "What is remembered" section, and supply voltage and link status (#6282, #6129, #6137, #6211). - **NB2** on the HL2 and ANAN-G2 (#5966). - **DVK:** WAV import requirements, the slot menu without Delete, status text and licensing (#6247, #6262, #6251). - **Copy Assist** from unprocessed audio (#6216). - **Live CW contacts** replace the decoder's contact card (#6268). - **Status-bar indicators** work from the keyboard (#6267). - **Volume:** controllers, hotkeys and TCI `volume` follow the active output (#6129, #6260). - **Peripherals:** the KPA1500 (#5905), CTR2 link extensions and the AetherKnob (#6338), TMate 2 udev (#6250), MIDI RADE Modem and FDV modes (#6031, #6038). - **RTL-SDR:** live PPM (#6082). - **Icom:** COMP and the MK2 squelch scale (#6174, #6184). - **Known issues.** Entries whose GitHub issues are closed are removed (each checked with `gh issue view`). As decided, this includes three closed today without a demonstrated fix: #5107, #4824 and #5665. - **US spelling.** `docs-style-guide.md` asked for British spelling; it now asks for US, by the maintainer's decision, and every page is converted. The generated TCI pages are converted at their source, the prose strings in `tools/docs/gen_reference.py`. Code spans, links and URLs were left alone. UI labels were checked against the source strings, and none of them are British. Two in-page anchors and two Log Analyzer rule anchors (`#bnr-is-grayed-out-…`) follow the renamed headings. - **Stable snapshot.** `snapshot_stable.py 26.10.2` was re-run on the release tree with these corrections, before `main` was merged in, so Stable carries no post-release docs. `docs/` also has `main`'s post-release pages, in US spelling. Stable alone keeps the IC-9700 waterfall-rate entry (#5996): #6402 fixed it after the 26.10.2 tag, so 26.10.2 still has it. The PDF manual already attached to the v26.10.2 release keeps the tag's docs. ## Validation - `npm ci && npm run build` in `docs/user`: success. This is the broken-link and broken-anchor check. - `node tools/docs/test_log_rules.mjs`: 707 checks passed for 45 rules. - `python3 tools/docs/gen_reference.py --check`: up to date (8 pages). - `git diff --check`: clean. - Menu paths on a build of this branch (offscreen, isolated settings, no radio), `menu list`: `Tools → AetherModem...`, `Tools → Callsign Lookup...`, `Tools → AetherMap...`, `Tools → FreeDV Reporter...`, `Tools → Waveforms...`, `View → Show live CW contacts`. ## Screenshots now out of date (not recaptured) These need a recapture pass on the radio: - **Old panadapter title strip (#6386):** `panadapter`, `main-window`, `first-session-*`, `multi-slice-two-slices`, `split-pair`, `tnf-markers`, `offscreen-vfo-indicator`, `pan-3d-stacked-trace`, `default-light-theme`. - **Pre-canon windows (#6334, #6339, #6341, #6372):** `aetherrx*`, `aethertx*`, `aethermodem-aprs`, `bnr-panel`, `connect-dialog`, `connect-by-ip-card`, `connect-icom-rows`, `connect-smartlink-card`, `rx-agc-calibration`. - **Other:** - `title-bar` (lowercase tab states, #6281); - `menu-help` (#6294); - `display-panel` (#6371); - `support-diagnostics-dialog`; - `waveforms-dialog` and `tailnet-remote-access` (show shim 0.4.0); - `about-window` (v26.10.1); - `radio-setup-qrz`. ## Found during the audit, not fixed here - **App string bug:** `TailnetShimDialog.cpp:628` tells the user to "Connect remotely with Connect → Manual"; the real path is **File → Connect to Radio... → Connect by IP**. - **ANAN AGC-T tooltips:** the RX applet and VFO tooltips show the slider position as dB, which is wrong on ANAN. #6282 lists this as a follow-up. - **MIDI Master Volume on non-Flex radios:** it is refused while PC Audio is off, whereas hotkeys, FlexControl and HID drive the line out. The docs describe the code as it is. - **RTL mode names:** `rtl-sdr.md` writes "FM-N" and "CW-R", but the source strings are `FMN` and `CWR`. Left as is until someone confirms what the mode menu displays. - **IC-705 COMP readback:** inferred from the MK2 (#6174) and not verified on an IC-705. - **Not yet documented:** the #6230 Radio Health row "Forward responses skipped", and the Store/MSIX package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
On a Hermes-Lite 2, every click on the title-bar speaker (mute or unmute) shows the notice that the radio does not support the function:
The click mutes this computer's output (
AudioEngine::setMuted) and sendsRadioModel::setLineoutMute(). On a radio without a command plane that call sends the Flex textmixer lineout mute N,sendCmd()drops it, and the drop notice fires. The HL2 has no line out of its own, so there was never anything to mute there.Revised after review. The first version gated the text on "no command plane", which also silenced the notice on HL2, Icom and RTL for a request that nothing applies (only ANAN implements the line-out seam). That reversed the convention
transmit_seam_drop_notice_testpins for VOX/MON/PROC, so it is replaced by a declared record.Change
RadioCapabilities::lineoutControl, an optional record, thetxMonitorControlshape. The radio has its own line out: Flex by its command plane, otherwiseIRadioBackend::setLineoutGain/Mutemust apply it.RadioModel::lineoutThroughSeam(): a declaring non-Flex backend takes the request typed and no text is sent. Otherwise the text goes out exactly as onmain, so an undeclared radio keeps its drop notice.lineoutControlsAvailable()inMixerControlAvailability.h:!connected || hasCommandPlane || declaresLineout.MainWindow_Controllers.cpp).connectionStateChanged/capabilitiesChanged, so the dialog follows a reconnect to another radio.lineoutControlrow.menu-reference.md, "Speaker and master slider", says what the speaker does on a radio with no line out.Headphone gain and mute (#5482) are not touched, including Radio Setup's ungated headphone Mute.
Evidence
transmit_seam_drop_notice_test:lineoutReachesDeclaredSeamWithoutDropNotice: with the record, mute, unmute and gain reach the backend typed, with no notice.lineoutWithoutRecordKeepsDropNotice: with the HL2's answers, there is no seam call and the notice stands for bothmixer lineout muteandmixer lineout gain.mixer_control_availability_test: four rows for the line-out predicate (HL2 unavailable, ANAN available, Flex available, disconnected available).Mutations, each red:
lineoutThroughSeam()ignores the recordlineoutThroughSeam()→falselineoutControlsAvailable()ignores the recordAlso green locally:
anan_backend_test,radiomodel_audio_mute_test,check_capability_records --strict(70 booleans, at the frozen 70).Not run:
transmit_seam_drop_notice_testandmixer_control_availability_testare not in aci.yml-Rfilter, so CI's green does not cover them; the results above are local (macOS 26, Qt 6.12.0).CHANGELOG.mduntouched.