Skip to content

fix(radio): route receive intents through RadioModel::isConnected(), not the backend's own LAN-only link - #6125

Merged
Ozy311 merged 7 commits into
aethersdr:mainfrom
K5PTB:fix/wan-slice-command-drop
Oct 3, 2026
Merged

Ozy311 merged 7 commits into
aethersdr:mainfrom
K5PTB:fix/wan-slice-command-drop

Conversation

@K5PTB

@K5PTB K5PTB commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #6121. WAN/SmartLink receive edits reached a gate that checked only FlexBackend's undialed LAN connection, so tuning, mode, filter and AGC requests never reached the radio.

  • Gate receive intents on RadioModel::isConnected(), which checks LAN, WAN and backend connectivity. The WAN check also works without a LAN connection object. If a vended LAN connection is down, a connected backend can still establish connectivity; this includes the simulator's direct backend path.
  • Log a disconnected receive intent through aether.protocol. Do not emit commandDropped, whose UI consumer reports an unsupported control rather than a disconnected session.
  • Cover both a backend without a LAN connection and the real FlexBackend with its LAN connection undialed. Inject WAN connectivity without opening a socket. The real Flex fixture checks exact tune/mode/filter/AGC command strings through the injected slice command sink, disconnected refusal, warning output, and absence of the misleading UI signal.

No new protocol verb, capability, setting, dependency, or transmit path is added.

Constitution principle honored

Principle VIII — Evidence Over Assertion. The reporter bisected the regression to #5907 (142f1d63 versus e48a242f). Local mutation checks independently demonstrate that the regression suite detects the broken connectivity gates and diagnostics.

Validation

  • macOS arm64, pinned Qt 6.12.0: full app build and focused receive_intent_routing_test pass; 50 assertions. After incorporating main fix(radio): route VOX, monitor, speech processor, CW speed and break-in through the seam gate. Principle VIII. #6111, both the receive test and neighboring transmit_seam_drop_notice_test pass (2/2 CTests).
  • Four independently rebuilt mutants fail: backend-only receive gate; LAN check masking a connected WAN link; restored unsupported-control signal; removed disconnected warning. The correct implementation is restored and rebuilt afterward.
  • Agent automation bridge: isolated offscreen DEMO-0001, TX disabled. Tune/mode/filter/AGC readbacks pass for LSB and USB; disconnected tuning produces no unsupported-control status notice. Owned demo process stopped after verification.
  • Engine-boundary, touchpoint, capability-record and command-plane checks pass. PR-only output matched the original merge base; the final candidate also includes the upstream fix(radio): route VOX, monitor, speech processor, CW speed and break-in through the seam gate. Principle VIII. #6111 capability ratchet decrease from 71 to 70.
  • The submitter reports real-radio verification on a FLEX-6500 firmware 3.9.18 over WAN and a separate FLEX-6500 firmware 4.2.20 over LAN. This review did not repeat hardware or RF testing.
  • Full CTest suite was not rerun in this review. The submitter's earlier 620/621 result predates fix(hl2): update seam tests for the transmit tail. Principle XI. #6126, which repairs the unrelated HL2 seam-test expectations now included from main.

Checklist

  • Commits are signed
  • No new flat-key AppSettings calls
  • Clean-room implementation
  • No meter UI change or CHANGELOG edit
  • Regression tests use injected state/command capture, with no network peer

Original implementation generated with Claude Code. Additional review and regression coverage by Codex.

…not the backend's own LAN-only link

Fixes aethersdr#6121. Every slice-level receive intent (tune, mode, filter, AGC)
has been silently dropped for the entire lifetime of any WAN/SmartLink
session since PR aethersdr#5907 landed (142f1d6). RadioModel::receiveCommandSource()
gated dispatch on m_backend->isConnected(), which for Flex reports only on
FlexBackend's own internally-owned, LAN-only RadioConnection.
RadioModel::connectViaWan() never dials that object -- a WAN session is
driven entirely through the separate m_wanConn, bypassing the backend
abstraction -- so the gate reported "not connected" for the whole session
even though the radio was genuinely connected, and silently dropped every
intent with no warning.

- Routes receiveCommandSource()'s connectivity check through
  RadioModel::isConnected(), which already accounts for both m_connection
  (LAN) and m_wanConn (WAN), instead of m_backend->isConnected() alone.
- Fixes a latent bug in isConnected() itself, found during triage: the
  m_wanConn check was nested inside the m_connection branch, making it
  unreachable once a non-Flex family switch left m_connection null.
  Hoisted so m_wanConn is checked unconditionally.
- Makes the drop loud: a connectivity failure at this gate now emits the
  same qCWarning + commandDropped() shape sendCmd()'s hasCommandPlane()
  guard already uses for the equivalent dead-control case, so this class
  of bug surfaces in the log instead of requiring a bisect next time.
- Adds a regression test (tests/receive_intent_routing_test.cpp,
  wanFallback()) that reproduces the exact shape: a real WanConnection
  reporting connected while the backend's own link reports disconnected.
  The fixture never calls connectToRadio(), so no socket is opened --
  only the connection's already-private m_connected flag is set
  directly, through a new test-only friend (WanConnectionTestAccess)
  matching the existing *TestAccess pattern in this file.

Root cause, bisection, and the diagnosis this PR implements are in aethersdr#6121
and the triage comment on it.

