Skip to content

fix(cat): RF gain and VFO letters follow the backend. Principle VIII. - #6013

Merged
ten9876 merged 11 commits into
aethersdr:mainfrom
on8st:hl2/cat-receivers
Oct 3, 2026
Merged

ten9876 merged 11 commits into
aethersdr:mainfrom
on8st:hl2/cat-receivers

Conversation

@on8st

@on8st on8st commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #5774
Fixes #5775
Fixes #5776

Three Hermes-Lite 2 reports from Erik ON4PB, all in shared code (RigctlProtocol, MainWindow) and all family-neutral: no family string test above the seam, no new usesFlexCommandPlane() branch. Every decision is made from something the model already exposes: RadioModel::hasCommandPlane(), whether the slice has a pan, and RadioModel::maxSlices(), which already defers to the backend's declared capacity for non-Flex families.

#5774: L RF / l RF did nothing on HL2. set_level RF called SliceModel::setRfGain, whose whole body is Flex wire text (slice set N rfgain=X). A backend without a command plane drops it, and rigctl answered RPRT 0 either way; get_level RF then echoed the slice's local copy of that write. The routing is now keyed on RadioModel::hasCommandPlane():

  • With a command plane (Flex): unchanged from main, byte for byte. L RF sends slice set N rfgain= (0–100) and l RF reads the slice's rfgain. Third-party CAT clients bind to this wire text, so it is pinned by a test.
  • Without a command plane (HL2 and the other host-DSP families): L RF / l RF go through the panadapter's RF gain, the control the ANT slider, the controller wheel and the bridge's pan rfgain already use (RadioModel::setPanRfGainFor → IRadioBackend::setPanRfGain, read back from PanadapterModel::rfGain()). Hamlib RIG_LEVEL_RF is normalized 0.0–1.0, so it is spread across the pan's published rfGainLow/High and snapped to rfGainStep: on the HL2's −12…+48 dB LNA, 0.5 → +18 dB and 0.0 → −12 dB. l RF does the reverse. With no pan to address, or before the backend has published the pan's range (PanadapterModel::hasRfGainRange(); until then the model holds Flex-shaped defaults, −8…32 step 8, that describe no other radio), set and get answer RPRT -11 (RIG_ENAVAIL) rather than acknowledging a no-op or scaling against the wrong range.

#5775: receivers B/C/D had no tabs in RX Controls. The slice tab row was sized only from RadioModel::infoChanged, which fires at the start of a connect, when an HL2 honestly reports maxSlices = 1. Its real ceiling arrives after the link comes up, and is revised with span, on capabilitiesChanged, which never resized the row. A small model-side tracker, ReceiverSlotCount, now follows every edge that can move the number (capabilities, connection, info, slice added/removed) and announces changes; MainWindow resizes the tabs from it. The count is floored at the highest occupied slot, so a ceiling that falls on zoom-out cannot strand a running receiver without a tab, and capped at eight (RadioSession::kCatPorts, the letters A–H), because the floor comes from a slice id, which is wire data. The existing infoChanged sizing is kept with the same floor and cap, because that is what lets a Flex draw its tabs before the first slice arrives (#2243).

#5776: CAT could only select A/B. MainWindow::catPortTargetCount() returned RadioModel::maxSlicesForModel(model()), the Flex model table, which has no row for any other family and falls back to its 2-slice default. The CAT letters now come from the same backend-aware number as the tabs and are refreshed on the same edge. ReceiverSlotCount::catLetters() keys the choice on the connection: every letter while no radio is connected, the radio's own count while one is, even when that is one (ANAN, RTL-SDR and the demo backend declare one receiver). On a Flex the CAT count now uses maxSlices() (declared/licensed capacity) instead of the model table; for a radio that declared nothing the two are the same number.

Constitution principle honored

Principle VIII (Evidence Over Assertion): L RF no longer reports success for a write that reaches nothing, and l RF reports the value the radio holds instead of echoing the client's own last write. Principle XI (Fixes Are Demonstrated): every behaviour below has a test that fails at the assertion without the fix.

Test plan

  • Local build passes
  • Behavior verified on a real radio if applicable: not verified on a real radio. No HL2, Flex or other hardware was driven. Evidence is socket-free unit tests.
  • Existing tests pass (non-socket ctest suite; socket-owning tests not run locally)
  • Reproduction steps documented: in each issue; each has a regression test

