Skip to content

fix(icom): Fix Icom RX filter preset identity, skirt mapping, and recall - #5363

Merged
rfoust merged 5 commits into
aethersdr:mainfrom
jensenpat:aether/fix-icom-filter-applet
Sep 5, 2026
Merged

fix(icom): Fix Icom RX filter preset identity, skirt mapping, and recall#5363
rfoust merged 5 commits into
aethersdr:mainfrom
jensenpat:aether/fix-icom-filter-applet

Conversation

@jensenpat

@jensenpat jensenpat commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fix the Icom RX filter controls so FIL1/FIL2/FIL3 remain stable radio-owned preset identities, skirt dragging follows the connected radio's mode-specific capabilities, and clicking any FIL button—including the already-selected button—recalls that preset's default width with Twin PBT centered.

This addresses the filter applet and VFO behavior observed on an IC-7300MK2 across USB, LSB, AM, and CW.

User-visible behavior fixed

  • Filter buttons remain labeled FIL1, FIL2, and FIL3 after their widths are customized.
  • A customized slot remains the same slot; its mutable width no longer causes it to be renamed, reordered, or highlighted as a different preset.
  • The selected button follows the radio-reported FIL identity rather than whichever button happens to have the nearest width.
  • Applet and VFO filter controls use the same preset identity and selection path.
  • Skirt dragging scales to the current mode's real Icom range instead of a fixed 6 kHz/4 kHz pixel mapping:
    • SSB/CW family: 50 Hz to 3.6 kHz
    • AM family: 200 Hz to 10 kHz
  • Low-edge and high-edge drags preserve the untouched edge while enforcing the radio's minimum and maximum widths.
  • Drag results snap to the radio-advertised step rather than an unconditional 50 Hz grid.
  • Clicking FIL1/FIL2/FIL3 is an explicit preset-recall action, separate from a free-form skirt edit.
  • Clicking the currently selected FIL button resets its customized width and centers both Twin-PBT controls.
  • Radio-owned Icom filter state is not replaced by client AppSettings filter presets during mode changes or reconnect presentation rebuilds.

Root causes

The UI previously represented radio filter presets only as a sorted list of widths. That loses an important part of the Icom contract: FIL1/FIL2/FIL3 are stable slot identities, while the width stored in each slot is mutable. Once a slot was customized and 1A 03 read it back, sorting and nearest-width matching could rename, reorder, or highlight the wrong button.

Preset selection and passband editing also shared setSliceFilter(low, high). The backend tried to infer a button click by comparing the requested width with the factory ladder. A drag that happened to land on a ladder width was indistinguishable from a FIL selection, and a customized preset could no longer be selected reliably by identity.

Finally, FilterPassbandWidget used fixed pixel-to-Hz constants. Those constants could not simultaneously describe the IC-7300MK2's 3.6 kHz SSB/CW range and 10 kHz AM range, causing the panadapter/app skirt gestures to over- or under-drive the radio depending on mode.

Implementation

Stable preset identity

  • Add neutral RxFilterPreset / RxFilterControl capability data containing stable ID, label, mutable width, selected ID, and mode-specific width limits.
  • Publish the selected Icom slot in radio order as FIL1/FIL2/FIL3 while retaining the legacy width-only list for existing consumers.
  • Add a dedicated IRadioBackend::setSliceFilterPreset() seam and validate advertised preset IDs in RadioModel before dispatch.
  • Keep the default capability empty and the default backend method a no-op, preserving the existing Flex, HL2, ANAN, and Sim width-button contract.
  • Feed the same capability into both RxApplet and VfoWidget; reject incomplete/stale metadata during disconnect or mode-transition rebuilds.

Correct skirt mapping

  • Introduce socket-free FilterPassbandMath helpers for mode-aware Hz-per-pixel scaling and width constraints.
  • Anchor the untouched edge for one-sided skirt drags.
  • Clamp symmetric and one-sided edits to the current radio/mode minimum and maximum.
  • Use the advertised width step for gesture quantization.

