Repository navigation
fix(icom): squelch on at threshold 0 reads back as on, so it can't stick off. Principle II. - #6175
Conversation
…ick off. Principle II. Fixes aethersdr#6172. Icom has no squelch enable; the 14 03 threshold is the control, and the backend derived squelchOn = level > 0 from every readback. The confirmation read of our own "on, level 0" write therefore published Off, and with SliceModel confirming from the radio, every later setSquelch(receiveSquelchOn(), level) — TCI sql_level / sql_enable, the SQL shortcut, controller bindings — was sent as Off and wrote 0000 again. Once the remembered manual level was 0, SQL -> Manual wrote 0000 and its readback returned the slice to Off: squelch could not be turned on from AetherSDR. Shared by the IC-7300MK2, IC-705 and IC-9700. - The backend remembers that its last 14 03 write was on at 0, and that write's 0 readback (and the MK2's periodic 0 poll) stays on. - A radio-side non-zero value clears it, so a later return to 0 reads as off; an explicit Off write reads as off; disconnect forgets it. - The controls scrub re-asserts on-at-0 as on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Issue fit
Yes. #6172's mechanism is exactly the asymmetry between the write (on ? percentToLevelRaw(level) : 0) and the decode (squelchOn = pct > 0), and this PR closes it at the only seam where it can be closed — 14 03 00 00 is the same frame for "off" and "on at threshold 0", so the enable intent has to be carried somewhere. The issue's four acceptance cases all hold in the code as written:
| event | code path | result |
|---|---|---|
echo of our own (on, 0) write |
IcomCivBackend.cpp:2136-2139, flag set at :4644 |
on ✅ |
MK2 periodic 14 03 poll of 0 |
same, flag untouched by pct == 0 |
on ✅ |
| front-panel non-zero, then back to 0 | :2136 clears on the non-zero |
off ✅ |
| explicit Off write | :4644 on && raw == 0 is false |
off ✅ |
It also picks up the two things the triage comment on #6172 flagged beyond the report: scrubDrive (:5650) no longer re-infers on/off from m_squelchPercent > 0, and the manual-memory-at-0 state is left as an honest draggable state rather than clamped away.
The implementation differs from the triage's proposed sketch (clear-on-non-zero-readback instead of comparing the previous m_squelchPercent inside the zero branch). I walked both: they agree on all four cases above and on the mixed sequences I could construct, and the flag version has one fewer ordering dependency on when m_squelchPercent is overwritten. No objection.
Scope
| File | What it changes | Claimed by the body? | Verdict |
|---|---|---|---|
src/core/backends/icom/IcomCivBackend.cpp |
flag set on write (:4644), consumed in the level::kSquelch decode (:2136-2139), used by scrubDrive (:5650), reset in onSessionDisconnected (:1572) |
yes, all four bullets | in scope |
src/core/backends/icom/IcomCivBackend.h |
one bool member + why-comment (:521-523) |
yes | in scope |
tests/icom_control_profile_test.cpp |
deliverSquelch helper + six assertions × 3 models |
yes | in scope |
Everything in the diff is explained by the issue. Specifically checked and clean:
- No
CHANGELOG.mdentry — correct. - The PR body carries an SSB S-meter calibration table, and no code touches it:
squelchLevelScale = legacyDbmSquelchScale()atIcomCivBackend.cpp:513is untouched, so the measurement really is deferred as the body says. - No new public/protocol/settings/capability surface.
SliceDelta::squelchOnalready existed;m_squelchOnAtZerois a private member. - No
-lines beyond the two comment/expression lines being replaced; no guard or early return removed. - Sibling backends (
Hl2Backend,RtlSdrBackend) have real squelch enables and need nothing — this defect is Icom-specific. - Socket tests: none added, modified or removed.
icom_control_profile_testis socket-free (directonCivFrame/seam calls) and already registered attests/tests.cmake:1134-1137, including in the list at:7611.
CI on 1a9aa771: 5/5 green (build, check-macos, check-windows, Static checks, Sanitizer option configures).
Blockers
None.
Nits (all non-blocking)
src/core/backends/icom/IcomCivBackend.cpp:1572— "disconnect forgets the flag" is only true for a radio-side drop.onSessionDisconnected()is reached only from the session'sdisconnected()signal (connected at:881), andIcomSession::stop()(IcomSession.cpp:125-170) setsm_connected = falsewithout emitting it — onlyfail()(:177-188) does. So an operator-initiateddisconnectRadio()(:940-948,m_session->stop(); m_session.reset();) carriesm_squelchOnAtZerointo the next session, where the connect-time14 03read of a genuine 0 publishessquelchOn = true. Consequence is cosmetic rather than a trap — threshold 0 is wide open either way and the control still writes — and the sibling mirrors reset in the same block (m_micGainReported,m_pcAudioEnabled) leak identically, so this is pre-existing shape, not a regression. Either narrow the body's claim or add it todisconnectRadio's own reset block:m_controlsValueKnown.clear(); m_squelchOnAtZero = false;src/core/backends/icom/IcomControls.cpp:172-173— theControlSpecnote still reads"NO separate enable exists on this radio — the threshold IS the control, and squelch is off at zero."That is now only true absent an on-at-0 write, and this string is surfaced byciv controls. The triage comment on #6172 asked for the reword in the same change. Same drift in the retained comment atIcomCivBackend.cpp:4638-4642("squelch is 'off' when it sits at zero") and indocs/architecture/aetherd-icom-civ-backend-design.md:1292("No enable exists: the threshold IS the control and off is zero").tests/icom_control_profile_test.cpp:433-463— three cheap cases missing, all socket-free and constructible with the existing fixture: (a) the reset — on-at-0 write,onSessionDisconnected, re-prepare, deliver 0 → must read off (this is the one that would have caught the nit above, at least for the signal path); (b) thescrubDrivebranch at:5650has no coverage; (c) nothing asserts the outbound frame, so the issue's headline symptom ("every later level written as 0") is not pinned —IcomCivBackendTestAccess::lastOutboundCivalready exists andprepareSessiongives a livem_session, sosetSliceSquelch(0, true, 20)could assert14 03 00 51directly.- The flag is not cleared if the radio answers the
14 03write with NG/FA. Only frequency writes handle NG (:1811-1815), so this introduces no new gap — noting it only because the flag is optimistic by construction.
What I tried to break
- Stale
14 03readback clearing the flag. The attack: an MK2 periodic poll (:6715-6716) is in flight or queued when the operator writes on-at-0, its old non-zero value arrives after the flag is set, clears it, and then the write's own confirmation read of 0 publishes Off — the original trap, intermittently. It does not survive the scheduler:sendUserCommand→queueWrite(..., supersedes=true)bumps theciv.20.3generation (IcomCivScheduler.cpp:74-77),observe()returnsStale, andonCivFramereturns at:1804before the decode. A queued-not-yet-dispatched older read is erased by the confirmation read's own enqueue (IcomCivScheduler.cpp:111-113,it->generation < currentGeneration). The comment at:3886-3888claims exactly this and it checks out. - An off-by-one making a non-zero level look like on-at-0.
percentToLevelRawis a ceiling (CivCodec.cpp:294-298), so pct 1 → raw 3;on && raw == 0is exactlyon && level <= 0. No percentage can accidentally arm the flag. - An unguarded caller setting the flag with nothing on the wire.
sendUserCommandreturns early on!m_session || !m_connected(:3871) while the flag is set before it. Grepped the whole tree: outside the backend,setSliceSquelchexists only as theIRadioBackend.h:673virtual and the HL2/RTL overrides — the only Icom routes in arerequestSliceSquelch(:4354-4360, guarded onm_connected && m_session && m_model) andscrubDrive, which bails at:5638becausem_controlsValueKnownis cleared on teardown (:982). The disconnected-write attack only lands through the already-torn-down path in the nit above. - Downstream consumers newly seeing a state that was previously unreachable.
squelchOn == true && level == 0now happens, which flipsSmartCatProtocol.cpp:1397(BY→ busy) and feedsRigctlProtocol.cpp:2151(DCD). Both already encode "squelch at 0 means always open" deliberately in their own comments, so this is the semantics they were written for, not a regression. - The test proving nothing. The two on-at-0 assertions (
:449-453) readon == truewhere the pre-fix decode emitsfalse, and the optional-based capture fails on a missing delta rather than passing vacuously — so they do fail against the unfixed backend, as the body claims. The three negatives (radio-side 0 after non-zero, connect-time 0, explicit Off) each exercise a distinct branch. - Coverage boundary I could not close and do not think should be: on the IC-705 and IC-9700 nothing polls
14 03, so a front-panel 0 → N → 0 between our writes is invisible and the UI stays "on at 0". That is the pre-existing no-poll limitation, not something this PR worsens, and chasing it would mean polling those profiles. The live proof in the body (the TCI sweep writing0000…0255, RX audio gating from0153up) is the right evidence layer for that half and is not reproducible headlessly.
On Principle II: the enable bit here is synthetic, but the PR keeps the threshold radio-authoritative — every readback still adopts pct into m_squelchPercent and d.squelchLevel — and carries client intent across only the one frame the wire genuinely cannot disambiguate. I read that as within II rather than against it.
All of the above is reasoned from the head checkout; I have no build or runtime here, so nothing is reproduced empirically.
Recommendation
Approve with nits. The fix is at the right seam, the case table is complete, the scheduler already defends the stale-readback race I expected to find, and the scope is exactly three files with the calibration work correctly deferred. The disconnect-path nit and the stale "off at zero" notes are worth a follow-up commit on this branch but none of them block — and the maintainer may want this reviewed alongside #6171, which the triage identified as the same defect class on COMP/PROC. Nice report-to-fix pair, @jensenpat; the pre-fix-failure claim for the new assertions is the part that made this quick to check.
🤖 aethersdr-agent · cost: $6.3164 · model: claude-opus-5
…h the new rule. Principle II. Review follow-up on aethersdr#6172. - disconnectRadio() resets m_squelchOnAtZero. IcomSession::stop() emits no disconnected(), so onSessionDisconnected's reset only ran for a radio-side drop; after an operator disconnect the next session's connect-time 0 read as "on". - The squelch ControlSpec note (surfaced by `civ controls`), the setSliceSquelch comment and the backend-design row no longer say "off is zero": a zero readback is off unless it echoes our own on-at-0 write. - Tests, per model: scrubbing an on-at-0 squelch keeps it on, and after an operator disconnect a connect-time 0 reads as off. Each fails with its fix removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressed in afda8e9:
Not changed:
31 Icom/squelch/P-CW ctest targets pass headless. 🤖 Addressed by Claude Code |
There was a problem hiding this comment.
The fix addresses #6172 at the shared Icom write/readback seam: an explicit on-at-zero remains on through confirmation and polling, while a subsequent nonzero radio value, explicit Off, and disconnect clear the remembered intent. No blocker found at head afda8e96a06a3bb246b99622b16e0623fbe3f00b.
| File/group | Change claimed in body | Scope verdict |
|---|---|---|
IcomCivBackend.cpp/.h |
Remember on-at-zero; clear on radio changes/disconnect | Direct fix |
IcomControls.cpp |
Declare the matching squelch semantics | Direct fix |
icom_control_profile_test.cpp |
Inject confirmation/poll/reset cases for all three models | Regression coverage |
| Backend design document | Explain the zero-threshold behavior | In scope |
Everything in the diff is explained by the issue; neither SQL axis calibration nor Auto SQL calibration is bundled.
Blockers: None.
Non-blocking coverage boundary: src/core/backends/icom/IcomCivBackend.cpp:4639 implements a shared protocol path, but the author's live IC-7300MK2 evidence was not independently repeated and IC-705/IC-9700 firmware convergence remains unverified. Demo cannot exercise an Icom threshold confirmation; I used production-handler injection rather than a synthetic firmware peer.
What I tried to break: Built and ran icom_control_profile_test on the exact head (1/1 passed). Replacing the production decode with its pre-fix squelchOn = pct > 0 behavior made nine assertions fail: confirmation, periodic poll, and controls scrub at zero across the three profiles. Restored the production source and rebuilt successfully. Inspected the explicit-Off, nonzero-to-zero and both disconnect/reset paths; the previous review's disconnect concern is fixed in this head.
The head/base engine-boundary, command-plane, capability, test-registration, frozen CI gate, settings and timeout audits passed without per-file growth; touchpoint manifests are current. No new socket-owning test was found. Required hosted checks passed, but their run was created before current main, so they do not establish integration with today's main. The separate code-review skill was unavailable; existing bot leads were checked manually.
Recommendation: Approve. This is a COMMENT review; approval remains the maintainer's decision.
Final CI recheck: required checks still pass and the reviewed head is unchanged. Current main is 14ef3e78841009d73ddf2e7de22b9a5a83d5ee0a (2026-10-08 08:23:34 UTC), newer than this head's earliest pull-request run creation. With non-strict branch protection, the green checks do not prove current-main integration.
…nual level, and a TCI enable+level burst keeps both halves. Principle II. - The kSquelch decode judges on/off and clears the on-at-0 flag on the raw threshold: raw 1..2 round to 0 % but the radio is gating. - An Off readback publishes squelchOn=false only. Publishing level 0 made SliceModel adopt 0 as the remembered manual threshold, so the next SQL -> Manual came back on at a threshold that gates nothing. - setSliceSquelch records nothing outside a connected session, where sendUserCommand would drop the write; a stale on-at-0 flag would read the next real 0 as on. - The controls scrub re-asserts the published on/off (m_squelchOn) rather than re-deriving it. - TciProtocol holds each half of a sql_enable/sql_level pair until its queued setSquelch runs, so a burst carrying both no longer reads the slice's old value for the other half and undoes its partner. Tests: icom_control_profile_test (raw 1/2 boundary, disconnected write, Off keeps level 40) and reroute_dead_controls_test (TCI burst both orders, lone command after the burst). Each goes red with its fix reverted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Landing pass —
|
| # | Finding | Disposition |
|---|---|---|
| 1 | TCI sql_enable:true;sql_level:20; burst: cmdSqlLevel read receiveSquelchOn() before the queued enable ran, so it wrote Off |
Fixed (maintainer call to fold here). TciProtocol holds each half of the pair until its queued setSquelch runs (m_pendingSquelch, queueSquelch). Proved by reroute_dead_controls_test → TCI: sql_enable true then sql_level 20 in one burst requests on at 20, the reverse order (level 35 then enable false keeps level 35), and a lone sql_level after the burst uses the slice's own enable. With the pending lookup reverted, the first three go red. |
| 2 | On/off and the flag clear used the rounded %, so raw 1..2 read Off | Fixed. The kSquelch decode uses *raw > 0. Proved by a radio-side 0 after a raw 1 threshold reads as off and scrubbing a raw 2 squelch re-asserts it as on, ×3 models. Red with pct restored. |
| 3 | An Off readback published level 0, which replaced the remembered manual level, so the next SQL → Manual came back on at 0 | Fixed (maintainer call to fold here). A 0 that reads as Off publishes squelchOn=false only. Proved by an Off readback publishes no level, so the remembered 40 survives and a connect-time 0 … publishes no level, ×3. Red with the unconditional squelchLevel restored. |
| 4 | The flag was set even when sendUserCommand dropped the write (session present, not connected) |
Fixed. setSliceSquelch returns before recording anything outside a connected session. Proved by an on-at-0 write dropped while disconnected leaves a 0 readback off, ×3. Red with the guard removed. |
| 5 | IC-705 / IC-9700 don't poll 14 03, so the flag never clears |
Refuted. On those models every 14 03 read is either the connect-time read, after the flag is reset at both disconnect sites, or the confirmation of our own write. Each write re-derives the flag in setSliceSquelch, so no read can observe a stale flag. |
| 6 | Altitude: model "no squelch enable" as a capability | Declined. The shape of the fix was accepted when the PR was approved for landing. This pass doesn't re-open it. |
| 7 | The scrub duplicated the on/off rule | Fixed. The scrub re-asserts the published m_squelchOn instead of re-deriving it. |
| 8 | No raw 1..2 or burst coverage | Fixed by the tests in 1, 2 and 4. |
Gates on head: check_engine_boundary --strict, gen_touchpoint_manifest --check, check_capability_records --strict, check_command_plane --strict, check_test_registration and check_qsettings are all rc=0. The only finding in a touched file is the pre-existing, baselined EB3 DaxIqModel.h include. Locally, a full build plus ctest -R "icom|tci|squelch|sql|reroute" gives 36/36 passing.
ten9876
left a comment
There was a problem hiding this comment.
Approved at 61036a9. Every review finding is resolved with proof (dispositions: #6175 (comment)). Each new test goes red with its fix reverted; the related ctest set passes locally 36/36; required CI is green on this head; the static gates are rc=0.
…librate SQL (#6205) Four IC-7300MK2 defects reached the operator in v26.10.1, and each had a backend test that agreed with the backend: | PR | Defect | Class | |---|---|---| | #6174 | PROC NOR/DX snapped to DX+ on their own confirmation read | a scaled control decoded in a different domain from the one it was written in | | #6175 | squelch "on at threshold 0" read back as Off and stuck | a boundary value whose enable is derived from the level | | #6183 | ATT on dropped the preamp, but the button changed only at the 3 s poll | a control the radio changes on its own, unreported | | #6184 | SQL line drawn on Flex's `−160 + level`, 7–10 dB from the MK2's gate | a published scale nothing in the client can contradict | This PR adds checks for those classes to `radiocert meters` and `radiocert persist`. They're family-neutral and gated on published capabilities, so the next Icom gets them without any new code. ## `radiocert meters`: three non-keying stages, before the keyed ones | Stage | Method | Concern when | |---|---|---| | `control-domain` | Writes every published speech-processor level (PROC enabled for the sweep) and squelch level 0…100 through the operator's setters, and samples the model until the radio answers. Ranges of ten or fewer get every step; wider ranges get both ends, their neighbours and the quarters. The ends are held 6.5 s (two MK2 polls). The squelch sample carries the enable. | a value is never read back as written, or is read back and then moves | | `front-end-interlock` | Five preamp/ATT transitions from both-off; times when the stage *not* written settles. Restores the front end. | the radio moved the other stage and the pan showed it after 500 ms | | `squelch-scale` | Parks the dial 1.5 kHz below a steady carrier (`sql=<MHz>`), binary-searches the lowest level at which the operator's audio drops 20 dB, confirms it at ±1 level, and compares the published line there with the carrier's pan peak. Where Auto SQL is published, it repeats the search on band noise. | the line is more than 6 dB from the carrier, or Auto's widest margin is below the noise gate | - `squelch-scale` declines (INCONCLUSIVE) above S9, where a wrong scale passes too (measured below). It also declines when the carrier fades mid-search or there's no audio with squelch open. - With bridge TX permission off, every key in the run is refused (`keyRefusals`), and these three stages still run. - New bridge argument `radiocert <phase> [freqMhz] [sql=<MHz>]`. The persist snapshot now publishes `transmit.speechProcLevelMaximum`. - Pure helpers (`domainProbeValues`, `readbackOf`, `settledAtMs`, `lowestClosedLevel`, `panPeakNear`, `median`) live in `RadioCertificationMath.h`. `radio_certification_math_test` checks each one against the four bugs' own live numbers. For example, the pre-fix percent decode moves NOR and DX but not DX+, and a level-only squelch comparison certifies the on-at-0 defect. ## `tools/radiocert_persist_controls.py`: the same classes across a restart This is a new RX-only runner, separate like the multislice one. The existing persist runners refuse non-Flex radios by design; this one accepts Flex, HL2 and Icom. 1. **Range:** seeds the processor level at an interior value of the published maximum (0..10 → 3/6, the MK2 bins; 0..2 → 1/2) through the real slider. It must hold through two polls, and come back from the connect-time read after a Quit/relaunch. 2. **Boundary:** manual SQL at 0 must keep `squelch: true` and the applet's Manual through two polls. 3. **Restart:** a distinctive manual SQL level must come back from the radio. - **An Icom connects by `--icom-host`. The runner never handles credentials:** the operator signs in at each launch. - **Every mutation is pinned to family, model and session serial,** including an Icom's endpoint-derived `icom:<host>` serial. A radio with no serial is refused, so a reconnect to another radio of the same model is never seeded or restored. - If Manual can't be re-entered after the boundary contract (the #6175 trap itself), that's recorded as a concern, not a stop. - 13 socket-free policy cases cover the runner, including the on-at-0 and COMP-snap fixtures and the same-model/different-serial and missing-serial cases. ## Live proof (IC-7300MK2, RX only, isolated TX-off clients, 2026-10-05) I built three binaries from this branch: main as-is (#6183/#6184 merged, #6174/#6175 open), main with #6183/#6184 reverted, and main with #6174/#6175 merged. Every meters run had `keyRefusals: 3`, so nothing transmitted. | Check | Main | #6183/#6184 reverted | #6174/#6175 merged | |---|---|---|---| | `control-domain` PROC | **NOR and DX read back, then moved to DX+ 200 ms later** | same | all 11 COMP steps held | | `control-domain` squelch | **on-at-0 never read back on** | same | 0…100 held, on-at-0 included | | `front-end-interlock` | 4 coupled changes shown in 100–200 ms | **3 of 4 only at the poll: 2700 / 1300 / 1800 ms** (one early at 300 ms) | 4 in 100–200 ms | | `squelch-scale` 1120 kHz behind 20 dB ATT (−108…−112 dBm) | gate 45, line −3.8 dB from carrier | **gate 41 confirmed, line +7.5 dB** | carrier faded to S0: declined | | persist range | **DX written; model and slider at DX+ from sample 1** | — | COMP 3 held, and came back after restart | | persist boundary (manual SQL 0) | **`squelch: false`, applet Off, all 14 samples** | — | held on | | persist restart (SQL 26, COMP 3) | — | — | ESTABLISHED; cleanup restored COMP 9 / SQL Off | Two other `squelch-scale` results shaped the stage: - **1480 kHz with P.AMP1 (−64 dBm, above S9):** gate 70, and *both* scales landed within 6 dB. That's why it now declines above S9. - **1210 kHz behind the ATT (−78 dBm):** gate 63 confirmed, and even the measured MK2 record sits **7.5 dB** above the carrier (Flex's would be 11.5). This is the limit #6184 documents for a near-S9 carrier with ATT in or the preamp off, and the stage reports it as a concern. On the confirmation rule: night-to-dawn medium-wave carriers faded by up to 25 dB within one search. Requiring an exact repeat at the gate level declined steady carriers too, because at the gate the carrier sits on the threshold and chatters. So the confirmation is a one-level bracket. The radio was left as found: 1.340 MHz AM, SQL manual 12, PROC on at COMP 9, P.AMP1 / ATT off. ## Tests (headless, `QT_QPA_PLATFORM=offscreen`) `radio_certification_math_test`, `radiocert_persist_policy` (65 cases), `bridge_docs_check`, `automation_tx_watchdog_test`, `automation_persist_diagnostics_test`, `automation_boundary_core_test`, `icom_control_profile_test`, `squelch_level_scale_test`: all pass. The `check_*` static scripts and `gen_bridge_docs.py --check` pass. The full suite is left to CI. The front-end interlock stage never writes through, or reads from, a radio or pan that went away during an event-loop wait. `automation_tx_watchdog_test`'s `interlockSurvivesRadioTeardown` destroys the radio mid-stage and checks for an INCONCLUSIVE result; without the guards it segfaults. ## Docs - `CERTIFICATION.md` §1.41–1.43 (one lesson per class) and a new-radio checklist step. - `radio-certification.md`: Controls rows and the measured table above. - `automation-bridge.md`: the stages, `sql=`, and the controls runner. ## Not in this PR - No screenshots: there's no UI change; the evidence is the report JSON above. - The persist boundary contract isn't asserted across a restart. A radio with no squelch enable reads threshold 0 as Off after reconnect, which is #6175's documented behaviour. - Bridge verbs that would make this cheaper, left for follow-ups: - a `pan frontend preamp|att <step>` verb, plus `preampStep`/`attenuatorStep` in `get pan`. The proof had to open the ANT panel and click its buttons to choose a carrier's front end. - a backend-neutral squelch-gate readback (Icom `15 01`), so `squelch-scale` could use the radio's own gate as a second instrument beside the audio. 🤖 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>
Fixes #6172.
What was wrong
On Icom, AetherSDR's squelch could get stuck off.
There is no squelch enable on these radios: the
14 03threshold is the control.IcomCivBackendtherefore derivedsquelchOn = level > 0from every readback, and the confirmation read of our own "on, level 0" write published Off.SliceModelconfirms receive controls from the radio, so from then on:setSquelch(receiveSquelchOn(), level)was sent as Off and wrote0000again. That covers TCIsql_level/sql_enable, the SQL shortcut and controller bindings;0000and its readback returned the slice to Off. Squelch could not be turned on from AetherSDR again.This affects the IC-7300MK2, IC-705 and IC-9700, which share the write, the decode and the confirmation read. The MK2 also re-reads
14 03every few seconds, so it re-asserted Off even without a write.Live on v26.10.1, Manual at 75 when a TCI client connected:
The fix
IcomCivBackendremembers that its last14 03write was on at threshold 0 (m_squelchOnAtZero).Proof (live IC-7300MK2, proof build)
1. The trapped state is no longer a trap. Starting from SQL Off with the remembered manual level at 0, clicking SQL → Manual wrote
0000. The radio echoed0000and squelch stayed on through three periodic polls. Before this fix it bounced back to Off.2. The TCI sweep that used to stick at 0 (
sql_enable:true, thensql_level0 → 100) now writes every level as itself:0000, 0051, 0102, 0153, 0204, 0255. Nosql_enable:falseis broadcast back. RX audio over TCI shows the radio honouring each write: open at -17 dBFS through 0102, digital silence (-200 dBFS) from 0153 up.Tests (headless)
icom_control_profile_testruns a new case per model (IC-7300MK2, IC-705, IC-9700):Run against the pre-fix backend, the two on-at-0 checks fail on all three models. All 27 Icom and squelch-related ctest targets pass.
Not in this PR: the SQL line / Auto SQL scale on Icom
Icom still publishes
legacyDbmSquelchScale()(Flex's−160 + leveldBm, #6127). I measured the MK2's SSB S-meter squelch through this build:14 03stepped while the TCI RX audio was watched for the gate, on 14.2968 USB band noise.14 03raw)That fits roughly
gate_raw ≈ 0.82 · smeter_raw + 75, or about−168 + 1.4 · leveldBm below S9. The current line is therefore around 15–20 dB below where the radio gates.A correct line also needs the Icom pan's nominal dBm axis reconciled with the S-meter: the pan floor read −123 while the S-meter read −87 on the same noise. That is a calibration change for its own PR.
🤖 Generated with Claude Code