New socket-free tests (no socket bound or dialled, nothing keyed):

  • rigctl_rf_gain_pan_test
    • No command plane (injected backend recording the pan RF gain it is handed, pan materialised through the seam's geometry signal): L RF 0.5 → +18 dB on a −12…+48/1 dB pan, addressed at the slice's own pan in the backend's namespace; 0.0 → −12, 1.0 and out-of-range → +48; l RF round-trips 0.5, reads a slider-set +30 dB as 0.7 and −12 dB as 0.0. With no pan, and with a pan whose range is not yet published, set and get answer RPRT -11 and nothing is sent; once the range lands, L RF 0.5 sends +18 dB and reads back 0.5. On main's RigctlProtocol.cpp, 13 of these checks fail.
    • Command plane (the in-process Demo, which carries a Flex command plane and dials no socket; its pan is given a published Flex range so a pan-routed mutation would actually send): L RF 0.5 answers RPRT 0 and emits exactly slice set 0 rfgain=50; l RF answers 0.5; out-of-range L RF 7 emits slice set 0 rfgain=100. main's RigctlProtocol.cpp passes all four of these unchanged; routing a command-plane radio to the pan fails three of them.
  • receiver_slot_count_test: a backend whose ceiling goes 1 → 4 after connect ends with 4 letters (A–D), announced. A ceiling that falls to 3 with four receivers running keeps 4; the floor is by slot, not by count. Disconnect withdraws the count. A slice id of 20 gives 8, and a declared ceiling of 12 gives 8. CAT letters: 8 with no radio, exactly 1 for a connected radio that declares 1, 4 after the revision to 4, and 8 again after disconnect.

Not covered by a test: MainWindow's wiring of ReceiverSlotCount::countChanged into AppletPanel::setMaxSlices / applyCatPortCount(), and applyCatPortCount() passing catLetters() to the applet (a few lines; MainWindow cannot be built in a unit test).

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls. Nothing is persisted. CAT L RF does not write the slider's per-pan settings key, the same as the bridge's pan rfgain.
  • Code is clean-room, not decompiled, disassembled, or reverse-engineered from a proprietary binary (Principle IV)
  • All meter UI uses MeterSmoother (no meter UI touched)
  • Documentation updated if user-visible behavior changed: no docs describe rigctl RF scaling or the slice-tab sizing; the behaviour is described here
  • Security-sensitive changes reference a GHSA if applicable (n/a)

aetherd touchpoint: src/gui/MainWindow.cpp now includes models/ReceiverSlotCount.h, one new gui/→models/ touchpoint, tagged universal in docs/architecture/aetherd-touchpoint-tags.json; docs/architecture/aetherd-touchpoints.md is regenerated by tools/gen_touchpoint_manifest.py. RigctlProtocol.cpp's include of models/PanadapterModel.h is core → models, not a touchpoint.

73, ON8ST

on8st and others added 3 commits September 28, 2026 21:54
`L RF` wrote SliceModel::setRfGain, whose whole body is Flex wire text
(`slice set N rfgain=X`), and answered RPRT 0 unconditionally. A backend
without a command plane drops that text at the sink, so on a Hermes-Lite 2
the command was acknowledged and changed nothing. `l RF` read the slice's
local copy of that write, so a client saw its own number echoed back and
never the ANT panel's slider.

The operator's RF Gain control is the panadapter's on every family: the
slider, the controller wheel and the bridge's `pan rfgain` all go through
RadioModel::setPanRfGainFor and read PanadapterModel::rfGain(). rigctl now
does the same when the addressed slice has a pan, which reaches the HL2's
AD9866 LNA through IRadioBackend::setPanRfGain and a Flex through its
existing `display pan set ... rfgain=` path. No family is named: the
decision is whether the slice has a pan, the rule the slider already uses.

The scale was wrong as well. Hamlib RIG_LEVEL_RF is normalized 0.0-1.0 and
was treated as 0-100 %, which cannot express a negative gain. It is now
spread across the pan's published rfGainLow/High and snapped to its step:
on the HL2's -12..+48 dB range, 0.5 is +18 dB and 0.0 is -12 dB; `l RF` is
the inverse.

A slice with no pan keeps the slice setter, as the slider falls back to it.
Where no command plane exists to carry that either, set and get answer
RIG_ENAVAIL (-11) instead of acknowledging a no-op.

rigctl_rf_gain_pan_test drives it socket-free through an injected backend
that records the gain it is handed. On the unchanged code 10 of 13 checks
fail at the assertion, including "L RF 0.5 on a -12..+48 dB pan asks for
+18 dB" and "l RF reports a gain the operator set on the slider".

Refs: aethersdr#5774

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The RX applet's slice tab row was sized from RadioModel::infoChanged alone,
which fires at the START of a connect. A backend that declares its own
capacity may only know it once the link is up: a Hermes-Lite 2 reports one
receiver until connected, then its board's ceiling, revised again when the
span outgrows the link budget. Both arrive on capabilitiesChanged, which
never resized the row. The row was built for one receiver (no tabs), and
every later updateSliceButtons() ran into an empty button list, so receivers
B, C and D existed with no tab to appear in.

ReceiverSlotCount (src/models) is now the one number for "how many receiver
letters to offer". It follows every edge that can move it (capabilities,
connection, info, slice added/removed) and announces changes; MainWindow
resizes the tabs from it. It reads RadioModel::maxSlices(), which already
defers to the backend's declaration for every non-Flex family, and names no
family. The count is floored at the highest occupied slot so a ceiling that
falls below the running receivers cannot strand one without a tab. The
existing infoChanged sizing stays (with the same floor): it is what lets a
Flex draw its tabs before its first slice arrives (aethersdr#2243).

receiver_slot_count_test drives a backend whose ceiling goes 1 -> 4 after
connect. Mutation-checked: with the tracker restricted to the pre-fix edge
(infoChanged only, no floor) 6 checks fail at the assertion, including
"a ceiling revised to 4 after connect offers four letters (A-D)"; removing
only the floor fails "a ceiling that falls below the running receivers
keeps a letter for each". MainWindow's two-line wiring of the signal into
AppletPanel is not under test.

Refs: aethersdr#5775

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MainWindow::catPortTargetCount() sized the CAT applet's VFO pickers from
RadioModel::maxSlicesForModel(model()), the Flex model table. It has no row
for a radio outside that family and answers its 2-slice default, so a
four-receiver Hermes-Lite 2 offered A and B only. RadioModel::maxSlices()
exists precisely to avoid that read; the RX applet's tabs already used it.

catPortTargetCount() now returns the same ReceiverSlotCount number the slice
tabs take (the backend-aware maxSlices(), floored at the occupied slots), and
the CAT applet's letters are refreshed on the same edge as the tabs, so a
ceiling declared or revised after connect reaches both. CatPort and both
dialects resolve targets by slice id and need no change.

On a Flex this reads the declared/licensed capacity instead of the model
table; for a radio that declared nothing the two are the same number.

The count itself is pinned by receiver_slot_count_test: with the tracker
fed from maxSlicesForModel() instead, 5 checks fail at the assertion,
including "the count is the backend's declaration, not the model table's
default". The MainWindow call site is not under test.

Refs: aethersdr#5776

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@on8st
on8st requested review from a team as code owners September 28, 2026 20:56
@aethersdr-agent

Copy link
Copy Markdown
Contributor

Thanks for this, @on8st — nice, well-scoped change, and the two new socket-free tests are exactly the kind of evidence that makes these easy to review.

CI is red on one step only, and it's not a compile error — it's the aetherd touchpoint ratchet.

What failed: Static checks → aetherd touchpoint manifest — python tools/gen_touchpoint_manifest.py --check (.github/workflows/static-checks.yml:178). Every other step in that job passed, and build / check-macos / check-windows were still running when I looked, so this is the only known breakage.

Why: src/gui/MainWindow.cpp now does

#include "models/ReceiverSlotCount.h"

That's a brand-new gui/ → src/models/ include, i.e. a new aetherd migration touchpoint. The generator scans src/gui/ + src/main.cpp for engine includes and regenerates docs/architecture/aetherd-touchpoints.md, so two things are now out of date and the check fails on both:

  1. No semantic tag. models/ReceiverSlotCount.h has no entry in docs/architecture/aetherd-touchpoint-tags.json, so invalid_live_tags() trips first and prints ::error::aetherd touchpoints require a valid semantic tag: models/ReceiverSlotCount.h (tools/gen_touchpoint_manifest.py:123).
  2. Stale manifest. docs/architecture/aetherd-touchpoints.md is a generated file and has no row for the new header (its totals line is wrong too).

How to fix — two steps, no rebuild:

Add the tag sidecar entry to docs/architecture/aetherd-touchpoint-tags.json. The tag must match ^(universal|ui-support|(vendor|mixed|peripheral)\(<family>\))$. Given the header asks nothing about the family — it's the connect/capability/slice edges and a max() — universal is the right call:

"models/ReceiverSlotCount.h": {
  "tag": "universal",
  "note": "How many receiver letters (A, B, C, ...) the UI offers, tracked from the connection/capability/slice edges. Family-agnostic: a declared ceiling floored by the slots live receivers occupy (#5775, #5776).",
  "split": "",
  "confidence": "high"
}

Then regenerate the table (never hand-edit it — the header says so):

python tools/gen_touchpoint_manifest.py
python tools/gen_touchpoint_manifest.py --check   # should print "manifest up to date"

and commit both docs/architecture/aetherd-touchpoint-tags.json and docs/architecture/aetherd-touchpoints.md.

Background on the ratchet: docs/aetherd-headless-engine-design.md §2/§10.

Two notes, neither blocking:

  • src/core/RigctlProtocol.cpp's new #include "models/PanadapterModel.h" is core → models, not gui → engine, so it doesn't add a touchpoint and the "Engine/UI dependency direction" check passed on it. No action there.
  • No Copilot or reviewer comments on the PR yet, so nothing else to fold in.

Once the two docs files are in, Static checks should go green; the compile jobs are unaffected by this step.


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

MainWindow now includes models/ReceiverSlotCount.h, a new gui->models
touchpoint, so the aetherd touchpoint ratchet in Static checks failed:
the header had no semantic tag and the generated manifest had no row
for it.

Tag it universal. It asks nothing about the radio family: it tracks the
connection, capability and slice edges and offers the backend's declared
receiver ceiling, floored by the slots live receivers occupy. The
manifest is regenerated with tools/gen_touchpoint_manifest.py, not
hand-edited.

Refs: aethersdr#5775
@on8st

on8st commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — fixed in b3f1ccc.

models/ReceiverSlotCount.h now has a universal entry in docs/architecture/aetherd-touchpoint-tags.json (your suggested note, extended to name the two surfaces it sizes: the RX slice tabs and the CAT VFO letters), and docs/architecture/aetherd-touchpoints.md is regenerated with tools/gen_touchpoint_manifest.py, not hand-edited: 231 headers (193 core, 38 models), 231/231 tagged. --check prints "manifest up to date".

I also ran the other Static checks scripts locally on the new head; all pass. The a11y step reports 4 warnings in MainWindow.cpp, all on existing menu actions around line 10410, none on lines this PR touches. Full ctest on b3f1ccc: 554/554 passed.

Agreed on the RigctlProtocol.cpp → PanadapterModel.h include: it's core → models, so not a touchpoint.

ON8ST

@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

Partially. #5774 and #5775 look solved at the mechanism level: L RF/l RF now go through the pan, normalized across the pan's published range, and ReceiverSlotCount follows the capability edge that an HL2's post-connect ceiling actually arrives on — I traced that relay end-to-end (Hl2Backend::announceReceiverCeilingRevision at Hl2Backend.cpp:1143 → IRadioBackend::capabilitiesChanged → RadioModel.cpp:1246 → publishCapabilities() → emit capabilitiesChanged at RadioModel.cpp:4665), so the fix does not depend on the test's hand-emitted signal. #5776 is solved for the HL2 but the half of the triage fix the PR declined is load-bearing, and skipping it introduces a new defect of the same shape on three other families — blocker 1.

Scope

File(s) Change Claimed? Verdict
src/core/RigctlProtocol.cpp pan-routed L RF/l RF, 0–1 ↔ published range yes (#5774) in scope
src/models/ReceiverSlotCount.{h,cpp} new tracker yes (#5775/#5776) in scope; registered in CMakeLists.txt MODEL_SOURCES:1100 → aethercore, and included by MainWindow.cpp:49 — not a dead addition
src/gui/MainWindow.cpp tab sizing on the capability edge; catPortTargetCount() yes in scope (see blocker 1)
CMakeLists.txt, tests/tests.cmake source + 2 test targets yes in scope
docs/architecture/aetherd-touchpoint* 230 → 231, one new row implied by the new header in scope; the repo's static check requires it
tests/*_test.cpp 2 new socket-free tests yes in scope

No CHANGELOG.md entry (correct). No new protocol verb, settings key or capability field. Nothing in the diff is unexplained by the three issues.

Socket-test disclosure: this PR adds no socket test. Both new tests drive in-process objects only — rigctl_rf_gain_pan_test calls RigctlProtocol::handleLine() directly, receiver_slot_count_test emits signals; neither constructs QTcpServer/QTcpSocket/QUdpSocket or binds anything, despite both linking Qt6::Network. Neither target appears in .github/ci-test-gate.txt, so the five green checks on b3f1ccc are compile + static only, not evidence these tests pass.

Blockers

1. A connected radio that declares one receiver now gets all eight CAT letters (A–H). Inline on MainWindow.cpp:6356.

applyCatPortCount() at MainWindow.cpp:6396 reads const int hwSlices = (target > 1) ? target : kCatPorts; (kCatPorts == 8, RadioSession.h:92). The target <= 1 arm is the "not connected, offer everything" fallback. Before this PR that arm was unreachable while connected: the old catPortTargetCount() returned maxSlicesForModel() = capabilitiesFor().maxSlices, whose default is 2 (ModelCapabilities.h:42) and whose every table row is ≥ 2. After this PR it returns ReceiverSlotCount::forCeiling(maxSlices(), slices()), and maxSlices() defers to the backend for every non-Flex family (RadioModel.h:330). Three backends declare maxSlices = 1:

  • AnanBackend.cpp:458
  • RtlSdrBackend.cpp:165
  • SimBackend.cpp:295

So on a connected ANAN-G2, RTL-SDR or the demo, target becomes 1 and the CAT applet is handed 8 — CatControlApplet::populateVfoCombo() then offers A…H for a single-receiver radio, where before it offered A,B. That is #5776's defect with the sign flipped. The HL2 also passes through it transiently: it declares 1 at the connect edge, so the letters go 8 → 4 as the ceiling is revised.

The PR body says this is "left alone on purpose… applyCatPortCount() still offers all 8 letters when a connected radio reports 1 receiver". That reads as pre-existing; per the above it is not — this PR is what makes that branch reachable. The #5776 triage comment proposed exactly this as item 2 of the fix and named RTL-SDR:

const int hwSlices = m_radioModel.isConnected() ? target : kCatPorts;

One line, at MainWindow.cpp:6396. If the maintainer prefers to keep offering all letters on a 1-receiver radio, that is a legitimate call — but it should be a decision taken knowing it is a change this PR introduces, not one it inherits.

This is also the untested seam: receiver_slot_count_test covers the tracker, and the body correctly discloses that MainWindow's wiring has no test. The single production line changed for #5776 is catPortTargetCount(), and the regression lives one function below it.

Nits (non-blocking)

  • forCeiling()'s floor is unbounded by wire data. ReceiverSlotCount.cpp raises the answer to slice->sliceId() + 1 with no ceiling, and RxApplet::setMaxSlices() constructs one QToolButton per unit. Previously the tab count came from maxSlices(), implicitly bounded by the model table / backend declaration. RadioModel::handleSliceStatus() creates a SliceModel for whatever slice number the wire carries; the 0 <= id < 8 guard at RadioModel.cpp:3071/3204 sits on the automation-fixture path, not on that ingest. A std::min(letters, 8) in forCeiling() would restore the bound cheaply. I have not shown that a real radio emits an out-of-range slice id — this is the removal of a bound, not a demonstrated failure.
  • The comment at MainWindow.cpp:1951-1952 is inaccurate. It says disconnect "is left to onConnectionStateChanged, which clears the tabs and trims the CAT letters itself". Grepping the head checkout, setMaxSlices has exactly four call sites (MainWindow.cpp:1942,1958,6397, MainWindow_Session.cpp:2241) and onConnectionStateChanged is none of them — tab capacity is never reset on disconnect. The CAT half is right (MainWindow_Session.cpp:2245). Behaviour is unchanged from main; only the comment overclaims.
  • The pan's default RF-gain range is Flex-shaped. PanadapterModel.h:300-302 defaults to -8..32 step 8. Between pan creation and the backend's rfGainInfoChanged, L RF 0.5 on an HL2 would compute +16 dB against the wrong scale instead of +18. Narrow window, pre-existing defaults, but the new code is the first thing to depend on them being right.
  • Flex l RF semantics change, as the body says, and the body also says it was not verified on a Flex. Worth a maintainer eye since third-party CAT clients bind to it.

What I tried to break (and failed)

  • Handler ordering on the connect edge. If MainWindow::onConnectionStateChanged ran after the tracker it could overwrite the good count with a stale one. It cannot: wireRadioModel() binds it at MainWindow.cpp:1662, the tracker is constructed at 1953, and Qt invokes in connection order — the tracker's countChanged lands last.
  • Sibling RF-gain surfaces left unfixed. AutomationServer.cpp:11466 already used setPanRfGainFor; SmartCatProtocol and TciServer have no RF-gain setter at all; SpectrumOverlayMenu.cpp:826-829 uses the same pan-first / slice-fallback shape rigctl now adopts. rigctl really was the last one.
  • Thread safety of the new direct pan reads. rfGainPanFor() dereferences a PanadapterModel synchronously while every setter around it uses Qt::QueuedConnection, which looks like a cross-thread read. CatPort.{h,cpp} contains no QThread/moveToThread — it lives on the GUI thread, so the queued invokes are reentrancy deferral and there is no dangling-pointer window.
  • Division by zero / degenerate ranges. panGainToRfLevel() divides by high - low; both call sites guard with rfGainHigh() <= rfGainLow() → RPRT -11 first. rfLevelToPanGain() floors the step at 1 and qBounds the level before scaling.
  • Shrinking counts stranding state. RxApplet::setMaxSlices() rebuilds when the count differs, and CatControlApplet::setMaxSlices() restores each VFO by data, not index, so a shrink cannot silently repoint a CAT port at a different slice.
  • Tests that would pass against unfixed code. They would not: l RF reading a slider-set +30 dB as 0.7, and the RPRT -11 pair, are unreachable for the old reader that echoed the client's own write. The author's own note that round-trips 0.5 passes pre-fix is honest and matches what I read.
  • CodeGuard's six CG-PATH-001 advisories — refuted. RigctlProtocol.cpp:572 is inside the hamlib_version reply string; MainWindow.cpp:9564/9676/9795-9797 are untouched by this diff. None is on a changed line and none involves a path.

Everything above is reasoned from the head checkout at b3f1ccc; I ran no build, no tests, and no GUI, so no runtime claim here was reproduced.

Recommendation

Needs maintainer decision. The RF-gain work is careful, the capability-edge tracker is the right shape, and the scope is clean. The one thing I would not merge as-is is the CAT-letter fallback: a connected ANAN/RTL/Sim going from two letters to eight is a new user-visible defect of exactly the kind #5776 reports, and the body describes it as inherited when it is introduced. Either take the triage's one-line isConnected() fallback, or rule explicitly that eight letters on a one-receiver radio is intended — then this is good to go.

Nice work on the tracker, and thank you for flagging the Flex l RF change and the untested MainWindow lambda yourself rather than leaving them to be found.


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

Comment thread src/gui/MainWindow.cpp Outdated
Comment thread src/models/ReceiverSlotCount.h
Comment thread src/gui/MainWindow.cpp Outdated
Comment thread src/core/RigctlProtocol.cpp Outdated
@aethersdr-agent

Copy link
Copy Markdown
Contributor

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

  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/core/RigctlProtocol.cpp /tmp/aetherclaude/pr-6013/src/core/RigctlProtocol.cpp:572
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6013/src/gui/MainWindow.cpp:9564
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6013/src/gui/MainWindow.cpp:9676
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6013/src/gui/MainWindow.cpp:9795
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6013/src/gui/MainWindow.cpp:9796
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6013/src/gui/MainWindow.cpp:9797

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


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

on8st and others added 2 commits September 29, 2026 10:43
applyCatPortCount() handed the CAT applet `(target > 1) ? target : 8`,
treating a count of 1 as "not connected, offer every letter". That held
while the count came from the Flex model table, whose smallest row is 2.
The previous commit on this branch made the count backend-aware, and
ANAN, RTL-SDR and the demo backend declare maxSlices = 1 (as does an
HL2 at its connect edge), so a connected one-receiver radio was offered
A-H where main offered A and B. This branch introduced that; the PR
body was wrong to call it pre-existing.

The fallback is now keyed on the connection, not the count, as the
aethersdr#5776 triage proposed: ReceiverSlotCount::catLetters() answers every
letter with no radio connected and the radio's own count while
connected, and applyCatPortCount() hands that to the applet unchanged.
The HL2's 1 -> 4 edge still ends at A-D: the capability edge refreshes
the tracker, whose countChanged re-runs applyCatPortCount().

forCeiling() is also bounded at RadioSession::kCatPorts (the letters
A-H). Its floor raises the count to sliceId + 1, a slice id is wire
data, and RxApplet builds one tab per unit, so the number had no upper
bound. No radio has been seen sending an id past 7; this restores the
bound the model table used to give implicitly.

The comment above the tracker's wiring claimed onConnectionStateChanged
clears the slice tabs on disconnect. It does not (setMaxSlices has no
call site there); only the CAT half was true, and that is all it now
says. Two comments that still named catPortTargetCount() as the CAT
letters' source are corrected with it.

receiver_slot_count_test, run against the old fallback lifted into
catLetters() verbatim, fails "a connected radio declaring one receiver
offers exactly one CAT letter" and both bound checks; all pass after.

Refs: aethersdr#5776
Refs: aethersdr#5775

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`L RF` / `l RF` scale Hamlib's 0-1 level across the pan's published RF
gain range. A pan exists from its geometry signal on, but its range
lands later (a Flex's rfgain_info reply, or a backend's
panRfGainInfoChanged). In between, PanadapterModel holds its own
defaults, -8..32 dB step 8 - a Flex's shape, and no statement by the
backend - so `L RF 0.5` on an HL2 would send +16 dB where half travel
of its -12..+48 dB LNA is +18, and the client could not tell.

PanadapterModel::hasRfGainRange() is set when a range is published,
by whichever family publishes it. rigctl answers RPRT -11 (RIG_ENAVAIL)
until it is true, as it already does for a degenerate range and for no
addressable RF gain control at all. Nothing is sent to the radio.

rigctl_rf_gain_pan_test gains the case: before the range is published,
`L RF 0.5` answered RPRT 0 and sent a gain, `l RF` returned a reading
(3 checks fail); after, both refuse, and once the range lands `L RF 0.5`
sends +18 dB and reads back as 0.5.

Refs: aethersdr#5774

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@on8st

on8st commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

You're right about the blocker, and the PR body was wrong. The target <= 1 fallback in applyCatPortCount() could not fire while connected before this PR, because the model table's smallest row is 2. This PR is what made it reachable: ANAN, RTL-SDR and the demo backend declare maxSlices = 1, as does an HL2 at its connect edge. I called that pre-existing and it was not. It is fixed in a1610e9, and the body now says explicitly that the earlier sentence was wrong.

Two new signed commits on top, no rebase: a1610e9 (points 1–3) and 2f02761 (point 4).

1. CAT letters on a connected one-receiver radio (blocker). Fixed the way the #5776 triage proposed: the fallback is keyed on the connection, not the count. So that it can be tested outside MainWindow, the choice now lives in ReceiverSlotCount::catLetters(const RadioModel*). It returns every letter (RadioSession::kCatPorts) with no radio connected, and forCeiling(maxSlices(), slices()) while connected. applyCatPortCount() hands that to the applet as is. On the HL2 1 → 4 edge, A is offered at connect, then the capability edge fires countChanged, which re-runs applyCatPortCount(), and it ends at A–D. New checks in receiver_slot_count_test: 8 letters with no radio, exactly 1 for a connected backend that declares 1, 4 after the revision to 4, and 8 again after disconnect. With the old (target > 1) ? target : kCatPorts fallback moved into catLetters() unchanged, [FAIL] a connected radio declaring one receiver offers exactly one CAT letter fails. With the fix, all 17 checks pass. What I have not tested is applyCatPortCount() passing the value on. It is one line, and MainWindow cannot be built in a unit test.

2. Unbounded slot floor. forCeiling() now returns at most RadioSession::kCatPorts, the existing constant behind the "all letters" fallback, so 8 is not typed in again. That caps both the floor and the declared ceiling. New checks: a slice id of 20 gives 8, and a declared ceiling of 12 gives 8. Without the cap, both fail. As you said, I have also not seen a radio send an id past 7.

3. Disconnect comment. You're right: onConnectionStateChanged does not reset the tab capacity. The comment now keeps only the CAT sentence. While editing it I also fixed two other comments that still named catPortTargetCount() as the source of the CAT letters. One of them said disconnect "trims CAT ports back to 1", which the old code did not do either.

4. Flex-shaped RF-gain defaults. Taken. PanadapterModel::hasRfGainRange() becomes true when setRfGainInfo() is called. That covers both the Flex rfgain_info reply and a backend's panRfGainInfoChanged, and nothing in it depends on the family. L RF and l RF answer RPRT -11 until it is true, the same way the degenerate-range and no-command-plane cases are handled. New case in rigctl_rf_gain_pan_test: while no range is published, set and get refuse and nothing reaches the backend. Before the guard those 3 checks fail: L RF 0.5 answered RPRT 0 and sent a gain. After the range is published, L RF 0.5 sends +18 dB and reads back as 0.5. One side effect on Flex: if the rfgain_info reply never arrives, L RF now refuses instead of scaling against the defaults.

5. CodeGuard. Same result as yours. None of the six findings is on a line this PR adds or changes. RigctlProtocol.cpp:572 is the hamlib_version comment (blame: 36c62ea, June), outside our hunks, which are at lines 3, 13–15, 230–259, 1467–1476 and 1692–1710 on b3f1ccc. MainWindow.cpp:9564, 9676 and 9795–9797 are the NR2/FFTW progress strings and the app-bundle QDir walk (blame: a8937a0, abbfc28, 8d2cd0f, May), and our MainWindow hunks on b3f1ccc are at 52, 1935–1961 and 6352–6356. On the new head they add 6362–6399 and 6819–6821, which are still far from any of the flagged lines. None of them involves a path taken from input. No change.

On 2f02761: every Static checks script I can run locally passes (gen_touchpoint_manifest --check: manifest up to date, since no new cross-layer include was added). The colour ratchet is +0. The a11y step reports the same 4 existing warnings around MainWindow.cpp:10410. Full ctest on 2f02761: 554/554 passed. Still not verified on a real radio.

ON8ST

main moved to 0817cec and the branch went CONFLICTING on one file,
docs/architecture/aetherd-touchpoints.md. Both sides added a row to the
generated touchpoint table and both rewrote its Totals line: main's
c50a2d3 added core/QtAudioBackendGuard.h (194 core), this branch added
models/ReceiverSlotCount.h (38 models).

The table is generated, so it is not hand-merged: it is the output of
tools/gen_touchpoint_manifest.py on the merged tree, which carries both
rows and 232 headers (194 core, 38 models). The tag sidecar
aetherd-touchpoint-tags.json merged cleanly and holds both entries.

CMakeLists.txt, tests/tests.cmake and src/gui/MainWindow.cpp merged
without conflict. aethersdr#5992's MainWindow hunk is in the PC Audio toggle and
this branch's are in the slice-tab and CAT-letter paths; they share no
symbol.

Refs: aethersdr#5774, aethersdr#5775, aethersdr#5776

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@on8st

on8st commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Merged main (0817cec) into the branch as 0feb92e, a merge commit and a fast-forward push, no rebase. One file conflicted, docs/architecture/aetherd-touchpoints.md: c50a2d3 added the core/QtAudioBackendGuard.h row and this branch added models/ReceiverSlotCount.h, and both rewrote the Totals line. It is not hand-merged: the table is the output of tools/gen_touchpoint_manifest.py on the merged tree (232 headers, 194 core, 38 models, both rows present), and --check reports it up to date. #5992 itself does not touch that file, and its MainWindow.cpp hunk (PC Audio toggle, ANAN only) shares no symbol with the slice-tab and CAT-letter paths changed here. Full suite on the merge commit, macOS, Qt 6.12.0: 560 of 560 pass; every step of static-checks.yml passes locally.

73, ON8ST

@ten9876

ten9876 commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Ruling on the Flex L RF/l RF change: keep Flex on its current path.

Third-party CAT clients bind to this, and nobody has checked the new semantics on a Flex. Please key the new routing on RadioModel::hasCommandPlane(), not on family:

  • With a command plane (Flex): L RF sends slice set N rfgain= (0–100) as on main, and l RF reads what it read on main. No change in behaviour, and no new RPRT -11 paths.
  • Without a command plane: the pan-routed path you built, including the hasRfGainRange() refusal.

A test case that pins the Flex path to the old wire text would be good. If you want to move Flex to the pan rfgain later, open it as its own issue with a capture from a Flex.

The CAT-letter fix (keyed on isConnected()) and the ReceiverSlotCount tracker are fine as they are.

@ten9876 ten9876 self-assigned this Oct 3, 2026
@ten9876

ten9876 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

We're updating this branch against main and landing it (maintainer pass); please don't push to it meanwhile. New commits go on top, with no rebase or force-push.

ten9876 and others added 3 commits October 2, 2026 17:47
One conflict, tests/tests.cmake: main retired wdsp_nb_hold_invariant_test
(aethersdr#6075, source removed) in the block next to this branch's two new test
registrations. Kept the two registrations, dropped the retired one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
…nd plane. Principle VIII.

Maintainer ruling on aethersdr#6013: key the rigctl RF routing on
RadioModel::hasCommandPlane(), not on family. With a command plane (Flex),
`L RF` sends `slice set N rfgain=` (0-100) and `l RF` reads the slice's rfgain,
byte for byte as on main. Without one, the pan-routed path with the
hasRfGainRange() refusal. rigctl_rf_gain_pan_test pins the command-plane wire
text on the in-process Demo. Comments condensed to AGENTS.md § Comments.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
@ten9876

ten9876 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

Maintainer pass, now at head 4ee29f1b0. Three commits were added on top of 0feb92e4. Nothing was rebased or force-pushed.

Evidence on 4ee29f1b0:

  • Unit tests: rigctl_rf_gain_pan_test 24/24 and receiver_slot_count_test all pass. Under an LD_PRELOAD shim, neither test makes a bind, connect or listen call.
  • Non-socket suite: ctest with the socket-owning tests excluded passes 542/543. The one failure is connection_panel_size_test, a known local failure (Four tests fail deterministically on clean main locally: the a11y announcement fixture never receives focus (offscreen) #5811).
  • Live run: the Demo ran offscreen, and a raw rigctl client was driven on a test port. L RF 0.37 → RPRT 0, l RF → 0.37. L RF 7 → l RF → 1. The slice's rfGain followed the client, and the pan's RF gain stayed at 0.
  • Static checks: the engine-boundary, capability-record, command-plane, test-registration, qsettings, network-timeout and a11y gates show the same findings at the merge base and at the head.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at 1e2eb1abc, the maintainer pass.

Maintainer ruling (Jeremy, 2026-10-02), implemented in 3b76ab67d: L RF / l RF are keyed on RadioModel::hasCommandPlane().

  • With a command plane (Flex): behaves exactly as on main. slice set N rfgain= (0–100), the slice's rfgain read back, and no new RPRT -11.
  • Without one: the pan-routed path, with the hasRfGainRange() refusal.
  • Test pin: a new case in rigctl_rf_gain_pan_test runs on the in-process Demo, which has a command plane and dials nothing. It asserts that L RF 0.5 emits exactly slice set 0 rfgain=50, that l RF answers 0.5, and that L RF 7 emits slice set 0 rfgain=100. The Demo's pan carries a published range, so routing this radio to the pan would actually send something.
  • Mutation-checked: against main's own RigctlProtocol.cpp, all 4 command-plane checks pass, which shows the Flex path is byte-identical to main. Against the PR's earlier pan-first code, 3 of them fail.
  • Live: the Demo ran offscreen with a raw rigctl client on a test port. L RF 0.37→RPRT 0, l RF→0.37. L RF 7→l RF→1. The slice rfGain followed the client, and the pan rfGain stayed at 0.

The CAT-letter fix (keyed on isConnected()) and the ReceiverSlotCount tracker are accepted as they are, per the same ruling.

Review findings: all four threads were fixed by @on8st and re-verified at this head. Each thread has a reply with file:line and a test name.

  1. CAT letters on a one-receiver radio: catLetters() is keyed on isConnected() (a1610e9). Pinned by receiver_slot_count_test.
  2. Unbounded slot floor: capped at RadioSession::kCatPorts (a1610e9). Pinned by receiver_slot_count_test.
  3. Disconnect comment overclaim: trimmed (a1610e9). The connect-side twin is corrected in 3b76ab67d.
  4. Flex-shaped default range: hasRfGainRange() refusal (2f02761). It now applies to radios without a command plane only, per the ruling.

Merges: 6519e6b2d, 4ee29f1b0 and 1e2eb1abc are signed merges of main, with no rebase and no force-push. The only conflict was in tests/tests.cmake: main retired wdsp_nb_hold_invariant_test (#6075) next to this PR's two registrations, and I kept the two registrations.

aetherd: one new touchpoint row, models/ReceiverSlotCount.h, tagged universal. The bot review proposed that tag and the tracker was accepted with it. The manifest was generated by the tool and is up to date. The engine-boundary, capability-record, command-plane, test-registration, qsettings, network-timeout and a11y findings are the same at the merge base and at the head.

Verified locally (Linux):

  • Non-socket suite: ctest with socket-owning tests excluded passes 542/543. The one failure is the known local connection_panel_size_test (#5811).
  • This PR's two tests: both pass, and an LD_PRELOAD shim showed no bind, connect or listen calls.
  • Automated review: code-review at medium found no bugs.

Not verified: no real radio was driven. HL2 behaviour comes from unit tests with an injected backend.

@ten9876
ten9876 merged commit ad46155 into aethersdr:main Oct 3, 2026
5 checks passed
ten9876 added a commit to on8st/AetherSDR that referenced this pull request Oct 3, 2026
Conflict: tests/tests.cmake, adjacent test blocks (aethersdr#6013's two tests and
reroute_dead_controls_test); all kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
ten9876 added a commit to on8st/AetherSDR that referenced this pull request Oct 3, 2026
Brings in aethersdr#6013 (adjacent tests/tests.cmake registrations); no conflict.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
ten9876 added a commit that referenced this pull request Oct 3, 2026
…6050)

## Summary

On a Hermes-Lite 2, two receive controls had no route to the DSP that
runs on this host:

- **The AGC-T slider with AGC Off** moved `SliceModel::agcOffLevel`, and
the HL2 backend ignored the request, so every HL2 ran AGC Off at WDSP's
channel default of 10 dB (`WdspChannel::Config::agcFixedGainDb`)
whatever the slider said.
- **The CW audio peaking filter (APF)** was Flex wire text only (`apf=`
/ `apf_level=`). That is why HL2 declared `hasAudioPeakingFilter =
false` and the APF row stayed hidden, although WDSP builds a
peaking-filter stage into every receive channel.

## Seam

- **AGC-off level: main's typed route.** `SliceModel::setAgcOffLevel`
emits `receiveAgcRequested(SliceAgcRequest::Field::OffLevel)` →
`IRadioBackend::requestSliceAgc` (#5907). `Hl2Backend` now overrides
`requestSliceAgc`: it handles `Field::OffLevel` itself and passes every
other field to `IRadioBackend::requestSliceAgc` (the paired
`setSliceAgc`). No new verb.
- **APF: one new seam verb, interim.**
`IRadioBackend::setSliceApf(sliceId, on, level)` is a defaulted no-op.
`SliceModel` emits `apfCommandIssued(on, level)` from the operator
setters only, never from status application (Principle II), and
`RadioModel::wireBackendReceiverState()` routes it next to the squelch
route. **It folds into #5919's `SliceDspRequest::Feature::Apf` when
#5919 lands.**
- **Flex is unchanged.** It overrides neither verb, its
`apf=`/`apf_level=` text from SliceModel is byte-identical, and its
`agc_off_level=` text comes from `FlexBackend::requestSliceAgc` as on
main. Icom, RTL, Sim and ANAN keep the base no-op and still declare
`hasAudioPeakingFilter = false`.
- The `agcOffLevel` branch in `SliceModel::applyChanges()` gets the same
unchanged-value guard as the threshold beside it. HL2 now echoes the off
level on every slice-state publish, and `AgcCalibrationDialog` wires
`agcOffLevelChanged` to the calibrator.

**Maintainer decisions (KK7GWY, 2026-10-02):** (1) the AGC-off level
uses main's `requestSliceAgc` route, not a parallel verb; (2)
`setSliceApf` is accepted as an interim verb until #5919's request type
replaces it.

## HL2

- **AGC-off level → `SetRXAAGCFixed`**, via a new
`WdspChannel::setAgcFixedGain()`. The mapping is `gain_dB = 10 +
0.6·(level − 10)`, so the range is 4..64 dB. **The default level 10
gives exactly 10.0 dB**, so AGC Off sounds the same until the operator
moves the slider. The slope matches the AGC-T threshold's 0.6 dB/unit.
WDSP applies the fixed gain only in AGC mode 0, so the value is pushed
in every mode and takes effect when AGC is switched off.
- **APF → WDSP SPCW** (`apfshadow`:
`SetRXASPCWSelection/Run/Freq/Bandwidth/Gain`, declared in
`aether_wdsp.h`), via `WdspChannel::setApf()`:
- The filter is centred on the CW pitch in audio Hz, and it follows a
pitch change on every receiver, whether or not it is in CW at the time.
- Level 0..100 sets the bandwidth, because the control is labelled "APF
bandwidth" / "Higher values narrow the peak". The map is 200 Hz → 50 Hz,
geometric. The default of 50 gives **100 Hz at gain 2.0, which is
RXA.c's own construction design**.
- The stage runs only in CWL/CWU. The request is kept through a trip to
SSB and comes back with CW.
- `open()` selects the double-pole explicitly. An unchanged `setApf()`
returns before the control handshake and the FFTW planner lock.
- While it runs, both audio outputs derive from I (WDSP's mode-2
double-pole copies I into Q).
- `hasAudioPeakingFilter` is now `true` for HL2. The caps map row says
the host runs it.
- Both settings are held per receiver outside `Config` and applied again
on channel rebuild, reconnect and new panadapter, the same way the noise
blanker is.
- Both appear in `dsp.chains` as what WDSP last **accepted** (`apfRun`,
`apfCenterHz`, `apfBandwidthHz`, `apfGain`, `agcFixedGainDb`).
- **Not persisted, on purpose:** like the host noise blanker. After a
reconnect the off level is 10 again and the APF is off.

## Tests

Both new tests are socket-free and registered in `tests/tests.cmake`.

- **`slice_apf_agc_off_seam_test`** (a recording backend installed
through `RadioModel`'s production receiver bindings):
  - Flex's APF wire text is byte-identical.
- The operator setters emit the APF intent with enable and level
together.
- `RadioModel` routes the APF to `setSliceApf` and the off level to
`requestSliceAgc(Field::OffLevel)` for the right slice, including
through the AGC-T knob and the AGC-T calibrator's sweep and restore.
- Status application never re-emits a command, and an echo at an
unchanged off level notifies nothing.
- The base verbs emit no signal, and the base drops `OffLevel` rather
than calling `setSliceAgc`.
- **`hl2_apf_agc_off_test`** (a real `Hl2RxDsp` and WDSP channel,
measured audio):
  1. the level maps;
2. refusals, and an unchanged `setApf()` that does not wait on the
planner lock;
3. AGC Off: 50 units is 30 dB of audio, and with AGC on the off level
changes nothing;
4. APF in CWU lifts the pitch over a tone 200 Hz away by more than 10
dB, outside CW it is held and inert, and CWL is centred on the pitch;
  5. both survive a rebuild;
6. through `Hl2Backend`: `setSliceApf` / `requestSliceAgc(OffLevel)` →
WDSP → published slice state and `dsp.chains`. A pitch set outside CW is
the centre on entering CW, and a `Mode` request still reaches the
chain's AGC and leaves the off level alone.
- **Mutation-checked** (each goes red, and green again when restored):
  - sending the APF only to receivers already in CW;
  - `Hl2Backend::requestSliceAgc` not handling `OffLevel`;
  - not delegating other fields to the base.

**Results at the head (Linux, RelWithDebInfo):**
- Full build. Non-socket ctest 549/550 (including #6049's
`reroute_dead_controls_test`); the one failure is
`connection_panel_size_test`, which fails locally (#5811).
- Static gates: engine boundary, touchpoint manifest, capability records
(frozen count unchanged), command plane, test registration and the CI
test gate all pass, with findings identical to main's.
- on8st's earlier run: 556/556 on macOS.

### Hardware evidence (receive only)

Hermes-Lite 2 (gateware 74) with the antenna switch on the dummy load,
read through the automation bridge with TX blocked. This run was at an
earlier head, when the AGC-off level still used its own verb. The
HL2/WDSP half is the same code, and the route change is covered by the
tests above.

| # | Condition | Expected | Observed | Result |
|---|---|---|---|---|
| A1 | AGC Off, off level 10 | fixed gain 10 dB | 10 dB | PASS |
| A2 | AGC Off, off level 40 | 28 dB | 28 dB | PASS |
| A3 | AGC Off, off level 100 | 64 dB | 64 dB | PASS |
| P1–P3 | APF not requested (USB / CW / CWL) | not running | `apfRun`
false | PASS |
| P4 | APF on, CW | running at CW pitch | `apfRun` true, 600 Hz (pitch
600), 100 Hz, gain 2 | PASS |
| P5 | APF on, CWL | running at CW pitch | `apfRun` true, 600 Hz, 100
Hz, gain 2 | PASS |
| P6 | APF requested, USB | held, not running | `apfRun` false, model
still on | PASS |
| P7 | back to CW | running again | `apfRun` true, 600 Hz | PASS |

## Not covered

- No listening check of the APF on hardware. The audible effect is
covered by the offline measurements (sections 3 and 4).
- The APF level slider was not swept on hardware.
- No transmit path is touched, and nothing was transmitted.

## Update against main

Merged `main` into this branch (no rebase). The conflicts were adjacent
additions next to #5982's squelch, #5980's pan averaging and #5981's
filter length in `Hl2Backend`, `Hl2RxDsp` and `WdspChannel`, plus test
registrations; both sides were kept. `SliceModel::setAgcOffLevel` took
main's version, and the following commit moved the HL2 route onto it
(decision 1 above). Two later merges brought in #6013 and #6049 cleanly;
#6049's band-stack AGC recall reaches `requestSliceAgc`, which
`Hl2Backend` passes to the base for every field but `OffLevel`.

Refs #5262, #5917.

73, ON8ST

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants