Skip to content

fix(hl2): match CW carrier level to TUNE - #5975

Merged
jensenpat merged 1 commit into
aethersdr:mainfrom
dacacioa:fix-hl2-cw-carrier
Oct 7, 2026
Merged

jensenpat merged 1 commit into
aethersdr:mainfrom
dacacioa:fix-hl2-cw-carrier

Conversation

@dacacioa

@dacacioa dacacioa commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Closes #5610

Summary

Hermes Lite 2 TUNE uses a full-scale host IQ carrier amplitude of 1.0, while software CW used 0.5. Because RF power follows the square of IQ amplitude, CW was approximately 6 dB below TUNE at the same RF Power setting.

This changes the CW carrier amplitude to the shared full-scale value, so CW and TUNE use the same host-side carrier level while preserving the existing 5 ms raised-cosine keying envelope. The TX drive register remains the operator's power control and the existing transmit-authorisation gates are unchanged.

Implementation

  • Added the shared kHl2CarrierAmplitude constant in MetisProtocol.h.
  • Used it for both host-generated CW and TUNE carriers.
  • Added a deterministic hl2_tx_gate_test regression check that compares the encoded CW plateau IQ with the encoded TUNE carrier.
  • Rebased the branch onto the latest main, preserving the newer TUNE drive-restoration logic.

Operator impact

At a fixed drive setting, this restores the intended CW level and represents a +6.02 dB RF-power correction relative to the previous host-CW amplitude. It does not introduce a new maximum beyond the existing full-scale TUNE path. The change has maintainer approval for the safety-labelled issue.

Validation

  • Local Windows build completed with MSVC 2022, Qt 6.12.0, and CMake.
  • hl2_tx_gate_test passes all checks.
  • The resulting AetherSDR.exe was deployed with its Qt runtime and launched successfully.
  • Functional testing on real Hermes Lite 2 hardware with an external wattmeter measured more than 5 W and confirmed that CW was no longer limited to approximately 1 W.
  • The commits are SSH-signed.

No CHANGELOG entry is included; the operator-visible power correction is documented above for release-note consideration.

@dacacioa
dacacioa requested a review from a team as a code owner September 27, 2026 11:40
@aethersdr-agent

Copy link
Copy Markdown
Contributor

Welcome to AetherSDR, @dacacioa! Thanks for your first pull request.

A few things that might help:

  • Our CONTRIBUTING.md covers coding conventions and the PR process
  • CI will run automatically — if it fails, I'll post a comment explaining what went wrong
  • Jeremy (KK7GWY) reviews all PRs before merge
  • Come say hello on the AetherSDR Discord — most of the community hangs out there, and merged contributors get the Contributors role

If you have questions, feel free to ask here or in Discussions.

— AetherClaude (automated agent for AetherSDR)


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

@dacacioa

dacacioa commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Local validation completed

  • Local build completed successfully on Windows with MSVC 2022, Qt 6.8.3, and CMake/Ninja.
  • Generated the AetherSDR.exe test executable.
  • Ran the test build.
  • Functional testing was performed on a Hermes Lite 2 with an external wattmeter: CW is no longer limited to approximately 1 W, and more than 5 W was measured in the tested setup.
  • The existing 5 ms raised-cosine envelope is preserved to avoid key clicks.

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue fit

Yes — this is the exact one-line change #5610 asks for, and the issue's diagnosis holds up against the head checkout. I confirmed both halves of the comparison rather than taking them on faith:

  • Hl2Backend::setTune() calls setTxTestTone(0.0, kTuneCarrierAmplitude, …) with kTuneCarrierAmplitude = 1.0 (src/core/backends/hl2/Hl2Backend.h:1463, used at Hl2Backend.cpp:5788).
  • At TUNE's zero offset, dphi == 0, the phase accumulator never advances, and the tone arm emits {m_toneAmp*cos(0), -m_toneAmp*sin(0)} = {1.0f, 0.0f} — bit-identical to the CW plateau after this change (MetisClient.cpp:1583-1594 vs :1574). So "match CW carrier level to TUNE" is exact, not approximate.
  • Drive is genuinely shared: CW rides setTxPower() → applyDrive(), TUNE applyDrive(tunePowerPercent); at 100/100 both write the same register. Nothing else attenuates CW.

What the issue's triage asked for and this PR does not do: the regression test, and folding the two literals into one shared constant. Those are my findings below.

Scope

File Change Claimed by title/body? Verdict
src/core/backends/hl2/MetisClient.cpp kCwCarrierAmplitude 0.5 → 1.0 (one line) Yes In scope