Explicit recall semantics

  • A FIL click now builds one ordered CI-V recall plan:
    1. select the requested FIL identity while preserving the DATA flag (26 00, or model fallback 06);
    2. write the preset's default IF width (1A 03);
    3. center inner PBT (14 07, value 128);
    4. center outer PBT (14 08, value 128).
  • The sequence is issued even when the requested FIL slot is already selected.
  • Optimistic state uses the same centered-passband calculation as codec readback; subsequent CI-V replies remain authoritative and correct radio quantization.

Automation coverage

  • Add slice filterpreset FIL1|FIL2|FIL3 so automation selects a preset by stable identity rather than synthesizing a width.
  • Add filterPresetId and filterPreset to slice snapshots.
  • Document the new verb and the existing CI-V incident diagnostic used while investigating the reported test crash.

Expected recall geometry

Mode FIL1 FIL2 FIL3
USB 0..3000 Hz 300..2700 Hz 600..2400 Hz
LSB -3000..0 Hz -2700..-300 Hz -2400..-600 Hz
AM -4500..4500 Hz -3000..3000 Hz -1500..1500 Hz
CW -600..600 Hz -250..250 Hz -125..125 Hz

Validation

Live IC-7300MK2 automation-bridge proof

Ran a customize-then-click-the-same-visible-button sweep for all 12 combinations of USB, LSB, AM, and CW with FIL1/FIL2/FIL3.

For every case:

  1. selected the mode and FIL slot;
  2. applied a non-default skirt shape;
  3. confirmed the customized radio readback;
  4. invoked the visible Receive filter FILn button;
  5. waited for radio-authoritative state;
  6. confirmed the exact expected edges in the table above.

Result: 12/12 passed. The authenticated CI-V scheduler recorded 14,353 replies, 0 timeouts, 0 late replies, 0 stale replies, and 0 unmatched frames in the final verification snapshot.

The bridge was launched with transmit disabled (txAllowed=false). The radio remained unkeyed. After testing it was restored to 14.2435 MHz, DIGU, FIL1, -300..3300 Hz, ANT1, verified transmitting=false, and disconnected cleanly.

Hardware proof is specifically from the IC-7300MK2. The implementation is in the shared profile-driven Icom backend used by IC-705, IC-7300MK2, and IC-9700; socket-free codec coverage exercises the shared path, but this PR does not claim live IC-705 or IC-9700 hardware validation.

Automated tests

  • icom_civ_test: stable preset identity plus the exact four-command recall plan and expected geometry for USB/LSB/AM/CW x FIL1/FIL2/FIL3.
  • filter_passband_math_test: mode-aware gesture scaling, minimum/maximum clamps, untouched-edge anchoring, and stale-capability rejection.
  • Mutation check: removing the outer-PBT centering command caused all 12 recall-plan checks to fail; the command was restored before the final run.
  • Focused headless gate: 9/9 passed:
    • icom_civ_test
    • icom_civ_scheduler_test
    • icom_meters_test
    • icom_family_test
    • rf_gain_presentation_test
    • icom_power_derivation_test
    • phone_cw_mic_gain_authority_test
    • filter_passband_math_test
    • bridge_docs_check
  • tools/check_test_registration.py --strict: passed.
  • tools/check_engine_boundary.py --strict: passed with only tracked legacy warnings.
  • git diff --check: passed.
  • macOS AetherSDR.app build: passed using -j22.

Scope and compatibility

  • No transmit behavior is changed.
  • No Flex command encoding or Flex filter UX path is changed.
  • Flex, HL2, ANAN, Sim, and other backends continue to publish an empty rxFilterControl, so their existing width-based buttons and persistence remain unchanged.
  • The new automation verb validates against radio-advertised presets and cannot select an undeclared ID.
  • All commits are signed.

Generated with OpenAI Codex (Daybreak Blue)

