Repository navigation
fix(hl2): forward-power window skips non-measurements. Principle VIII. - #6230
Conversation
For about 2 ms after the T/R switch-over a Hermes-Lite 2 (gateware 74.2) reports slow-ADC words that are not measurements. In one of 34 recorded key-ons the forward word in such a response was 2944 counts on a carrier of 1649; the window maximum took it and the peak hold showed 3.82 W on a 1.23 W carrier for seconds. Those responses carry a temperature word of 0 in the same RADDR-1 word: 54 responses at 27 of 34 key-ons, none in 292842 responses elsewhere. ForwardPowerWindow::observe now skips a response whose DATA[31:16] is 0. No timer, no threshold. A window left empty falls back to the last value, as before aethersdr#6019. The last-value fields are unchanged. Tests replay the recorded RADDR-1 words of three key-ons through MetisClient's receive loop and Hl2Backend::publishTelemetry. Refs aethersdr#5198 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Thank you for one of the most honestly-framed PR bodies I have reviewed on this repo. Every number in it that I could check independently checks out, and the "Limits first" section pre-empted most of what I went looking for. The one thing that changed my verdict is not in your diff — it is a triage comment that landed on #6229 52 minutes after you opened this PR, so you could not have seen it.
1. Issue fit — Partially
Refs #6229 (the measurement), Refs #5198 (explicitly not explained — correctly stated), and it self-corrects your own #6019. Against #6229 the PR implements Option 1 exactly as the issue wrote it, and that works for the one measured defect.
But #6229 carries the maintainer-review label, and its triage comment (2026-10-06 11:21, after this PR at 10:29) rules the other way on placement:
"This is the one place I would scope the fix wider than option 1 as written. … Apply it in
MetisClient.cpp's receive loop before bothm_telemetry.apply(*resp)andm_fwdWindow.observe(*resp), so the window peak, the last-value forward count, the SWR pair and the PA-temperature pole are all protected by one gate."
I did not take that on faith — I reached the same two residual paths from the code before I read it, and I verified its arithmetic. See "Needs maintainer decision" below. Requirements map:
| Requirement (#6229 + its triage) | This PR | Verdict |
|---|---|---|
| Reject temp-0 RADDR-1 from the forward-power window (Option 1) | MetisProtocol.h:487-488 |
Met |
Rule pinnable in hl2_metis_protocol_test without a socket |
Met, but as an inline if rather than the named free predicate the triage asked for |
Partly |
Gate applied before both apply() and observe() |
Accumulator only | Not met — the triage's central ask |
| Last-value forward count, SWR pair, PATEMP pole protected | Explicitly left (body + MetisProtocol.h:486) |
Not met, disclosed |
| Justify via the sensor formula (0 counts = 0 V, no working sensor reaches it) | Comment gives the −50 °C value, not the reasoning | Partly |
| Check the RADDR-2 reverse word in the 30 bias-4095 responses | Not done | Open (triage calls it non-blocking) |
| Count the drops, surfaced in Radio Health | Not done | Open (triage calls it non-blocking, "cheap") |
Test layer is correct. Wire-level decode rule and client-side accumulation → socket-free CTest, which is what both test files are. No fake peer, no socket. Nothing to ask for here.
Socket tests: none added, modified, or removed. I grepped the two changed test sources and the new fixture header for QUdpSocket/QTcpSocket/bind(/listen(/connectToHost/Fake* — nothing. The replay drives MetisClient::handleDatagram directly through MetisClientTestAccess, no socket involved.
2. Scope
| File | What it changes | Claimed? | Verdict |
|---|---|---|---|
src/core/backends/hl2/MetisProtocol.h |
The 2-line guard + its comment; one word added to the struct's doc comment | Yes | In scope |
tests/Hl2KeyOnRaddr1Fixture.h |
New 717-line recorded-data header, 3 captures | Yes | In scope; not dead — tests.cmake:1763 gives hl2_backend_seam_test PRIVATE src tests, so the include resolves |
tests/hl2_backend_seam_test.cpp |
replayKeyOn + forwardPowerKeyOnReplay, registered at line 706 |
Yes | In scope |
tests/hl2_metis_protocol_test.cpp |
5 leaf checks on the new rule | Yes | In scope |
Everything in the diff is explained by the issue. No new public surface, no settings key, no capability field, no CMake change, no CHANGELOG.md entry (correct — don't add one). No - lines beyond the one reworded comment; no guard, early return or confirmation removed. Checked the sibling accumulator paths: ForwardPowerWindow is the only window of its kind, and hl2_fwd_window_wiring_test builds every RADDR-1 response through fwd() (tests/hl2_fwd_window_wiring_test.cpp:59-62), which hardcodes temperature 1234 — so your claim that the #6019 tests are unchanged and still pass holds, statically verified.
3. Blockers
None. The change is correct for what it claims, degrades gracefully, and introduces no new risk.
4. Needs maintainer decision
1. src/core/backends/hl2/MetisProtocol.h:487-488 — the guard sits in the accumulator, not at the seam, so two paths carry the same bad word through.
if ((r.data >> 16) == 0)
return;You disclose this (MetisProtocol.h:486: "The last-value fields in apply() still take it") and you list the wider gate under "Options not built" with a real reason — a unit that always reads 0 would lose forward power, SWR and temperature outright instead of degrading to last-value. That objection is sound and the triage underweights it; its only mitigation is visibility ("count the drops"), not safety. So this is a genuine author/triage disagreement for a human to settle, not a defect. What I confirmed independently at the PR head:
- PATEMP.
MetisProtocol.cpp:393setstemperatureRaw = 0;Hl2Backend.cpp:7619testsif (t.temperatureRaw)— the optional, not the value — so 0 passes.hl2TemperatureCelsius(0)=(3.26·0/4096 − 0.5)/0.01= −50.0 °C exactly (MetisProtocol.h:661-664), folded into the α = 0.2 pole atHl2Backend.cpp:7623-7628(kPaTempAlpha,Hl2Backend.h:1069). From a 40 °C display that is a ~18 °C single-step dip decaying over ~5 publishes (~0.5 s) — so your body's "can still show a raw 0 for one publish" understates the tail, because the pole stretches it. It needs the 10 Hz emit to land in the ~656 µs after a marked response, so ~2% of key-ons; rare, visible when it hits, and pre-existing. - Last-value forward.
*t.forwardPowerRaw= 2944 still reachesswrFromRaw()(Hl2Backend.cpp:7568-7571, and 2944 clearskMinForwardCountsForSwr= 320), Radio Health (6218-6235) and the bridge. Same ~2% reachability.
2. The RADDR-2 hazard the captures may already answer. #6229 reports bias = 4095 (0x0FFF, 12 bits saturated) at 30 key-ons, and MetisProtocol.cpp:397-398 puts reversePowerRaw in the upper half of that same word. If reverse is junk while forward is a genuine carrier, swrFromRaw() spikes and kMinForwardCountsForSwr cannot gate it, because forward is real. That is a worse artifact than the one fixed here — a false high SWR reads as an antenna fault. You already hold the captures that would settle it. Maintainer's call whether it belongs in this PR or a successor.
5. Nits (non-blocking)
-
src/core/backends/hl2/MetisProtocol.h:459-462— the field's own contract doc is now stale. You updated theForwardPowerWindowcomment at 472-474 but not the one consumers actually read:// Maximum of the publish window's non-ACK RADDR-1 DATA[15:0] (the radio // re-samples forward power every other EP6 response, control.v:261, so the // last value alone misses speech peaks); nullopt when the window saw none. // `forwardPowerSamples` counts them; the window length is `adcWindowMs`.
Drop-in:
// Maximum of the publish window's non-ACK RADDR-1 DATA[15:0] over responses // that carry a measurement (ForwardPowerWindow::observe skips the rest; the // radio re-samples forward power every other EP6 response, control.v:261, so // the last value alone misses speech peaks); nullopt when the window saw none. // `forwardPowerSamples` counts the ones kept; the window length is `adcWindowMs`.
The
sampleshalf matters:forwardPowerSamplesis the denominator the debug line atHl2Backend.cpp:7606-7608calls "RADDR-1 samples", and it now excludes the skipped ones. -
MetisProtocol.h:483-486— the comment states the value, not the reason. The triage's framing is the stronger one and worth keeping in the tree, because it is what makes the marker principled rather than fitted to one unit: the sensor formula puts 0 °C at 0.5 V, so 0 counts is 0 V, which no working sensor produces. One clause after "-50 C by hl2TemperatureCelsius" — e.g. "(0 counts is 0 V, and the sensor puts 0 C at 0.5 V, so no working sensor reads it)" — pre-empts the "why not a threshold?" question permanently. -
No counter for the drops. The triage asks for one beside
forwardPowerSamples, surfaced in Radio Health, so that a unit with a dead temperature channel is visibly degraded rather than silently back to pre-#6019 behaviour. Your test pins that fallback ("a window of marked responses only has no peak"), which is the graceful half; the counter is the observable half. Cheap, and it would also tell you whether a second unit behaves like yours. -
tests/hl2_backend_seam_test.cpp:570— "Nothing is keyed here" reads wrong beside line 509.replayKeyOndoesAccess::setKeyedFlag(b, true)for the whole replay. I take your meaning as "no transmitter, no radio" — worth saying that way, since the flag state is load-bearing for whichpublishTelemetrybranch the replay exercises. -
tests/hl2_backend_seam_test.cpp:494, 520—publishedTopis the plain max, not the published one. It is documented "highest forward word in any published window" but assigned fromplainMaxat 520, i.e. pre-guard. Harmless today because arms 2 and 3 assertpeakDiffersFromPlainMax == 0alongside it, so the two coincide — but the name invites a future reader to treat it as the published value in an arm where they diverge.plainTopwould say what it is.
6. What I tried to break
- Reproduced your headline numbers from the fixture and the curve, independently.
directionalWatts(2944)interpolates{2616.77, 3.0086}→{3173.82, 4.3927}= 3.822 W;directionalWatts(1638)(the capture's own max at ≥100 ms) = 1.211 W;3621→ 5.715 W;1374→ 0.879 W. All four match your table to the digit, so the mutation claim "guard removed → 3.822 W" is arithmetically entailed by the fixture, not merely asserted. - Audited the fixture for hand-editing. Parsed all three arrays: 1539 / 625 / 625 rows (matching each header), timestamps strictly monotonic, inter-sample gaps clustered at 656 µs median — which is the 384 kHz rate the headers claim, i.e. 8× the "~190/s at 48 kHz" in the production comment, so those two figures are consistent rather than contradictory. The temp-0 rows are exactly the ones documented, with exactly the stated forward words
(2572,0)(3226,2944)/(1868,0)(2531,0)/(1877,0)(2531,0)(3180,0). Nothing looks synthesised. - Cross-checked #6229 against the body for drift. #6229 says 61 responses at 28 key-ons spanning 1.74–5.23 ms; the body says 54 at 27 key-ons spanning 1.74–4.05 ms. Not a contradiction — the body's figures are the temp-0 subset, so a narrower span and a lower key-on count are exactly what you'd expect. No finding.
- Tried to make the guard eat a real sample. Walked the three arms: the TUNE captures' marked rows all carry forward 0, so
peakDiffersFromPlainMax == 0is a real constraint and not vacuous; a window containing only marked rows would make the peaknulloptagainstplainMax == 0and fail that check, but the marked rows sit 654 µs apart inside a 100 ms window, so it cannot arise in these captures. The synthetic arm (5263 µs spacing = 190/s, 380 rows ≈ 20 windows, asserting>= 19) does cover one-sample peaks surviving. - Checked whether the test only passes because the keyed flag is pinned. It isn't a cheat:
m_keyedis set inapplyKeying(Hl2Backend.cpp:4218), the client's own key-command path, not from the radio's PTT echo — so at MOX + 3.2 msm_keyedis already true and the real code takes the keyed/peak branch the replay exercises. The unkeyed last-value branch is not the one at risk at key-on. - Verified the pre-existing leaf tests would have broken if the guard were too broad — and that they don't, because
fwd()hardcodes temperature 1234. Had they used a bare 16-bit word, this PR would have turned them red. - Sanitizer-lane cost. The replay pushes ~32k synthetic 1032-byte datagrams through
handleDatagramand constructs ~40Hl2Backendobjects.hl2_backend_seam_testdeclares no explicitTIMEOUT(tests.cmake:1762-1765), so it takes the retrofit default (≤300 s, ×AETHER_SANITIZER_TIMEOUT_SCALE= 4 under instrumentation) rather than opting out of it — headroom is fine, no timeout risk. - Governance. Read CONSTITUTION I/II/III/V/VI against the diff and found nothing to cite:
parseEp6Responseandapply()are untouched, so no wire semantics are invented — the guard only declines to trust a sentinel inside a client-side display accumulator, which is where a presentation policy belongs. No persistence, no capability, no credentials, no TX authority, no dialog, no a11y surface. - CI. All five checks green at
7bae2610. But no HL2 test runs in the per-PR gate — the onlyctest -Rsteps inci.ymlare the digital-voice set,cross_needle_meter_test,mac_nr_filter_testandasr_gpu_probe_test. So green here means it compiles on three platforms, and your local full-suite run is the only test evidence. Your three disclosed failures are all reproducible on cleanmainand none touches this diff.
Not verified / unavailable: everything above is reasoned from source at the PR head — I have no build, no test run, and no radio, so I did not execute the replay or confirm your mutation results. Your own "nothing was keyed, nothing observed on a radio" stands, as does the single-unit, single-gateware basis for the marker. One further check I'd value that neither of us has: a second unit's key-on capture, to see whether its non-measurement responses also zero the temperature word.
security-audit (CodeGuard): clean on MetisProtocol.h — the guard adds no read and no new arithmetic (r.data is uint32_t, so >> 16 is well-defined; static_cast<int>(r.data & 0xFFFF) is 0..65535). It also walked the EP6/EP4/discovery bounds in this header and found them all gated. Separately it noted that MetisClient::onReadyRead() accepts EP6 datagrams without comparing senderAddress() to the connected radio — pre-existing, unchanged by this PR, and the parsers are bounds-safe regardless; route separately if it's worth tracking.
7. Recommendation
Needs maintainer decision — on seam placement only. The code is correct, the evidence is unusually well-documented, and I found no blocker. The open question is whether to take this narrow accumulator-level guard now and track the last-value paths (PATEMP's −50 °C step, the SWR pair, Radio Health) plus the RADDR-2 bias-4095 hazard as follow-ups, or to ask for the single seam-level gate the #6229 triage proposed. Both positions are defensible and the author's objection to the wider gate — that a unit with a dead temperature channel would lose forward power, SWR and temperature outright rather than degrade — is the real trade-off, so this wants a human and not a reviewer.
If the narrow fix is taken as-is, the only thing I'd ask for before merge is nit 1 (the stale forwardPowerPeakRaw contract comment, drop-in above), since that comment is what the next person will read as the field's definition. Everything else is optional.
🤖 aethersdr-agent · cost: $16.9727 · model: claude-opus-5
|
Antares Detector — candidate vulnerable file(s)
The code parses HL2 forward-power responses (bits 0x1e-0x20) into forwardPowerRaw/reversePowerRaw and then uses them in telemetry/control logic. The parsing logic reads those fields and assigns values, but the protocol/implementation skips non-measurement forward-power responses. This behavior can cause non-measurement RRs to be ignored rather than treated as corrupt, which is a misuse of untrusted HL2 data (a functional/security issue) rather than an obvious memory-safety vulnerability. 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: $18.7892 · model: claude-opus-5 |
Hl2Telemetry::forwardPowerPeakRaw and forwardPowerSamples are the fields consumers read. Their comment said every non-ACK RADDR-1 response counts. Since 7bae261 the window skips responses that are not measurements, so the peak is over the kept ones and the sample count is the kept ones. Comment only. Text as proposed in the review of aethersdr#6230. Refs aethersdr#6229 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule 'a non-ACK RADDR-1 response whose DATA[31:16] is 0 is not a measurement' was an inline test in ForwardPowerWindow::observe. It is now the free predicate isNonMeasurementRaddr1() beside the window, which the window calls, so the same rule can be applied elsewhere by one call. Its comment carries the reason and not only the value: the sensor formula puts 0 C at 0.5 V, so 0 counts is 0 V, which no working sensor reads. That is why the rule needs no threshold and no per-unit time. No behaviour change. hl2_metis_protocol_test pins the predicate directly: 5 checks and 2 static_asserts (ACK, RADDR 0/2/3 and any non-zero temperature word are not marked). Asked for in the triage of aethersdr#6229 and the review of aethersdr#6230. Refs aethersdr#6229 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ForwardPowerWindow skips a RADDR-1 response that is not a measurement. A unit whose temperature word reads 0 all the time would have every response skipped: the peak is then absent and the gauge falls back to the last value, as before aethersdr#6019, with nothing to show for it. The window now counts what it skips. The count rides the telemetry emit as Hl2Telemetry::forwardPowerSkipped beside forwardPowerSamples, and the backend keeps a session total, published as one Radio Health row, forwardPowerSkippedTotal, in the directional-coupler section. The row is absent until a RADDR-1 response arrived, so 0 is a reading. The debug line that prints the window's sample count prints the skipped count too. Nothing is driven by the count. Where the rule is applied is unchanged. Tests: the count rises on a marked response and on nothing else; a window of marked responses only shows a count and no peak; the emit carries it and the next window starts at zero; the health row is absent, then 0, then the session total; the tone replay shows exactly the recording's marked responses and the synthetic speech replay shows 0. Asked for in the triage of aethersdr#6229 and the review of aethersdr#6230. Refs aethersdr#6229 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two names in hl2_backend_seam_test read wrong beside the code. 'Nothing is keyed here' sat above a replay that sets the backend's keyed flag for its whole length. No transmitter and no radio exist in the test; the flag is set because it selects the window-peak branch of publishTelemetry, which is the branch under test. The comment says that. KeyOnReplay::publishedTop was assigned from the plain maximum of the words fed, before the window skips anything. It is plainTop now. The checks that use it assert that the shown value equals the plain maximum, so no check changes meaning. No check added or removed. Both from the review of aethersdr#6230. Refs aethersdr#6229 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Bad news first.
What the four new commits do (
Seam placement is left for the maintainer. The trade-off in two lines:
With the predicate, the move is one call in Two figures in the review I read differently, from the three captures in the fixture (384 kHz, emit phase taken as uniform, which is not measured):
The other citations I checked at Checks at
"GREEN" is our local runner's word with those two failures waived; it is not 646 of 646. Mutations, each a scratch commit on
The two predicate mutations also delete the two Static checks: every step of Not verified
On the detector note: a skipped response is no longer silent. The new row counts every one. 73, ON8ST |
Ozy311
left a comment
There was a problem hiding this comment.
Re-review of the four commits since the aethersdr-agent review at 7bae2610 (head ba223304).
Every non-blocking item from that review is taken:
- The stale contract comment.
Hl2Telemetry::forwardPowerPeakRawnow says it is the maximum over responses that carry a measurement, and thatforwardPowerSamplescounts the ones kept (MetisProtocol.h:459-463). - The rule is named.
isNonMeasurementRaddr1()is aconstexprfree predicate (MetisProtocol.h:476-483), and its comment gives the sensor-formula reason the triage asked for: 0 counts is 0 V, and no working sensor reads 0 V. - The drops are counted.
ForwardPowerWindow::skippedflows throughHl2Telemetry::forwardPowerSkippedinto a session total, which Radio Health shows asforwardPowerSkippedTotal. The row is absent until a RADDR-1 response arrives, so a 0 there is a real reading, not a default. The total is cleared inapplyRestoredState()and the per-window count inMetisClient::start(). - The test names. "Nothing is keyed here" and
publishedTopare renamed.
Verified: I built hl2_metis_protocol_test with MetisProtocol.cpp standalone on the PR head (Qt 6.12, Linux), and all checks pass. As a mutation, I made isNonMeasurementRaddr1() return false, and the test then fails to compile on its static_assert at line 842, so the rule is pinned at compile time. The branch merges cleanly with current main. Not run: hl2_backend_seam_test and hl2_fwd_window_wiring_test, which need the core build.
Still open, and not mine to settle: the seam-placement question. Should this rule apply only to the peak window, or before both apply() and observe()? With the counter in Radio Health, a unit that loses its temperature channel now shows up as degraded instead of silently falling back to last-value readings, which was the visibility half of the triage's ask. The −50 °C PA-temperature dip and the last-value SWR path are unchanged and disclosed. The RADDR-2 bias-4095 question is also still unanswered.
No new findings.
73, K6OZY
Generated by Claude Code
jensenpat
left a comment
There was a problem hiding this comment.
Verdict
Approve. Fits #6229 option 1: the forward-power window skips a non-ACK RADDR-1 response whose temperature word is 0. Reviewed ba2233047e98167dbc8bdc29ed09bd0742620956.
The change does not cross the radio seam. Every production edit is under src/core/backends/hl2/. isNonMeasurementRaddr1 and ForwardPowerWindow stay in MetisProtocol.h. Nothing above src/core/backends/ gained a vendor include, an IRadioBackend method, a capability, a family-string branch, or a Flex wire command. The new fact leaves the backend only as the existing TX:FWDPWR meter and as one string row on IRadioBackend::HealthSnapshot (forwardPowerSkippedTotal). Flex, Icom, ANAN, RTL, and sim are untouched. ConnectionPanel.cpp already includes MetisProtocol.h; this diff does not touch it. Static checks, including EB3, are green on this SHA.
The earlier question about filtering before Hl2Telemetry::apply() is still open and is not a seam crossing. That gate would also live in MetisClient. This PR keeps the skip inside the peak window, which is what #6229 option 1 asked for. Last-value temperature, SWR, and the unkeyed forward word can still see a marked response for one publish. That is disclosed in the PR body and stays inside Hl2Backend::publishTelemetry.
Scope
| Path | What it does |
|---|---|
src/core/backends/hl2/MetisProtocol.h |
Predicate and window skip; skipped count on the window |
src/core/backends/hl2/MetisClient.cpp |
Copies the count onto the 10 Hz emit; clears it in start() |
src/core/backends/hl2/Hl2Backend.h, Hl2Backend.cpp |
Session total on the existing health snapshot; cleared in applyRestoredState() |
docs/automation-bridge.md |
Describes the health key |
tests/Hl2KeyOnRaddr1Fixture.h, tests/hl2_* |
Recorded key-ons and checks in the three existing socket-free tests |
No CHANGELOG entry. No new test binary. The replay calls handleDatagram and does not call MetisClient::start(), so it does not bind the UDP socket.
Verification
- Fixture at this SHA: tone 1539 rows, TUNE 100% 625, TUNE 10% 625; timestamps monotonic; median gap 656 µs; temperature-0 rows are
(2572,0)/(3226,2944),(1868,0)/(2531,0),(1877,0)/(2531,0)/(3180,0). - Built
hl2_metis_protocol_test,hl2_fwd_window_wiring_test, andhl2_backend_seam_testfrom this SHA (Qt 6.12.0). CTest 3/3 passed. The tone replay prints carrier maximum 1.211 W, shown at most 1.211 W, plain window maximum 3.822 W. TUNE 100% shows 5.715 W and TUNE 10% shows 0.879 W, equal to the plain maximum in every phase. - Mutation, restored afterward: dropping the
returninForwardPowerWindow::observemakeshl2_metis_protocol_testfail five checks at runtime, including the marked-word peak. Restored header matches this SHA and the same test passes again. - CI
build,check-macos,check-windows,Static checks, andSanitizer option configuresare success on this SHA. Those jobs compile; they do not run these HL2 tests.
Non-blocking follow-up
A receive-loop skip before apply() would also keep the marked word out of temperatureRaw, the SWR pair, and the last-value forward word. It still belongs in MetisClient. A unit whose temperature word is always 0 would then lose those readings rather than fall back, which is why this PR does not do it. The seven key-on responses with a wrong non-zero temperature word are still accepted. Neither is a merge prerequisite.
… 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>
Refs #6229, Refs #5198.
Limits first
What happens
For about 2 ms after the T/R switch-over, the radio's slow-ADC words in RADDR 1 and RADDR 2 are not measurements (#6229 has the counts). In one of 34 recorded key-ons the forward word was 2944 counts on a carrier of 1649.
ForwardPowerWindowtook it as the window maximum and the peak hold showed 3.82 W on a 1.23 W carrier, falling over 3 s.The change
ForwardPowerWindow::observeskips a non-ACK RADDR-1 response whose temperature word, DATA[31:16], is 0. The rule is the free predicateisNonMeasurementRaddr1()inMetisProtocol.h. The sensor formula puts 0 °C at 0.5 V, so 0 counts is 0 V, which no working sensor reads. The window counts what it skips; the count rides the telemetry emit asforwardPowerSkippedand the backend publishes its session total as one Radio Health row,forwardPowerSkippedTotal. Four source files:MetisProtocol.h,MetisClient.cpp,Hl2Backend.h,Hl2Backend.cpp.nulloptandpublishTelemetryfalls back to the last value, as before fix(hl2): forward power meter keeps the loudest sample per window #6019: low on speech, never high.forwardPowerSkippedTotalthen rises all the time, which is how that unit shows.forwardPowerRaw, SWR, temperature, reverse power, bias and the existing Radio Health rows are unchanged. One row is new:forwardPowerSkippedTotal, "Forward responses skipped, not measurements (since connect)", absent until a RADDR-1 response arrived. TheHL2 directionaldebug line prints the window's skipped count. Nothing else above the backend seam, no capability, no setting.Options not built: a wider marker (temperature word jumped by more than a threshold); a fixed blanking time after key-on (needs a time per unit); dropping the marked response from the last-value fields too, which the #6229 triage asks for at the receive loop. It protects the last-value forward word, the SWR pair and the temperature pole, and a unit that always reads 0 would then lose forward power, SWR and temperature outright. Left for the maintainer; with the predicate it is one call. Rejecting a lone sample above both neighbours, or a median at the head of the over, would discard real one-sample speech peaks, which #6019 exists to catch.
Tests
Socket-free.
tests/Hl2KeyOnRaddr1Fixture.hholds the recorded RADDR-1 words of three key-ons (2789 responses; captures named with their sha256 in the header). The replay feeds them throughMetisClient::handleDatagramandHl2Backend::publishTelemetryat ten phases of the 100 ms publish window.hl2_backend_seam_test: 86 checks, 16 new. The tone over never displays above its carrier; both TUNE key-ons publish the plain maximum in every window, so the rise is not delayed by any window; a synthetic speech-like envelope with one-sample peaks at 190 responses a second still wins every window. The health row is absent, then 0, then the session total; the tone replay shows exactly the recording's 2 marked responses and the synthetic speech replay shows 0.hl2_metis_protocol_test: 15 new checks and 2static_asserts. A marked response is neither peak nor counted; the response after it is kept; a lone sample far above both neighbours is kept; a temperature word of 1 is not a marker; a window of marked responses only has no peak. The predicate alone: an ACK, RADDR 0, 2 and 3, and any non-zero temperature word are not marked. The skipped count rises on a marked response and on nothing else; a window of marked responses only shows a count and no peak.hl2_fwd_window_wiring_test: 5 new checks: the emit carries the skipped count and the next window starts at zero. Its existing checks,hl2_tx_level_policy_test, and the existing checks in the two files above are unchanged and pass.Full suite on macOS (Qt 6.12.0) at
ba223304, onmain5f09371e: 646 tests, 643 pass. Three fail at-j8, and cleanmain5f09371efails the same three on this Mac:vkamp_connection_test: Replace retained socket fixtures with injected and opt-in coverage #5254; passes serially here.peripheral_auth_dialog_test: feat(peripherals): authenticate direct TGXL, PGXL, and Antenna Genius connections #6008; this diff touches no dialog file.unified_title_bar_test: two checks ("Alt+F still opens the File menu with the menu bar hidden", "opening a menu rounds the submenus added to it since"), the same two on cleanmain. New with feat(gui): unified 52 px title bar on Qt 6.12 window chrome — native captions on macOS/Windows, frameless on Linux (RFC #4764) #6198; this diff touches no GUI file.Mutations
Each a scratch commit on
ba223304, built and run through the full suite, never pushed. Red checks in the three HL2 tests:< 2048): both TUNE replays, the synthetic speech check and both replay health counts are red, plus 16 leaf and wiring checks.The two predicate mutations also delete the two
static_asserts, which otherwise stop the build.Not verified
healthreply with the new row: the app was not launched; the row is tested throughhealthSnapshot().docs/HERMES.mdis not updated.docs/automation-bridge.mdhas one paragraph on the new row.Static checks: every step of the "Static checks" job in
static-checks.ymlwas run locally and is green atba223304. The sanitizer configure job needs the CI image and was not run.73, ON8ST
🤖 Generated with Claude Code