Everything in the diff is explained by the issue. No unrelated files, no formatting churn, no deleted guards, no new public surface, no CHANGELOG.md entry (correct — don't add one).

Blockers

  1. No test pins the CW carrier level, and a deterministic socket-free seam exists. — inline at MetisClient.cpp:1562. tests/hl2_tx_gate_test.cpp already decodes EP2 payload bytes byte-exactly (:458-461, for the queued-voice arm), but its CW section (:483-523) asserts only payloadNonZero(). That means (a) nothing in the suite would have caught the original 0.5, (b) nothing fails today, which is why all five checks are green, and (c) nothing stops the constant drifting back. This is wire encoding with an existing, deterministic, socket-free harness — the layer AGENTS.md puts in CTest — so the coverage gap is actionable rather than a nice-to-have. Concrete shape in the inline comment.

Needs maintainer decision

  1. This is a power-increasing change on a keyed transmitter (4× on the CW carrier), and #5610 carries both safety and maintainer-review. Nothing in the thread is a maintainer ruling yet — the only comment is the agent triage. Two things bound the risk and are worth stating in the PR body: the new level is exactly what TUNE already emits at the same drive register, so no new maximum is introduced anywhere in the HL2 TX path; and CW stays gated by m_txAllowed at both setCwKeying() and setCwKeyDown(). But operators who compensated for the deficit with external gain will see a real 6 dB step on their next over, so this wants a release-note line (in the PR body / release notes, not CHANGELOG.md).

Nits (non-blocking)

  • Two unlinked literals now have to stay equal. Hl2Backend.h:1463 holds kTuneCarrierAmplitude = 1.0; MetisClient.cpp:1562 holds a file-local kCwCarrierAmplitude = 1.0. The entire point of this PR is that they match, and nothing enforces it — the next person to change one won't see the other. #5610's triage suggested promoting it to one shared named constant; at minimum a cross-reference comment.
  • The comment block above the constant still justifies only the ramp, not the plateau — the raised cosine and the 5 ms figure are explained, the level is not. That unexplained literal is precisely how the original defect survived review in #5061. One line ("full scale, same as kTuneCarrierAmplitude — the drive register is the operator's only power control") would close it.
  • The commit message contains a literal \n\n before "Principle VI." Also, VI is "never transmits without operator intent" — this change doesn't touch keying or intent at all; the safety relevance here is the level step, which is better described than cited.

What I tried to break

  • Full-scale clipping / rail wrap. ep2WriteTxIq's clamp is symmetric to ±32767, not ±32768 (MetisProtocol.cpp:271-277), so 1.0 encodes 0x7FFF with no wrap to the negative rail. TUNE has been riding that exact path at 1.0 already. Survived.
  • Envelope / key-click regression. kCwRampSamples = 5 * 48000/1000 = 240; kTxSamplesPerPacket = 126, so the ramp still spans ~1.9 packets. The constant multiplies shaped, so the raised-cosine shape, the rise, and the symmetric fall are untouched — only the plateau scales. No new discontinuity at key-up. Survived.
  • Sibling call sites. Grepped the head checkout: kCwCarrierAmplitude has exactly one definition and one use. No second host-CW generator in aetherd, Hl2TxDsp, or elsewhere. The voice path is separately governed by the Hl2TxDsp ALC and is unaffected. Survived.
  • Would an existing test break? No — the byte-exact assertions at hl2_tx_gate_test.cpp:458-461 belong to the queued-voice arm (0.5 → 0x3FFF), not CW. Consistent with the five green checks, and also why finding 1 matters.
  • Was 0.5 deliberate? git log -L traces it to 267c86f2 (#5061), introduced with the feature and with no rationale in the commit or the surrounding comments. A duty-cycle argument wouldn't distinguish CW from TUNE anyway, since TUNE is itself a continuous carrier held as long as the operator holds it. I found nothing defending the old value.
  • Could not check: real HL2 gateware / PA behaviour under sustained full-scale key-down, and whether the gateware applies any CW-specific level scaling of its own. I'm headless — no build, no hardware. Every finding above is reasoned from the code in /tmp/aetherclaude/pr-5975, not reproduced at runtime.

Recommendation

Approve with nits, pending a maintainer sign-off on the power step and a regression test. The change itself is correct and minimal, and I could not find a way to make it misbehave — it makes CW bit-identical to a path that already runs at this level. What I'd want before merge is the test from finding 1 (cheap, deterministic, and the thing that would have prevented the original bug), plus the maintainer ruling the safety label is asking for. The two constants-hygiene nits can ride along in the same push.

Thanks for the clean diagnosis in #5610 — the amplitude-squared reasoning matched the source exactly, which made this fast to verify.


One more thing: commit signing

main requires verified signatures, and 1 of your commits on this branch is unsigned — routine setup, not a code problem.

Quickest path (SSH key signing, no GPG needed):

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

(If you have no SSH key yet: ssh-keygen -t ed25519 first.) Then on GitHub: Settings → SSH and GPG keys → New SSH key, set the key type dropdown to Signing Key, and paste the .pub.

Re-sign what's already on the branch:

git rebase main --exec "git commit --amend --no-edit -n -S"
git push --force-with-lease

Full reference, including GPG and troubleshooting: https://docs.github.com/authentication/managing-commit-signature-verification


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

Comment thread src/core/backends/hl2/MetisClient.cpp Outdated
@on8st

on8st commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

@dacacioa, v26.10.1 shipped without this, so CW is still 6 dB under TUNE there: kCwCarrierAmplitude is still 0.5 in MetisClient.cpp against kTuneCarrierAmplitude = 1.0 in Hl2Backend.h. This PR no longer merges: at head e7e9b3ac a trial merge with main (8df6db2f) conflicts in Hl2Backend.cpp and Hl2Backend.h, and the PR shows no CI run.

Merging main into the branch should get it moving again. If you want a hand with the two conflicts, say so and I will propose resolutions here; the branch stays yours.

Not checked: I did not build the branch, run its test or key a radio, and I have no power measurement of my own for CW against TUNE.

73, ON8ST

@ten9876 ten9876 self-assigned this Oct 4, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed head e7e9b3ac89460b65d4e3b635f74226e3a4b3e180.

Issue fit: Yes at the host-IQ seam. #5610 reports CW at half TUNE's amplitude for the same drive. The shared full-scale constant makes their encoded plateaus equal; the 5 ms raised-cosine envelope and authorization gates remain unchanged. Actual RF output remains hardware-owner evidence, which I did not independently reproduce.

Scope

File/group Change Claimed? Verdict
MetisProtocol.h, Hl2Backend.{h,cpp} Share carrier amplitude, remove duplicate constant Issue proposes it In scope
MetisClient.cpp CW plateau 0.5→1.0 Yes Direct fix
hl2_tx_gate_test.cpp Compare encoded plateau against TUNE Issue proposes it In scope; update body

No unrelated file, changed drive setting, new command, removed TX gate or changelog entry. Authored commits are limited to this fix.

Blockers

None found.

Nits (non-blocking)

  1. tests/hl2_tx_gate_test.cpp:519: describe the added host-level regression check in the PR body; the text currently mentions only the absence of an automated RF test and still says validation is in progress.
  2. src/core/backends/hl2/MetisProtocol.h:46: retain explicit maintainer attention to the 6.02 dB CW amplitude correction, as requested by the issue thread. Principle VI concerns operator transmit intent, not the body's description of “unrelated scope.” The existing gates remain intact.

Verification and attacks

  • Rechecked the complete modified test: it injects stream/sink state and never calls transport start/connect/bind. No synthetic peer, sockets, hardware, or RF used.
  • Compiled a focused C++20/Qt 6.11.2 harness against this head's production MetisClient, MetisProtocol, control-request, emergency-stop and TxCoordinator code, using the real test authority. After five CW packets, first IQ bytes versus TUNE were 7f/7f ff/ff 00/00 00/00 equal=1.
  • Changed only the CW multiplier back to 0.5 in scratch: 3f/7f ff/ff 00/00 00/00 equal=0, exit 1. The encoded-sample comparison detects the reported defect. This was a focused harness, not a run of the full hl2_tx_gate_test or application.
  • Read the key-up ramp and closed-gate branches; no logic changed there. No bridge session: SimBackend cannot transmit or exercise this HL2-specific path. Wattmeter and Windows-build claims remain author evidence.
  • All four engine-boundary/manifest/capability/command-plane gates pass on merge base and head with byte-identical output. No new #5554 coupling. Separate automated review skill unavailable; independent manual audit and mutation performed instead.

CI and recommendation

Approve with nits, subject to maintainer acceptance of the power correction and required checks. Current PR check rollup is empty; this is not a green-CI claim. Earliest historical run at this head is 2026-09-28T17:24:30Z, before main's 2026-10-04T18:26:40Z; historical merge checks do not prove today's integration, and non-strict branch protection does not force their refresh. Update test-plan prose and verify required checks before landing.

— Jeremy KK7GWY + Codex

@ten9876 ten9876 removed their assignment Oct 4, 2026
ten9876
ten9876 previously requested changes Oct 4, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent re-review at head e7e9b3ac89460b65d4e3b635f74226e3a4b3e180. Same head as the earlier ten9876 + Codex review. I re-checked its claims, ran the real test target where it used a scratch harness, and went to the HL2 gateware for the level itself.

Issue fit: yes. #5610 reports CW about 6 dB under TUNE at the same drive. Two things now match on the wire: the shared kHl2CarrierAmplitude = 1.0 (MetisProtocol.h:46) sets the CW plateau (MetisClient.cpp:1574), and TUNE reads the same constant (Hl2Backend.cpp:5788). The 5 ms raised-cosine ramp, the drive register and every TX gate are untouched.

Scope

File Change Claimed? Verdict
MetisProtocol.h Adds shared kHl2CarrierAmplitude Proposed in #5610 triage In scope
Hl2Backend.{h,cpp} Drops the duplicate kTuneCarrierAmplitude and uses the shared one Same In scope
MetisClient.cpp CW plateau 0.5 → shared 1.0 Yes The fix
tests/hl2_tx_gate_test.cpp Encoded CW plateau compared byte-for-byte to TUNE Asked for in #5610 In scope; not mentioned in the body

Everything in the diff is explained by the issue. There's one signed commit and no CHANGELOG entry.

Blockers

  1. This no longer merges with main. git merge-tree origin/main <head> reports conflicts in src/core/backends/hl2/Hl2Backend.cpp and Hl2Backend.h. main has moved through this code since the branch was cut (#6104 live TUNE power, #6119, #6127, #6109, #5919). The conflicts are context-only:

    • On main, kTuneCarrierAmplitude is still defined once (Hl2Backend.h:1059) and used once (Hl2Backend.cpp:5073).
    • On main, kCwCarrierAmplitude is still 0.5 (MetisClient.cpp:1459), so the fix is still needed.

    Rebase, keep the deletion and the shared constant, and re-run hl2_tx_gate_test.

Nits (non-blocking)

  • The PR body's test plan is out of date. It still says "validation in progress" and "No automated RF test was added", but tests/hl2_tx_gate_test.cpp:519-526 now pins the host-side level. Say so in the body: it is the regression guard the original 0.5 never had.
  • Principle VI is cited for "focused scope". VI is about operator transmit intent. This change does not touch intent, keying or gating. What it does change is a +6.02 dB step in CW output at a fixed drive setting, and that should be stated plainly for the release notes (not CHANGELOG) and for the safety label's sign-off.

HL2 gateware: the authority for the level

The earlier reviews could only show that CW now equals TUNE. The gateware shows that 1.0 is also the level its own designers picked for CW.

  • In softerhardware/Hermes-Lite2 gateware/rtl/radio_openhpsdr1/radio.v, the gateware's own keyer feeds the TX CORDIC directly with tx_i = tx_cw_key ? {1'b0, tx_cwlevel[18:4]}, ramping to MAX_CWLEVEL = 19'h4d800, i.e. 0x4D80 at the CORDIC. The comment says "Set CORDIC for max DAC output … 0x7FFF/1.7".
  • Host IQ instead passes through the interpolating FIR and the CIC. The gain note in the same file (the block before generate if (NT == 0)) puts the CIC at 0.61 × (1 − 1/128) ≈ 0.605. The 1/128 trim exists precisely so "a full range 16 bit I/Q signal" does not wrap the DAC.
  • Host full scale is therefore 0x7FFF × 0.605 ≈ 19,830 ≈ 0x4D76 at the CORDIC, within 0.05 % of the gateware keyer's 0x4D80.
  • The old 0.5 put host CW 6 dB under the gateware's own CW level. This change puts it at that level.
  • Caveat: I took the FIR stage (FirInterp8_1024) as unity passband gain and did not trace its coefficients.

Verification and what I tried to break

  • Built the exact head and ran hl2_tx_gate_test, the real target rather than a harness: all checks passed. Socket preflight: the file states "No start(), bind(), peer or datagrams" (:30) and injects stream state. No socket and no synthetic peer.
  • Mutation: I put the CW plateau back to 0.5 * shaped and rebuilt. Result: FAIL: CW plateau matches TUNE carrier amplitude (#5610). Restored (sha256 matches HEAD, touched, rebuilt): all checks pass again. The test pins exactly the reported defect.
  • The test samples the right packet: packet 5 starts at sample 504, past the 240-sample (5 ms × 48 kHz) ramp, so it compares plateau to plateau rather than catching the ramp.
  • Clipping and wrap at 1.0: ep2WriteTxIq clamps symmetrically to ±32767, and TUNE already runs this exact code path at 1.0 in the field. The gateware's 1/128 CIC trim covers full-scale input.
  • Not reproduced: RF output on a real HL2 and the wattmeter numbers (author's evidence). No bridge session, because SimBackend cannot transmit and does not run the HL2 EP2 path. I never keyed anything.
  • CI: the earliest run on this head (2026-09-28) predates main's current tip, and the conflict above confirms the green no longer describes a real merge.

Recommendation: request changes for the rebase. After that it is ready, subject to Jeremy's sign-off on the +6 dB CW step, which the safety label on #5610 asks for. The level matches the HL2 gateware's own CW keyer.

— Jeremy KK7GWY + Claude

@ten9876

ten9876 commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Maintainer sign-off: the +6 dB HL2 CW carrier step (0.5 → 1.0, matching TUNE and the gateware keyer level) is approved for #5610's safety label. Remaining: the rebase onto main from the review above.

— Jeremy KK7GWY + Claude

@dacacioa
dacacioa force-pushed the fix-hl2-cw-carrier branch from e7e9b3a to 9b6935a Compare October 5, 2026 19:54

dacacioa commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto the latest main and resolved the conflicts while preserving main's newer TUNE drive-restoration logic.

Validation after the rebase:

  • Local Windows Release build completed with MSVC 2022, Qt 6.12.0, and CMake.
  • hl2_tx_gate_test passes all checks.
  • The resulting AetherSDR.exe was deployed with its Qt runtime and launched successfully.
  • The commits remain SSH-signed.

The PR description has also been updated to document the deterministic regression test and the +6.02 dB CW power correction. GitHub now reports the PR as mergeable.

dacacioa commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Review follow-up completed.

The requested regression test, shared carrier constant, maintainer safety approval, rebase, local Release build, and executable validation are all complete. There are no unresolved inline review threads remaining, and GitHub reports the PR as mergeable.

@jensenpat jensenpat self-assigned this Oct 7, 2026

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 9b6935a96df2a6845c3d78a94f28ea2a8844cbda.

Issue fit: Yes. #5610 asks for host CW to use the same carrier amplitude as TUNE at the same drive. This head does that with one shared kHl2CarrierAmplitude = 1.0. The 5 ms raised-cosine envelope, the transmit gates, and the current TUNE drive-restore path stay as they are.

Scope

File Change Claimed? Verdict
MetisProtocol.h Shared full-scale kHl2CarrierAmplitude Yes In scope
Hl2Backend.h, Hl2Backend.cpp Drop the duplicate tune constant; TUNE uses the shared one. m_rfDriveOwed / applyDrive before the carrier is unchanged Yes In scope
MetisClient.cpp CW plateau uses the shared constant Yes The fix
tests/hl2_tx_gate_test.cpp Encoded CW plateau compared with a zero-offset TUNE carrier Yes In scope

No unrelated files, no new protocol or keying surface, no removed gate, no CHANGELOG entry. One verified-signed commit; its parent is current main (334276112c4bcbf3dd52ad26116569638d21d20a). #5554: no new family-string branch, raw Flex wire text, dynamic_cast, capability, or keying verb. Hermes-Lite 2 only.

Blockers

None. The earlier changes-requested review was the rebase onto main. This head is that rebase: one commit ahead, zero behind, and the newer TUNE drive-restore logic is still in setTune.

Verification

  • Required checks on this SHA are green: build, check-macos, check-windows, Static checks, and Sanitizer option configures. Per-PR CI compiles hl2_tx_gate_test and does not run it (frozen gate in ci.yml).
  • Local Qt 6.12.0 RelWithDebInfo, QT_QPA_PLATFORM=offscreen: hl2_tx_gate_test passed, 1/1.
  • Mutation of the CW plateau back to 0.5 * shaped failed CW plateau matches TUNE carrier amplitude (#5610) and exited 1. Restored source matches this SHA and the test passed again.
  • Packet 5 starts at sample 504. The ramp is 240 samples (5 ms × 48 kHz), so the compared sample is on the plateau. ep2WriteTxIq clamps full scale to 0x7FFF, the same encoding TUNE already uses.
  • The modified test injects builder state. It does not bind, listen, or connect, and it does not start a peer.
  • Not run here: a wattmeter on a live Hermes-Lite 2. The author's hardware note stays author evidence. This does not raise a host maximum above the existing TUNE carrier. At a fixed drive it is a +6.02 dB CW correction, which is what #5610 asks for. Principle VI is unchanged: keying still requires an admitted operation and an open transmit gate.

Recommendation: Approve.

Non-blocking follow-up

hl2_tx_gate_test is outside the frozen per-PR ctest gate, so this assertion is proven by the local run above and by the post-merge full suite, not by the PR checks. A later seam worth adding, not a merge prerequisite: drive the TUNE side through Hl2Backend::setTune as well as MetisClient::setTxTestTone, so a backend-only amplitude split fails the same comparison. Hermes-Lite 2 only.

CW used half the host IQ carrier amplitude of TUNE, reducing carrier power by about 6 dB. Use one shared full-scale carrier constant for both paths and pin the wire-level invariant with a regression test.

Closes aethersdr#5610

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 78331f1e878f843a12391658607c4e0fd0098af8.

Taking the review. This is the same CW/TUNE carrier fix rebased onto current main (4249b71d5c24d078832f4f6bbc0a2f0e1777c905). Author remains David Acacio; the rebase commit is signed by jensenpat. One commit ahead, zero behind. The diff is still the five host-IQ files.

The earlier changes-requested review asked for that rebase. It is done. hl2_tx_gate_test passed on the pre-rebase patch (9b6935a96), and this rebase applied with no conflicts and no further edits. Required checks on this SHA are still starting.

Recommendation: Approve.

@jensenpat
jensenpat dismissed ten9876’s stale review October 7, 2026 15:17

Rebase requested by this review is now head 78331f1, parent current main, signed, and approved.

@jensenpat
jensenpat enabled auto-merge (squash) October 7, 2026 15:57
@jensenpat
jensenpat merged commit f3a87ba into aethersdr:main Oct 7, 2026
5 checks passed
jensenpat pushed a commit that referenced this pull request Oct 10, 2026
…II. (#6254)

Refs #5942

**Hermes-Lite 2 receive audio becomes 12.04 dB quieter, at every slider
position.** The panel does not depend on the mode. The level is measured
in USB and LSB. AM is checked only for the setting surviving a rebuild,
and CW and FM are not measured. That covers the speaker (and the HL2's
codec, which the speaker mix feeds), the per-slice audio sent to TCI,
and the in-app decoders. Today the chain output sits above full scale:
WDSP's panel multiplies by 4.0 after the AGC has limited at about -0.16
dBFS. **No maintainer has ruled on this level change.** It needs a
release note: anyone who pulled the slice AF slider down to about 25 to
stop strong stations distorting can put it back at 100.

## Limits first

- **HL2 only.** ANAN and RTL behave exactly as before. ANAN in every
mode, and RTL outside narrow FM, still run WDSP's 4.0. Those backends
have their own owners, and this PR does not decide for them. RTL narrow
FM already opens the panel at 1.0 (#5924). The new field is not set on
any RTL path, wide FM (#5973) included.
- **Offline only.** The figures below come from a real `Hl2RxDsp` fed
synthetic IQ. No radio, simulator or socket was involved, and nothing
was measured on the air. Step 5 of #5942 (does the slider-25 crossover
hold across LNA gain) is still unrun. This PR fixes the gain structure,
which is provably 12 dB hot. It does not claim to cure any one
listener's distortion.
- **Unity does not promise `|out| <= 1.0`.** Speak, mpeak and the second
bandpass pass all run between the AGC and the panel, as the triage
noted. The test therefore bounds a steady tone, not every signal.
- This is #5942's step 1, plus step 4's test for the HL2 chain only.
Step 2 (the slider moving into `SetRXAPanelGain1`) is not here, because
the triage asked for it as a separate change. Step 3 (`Meter::AgcGain`)
is not here either.
- Measured on one Mac (macOS, Qt 6.12.0).

## What changes

- `WdspChannel::Config` gains `std::optional<double> rxPanelGain`. When
it is unset, `open()` leaves WDSP's `create_rxa()` value of 4.0
untouched, so every existing owner is unaffected. When it is set,
`open()` calls `SetRXAPanelGain1` after the FM block, so a set value
wins over the FM recipe's 1.0 (no caller sets both). `validateConfig()`
refuses a set value that is non-finite, negative, or on a transmit
channel.
- `Hl2RxDsp::buildChannel()` sets it to `Hl2RxDsp::kRxPanelGain = 1.0`,
so every HL2 build, rebuild and rate change opens at unity.
- Two comments in `hl2_rxdsp_unmute_staleness_test` that described the
x4 are updated (its marker is still above its own floor).
`docs/HERMES.md` §AGC notes that its recorded peaks (3.186, 2.664)
include the x4. It also notes that the stages between the AGC and the
panel mean the AGC's ceiling does not bound the output.

## The test: `hl2_rx_panel_gain_test` (socket-free, about 1 s)

| leg | this commit | HL2 without the panel line (= main) |
|---|---|---|
| AGC off, 1500 Hz tone at 0.01: chain gain | **+10.01 dB** (the 10.00
dB fixed gain alone) | +22.05 dB |
| control: the built config re-opened directly | -0.01 dB vs the HL2
chain | -0.01 dB |
| negative control: the same config, panel unset | **+12.03 dB** vs the
HL2 chain | -0.01 dB (fails, as it should) |
| AGC med (39 dB ceiling), tone at 0.1, USB peak | **0.830 (-1.62
dBFS)**, rms 0.587 | 3.318 (+10.42 dBFS) |
| the same, LSB | 0.828 (-1.64 dBFS) | 3.311 (+10.40 dBFS) |
| rebuild to AM at 96 kHz keeps unity | yes | no |

The ratio between the two AGC-med columns is 4.00 (12.04 dB), and the
AGC-off gain moves by the same amount. The negative control takes
`Hl2RxDsp::channelConfig()` and clears only `rxPanelGain`. That isolates
the panel from everything else the HL2 sets.

**Red on mutation** (scratch commits, never pushed; the full suite each
time):

| mutation | result |
|---|---|
| `Hl2RxDsp` does not set `rxPanelGain` (main's HL2 behaviour) | RED:
the gain, negative-control, both AGC-med and both config checks |
| `WdspChannel::open()` ignores `rxPanelGain` (the field is set but
never applied) | RED: the gain, negative-control and both AGC-med
checks, while the config checks pass. So the level legs check the
effect, not the field. |

Full ctest suite on macOS (Qt 6.12.0, offscreen), on `121340c0` (head
`594d96cf`): 654 of 658 pass at -j8 and 655 after the serial rerun. The
three remaining failures are known on clean main on this machine, each
with only its known check lines: `control_receive_test` (#6248),
`peripheral_auth_dialog_test` (#6008) and `unified_title_bar_test`
(#6224). `vkamp_connection_test` (#5254) failed at -j8 and passed
serially. This PR touches none of their files. The figures in the table
above were re-read from `hl2_rx_panel_gain_test` on this head and are
unchanged; the main-column figures and the two mutations are from before
the rebase. The rebase brought in #5973 (RTL wide FM, inside
`WdspChannel`) and #5975 (HL2 CW carrier level, transmit side); neither
touches the panel gain or `Hl2RxDsp`. On the second mutation
`vkamp_connection_test` also failed at -j8 and passed serially (#5254).
The level figures and the mutations below were taken at `e9a0c396`;
`594d96cf` changes comments and one doc line, no executable line.

73, ON8ST

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 11, 2026
…remote operation over CGNAT (#6389)

## Summary

Release prep for **v26.10.2**: the six release files plus the Stable
docs snapshot. Metadata and docs only; no source, test or CI changes.

Cutoff: `origin/main` at `46943eb3c` (#6083, merged 2026-10-11).
Anything that merges after it is folded in before the tag.

**106 merged changes** from **15 human contributors**, AetherClaude (2)
and Dependabot (0). First-time contributor: **@dacacioa**. This was
confirmed by checking each login for a merged PR before the previous
tag, not from `author_association`.

## Changelog

The range `v26.10.1..46943eb` was mapped through the commit-to-PR API
(`repos/…/commits/<sha>/pulls`), not from commit subjects. It holds 106
PRs and no direct commits, and every one is cited in the section.
`v26.10.1` is an ancestor of `main`, so nothing is excluded. #6083
merged before the branch was cut, so it is inside the range and needs no
unmerged-PR banner.

The notes lead with:
- the unified 52 px title bar (#6198);
- docs.aethersdr.com (#6286, #6298, #6294);
- remote operation over CGNAT through the in-radio tailnet shim (#6273,
#6318, #6325, #6319);
- the canon windows (#6227, #6334, #6339, #6340, #6341, #6364, #6372,
#6386).

Then come DVK import, FFT Smooth/De-comb, TCI tune and receiver events,
RTL-SDR multi-receiver delivery, HL2 and ANAN-G2 state memory, the
IC-7300MK2 fixes, the first clean sanitizer runs (stated beside #6350's
TSan red) and Authenticode signing.

Numbers come from the PR bodies. "On upgrading" callouts cover the
behavior changes the bodies flagged:
- HL2 receive audio is 12.04 dB quieter (#6254);
- HL2 host CW is +6.02 dB at a fixed drive (#5975);
- ANAN AGC-T is remapped (#6282);
- sleep prevention is on by default (#6195);
- the Tailscale MTU cap has no opt-out (#6319);
- IC-7300MK2 Auto SQL is withdrawn (#6184).

DeepFist (#5953, #6103) is described as applying only to
`ENABLE_DEEPFIST_EXPERIMENT` builds.

## Docs refresh, not just version strings

- **README**:
- Highlights: FFT Smooth/De-comb on the spectrum line; AetherMap naming;
remote over CGNAT and TCI tune/receiver events on the SmartLink/TCI
line; pre-NR Copy Assist; a new docs.aethersdr.com line.
- Supported Hardware: the KPA1500, marked as not yet validated on
hardware.
- Other radio families: HL2 state restore extended; ANAN-G2 gains AGC-T,
NB2, supply voltage/link and per-radio memory; RTL-SDR now "bundled in
every official package", with WFM stereo, PPM, per-receiver SQL/meters
and DAX/TCI feeds, and multi-receiver explicitly an evaluation.
- Roadmap section: the CTR2 line advanced and a visual-canon line added.
  - Maturity labels are unchanged.
- **ROADMAP**:
- In flight: a v26.10.2 sentence each for aetherd, Icom, ANAN-G2,
RTL-SDR (remaining is now production multi-receiver qualification and
digital FM, RFC #6079), Hermes-Lite 2 and the CTR2 relay (RFC #6332
extensions). A new entry for the visual canon (RFC #6226 / #6239).
  - Queued: RFC #4214 (approved 2026-10-09) added.
- Open RFCs, checked against `label:rfc`: #4214 leaves (approved); #4764
and #4861 leave (shipped by #6198 and #6216); #6346, #6232, #6212 and
#6209 join. #6332 is approved and folds into the CTR2 entry.
- Recently shipped: nine new `(v26.10.2)` entries, still within the
v26.10.x cycle, so no month-rollover trim.
- Backends (six: anan, flex, hl2, icom, rtl, sim) and NR engines (seven)
were checked against the tree; both are unchanged.

## Judgment calls flagged for the maintainer

All of these were decided through AskUserQuestion:
- Version **26.10.2**, date **2026-10-11**, opened ready for review.
- **#6350**, the TSan red in the weekly sanitizer lane: proceed, and
name it below.
- RFCs **#4764** and **#4861** come off Open RFCs as shipped. The issues
themselves are left open for you to close.
- RFC **#4214** goes under Queued (next cycle).
- A **visual canon** In-flight entry, plus a README Roadmap line.
- The **KPA1500** is listed in README Supported Hardware with "not yet
validated on hardware".
- Including **#6083**: you asked for it, and it merged before the
cutoff, so it ships as an ordinary part of the range.

## Known failing on `main`

- **`ctr2_hid_thread_port_test`** fails in the weekly TSan lane (runs of
2026-10-08 and 2026-10-10; #6350). `main`'s CI and Full Suite were green
at `414082197`. The CHANGELOG headline and intro say so: the lanes
achieved their first clean runs this cycle, and the test remains failing
under TSan. This prep does not fix the test, and the release ships with
it red in that lane.

## Scope

| file | change |
|---|---|
| `CMakeLists.txt` | `project(AetherSDR VERSION 26.10.2)` |
| `README.md` | Current version line, plus the prose refresh above |
| `AGENTS.md` | Current version line |
| `CHANGELOG.md` | new `[v26.10.2] — 2026-10-11` section; `[Unreleased]`
stays and stays empty; history below byte-identical |
| `packaging/linux/io.github.aethersdr.aethersdr.metainfo.xml` |
`<release version="26.10.2" date="2026-10-11"/>` first in `<releases>` |
| `ROADMAP.md` | cycle heading → post-v26.10.2, plus the prose refresh
above |
| `docs/user/versioned_docs/version-stable/**`,
`docs/user/versioned_sidebars/version-stable-sidebars.json`,
`docs/user/stable-version.json`,
`docs/user/src/components/LogAnalyzer/rules.json` | docs snapshot
(`tools/docs/snapshot_stable.py 26.10.2`), in its own commit. This is
the first snapshot since #6286. |

## Validation

Output of `check_release_prep.py --mapping mapping.json` at the branch
head: 30 checks, 1 FAIL (explained below), 1 WARN (the snapshot files,
listed in the scope table).

```
PASS v26.10.1 is an ancestor of origin/main
PASS all six version spots say 26.10.2
PASS CHANGELOG heading date equals metainfo release date
PASS appstreamcli validate
PASS metainfo <release> list is date-descending with unique versions
PASS `## [Unreleased]` stays and stays empty
PASS `## [v26.10.2]` sits directly under `[Unreleased]`
PASS the previous section is `[v26.10.1]`
PASS CHANGELOG diff is exactly the inserted section
PASS new section opens with a `### <headline>`
PASS intro sentence has the house shape
PASS new section has a `### Contributors` block
PASS release_contents.py --check (citations, intro and Contributors counts)
     PRs in range: 106  cited: 106; intro 106/15 matches; Contributors (16 entries) matches; first-time ['dacacioa']
     citations outside the range: #4764, #4861, #5468, #6192, #6226, #6271, #6332 (RFCs), #6238, #6350 (issues)
PASS code fences balance / no duplicated table headers or doubled `---` (README, ROADMAP, new section)
PASS every local link in README.md and ROADMAP.md resolves
PASS no heading removed from README/ROADMAP is still linked elsewhere
PASS Recently shipped `(vX)` markers run newest-first and lead with the new version
PASS Recently shipped intro names the current cycle (v26.10.x)
FAIL only the four current-version lines lose the `26.10.1` string outside CHANGELOG
     historical mentions must stay: "label": "26.10.1"
PASS diff touches no src/, tests/, third_party/ or .github/workflows/ path
PASS all six release files are in the diff
WARN files beyond the six (the docs snapshot)
PASS git diff --check
```

- **The one FAIL is the snapshot's version label, not a historical
mention.** `docs/user/stable-version.json`'s `"label"` is the docs
site's current Stable version, and `snapshot_stable.py` rewrites it by
design. The validator predates #6286 and knows only the four
current-version lines. Run on the six-file commit alone, the same check
passes (29 checks, 0 FAIL, 0 WARN).
- The docs build passes: `npm ci && npm run build` in `docs/user`. `node
tools/docs/test_log_rules.mjs` reports 707 checks passed for 45 rules.
The line moves it reports were already on `main`.
- Four merged PRs (#6340, #6341, #6364, #6372) named screenshots to
recapture after merge (`psk-reporter-sidebar.png`, `menu-tools.png`,
`connect-dialog.png`, `rx-agc-calibration.png`), and #6386 named the
panadapter shots (`panadapter.png` and the main-window captures, which
still show the old title strip). They were not recaptured, so the Stable
snapshot carries the older images.
- No configure, build or test was run for this prep; it is metadata
only, and CI builds it.
- `main` CI at the cutoff `46943eb3c`: Docs and CodeQL - Code Quality
are green; CI, Full Suite, CodeQL and the System Libraries Canary were
still running when this was opened. At `414082197`, every one of them
was green.

After merge: tag the resulting **main** commit, not this branch.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 12, 2026
… Principle VIII.

An audit of every page against the 106 PRs in v26.10.2, each fact checked
against the source at the release commit:

- Menu paths: AetherMap, AetherModem, Callsign Lookup and FreeDV Reporter
  are on View, and Waveforms is on Tools.
- New coverage: Windows Firewall rules and the Network Diagnostics page
  (#6289), live CW contacts (#6268), NB2 on the HL2 and ANAN-G2 (#5966),
  ANAN-G2 AGC-T and per-radio memory (#6282, #6129), HL2 state restore
  and its level changes (#6259, #6254, #5975), DVK import (#6247, #6262,
  #6251), Copy Assist from unprocessed audio (#6216), keyboard-operable
  status-bar indicators (#6267), the KPA1500 (#5905), CTR2 extensions
  and the AetherKnob (#6338), live PPM (#6082), and Icom COMP and the
  squelch scale (#6174, #6184).
- Known issues whose GitHub issues are closed are removed.
- The style guide now asks for US spelling, and every page follows it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 12, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hermes-Lite 2 CW output is about 6 dB lower than TUNE at the same RF Power

4 participants