Review follow-up: fixed-width selection and legacy gesture compatibility

  • Fixed-width FM/NFM/DFM/WFM/DV/DSTAR recall retains the mode/slot command and omits IF-width and Twin-PBT writes. The backend receives a zero programmable-width value while retaining the slot's display edges.
  • Empty filter capabilities now preserve legacy unrestricted width, 6 kHz horizontal / 4 kHz vertical gesture scales, and 50 Hz truncation. Clearing an Icom range on disconnect restores that behavior.
  • icom_civ_test now covers all advertised fixed-mode slots through both mode-command variants, the DATA flag, and invalid IDs.
  • New socket-free filter_passband_widget_test injects Qt mouse events into the production widget. It covers 10 kHz AM shift and widening, legacy vertical scaling/minimum width, advertised AM/USB maximums, and clearing capabilities.
  • Mutation verification: removing the two repairs while retaining the new tests produced 32 codec failures and four widget failures. Both fixes were restored.

Known non-blocking boundary: final edge rounding can still move an off-grid untouched edge (for example the 125 Hz edge of CW FIL3); this predates these repairs. Icom's 50 Hz fine gesture step also remains subject to the codec's coarser quantization above 500 Hz.

Validation of the repaired current-main tree (a4227e68 plus repair b77b84e5): full macOS ARM64 application build with RADE enabled passed; icom_civ_test, filter_passband_widget_test, filter_passband_math_test, and bridge_docs_check passed (4/4). Strict engine-boundary, test-registration, and frozen CI-gate checks passed. Both added commits are signed. The earlier full-app offscreen launch could not expose the bridge after QRhi initialization failures; runtime evidence here is the socket-free production widget event test, not a new hardware or full-app demo sweep.

@jensenpat
jensenpat marked this pull request as ready for review September 1, 2026 03:12
@jensenpat
jensenpat requested a review from a team as a code owner September 1, 2026 03:12
@jensenpat jensenpat changed the title Fix Icom RX filter preset identity, skirt mapping, and recall fix(icom): Fix Icom RX filter preset identity, skirt mapping, and recall Sep 1, 2026

@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.

1. Issue fit

There is no linked issue — the body describes behaviour "observed on an IC-7300MK2" rather than citing a report. That matters here because the PR does two different things under one title: (a) it fixes a genuine defect (FIL identity was being inferred from a sorted width list, so a customised slot got renamed/reordered/mis-highlighted — that root cause is real and the fix is well-shaped), and (b) it introduces a new recall semantic ("clicking FILn resets its customised width and centres both PBTs") that is not a defect fix and has no authority behind it. Part (a) is solid. Part (b) needs a maintainer ruling, and as written it deletes state the radio persists.

The validation is honest and detailed, but it covers USB/LSB/AM/CW only. FM, DV and WFM are neither tested nor exercised, and that is exactly where the diff breaks (Blocker 1).

2. Scope

File / group What it changes Claimed by the body? Verdict
RadioCapabilities.h (RxFilterPreset/RxFilterControl, hasCompleteRxFilterPresets) New capability struct + helper Yes In scope. New capability surface — caps-map row is present, good.
IRadioBackend.h (setSliceFilterPreset default no-op) New backend seam Yes In scope; default no-op preserves Flex/HL2/ANAN/Sim.
CivCodec.{h,cpp} (filterPresetsForMode, filterPresetRecallPlan) Preset identity + wire recall plan Yes In scope; identity half is the actual fix.
IcomCivBackend.{h,cpp} Publish rxFilterControl; split slot-pick out of setSliceFilter Yes In scope, but see Blockers 1 & 2.
RxApplet, VfoWidget, AppletPanel, MainWindow* Feed the capability to both filter surfaces; label/tooltip/a11y Yes In scope. a11y names added — nice.
FilterPassbandMath.h + FilterPassbandWidget Mode-aware Hz/px and clamps Yes In scope. Header is referenced and the test target is registered in tests.cmake:4030.
AutomationServer.cpp (slice filterpreset, snapshot fields) New public protocol verb + 2 new snapshot fields Yes Needs maintainer decision — third parties bind to bridge verbs and they outlive the fix. Reasonable addition, but it is protocol surface arriving inside a fix PR.
docs/automation-bridge.md civ ... incident row Syncs the doc table to the help string already on main (AutomationServer.cpp:3459) Partly ("document the existing diagnostic") Not a blocker — unrelated doc drift, but it is one line and bridge_docs_check likely requires it.
docs/architecture/radio-capabilities-map.md New caps row Yes Required by AGENTS.md. In scope.

