fix(icom): Identify radios over CI-V and add optional wake on connect - #5438
fix(icom): Identify radios over CI-V and add optional wake on connect#5438jensenpat wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
1. Issue fit
Yes, on the core defect. #5164's mechanism — unknownModel() at connect → connectionStateChanged evaluates takesTxAudioOverSeam && canTransmit once → late capabilitiesChanged never re-runs the TX-audio lifecycle → feedDaxTxAudioInternal drops every TCI block — is closed by rebinding to RadioModel::capabilitiesChanged (MainWindow_Session.cpp:753), which RadioModel::publishCapabilities (src/models/RadioModel.cpp:4112) emits on the connect edge, the disconnect edge, and every backend capability revision. The idempotent helper is exactly the "Suggested implementation direction" the issue asked for, and removing modelForName() from the connect path is the issue's "do not infer hardware identity from the nickname" taken literally.
Acceptance criteria mapping: custom name can't alter TX capability ✅ (onSessionConnected now unconditionally &unknownModel()); late false→true re-evaluates without restart ✅; regression covering custom name → unknown → CI-V reply → TCI PCM → submitTxAudio() ✅ (tests/icom_identity_test.cpp:116-142 drives the real AudioEngine::feedDaxTxAudio and asserts zero frames before identity, one after — it would fail against unfixed code); RX-only fail-closed ✅ (icom_identity_test.cpp:202-206); no duplicate capture for Flex/Sim/HL2 ✅ (:193-201, plus the !isTxStreaming() / !isRxStreaming() guards). Live dummy-load proof is claimed nowhere in the body — that criterion is unmet and worth stating.
One confirmed collateral breakage (Blocker 1): tests/icom_backend_test.cpp is a registered CTest target that this PR does not touch and that asserts the now-removed behavior.
2. Scope
No CHANGELOG.md entry — correct. No settings-schema, credential, dialog or a11y surface touched.
Socket-test surface (disclosure, not a finding): this PR adds no socket-owning test and no fake peer. icom_identity_test constructs a real IcomSession but never calls start(); frames enter through onCivFrame directly (tests/icom_identity_test.cpp:22-41). It links aethercore and joins AETHER_SETTINGS_CONSUMERS. It is not on the frozen PR gate — it first runs on full-suite.yml after merge, per AGENTS.md.
3. Blockers
1. icom_backend_test asserts behavior this PR deletes, and it is not on the PR gate — so green CI does not cover it
src/core/backends/icom/IcomCivBackend.cpp:1366 now calls publishIdentity() unconditionally at onSessionConnected, before emit connected() at :1396. publishIdentity() always sets r.model (:~5900), and m_model is kUnknown whose name is "Unknown Icom" (IcomModels.cpp:161).
tests/icom_backend_test.cpp:2376 snapshots publishedModel inside the connected() handler and asserts:
check(modelAtConnect.isEmpty(),
"without misreporting the custom nickname as an early hardware model");That is now "Unknown Icom", not empty. icom_backend_test is registered at tests/tests.cmake:602-605 and is deliberately absent from the per-PR -R allow-list (.github/workflows/ci.yml:487 even carries a comment about it having fallen off a gate before), so all five green checks on cc3b8aa prove nothing about it — it runs on full-suite.yml after merge.
I read this rather than ran it, but the chain is mechanical: unconditional publishIdentity() before connected(), r.model always assigned, kUnknown.name non-empty.
The surrounding block (:2328-2385) and the "CI-V ADDRESS RESOLUTION" section that follows (:2400+, CivCase, retarget and two-responder cases against FakeIc705) were written for the old state machine. Please rerun that target locally and update it in this PR — either assert "Unknown Icom" as the honest pre-identity placeholder, or, if this is the moment to retire that fake-peer fixture under the #5254 remediation, delete the affected cases and say so in the body. Landing with it stale means the first full-suite run after merge fails on main.
2. A pinned CI-V address that doesn't match the radio now fails silently in the one place the old code was loudest
IcomCivBackend.cpp:1673-1678 drops any 19 00 reply whose source differs from a pinned address before adoptCivIdentity can compare them. The deleted code (old adoptReportedCivAddress) emitted a named warning for exactly this case:
"This radio reports CI-V address %1, not the %2 that was entered. The entered address is being used."
with a comment stating why it was load-bearing: "on a point-to-point radio it means the typed address is simply wrong and the symptom (a connected radio that answers nothing) names no cause at all."
Post-merge, a pinned-address typo produces five dropped replies and then only the generic exhaustion text ("Check its network CI-V settings and selected CI-V address"), which never mentions that the radio did answer and did name a different address. The reply is in hand; naming it costs one branch. This is the removed-guard-whose-symptom-recurs case from §3 — I'd rather see it restored than argued away, but it's a narrower loss than the old code's, so if you disagree, say so in the body and I'll defer.
4. Nits (non-blocking)
tests/tests.cmake:574-577—icom_identity_testconstructs a liveAudioEngine, which readsAppSettings(AudioEngine.cpp:1855,:4121,:5117). It is correctly added toAETHER_SETTINGS_CONSUMERS(:4341) but does not usetests/TestSettingsProfile.h, so it reads and can write the developer's real store.anan_settings_test.cpp:35andantenna_alias_test.cpp:29show the one-line pattern. I could not find a canon sentence mandating it, so: convention, hence a nit.profileMap()'smodelIdis genuine new bridge surface. It's documented and read-only, but it belongs in the PR body's change list, not only indocs/automation-bridge.md.AudioEngine.cpp:1putscore/backends/RadioCapabilities.haboveAudioEngine.h, so the own-header-first convention no longer provesAudioEngine.his self-sufficient. Cosmetic.- The body claims nothing about live proof. #5164's last acceptance bullet asks for dummy-load evidence (fresh TX bytes, nonzero forward power, safe unkey). Worth stating plainly that it wasn't run, rather than leaving it unmentioned.
5. What I tried to break (and failed)
- "
capabilitiesChangeddoesn't cover disconnect, so capture leaks." It does:RadioModel.cpp:1991wiresconnectionStateChanged → publishCapabilities(connected), andapplyBackendAudioCapabilities's(wasSeamAudio || seamAudio) && isTxStreaming()branch is strictly wider than the oldseamTxAudio && isTxStreaming()— it closes capture even when the backend has already resetcanTransmit, which the old code did not. Genuine improvement, not a regression. - "
audioStartTx/audioStartRxdid more than the raw engine calls." They don't —MainWindow.cpp:4657-4677are bareQMetaObject::invokeMethodwrappers ontom_audio. SubstitutingstartTxStream/startRxStreaminside a lambda already queued tom_audiois equivalent, andpcAudioEnabled/pcAudioRequiredreduce algebraically to the same predicate set (hostModulatesis the only discriminator, and it's still checked before RX starts). - "A silent radio now reconnect-loops for five seconds." It doesn't. The CI-V stall watchdog in
onLinkTickruns above the newm_civReported == 0gate, but it is driven byprofileFor(*m_model).civRecovery, and only the IC-9700 declares one (IcomModels.cpp:653);kUnknownhas none. Discovery therefore can't trip the reconnect path. - "The rejected-identity early return starves the scheduler."
onMeterTickstill callspumpCiv(now)before its own gate (IcomCivBackend.cpp:5980), so dispatch keeps draining while unidentified — including the emergency unkey queued by the ambiguity branch. Reads expire onkReadTimeoutMs = 350anyway, well inside the 1 s retry. - "Ambiguity now fires on the radio's own duplicate reply."
adoptCivIdentity:1273treats same-address/same-ID as inert before the conflict branch, so a retry landing after a late first reply is absorbed.icom_identity_test:139-140pins it. - "Withdrawing the model traps a keyed radio." No:
setKeyingis now gated onkey && !hasTransmit(:4591), so unkey always reaches the radio, and the conflict branch callssetKeying(false)before reassigningm_modeland without reverting the session address — Principle VI holds through the withdrawal.icom_identity_test:169-178pins both halves. - Sibling call sites:
modelForCivAddresshas zero remaining references tree-wide;modelForNamehas zero production callers and survives only in tests, matching its new header comment.AudioEngine::applyBackendAudioCapabilitieshas exactly two callers (MainWindow_Session.cpp:2746, the new test). - Couldn't check: anything runtime. No build, no ctest, no GUI, no radio. Every finding above is reasoned from the head checkout at
/tmp/aetherclaude/pr-5438, not reproduced. In particular I could not confirm whethericom_backend_test'sCivCaseblock (:2400+) also breaks — it depends onFakeIc705's broadcast-reply behavior against the hardenedparseModelIdReply, which I can only settle by running it.
6. Recommendation
Request changes, narrowly. The fix for #5164 is right, well-reasoned, and the new socket-free test genuinely fails against unfixed code — this is a good change. But it lands with a stale assertion in a registered target that the PR gate does not run, so the breakage surfaces on main rather than here, and it drops a diagnostic whose deleted comment says exactly why it existed. Concrete next step: build and run icom_backend_test locally, update (or knowingly retire) the :2328-2385 block and whatever else in :2400+ moves, and restore the pinned-address mismatch warning inside adoptCivIdentity. The modelId bridge field is a small maintainer call, not a blocker.
Thanks for a clear diff — the comment discipline on the deleted state machine made this much easier to audit than it would otherwise have been.
🤖 aethersdr-agent · cost: $10.7022 · model: claude-opus-5
| // Network Radio Name is operator-defined presentation text. Even a name | ||
| // matching a supported model cannot grant capabilities before CI-V replies. | ||
| m_model = &unknownModel(); | ||
| publishIdentity(); |
There was a problem hiding this comment.
Blocker — breaks an existing registered test that this PR does not update.
publishIdentity() runs here, before emit connected() at :1396, and it always assigns r.model — which is now kUnknown.name = "Unknown Icom" (IcomModels.cpp:161). The old code took the else branch for an unresolved name and emitted nickname only, with no model.
tests/icom_backend_test.cpp:2376 snapshots publishedModel inside its connected() handler and asserts:
check(modelAtConnect.isEmpty(),
"without misreporting the custom nickname as an early hardware model");That is now non-empty. icom_backend_test is registered (tests/tests.cmake:602-605) but is not on the frozen per-PR -R allow-list, so the green checks on cc3b8aa say nothing about it — it runs on full-suite.yml after merge, on main.
Reasoned from code, not run. Please build that target and either update the assertion to expect the honest "Unknown Icom" placeholder, or retire the affected FakeIc705 cases under the #5254 remediation and disclose that in the body. The CivCase block at :2400+ exercises the retarget/two-responder paths this PR rewrote and likely needs the same treatment.
| // Reject invalid/foreign ID replies before they can retire a scheduler | ||
| // transaction. Model ID and source need not match on a customized bus. | ||
| if (frame.cmd == cmd::kReadId | ||
| && (!parseModelIdReply(frame) | ||
| || (m_civAddressPinned && m_session | ||
| && frame.from != m_session->civAddress()))) { |
There was a problem hiding this comment.
Blocker — this drops the reply before the one comparison that could explain the failure.
A pinned address that doesn't match the radio's actual address is rejected here, so adoptCivIdentity's own m_civAddressPinned && address != m_session->civAddress() check at :1270 is unreachable for it and nothing is ever said.
The deleted adoptReportedCivAddress emitted a named warning for precisely this case:
"This radio reports CI-V address %1, not the %2 that was entered. The entered address is being used."
with a comment recording why: "on a point-to-point radio it means the typed address is simply wrong and the symptom (a connected radio that answers nothing) names no cause at all."
After this change the operator gets five silently-dropped replies and then only the generic exhaustion text, which never mentions that the radio answered and named a different address. The information is in frame.from; surfacing it costs one branch before the return. Suggest checking the pinned mismatch and emitting the warning (once) rather than dropping silently.
|
|
||
| # Socket-free CI-V identity and late TX-audio lifecycle. IcomSession is never | ||
| # started; literal replies enter the existing injected frame-handler seam. | ||
| add_executable(icom_identity_test tests/icom_identity_test.cpp) |
There was a problem hiding this comment.
Nit, non-blocking: this target constructs a live AudioEngine, which reads AppSettings on construction and in the sidetone/NR paths (AudioEngine.cpp:1855, :4121, :5117). Joining AETHER_SETTINGS_CONSUMERS at :4341 is correct, but without tests/TestSettingsProfile.h the test reads — and can write — the developer's real store, which makes it order-dependent with any other settings-touching test in the same run.
One line, matching anan_settings_test.cpp:35 / antenna_alias_test.cpp:29:
TestSettingsProfile profile(QStringLiteral("icom-identity-test"));(plus #include "TestSettingsProfile.h"). I couldn't find a canon sentence making this mandatory, so it stays a nit.
|
Antares Detector — candidate vulnerable file(s)
The Icom backend implements UDP packet handling and radio-name deserialization; raw UDP payloads are parsed without length validation, enabling unsafe deserialization or out-of-bounds reads (e.g., N1MMSpotParser). 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: $11.4241 · model: claude-opus-5 |
Integrate the IC-9700 network wake contribution with wire-based identity discovery, a bounded cancellable reconnect, and a persisted connection checkbox. Credit the original wake investigation and framing from PR aethersdr#5360. Co-authored-by: W5JWP <justin@w5jwp.radio>
|
CI on What the gate saidI reproduced it locally by running exactly what CI runs ( Only the third counter moved, and only by one. The single new call site is the Wake on connect checkbox:
wakeOnConnect->setStyleSheet(lowBandwidthCheckStyle);Nothing is wrong with the colours — you reused the existing Completely understandable how it happened: you copied the shape of the checkbox immediately above it — m_autoConnectCheck->setStyleSheet(lowBandwidthCheckStyle); // :1145— which is a pre-existing, grandfathered call site. The ratchet only forbids new ones, so the neighbour is fine and yours isn't. That asymmetry is invisible from the surrounding code. The fixRoute it through AetherSDR::ThemeManager::instance().applyStyleSheet(wakeOnConnect, lowBandwidthCheckStyle);
Green, exit 0. This is also a small behavioural upgrade rather than a workaround: You can check it locally before pushing: On the reviewer commentsSeparate from CI — none of them caused this failure, and the two blockers won't show up on the PR checks either:
Nice work on this one — the identity-from- 🤖 aethersdr-agent · cost: $4.3766 · model: claude-opus-5 |
Use model-specific standard CI-V power-on frames from the official guides over RS-BA1, retaining the IC-9700 hardware-derived profile. Verify both added models at custom addresses and preserve opt-in policy. Route checkbox styling through ThemeManager.
Use the RS-BA1 advertised CI-V destination for wake while preserving wire model identity. Keep connection guidance in the panel and status controls permanent. Replace the standard-profile ten-second pause with early reconnect and bounded per-second identity probing.
nigelfenton
left a comment
There was a problem hiding this comment.
Reviewed at head 10a9ee63. Built the PR on Windows/MSVC (Ninja, Qt 6.10.3) and ran icom_identity_test with real stdout, plus a mutation check and a targeted probe of the wake framing path.
Two findings from the previous review do not survive contact with tests.cmake — please don't spend time on them. Details in "What I tried to break" below.
1. Issue fit
The #5164 fix is right. Rebinding the TX-audio lifecycle to capabilitiesChanged via the idempotent AudioEngine::applyBackendAudioCapabilities, and identifying from the 19 00 payload instead of modelForName(), is exactly the "one idempotent helper … do not infer hardware identity from the nickname" direction the issue asked for. Every acceptance criterion maps to a hunk except the last — live dummy-load proof (fresh TX bytes, nonzero forward power, safe unkey). The body doesn't claim it; worth saying so plainly rather than leaving it unmentioned.
2. Scope
| File / group | Change | Claimed? | Verdict |
|---|---|---|---|
MainWindow.h/_Session.cpp, AudioEngine.{h,cpp} |
rebind TX audio to capabilitiesChanged |
yes — the fix | in scope |
IcomCivBackend, IcomModels, CivCodec (identity) |
adoptCivIdentity, modelForId, hardened parseModelIdReply, 5x1 s retry |
yes | in scope |
Wake feature (PowerOnProfile, power.wake, wakeIcomRadio, checkbox, wakeOnConnect) |
~578 lines / 23 files, added after the last review | yes in body, not in #5164 | cleared on governance — see below |
civ wake bridge verb + profileMap modelId |
new automation surface | verb yes; modelId only in docs/automation-bridge.md |
nit |
addWidget -> addPermanentWidget |
global, all radio families | yes, with rationale | disclosed; #3500 interaction checked, holds |
configurationWarning -> connection panel |
Icom-only in practice | yes | Blocker 2 |
| docs x3, mechanical test renames | — | yes | in scope |
No CHANGELOG.md entry — correct.
Governance: the wake feature is cleared, and I want to be explicit about why. At first read it looks like a large unratified feature bundled into a bug fix. It isn't: #5349 carries your own ruling — "I support this for wake" as an option, with sleep-on-disconnect explicitly ruled out because "a remote user can lock themselves out of their radio". This PR implements precisely that: opt-in, default off, wake-only, no power-off on disconnect. No RFC needed. The only process note is that the wake half has no linked issue of its own (#5349 is referenced as "related" only), which makes it harder to find later.
3. Blockers
1. An IC-9700 at a non-default CI-V address silently gets the wrong wake frame
populateFamilyParams inserts icom.wakeModelId only when civSelection() == Model. In Auto and Custom it is absent, so modelId == 0, which bypasses the powerOn profile requirement in power.wake. The only 9700 rescue is the hardcoded address == 0xA2 && advertisedCivAddress() == 0xA2, so a 9700 whose CI-V address was changed misses it and falls through to standard{}.
I drove power.wake through the real backend on the PR head. Same radio, only the address differs:
# IC-9700 at DEFAULT 0xA2 — correct
PROBE frame len=470: fe fe fe fe ... (150x fe) ... a2 e1 18 01 fd
# IC-9700 at CUSTOM 0xB0 — wrong
PROBE frame len=20: fe fe b0 e0 18 01 fd
PROBE started=1 err=
Zero preamble bytes and controller E0 instead of the measured 150xFE / E1. It returns started=1 with no error, the radio never wakes, and the 20 s deadline expires. This defeats the exact hardware evidence @w5jwp contributed in #5360, in a configuration this file's own comments call out as real ("on a shared CI-V bus … the operator SELECTING WHICH DEVICE to talk to").
Suggested direction: carry wakeModelId for Custom/Auto as well, or key framing off the advertised/identified model rather than the literal 0xA2 — and refuse rather than silently sending standard framing when a 9700 is indicated but unprofiled.
2. Mid-session Icom configuration warnings now render into a hidden widget
Moving configurationWarning from statusBar()->showMessage(message, 15000) to m_connPanel->setStatusText(message) is sound for connect-time advice, and the rationale (don't cover the Connect control) is good. But setStatusText is a bare m_statusLabel->setText() with no visibility guarantee, and m_connPanel->hide() runs on the connected edge (MainWindow.cpp:6070).
configurationWarning also carries mid-session warnings — the out-of-band tune refusal (IcomCivBackend.cpp:3594) and the DATA MOD-input advisory (:1636, whose comment says it exists to "NAME THE REMEDY, because this client deliberately will not apply it unasked"). Those now go somewhere the operator cannot see. Routing by connection state, or keeping showMessage while connected, would preserve both goals.
This one is a judgement call — if you'd rather keep it uniform, say so and I'll defer.
4. Nits (non-blocking)
profileMap()'s newmodelIdis genuine bridge surface documented only indocs/automation-bridge.md; it belongs in the body's change list.wakeIcomRadioconnectsextensionResultasDirectConnection, callsinvokeExtension, then disconnects and readssent— assuming a synchronous reply.IRadioBackend.h:797documents the opposite ("A synchronous QVariant return would have to block or fabricate a local value against an async backend"). Safe today because the path is gated onm_family == "icom"and that backend replies inline, but it's a trap if it ever defers. An assertion or a comment naming the coupling would pin it.onConnectionErrorearly-returns during wake before theqCWarning(lcProtocol), so wake-path connection errors leave no log line.- No test injects a mismatched identity after wake, so the "The radio identified as a different model." branch — which disconnects an otherwise-connected radio — is uncovered. Every completion case injects a matching reply.
- IC-705 physical wake untested; the body says so, which is the right call.
5. What I tried to break (and failed)
- The previous review's Blocker 1 (
icom_backend_testbreaking onmain) is refuted. That target sits inside a CMake bracket comment,tests/tests.cmake:584-606, retired under the #5254 fake-peer remediation — and it was already commented out onorigin/mainbefore this PR. It is not registered, cannot fail CI, and perAGENTS.mda bracket-commented test isn't coverage either way. The stale assertions are in dead code; at most a nit, and arguably just history. - Its Nit 1 (the new test writing the developer's real settings store) is also refuted.
icom_identity_test.cpp:108-109setsAETHER_SETTINGS_DIRfrom aQTemporaryDirbeforeAudioEngineis constructed, so it is properly isolated. - The new test is not a rubber stamp — I inverted it. Replacing the wake TX guard
if (m_keyed || m_tuning || m_pendingPttIntent.value_or(false))withif (false)and rebuilding producedFAIL: wake refuses an active transmitter(2 failures, exit 1). Restored:icom_identity_test: 0 failure(s), exit 0, on Windows/MSVC. - Principle VI holds. Wake sends
18 01, never a keying command, and refuses whilem_keyed || m_tuning || m_pendingPttIntent. The commit trailers are apt. addPermanentWidgetvs #3500. #3500 (a08e66f9) fixed width/minimum-size clipping, not widget placement, andupdateStatusBarMinimumWidth()still runs. No regression there.- Test-boundary preflight is clean. The PR adds no socket-owning test and no fake peer;
icom_identity_testnever callssession.start()— frames enter viaonCivFrame. - Could not verify: anything needing a real Icom. The wake path is unreachable in demo mode (
SimBackendisn't Icom andwakeIcomRadiois family-gated), so the checkbox's GUI behaviour is reasoned from code plus your hardware evidence. Blocker 1 is reproduced at the backend level, not on a physical 9700 — if the framing fallback is deliberate for some reason I've missed, I'd rather hear it than have you work around it.
6. Recommendation
Request changes, narrowly — Blocker 1 only; Blocker 2 is yours to rule on.
The #5164 fix is correct and genuinely well-tested, and the wake feature is properly scoped, default-off, TX-safe and maintainer-sanctioned. But it silently sends the wrong frame to the one model whose framing was hardware-measured, in a configuration the code itself describes as common — and the frame dumps are above.
Reviewed by Claude Opus 5 on aurora13 for @nigelfenton. Built and ran the PR head; frame dumps and the mutation check are from that build.
| req.params.insert(QStringLiteral("icom.wakeOnConnect"), IcomSettings::wakeOnConnect()); | ||
| if (IcomSettings::civSelection() == IcomSettings::CivSelection::Model) { | ||
| // An explicit model choice may authorize power framing, never capabilities. | ||
| req.params.insert(QStringLiteral("icom.wakeModelId"), IcomSettings::civAddress()); |
There was a problem hiding this comment.
Blocker 1. icom.wakeModelId is inserted only for CivSelection::Model. In Auto and Custom it is absent, so power.wake receives modelId == 0 — which bypasses the powerOn profile requirement and falls through to standard framing.
For an IC-9700 at a non-default CI-V address this sends fe fe <addr> e0 18 01 fd (0 preamble, controller E0) instead of the measured 150xFE / E1. Verified on the PR head:
# 0xA2 (default): fe fe ...150x fe... a2 e1 18 01 fd (len 470)
# 0xB0 (custom): fe fe b0 e0 18 01 fd (len 20)
Carrying the model id in the other two selection modes would close it — the wire identity still gates capabilities, so this only affects framing:
| req.params.insert(QStringLiteral("icom.wakeModelId"), IcomSettings::civAddress()); | |
| req.params.insert(QStringLiteral("icom.wakeOnConnect"), IcomSettings::wakeOnConnect()); | |
| // Framing hint only: an address never grants capabilities, so carrying it in | |
| // every selection mode cannot widen what the radio is trusted to be. Omitting | |
| // it for Auto/Custom sent an IC-9700 at a non-default address standard framing. | |
| req.params.insert(QStringLiteral("icom.wakeModelId"), IcomSettings::civAddress()); |
| // the later identity reply. An explicit model also handles custom 9700s. | ||
| const PowerOnProfile standard{}; | ||
| const IcomModel* framing = selected; | ||
| if (!framing && address == 0xA2 && m_session->advertisedCivAddress() == 0xA2) { |
There was a problem hiding this comment.
Blocker 1 (the other half). This is the only rescue for the IC-9700's measured framing, and it is keyed to the literal default address. A 9700 whose operator changed its CI-V address advertises the new value (byte 0x94), misses this branch, and gets standard{} — silently, with sent: true.
The comment above says the IC-9700 hint is retained "for its default advertised destination", so the narrowness looks deliberate — but the effect is that the one model with hardware-verified framing is also the one that breaks when its address is customised, which is a supported and (per IcomSettings.h) expected configuration on a shared bus.
Either derive framing from the advertised/identified model rather than the literal 0xA2, or refuse the wake when a 9700 is indicated but no profile can be selected — a refusal the operator can see beats a frame the radio ignores.
| this, &MainWindow::onWanCertFingerprintMismatch); | ||
| connect(&m_radioModel, &RadioModel::radioWakeProgress, this, | ||
| [this](const QString& message, bool active) { | ||
| m_connPanel->setStatusText(message); |
There was a problem hiding this comment.
Blocker 2 (judgement call). Good rationale for connect-time advice, but configurationWarning also carries mid-session warnings, and m_connPanel->hide() runs on the connected edge (MainWindow.cpp:6070). setStatusText is a bare m_statusLabel->setText() with no visibility guarantee.
So the out-of-band tune refusal (IcomCivBackend.cpp:3594) and the DATA MOD-input advisory (:1636) now render into a hidden panel. The latter's own comment says it exists to "NAME THE REMEDY, because this client deliberately will not apply it unasked" — it can't do that unseen.
Routing by connection state keeps both properties:
| m_connPanel->setStatusText(message); | |
| qCWarning(lcProtocol).noquote() << "radio configuration:" << message; | |
| // Connect-time advice belongs in the panel, where it cannot cover the | |
| // Connect control. Once connected the panel is hidden, so a warning put | |
| // there would never be seen — mid-session advice stays on the status bar. | |
| if (m_radioModel.isConnected()) { | |
| statusBar()->showMessage(message, 15000); | |
| } else { | |
| m_connPanel->setStatusText(message); | |
| } |
| QString failure = tr("Wake is unavailable for this backend."); | ||
| // The extension replies synchronously. The reserved local ID is scoped to | ||
| // these connections and cannot consume another caller's asynchronous reply. | ||
| constexpr quint64 requestId = std::numeric_limits<quint64>::max(); |
There was a problem hiding this comment.
Nit, non-blocking. This assumes invokeExtension replies synchronously — the connections are DirectConnection and both are disconnected before sent is read. IRadioBackend.h:797 documents the opposite contract: "A synchronous QVariant return would have to block or fabricate a local value against an async backend."
It's correct today because the path is gated on m_family == "icom" and that backend emits inline, but nothing here would fail loudly if it ever deferred — sent would simply stay false and wake would report "unavailable for this backend". A one-line comment naming the coupling (or an assert) would keep the next reader out of the trap.
| const bool matches = m_radioWakeModel.isEmpty() | ||
| || m_backend->capabilities().model == m_radioWakeModel; | ||
| finishRadioWake(matches ? tr("Radio ready.") | ||
| : tr("The radio identified as a different model."), matches); |
There was a problem hiding this comment.
Nit, non-blocking. This branch disconnects a radio that is connected and identified, purely because it isn't the model the wake targeted — but no test injects a mismatched identity after a wake. Every completion case in icom_identity_test.cpp injects a matching reply, so the failure half of this comparison is uncovered.
Given the branch tears down a working session, it seems worth one fixture: wake as 0xA2, answer 19 00 with B6, assert the disconnect and the message.
Ozy311
left a comment
There was a problem hiding this comment.
Operator-requested second opinion at 10a9ee63d94a72ca516f7322a950e4b08052f0e9. Request changes for two additional reproduced P2 defects, detailed inline: native CW cancellation is lost after identity withdrawal, and the permanent full-width status container hides transient status messages.
The identity/TX-audio approach addresses #5164: the CI-V model-ID payload selects capabilities independently of the nickname and operating address, and late capability changes reapply audio routing. Live dummy-load acceptance remains unverified in this review. Optional wake is explicitly claimed in the title/body and related to #5349.
Scope
| File group | Change | Claimed? | Assessment |
|---|---|---|---|
| IcomCivBackend, IcomSession, IcomModels, CivCodec | CI-V identity, parsing, bounded discovery, capability withdrawal | Yes | Central fix; CW cancellation defect below |
| AudioEngine and MainWindow capability wiring | Late audio-routing updates | Yes | Relevant to #5164; local tests pass |
| IcomProtocol, IcomSettings, RadioModel | Opt-in wake profiles, reconnect/deadline/cancellation | Yes | Related #5349 scope |
| ConnectionPanel and MainWindow session/status UI | Wake checkbox, advice, persistent controls | Yes | Relevant support; status-message regression below |
| AutomationServer | Explicit civ wake and diagnostics |
Yes | Disclosed surface; normal human owner review applies |
| Tests and tests.cmake | Socket-free identity/wake and protocol/model coverage | Yes | Passing suite misses the CW interleaving |
| Three documentation files | Backend, audio-path and bridge contracts | Yes | Supporting scope; generated docs check passes |
All 32 changed files fit these groups. No governance, changelog, or frozen CI gate changes. Author-reported hardware evidence was not independently repeated.
Verification
- Built the exact-head macOS application and focused targets with Qt 6.11.1, RelWithDebInfo; ASR, RADE, DSTAR and DFNR disabled locally.
- All 10 focused Icom CTests passed after restoring the temporary probes: identity, family, CI-V, scheduler, protocol, scope, audio, meters, memory and power derivation (12.68 seconds).
- A socket-free probe through real RadioModel/CwxModel/backend code failed on missing CW abort after identity conflict. Temporarily aborting before capability withdrawal made the same assertion pass; restored both production and test sources, rebuilt, and reran the original suite.
- Drove the exact-head application offscreen through an isolated authenticated local bridge, with isolated settings, muted PC output, TX disabled and outbound IP traffic denied. Verified the owned PID, disconnected state and zero slices; clicked the RX step control, observed
Step: 250 HzinQStatusBar.statusMessage, and captured the status bar without that text. Stopped the owned instance afterward. - Engine-boundary strict check passed (102 tracked legacy warnings, zero blocking findings); test registration, frozen CI gate, generated bridge docs and whitespace checks passed.
- All five current CI checks pass, including actual Linux/macOS/Windows build steps. All five commits have valid GitHub signature verification. The Icom identity test is not run by the per-PR CI gate; it was run locally here.
PR base/merge base: 50d70ebe9fdc4d6f01171b5cb17479bc89537be2. Trusted current-main governance snapshot: d58e2b8a94af82f273f60010df17b080e6b2d907; manifest PASS, Constitution mirror matches. Checked intervening main changes for stale overwrites in these paths; none found.
I am not duplicating the existing custom-address IC-9700 wake and hidden connection-panel warning threads. The new status-bar finding affects other showMessage() callers and remains even if the existing warning-routing issue is corrected.
No physical wake, live RF/TX, microphone, forward-power/unkey acceptance, full-suite or sanitizer-runtime test was performed. The CW probe establishes missing cancellation dispatch, not measured continued RF. No additional nits. Please fix the two reproduced paths and cover their regressions; required human CODEOWNERS approval remains separate.
Reviewed with Codex for @Ozy311.
| if (m_keyed || m_tuning || m_pendingPttIntent.value_or(false)) { | ||
| setKeying(false); | ||
| } | ||
| m_model = &unknownModel(); |
There was a problem hiding this comment.
[P2] Abort native CW before withdrawing the identified profile
If a conflicting identity arrives after native CW text has been dispatched, this branch withdraws the model without issuing the radio-side CW abort. The PTT/tuning release here does not cancel the separate CI-V text buffer. Publishing unknown capabilities also removes hasRadioSideCwKeyer, so RadioModel.cpp:2432–2436 stops forwarding CwxModel::transmissionCancelled; a subsequent Clear/ESC therefore cannot send 17 FF for text already dispatched.
Reproduced using the existing socket-free identity-test seam and the real RadioModel/CwxModel/backend path: identify IC-705, send TEST, wait for its frame, inject acknowledgement, optionally inject a conflicting Auto-discovery reply, then call clearBuffer():
CW_ABORT_PROBE conflict=0 automatic_aborts=0 after_clear=1
CW_ABORT_PROBE conflict=1 automatic_aborts=0 after_clear=0
FAIL: identity withdrawal or subsequent clear must dispatch 17 FF
Temporarily calling abortCwText() before withdrawing the profile made the same assertion pass (automatic_aborts=1, after_clear=1). Disconnect already sends a native abort; the probe checked before disconnect so cleanup could not mask the missing command. No socket peer or physical RF was used.
Please abort native CW to the previously selected destination before invalidating the profile, and add a regression case for cancellation after identity withdrawal. The existing passing identity suite does not cover this interleaving.
| hbox->addWidget(timeStack); | ||
|
|
||
| statusBar()->addWidget(m_statusBarContainer, 1); | ||
| statusBar()->addPermanentWidget(m_statusBarContainer, 1); |
There was a problem hiding this comment.
[P2] Preserve visible space for transient status messages
Making the entire stretched status container permanent leaves no usable area for Qt to paint temporary messages: those are drawn before permanent widgets, while this container occupies virtually the full bar. Existing showMessage() calls across radio families therefore lose visible feedback, including unsupported-command notices, memory-recall refusals and “TX is owned by another station.” This is separate from the existing configuration-warning/hidden-connection-panel thread.
Confirmed in the exact-head app through its isolated bridge while disconnected and TX-disabled: clicking RxApplet/Step size up changed 100 to 250 and set QStatusBar.statusMessage to Step: 250 Hz, but the captured bar displayed none of that message. The permanent child remained visible at x=2, width 1398, in a 1400-pixel bar. A Qt before/after harness likewise showed zero changed pixels after showMessage() with the permanent container, versus 2645 with the previous normal-widget arrangement.
Please retain visible transient-message space while keeping the required Connect controls available; making the whole full-width container permanent suppresses the existing feedback channel. The Icom unit tests do not exercise this rendering behavior.
ten9876
left a comment
There was a problem hiding this comment.
Reviewed at head 10a9ee63, but the load-bearing part of this review is the one thing the three earlier passes could not have done: I merged this branch with current main and built the result. It does not compile. Everything below the scope table is new; I have deliberately not re-litigated the IC-9700 custom-address wake framing, the hidden connection-panel warning, or the CW-cancellation finding, which belong to their existing threads.
1. Issue fit
Yes on #5164. Rebinding the TX-audio lifecycle to capabilitiesChanged through the idempotent AudioEngine::applyBackendAudioCapabilities, and identifying from the 19 00 model-ID payload instead of modelForName(), is exactly the "one idempotent helper … do not infer hardware identity from the nickname" direction the issue asked for. Every acceptance criterion maps to a hunk except the last (live dummy-load proof of fresh TX bytes / nonzero forward power / safe unkey), which the body correctly does not claim.
The issue thread's two tightening asks are both satisfied, and I checked them rather than assuming:
AppSettings::save()per capability edge —MainWindow_Session.cpp:2750-2753writes only insideconnected && pcAudioRequired && !savedPcAudio, so an unchanged value does not re-open the #4273 non-atomic write race.notePcAudioEnabled()repeated with an unchanged value — it does re-entercheckModInput()on every edge, but that path dedupes onm_lastModInputWarning(IcomCivBackend.cpp:1618-1620), so no repeated DATA OFF MOD advisory.
2. Scope
| File / group | Change | Claimed? | Verdict |
|---|---|---|---|
MainWindow.{h,_Session.cpp}, AudioEngine.{h,cpp} |
TX audio rebound to capabilitiesChanged via applyTxAudioCapabilities / applyBackendAudioCapabilities |
yes — the fix | in scope |
IcomCivBackend, IcomModels, CivCodec, IcomProtocol (identity) |
adoptCivIdentity, modelForId, hardened parseModelIdReply, parseCapabilitiesCivAddress, bounded 5×1 s discovery |
yes | in scope |
Wake feature (PowerOnProfile, power.wake, wakeIcomRadio, checkbox, wakeOnConnect) |
~578 lines | yes in body; not in #5164 | cleared on governance under the #5349 ruling (opt-in, wake-only, no sleep-on-disconnect) — already established on this PR |
civ wake bridge verb + profileMap modelId |
new automation surface | verb yes; modelId docs-only |
nit, already raised |
addWidget → addPermanentWidget; configurationWarning → connection panel |
global / Icom-in-practice | yes, with rationale | existing threads |
docs ×3, mechanical test renames, icom_identity_test |
— | yes | in scope |
No CHANGELOG.md entry — correct. Test-boundary preflight is clean: the PR adds no socket-owning test and no fake peer. icom_identity_test constructs real IcomCivBackend / IcomSession / AudioEngine / RadioModel objects but never calls session.start(); frames enter through onCivFrame (tests/icom_identity_test.cpp:32-41), and AETHER_SETTINGS_DIR is pointed at a QTemporaryDir at :107-108 before AudioEngine is constructed.
3. Blocker
1. Merged with current main, this branch does not compile — and main's own CI would be the first thing to find out
mergeable is CONFLICTING. That alone is routine; what is not routine is what is on the other side of the conflict. Two commits landed on main after this PR's merge base 50d70ebe:
4bf9c5a6— fix(icom): preserve finite AX.25 transmit audio over RS-BA1 (#5311)6797007f— fix(icom): Restore durable memories and safe synced recall
#5311 added tests/icom_ptt_authority_test.cpp, a registered CTest (tests/tests.cmake:638-643, not bracket-commented), and it calls the function this PR removes:
tests/icom_ptt_authority_test.cpp:96: const IcomModel* ic705 = modelForCivAddress(0xA4);
tests/icom_ptt_authority_test.cpp:199: const IcomModel* ic9700 = modelForCivAddress(0xA2);
I merged pr5438-review into origin/main in a scratch worktree, resolved both conflicts conservatively (combining the TX-audio gate, keeping both verb blocks, keeping both test registrations) and built:
FAILED: CMakeFiles/icom_ptt_authority_test.dir/tests/icom_ptt_authority_test.cpp.o
tests/icom_ptt_authority_test.cpp:96:30: error: 'modelForCivAddress' was not declared in this scope
tests/icom_ptt_authority_test.cpp:199:31: error: 'modelForCivAddress' was not declared in this scope
This PR does the mechanical modelForCivAddress → modelForId rename in icom_family_test, icom_meters_test, icom_memory_test and icom_power_derivation_test — correctly. It could not have renamed the one in icom_ptt_authority_test, because that file did not exist when the branch was cut.
Why the five green checks do not cover this. The Linux build job runs cmake --build build -j$(nproc) (.github/workflows/ci.yml:195) — all default targets, tests included — so this is a build-job failure, not merely a full-suite one. And branch protection deliberately does not require branches to be up to date (ci.yml:22-25 explains why), so nothing forces a re-run against the current tip. The breakage lands on main.
The second conflict is semantic, and resolving it the obvious way silently reverts #5311. Both sides edit the same submitTxAudio gate:
// merge base
if (m_tuning || !m_keyed) {
// this PR
if (m_tuning || !m_keyed || !m_model->hasTransmit) {
// main, after #5311
if (m_tuning || !txAudioGateOpen()) {txAudioGateOpen() (IcomCivBackend.cpp:3455-3461) is #5311's bounded key-on window — the thing that keeps the AX.25 FCS and postamble from being dropped. Taking "ours" here compiles fine and quietly undoes that fix. I resolved it as if (m_tuning || !txAudioGateOpen() || !m_model->hasTransmit), which is what I believe you want, but it is your call and it needs a test.
Concrete next step: merge main in, update icom_ptt_authority_test's two modelForCivAddress calls to modelForId, combine the TX-audio gate deliberately, and re-run the focused Icom set plus icom_ptt_authority_test on the merged tree. I ran the pre-merge set here and it is clean (below), so this is genuinely the only thing standing between the branch and a build that holds on main.
4. Nits (non-blocking)
IcomSettings.h:32-35— the two new declarations were inserted between theusername()doc comment andusername()itself, so "The operator's network username on the radio. NOT a secret…" now reads as documentation forwakeOnConnect(). Suggestion inline.RadioModel.cpp:1210— wake completion is defined ascanTransmit, not "identity arrived". Every model inkModelstransmits today, so this is latent, but an RX-only Icom (an IC-R receiver) would be force-disconnected by the 20 s deadline after a wake that actually succeeded, viafinishRadioWake(..., false). A comment naming the coupling, or keying completion offm_civReported != 0, would pin it.ConnectionPanel.cpp:1153— "Wake on connect" is visible and enabled for every family. I confirmed it on the bridge in a session where only a FLEX-8600, a Hermes-Lite 2 and the demo simulator were discovered:visible: true, enabled: true. Only the accessible description says "Icom"; the visible label does not. A FLEX-only operator sees a control that cannot do anything.IcomProtocol.cpp:459— the comment ("civ at record offset 0x52, after the 0x42-byte envelope header") is arithmetically right but reads as a contradiction next topkt[0x94], especially sinceparseCapabilitiesNameuses absolute0x52. Bounds themselves are fine:startsWithpinspkt.size() == 0xA8, and0x94 < 0xA8.
5. What I tried to break, and could not
- Ambiguity latch as a permanent TX lockout.
m_civAmbiguous = truedisables transmit for the session andadoptCivIdentityreturns early forever after — but it is cleared on bothconnectRadio(:842) and session reset (:977), so a shared-bus collision does not survive a reconnect. Held. - Repeated capability edges duplicating capture or thrashing settings. Both tightenings from the issue thread are implemented; see §1. Held.
- OOB read in the new
parseCapabilitiesCivAddress. Size-pinned, index in range. Held. - The pre-merge Icom suite. Built the PR head on Arch/GCC 15/Qt 6.10 RelWithDebInfo and ran the full
icom_set offscreen: 16/16 passed in 13.79 s, includingicom_identity_test(13.48 s). No local failures to attribute. - Bridge session, demo simulator only. Isolated
AETHER_SETTINGS_DIR+ scratchHOME,AETHER_AUTOMATION_NO_TX=1, explicit socketaethersdr-pr5438, own PID.connect listofferedDEMO-0001;get radioreturned{"model": "AetherSDR Demo", "serial": "DEMO-0001", "transmitting": false}. The operator's FLEX-8600 was discovered and never touched.- Checkbox placement claim confirmed:
Connect to last radio on start upaty=618,connectionWakeOnConnectaty=652— directly beneath, as the body says. - Persistence claim confirmed across a process boundary: toggled it on, killed the process, relaunched against the same store,
checked = true. - Sim path not regressed by the rewiring: two disconnect/reconnect cycles,
orphanCount: 0,transmitting: falsethroughout.
- Checkbox placement claim confirmed:
- Corroboration for the existing hidden-warning thread (not a new finding, but the bridge settles the mechanism): while connected,
invoke connectionWakeOnConnectreturns{"error": "refused: 'connectionWakeOnConnect' is not visible", "hidden": true}— the whole connection panel is hidden on the connected edge, so everything routed tosetStatusTextis unreachable mid-session. Worth noting thatadoptCivIdentity's ambiguity notice, "Conflicting CI-V identification replies. Transmit is disabled; select the radio's CI-V address and reconnect." (IcomCivBackend.cpp:1296-1298), goes through that sameconfigurationWarningpath. That one is a transmit-disabled notice, which raises the stakes on that thread beyond the tune-refusal and DATA MOD advisories already cited. - Generated-docs drift:
tools/gen_bridge_docs.py --check→ok: docs verb table matches the registry (71 verbs). - Could not verify: anything needing a real Icom — the wake path is family-gated and unreachable in demo mode. I did not independently re-run the IC-9700 frame dumps or the CW-cancellation probe from the other two reviews; I have no evidence against either.
6. Recommendation
Request changes, on Blocker 1 only. The #5164 fix is right, the identity state machine is well-commented and survived the attacks I could mount against it, and the new socket-free test is real coverage rather than a rubber stamp. But the branch is three days stale against a main that grew a registered test for the exact subsystem this PR rewrites, and the merged tree does not compile — with the added trap that the second conflict can be "resolved" into a silent revert of #5311. That is a merge-and-fix, not a redesign.
Reviewed by Claude Opus 5 for @jeremy_fielder. Merged, built and ran the branch locally on Arch Linux; the compiler output and bridge JSON above are from that run.
| // has ~130 CI-V addresses and this table has a handful. | ||
| [[nodiscard]] const IcomModel* modelForCivAddress(std::uint8_t addr); | ||
| // Look up the model ID payload returned by CI-V 19 00. Unknown IDs return | ||
| // nullptr; a configured command address is never an input to this lookup. |
There was a problem hiding this comment.
Blocker 1 — this rename breaks the build once merged with current main.
modelForCivAddress has zero remaining references on this branch, and the four Icom tests that used it are correctly updated here. But main grew a fifth caller after this branch was cut — tests/icom_ptt_authority_test.cpp, added by #5311 (4bf9c5a6) and registered at tests/tests.cmake:638-643:
tests/icom_ptt_authority_test.cpp:96: const IcomModel* ic705 = modelForCivAddress(0xA4);
tests/icom_ptt_authority_test.cpp:199: const IcomModel* ic9700 = modelForCivAddress(0xA2);
Merging this branch into origin/main locally and building gives:
FAILED: CMakeFiles/icom_ptt_authority_test.dir/tests/icom_ptt_authority_test.cpp.o
tests/icom_ptt_authority_test.cpp:96:30: error: 'modelForCivAddress' was not declared in this scope
tests/icom_ptt_authority_test.cpp:199:31: error: 'modelForCivAddress' was not declared in this scope
The Linux build job runs cmake --build build -j$(nproc) over all default targets (ci.yml:195), so this is a build failure on main, not just a full-suite one — and branch protection deliberately does not require branches to be up to date, so nothing re-runs CI against the tip.
Both call sites are pure renames (modelForId(0xA4) / modelForId(0xA2); the model IDs equal those factory addresses). Please merge main in and fix them here.
While you are in the merge: the other conflict is in IcomCivBackend::submitTxAudio and is semantic, not textual. main replaced !m_keyed with !txAudioGateOpen() — #5311's bounded key-on window that preserves the AX.25 FCS and postamble — while this branch added !m_model->hasTransmit to the same line. Resolving it as "ours" compiles and silently reverts #5311. I built it as if (m_tuning || !txAudioGateOpen() || !m_model->hasTransmit), which I think is what you want, but it is your call and it deserves a test.
| // The operator's network username on the radio. NOT a secret — the radio | ||
| // pairs it with a password and the username alone grants nothing. | ||
| // Client-owned, opt-in connection policy, stored in the Icom document. | ||
| static bool wakeOnConnect(); |
There was a problem hiding this comment.
Nit, non-blocking: these two declarations landed between the username() doc comment and username() itself, so "The operator's network username on the radio. NOT a secret…" now reads as documentation for wakeOnConnect().
| static bool wakeOnConnect(); | |
| static QString username(); | |
| static void setUsername(const QString& username); | |
| // Client-owned, opt-in connection policy, stored in the Icom document. | |
| static bool wakeOnConnect(); | |
| static void setWakeOnConnect(bool enabled); |
(the following static QString username(); / static void setUsername(...) lines then go away — this suggestion replaces the block through them.)
| connect(m_backend.get(), &IRadioBackend::capabilitiesChanged, this, | ||
| [this] { publishCapabilities(isConnected()); }); | ||
| [this] { | ||
| publishCapabilities(isConnected()); |
There was a problem hiding this comment.
Nit, non-blocking: wake completion is defined as canTransmit, not as "identity arrived".
Every model in kModels has hasTransmit = true today — only kUnknown does not — so this is latent rather than live. But an RX-only Icom (an IC-R receiver) that woke and identified perfectly would never satisfy this branch, and the 20 s deadline below would then call finishRadioWake(..., false) and disconnect an already-working radio, reporting "Wake did not complete."
Either key completion off identity (m_civReported != 0, which is what "the radio answered" actually means) or leave a comment naming the coupling, so whoever adds the first receive-only Icom sees it.
| }); | ||
| root->addWidget(m_autoConnectCheck); | ||
|
|
||
| auto* wakeOnConnect = new QCheckBox(tr("Wake on connect"), this); |
There was a problem hiding this comment.
Nit, non-blocking: this checkbox is added unconditionally, so it is visible and enabled for every radio family. I confirmed it on the automation bridge in a session where only a FLEX-8600, a Hermes-Lite 2 and the demo simulator were discovered — no Icom anywhere:
{"objectName": "connectionWakeOnConnect", "class": "QCheckBox",
"text": "Wake on connect", "checked": false,
"visible": true, "enabled": true}Only the accessible description mentions Icom; the visible label does not. A FLEX-only operator sees a control that cannot do anything for their radio. Either gate the widget on the selected radio's family, or say "Icom" in the label.
(The a11y work here is otherwise better than its neighbours — accessible name and description where m_autoConnectCheck has neither. Placement matches the body's claim exactly: y=618 for "Connect to last radio on start up", y=652 for this one.)
Resolves the branch against current main, which grew three Icom commits after this branch was cut: aethersdr#5311 (bounded PTT key-on window and the txAudioGateOpen TX-audio gate), aethersdr#5436 (native controls and meter precision) and aethersdr#5435 (scheduler poll fairness). Two textual conflicts, both in code this branch rewrites: * IcomCivBackend::submitTxAudio — main replaced `!m_keyed` with `!txAudioGateOpen()`; this branch added `!m_model->hasTransmit`. Both terms are kept. txAudioGateOpen() follows PTT intent inside aethersdr#5311's bounded window so a finite AX.25 packet's resampler tail is not dropped; hasTransmit refuses audio while identity is still unresolved and m_model is unknownModel(). Keeping only one of them would either submit audio against an unidentified radio or reopen aethersdr#5311. * The Icom extension verb chain and tests.cmake — pure additions on both sides (power.wake beside debug.ax25.capture.*, icom_identity_test beside icom_ptt_authority_test and icom_control_profile_test). Both sides kept. Three stale callers of the removed modelForCivAddress() came in with main and are renamed to modelForId(): icom_ptt_authority_test (x2) and memory_import_test. Every call passes a factory-default CI-V address, which equals that model's 19 00 model ID, so the rename is behaviour preserving. Two merged-tree test failures fixed, both fixtures that predate the semantics this branch and aethersdr#5311 introduce: * icom_control_profile_test::prepareSession did not mark identity verified, so the new "routine polling waits for verified identity" gate made onLinkTick() return before queueing. The fixture now sets m_civReported/m_civModelId, which is the state a real session reaches through a 19 00 reply. * icom_identity_test asserted that the ambiguity branch clears m_keyed. Since aethersdr#5311 m_keyed is radio state, moved only by a decoded 1C 00 readback; the client owns the unkey INTENT and the address it goes to. The assertion now pins both, plus the literal "fe fe 50 e0 1c 00 00 fd" leaving for the already-selected destination rather than the conflicting responder — strictly stronger than the flag it replaces. Verified on the merged tree: 19/19 Icom + memory_import CTests pass, all seven static checks pass (engine boundary, touchpoint manifest, test registration, frozen CI gate, network timeouts, theme seed, shader dialects), gen_bridge_docs --check is clean at 72 verbs, and AetherSDR itself links. Three mutations confirm the rewritten assertions still pin behaviour: dropping the ambiguity unkey, retargeting that unkey to the conflicting responder, and forcing the routine-poll gate closed each fail the corresponding test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Fixes #5164. Supersedes #5360; related to #5349.
An editable RS-BA1 Network Radio Name could select the wrong Icom capability profile. Late identification also left TX-audio routing disabled, and a missed first identity reply could strand startup polling the wrong address. This change identifies the radio from its CI-V
19 00model-ID payload, keeps the network name as its nickname, and updates audio routing when capabilities arrive.The reply source independently selects the command destination:
FE FE E0 50 19 00 A4 FDidentifies an IC-705 at address50. Auto discovery broadcasts; a pinned selection accepts only its selected responder. Unknown, malformed, conflicting, and stale replies cannot grant transmit capabilities. Discovery sends at most five queries, one second apart; routine polling waits for verified identity. Generic FB/FA acknowledgements cannot finish identification.Optional wake on connect
The connection panel now has Wake on connect directly beneath Connect to last radio on start up. It defaults off and persists through the existing Icom settings document. When enabled, exhausted identity discovery can request one wake for the network-advertised radio destination, including Auto mode. A responding radio is not woken, and disconnect never sends power-off.
IC-705, IC-7300MK2, and IC-9700 have explicit wake profiles. IC-705 and IC-7300MK2 send the standard
FE FE <address> E0 18 01 FDframe through RS-BA1, based on their official CI-V guides. The MK2 guide's baud-dependent FE fill specifically applies to the serial REMOTE jack. Both begin reconnecting after one second and probe identity once per second until it arrives, with a 20-second deadline. A network interface that is fully off cannot receive the command.IC-9700 retains its measured 150 additional FE bytes, E1 controller address, and 10-second delay from @w5jwp's original #5360 investigation and hardware evidence. This contribution is credited by a co-author trailer and documentation. None of these profiles infer model identity from an editable nickname. IC-7300MK2 standby/wake is verified on hardware below; IC-705 physical standby/wake remains untested.
Wake owns one delayed reconnect and a bounded identity deadline. Auto reads the CI-V destination from the authenticated RS-BA1 capability record (byte 0x94); the editable nickname and default A4 settings seed cannot select the destination. The network address is only a wake/framing hint: model capabilities still require a wire identity reply. Disconnect or a new connection cancels delayed callbacks. Terminal failure stops automatic retry and preserves the failure in the connection UI. Connection advice/progress stays in the connection panel. Status-bar controls are permanent widgets, so temporary messages cannot hide the connect control. The backend refuses invalid model/address selections, identity or pinned-address mismatches, ambiguous buses, and active transmit/tune. The agent automation bridge also exposes explicit
civ wake <model-id-hex> <address-hex>through the same model lifecycle.Validation of this update
cmake --build build -j22; final source commit10a9ee63.QT_QPA_PLATFORM=offscreen,-j22, and--no-tests=error, with executables, settings, and FFTW cache staged under/private/tmp.IC-7300MK2 hardware wake and reconnect UX
The revised Auto-mode flow was exercised on the test Mac with the radio in standby, TX pinned off, and an isolated settings copy. With wake off, identity remained unanswered. The status-bar connect control stayed visible and opened the connection panel; the checkbox was clickable. Enabling wake and reconnecting sent power-on to B6, identified IC-7300MK2, produced fresh serial/audio data, and disconnected cleanly with
transmitting=false. The operator also confirmed wake and autoconnect worked.The second standby run on final commit
10a9ee63completed in 9.29 seconds from Connect to verified identity, versus 15.48 seconds with the old ten-second pause. The trace contains exactly oneFE FE B6 E0 18 01 FDwake command, followed by identity probes approximately one second apart; the first valid identity arrived 3.939 seconds after the wake command. Fresh serial and audio data were verified, the status-bar control and checkbox remained usable, and cleanup disconnected the radio and released the test lock with TX disabled. The operator confirmed this faster run worked perfectly. The overall time includes approximately five seconds of initial identity discovery before wake. IC-9700 retains its measured delay. IC-705 physical wake remains untested.Earlier hardware evidence, separate from this update
At
cc3b8aa0, three fresh-process receive-only checks on the operator's IC-7300MK2 identified B6, preserved its custom name, initialized the audio route, received fresh audio, and disconnected cleanly. Model/audio readiness was 0.242 s, 0.238 s, and 0.236 s. All three received the first identity reply; dropped-first-reply recovery was covered separately by the injected timer test and its mutation check. Local TX streaming initialization did not mean RF transmission.The earlier three-start identity checks have not all been repeated on this update; the new MK2 standby/wake evidence is described separately above. No RF transmission was requested. The bridge is sufficient for the checkbox checks; physical standby/wake behavior still requires model-specific hardware validation. CI results are separate from the local evidence above. No merge is requested.
Generated with OpenAI Codex (GPT-6 Astra)