Principle VIII -- Evidence Over Assertion. The root cause was confirmed
by building and testing both sides of PR aethersdr#5907's merge commit directly,
with aether.smartlink debug logging enabled for ground truth (not
inference from its absence, which cost real time during triage): zero
slice tune commands at 142f1d6, correct dispatch at its direct parent
e48a242, same radio, same WAN session. The new test reproduces the same
shape without a live radio.

Test plan:
- Local build passes (cmake --build build -j4, macOS arm64, Qt 6.12)
- receive_intent_routing_test passes, including the new wanFallback()
  case (and fails without the RadioModel.cpp fix, confirmed before
  adding it)
- Full ctest suite: 620/621. The one failure, hl2_backend_seam_test, is
  pre-existing on current upstream/main -- introduced by aethersdr#6104, touches
  HL2 TX-power/TUNE logic this diff never touches, and reproduces
  identically with this diff reverted.
- Reverified on two physical radios post-fix, confirmed by log in both
  cases (slice tune commands dispatched matching every TunePolicy
  event): FLEX-6500 firmware 3.9.18 over WAN/SmartLink (the originally
  broken configuration), and a separate FLEX-6500 firmware 4.2.20 over
  LAN. No crossover tested (3.9.18/LAN and 4.2.20/WAN were not
  available).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@K5PTB K5PTB left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review: PR #6125 (independent pass via .claude/skills/pr-review)

Verdict: Request changes (posted as a comment). There are 2 blockers, each with a verified drop-in fix below.

Issue fit. #6121 reports that every slice receive intent (tune/mode/filter/AGC) is silently dropped over WAN/SmartLink because receiveCommandSource() gates on m_backend->isConnected(). For Flex, that call only checks FlexBackend's own LAN RadioConnection, which connectViaWan() never dials. The core fix is correct and solves the issue: src/models/RadioModel.cpp:9801 now gates on RadioModel::isConnected(), which sees m_wanConn. Both blockers are in the parts added around that fix: the new "loud drop" and the regression test.

Scope