No CHANGELOG.md entry — correct.

3. Blockers

1. FIL buttons are now completely dead in FM / DV / WFM — a removed guard whose symptom recurs

setSliceFilter previously had this branch, which this PR deletes:

"FM, DV and WFM have no settable width at all, so the slot IS the only filter control the radio offers there and every request has to be a slot pick."

The replacement comment at IcomCivBackend.cpp:3783 asserts "Their FIL selection travels through setSliceFilterPreset()". Reading the head checkout, it does not:

  • widthClassFor() returns WidthClass::Fixed for FM/NFM/DFM/WFM/DV/DSTAR (CivCodec.cpp:555).
  • filterWidthCodeFor() returns nullopt immediately for Fixed (CivCodec.cpp:591).
  • filterPresetRecallPlan() therefore hits if (!widthCode) return std::nullopt; (CivCodec.cpp:534) and discards the whole plan, including the already-built slot-select command.
  • setSliceFilterPreset() then returns on !planno CI-V frame is sent at all, silently, with no qWarning.

FM's ladder is {15000, 10000, 7000} (distinct), so filterPresetsForMode publishes three FIL buttons in FM — all three inert. WFM collapses to one button, also inert. Before this PR, clicking a filter button in FM sent cmdSetVfoMode(addr, m_mode, m_dataMode, filter) and worked.

Fix: emit the plan when only the width write is unavailable, rather than failing the whole plan — build the select command, and append the width/PBT writes only when filterWidthCodeFor yields a code. (Reasoned from code; I cannot run the Icom backend here.)

There is also no test for it — checkRecall in tests/icom_civ_test.cpp:504-515 covers USB/LSB/AM/CW only. An FM case asserting a one-command plan would have caught this and is exactly the socket-free codec layer AGENTS.md points at.

2. Every FIL click overwrites the slot's radio-persisted width (Constitution III)

setSliceFilterPreset always sends the mode's client-side factory 1A 03 width plus centred 14 07/14 08 after the slot select (IcomCivBackend.cpp:3855, and the body states this is deliberate: "The sequence is issued even when the requested FIL slot is already selected.").

On an Icom the width and Twin-PBT position stored inside FIL1/FIL2/FIL3 are state the radio persists and recalls. Constitution III:

"If the radio can persist and recall a setting, that setting is saved to and recalled from the radio — never duplicated in client-side config. … the deciding test is simply whether the radio can save and restore the value."

So clicking through FIL1→FIL2→FIL3 now destroys all three of the operator's customised filters, replacing them with a table hardcoded in ladderFor(). That is the client writing its remembered value back over the radio's, which II forbids and III owns. It also directly reverses the comment the first commit in this same branch preserves and the second deletes:

"the slots are the operator's own three presets and clicking through them must not redefine them."

Commit 1 and commit 2 hold opposite positions on the same question. I don't think this is smuggled — the body is upfront about it — but "the filter button should reset the slot" is a preference/design decision, not a defect fix, it has no issue behind it, and it conflicts with canon. Per GOVERNANCE.md this needs its own issue and a maintainer ruling.

Suggested split: land the identity fix (commit 1 + the capability plumbing + the skirt math) as-is; take the recall semantics to a separate issue. If recall is wanted, a plausible middle ground is to recall only when the requested slot is already selected (an explicit "reset this preset" gesture), leaving a plain slot change non-destructive.

