Repository navigation
feat(ctr2): AetherSDR starts the USB link handshake. Principle VIII. - #6114
Conversation
ten9876
left a comment
There was a problem hiding this comment.
Issue fit
Partially meets the stated intent; request changes. Reviewed head 3712bb55fcfcc722d53ac1e0fd3027b2eae2c15f against merge base c7513e125b0675308407bbe3e8f49ea1244944b8. There is no closing issue; RFC #6091 supplies the approved independent-client relay scope. The new Start → HELLO → device READY path implements the firmware-author request, but restarting is admitted before the previous close/back-off or drain has completed. These are correctness problems, not objections to the requested workflow. Jeremy retains the decision on the changed external HELLO/READY semantics; the PR explicitly discloses them.
Scope
| File/group | Change | Claimed? | Verdict |
|---|---|---|---|
src/core/Ctr2UsbRelay.{cpp,h} |
Host calls, accepts READY, retries/back-off | Yes | In scope; findings below |
src/core/Ctr2HidFraming.h |
Direction/counter comments | Yes | In scope; no codec implementation change |
tests/ctr2_usb_relay_test.cpp |
Handshake, retry, queue and late-answer coverage | Yes | In scope; socket execution boundary below |
docs/ctr2-usb-relay-design.md |
Updated host/device contract | Yes | In scope |
tools/ctr2-firmware-reference/{README.md,ctr2_link.h} |
Firmware usage example and comments | Yes | In scope; C implementation unchanged |
Everything in the diff is explained by the stated change. The single commit's authored date is 2026-10-03; no unrelated commits, UI restyles, settings, dependencies, capability changes, or enforcement-baseline growth. The framing/type values are unchanged, while their handshake semantics deliberately change. The body's claims that CLOSED retention and closing behavior remain unchanged do not hold for the sequences below.
Blockers
-
[P2] A delayed READY discards undelivered CLOSED and bypasses failure back-off —
src/core/Ctr2UsbRelay.cpp:568–569.sessionEnded()setsm_awaitingDevice=trueimmediately on refusal, before CLOSED is delivered or another HELLO is sent. Any delayed READY from an earlier HELLO therefore entersonDeviceStart(), whosediscardOutput()deletes that pending CLOSED and immediately opens another radio connection. This is possible after multiple host HELLOs produce multiple device answers: an answer arriving after the first connection fails is treated as permission for a new attempt. It regresses the previous CLOSED-retention behavior and defeats the advertised five-second retry delay.Reproduced without sockets: injected HID port holds outgoing reports; injected
QTcpSocket::connectToHostonly scheduleserrorOccurredand never calls the network implementation. After one refusal, inject a second READY before acknowledging CLOSED:PR head: before late READY: connections=1 queued=1 type=3 after late READY: connections=2 queued=0 FAIL: late READY preserves pending CLOSED FAIL: late READY does not bypass retry backoff Merge base: before late READY: connections=1 queued=1 type=3 after late READY: connections=1 queued=1The probe uses READY for initiation on the head and falls back to the base's supported device HELLO initiation on the base; the refusal and subsequent late READY are identical. The existing socket-free
ctr2_usb_closed_retention_testpasses on both revisions and misses this new entry point.Fix: distinguish waiting to issue a fresh HELLO from waiting for its answer. Ignore READY during failure back-off and while the prior CLOSED remains undelivered; only admit it for an eligible new handshake. Keep explicit device HELLO's force-restart semantics. Extend the existing injected-transport retention test with this sequence and mutation-check that guard.
-
[P2] Host retries can abort the previous link's graceful drain —
src/core/Ctr2UsbRelay.cpp:558–563.Reasoned from code, not runtime reproduced. Device CLOSED marks
m_awaitingDevice=true, callsdeviceClosed(), and starts a one-second HELLO timer even when the old session is still draining accepted device-to-radio bytes.sendHello()checks outstanding HID output, but not the existing session or radio write queue. With no outbound HID reports and radio writes still pending, HELLO goes out, the device answers READY, andonDeviceStart():578terminates the old session. That callsByteRelay::stop()and ultimately aborts the TCP endpoint (ByteRelay.cpp:309–311), dropping bytes before the existing ten-second drain allowance (ByteRelay.h:52). The new host behavior induces this without an explicit device restart request.Fix: defer automatic HELLO/READY admission until the draining session has ended;
sessionEnded()already schedules the retry. An explicit device HELLO may still force a restart. Cover the draining state with injected transport/state-machine coverage. I did not run a connected-session probe because this Session unconditionally binds UDP on connection, triggering the socket-test permission boundary.
Nits — non-blocking
tests/ctr2_usb_relay_test.cpp:826–877: explicitly disclose the socket use in the PR validation text. The named target binds loopback TCP/UDP, including the new refusal probe. Its existingtests/tests.cmake:392–402block names the sockets and configures exit-77 skipping. I notified the operator before continuing and did not build or execute this target. It runs in the unfiltered full-suite/sanitizer lanes, not the filtered per-PR CTest steps. The claimed eight-suite, 15-repeat and five-mutation results were not independently rerun in this review.
Verification and attacks that held
- Built the relevant production relay/framing/byte-pump sources in isolated scratch harnesses with Qt 6.11.2 on Linux; no production source edits and no network I/O. The existing socket-free CLOSED-retention test passed on head and merge base. The late-READY probe passed on base and failed on head as quoted above.
- Additional socket-free head checks passed: Start sends HELLO with counter zero; a stalled HELLO does not accumulate or open a radio connection; acknowledging it permits retries; duplicate READY while Connecting opens only one connection; retry HELLO does not stack behind pending CLOSED; HELLO resumes after CLOSED acknowledgement.
- Compared engine-boundary, touchpoint-manifest, capability-record and command-plane checks on base/head: identical results, no new blocking findings; manifest current.
git diff --checkpasses. Confirmed firmware C implementation, UDP implementation and radio-switch teardown code are untouched; that is source evidence, not hardware convergence evidence. - Required PR checks are successful, but runs were created at 2026-10-03 16:16:13 UTC, before current main
fe1fec519at 17:27:28 UTC. CI usespull_requestmerge results and branch protection hasstrict:false; this green does not establish integration with current main. - No GUI/bridge session was launched: the changed path requires CTR2 HID and a dedicated external radio connection, which the demo simulator does not provide. No live radio or operator instance was touched. Actual CTR2 firmware, USB delivery, macOS/Windows runtime and the drain scenario remain unverified. A model-invocable
code-reviewskill was unavailable; this was a manual review with an independent code-reading pass.
Recommendation: Request changes for the two lifecycle defects, then rerun the focused injected-transport checks and refresh integration evidence against current main.
Posting note: GitHub rejected REQUEST_CHANGES because the authenticated account (ten9876) is also the PR author. This is posted as a COMMENT review; the recommendation remains Request changes.
…id-drain. Principle VIII. Addresses the review on #6114. - A delayed READY answering a HELLO sent before a radio refusal could be taken as a new start: it discarded the still-undelivered CLOSED and opened another connection, skipping the 5 s back-off. READY now opens a link only after a HELLO the host has sent since the last link ended, so back-off and a pending CLOSED are respected. A device HELLO still forces a restart. - After the device's CLOSED, the host no longer schedules HELLO while the old session is still draining the device's last bytes to the radio; the session's end schedules it, so an answer cannot abort the drain. - Tests: ctr2_usb_closed_retention_test gains the late-READY sequence (socket-free); ctr2_usb_relay_test gains a stalled radio write that holds a drain open while no HELLO may go out. Spec updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
3712bb5 to
21f6160
Compare
|
Thanks. Both lifecycle defects are fixed in 21f6160, and the branch is rebased onto current main (
Locally on the rebased head: all eight CTR2 suites pass, the two affected tests passed 30 out of 30 repeated runs, the app builds, and every static-check script passes. The spec's host rules now state that a READY is accepted only after a fresh HELLO, and that a drain completes before the next HELLO. |
There was a problem hiding this comment.
1. Issue fit
No linked issue. There is no fixes/closes #NNNN in the body, so I reviewed against the PR's own stated intent plus RFC #6091. GOVERNANCE.md wants an RFC for architectural changes, and there is one: #6091 is RFC Approved, and its stage-2 approval is explicitly conditional — "stage 2 (USB HID transport, once the envelope, handshake and descriptor are agreed with the firmware side)" — with the message-type semantics listed in the 2026-10-02 amendment as "Additions for him to confirm: message types in the spare header byte (DATA / HELLO / READY / CLOSED)". Inverting which side sends HELLO lands squarely inside that confirmation loop, so I read the body's "stays within approved RFC #6091" as substantially correct, not a scope violation — with one caveat under Blockers/maintainer call below.
The stated intent is delivered. The host calls on Start (start(), Ctr2UsbRelay.cpp:415), repeats on a single-shot timer, accepts the device's READY only as a fresh answer, keeps the device-HELLO restart path, and holds HELLO behind a drain and behind undelivered output. Each of the seven claimed guards maps to a hunk and to a named test.
2. Scope
| File / group | What it changes | Claimed by title/body | Verdict |
|---|---|---|---|
src/core/Ctr2UsbRelay.{h,cpp} |
HELLO timer, m_awaitingDevice/m_helloSent, READY-as-answer guard, back-off, drain gate |
Yes | In scope |
src/core/Ctr2HidFraming.h |
Comment-only (verified: the only non-comment delta is the two trailing // comments on the enumerators) |
Yes | In scope |
docs/ctr2-usb-relay-design.md |
Spec: link flow, host rules, device rules. Test-vector table untouched (correct — framing is unchanged) | Yes | In scope |
tools/ctr2-firmware-reference/README.md |
Reference example follows the new flow | Yes | In scope (one nit, #2) |
tools/ctr2-firmware-reference/ctr2_link.h |
Comment-only (verified: git diff shows zero non-comment lines) |
Yes ("only its header comments moved") | In scope — claim holds |
tests/ctr2_usb_relay_test.cpp |
3 new cases, updated wording/assertions | Yes | In scope |
tests/ctr2_usb_closed_retention_test.cpp |
testLateReadyKeepsClosedAndBackoff |
Yes | In scope |
No build-config, CI, plugin or vendored churn. CHANGELOG.md correctly untouched. No new config key, settings key, capability field, CLI flag or exported API; the only new surface is two Tuning ints with defaults, not operator-facing. No sibling relay implementation exists (Ctr2UsbRelay is the only ByteRelay HID consumer; TcpByteProxy is the Wi-Fi path and is untouched by design).
Deleted behavior, named explicitly: linkFault("CTR2 sent READY, which only the host sends") is gone, and testProtocolViolations flips from "READY from the CTR2 is a fault" to "…is ignored". I checked whether this resurrects a symptom and concluded it does not: the deleted comment named no user-visible symptom, READY now has a defined device-side meaning, and a mid-link READY is reachable as a legitimate late answer. Treating it as a fault would tear down a healthy link. Correct removal.
Socket-test disclosure (reporting duty, not a finding): ctr2_usb_relay_test binds loopback TCP (RadioPeer/QTcpServer on 127.0.0.1 ephemeral) and UDP, and this PR adds StalledWriteSocket — an injected QTcpSocket subclass whose writeData returns 0, i.e. a stalled local socket, not a fake radio/firmware peer. Registered at tests/tests.cmake:398-402 with SKIP_RETURN_CODE 77 and TIMEOUT 120; the test std::exit(kSkip)s when loopback cannot bind. ctr2_usb_closed_retention_test (tests.cmake:349-352) opens no sockets. The body discloses both accurately. Neither target appears in any ctest -R gate in ci.yml, so the body's "all eight CTR2 suites pass" is author-local evidence; CI green here means compile + static checks only (build / check-windows / check-macos / Static checks / sanitizer-configure / CodeQL JS+Py all pass on 21f61602; there is no analyze (cpp) run on this head).
3. Blockers
None.
One item for the maintainer, not a blocker:
- The RFC that authorizes this handshake is closed, and the firmware author's request is unevidenced. #6091 was closed as completed on 2026-10-03, and the body's load-bearing premise — "At the CTR2 firmware author's request" — appears nowhere in #6091's thread or this PR's. The inversion is within the RFC's own "agree the handshake with the firmware side" condition, so I am not calling it out of scope, but the wire contract for a third party is now defined only by a sentence in a PR body. Ask for a one-line amendment comment on #6091 (or a link to the firmware author's request) recording the inversion, so the next reader of the RFC does not find
HELLO (device: start the link), READY (host: radio connected)in the approved text and the opposite in the code. Inline atsrc/core/Ctr2HidFraming.h:37.
4. Nits (non-blocking)
- The documented back-off is inaccurate in two ways (inline at
Ctr2UsbRelay.cpp:363and:623). The timer lambda reschedules withhelloIntervalMsunconditionally, sohelloRetryAfterFailureMsgoverns only the first retry after a failure; andlinkFaultapplies the same 5 s to framing errors and incomplete-message timeouts, which have nothing to do with the radio. Practically narrow — when the device answers promptly each failure cycle re-arms 5 s viasessionEnded(error=true), so the 1 s cadence only emerges if the device misses the first post-failure HELLO, and HELLO alone never touches the radio. So this is doc/comment accuracy, not a safety hole;docs/…:191and theTuningcomment atCtr2UsbRelay.h:38-40are the two places to reword. waiting_for_hostloses its initial1in the firmware reference example (inline attools/ctr2-firmware-reference/README.md:25). The oldlink_start()set it on entering USB mode; the newanswer_hello()sets it only on a received HELLO, andstatic int waiting_for_host;is zero-initialized. A device that resets or enters USB mode mid-stream now feeds host bytes straight intoctr2_rx_feed/radio_bytes()instead of skipping to the next0xFFcontrol header — the exact case the design doc's "this skips the tail of anything sent earlier without misreading it" sentence promises.ctr2_link.c/.hare genuinely unchanged, so the body's claim holds; this is in the example only. The design doc's Device-behavior section (docs/…:230) is likewise silent on initial state.- A device CLOSED during the failure back-off shortens it.
onMessage'sClosedcase with no session callsscheduleHello(helloIntervalMs)(Ctr2UsbRelay.cpp:568), restarting the single-shot timer at 1 s and discarding the remaining 5 s. The body says a late READY "never … skips the back-off"; an off-spec duplicate CLOSED does. Impact is one HELLO ~4 s early, so cosmetic, but the invariant is stated more strongly than the code holds. - The new normal path is tested narrowly.
linkUp()/linkUpHeld()still bring the link up withdev.hello(), so the framing-fault, drain, datagram, restart and connect-timeout scenarios all exercise the device-initiated path. OnlytestHostCallsAndDeviceAnswersand the retention test go through host-HELLO → device-READY. SwitchingRig::linkUp()todev.answer()after a host HELLO would re-point the whole suite at the path that will actually run in the field, with the device-HELLO restart kept as its own case. m_awaitingDeviceis a slightly misleading name for a flag that is true while the host is calling;m_noLinkUporm_linkDownreads closer to what the three guard sites test. Purely naming.
5. What I tried to break (and it held)
Findings below are reasoned from the code at /tmp/aetherclaude/pr-6114; I have no build, no test run and no hardware, so nothing here was reproduced at runtime.
- Old-firmware interop, the thing a handshake inversion most plausibly breaks. A CTR2 that still HELLOs first works unchanged:
onMessage'sHellocase routes toonDeviceStart()at any time, with nom_helloSentor state precondition. In the other direction the host's unsolicited HELLO is harmless to old firmware — itswaiting_for_hostfilter skips any header that is not READY/CLOSED, and itsswitchhas no HELLO case — and becausesendHello()is gated onm_awaitingDevice, no HELLO is ever emitted mid-link, so no DATA follows a counter reset the old device didn't expect. The version byte is deliberately not bumped and I think that's defensible here, since both handshakes are accepted. - Late-READY orderings, the regression the previous review round found. I walked: READY during a link (
m_session != nullptr→ ignored); READY with a CLOSED still queued (m_helloSent == false→ ignored, CLOSED survives, matchingtestLateReadyKeepsClosedAndBackoff); READY during the back-off (same); multiple READYs answering repeated HELLOs (the first wins, the rest seem_session != nullptr); READY arriving while a newer HELLO is still queued (onDeviceStart'sdiscardOutput()drops the queued HELLO, and if it is already on the wire the device sees HELLO-then-READY and converges). Every ordering converged. I also confirmed the guard is load-bearing by reading the mutation: dropm_helloSentand the retention test'sconnections == 1assertion fails. - Timer lifecycle across all five
scheduleHellocallers (start, the lambda,onMessage/Closed,linkFault,sessionEnded) plus the threestop()sites (onDeviceStart,stop,onPortFailed).scheduleHellois guarded onm_port && m_awaitingDeviceand the timer is single-shot, so a double-start()cannot double the cadence, and I found no path that leaves it armed while a session is draining —onDeviceStartstops it before the only window wherem_awaitingDeviceis true with a live session.releasePort()nullsm_port, which makes every laterscheduleHelloa no-op;~Ctr2UsbRelaygoes throughstop(). - The "cannot accumulate HELLOs" claim. The
!m_reportCosts.empty()guard is a host-queue guard: it bounds the queue, which is what the body claims, and the mutation is visible (drop it andpending() == 1fails). What it does not bound is HELLOs that the OS accepts on behalf of a non-reading device — whetherreportsSentfires in that case is a hidapi/OS question I cannot answer here, so the real on-hardware cadence is unverified. The converse failure (a port that never acks, stranding the host at exactly one HELLO forever) is survivable because device-HELLO still works. - Stale-generation callbacks.
sessionConnected/sessionDraining/sessionEndedall checkgeneration != m_generation,endSession()bumps the generation before the newSessionis built, andsessionEndedadditionally requiresm_session. Aterminate()that lands afteronDeviceStarthas replaced the session cannot resetm_awaitingDeviceor re-arm the timer for the new link. - Could not check: whether
StalledWriteSocket::writeDatareturning 0 actually produces aByteRelaydrain rather than an immediate transport fault — that determines whethertestNoHelloWhileDraining'sstate() == Closingassertion holds, and it is the one new test whose mechanism I am taking on trust. Also unchecked: Stop→Start reuse of the same USB device, where the old port's asynchronous CLOSED now races a new port's immediate HELLO. The port lifecycle there is pre-existing andreleasePort()detaches cleanly, but it is a new ordering surface that this PR creates. - Not a finding, just confirmed: the applet's USB status string is
tr("Waiting for CTR2")(Ctr2ProxyModel.cpp:360), which stays accurate under the new flow, and no UI string tells the operator to press anything on the controller — so the body's "the operator presses only Start" needs no UI change to be true.
6. Recommendation
Approve with nits, plus one maintainer call. The state machine is the strongest part of this PR: the m_helloSent-as-answer-token design is the right shape, the three guards are individually load-bearing and individually tested, and the two lifecycle defects from the previous round are genuinely fixed rather than papered over. I attacked the handshake inversion from the direction that usually breaks these — old-firmware interop and late/duplicate control messages — and found nothing that misbehaves.
Concrete next step: fix the waiting_for_host = 1 initializer in the reference example (nit #2) before the firmware author builds against it, since that file is a third-party contract; reword the back-off sentence in the design doc and the Tuning comment to match what the timer actually does; and get a one-line amendment onto #6091 recording the handshake inversion, since the RFC is closed and its approved text still says the opposite. The maintainer should decide whether that amendment is a precondition for merge or a follow-up.
🤖 aethersdr-agent · cost: $7.3189 · model: claude-opus-5
…id-drain. Principle VIII. Addresses the review on #6114. - A delayed READY answering a HELLO sent before a radio refusal could be taken as a new start: it discarded the still-undelivered CLOSED and opened another connection, skipping the 5 s back-off. READY now opens a link only after a HELLO the host has sent since the last link ended, so back-off and a pending CLOSED are respected. A device HELLO still forces a restart. - After the device's CLOSED, the host no longer schedules HELLO while the old session is still draining the device's last bytes to the radio; the session's end schedules it, so an answer cannot abort the drain. - Tests: ctr2_usb_closed_retention_test gains the late-READY sequence (socket-free); ctr2_usb_relay_test gains a stalled radio write that holds a drain open while no HELLO may go out. Spec updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
21f6160 to
a905c84
Compare
|
Review follow-ups are in a905c84. The branch is rebased on current main (
Validation on a905c84:
|
At the firmware author's request, the host now initiates the CTR2 link so the operator presses only Start in AetherSDR, not also a connect button on the controller. Link format v0 is unchanged; only who starts changes. - On Start, and whenever no link is up, the host sends HELLO and repeats it about once a second (every 5 s after a radio connection failed), never queuing a new HELLO behind undelivered output. - The device answers READY; that opens a fresh radio connection, and the host replies READY (connected) or CLOSED (failed), as before. - A READY arriving during a link is a late answer to an earlier HELLO and is ignored. A device HELLO still forces a restart. - Spec, firmware reference README and tests updated: host calls first, retries until answered and stops once linked, HELLOs never stack on a stalled port, back-off after a radio failure, late READY ignored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…id-drain. Principle VIII. Addresses the review on #6114. - A delayed READY answering a HELLO sent before a radio refusal could be taken as a new start: it discarded the still-undelivered CLOSED and opened another connection, skipping the 5 s back-off. READY now opens a link only after a HELLO the host has sent since the last link ended, so back-off and a pending CLOSED are respected. A device HELLO still forces a restart. - After the device's CLOSED, the host no longer schedules HELLO while the old session is still draining the device's last bytes to the radio; the session's end schedules it, so an answer cannot abort the drain. - Tests: ctr2_usb_closed_retention_test gains the late-READY sequence (socket-free); ctr2_usb_relay_test gains a stalled radio write that holds a drain open while no HELLO may go out. Spec updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ng. Principle VIII. Review follow-ups on the host-initiated handshake: - The failure back-off now holds for every call until a link ends cleanly or the operator restarts, not just the first retry. It applies to any link fault as well as a failed radio connection, so the tuning field is helloRetryAfterFaultMs and the spec says so. - A CLOSED with no link up never restarts a running call timer, so stray CLOSEDs cannot postpone the next HELLO indefinitely. - The firmware reference example boots with waiting_for_host = 1, and the spec states the device's power-up state, so a device that resets mid-stream skips to the next control header instead of parsing the tail. - m_awaitingDevice is m_linkDown: it is true while the host is calling or the last link is still draining. - The relay tests bring links up the way the field will: the host's HELLO, answered by the device's READY. Device-sent HELLO stays covered as the restart case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ple VIII. Stop wrote the old link's CLOSED on the old port's I/O thread while Start opened a new handle on the same device and sent HELLO from another, with nothing ordering the two. A CLOSED landing after the new link's READY would make the CTR2 drop a link AetherSDR still relays, and AetherSDR never calls while a link is up. Ports opened with the same device path now reach it in order: the new port's I/O thread waits, up to 1 s, until the previous port has written its final reports and closed the device. A port that failed has already closed it, so a reopen after a failure does not wait. A port the relay refuses is handed back with shutdown(), which never blocks, rather than deleted in place, which would wait on that I/O thread. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
a905c84 to
d63bad1
Compare
|
Final pass before landing: one more finding, fixed in d63bad1. The head is rebased on main Stop→Start could reorder CLOSED and the new link. This is the ordering the review couldn't check. On Stop, the old HID port writes its final CLOSED on its own I/O thread. Start opened a new handle on the same device immediately and sent HELLO from another thread, with nothing ordering the two writes. If the old CLOSED reached the CTR2 after the new link's READY, the CTR2 would drop a link AetherSDR keeps relaying, and AetherSDR doesn't call while a link is up.
Proof:
|
…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>
Summary
At the CTR2 firmware author's request, AetherSDR now starts the USB link handshake. The operator presses only Start in the CTR2 Proxy applet; nothing needs pressing on the controller. Link format v0 is otherwise unchanged: same message types and framing, only the side that starts changes. This stays within approved RFC #6091.
Behaviour
shutdown(), which never blocks.For the firmware
The device side gets simpler. Boot into the waiting state (
waiting_for_host = 1). On HELLO, reset the counter and answer READY, then wait for the host's READY (connected) or CLOSED, skipping anything else until one of those headers arrives.docs/ctr2-usb-relay-design.mdandtools/ctr2-firmware-reference/README.mdshow the new flow. The C reference code itself is unchanged; only its header comments moved.Validation (Linux)
ctr2_usb_relay_test:ctr2_usb_relay_testcome up the field way: the host's HELLO answered by the device's READY. Device-sent HELLO is covered as the restart case.ctr2_hid_thread_port_test(no sockets): a reopened port writes only after the old port's slow final CLOSED, ports for different devices don't wait for each other, and a reopen after a failure writes at once. New inctr2_proxy_model_test: a port the relay refuses is shut down, not deleted in place.ctr2_usb_closed_retention_test(socket-free): a late READY after a radio refusal keeps the undelivered CLOSED, opens no connection, and respects the back-off, while a device HELLO still forces a restart.579efd73f). All eight CTR2 suites pass; the relay test passed 10 out of 10 repeated runs and the thread-port test 8 out of 8. The app builds, and everystatic-checks.ymlscript passes.ctr2_usb_relay_testbinds loopback TCP and UDP: a generic radio byte peer on 127.0.0.1, the relay's own UDP socket, and UDP peers on 127.0.0.1 and 127.0.0.2. It exits 77 if loopback can't be bound.ctr2_usb_closed_retention_testopens no sockets: its radio socket'sconnectToHost()only queues an injected refusal. Both are registered intests/tests.cmakeonly; the frozen per-PR CTest gate is unchanged.🤖 Generated with Claude Code