fix(icom): IC-9700 support — a GUI crash, a dBm runaway, and a self-inflicted reconnect loop - #4786
Conversation
The IC-9700 row carried 475 points / 0..160 / 11 divisions copied from the IC-705, which the file itself flags as exactly the thing that cannot be assumed across models — the IC-7610 needs 689 and 0..200. Measured on a live IC-9700 (10.0.0.7) on 2026-08-05: 618 consecutive scope frames off the RS-BA1 CI-V data stream, every one 475 pixels wide. The frame's own bounds header cross-checks — centre 439.864060 MHz, matching where the radio was tuned, and a 500 kHz span, giving 1052.6 Hz per pixel. So the inherited numbers are RIGHT for this model. That is worth recording precisely because it could not be assumed, and the next person to wonder should not have to re-derive it. ⚠ `verified` deliberately stays FALSE. Per IcomModels.h:62-64 that flag means "confirmed against this model's own CI-V Reference Guide", and this is hardware measurement instead — a different kind of evidence. Flipping it would overstate what was done and weaken a flag whose whole value is that it means one specific thing. Comment only; no behaviour change. All nine icom_*_test suites pass.
|
Thanks for this one — the IC-9700 write-ups are excellent, especially catching that the mod-input warning was itself the disconnect. The CI failure is unrelated to any of that; it's a one-line style call. 👍 What failedOnly Static checks → Hardcoded-colour ratchet (run 31063733326). Every other step in that job passed, and Reproduced locally at The line
m_manualIcomCivEdit->setStyleSheet(lineEditStyle);The two edits it sits directly beneath route through ThemeManager instead: ConnectionPanel.cpp:709 ThemeManager::instance().applyStyleSheet(m_manualIcomUserEdit, lineEditStyle);
ConnectionPanel.cpp:720 ThemeManager::instance().applyStyleSheet(m_manualIcomPassEdit, lineEditStyle);The fixThemeManager::instance().applyStyleSheet(m_manualIcomCivEdit, lineEditStyle);That's the whole thing — same Worth knowing for next time: the ratchet is a delta vs. your PR's base, not a fixed ceiling (static-checks.yml:132-151), so it only ever complains about what your own diff adds. And the You can reproduce it locally without waiting on CI: No reviewer or Copilot comments on the PR yet, so nothing else outstanding. Once this is pushed the remaining three jobs should get a clean run. 🤖 aethersdr-agent · cost: $4.0173 · model: claude-opus-5 |
…ator set the CI-V address Three defects found bringing PR aethersdr#4784 up on a live IC-9700. Each is invisible from an IC-705, and each is silent — the radio connects and the operator is left guessing. 1. THE CI-V OPEN MUST BE RETRIED. The radio accepted the open, reported the pipe ready, and streamed NOTHING — not one frame in 45 s. kappanhang and SDR9700 both re-send the open every 100 ms until data flows (startCivDataTimer); AetherSDR sent it once. Aether-gate drives this same radio that way and gets 1356 frames in 45 s at 30.1 fps. Now retried at the reference cadence, capped at 50 attempts so a radio that will never answer SAYS SO instead of retrying in silence forever. Verified: "CI-V stream live — open-retry stopped" at 76 ms on a stream that previously carried nothing. 2. NO dBm RANGE WAS PUBLISHED, so the pan auto-ranged into a runaway. With no authoritative Y axis the display estimates from its own noise floor, and because MainWindow rejects anything below -180 dBm the radio never adopts the value — so the estimate is never corrected and drifts 24 dB/s forever. ⭐ Measured with the fix reverted as a control: 79 rejections, -202 falling to -2074 dBm, and the session tearing down and reconnecting EVERY 5.3 s. That last part is the important bit — the runaway does not just reset the waterfall, it kills the session. The stutter and the "unexpected disconnect" loop are consequences of it, not separate faults. With the fix: zero rejections. Publishes m_scopeCal's own numbers, which are ESTIMATES as its header says at length — but an uncalibrated stable axis beats a self-referential one, and it is the same estimate toDbm() already decodes with, so display and decoder agree. 3. THE CI-V ADDRESS WAS UNREACHABLE. IcomSettings::setCivAddress() had no caller anywhere in the tree, so the address could only ever be its 0xA4 default — the IC-705's. CI-V is addressed, so an IC-9700 on 0xA2 ignores everything, never answers 0x19 0x00, and falls back to the conservative unknown model: no scope, no transmit. That reads as "this backend has no panadapter yet" rather than "wrong address". Adds a CI-V field to the manual-connect form, shown by the same condition that already reveals the Icom user and password, because it is the third thing the operator reads off the radio's own menus. Blank means auto; an unparseable entry is ignored rather than clamped, since guessing would hide the typo behind the exact symptom the field exists to cure. ⚠ NOT fixed, and reported separately: a deterministic crash — three dumps, all identical, null read at 0x30 in Qt6Core, reached via SpectrumWidget::resetNoiseFloorBaseline <- PanadapterStream::setDbmRange <- PanadapterModel::setRange. Whether suppressing the runaway also removes it is untested; the control run above never reached a crash, so the two may or may not be the same defect. All nine icom_*_test suites pass.
…t loop on IC-9700
Four faults found connecting a real IC-9700 to the CI-V backend. Each is
independent, and three of them are not Icom-specific.
1. CRASH — null PanadapterStream (all backends)
RadioModel's panRangeChanged handler dereferenced m_panStream bare. That
pointer is assigned only on the Flex and Sim paths, so any backend that
decodes its own scope (Icom, HL2, Kiwi) left it null and the first dBm range
the backend reported killed the GUI thread. Four minidumps, all 0xC0000005
reading 0x30, all reaching PanadapterStream::setDbmRange from this lambda.
This is the same defect radiomodel_dax_null_test already fixed four times over
("still reachable by four paths behind it"). This is the fifth path.
2. dBm RUNAWAY — the auto-floor needs an echo that a fixed-scale radio never sends
The noise-floor auto-adjust slides the reference level, requests the new range,
and waits for the radio to confirm before moving again. An Icom has no command
plane for a display range, so the request is dropped, the confirmation never
arrives, the unchanged floor reads as "not there yet", and it steps again.
Measured at a linear 24 dB/s, span locked at 90 dB:
-154/-64 dropped -> -178/-88 dropped -> -202/-112 REJECTED
... 71 rejections later ... -1882/-1792
The "implausible dBm range" warnings are the SYMPTOM: the run was already 48 dB
adrift before the first one. Raising that floor would have hidden it.
New RadioCapabilities::radioOwnsDbmScale, defaulting TRUE so every existing
backend is untouched. Gating the outbound command paths alone is NOT enough --
m_refLevel is local widget state, so the traffic stops and the ratchet keeps
running (observed live). The gate has to be on applyNoiseFloorAutoAdjust itself.
Deliberately not setNoiseFloorEnable(false): that is the operator's own toggle,
and forcing it would fight the overlay menu and persist to the next radio.
3. RECONNECT LOOP — configuration advice raised on a fatal channel
The MOD Input check emitted connectionError, which every consumer treats as
fatal; RadioModel starts its reconnect timer on it unconditionally. So the
session came up healthy and the warning tore it down 4 ms later, forever:
21:19:09.353 CI-V stream live
21:19:09.357 connection error: "The radio is not listening to network audio..."
21:19:09.357 connection error - reconnecting in 5s
The operator sees a radio that will not stay connected and a message about a
menu setting, with no way to tell that the message IS the disconnect.
New IRadioBackend::configurationWarning for advice that does not end the
session. Surfaced in the status bar for 15 s -- it names a four-level menu path
the operator has to walk on the radio while reading it.
4. THE MOD INPUT WARNING WAS WRONG ON THIS RADIO
It demands MOD Input = WLAN (0x03). The IC-9700 is LAN-only: set correctly on
the front panel, it reports 0x01. kModels already carried the discriminator --
of the seven models, exactly ONE has Wi-Fi (the IC-705, whose 0xA4 is also the
default CI-V address, and so almost certainly the radio this was written
against). On every other networked Icom the warning asked for a setting the
radio cannot offer and no front-panel action could satisfy.
Now gated on hasWifi, and the health readout no longer appends "NOT WLAN" to a
correctly configured radio. Nothing is lost: the warning was WRONG here, not
merely noisy.
Underneath is a wider issue left alone deliberately: the 1A 05 item numbers
(118/119) and the value enum are hardcoded from one model's CI-V Reference
Guide and sent to every Icom, but each model numbers its own SET menu. On an
IC-9700 either 118/119 is not MOD Input, or 0x01 IS its network source. Both
need that model's guide to settle, so this does not guess at a TX-path setting.
TESTS
radiomodel_pan_range_null_test - reproduces the crash with no hardware.
Guard removed, it exits 0xC0000005.
dbm_range_plausibility_test - pins the ratchet arithmetic and the
defaults-true gate.
icom_family_test - extended with the capability and the hasWifi
discriminator. Capability inverted, it fails.
Each fix was falsified by breaking it deliberately, not by a green re-run.
VERIFIED ON HARDWARE (IC-9700, 2026-08-05)
Before: 71 implausible rejections, reconnect every 5 s, crash on connect.
After: 0 rejections, 0 connection errors, 0 reconnects, one CI-V bring-up,
stable session.
NOT verified: transmit. Whether TX audio actually reaches the modulator depends
on the 118/119 mapping above, which is unresolved on this model.
ac9a800 to
bdccdee
Compare
The force-push carrying the theme-ratchet fix (raw setStyleSheet -> ThemeManager::applyStyleSheet) did not trigger a workflow run. Empty commit to re-evaluate the PR; no code change.
There was a problem hiding this comment.
Thanks — this is a strong, well-evidenced PR, and the m_panStream null guard is a genuine live-on-main crash fix that I'd want in regardless of the Icom work. The radioOwnsDbmScale capability defaulting true is the right shape, and your note that gating only the outbound command paths is insufficient (because m_refLevel is local widget state) is correct — the gate in applyNoiseFloorAutoAdjust is where it has to be. Splitting configurationWarning off connectionError is also clearly right; a fatal channel carrying menu advice was always going to loop.
One real defect found in the new code, plus a few smaller things. Nothing here changes my view that the crash fix and the reconnect-loop fix should land.
Would like fixed before merge
- The new
panRangeChangedemit addsreferenceDbwheretoDbm()subtracts it, so the axis moves the wrong way when the scope reference level is non-zero — and the range is never re-published when it changes.
Polish
- The
hasWifigate also silences the MOD Input check on an unidentified radio, becausekUnknown.hasWifi == false. - An unparseable CI-V address is discarded with no feedback, and there's no obvious way back to "auto" once an override is stored.
- Orphaned comment left behind in
IcomSettings.cppwhen the constant moved to the header.
Non-blocking notes
dbm_range_plausibility_testasserts against its own copy ofdbmRangeLooksPlausible(), so it can't catch a change to the real constants.- CodeGuard's five CG-PATH-001 hits on
MainWindow.cpp(7977, 8089, 8208–8210) are false positives for this PR: your only change to that file is at ~6556, and those lines are untouched pre-existing code.
On the 1A 05 118/119 mapping you left open — agreed that guessing at a TX-path setting on someone else's model is the wrong call, and flagging transmit as unverified rather than letting it look tested is the right way to ship this.
🤖 aethersdr-agent · cost: $6.3636 · model: claude-opus-5
|
Cisco CodeGuard — static analysis of this PR (5 finding(s))
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: $7.0606 · model: claude-opus-5 |
|
Antares Detector — candidate vulnerable file(s)
HidEncoderManager reads HID reports into a fixed-size buffer using m_parser->reportSize() without validating available data, enabling a heap/buffer overflow and crash. Localized by Cisco Foundation AI Antares-1B running locally in the AetherClaude sandbox, seeded by the Cartographer security map. Advisory only — please verify before acting. 🤖 aethersdr-agent · cost: $7.0606 · model: claude-opus-5 |
…reference change
Review found a real defect in the new panRangeChanged emit.
THE SIGN. toDbm() maps a sample to `floorDbm + (v/max)*spanDb - referenceDb`, so
raising the radio's reference level moves the decoded trace DOWN. The emit ADDED
referenceDb where toDbm subtracts it, leaving the axis wrong by twice the
reference — invisible at the default 0, and a growing error the further the
operator moved it. The trace and the scale it is drawn against disagreed.
THE STALE AXIS. The range was published once at connect and never again, so a
reference-level change slid the trace while the scale stayed put.
Both now go through one publishScopeDbmRange(), derived from the same
ScopeCalibration toDbm() decodes with, called at connect AND on every
scope.reference change — so the two cannot drift apart again.
Also from review:
- The hasWifi gate silences the MOD Input check on an unidentified radio too.
That is correct, for a second reason worth recording: kUnknown is also
hasTransmit=false, and a radio this client will not let key has no modulation
path to warn about.
- An unparseable CI-V address now REFUSES THE CONNECT and says why, instead of
being silently discarded. This field exists because a wrong CI-V address fails
silently — the radio simply never answers — so ignoring bad input reproduced
the exact symptom the field is here to cure.
- Clearing the field now restores the default, so an override can be taken back.
Previously blank meant "auto" in the placeholder while the old override was
still in force.
- Removed the orphaned comment left in IcomSettings.cpp when kDefaultCivAddress
moved to the header (it is documented there).
TESTS
icom_scope_test pins the axis against toDbm's own output rather than against a
constant: the published floor must equal what toDbm maps display 0 to, at a
non-zero reference. That is the relationship the bug broke.
icom_family_test drives invokeExtension("scope.reference") on a real backend and
pins the quiet case — a radio with no scope publishes no range.
Recorded honestly: reaching a scope-capable model needs the 0x19 0x00 reply over
a live serial stream, so the emitted VALUE is exercised on hardware, not faked
with a mock. dbm_range_plausibility_test's header now states plainly that its
copy of dbmRangeLooksPlausible() cannot catch a change to the real constants.
|
Thanks — the sign defect is real and I've fixed it. Good catch; it was invisible at the default reference of 0, which is exactly why it survived a live session on the radio. The sign. Pinned in Polish, all taken:
On the duplicated predicate — you are right, and I have said so plainly in the test header rather than leaving it implied: that file cannot catch a change to the real constants. It pins the arithmetic and the shape of the runaway; the behavioural half is pinned in One correction to my own work while fixing this: I briefly added a null guard on Also worth flagging separately: |
|
Dispositioning the two automated scanner comments — both are out of scope for this PR, but they are not equally empty. Cisco CodeGuard (5 × CG-PATH-001, This PR touches So: not introduced here, not modified here, and not a defect on inspection. Antares ( The file is not in this PR at all (last touched by #3401 / #3542 / #4062). The finding is also imprecise about the mechanism — it says "without validating available data", but What is actually there, one step across: uint8_t m_buf[64]{}; // HidEncoderManager.h:187
int res = hid_read(m_device, m_buf, m_parser->reportSize()); // .cpp:591The length argument is Unrelated to the IC-9700 work, so I do not want to fold it into this PR's diff. Happy to open it separately — say the word and I will, or leave it if you would rather it ride with whoever next touches the HID path. Neither finding blocks this PR. |
|
Opened the HID clamp separately as #4819 so it stays out of this diff — |
Seven findings from the review on #4799. Two of them are mine one step short, and one contradicts a claim I made in the PR description. 1. THE STALE WATCH FIRED MID-TRANSMISSION (the blocker). The window is 2 s, and a 2 s gap in meter packets is reachable during a transmission on a lossy streaming link. As written, the watch would zero the forward-power gauge and invalidate SWR with the operator still keyed — trading a stuck reading at rest for a wrong reading under load, and only the second one is on the air. MeterModel now holds while transmitting, driven by TransmitModel::transmittingChanged (which covers VOX, break-in and a footswitch, not just an explicit MOX). The freeze this exists for happens at UNKEY, which is exactly where the guard stops applying, so nothing is lost. A backend that never reports transmit state gets the un-gated behaviour, which is the correct default for a polling backend. 2. THE TOKEN-RENEWAL LATCH NEVER RELEASED. Once retries were exhausted m_renewUnacked stayed true forever, so the cadence branch could never run again and the session coasted into a guaranteed teardown. A 10 s outage at t=20 s lost the four resends and then sat silent from t=30 s until fail() at t=50 s — a full disconnect for an outage that had been over for twenty seconds. The latch is now released after the cap, so a fresh renewal starts with a fresh budget: effectively continuous retry up to the dead-session deadline. 3. UNKNOWN WAS STILL BEING READ AS OFF IN THE SCRUB. fa129e6 fixed the read-after-clear, but -1 (never established) still collapsed to false, so the scrub SENT "off". On this backend's own lossy link the connect-time read is one datagram; lose it and an operator with NR, ANF or the notch running has it switched off by a check documented to leave the radio untouched — reported LINKED, because the intent did reach the wire. All four branches now return NOT-TESTED for unknown, which is the state the scrub already has for exactly this. 4. IC-705 FRONT-END LADDERS WERE PUBLISHED TO EVERY ICOM. The preamp and attenuator labels were IC-705 literals emitted unconditionally, so an IC-7610 (multi-step attenuator) or IC-9700 (different preamp ladder) got a control that misdescribed its own register — the defect class this PR's registry exists to surface, reintroduced by the fix for it, and #4786/#4791 are driving an IC-9700 through this backend now. Moved to preampLabelsFor() / attenStepsFor() in IcomModels, following powerCurveFor's rule exactly: no verified table means publish nothing, so the operator gets no button rather than a lying one. setPanAttenuator now takes the wire dB from that table instead of a hardcoded 20, and the decode maps dB back through it. 5. THE static_assert COULD NOT CATCH WHAT ITS COMMENT CLAIMED. It reads kAudioRateHz, a compile-time constant, while the rate that reaches the wire is the runtime StreamRequest::sampleRateHz. Setting that to 16000 still produced 1920-byte frames, still 60 ms per frame, and still the silent zero-power transmit — with the build green. There is now a runtime clamp where the request is actually built. 6. THE CREDENTIAL COMMIT WAS NOT SCOPED TO THE CONNECTED FAMILY. Stage an Icom password, have the attempt fail without driving a disconnected edge, then connect to a Flex, and the unproven password was written over a working keychain entry — the failure the staging was added to prevent, one step removed. The commit now requires the manual family to still be Icom, and every other connect path clears the staged pair, so it cannot outlive its own attempt. 7. THE INTENT PARAMETER DID CARRY A DEFAULT. The PR description said it was "deliberately NOT a defaulted extra argument". That was simply wrong — it read `= PanCenterIntent::Range`. The reviewer is also right about the hazard: a caller that omitted it silently got Range, which on the Icom means the drag is refused and re-asserted, the exact bug the parameter was added to fix. The default is gone, which is what the description claimed and what the one call site already did. Also documented why `controls scrub` is not TX-gated where `civ send` is — it drives named seam verbs rather than raw frames, and ptt/tuner/power are excluded from the walk — since the asymmetry otherwise reads as an oversight. The CodeGuard path-traversal findings are on MainWindow.cpp:8082-8315, outside every hunk in this PR (they stop at ~6749); the scanner scans changed files rather than diffs. Tests 264/265, the one failure being hl2_tx_loopback_test, which #4815 now explains: it targets a hardcoded 192.168.1.12 and its presence probe accepts any datagram without checking sender or payload. This LAN is 192.168.1.0/24 and something does answer at .12, so it runs a transmit test against whatever that is. Pre-existing and unrelated; not touched here.
|
Correction to my note above: I've closed #4819 (the clamp) and replaced it with #4820, a comment-only note at the The clamp was speculative hardening — no parser exceeds 64, so nothing overflows today, and the failure needs a future parser to return a larger value. What's actually missing is that the virtual's declaration never said it bounds a write into a fixed 64-byte buffer. #4820 says it, and records that TMate2 sits at exactly 64 with no headroom. Still nothing changing in this PR. |
jensenpat
left a comment
There was a problem hiding this comment.
Reviewed and verified. The null m_panStream guard is a real fix to a crash live on main that affects every self-decoding backend, not just Icom. radioOwnsDbmScale defaults true so no existing backend changes behavior, and gating applyNoiseFloorAutoAdjust itself rather than the outbound commands is the correct placement — you're right that gating the commands alone leaves the ratchet running on local widget state.
configurationWarning as a separate non-fatal channel is the right call; advice should never reach a consumer that treats it as a session teardown.
On the 1A 05 118/119 mapping: agreed, leave it. Per-model SET menu numbering means guessing at a TX-path setting, and silencing on hasWifi loses nothing that was correct. I'll pull the IC-9700 guide and wire it per model separately.
Transmit being explicitly marked unverified is appreciated — that's the right way to hand it over.
Noting the connection_panel_size_test failure on main separately; not yours.
Seven findings from the review on #4799. Two of them are mine one step short, and one contradicts a claim I made in the PR description. 1. THE STALE WATCH FIRED MID-TRANSMISSION (the blocker). The window is 2 s, and a 2 s gap in meter packets is reachable during a transmission on a lossy streaming link. As written, the watch would zero the forward-power gauge and invalidate SWR with the operator still keyed — trading a stuck reading at rest for a wrong reading under load, and only the second one is on the air. MeterModel now holds while transmitting, driven by TransmitModel::transmittingChanged (which covers VOX, break-in and a footswitch, not just an explicit MOX). The freeze this exists for happens at UNKEY, which is exactly where the guard stops applying, so nothing is lost. A backend that never reports transmit state gets the un-gated behaviour, which is the correct default for a polling backend. 2. THE TOKEN-RENEWAL LATCH NEVER RELEASED. Once retries were exhausted m_renewUnacked stayed true forever, so the cadence branch could never run again and the session coasted into a guaranteed teardown. A 10 s outage at t=20 s lost the four resends and then sat silent from t=30 s until fail() at t=50 s — a full disconnect for an outage that had been over for twenty seconds. The latch is now released after the cap, so a fresh renewal starts with a fresh budget: effectively continuous retry up to the dead-session deadline. 3. UNKNOWN WAS STILL BEING READ AS OFF IN THE SCRUB. fa129e6 fixed the read-after-clear, but -1 (never established) still collapsed to false, so the scrub SENT "off". On this backend's own lossy link the connect-time read is one datagram; lose it and an operator with NR, ANF or the notch running has it switched off by a check documented to leave the radio untouched — reported LINKED, because the intent did reach the wire. All four branches now return NOT-TESTED for unknown, which is the state the scrub already has for exactly this. 4. IC-705 FRONT-END LADDERS WERE PUBLISHED TO EVERY ICOM. The preamp and attenuator labels were IC-705 literals emitted unconditionally, so an IC-7610 (multi-step attenuator) or IC-9700 (different preamp ladder) got a control that misdescribed its own register — the defect class this PR's registry exists to surface, reintroduced by the fix for it, and #4786/#4791 are driving an IC-9700 through this backend now. Moved to preampLabelsFor() / attenStepsFor() in IcomModels, following powerCurveFor's rule exactly: no verified table means publish nothing, so the operator gets no button rather than a lying one. setPanAttenuator now takes the wire dB from that table instead of a hardcoded 20, and the decode maps dB back through it. 5. THE static_assert COULD NOT CATCH WHAT ITS COMMENT CLAIMED. It reads kAudioRateHz, a compile-time constant, while the rate that reaches the wire is the runtime StreamRequest::sampleRateHz. Setting that to 16000 still produced 1920-byte frames, still 60 ms per frame, and still the silent zero-power transmit — with the build green. There is now a runtime clamp where the request is actually built. 6. THE CREDENTIAL COMMIT WAS NOT SCOPED TO THE CONNECTED FAMILY. Stage an Icom password, have the attempt fail without driving a disconnected edge, then connect to a Flex, and the unproven password was written over a working keychain entry — the failure the staging was added to prevent, one step removed. The commit now requires the manual family to still be Icom, and every other connect path clears the staged pair, so it cannot outlive its own attempt. 7. THE INTENT PARAMETER DID CARRY A DEFAULT. The PR description said it was "deliberately NOT a defaulted extra argument". That was simply wrong — it read `= PanCenterIntent::Range`. The reviewer is also right about the hazard: a caller that omitted it silently got Range, which on the Icom means the drag is refused and re-asserted, the exact bug the parameter was added to fix. The default is gone, which is what the description claimed and what the one call site already did. Also documented why `controls scrub` is not TX-gated where `civ send` is — it drives named seam verbs rather than raw frames, and ptt/tuner/power are excluded from the walk — since the asymmetry otherwise reads as an oversight. The CodeGuard path-traversal findings are on MainWindow.cpp:8082-8315, outside every hunk in this PR (they stop at ~6749); the scanner scans changed files rather than diffs. Tests 264/265, the one failure being hl2_tx_loopback_test, which #4815 now explains: it targets a hardcoded 192.168.1.12 and its presence probe accepts any datagram without checking sender or payload. This LAN is 192.168.1.0/24 and something does answer at .12, so it runs a transmit test against whatever that is. Pre-existing and unrelated; not touched here.
Tested #4784 against a real IC-9700 the evening it merged. It connects, and RX works — but there were four faults in the way, one of which is a crash that is live on
mainright now and is not Icom-specific.@jensenpat — this is the PR you asked for. Everything here is verified on the radio; the one thing that is not verified is transmit, and I have said so explicitly at the bottom rather than let it look tested.
Three commits: two were written while #4784 was open (they were meant as review fixes and are now fixes to shipped code), and one is tonight's work.
1. Null
PanadapterStream— crashes the GUI thread on connectmainand affects HL2 and Kiwi too, not just Icom.RadioModel'spanRangeChangedhandler dereferencedm_panStreambare:m_panStreamis assigned only on the Flex and Sim paths. Any backend that decodes its own scope leaves it null, so the first dBm range the backend reports kills the GUI thread. Four minidumps, all0xC0000005reading0x30, all reachingPanadapterStream::setDbmRangefrom this lambda.This is the same defect
radiomodel_dax_null_testalready fixed four times — its own header says "the same crash already fixed once at thestartDax()entry, still reachable by four paths behind it." This is the fifth path, and unlike the DAX ones it needs no user action: it fires by itself on connect.radiomodel_pan_range_null_testreproduces it with no hardware. Guard removed, it exits0xC0000005.2. dBm auto-floor runaway — the loop waits for an echo that never comes
The noise-floor auto-adjust slides the reference level, requests the new range, and waits for the radio to confirm before moving again. An Icom has no command plane for a display range, so the request is dropped, the confirmation never arrives, the unchanged floor reads as "not there yet", and it steps again. Linear 24 dB/s, span locked at 90 dB:
The
implausible dBm rangewarnings are the symptom. The run was already 48 dB adrift before the first one, so raising that floor would have hidden it, not fixed it.Fix: new
RadioCapabilities::radioOwnsDbmScale, defaultingtrueso every existing backend is untouched.Worth a close look in review: gating the outbound command paths alone is not sufficient, and I had it wrong that way first.
m_refLevelis local widget state, so the traffic stops while the ratchet keeps running — thesendCommandlines vanished from the log and the rejections carried on. The gate has to be onapplyNoiseFloorAutoAdjustitself.Deliberately not
setNoiseFloorEnable(false)— that is the operator's own toggle, and forcing it would fight the overlay menu and persist to the next radio.3. Configuration advice on a fatal channel — a self-inflicted reconnect loop
The MOD Input check emitted
connectionError, which every consumer treats as fatal (RadioModelstarts its reconnect timer on it unconditionally). The session came up healthy and the warning tore it down 4 ms later:Then again every 5 s, forever. From the operator's side that is a radio that will not stay connected, plus a message about a menu setting, with no way to tell that the message is the disconnect.
Added
IRadioBackend::configurationWarningfor advice that does not end the session. Status bar, 15 s rather than the usual 4 — it names a four-level menu path you have to walk on the radio's front panel while reading it.The original intent is kept: this genuinely does cost the operator every transmission and should be visible. Just not on a channel that is fatal.
4. The MOD Input warning is wrong on every non-705 networked Icom
It demands
MOD Input = WLAN(0x03). The IC-9700 is LAN-only — there is no WLAN option in its menu. Set correctly on the front panel, it reports0x01, and the warning fired on every session.kModelsalready carried the discriminator:0xA4)Exactly one model has Wi-Fi — the IC-705, whose
0xA4is also the default CI-V address, so almost certainly the radio this was written and tested against. On every other networked Icom it asked for a modulation source the radio does not have, and no front-panel action could satisfy it.Now gated on
hasWifi, and the health readout no longer appends— NOT WLANto a correctly configured radio. Nothing is lost: the warning was wrong here, not merely noisy.Left alone deliberately — needs your call. The
1A 05item numbers (118/119) and the value enum are hardcoded from one model's CI-V Reference Guide and sent to every Icom, but each model numbers its own SET menu. So on an IC-9700 either 118/119 is not MOD Input at all, or0x01is its network source. Both need that model's guide to settle, and I was not willing to guess at a TX-path setting. If you know the 9700 mapping I will wire it up per model.Also included (written during #4784, now fixes to merged code)
buildSerialOpenwas not reliably enough: the radio accepted the open and sent nothing. Retries at 100 ms up to 50 attempts, stops on the first real payload, and warns with a specific cause if it never starts. Took the CI-V stream from dead (0 frames in 45 s) to live at 76 ms.A2,0xA2,A2h; unparseable input is ignored rather than clamped.verified: false— measured on hardware is not the same as confirmed against that model's own CI-V guide.Verified on hardware — IC-9700, 2026-08-05
Each fix was falsified by breaking it deliberately and watching the test catch it, rather than by a green re-run. I re-ran the crash falsification after rebasing onto
main, because a rebase can silently drop a hunk and a green test alone would not tell the difference.Tests
radiomodel_pan_range_null_test— reproduces the crash with no hardwaredbm_range_plausibility_test— pins the ratchet arithmetic and the defaults-truegateicom_family_test— extended with the capability and thehasWifidiscriminatorNot verified
Transmit. Whether TX audio actually reaches the modulator depends on the 118/119 mapping above, which is unresolved on this model. RX, scope and stability are solid; I have not keyed the radio.
Unrelated but visible in the logs:
TgxlConnection/PgxlConnectionretry a refused socket every ~9 s on a non-Flex radio. Harmless, pre-existing, untouched here — happy to look at it separately if you want.