4. Nits (non-blocking)

  • IcomCivBackend.cpp:444widthStepHz = limits.minHz == 200 ? 200 : 50; discards limits.stepHz, which filterWidthLimitsFor already authors (SSB/RTTY = 100). Above 600 Hz the SSB table is a 100 Hz grid (filterWidthHzFromCode, CivCodec.cpp:582), so a 50 Hz snap produces requests the radio quantises straight back. = limits.stepHz is the answer the codec already has. (Not a regression — the old constant was an unconditional 50.)
  • filterForWidthHz() now has no production caller — its only one was the deleted slot-pick branch. It survives in CivCodec.h:470 and in tests. Worth deleting or explicitly keeping with a note.
  • MainWindow::applyCapabilitiesToUi calls setRadioFilterControl and then setRadioFilterWidths; the second stomps the m_radioFilterWidths the first just wrote, and when presets are complete only the list's size is ever read. Harmless today, but the two setters are load-bearing on ordering for a value nothing consumes. Worth a comment or collapsing to one setter.
  • Radio widths now beat the operator's saved FilterPresets_<mode> (RxApplet.cpp:2822, VfoWidget.cpp:5443). The body discloses this, but the right-click "Set Custom Edges…" path still writes settings that are now unreadable while an Icom is connected — the menu item should probably be disabled there.
  • CodeGuard's five CG-PATH-001 hits are all in MainWindow.cpp at lines 8872–9105, far from this diff's applyCapabilitiesToUi hunks (~7194/7318). Pre-existing, not introduced here — dropping them.

5. What I tried to break (and failed)

  • Stale/mismatched preset metadata during a mode or connect transition. hasCompleteRxFilterPresets gating both presets.at(i) indexing sites (RxApplet.cpp:3038ff, VfoWidget.cpp:5569ff) holds: I walked USB↔WFM↔FM transitions where the preset count changes 3↔1 and the size check rejects the mismatched pair before any .at(). No out-of-range path found.
  • Disconnect. RxFilterControl{}setWidthRange(0,0,0) falls back to 50/6000/50, and setRadioFilterWidths({}) clears the width list, so the operator's own presets come back. Reversible as claimed.
  • The stated recall geometry table. I checked all 12 entries against passbandCentreHz + passbandFromWidthAndPbt by hand; USB FIL1 = 0..3000 etc. all follow, and they agree with the readback path (not with passbandForModeAndFilter, but the readback path is what the UI actually sees, so that is consistent).
  • The automation verb. slice filterpreset rejects non-FIL junk (toInt fails), rejects undeclared ids via find_if on the advertised list, and RadioModel::selectRadioFilterPreset re-validates independently. Empty-slice-list case returns a proper error rather than dereferencing. Clean.
  • Test honesty. I checked whether the new tests would pass against unfixed code: filterPresetsForMode with a custom width and filterPresetRecallPlan's four-command assertion are both new surface, so they cannot pass on main — they are real tests, not tautologies. filter_passband_math_test is registered at tests/tests.cmake:4030 and links Qt6::Core, so it is not a dead target.
  • Socket tests: none added or removed by this PR. Both new tests are socket-free CTest at the codec/math layer, which is the correct boundary for wire encoding and presentation math.
  • Could not check: anything requiring a build, a GUI, or a radio. Every finding above is reasoned from the head checkout at /tmp/aetherclaude/pr-5363, not reproduced at runtime. I also could not exercise the live IC-7300MK2 sweep the body reports; I have no way to confirm or refute the 12/12 result, though its scope (USB/LSB/AM/CW) is what makes Blocker 1 invisible to it.

6. Recommendation

Needs maintainer decision, with one hard blocker to fix first.

Blocker 1 is a straightforward regression with a small fix and a missing FM test case — that one is unambiguous. Blocker 2 is a genuine design question the maintainer should rule on, not something I should decide: the identity fix in this PR is good work and clearly correct, but the recall semantics bolted onto it overwrite radio-persisted state and contradict the comment the same branch's first commit preserves.

Concrete next step: fix the FM/DV/WFM plan and add the codec test case; then either split the recall commit into its own issue/PR, or get an explicit ruling that a FIL click may redefine the slot before merging it.

Thanks for the unusually thorough write-up — the wire-format derivation and the mutation check on the outer-PBT command are the kind of evidence that makes a review like this possible.


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

Comment thread src/core/backends/icom/CivCodec.cpp
Comment thread src/core/backends/icom/IcomCivBackend.cpp
Comment thread src/core/backends/icom/IcomCivBackend.cpp
Comment thread src/core/backends/icom/IcomCivBackend.cpp
@aethersdr-agent

Copy link
Copy Markdown
Contributor

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

  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-5363/src/gui/MainWindow.cpp:8872
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-5363/src/gui/MainWindow.cpp:8984
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-5363/src/gui/MainWindow.cpp:9103
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-5363/src/gui/MainWindow.cpp:9104
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-5363/src/gui/MainWindow.cpp:9105

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: $5.1570 · model: claude-opus-5

ten9876
ten9876 previously requested changes Sep 1, 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.

Issue fit

No linked issue; reviewed against the title's three claims (preset identity, skirt mapping, recall). The Icom-decomposition core is genuinely good: two Flex-style passband edges reduced to one width + one Twin-PBT shift, reversibly, with FilterPassbandMath extracted and unit-tested, and the preset-recall-is-not-slot-select insight (re-assert factory width + centred PBT because the radio remembers a mutable shape per FIL slot) is correct and well-commented. The skirt/PBT and SSB/CW/AM/RTTY recall paths look right.

But one mode class the feature explicitly enumerates — the fixed-width modes — comes out with dead filter buttons, and that's the blocker.

Scope

25 files, all explained by the three claims. Preflight: no sockets, no fake peers; two new pure-math/codec tests. CI green.

Blockers

1. FM/WFM/DV filter-slot buttons now send nothing (inline). filterPresetRecallPlan pushes the slot-select command first (cmdSetVfoMode/cmdSetMode), then computes the 1A 03 width via filterWidthCodeFor and returns nullopt when there is no settable width — which is exactly the fixed-width modes (widthClassFor(FM) == Fixed), whose own comment says FM has three real fixed slots at 15/10/7 kHz. setSliceFilterPreset early-returns on !plan, so the slot-select command that was already built is discarded with it. Clicking FIL1/FIL2/FIL3 in FM/WFM/DV issues no CI-V at all. The pre-refactor setSliceFilter sent the mode/slot command in precisely the maxHz <= 0 case; the new path dropped it. The tests cover only USB/LSB/AM/CW, so the gap is uncovered. Fix: when widthCode is absent, return the plan with just the select command (and no 1A 03 / PBT writes) rather than discarding it — the fixed slots need no width write.

Nits (non-blocking)

  • widthStepHz re-derived instead of read (inline): c.rxFilterControl.widthStepHz = limits.minHz == 200 ? 200 : 50 ignores the authoritative FilterWidthLimits.coarseStepHz (SSB/CW/RTTY = 100, AM = 200) that filterWidthLimitsFor already returns. RTTY and SSB get a 50 Hz gesture grid the radio doesn't use, so radio readback silently re-quantizes every drag — contradicting the PR's "snap to the radio-advertised step". Read coarseStepHz.
  • Per-slice snapshot uses the radio-global control (inline): doGet("slices") passes the same radio->radioFilterControl() (single-active-slice, radio-global) into every slice's snapshot, so on a main+sub in different modes both slices report the active slice's filterPreset/filterPresetId. Automation reads the sub slice's filter wrong. Resolve per-slice or document the field as active-slice-only.
  • Double rebuild of m_radioFilterWidths (MainWindow.cpp:7197, and the VfoWidget twin at :7321): setRadioFilterControl sets it from presets in radio order and rebuilds, then setRadioFilterWidths(sorted) overwrites and rebuilds again — final list is sorted while m_radioFilterControl.presets stay radio-ordered, and rebuildFilterButtons indexes both at the same i. Benign only because the width w is unused in the stable-preset branch; a future consumer reading effectiveFilterWidths()[i] against preset[i] gets a mismatch, plus wasted work on every capability update.

What was verified vs read

  • Verified by me in the PR head: the FM-dead-button path end to end (filterPresetRecallPlan's nullopt return under !widthCode, widthClassFor(FM)==Fixed, setSliceFilterPreset's !plan early return dropping the already-built select command); coarseStepHz existing as an ignored field; the radio-global control fed to every sliceSnapshot; the double m_radioFilterWidths write.
  • Read, not re-run: the passband math and the SSB/CW/AM recall correctness (the new filter_passband_math_test and icom_civ_test cover them and CI is green); the Twin-PBT decomposition reasoning.
  • Not run: no hardware, no bridge session — blocker 1 is a static control-flow gap reproducible from the code; the fix needs a fixed-width-mode recall test row (FM FIL2 → expect a select command, no 1A 03) that would have caught it.

Comment thread src/core/backends/icom/CivCodec.cpp
Comment thread src/core/backends/icom/IcomCivBackend.cpp
Comment thread src/core/AutomationServer.cpp
Keep fixed-mode FIL selection plans without writing programmable width or PBT. Preserve unrestricted width and legacy drag scaling/rounding when filter capabilities are empty, including after disconnect.

Add socket-free codec and production QWidget event coverage. Removing both fixes caused 32 codec checks and four widget checks to fail; restored fixes pass the focused tests.
@aethersdr-agent

Copy link
Copy Markdown
Contributor

Thanks for this — the filter-slot-as-identity model is the right shape, and the live IC-7300MK2 sweep behind it is a lot of work. The CI failure is a one-line bookkeeping miss, not anything wrong with your logic.

What failed

On b77b84e5, exactly one job is red: Static checks (run 33942796895), and within it exactly one step:

aetherd touchpoint manifest   ✗

Every other step in that job passed (engine/UI dependency direction, test registration, per-PR test gate, colour ratchet, a11y). check-macos, check-windows and build were still in progress when I looked, so this is not a compile error.

The step is:

# .github/workflows/static-checks.yml:161
- name: aetherd touchpoint manifest
  run: python tools/gen_touchpoint_manifest.py --check

Why

tools/gen_touchpoint_manifest.py scans src/gui/ + src/main.cpp for #include of engine headers and regenerates the burndown table in docs/architecture/aetherd-touchpoints.md. --check fails if the committed table differs from what it would generate. The table is generated — never hand-edited (the two sidecar JSONs carry the human-authored tags/status and survive regeneration).

Your PR adds #include "core/backends/RadioCapabilities.h" to three new UI headers:

  • src/gui/AppletPanel.h:4
  • src/gui/RxApplet.h:7
  • src/gui/VfoWidget.h:18

so that header's UI-includer count goes 2 → 5, and the committed row still says 2.

The fix

python tools/gen_touchpoint_manifest.py
git add docs/architecture/aetherd-touchpoints.md

I ran this against your exact head in a throwaway worktree; the whole diff is one line, and --check then exits 0:

-| `core/backends/RadioCapabilities.h` | 2 | universal — Backend-neutral capability descriptor … | unconverted |
+| `core/backends/RadioCapabilities.h` | 5 | universal — Backend-neutral capability descriptor … | unconverted |

No tag or status change is needed: RadioCapabilities.h is already tagged universal ("the UI reaching this header is the seam working as designed"), which is exactly what setRadioFilterControl() is doing — so this is a count bump, not a new coupling that needs justifying. Your PR body's "no new gui→core header include" line was the near miss: the header isn't new to the manifest, but the includers are, and the count is part of the generated table.

Worth noting the sibling static check, Engine/UI dependency direction, passed — so nothing about the direction of these includes is objectionable.

On the reviewer comments