File / hunk Claimed in body? Verdict
RadioModel.cpp:9801 gate → RadioModel::isConnected() Yes In scope. This is the fix for #6121.
RadioModel.cpp:2691-2707 isConnected() hoist Yes (requested in the #6121 triage) In scope. It also changes one more case than the body describes (nit 1).
RadioModel.cpp:9801-9808 qCWarning + commandDropped Yes (requested in triage) In scope, but the commandDropped half is blocker 1.
RadioModel.h:1732 / .cpp:9777 drop const No Mechanical. Only needed for the emit, and goes away with the blocker 1 fix.
WanConnection.h:101 test friend Yes In scope. Matches the existing *TestAccess pattern.
tests/receive_intent_routing_test.cpp Yes In scope, but blocker 2.

Everything in the diff is explained by the issue. There's no new public or protocol surface, no settings keys and no CHANGELOG.md edit. The body's checklist holds: commit signature verified (verified: true), and no flat keys or CHANGELOG.

Blockers

1. Tuning while disconnected now tells the operator "This radio doesn't support that control." Reproduced in the app.
src/models/RadioModel.cpp:9805-9806 emits commandDropped whenever the radio isn't connected. That signal's only consumer is src/gui/MainWindow_Session.cpp:781-782 → showUnsupportedControlNotice() (:2918-2921). It was built for the no-command-plane case (HL2/Icom), per the comment at :769-772. Slices stay active between a disconnect and the next reconnect (m_stagingReceiveModels is only set inside stageSessionModelsForReconnect(), :6879), so an ordinary tune on a disconnected session now gives a false "unsupported control" message. The body says this is "the same shape" as sendCmd()'s hasCommandPlane() guard. The logging shape is the same, but the cause is different, and the UI message is wrong for this cause.

Bridge evidence: PR head, offscreen, isolated settings, DEMO-0001 only:

connect local serial DEMO-0001 / connect wait  → connected, serial DEMO-0001
(after connect)     statusMessage: ""
disconnect          → connected:false, slices still [(0, 14.1)]
(after disconnect)  statusMessage: ""
tune 14.250         → {"ok":true,"sliceId":0,"tune":14.25}
(after tune)        statusMessage: "This radio doesn't support that control — nothing was sent to the radio. Further unsupported controls are logged."
log tail            → "RadioModel: not connected, dropping slice 0 receive intent"   (exactly one, after the tune)

Fix: keep the warning, which is what makes this class of bug visible in a log, and drop the emit. This also lets receiveCommandSource() go back to const (RadioModel.h:1732, RadioModel.cpp:9777).

    if (!isConnected()) {
        qCWarning(lcProtocol).noquote()
            << "RadioModel: not connected, dropping slice" << source->sliceId()
            << "receive intent";
        return nullptr;
    }
    SliceModel* receiveCommandSource() const;

If a UI notice for disconnected drops is wanted, it needs its own signal and wording. That would be a separate change.

2. The regression test doesn't exercise the #6121 shape, and a mutant that brings the bug back passes it.
Fixture installs the stub with setBackendForTest() (tests/receive_intent_routing_test.cpp:128), which tears down without aliasing a RadioConnection. So in wanFallback() (:344), m_connection is null. The test only reaches the hoisted m_wanConn branch, the non-Flex shape the body itself calls latent. In production #6121, m_connection is FlexBackend's own RadioConnection: it exists, but it's down. The body's "reproduces the exact shape" claim doesn't hold, and the #6121 triage asked specifically for "a RadioModel whose vended m_connection reports disconnected while a WAN connection reports connected".

Mutation results (rebuilt from a clean git checkout of RadioModel.cpp each time):

RadioModel.cpp mutant PR test result
revert the gate to m_backend->isConnected() FAIL (good)
revert the hoist to the old nesting FAIL. The test depends on the null-m_connection branch.
if (m_connection) return m_connection->isConnected(); before the WAN check. This brings #6121 back for every Flex WAN session. PASS. The regression is not pinned.

A socket-free seam for the real shape already exists: rebuildBackendForTest("flex") builds a real FlexBackend whose RadioConnection is never dialed. tx_operation_integration_test.cpp:1887 already uses it. Drop-in, added after wanFallback() and called from main() after wanFallback();:

void wanOverUndialedFlexConnection()
{
    RadioModel radio;
    check(radio.rebuildBackendForTest(QStringLiteral("flex")), "RadioModel builds a FlexBackend");
    WanConnection wan;
    RadioModelSliceLifecycleTestAccess::setWanConnection(radio, &wan);
    WanConnectionTestAccess::setConnected(wan, true);
    check(radio.backend() && !radio.backend()->isConnected(), "the FlexBackend's own link is down");
    check(radio.isConnected(), "a live WAN link connects the model despite the undialed LAN link");
    WanConnectionTestAccess::setConnected(wan, false);
    RadioModelSliceLifecycleTestAccess::setWanConnection(radio, nullptr);
}

Verified: 43/43 pass on the PR head. The third mutant above now fails at "a live WAN link connects the model despite the undialed LAN link". Keep wanFallback() as well, since it is what pins the gate change.

Nits (non-blocking)

  1. The isConnected() rewrite changes one case the body doesn't mention (RadioModel.cpp:2702-2706). When m_connection exists but is down and there's no WAN, the old code returned false. The new code falls through to m_backend->isConnected(). That's identical for Flex (FlexBackend.cpp:428-430), but SimBackend can diverge: connectRadio() sets m_connected without its RadioConnection (SimBackend.cpp:380-392). This has many callers, so either say so in the body or keep the old semantics and hoist only the WAN term. The version below passes both wanFallback() and the drop-in above:
        if (m_wanConn && m_wanConn->isConnected())
            return true;
        if (m_connection)
            return m_connection->isConnected();
        return m_backend && m_backend->isConnected();
    Also, the hoist's "latent bug" doesn't look reachable through the UI. startWanRadioConnect() declines when there's no PanadapterStream (MainWindow.cpp:888), and m_connection/m_panStream are aliased and cleared together (RadioModel.cpp:831-832, 855-856, 1747-1748). It's a fine defensive change, but the body overstates it.
  2. Comments narrate history (AGENTS.md §Comments: "History does not go in code… Aim for five lines or fewer"). Examples: RadioModel.cpp:2694-2698 ("the old nesting made that combination unreachable"), the 10-line block at :9791-9800 ("died here silently for the whole lifetime…", "loud, it is caught the first time…"), and receive_intent_routing_test.cpp:3-6, 26-28, 34-37, 338-343. For example, :9791-9800 could be:
        // RadioModel::isConnected(), not m_backend->isConnected(): a WAN session
        // never dials FlexBackend's own RadioConnection (#6121).
  3. Nothing asserts the new warning. With blocker 1 applied, QTest::ignoreMessage(QtWarningMsg, ...) in the "neither link connected" step would pin it (the target already links Qt6::Test). Separately, VFO-wheel tuning while disconnected logs one warning per step. Consider whether qCWarning is the right level for an expected state.

What I tried to break that held

  • Core gate fix: verified. Reverting only the gate fails the PR's test. The send path below the gate routes WAN first (RadioModel.cpp:9683-9684).
  • LAN and HL2 paths: reasoned from code. LAN is unchanged because m_connection->isConnected() still decides. HL2 is unchanged because m_connection and m_wanConn are both null, so the backend still decides.
  • Test-layer boundary: the new fixture constructs a WanConnection (it holds a QSslSocket member), but connectToRadio() is never called. The destructor's disconnectFromRadio() writes only when the socket isn't UnconnectedState (WanConnection.cpp:180). So this is an inert value, not a socket-owning test, and no notify is needed.
  • Fixture lifetime: wan is a stack object destroyed before Fixture, and the test nulls m_wanConn first. No dangling pointer.
  • Engine boundary / touchpoints: tools/check_engine_boundary.py gives identical per-file output on the merge base fe1fec51 and the head (102 findings, 0 blocking). gen_touchpoint_manifest.py --check is up to date. #5554 notice: no family branch, no dynamic_cast, no new capability.
  • TX safety (Principle VI): the change only re-enables receive intents and adds no keying path.
  • CI: the runs were created at 18:16:35Z, after main's tip a3f7b625 (18:12:08Z), so they test the current merge. At review time, Static checks and Sanitizer had passed, and build / check-macos / check-windows were pending.

Not verified: the real-radio WAN/LAN results in the body (needs hardware), the full-suite 620/621 claim, and the hl2_backend_seam_test attribution to #6104. The automated code-review pass wasn't run; the equivalent checks above were done by hand. The bridge session used a scratch build of the PR head, offscreen, with isolated AETHER_SETTINGS_DIR, AETHER_AUTOMATION_NO_TX=1, connected only to DEMO-0001, and was closed afterwards. The merge-base GUI wasn't driven. That blocker 1 is new in this PR is reasoned from the diff: the base gate returned nullptr with no emit.

…I notice, pin the real aethersdr#6121 shape

Two blockers from the independent review on aethersdr#6125:

1. receiveCommandSource() emitted commandDropped() on a disconnected-but-
   unowned tune, which MainWindow_Session.cpp's only consumer surfaces as
   "This radio doesn't support that control" -- correct wording for the
   no-command-plane case that signal was built for, wrong for this one
   (slices stay active between a disconnect and the next reconnect, so an
   ordinary tune on a disconnected session now gave a false notice).
   Keeps the qCWarning, which is what makes the drop visible in a log;
   drops the emit. receiveCommandSource() goes back to const now that it
   no longer needs to emit.

2. wanFallback() only reached the hoisted m_wanConn branch through the
   null-m_connection shape (RadioModel::setBackendForTest()'s stub never
   aliases a RadioConnection), not aethersdr#6121's actual shape: a real
   FlexBackend whose own RadioConnection exists but is never dialed.
   A mutant that re-nests the WAN check inside `if (m_connection)` --
   bringing aethersdr#6121 back for every Flex WAN session -- passed the test
   suite before this fix. Added wanOverUndialedFlexConnection(), built
   the same socket-free way tx_operation_integration_test.cpp's
   rebuildBackendForTest() cases are: a real FlexBackend, never dialed,
   alongside an undialed WanConnection reporting connected. The same
   mutant now fails at "a live WAN link connects the model despite the
   undialed LAN link" -- verified by hand before reverting it back to
   the fix. Kept wanFallback() too, since it still pins the gate change
   itself.

Nits from the same review (comment length/history, no warning assertion,
the isConnected() case the body didn't mention) are left as-is, per
request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@K5PTB

K5PTB commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Both blockers addressed in f670dd82.

Blocker 1 (false "unsupported control" notice). Dropped the emit commandDropped(...) entirely; kept only the qCWarning, exactly as suggested. receiveCommandSource() is back to const now that it no longer emits anything. Confirmed the diff matches the drop-in fix in the review comment.

Blocker 2 (test didn't pin the real shape). Added wanOverUndialedFlexConnection() after wanFallback(), using rebuildBackendForTest("flex") the same way tx_operation_integration_test.cpp builds its socket-free FlexBackend cases — a real, never-dialed FlexBackend connection alongside an undialed WanConnection reporting connected. That's the shape #6121 actually hit (own RadioConnection exists but is down), not the null-m_connection case wanFallback() alone reached.

Reproduced the review's own counter-evidence before trusting the fix: re-applied the exact mutant from the review (re-nesting the WAN check inside if (m_connection), which silently brings #6121 back for every Flex WAN session) against the strengthened suite. It now fails at "a live WAN link connects the model despite the undialed LAN link" — the mutant that previously passed 43/43 no longer does. Reverted the mutant back to the real fix before committing.

Kept wanFallback() alongside the new test, since it still pins the gate change on its own (the null-m_connection case is a real, if narrower, shape).

Left the three nits as-is per request — not re-litigating those here, but happy to take another pass at them if preferred.

Verified after the fix:

🤖 Generated with Claude Code

@K5PTB
K5PTB marked this pull request as ready for review October 3, 2026 18:54
@K5PTB
K5PTB requested a review from a team as a code owner October 3, 2026 18:54
@ten9876 ten9876 self-assigned this Oct 3, 2026
@Ozy311 Ozy311 assigned Ozy311 and unassigned ten9876 Oct 3, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review of becd2e9f8f5fbf2aefce7d4280aa05e594e01a08: the fix addresses #6121; no remaining blockers found. The receive gate now accepts the session's WAN connectivity, and the existing Flex command sink routes the admitted intents through WAN. Disconnected requests remain refused and logged without the incorrect unsupported-control signal. The two findings in the earlier review are resolved.

Scope

File Change / claimed? Verdict
src/models/RadioModel.cpp WAN-aware admission, combined connectivity, disconnected warning; yes In scope
src/core/backends/flex/WanConnection.h Friend for injected test state; yes In scope; no network peer or public protocol addition
tests/receive_intent_routing_test.cpp Stub and real-Flex WAN coverage, exact commands, refusal and diagnostics; yes In scope

Everything in the PR diff is explained by the issue and updated body. The merges from main introduce no unrelated PR-specific changes. No settings keys, capabilities, protocol verbs, dependencies, transmit paths, UI preferences, or CHANGELOG entries were added. The updated body accurately describes the broader connected-backend fallback. Commit dates show implementation, review remediation, and subsequent main merges rather than an unrelated cherry-pick.

Blockers

None.

Nits — non-blocking

  1. src/models/RadioModel.cpp:2697–2700: add braces to the two edited if statements, per AGENTS.md's “Braces on all control flow.” Drop-in replacement:

        if (m_connection && m_connection->isConnected()) {
            return true;
        }
        if (m_wanConn && m_wanConn->isConnected()) {
            return true;
        }

Verification and attacks

  • Built the full Linux/Qt 6.12 app at b0c11b43; after the subsequent push, built and ran the expanded focused test at becd2e9f. Production changes between these heads are comment shortening; the other upstream change is to an HL2 test. The expanded receive-routing test passes. Preflight confirmed the WAN/Flex socket objects remain undialed: no bind, connect, or synthetic firmware peer is executed.

  • Independently rebuilt mutations: restoring the backend-only gate failed WAN dispatch; allowing a disconnected LAN object to mask WAN failed the real-Flex connectivity assertion. Those were checked on b0c11b43, with the same production logic and assertions retained in the latest head. On becd2e9f, removing the warning failed a disconnected receive edit emits a protocol warning; restoring commandDropped failed disconnected receive edits never emit an unsupported-control notice. Restored the exact head and reran successfully.

  • Drove the b0c11b43 app offscreen through explicit socket /tmp/aether-pr6125-codex, isolated settings, DEMO-0001 only, and txAllowed:false. tune 14.25, slice mode LSB, slice filter -2400 -100, and slice agc slow produced this readback (selected fields):

    {"frequency":14.25,"mode":"LSB","filterLow":-2400,"filterHigh":-100,"agcMode":"slow"}

    After disconnect, get radio reported connected:false; tuning to 14.26 produced no unsupported-control text in the widget tree. After reconnect and waiting for slice creation, the readback was frequency:14.27. All owned demo processes were stopped. No live radio was connected and no transmitter was keyed.

  • Existing focused coverage also survived stale/reused slice IDs, reconnect staging, off-thread intents, reentrant edits, and disconnected refusal. Source inspection confirmed the Flex sink still routes WAN first and preserves the existing TX-inhibit checks.

  • All four boundary gates passed with byte-identical output on latest merge base 2394c232 and head becd2e9f: engine boundary 102 tracked findings/zero blockers, capability count 71, command plane 401 across 32 files, and current touchpoint manifest. No baseline or manifest expansion. An independent parallel audit agreed. The separate automated code-review skill was unavailable.

Limits: demo cannot reproduce the real SmartLink transport failure; the WAN proof here is injected-state/command-sink coverage and mutation evidence. I did not repeat the submitter's hardware tests, full-suite result, or macOS build, and did not drive a merge-base GUI. The bridge verifies the unchanged production behavior from before the final test/comment-only push, not a newly linked final-head GUI.

Recommendation

Approve with nits, subject to required CI and owner review. Posting COMMENT rather than an approval, as requested by the review workflow. At the final check, Static checks and sanitizer configuration passed; the three build jobs were pending. CI was created at 19:21:16–17 UTC, before main advanced to 579efd73 at 19:25:50 UTC, so it does not validate that newer main; the non-strict branch policy does not force a rerun. Check that gap during landing.

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review: PR #6125 at becd2e9f

Verdict: no blockers. Posted as a comment; approval is the maintainer's call.

Issue fit. #6121: since #5907, every slice receive intent (tune, mode, filter, AGC) is dropped silently for the whole of a WAN/SmartLink session. receiveCommandSource() gated on m_backend->isConnected(), and for Flex that only reports FlexBackend's own LAN RadioConnection, which connectViaWan() never dials. The PR moves the gate to RadioModel::isConnected() (src/models/RadioModel.cpp:9786), which also checks m_wanConn. I checked that the send path below the gate reaches the WAN link. FlexBackend::requestSliceTune / setSliceMode / requestSliceFilter / requestSliceAgc all go through sendSlice() → the RadioModel sink → sendCmd(). sendCmd() routes to m_wanConn first, and none of those methods checks connectivity again. wanOverUndialedFlexConnection() now captures the actual Flex command strings, which pins that whole path. The PR solves the issue. Both earlier blockers are fixed, and since c025c8fb the history comments are trimmed, the body is current, and the warning is asserted.

Scope

File / hunk Claimed in body? Verdict
RadioModel.cpp:9786 gate → RadioModel::isConnected() Yes In scope. This is the fix for #6121.
RadioModel.cpp:2691-2701 isConnected() checks each link separately Yes, including the fall-through to the backend In scope. Not needed for the Flex fix (nit 1).
RadioModel.cpp:9786-9790 qCWarning on a disconnected drop, no commandDropped Yes In scope.
WanConnection.h:101 friend class WanConnectionTestAccess Yes In scope. Test-only and private. Matches the existing *TestAccess pattern.
tests/receive_intent_routing_test.cpp Yes In scope.
Merge commits b0c11b43, becd2e9f n/a Bring in main only.

Everything in the diff is explained by the issue. There's no new public or protocol surface, no settings key, no CHANGELOG.md edit and no CI file change. The body's checklist holds; head commits are signed (verified: true).

Blockers

None.

Nits (non-blocking)

  1. The isConnected() change isn't needed for #6121, and wanFallback() asserts the one state it changes (src/models/RadioModel.cpp:2697-2701, tests/receive_intent_routing_test.cpp:334).
    • Not needed for the Flex fix. In the real Flex WAN shape, m_connection is FlexBackend's undialed connection: non-null, not connected. The old nested m_connection->isConnected() || m_wanConn->isConnected() already returned true there. Mutant 3 below (the merge base's isConnected()) still passes all of wanOverUndialedFlexConnection(), including the command-string check. It fails only wanFallback().
    • Where it does change behavior. It only matters when m_connection is null and WAN is live. In production that means a non-Flex backend is still loaded, so dispatching intents to it is the wrong result, and wanFallback() asserts exactly that. I couldn't reach that state: startWanRadioConnect() refuses when there's no PanadapterStream (src/gui/MainWindow.cpp:888), and only Flex and Sim provide one. So it causes no harm today, but the test pins behavior nobody wants.
    • Suggestion. Either drop the isConnected() hunk and wanFallback() (the gate change plus wanOverUndialedFlexConnection() is the whole fix), or change wanFallback() to assert that intents don't reach a non-Flex backend.
    • Related, and not caused by this PR. connectViaWan() (RadioModel.cpp:3704) never switches to the Flex family. A SmartLink connect right after a demo session leaves SimBackend loaded with its own synthetic m_connection. The gate then hands intents to SimBackend, which ignores them because it's disconnected (SimBackend.cpp:493). Flex status decoding is missing there too, because FlexBackend is never built. It existed before this PR, so it's worth its own issue, not this PR.
  2. The warning fires on every intent while disconnected (RadioModel.cpp:9787). Slices are kept for reclaim after an unexpected drop. So a held VFO wheel, or a CAT/TCI client such as WSJT-X writing frequency during the reconnect window, logs one W aether.protocol line per write at default log level. In the app, three tunes after disconnect gave three lines. That's fine for finding the bug; consider rate-limiting it, or logging once per disconnect.

What I verified, and what I tried to break

  • Mutation testing at becd2e9f (receive_intent_routing_test rebuilt each time; restore confirmed by sha256 against git show HEAD:). Unmutated head: 50/50 pass.

    RadioModel.cpp mutant Result
    1. Only the gate reverted to m_backend->isConnected() FAIL: wanFallback and "WAN receive edits reach the real Flex command sink…"
    2. if (m_connection) return m_connection->isConnected(); before the WAN check FAIL: "a live WAN link connects the model…" and the Flex sink check
    3. The merge base's original isConnected() FAIL: wanFallback only. The Flex shape passes (nit 1).
    4. qCWarning removed FAIL: "a disconnected receive edit emits a protocol warning"
  • App session (scratch build of f670dd82; every RadioModel.cpp change since then is a comment, checked with git diff f670dd82 becd2e9f). Offscreen, isolated AETHER_SETTINGS_DIR / HOME, AETHER_AUTOMATION_NO_TX=1, explicit socket. Connected only to DEMO-0001 (get radio → "model": "AetherSDR Demo", family sim), closed afterwards.

    tune 14.250 (connected)    → slice frequency 14.25, no drop logged
    disconnect                 → connected:false, connectState "idle", slice 0 retained
    tune 14.100/14.101/14.102  → ok; log seq 742/745/748:
                                 W aether.protocol RadioModel: not connected, dropping slice 0 receive intent
    dumpTree                   → no "doesn't support that control" text anywhere
    reconnect, tune 7.150      → slice frequency 7.15, no drop logged
    
  • The fall-through to the backend (now disclosed in the body): ran 8 sim-family RadioModel tests on the PR head, all pass: demo_backend_swap_test, backend_family_switch_test, radiomodel_audio_mute_test, control_receive_test, backend_receive_contract_test, radio_setup_recording_mode_dim_test, qso_record_start_policy_test, automation_boundary_core_test. In production SimBackend's own connected/disconnected handlers update m_connected before RadioModel sees the edge, because they are connected first.

  • Sibling gates: in src/, m_backend->isConnected() appears only in RadioModel::isConnected()'s own fallback and in DroopCalibration.h:14, which is ANAN-only. That's still true on current main after #6111, which adds no new gate on the LAN-only link.

  • Engine boundary / touchpoints: tools/check_engine_boundary.py reports 102 findings, 0 blocking. gen_touchpoint_manifest.py --check is up to date. #5554 notice: no family-string branch and no new capability. The test's qobject_cast<FlexBackend*> is test-only.

  • Test-layer boundary (step 0): the tests construct a WanConnection and, through rebuildBackendForTest("flex"), a RadioConnection. Neither is dialed: no bind, listen or connectToHost. Commands are captured through setSliceCommandSink. These are inert values, so no operator notification was needed.

  • Principle VI: the change only re-enables receive intents and adds no keying path.

  • Automated pass: I ran /code-review at medium. I checked its point that the isConnected() change isn't needed for Flex and that wanFallback pins the null-m_connection shape; it's in nit 1. It also claimed intents would route to a stale HL2 backend after an HL2 to SmartLink switch. I dropped that because MainWindow.cpp:888 refuses that connect. Its log-flooding point is nit 2.

  • CI: the runs on becd2e9f were created at 19:21:16Z. Since then main has moved to 579efd73f (19:25:50Z, #6111, TX seam gates, which touches RadioModel.cpp). git merge-tree against current main is clean, and #6111 doesn't touch the receive gate or isConnected(). Static checks have passed; build / check-macos / check-windows were still running when I posted this.

Not verified: a real SmartLink session. The demo has no WAN path, so the WAN shape is proven by the unit tests and by reading the send path. The real-radio WAN and LAN results in the body need hardware. I didn't run the full suite.

@Ozy311
Ozy311 enabled auto-merge (squash) October 3, 2026 19:36

@Ozy311 Ozy311 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the final candidate at 70b11dca9544cda9e1dcab13ede1a92295bbb849 against main 579efd73f65dbba0bc96eef93dc24f34a9a25060. The change addresses #6121: receive intents use the model's combined connection state, so an active WAN session is not rejected because FlexBackend's LAN connection is undialed. No remaining functional blocker was found.

Scope

File/group Claimed in PR Verdict
src/models/RadioModel.cpp Combined connection predicate and logged disconnected drops In scope; no new protocol, settings or TX path
src/core/backends/flex/WanConnection.h Test-only access to connected state In scope; no runtime behavior change
tests/receive_intent_routing_test.cpp WAN routing and disconnected regression coverage In scope; injected state and command sink, no opened socket or firmware peer

Findings and disposition

  • The false unsupported-control notice from the earlier review is absent at f670dd82. Commit c025c8fb additionally asserts that disconnected edits never emit commandDropped and that the protocol warning remains. Reintroducing the signal or removing the warning each fails its assertion.

  • The real undialed Flex connection case added at f670dd82 is retained. c025c8fb extends it through the real Flex encoder, asserting the exact tune, mode, filter, AGC mode, AGC threshold and AGC off-level commands, plus refusal when both links are down. Both the backend-only gate and a LAN-first early return fail the strengthened suite.

  • The historical-comment nit is addressed with concise invariant comments. The PR body now accurately describes warning-only behavior and the backend fallback when a vended LAN link is down. No demonstrated non-WAN regression was found; the simulator bridge check passes.

  • The later braces nit is fixed by 70b11dca; both edited connection checks now follow the project style. The complete app, both focused tests and the 24-request demo check were rerun on that commit.

  • The later non-blocking scope observation is valid: the model connectivity refactor is broader than the minimal Flex gate fix. The PR explicitly discloses and tests the combined-link predicate; no reachable new regression was demonstrated. A non-Flex-to-SmartLink family switch is a pre-existing session lifecycle concern, not established as caused by this change, so this PR does not expand into that redesign.

  • Per-intent disconnected warnings are retained as the chosen diagnostic behavior. Rate limiting is a possible follow-up; it is not needed to restore WAN commands or eliminate the false unsupported-control notice, and the reviewer classified it as non-blocking.

Verification

  • Final macOS arm64 / Qt 6.12.0 app build passes. Focused CTest: 2 selected, 2 passed (receive_intent_routing_test, transmit_seam_drop_notice_test); receive executable: 50 checks passed.
  • Four separately rebuilt mutants all fail: backend-only gate, LAN masking WAN, false unsupported-control notice, missing warning. Restored source rebuilt and retested before publication.
  • Four architecture checks pass. PR-only output matched the original merge base before the main refresh; the final checks include the upstream #6111 capability ratchet decrease to 70: engine boundary (102 tracked findings, 0 blockers), generated touchpoints, capability records and command plane. No new family branch, concrete-backend cast in production, vendor command above the seam, or capability is introduced.
  • Final agent automation bridge run used isolated settings, offscreen DEMO-0001, explicit local socket and txAllowed:false. At 14.250 MHz: LSB, filter -2400/-100, AGC slow/42. At 14.260 MHz: USB, filter 200/2600, AGC fast/55. After disconnect and a tune request, the widget tree contains no unsupported-control status message. All 24 requests completed successfully; owned PID 65475 was stopped.
  • All seven PR commits are GitHub Verified. Main was incorporated through 579efd73; there are no unresolved review threads or outstanding changes-requested reviews.

The automated code-review skill was unavailable; this was a manual adversarial pass. Live WAN/LAN hardware, RF/TX and the full CTest suite were not rerun by this reviewer. The submitter's physical-radio evidence remains separately attributed in the PR body. The demo cannot reproduce the SmartLink TLS transport itself; the WAN gate is demonstrated by socket-free injected connectivity and command capture.

Landing state

Approval is for the reviewed code and demonstrated local behavior; required GitHub CI is still pending and is not claimed green. Linux and Windows are building, while macOS and Static Checks are queued. Auto-merge is armed and must wait for those required checks. The final head's earliest runs were created at 2026-10-03 19:41:21 UTC, after the current main tip 579efd73 at 19:25:50 UTC; main is included in the verified head. No unresolved threads or changes-requested reviews remain. The operator explicitly authorized taking assignment and enabling auto-merge.

Reviewed with Codex using the repository's PR review and fb workflows.

@Ozy311
Ozy311 merged commit d9de044 into aethersdr:main Oct 3, 2026
5 checks passed
@K5PTB
K5PTB deleted the fix/wan-slice-command-drop branch October 3, 2026 20:27
ten9876 added a commit that referenced this pull request Oct 4, 2026
…lay, Qt 6.12 (#6159)

## Summary

Six-file release prep for **v26.10.1**, metadata and docs only. No
source, test or CI changes.

Cutoff: `origin/main` at `18c5245e5` (#6155, merged 2026-10-04).
Anything that merges after it is folded in before the tag.

**95 merged changes** (95 PRs, no direct commits) from **12 human
contributors**, plus AetherClaude (2) and Dependabot (0). There are no
first-time contributors this cycle. That was checked for each login by
looking for a merged PR before v26.9.5's tag, not from
`author_association`.

## Changelog

Built from `v26.9.5..18c5245` mapped through the commit-to-PR API
(`repos/…/commits/<sha>/pulls`), not from commit subjects. All 95 PRs
are cited in the section. The notes lead with three things:
- the sweep that makes every control on a radio without a Flex command
plane either work or refuse with a reason (#6044, #6049, #6015, #6111);
- the CTR2 relay (#6090, #6114, #6155) and authenticated 4O3A
connections with a Peripherals page (#6008, #6027);
- the Hermes-Lite 2's squelch, APF, AGC-off, RIT/XIT and FFT AVG.

The structural half covers Qt 6.12 for every binary and source build
(#5842, #6055) and the slice receive controls moving behind the seam
(#5892, #5907, #5919; raw Flex commands above the seam 413 → 369,
confirmed with `check_command_plane.py` at the cutoff). Upgrade-visible
changes are called out with **On upgrading** inline: the Ulanzi default
and removed MOX/TUNE pills (#5964), auto RF gain arming and law (#5976,
#6062), DIGU/DIGL opening with AGC off (#6119), HL2 squelch calibration
and the Auto SQL fallback (#6127), and the macOS 14.4 floor (#5842).

The previous tag v26.9.5 is an ancestor of `main`, so nothing is
excluded from the range.

## Docs refresh, not just version strings

- **README:** Highlights "Broad hardware control" gains the CTR2 relay.
Supported Hardware says direct TGXL/PGXL/Antenna Genius connections
authenticate. The Other radio families paragraphs gain the HL2's
squelch, APF, RIT/XIT, FFT averaging and CL1 reference, the ANAN-G2's
receiver audio and radio speaker, and RTL-SDR's confirmed receive state,
capture browsing and zoom FFT. In the Roadmap section, the aetherd line
gains the receive controls and a CTR2 relay line is added. No maturity
label changed. The six backends and seven NR engines were re-checked
against `src/core/backends/` and `AetherRxProfiles.cpp`.
- **ROADMAP, In flight:**
  - aetherd: the receive controls behind the seam.
- Icom: TUNE power live, no false drop notices, the one-panadapter
correction. The stale "VOX and CW break-in" leaves Remaining:
`IcomCivBackend` has implemented both since #4799/#5113.
  - ANAN-G2: receiver audio and radio speaker.
  - RTL-SDR: M1, with Remaining restated from #5924's body.
- Hermes-Lite 2: the v26.10.1 control parity, and the CW latency cost
updated to 4096 taps.
  - A new **CTR2 controller relay** entry.
  - Nothing left In flight as finished.
- **ROADMAP, Open RFCs:** diffed against `label:rfc`. #5893 leaves
(closed). #6106 and #6079 join under Radios, protocol and devices.
- **ROADMAP, Recently shipped:** 8 new `(v26.10.1)` entries. The list is
trimmed to the v26.10.x cycle at the month rollover, and its intro names
v26.10.x. Everything trimmed is still in CHANGELOG.

## Judgment calls flagged for the maintainer

- **Recently shipped trimmed to v26.10.x at the rollover:** maintainer's
call. This makes the validator's "only the four current-version lines
lose `26.9.5`" check FAIL. The lines it lists are exactly the trimmed
`(v26.9.5)` entry markers, and there is no historical mention elsewhere.
That is the one FAIL below. One side effect: the trim removes ROADMAP's
last mentions of #5630 and #5684, approved RFCs that shipped in v26.9.4.
Both are now closed as completed, and both remain in CHANGELOG.
- **A CTR2 relay In-flight entry:** maintainer's call. Wi-Fi works
today. USB's host side shipped and has carried the radio's status stream
to the firmware author's development firmware; it waits on a CTR2 USB
firmware release.
- **Intro wording with zero Dependabot updates:** "AetherClaude and no
dependency updates within that total" keeps the house shape and passes
the count check. The Contributors block has no Dependabot sentence.
- **#6125 is filed under the seam section.** It fixes a WAN/SmartLink
receive regression introduced by #5907 inside this range, so released
builds never had the regression. The section states what ships: receive
intents gate on `RadioModel::isConnected()`.
- Ready for review, not draft: maintainer's call.

## Scope

| file | change |
|---|---|
| `CMakeLists.txt` | `project(AetherSDR VERSION 26.10.1)` |
| `README.md` | Current version line, plus the prose refresh above |
| `AGENTS.md` | Current version line |
| `CHANGELOG.md` | new `[v26.10.1] — 2026-10-04` section; `[Unreleased]`
stays and stays empty; history below byte-identical |
| `packaging/linux/io.github.aethersdr.aethersdr.metainfo.xml` |
`<release version="26.10.1" date="2026-10-04"/>` first in `<releases>` |
| `ROADMAP.md` | cycle heading → post-v26.10.1, plus the prose refresh
above |

## Validation

`check_release_prep.py --mapping mapping.json`: 29 checks, **1 FAIL**
(explained above), 0 WARN.

- All six version spots say 26.10.1. The CHANGELOG heading date equals
the metainfo date, 2026-10-04.
- `appstreamcli validate` passed. The `<release>` list is
date-descending with unique versions (22 entries).
- `[Unreleased]` stays empty, `[v26.10.1]` sits directly under it, and
the CHANGELOG diff is exactly the inserted section, with history
byte-identical to `origin/main`.
- All 95 PRs in the range are cited. The only other numbers cited are
#3849 (RFC), #5262 (tracking issue), #5468 (RFC), #6091 (RFC, approved
and closed) and #6142 (open follow-up issue for #6119).
- The intro (95/12), the Contributors counts (13 entries) and
"first-time: none" are re-derived from the mapping, and they match.
- ROADMAP `(vX)` markers lead with v26.10.1. Every local link in README
and ROADMAP resolves. No removed heading is linked elsewhere. Code
fences balance, with no duplicated table headers or doubled `---`.
- **FAIL:** "only the four current-version lines lose `26.9.5`". The
lines are the trimmed Recently-shipped markers; see the judgment calls
above.
- `git diff --check` is clean. The diff touches no `src/`, `tests/`,
`third_party/` or `.github/workflows/` path.
- Configure: `cmake -S . -B <scratch>` reports
`CMAKE_PROJECT_VERSION=26.10.1`. No build or test was run, because this
is metadata only; CI builds it.
- `main` CI at the cutoff: `CI` passed on `18c5245e5`.

After merge: tag the resulting **main** commit, not this branch.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants