Repository navigation
feat(ctr2): CTR2 relay over Wi-Fi (TCP) and USB HID link v0. Principle VI. - #6090
Conversation
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
|
Thanks for the very thorough write-up @ten9876 — the validation section made this quick to diagnose. The failure is small and mechanical, not a problem with the proxy code. What failedOnly Static checks failed; the failing step is This is not CI infrastructure — the check is doing exactly its job. Worth noting: Fix (two steps, both required)1. Hand-add one entry to A suggestion, not a ruling — classification is yours, and this PR is RFC-pending anyway: 2. Regenerate the table and commit both files: Step 2 is not optional. I verified this locally on your head: with the entry added and the generator run, The other reviewer commentThe a11y bot's 4 findings are not yours. They're all in Scope
🤖 Generated with Claude Code 🤖 aethersdr-agent · cost: $5.3324 · model: claude-opus-5 |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
2 similar comments
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
2 similar comments
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
There was a problem hiding this comment.
Issue fit
Yes, for Stage 1; substantially for Stage 2. RFC #6091 carries the RFC Approved label and an explicit maintainer decision on the transmit question (operator intent is the enable, no extra conditions), plus three recorded amendments — USB link v0 + HID worker thread, UDP-over-USB as type 0x04, and "the relay follows AetherSDR's radio". I checked each amendment against the code and each one is actually implemented: Ctr2ProxyModel takes no typed radio address, setAetherRadio() stops a running relay when the address changes (Ctr2ProxyModel.cpp:196-211), the unavailable reasons are the three the amendment lists, and the multi-client gate is a capability check (backendCapabilities().hasMultiClientSessions, MainWindow_Controllers.cpp:2529) rather than a family check — Constitution III-shaped, which is the right call. The on-screen scope notice the RFC promises is really there (Ctr2ProxyApplet.cpp:77-84). Nothing persists and nothing is enabled at launch, so the Principle VI basis of the approval holds in code.
One genuine correctness gap in the USB path (Blocker 1 below) and four nits.
Socket-owning tests in this PR (disclosure, not a finding)
| Test | Sockets | Registered | In the CI gate? |
|---|---|---|---|
tcp_byte_proxy_test |
QTcpServer/QTcpSocket, 127.0.0.1 ephemeral |
tests.cmake, SKIP_RETURN_CODE 77, TIMEOUT 120 |
No |
ctr2_usb_relay_test |
QTcpServer/QTcpSocket + QUdpSocket, 127.0.0.1 ephemeral |
same | No |
Both are legitimate under the tests guide: our own TcpByteProxy/Ctr2UsbRelay server is the subject, the peers are generic byte endpoints, and there is no fake Flex radio anywhere. tests.cmake names the socket in its comment block and both skip with 77 on bind refusal. Neither is in any ci.yml -R filter (the only filters are the dv/cross_needle/mac_nr/asr sets), which matches the design doc's own "not the frozen CI gate" — so CI compiles these six tests but never runs them. The other four (byte_relay_test, ctr2_proxy_model_test, ctr2_hid_framing_test, ctr2_hid_reference_test) are socket-free.
Scope
Note for anyone reading the PR's file list: it is incomplete. The real diff is 38 files — tests/tests.cmake, tests/ctr2_hid_vectors.h, tests/ctr2_proxy_model_test.cpp, tests/ctr2_usb_relay_test.cpp, tests/tcp_byte_proxy_test.cpp and tools/ctr2-firmware-reference/{README.md,ctr2_link.c,ctr2_link.h} are all present. I reviewed against the checkout, not the list, so "the new tests aren't registered" is not a finding here.
| File / group | What it does | Claimed? | Verdict |
|---|---|---|---|
src/core/{ByteRelay,TcpByteProxy,TcpSocketEndpoint}.* |
Stage 1 opaque relay + listener | Yes (RFC §2 Stage 1) | In scope |
src/core/Ctr2Hid{Framing,Port,apiPort}.*, Ctr2UsbRelay.* |
Stage 2 HID link v0 + UDP | Yes (RFC Stage 2 + both amendments) | In scope |
src/models/Ctr2ProxyModel.* |
Config/state, frozen while running | Yes | In scope |
src/gui/Ctr2ProxyApplet.* |
New applet | Yes (RFC: new applet, additive) | In scope |
src/gui/AppletPanel.* |
Registers CTR2 tile, Integration, false default, outside kDefaultOrder |
Yes | In scope — existing applets untouched, no reorder, no default changed |
src/gui/MainWindow*.cpp/.h |
setupCtr2Proxy() + stop() in closeEvent |
Yes | In scope; new behavior stays out of MainWindow.cpp as the design requires |
CMakeLists.txt |
5 core + 1 model + 1 gui source, 2 under if(HIDAPI_FOUND) |
Yes | In scope, nothing else touched |
docs/ctr2-*-design.md |
Two design docs | Yes | In scope |
docs/architecture/aetherd-touchpoint* |
231→232, models 37→38, ThemeManager includers 156→157 |
Yes | In scope, arithmetic checks out |
tools/ctr2-firmware-reference/* |
MIT C99 reference for the firmware | Yes (amendment) | In scope — see nit 4 |
tests/* (6 targets) |
Pump, codec, reference cross-check, model, two socket tests | Yes | In scope |
No unexplained files, no formatting churn, no deleted guards (the only - lines in existing files are the HidDeviceParser.h) paren and the two touchpoint-count lines). CHANGELOG.md is correctly untouched. No new settings key, no new protocol verb on an existing surface, no capability field added, no user-visible default changed. New public surface is the HID link v0 wire format, which is exactly what the RFC and its amendments approve.
Two things the body/docs should say and don't:
- The USB per-direction budget is 16 KiB, not the 256 KiB the RFC states for Stage 1 (
Ctr2UsbRelay.hTuning::relay{16 * 1024}). It's a deliberate choice with a comment explaining it — just undisclosed in the RFC. - The issue body still reads "the operator picks … and the radio's IPv4 address and port", which the follow-the-radio amendment superseded. Worth syncing the body, since it describes a wider capability than what ships. The change is strictly narrowing, so this is housekeeping, not a scope problem.
Blockers
1. USB: a HELLO restart leaves the previous link's bytes queued on the HID port, so old-connection data reaches the CTR2 after it restarted. (inline: src/core/Ctr2UsbRelay.cpp:60)
ByteRelay::finish(graceful=false) aborts both endpoints; on the device side that is DeviceEndpoint::abort(), which clears only the inbound m_inbox (Ctr2UsbRelay.cpp:60). The outbound queue lives in the shared Ctr2HidPort — Ctr2HidapiPort's worker m_outbox, drained at kMaxWritesPerTick = 16 reports per 1 ms tick — and Ctr2HidPort exposes no purge operation at all (Ctr2HidPort.h:39-45: only send, close, closeAfter). onHello() (:520-535) tears the session down with sendClosed=false and never touches that queue, and m_unsentPayload/m_reportCosts are reset only in releasePort().
Failure scenario: the radio is mid-burst toward the CTR2 (so up to the 16 KiB per-direction budget is sitting in the port FIFO) when the controller resets and sends HELLO. The relay closes the old radio socket, opens a new one, and queues READY(counter 0) — behind the stale DATA reports. The device therefore receives the previous radio connection's bytes, then READY. Because the device's receive-side counter expectation was not reset by its own HELLO, those stale headers will most likely parse cleanly and be fed to its parser as new-link data; the alternative is a framing error, which per the spec makes the device send HELLO again, and the loop can repeat.
This contradicts three written guarantees: "HELLO received: close any existing radio connection, discard all link state" (docs/ctr2-usb-relay-design.md, Host behavior), and "Never replay data…" / "Unsent data never enters another connection" (docs/ctr2-tcp-proxy-design.md). The Wi-Fi path does honor them — aborting the downstream QTcpSocket discards its unsent buffer — so this is specifically the seam where the HID port outlives the session. linkFault() has the same shape: its CLOSED is queued behind the stale DATA.
testHelloRestartsWithFreshConnection (tests/ctr2_usb_relay_test.cpp:353-371) looks like it covers this but doesn't: it writes "stale" after the old connection is already closed, and the fake port acks every report on a singleShot(0) so the outbox is empty whenever HELLO lands. A test that holds acks (autoAck = false) across the HELLO would catch it.
Fix shape: add Ctr2HidPort::discardQueued() (worker m_outbox.clear(), plus reset m_reportCosts/m_unsentPayload/m_unsentDatagramBytes) and call it from onHello() and linkFault() before the new READY/CLOSED. Scoping it honestly: this blocks the USB stage, not the Wi-Fi prototype, and it cannot regress anything shipping — the firmware side doesn't exist yet. But it's a small fix and much cheaper to land now than to debug on the bench.
Nits (non-blocking)
- The per-link UDP socket binds every interface (
Ctr2UsbRelay.cpp:270), which is the one listener here that does —TcpByteProxy::validate()deliberately refuses0.0.0.0for the TCP listener. The return filter is source-IP only (:290), no port check, so a same-segment spoof of the radio's IPv4 lands attacker-chosen bytes in the CTR2's UDP consumer attributed to the radio. Bounded to 1472 bytes per datagram with a 3 KiB backlog cap, so it's content injection, not exhaustion.m_udp->bind(m_radio->localAddress(), 0)inonConnected()is functionally transparent (the radio learns the endpoint from the CTR2's own registration datagram sent from this socket) and narrows it to the interface facing the radio. The design doc specifies the filter as exactly "from the radio's IP", so whether the all-interfaces bind is acceptable is a threat-model call — flagging rather than asserting. Found and confirmed by security-audit (CodeGuard); its other candidates I was able to refute. Ctr2HidapiPort::close()blocks the GUI thread for an unbounded time (Ctr2HidapiPort.cpp:250-252):BlockingQueuedConnectioninto the worker plus a barem_thread->wait(), with the comment itself conceding "a few seconds".closeAfter()does the same at:266. This runs fromMainWindow::closeEventand from anystop(). The house pattern is a boundedwait(ms)with aqCWarningon expiry.- The upstream port is hardcoded to 4992 (
Ctr2ProxyModel.cpp:250and:317) whileRadioInfo::portexists andRadioModelitself honors it (req.port = info.port). The design doc claims the destination is the radio "at the address AetherSDR reached it by". Zero practical impact today — every Flex path inConnectionPanel.cppsets4992, and the relay is gated tohasMultiClientSessionswhich only Flex sets true — so this is latent coupling, not a current defect. One-line fix: pushlastRadioInfo().portthroughsetAetherRadio(). tools/ctr2-firmware-reference/ctr2_link.{c,h}are the only MIT-licensed files in a GPL-3.0 repo. The SPDX header and the design doc both state it, which is the right way to do this, and these are the only non-repo-licensed files I found undersrc/ortools/. The"and AetherSDR contributors"line in the copyright notice is the one thing worth a second look: it's new single-author code today, but the phrase invites a future contributor to assume their GPL-3 contributions can be taken under MIT. Maintainer awareness, not a code problem.
What I tried to break
Everything below is reasoned from the code at 40a5dbc7 — I have no build and ran nothing, so none of it was reproduced at runtime.
HAVE_HIDAPInot reaching the model, which would have made the whole USB half dead code behind a permanent "This build has no USB HID support". It holds:MODEL_SOURCEScompiles intoaethercore(CMakeLists.txt:1577) and the define istarget_compile_definitions(aethercore PUBLIC HAVE_HIDAPI)at:2747, soCtr2ProxyModel.cpp's#ifdefsees it. Also checked that the applet is reachable:AppletPanelconstructs it,appletCatalog()lists it, andsetupCtr2Proxy()wiressetModel()— no dead additions.FrameReassembleragainst hostile reports. Tried length/packet-count disagreement (packets != packetsFor(length)rejects it),Datagramwith length 2 to makem_partial[0..1]read out of bounds (thelength >= 3bound forecloses it), an oversize radio datagram (receiveDatagram(kMaxDatagramBytes + 1)+ the size check drops it, and the UDP read consumes the rest), and a type byte above0x04. Every error is sticky untilreset()and no partial message is ever released — fail-closed, and bounds are checked before anything is buffered.- Re-entrancy in the drain sequence.
DeviceEndpoint::closeGracefully()sends CLOSED and synchronously callsnotifyClosed(Side::A)from insidemaybeFinishDrain()'s owncloseGracefully(dst)call, which re-entersByteRelay::finish(). It survives:m_closingGracefullyis set before either close andm_finishedguards every subsequent path, so there's no double-finish and no double CLOSED (thesendClosed = errorwiring means a graceful close never sends a second one). - Byte loss across partial writes and backpressure. Walked
pump()'sd.pending = chunk.mid(accepted)against thewhile (… && d.pending.isEmpty())loop condition andflushPending()'sstd::min(n, pending.size())clamp — no double-count, no overwrite. When the loop exits onroom <= 0no continuation is scheduled, but resumption ridesnotifyBytesWritten(dst) → pump(src), so it isn't a stall. - Stale callbacks and use-after-free across connection generations. Both proxies gate every callback on a generation counter,
QObject::disconnect(session, …)beforedeleteLater(), accepted sockets reparented toSession(sodelete m_servercan't free them), and~Sessiondeletes the relay before its socket children. The hidapi worker'sinvokeMethod(m_port, …, Queued)uses the port as context, so pending deliveries drop when it's destroyed.delete hidon a failedCtr2UsbRelay::start()is not a double-free — everyfalsereturn precedesm_port->setParent(this). - Attacked the tests rather than trusting them.
byte_relay_testasserts byte-for-byte equality in both directions, budget ceilings,discardedA/Bcounts, and abort-vs-graceful-close — not implementation echo; emptying a guard would turn it red, consistent with the author's mutation-check list. Both socket tests use generic byte peers, never a fake radio. The one real gap is the HELLO-outbox case in Blocker 1. - Checked CI honestly. All 7 checks green at
40a5dbc7, includingcheck-macosandcheck-windows— so the "Linux only" caveat is about runtime validation; compilation is verified on three platforms. There is noAnalyze (cpp)lane, so no C++ static analysis ran. I could not verify any hardware claim: stock CTR2 behavior, MultiFlex binding, VPN/tailnet paths, hidapi report-ID handling on IOHIDManager/Windows HID, or whether the radio releases TX when a client's socket drops. That last one is the open item the #6091 thread already books as an on-air test before Stage 2, and it remains the right gate. - Did not re-litigate four things already settled or disclosed: the
TxCoordinatorbypass and the unauthenticated TCP listener (both ruled on in #6091 — andTxCoordinator.h:16-18really does scope itself to in-process intents, so an external client was never inside it), the progress timer being shared across directions, and the unsupported half-close. All three of the latter are stated in the PR's own validation record, which is the right way to ship a known limit.
Recommendation
Approve with nits, once Blocker 1 is resolved or explicitly deferred by the maintainer. This is unusually disciplined work: the opaque-forwarding contract is honored at the seam rather than just asserted, the budget/timeout/generation machinery is real and tested, the governance trail is complete with every amendment implemented as recorded, and the validation record states what was not verified instead of rounding up. The one substantive defect is narrow — a shared HID outbox that isn't purged on link restart — and it lives entirely in the Stage 2 path that cannot regress anything today.
Concrete next step: add Ctr2HidPort::discardQueued(), call it from onHello() and linkFault(), and extend testHelloRestartsWithFreshConnection to hold the fake port's acks across the HELLO so the fix is pinned. The four nits can land in the same push or be deferred; the UDP bind scope is the only one I'd want a maintainer opinion on rather than a patch. Thanks for the design docs and the MIT reference — handing the firmware author test vectors that CI cross-checks against the host codec is exactly the right shape for a two-sided protocol.
🤖 aethersdr-agent · cost: $13.4844 · model: claude-opus-5
ten9876
left a comment
There was a problem hiding this comment.
Issue fit
Partially implements approved RFC #6091. Request changes for four reproduced USB defects below. The Wi-Fi relay and the additive applet match the approved design. The RFC's explicit amendments authorize opaque independent-client forwarding (including the transmit boundary), the HID worker thread, UDP-over-USB, and stopping when AetherSDR leaves its radio. Those decisions are not being reopened. The USB implementation does not yet meet its link-reset, per-message timeout, bounded-queue, and nonblocking-UI guarantees.
Reviewed head: 40a5dbc756c1e8a40f60687badf829d80772be5b; merge base: 81806bb3abb713ed7657924befb7dee97b1825ea. This is a new feature, so the merge base has no CTR2 targets or applet to run as a before-fix comparison. The failures below exercise the newly added production code, not an inferred change in preexisting behavior.
Scope
| File / coherent group | Change | Claimed by body/RFC? | Verdict |
|---|---|---|---|
src/core/ByteRelay.*, TcpByteProxy.*, TcpSocketEndpoint.h |
Opaque bounded byte pump and TCP listener | Yes | In scope |
src/core/Ctr2HidFraming.*, Ctr2HidPort.*, Ctr2HidapiPort.*, Ctr2UsbRelay.* |
USB framing, device I/O, session and UDP relay | Yes, including RFC amendments | In scope; correctness findings below |
src/models/Ctr2ProxyModel.* |
Configuration, explicit enable, destination ownership and teardown | Yes | In scope |
src/gui/Ctr2ProxyApplet.*, AppletPanel.* |
New off-by-default Integration applet | Yes | In scope; no existing applet reordered |
src/gui/MainWindow.cpp, MainWindow.h, MainWindow_Controllers.cpp |
Construction, connected-radio updates, shutdown | Yes | In scope |
CMakeLists.txt |
Register production sources and optional HID implementation | Yes | In scope; no new dependency |
docs/architecture/aetherd-touchpoint* |
Add peripheral model touchpoint and regenerate counts | Yes | In scope; no enforcement baseline weakened |
docs/ctr2-tcp-proxy-design.md, docs/ctr2-usb-relay-design.md |
Design, protocol and validation record | Yes | In scope |
tests/{byte_relay,tcp_byte_proxy,ctr2_hid_framing,ctr2_hid_reference,ctr2_proxy_model,ctr2_usb_relay}_test.cpp, ctr2_hid_vectors.h, tests.cmake |
Six registered focused tests and vectors | Yes | In scope; socket disclosure needs correction |
tools/ctr2-firmware-reference/* |
MIT C99 firmware reference | Explicitly approved | In scope |
All 38 changed files and 13 authored commits are explained by the feature and its amendments. No unrelated changes, existing default changes, or CHANGELOG entry were found. The HID wire surface and worker thread are approved architectural additions, rather than incidental scope creep.
Blockers
1. [P2] Fence and discard the old HID output before accepting a new HELLO
src/core/Ctr2UsbRelay.cpp:520-528, src/core/Ctr2HidPort.h:39-45, tests/ctr2_usb_relay_test.cpp:353-371.
onHello() terminates the old radio session but retains the shared port's output queue, m_reportCosts, and unsent-payload accounting. The port interface has no live queue-purge/generation operation. DeviceEndpoint::abort() clears only its incoming buffer. Consequently the new session inherits outstanding bytes from the old connection, and its READY is appended behind obsolete DATA/control reports.
Reproduced using the production relay, the test's injected HID port with acknowledgements withheld, and a generic loopback byte peer:
after HELLO: old queued payload=17, new session queued=17, unacked reports before=4 after=5
device stream after HELLO: STALE_OLD_SESSION
100 restarts without HID progress: queued reports 5 -> 105; state Relaying
The old reports occur before the new READY in FIFO order; this does not claim delivery after READY or prove how unreleased firmware consumes them. It does prove that the documented "discard all link state" guarantee is false: a restarted receiver sees obsolete reports while resynchronizing, and the new link's budget includes old output. The existing restart test writes its stale bytes after the old TCP connection has closed, so it misses already queued HID output.
Add a worker-side cancellation/generation fence before restart and fault recovery, including bookkeeping and acknowledgements already queued back to the owner. Clearing only the counters is insufficient because the worker still owns the bytes. Cover a restart with held acknowledgements and a partially transmitted message.
2. [P2] Give each new partial message its own timeout
src/core/Ctr2UsbRelay.cpp:465-474.
Timeout management runs once after a whole batch. If the batch completes message A and begins message B, isMidMessage() remains true and the already active timer for A is reused. A valid B can therefore be rejected long before its own one-second deadline.
Reproduction: start a 14-byte A, wait 800 ms, then deliver A's last report and B's header in one batch:
second message age on rejection: 149ms; rejected=1;
error=CTR2 message incomplete after 1000 ms;
first payload relayed=AAAAAAAAAAAAAA
The timer is a coarse Qt timer, so the precise early interval varies; the wrong-message deadline is deterministic. Stop the old deadline when a complete message is emitted. The existing tail of the method can then start a fresh deadline for B. Replace the inner completed-message loop with:
for (const Message& m : messages) {
m_incompleteTimer->stop();
onMessage(m);
if (!m_port) {
return;
}
}This exact change was compiled in a temporary source copy: B remained connected through the reproduction’s 400 ms observation window, and the original USB relay suite still passed, including its incomplete-message timeout case. A separate trickle check still timed out at 189 ms with a 200 ms configured deadline despite a fragment at 150 ms. Add coverage for completion-plus-next-header in one batch, while preserving the absolute deadline for successive fragments of the same message.
3. [P2] Stop must not synchronously wait for the HID write batch on the main thread
src/core/Ctr2HidapiPort.cpp:249-252,265-266, with :129-139.
Both close paths use BlockingQueuedConnection; close() also waits for the worker thread without a timeout. The worker processes up to 16 blocking writes before it returns to its event loop. Stop, radio-disconnect teardown, and app exit can therefore block the GUI behind the remaining batch, not just one in-flight write. This defeats the RFC amendment's reason for introducing the worker thread.
Reproduced with the unmodified production HID port and a stub hidapi that takes 250 ms per successful write, with no device or socket. Queue 20 reports and call closeAfter(CLOSED) after the first write begins:
hid_write delay=250ms; writes begun before Stop=1;
Stop elapsed=4245ms; writes after Stop=16;
GUI heartbeats during Stop=0
This is a main-event-loop test, not a claim of a hardware-observed hang. Make shutdown asynchronous, check cancellation between writes, and retain the worker/port until completion. Merely timing out wait() and destroying a still-running worker would introduce a lifetime bug. Pin responsiveness with an injected delayed-write test.
4. [P2] Bound control-report output while waiting for HELLO
src/core/Ctr2UsbRelay.cpp:514-515,538-550,588-597.
A device READY always invokes linkFault(), including when already waiting for HELLO. Each fault appends a CLOSED report and another zero-cost ReportCost. The TCP payload budget does not count these reports, and there is no active ByteRelay progress timeout in this state. A faulty controller repeatedly sending READY while output is stalled can grow the output/accounting queues without limit.
Socket-free reproduction with an injected port that never acknowledges output and both configured byte budgets set to 16:
READY reports=100000; outgoing unacknowledged CLOSED reports=100000;
state=1; lastError=CTR2 sent READY, which only the host sends
State 1 is Listening. The injected port only counts reports; the production relay itself retains all 100,000 accounting entries, and the real worker's enqueue() also has no cap. This is a separate path from restarting an active session: no HELLO or radio connection is needed. Count control/framing overhead against a hard report budget, coalesce repeated fault replies while awaiting resynchronization, and bound stalled control delivery outside an active session. Add an injected-port regression.
Nits — non-blocking
tests/tests.cmake:377-385,tests/ctr2_usb_relay_test.cpp:1-3,506-556: correct the socket disclosure. The PR body says the socket tests bind only loopback TCP, and the registration comment mentions only TCP. The USB test also binds UDP, including the production relay'sAnyIPv4ephemeral socket (src/core/Ctr2UsbRelay.cpp:270) and loopback UDP peers. Document all binds. These are tests of our relay with generic byte peers, not synthetic radio firmware. The operator was notified before execution and explicitly authorized both socket targets for this review.
Verification and limits
- All six focused tests passed against PR production sources in small isolated harness builds. The harnesses supplied only the
lcDeviceslogging definition instead of linking unrelated application components; the model harness used its injected USB backend without hidapi. Socket tests initially returned their documented skip code 77 under the sandbox; authorized runs outside it passed. No hardware was contacted. - Mutation checks: removing queued-byte budget accounting made
byte_relay_testfail 10 assertions; removing stop-on-radio-change madectr2_proxy_model_testfail 7; removing sequence validation separately in C++ and C made their respective codec tests fail. Temporary copies were restored; PR source files were not modified. - Surviving attacks included binary payload preservation, partial/zero writes, connect/drain/progress timeouts, extra-client rejection, self-proxy refusal, malformed framing, counter wrap, C/C++ vector interoperability, same-radio updates, and radio-switch/disconnect teardown. The four new reproduction cases above fail guarantees not exercised by the existing tests.
- The four engine/touchpoint/capability/command-plane checks pass on both merge base and head. Engine and command-plane findings are identical after normalizing shifted line numbers; no lateral vendor include or baseline weakening appeared. Touchpoint growth is disclosed and justified. Test registration and the applet's accessibility lint pass. The separate model-invocable
code-reviewskill was unavailable; this review used direct inspection and independent focused passes. - Full Debug application build passed with Qt 6.12 and hidapi enabled. The normal CMake/CTest
byte_relay_testandctr2_proxy_model_testtargets also passed. Drove this build offscreen using isolated settings and explicit socketaethersdr-pr6090-gui, with TX disabled; connected onlyDEMO-0001, verifiedmodel: AetherSDR Demo,connected: true,transmitting: false, then disconnected and closed the instance. The widget tree reportedState: Stoppedand disabled Start withThis radio does not accept another client alongside AetherSDR; after disconnect the reason becameConnect AetherSDR to a radio first. The applet was hidden by default; a mode-change invocation was refused as hidden, so mode interaction and persistence were not verified through the GUI. Successful relaying is unavailable on the demo because it lacks concurrent-client support; relay failures above were reproduced in focused production-code harnesses. - All seven GitHub checks reported success for this head. The earliest run was created at
2026-10-02T21:25:38Z; the current-main snapshotf2acec1ewas committed at2026-10-02T23:27:17Z. The CI workflow usespull_request, so its green refers to the merge result at run creation, not today's main. Required-check strictness is false. A fresh merge-tree computation against fetched main succeeds, but that is not a build/test of the new merge. - USB hardware interoperability, macOS/Windows HID behavior, firmware agreement, and CW/PTT release on transport loss remain unverified. Those limits are already disclosed; no live-radio or transmit experiment was attempted.
Recommendation
Request changes. Resolve the four USB failure paths and add the targeted regressions before treating Stage 2 as complete. The Wi-Fi design and scope review survived; the remaining hardware questions need firmware/operator evidence, not a simulated radio peer.
Posted as COMMENT because the authenticated account is also the PR author; GitHub rejects a self-review with REQUEST_CHANGES. The review recommendation remains Request changes.
…ol replies. Principle VIII. Addresses the review on #6090. - Restart fence: Ctr2HidPort gains discardQueued(), a generation fence that drops reports queued before it and ignores late acknowledgements from before it. HELLO, link faults and error ends call it, so nothing from a previous link reaches the CTR2 after it restarted and the new link's budget starts empty. Spec: after HELLO the device skips reports until a READY or CLOSED header (data reports never start with 0xFF), then resets its receiver; the reference README shows it. - Non-blocking Stop: Ctr2HidPort::shutdown() replaces close/closeAfter. The new Ctr2HidThreadPort runs device I/O over injectable operations, checks cancellation between writes, and deletes itself when its thread finishes, so Stop, radio-change teardown and app exit never wait on a write. Ctr2HidapiPort now only enumerates and opens. - Per-message deadline: a completed message stops the incomplete-message timer, so the next message in the same batch gets its own second. - Bounded control output: one CLOSED per link, READY ignored while no link is up, and a hard cap on queued reports. - UDP socket bound to the interface that reaches the radio. - The radio's own port (RadioInfo::port) is used instead of 4992. - MIT notice on the firmware reference names its single author. - Tests: ctr2_hid_thread_port_test (slow writes, fence, reads, failures); ctr2_usb_relay_test gains restart with held acks and a partial message, stalled restarts, per-message deadline, bounded control output, and a second fault in one episode; the socket disclosure now lists UDP. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
|
Thanks to both reviewers. Every item is addressed in 218ceb1.
Mutation checks: I removed each guard in turn: the fence, the per-message timer stop, the CLOSED-once guard, READY-while-waiting, cancellation between writes, and the generation check on acknowledgements. Each removal turned its test red, and all were restored. Results: all seven CTR2 suites pass locally. The relay and thread-port tests passed 25 out of 25 repeated runs. Every static-check script passes, plus the colour ratchet (+0) and a11y. Built on Linux only; macOS and Windows coverage comes from CI. Unchanged and still unverified: USB hardware interoperability, macOS and Windows HID behaviour, and whether the radio releases transmit when a client's connection drops. |
…ple VI. Adds an off-by-default CTR2 Proxy applet that relays one stock CTR2-Max Wi-Fi connection to a configured radio over a dedicated upstream TCP connection, forwarding every byte unchanged in both directions. - ByteRelay: transport-agnostic bidirectional pump with 256 KiB per-direction budgets, 64 KiB yield slices, progress/drain timeouts. - TcpByteProxy: explicit IPv4 listener, one client, fresh upstream per connection, generation-isolated sessions, bind conflicts reported without fallback, self-proxy endpoints refused. - Ctr2ProxyModel/Ctr2ProxyApplet: validated endpoints frozen while running, state, endpoints, byte counters, last local error. The CTR2 is an independent radio client: its traffic never passes RadioModel, backend sockets or TxCoordinator. Recorded in docs/ctr2-tcp-proxy-design.md for RFC review. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…VII. Folds the host-side half of docs/ctr2-usb-relay-design.md into the branch so the CTR2 firmware has a concrete counterpart while the HID agreement is settled. - Ctr2HidFraming: encoder and fail-closed reassembler for the proposed 8-byte envelope (header with marker, version, counter, packet count, exact length; data reports of counter + 7 bytes). All negotiable values live in one WireFormat struct; nothing is agreed yet. - ctr2_hid_framing_test: every byte value, 7-byte boundaries to 65,535, padding, counter wrap, sender chunking, marker-valued payload, each rejection class, sticky failure until reset. - docs: USB relay design added with a host-side status section and the list of items needed from the firmware side; TCP design cross-reference updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ciple VI. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e C. Principle VII. Adopts the CTR2 firmware's proposed HID layout as link version 0: report ID 0x01, 0xFF start marker, 7-bit message counters, big-endian packet and exact payload counts. Adds message types in the spare header byte (DATA/HELLO/READY/CLOSED) so the link start, radio-ready and disconnect map onto the firmware's existing Wi-Fi socket events. No checksum in v0. - Ctr2HidFraming: v0 encoder and fail-closed reassembler. - tools/ctr2-firmware-reference: portable C99, no-heap encoder/receiver, MIT-licensed so it can be copied into the controller firmware. - Tests: published known-answer vectors, round trips, counter wrap, resync on HELLO, each rejection class; the reference compiled as C and cross-checked against the application codec in both directions. - docs/ctr2-usb-relay-design.md: full v0 specification, descriptor addition for the host-to-device Output report, message flow, vectors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…le VI. Adds a USB mode beside Wi-Fi in the CTR2 Proxy applet. The operator picks a HID interface (vendor usage page 0xFF00, usage 0x01) and the radio endpoint and presses Start; each HELLO from the controller gets a fresh dedicated radio TCP connection, announced with READY, and every end of that connection is announced with CLOSED. Bytes are relayed unchanged through the existing ByteRelay. - Ctr2HidPort / Ctr2HidapiPort: report I/O on one dedicated worker thread, because hidapi reads and writes block (RFC #6091 amendment). - Ctr2UsbRelay: link state machine; framing faults, data before READY, an incomplete message after 1 s, or an unplug close the radio connection and send CLOSED. 16 KiB per-direction budget to suit the ~7 KB/s HID link. - TcpSocketEndpoint shared by the TCP proxy and the USB relay. - ctr2_usb_relay_test: injected HID port plus a loopback byte peer for the radio; covers relay both ways, drain-then-CLOSED, device CLOSED, HELLO restart isolation, faults, timeouts, unplug, backpressure, Stop. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rinciple VIII. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nciple VII. Keeps 0xFF as the only start byte; the type byte distinguishes TCP data, control and (later) UDP. The datagram format waits on which UDP flows the controller actually uses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Defines link message type 0x04 DATAGRAM: one whole UDP datagram (up to 1472 bytes) prefixed with the radio's UDP port. The host relays them through one UDP socket per link; the radio learns that socket from the CTR2's own registration datagram, so no port is negotiated, no TCP command is parsed and AetherSDR's own UDP sockets are never involved. Radio datagrams come back whole with their source port; only the configured radio's datagrams are relayed, and a backed-up link drops datagrams (counted) rather than delaying the TCP stream. - Codec, MIT C reference (ctr2_tx_send_datagram), vectors and spec. - Tests: datagram round trips and limits, cross-check with the C reference, relay both ways with boundaries and ports kept, foreign sources ignored, flood dropped with TCP intact, datagram before READY. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CTR2 firmware enumerates with its ESP32-S3 board's product string: ESP32S3_DEV (CTR2-Max, CTR2-Nano), STAMP-S3 (M5Dial units) and XIAO_ESP32S3 (CTR2-MIDI). Label those and list them first; the operator still picks the device because the strings are generic board names. Records the firmware author's confirmation of link v0 in the design. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…III. The operator no longer types the radio's address: MainWindow pushes the connected radio into Ctr2ProxyModel, which captures it at Start and never retargets a running relay. Start stays dimmed with a stated reason when AetherSDR is disconnected, connected through SmartLink, or on a radio without multi-client sessions (a capability check, not a family test). - Applet: the Radio row is a read-only line naming the radio. - ctr2_proxy_model_test: socket-free destination and reason handling. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… VI. AetherSDR's transmit indicator is the operator's view of what the relayed CTR2 can key, so the relay must not outlive AetherSDR's connection to that radio. When AetherSDR disconnects, switches radios, or the radio becomes unusable for the relay, the model stops it: the CTR2 gets CLOSED (USB) or its connection closed (Wi-Fi), and the applet says why. An info update about the same radio keeps it running. - Ctr2ProxyModel: USB device list and port opener are injectable (hidapi by default), so the teardown is tested on a real relay. - ctr2_proxy_model_test: same-radio update, switch, disconnect, restart, manual Stop. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e VIII. Uses the firmware author's reported IDs: 303A:1001 with ESP32S3_DEV (CTR2-Max/Nano) or M5STACK_DIAL (M5Dial units), and 2886:0056 with XIAO_ESP32S3 (CTR2-MIDI). Requiring both stops a generic ESP32-S3 board or a stray product string from being labelled a CTR2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ol replies. Principle VIII. Addresses the review on #6090. - Restart fence: Ctr2HidPort gains discardQueued(), a generation fence that drops reports queued before it and ignores late acknowledgements from before it. HELLO, link faults and error ends call it, so nothing from a previous link reaches the CTR2 after it restarted and the new link's budget starts empty. Spec: after HELLO the device skips reports until a READY or CLOSED header (data reports never start with 0xFF), then resets its receiver; the reference README shows it. - Non-blocking Stop: Ctr2HidPort::shutdown() replaces close/closeAfter. The new Ctr2HidThreadPort runs device I/O over injectable operations, checks cancellation between writes, and deletes itself when its thread finishes, so Stop, radio-change teardown and app exit never wait on a write. Ctr2HidapiPort now only enumerates and opens. - Per-message deadline: a completed message stops the incomplete-message timer, so the next message in the same batch gets its own second. - Bounded control output: one CLOSED per link, READY ignored while no link is up, and a hard cap on queued reports. - UDP socket bound to the interface that reaches the radio. - The radio's own port (RadioInfo::port) is used instead of 4992. - MIT notice on the firmware reference names its single author. - Tests: ctr2_hid_thread_port_test (slow writes, fence, reads, failures); ctr2_usb_relay_test gains restart with held acks and a partial message, stalled restarts, per-message deadline, bounded control output, and a second fault in one episode; the socket disclosure now lists UDP. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…inciple VIII. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
218ceb1 to
345f9dd
Compare
|
The branch is rebased onto current main ( |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Lynn reports the radio's status stream arriving at a CTR2 over the USB link with a test build of this branch (radio to CTR2 only). Recorded as his report; the other directions and failure paths remain untested on hardware. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
rfoust
left a comment
There was a problem hiding this comment.
Implements approved RFC #6091, including its USB-thread, UDP, and radio-following amendments. The one reproduced blocker is fixed in signed commit a41fe84: a second incomplete-message fault now preserves the CLOSED notification already queued on a stalled HID port. No remaining actionable blockers or nits from this review.
| Scope | Claimed/approved | Verdict |
|---|---|---|
| ByteRelay, TCP proxy, socket endpoint | RFC and PR body | In scope |
| USB framing, HID I/O, USB relay and UDP | RFC amendments and PR body | In scope |
| Model, applet, MainWindow wiring | RFC radio-following amendment | In scope |
| Tests and CMake registration | Implementation and regression coverage | In scope |
| Firmware reference and transport designs | RFC and PR body | In scope |
| CMake source lists and touchpoint manifests | Required integration | In scope |
Everything in the diff is explained by the approved feature and its regression coverage; no unrelated changes, existing-UX preference changes, or CHANGELOG edits.
Verification: the repair merges cleanly with freshly fetched main 6fdadfa. Six socket-free suites passed in an isolated ARM64 component harness against installed Qt 6.11.2: byte_relay_test, ctr2_hid_framing_test, ctr2_hid_reference_test, ctr2_hid_thread_port_test, ctr2_proxy_model_test, and ctr2_usb_closed_retention_test. The new regression fails with the repair reverted (FAIL: the original unsent CLOSED survives the second fault) and passes with the repair restored. It holds HID output, injects connection refusal without opening a socket, times out an in-flight DATA header, verifies the exact CLOSED report survives, acknowledges it, and checks the next HELLO gets a fresh connection and its own CLOSED. Targeted mutations also made each of the five existing socket-free suites fail, followed by successful restored runs. Touchpoint manifest, strict registration, frozen CI gate, and whitespace checks passed.
I tested malformed framing and counters, slow-write shutdown, generation fencing, radio switching/disconnects, and alternate radio ports. No app was driven: the full local configure requires Qt 6.12 while this Mac has 6.11.2, so these results are component tests, not a full-app build or GUI proof. The two previously disclosed socket-owning targets (tcp_byte_proxy_test and ctr2_usb_relay_test) were neither built nor run locally. Hardware USB interoperability and mid-TX firmware teardown remain the PR's explicitly disclosed verification limits; no live radio or TX was used.
CI on reviewed head a41fe84: Linux build, macOS, Windows, Static checks, sanitizer configuration and both CodeQL jobs all passed. The macOS and Linux build logs explicitly compile and link the new regression target. CI covered hosted merge 2f2c930 against base 5ef09b0. Main advanced during CI with HL2 DSP PR #6050; I inspected the intervening change, constructed a clean merge against current main 6fdadfa, and reran all six focused suites plus registration/manifest checks successfully. The intervening change does not modify the CTR2 paths; no full local application build against that newer base is claimed. All four live review threads are resolved.
Approve. The CLOSED loss is independently reproduced, repaired and mutation-pinned; the feature scope follows the maintainer's RFC decisions.
…6114) ## 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. ```text Start ─► AetherSDR: HELLO (repeated ~1 s until answered; every 5 s after a failure) CTR2: READY ("I'm here") AetherSDR: opens a fresh radio connection AetherSDR: READY (connected) or CLOSED (radio unreachable; calls again later) ``` ## Behaviour - **Calling:** the host sends HELLO on Start and whenever no link is up. It never queues a HELLO while earlier output to the CTR2 is undelivered, so a controller that isn't reading can't accumulate them, and a pending CLOSED always goes out first. - **Answering:** the CTR2's READY opens a fresh radio connection only as the answer to a HELLO the host has sent since the last link ended. Any other READY is a late answer to an earlier HELLO and is ignored: one during a link, one during the back-off after a radio failure, or one while a CLOSED is still undelivered. It never discards a pending CLOSED or skips the back-off. A READY during a link was previously treated as a fault. - **Drain first:** after the CTR2 sends CLOSED, no HELLO goes out until the old session has finished draining the CTR2's last bytes to the radio. - **Back-off:** after a failed radio connection or any link fault (framing error, incomplete message), every call waits 5 s until a link ends cleanly or Start is pressed again. A stray CLOSED never restarts the call timer. - **Stop then Start:** the old link's CLOSED reaches the CTR2 before the new link's first HELLO. A HID port reopened on the same device waits on its own I/O thread, 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. A port the relay refuses is handed back with `shutdown()`, which never blocks. - **Restart:** a HELLO from the CTR2 still forces a restart, for error recovery. - **Unchanged:** the fence on restart, CLOSED retention (#6090 follow-up, now also covering a late READY), teardown when AetherSDR leaves its radio, and UDP. ## 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.md` and `tools/ctr2-firmware-reference/README.md` show the new flow. The C reference code itself is unchanged; only its header comments moved. ## Validation (Linux) - New and updated tests in `ctr2_usb_relay_test`: - Start calls with HELLO (counter 0); - HELLO is repeated until answered and stops once linked; - HELLOs never stack on a stalled port; - no HELLO is queued behind an undelivered CLOSED; - back-off after a radio failure, held for every call, and not postponed by stray CLOSEDs; - a late READY during a link changes nothing; - no HELLO while a session is still draining (radio writes stalled by an injected socket). - Links in `ctr2_usb_relay_test` come up the field way: the host's HELLO answered by the device's READY. Device-sent HELLO is covered as the restart case. - New in `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 in `ctr2_proxy_model_test`: a port the relay refuses is shut down, not deleted in place. - New in `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. - **Mutation checks:** each new guard was removed in turn, and each removal turned its test red: HELLO on Start, READY accepted while calling, no stacking, back-off, back-off held for every call, stray CLOSED not restarting the timer, late READY ignored, READY only after a fresh HELLO, no HELLO mid-drain, the reopen wait, release on close, and shutdown of a refused port. - Rebased onto current main (`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 every `static-checks.yml` script passes. - **Socket use:** `ctr2_usb_relay_test` binds **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_test` opens no sockets: its radio socket's `connectToHost()` only queues an injected refusal. Both are registered in `tests/tests.cmake` only; the frozen per-PR CTest gate is unchanged. - Not verified: the new flow against real CTR2 firmware, which needs the firmware update this describes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…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>
|
CTR2 firmware is connecting to AetherSDR using the HID protocol. It is receiving and parsing the initial data stream from the radio. However, the USB proxy doesn't appear to be forwarding data packets (API commands) from CTR2 on the the radio. No API command replies from the radio are received by CTR2. After I send the commands to subscribe to various services in the radio (sub pan all, sub radio all, sub slice all, sub tx all, sub atu all, and sub client all), AetherSDR sends CTR2 a CLOSE command, then issues a HELLO command and starts the initialization process over again. I will send you a beta version of CTR2-Max with logging enabled to try on your end. |
Summary
Implements approved RFC #6091: an operator-enabled relay that connects a CTR2-Max controller to a radio through AetherSDR, forwarding every byte unchanged in both directions. Two modes, selected in the new CTR2 Proxy applet (Integration, off by default):
Transport boundary (decided in RFC #6091)
The relayed connection is an independent radio client. It never uses
RadioConnection,RadioModel, a backend socket,IRadioBackendorTxCoordinator. Anything the CTR2 sends reaches the radio unchanged, including CW/PTT commands.AetherSDR's own transmit paths are unchanged. The relay never keys, retries, replays or invents a key-up. RFC #6091 approved this under Principle VI: the relay carries traffic only after the operator explicitly enables it (it is off on every launch), and AetherSDR's existing transmit indicator shows when the radio is on the air. To keep that indicator meaningful, the relay only ever targets the radio AetherSDR is connected to, captured at Start, and stops if AetherSDR disconnects or switches radios. In USB mode the CTR2 gets CLOSED; in Wi-Fi mode its connection is closed. The applet says why. The applet states the boundary on screen.
For Lynn (firmware)
Everything is in
docs/ctr2-usb-relay-design.md→ USB link specification (version 0):FF, version00, 7-bit counter, type, packet count (in reports, including the header) and exact payload count, both big-endian.client udpport.tools/ctr2-firmware-reference/ctr2_link.{h,c}is a portable C99, no-heap encoder and receiver, MIT-licensed so it can be copied into the firmware. The test suite compiles it and cross-checks it against the host codec in both directions.What's in it
ByteRelayEndpointseam)src/core/ByteRelay.{h,cpp}src/core/TcpByteProxy.{h,cpp},src/core/TcpSocketEndpoint.hsrc/core/Ctr2HidFraming.{h,cpp}src/core/Ctr2HidPort.{h,cpp},src/core/Ctr2HidThreadPort.{h,cpp},src/core/Ctr2HidapiPort.{h,cpp}src/core/Ctr2UsbRelay.{h,cpp}src/models/Ctr2ProxyModel.{h,cpp}src/gui/Ctr2ProxyApplet.{h,cpp}AppletPanel,MainWindow_Controllers.cpp(setupCtr2Proxy()), a stop inMainWindow::closeEventtools/ctr2-firmware-reference/docs/ctr2-tcp-proxy-design.md,docs/ctr2-usb-relay-design.mdmodels/Ctr2ProxyModel.h→peripheral(ctr2)Behavior:
aether.devices.Validation (Linux only)
AetherSDRand the test tree on Arch Linux with Qt 6.12.0 and hidapi 0.15. macOS and Windows are covered by CI only, and only for earlier commits so far.byte_relay_test(socket-free): every byte value, randomly split and merged reads, partial and zero writes, budget and backpressure, the 64 KiB yield, both timeouts, Stop, generation isolation, drain after a clean close, write errors.tcp_byte_proxy_test(loopback): 2 MiB each way with radio output arriving first, extra-client rejection, refused and timed-out upstream, bind conflict, self-proxy refusal, close from either side, reconnect isolation, Stop.ctr2_hid_framing_test(socket-free): the published vectors byte-exact, round trips up to 20 KB, payload zeros kept, counter wrap, HELLO resync, each rejection class, partial messages held.ctr2_hid_thread_port_test(socket-free, injected device operations): 250 ms writes never block shutdown, the fence drops queued output and late acknowledgements, reads and failures surface.ctr2_hid_reference_test: the C reference built as C99 must match the vectors, interoperate with the host codec both ways, and reject the same malformed input. It is also clean under GCC-Wconversion -Wsign-conversionand Clang-Weverything.ctr2_proxy_model_test(socket-free, injected USB backend): Start's reasons for each unusable radio state; the relay keeps running on a same-radio update and stops (CTR2 told CLOSED, reason shown) on a radio switch or disconnect; restart and manual Stop.ctr2_usb_relay_test(injected HID port plus a loopback byte peer):static-checks.ymlpasses locally, including the touchpoint manifest, colour ratchet (+0) and a11y (0 findings).S…|radio slices=1 …) arriving over the HID link in 7-byte data reports. That covers HELLO, READY and DATA, from radio to CTR2 only.Socket-owning test disclosure
tcp_byte_proxy_testbinds 127.0.0.1 ephemeral TCP ports.ctr2_usb_relay_testbinds 127.0.0.1 ephemeral TCP, the relay's own ephemeral UDP socket on the radio-facing loopback address, and loopback UDP peers on 127.0.0.1 and 127.0.0.2. Our proxy or relay is the subject; the peers are generic byte endpoints with no radio protocol and no firmware stand-in. Both exit 77 (SKIP_RETURN_CODE) only when loopback can't be bound. They are registered intests/tests.cmakeonly; the frozen CI gate is unchanged.Not verified / limitations
🤖 Generated with Claude Code