The bot's blockers on CivCodec.cpp:536 / IcomCivBackend.cpp:4004 are unrelated to this CI failure — the manifest check never compiles or runs anything. But since you'll be pushing again anyway, the first one looks worth a real look: widthClassFor() returns Fixed for FM/NFM/DFM/WFM/DV/DSTAR, so filterWidthCodeFor gives nullopt, filterPresetRecallPlan returns std::nullopt, and setSliceFilterPreset sends nothing — including the slot-select command that was already pushed. FM publishes three distinct FIL widths, so that would leave all three FM buttons inert where the deleted setSliceFilter slot-pick branch used to work. Returning the select-only plan (with widthHz = 0, so passbandWidthIsCurrent() doesn't start claiming a width the radio never reported) keeps them live. Your checkRecall sweep at tests/icom_civ_test.cpp:504-515 covers USB/LSB/AM/CW only; a one-command FM case would pin it.

Not a maintainer ruling on the third comment (the unconditional 1A 03 / 14 07/14 08 writes overwriting radio-persisted state) — that one's a genuine design call for @rfoust.

Nothing here is CI infrastructure flakiness; regenerate the manifest and the Static checks job should go green.


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

@rfoust rfoust 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.

No linked issue; reviewed against the stated preset identity, mode-aware skirt mapping, and explicit recall behavior. The fixed-width FIL regression and the shared widget's empty-capability regression are repaired. Recall semantics follow the maintainer's ruling in review 5083885393. Approving the repaired head at the operator's explicit direction; I authored the follow-up repairs.

Scope Claimed purpose Verdict
Capabilities, backend seam, RadioModel, Icom codec/backend Stable FIL identity and explicit recall In scope
RxApplet, VfoWidget, AppletPanel, MainWindow wiring Shared radio-authoritative preset presentation In scope
FilterPassbandMath and production widget Mode-aware gestures with legacy fallback In scope; fallback regression repaired
Automation server and bridge/capability docs Preset selection and snapshot surface Disclosed and reviewed
Codec/math/widget tests and registration Regression coverage In scope; socket-free
Generated touchpoint manifest Capability include inventory Required generated refresh

Blockers: none remaining in the reviewed tree. All seven existing threads have a reply explaining the repair or non-blocking disposition.

Non-blocking limitations: final edge snapping can move an off-grid untouched CW edge; the 50 Hz fine gesture grid is still quantized more coarsely by the codec above 500 Hz. Radio-global snapshot metadata is valid for today's single-slice Icom backend but needs revisiting before multi-slice Icom support. Existing redundant width-list rebuilds are unchanged.

Verification: merged current main a4227e6 into the PR. Full macOS ARM64 application build passed with RADE enabled, verified ARM64 host/system/executable and no RNNoise x86 sources. icom_civ_test, filter_passband_widget_test, filter_passband_math_test and bridge_docs_check passed (4/4). Strict engine-boundary, test-registration, frozen-gate, generated-manifest and whitespace checks passed. Final head 2ab0cc4 adds only the generated manifest row after that code build; hosted CI is still running.

Attacks survived: all advertised fixed-mode slots using both command variants retain select-only plans with no width/PBT writes; invalid IDs are rejected. Production QWidget mouse-event tests preserve legacy 10 kHz shift/widening, vertical scale and minimum width, enforce advertised AM/USB maximums, and restore legacy behavior when capabilities clear. Removing the repairs while retaining the tests caused 32 codec failures and four widget failures; restoring the repairs returned the tests to green.

No new hardware sweep was performed. The isolated offscreen full app failed QRhi initialization before the bridge became available, so runtime evidence is the production widget event test, not a full-app demo claim. The review instances were stopped; the operator's running app was untouched. Merge only after required CI succeeds.

@rfoust
rfoust enabled auto-merge (squash) September 5, 2026 03:51
@rfoust
rfoust dismissed ten9876’s stale review September 5, 2026 03:51

The sole blocking finding in this review (fixed-width FM/WFM/DV FIL selection discarded by the recall planner) is repaired in b77b84e. The planner now retains a select-only command, keeps widthHz=0 and preserves display edges. Added socket-free coverage exercises every advertised fixed-mode slot through both command variants; reverting the repair causes 32 failing checks. All inline threads have verified repair or explicitly non-blocking dispositions. Reviewed and approved final head 2ab0cc4 at the operator's explicit request to fix, push, approve and merge. Dismissing this addressed request so ordinary required CI and approval gates can control auto-merge.

@rfoust
rfoust merged commit 50d70eb into aethersdr:main Sep 5, 2026
5 checks passed
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.

3 participants