Skip to content

feat(ctr2): CTR2 relay over Wi-Fi (TCP) and USB HID link v0. Principle VI. - #6090

Merged
rfoust merged 17 commits into
mainfrom
ctr2-tcp-proxy-prototype
Oct 3, 2026
Merged

rfoust merged 17 commits into
mainfrom
ctr2-tcp-proxy-prototype

Conversation

@ten9876

@ten9876 ten9876 commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

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):

  1. Wi-Fi (TCP). The stock CTR2 points at this PC's IP. For each CTR2 connection AetherSDR opens a fresh TCP connection to the configured radio and relays bytes unchanged. Works with today's firmware.
  2. USB. The CTR2 talks to AetherSDR over 8-byte HID reports using link format v0, adopted from the firmware author's proposal. AetherSDR relays TCP to the radio the same way, and relays UDP datagrams through one UDP socket per link. The host side is complete. The CTR2 USB firmware is in progress, and its author reports radio-to-CTR2 traffic working on hardware.

Transport boundary (decided in RFC #6091)

The relayed connection is an independent radio client. It never uses RadioConnection, RadioModel, a backend socket, IRadioBackend or TxCoordinator. 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):

  • Header: FF, version 00, 7-bit counter, type, packet count (in reports, including the header) and exact payload count, both big-endian.
  • Data reports: the counter, then 7 bytes, zero-padded.
  • Message types: DATA / HELLO / READY / CLOSED / DATAGRAM. DATAGRAM is one whole UDP datagram with the radio's UDP port.
  • Limits: 512-byte DATA, 1472-byte datagrams; no checksum in v0.
  • UDP registration: send a DATAGRAM to radio port 4992 rather than using client udpport.
  • Descriptor: needs an Output report (two lines) so the host can send to the CTR2.
  • Link flow: maps onto the CTR2's existing Wi-Fi socket connect and disconnect events.
  • Hex test vectors for checking an implementation by hand.

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

Piece File
Transport-agnostic bidirectional pump (ByteRelayEndpoint seam) src/core/ByteRelay.{h,cpp}
Wi-Fi: listener, single client, per-connection upstream src/core/TcpByteProxy.{h,cpp}, src/core/TcpSocketEndpoint.h
USB link v0 codec src/core/Ctr2HidFraming.{h,cpp}
USB HID port on its own I/O thread; hidapi discovery and open src/core/Ctr2HidPort.{h,cpp}, src/core/Ctr2HidThreadPort.{h,cpp}, src/core/Ctr2HidapiPort.{h,cpp}
USB link state machine, fresh radio connection per HELLO, per-link UDP socket src/core/Ctr2UsbRelay.{h,cpp}
Model: mode, validation, follows and is torn down with AetherSDR's radio src/models/Ctr2ProxyModel.{h,cpp}
Applet src/gui/Ctr2ProxyApplet.{h,cpp}
Wiring AppletPanel, MainWindow_Controllers.cpp (setupCtr2Proxy()), a stop in MainWindow::closeEvent
Firmware reference (MIT) tools/ctr2-firmware-reference/
Designs docs/ctr2-tcp-proxy-design.md, docs/ctr2-usb-relay-design.md
aetherd touchpoint tag models/Ctr2ProxyModel.h → peripheral(ctr2)

Behavior:

  • Bounds. Each direction has a bounded budget with backpressure: 256 KiB for Wi-Fi, 16 KiB for USB to suit the roughly 7 KB/s HID link.
  • Timeouts. 10 s connect, 10 s with no progress while bytes are queued, and 10 s drain on clean close. In USB mode, a message still incomplete after 1 s ends the link.
  • Wi-Fi mode. One client at a time; extras are closed without a byte. Bind conflicts are reported with no fallback, and a radio endpoint that points back at the proxy's own listener is refused.
  • USB mode. A framing fault, DATA or DATAGRAM before READY, or an unplug closes the radio connection and sends CLOSED. Only the configured radio's UDP is relayed back; datagrams beyond a ~3 KB backlog toward the CTR2 are dropped and counted rather than delaying TCP. DATA already in flight after a CLOSED is dropped quietly until the next HELLO.
  • Radio: no address entry. MainWindow pushes the connected radio in. Start is unavailable, with a reason, when AetherSDR is disconnected, on SmartLink, or on a radio without multi-client sessions; this is a capability check, not a radio-family check.
  • Nothing persists. IPv4 only. Logging uses aether.devices.
  • Threading. One dedicated HID I/O thread, used only in USB mode (RFC [RFC] CTR2 controller relay: opaque independent-client TCP proxy, then USB HID transport #6091 amendment). Nothing ever waits on it: queued output is cancelled between writes, a restart fences the queue and late acknowledgements, and Stop hands the port its final CLOSED and lets it delete itself. Everything else runs on the GUI event loop.
  • Restart: each HELLO and each link fault discards everything still queued for the CTR2 before the next READY or CLOSED. After HELLO, the device skips reports until a READY or CLOSED header. A link sends at most one CLOSED.

Validation (Linux only)

  • Builds: AetherSDR and 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-conversion and 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):
    • HELLO → READY with the radio greeting after it; 30 KB/40 KB relayed byte-exact both ways;
    • radio close drains and then sends CLOSED; device CLOSED drains to the radio;
    • HELLO restart gets a fresh connection with no stale bytes;
    • framing fault, DATA before READY, READY from the device, refused radio, connect timeout, incomplete-message timeout;
    • unplug, backpressure toward a slow device, Stop;
    • UDP both ways with boundaries and ports kept, foreign sources ignored, a flood dropped with TCP intact, a datagram before READY;
    • restart with acknowledgements held and a part-delivered message, 20 restarts against a stalled port, a fresh deadline for each message, control output bounded while waiting, one CLOSED per episode.
  • Stress: the loopback tests passed 15 to 30 repeated runs each.
  • Mutation checks: guards were broken on purpose across all five suites, and every one turned its test red. Two guards that survived taught us something: one was redundant and removed; the other was unreachable from its test, so a test was added.
  • Static checks: every script in static-checks.yml passes locally, including the touchpoint manifest, colour ratchet (+0) and a11y (0 findings).
  • GUI: launched with a scratch config and the automation bridge. Both modes render, controls for the other mode are dimmed with reasons, Start stays dimmed with a stated reason until the configuration is valid, and the USB list enumerates vendor-page HID devices. No relay was started by the agent and no device was opened.
  • Hardware (reported, not observed by the agent):
    • The operator reports the Wi-Fi relay working with a stock CTR2.
    • The firmware author reports the first USB result with a test build of this branch: his CTR2's debug screen shows the radio's status stream (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.
    • CTR2-to-radio commands, UDP, restart/CLOSED handling and unplug have not been exercised on hardware yet.

Socket-owning test disclosure

tcp_byte_proxy_test binds 127.0.0.1 ephemeral TCP ports. ctr2_usb_relay_test binds 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 in tests/tests.cmake only; the frozen CI gate is unchanged.

Not verified / limitations

  • USB end to end on hardware, and USB on macOS and Windows: HID report-ID handling differs per backend, and Linux will likely need a udev rule.
  • Wi-Fi mode relays TCP only; the CTR2 and radio exchange UDP directly. USB mode carries UDP at about 7 KB/s, so no panadapter, waterfall or audio streams, and no discovery broadcasts. SmartLink is not supported. MultiFlex binding is whatever the CTR2's own traffic does.
  • CW/PTT through either mode, including what the radio does when the CTR2's connection drops mid-transmission.
  • The USB device list shows every interface on usage page 0xFF00 until the CTR2's VID/PID is known.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@aethersdr-agent

Copy link
Copy Markdown
Contributor

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 failed

Only Static checks failed; the failing step is aetherd touchpoint manifest (.github/workflows/static-checks.yml:178, which runs python tools/gen_touchpoint_manifest.py --check). The step list is all the run log exposes, so I ran that same check locally on 3beedc91:

::error::aetherd touchpoints require a valid semantic tag: models/Ctr2ProxyModel.h

This is not CI infrastructure — the check is doing exactly its job. src/gui/Ctr2ProxyApplet.cpp, src/gui/MainWindow.cpp and src/gui/MainWindow_Controllers.cpp all #include "models/Ctr2ProxyModel.h", so that header is a new engine/UI touchpoint for the aetherd burndown (docs/aetherd-headless-engine-design.md §2/§10), and every live touchpoint needs a semantic tag. Nothing in docs/architecture/ is touched by this PR.

Worth noting: core/ByteRelay.h, core/TcpByteProxy.h and core/Ctr2HidFraming.h are not flagged, because the applet only reaches the model. That's the right shape — only the one header needs a tag.

Fix (two steps, both required)

1. Hand-add one entry to docs/architecture/aetherd-touchpoint-tags.json, keyed models/Ctr2ProxyModel.h. Copy the shape of an existing peripheral entry such as models/AntennaGeniusModel.h (tag, note, split, confidence). Accepted tag shapes are universal, ui-support, vendor(<family>), mixed(<family>), peripheral(<family>), family lowercase alphanumerics plus ._-.

A suggestion, not a ruling — classification is yours, and this PR is RFC-pending anyway: peripheral(ctr2) looks like the fit. The legend defines it as "standalone accessory transport outside the radio-family seam", which is your own framing of the transmit boundary ("independent radio client… never uses RadioConnection, RadioModel, a backend socket, IRadioBackend or TxCoordinator"), and it matches the precedent set for models/AntennaGeniusModel.h and core/TgxlConnection.h.

2. Regenerate the table and commit both files:

python tools/gen_touchpoint_manifest.py     # note: no --check

Step 2 is not optional. main() validates tags first, then diffs the committed table — so adding the tag alone just moves the failure to ::error::aetherd-touchpoints.md is stale. The table is stale regardless, because Ctr2ProxyApplet.cpp also bumps core/ThemeManager.h's includer count. Never hand-edit docs/architecture/aetherd-touchpoints.md; it's generated.

I verified this locally on your head: with the entry added and the generator run, --check prints manifest up to date. The regenerated diff is three lines — totals 231 → 232 (37 → 38 models), core/ThemeManager.h 156 → 157, and the new models/Ctr2ProxyModel.h row.

The other reviewer comment

The a11y bot's 4 findings are not yours. They're all in src/gui/MainWindow.cpp around lines 10052–10100 (m_swrScanAction, m_preTuneAction, m_clearAtuAction, m_agcTCalibrationMenuAction) — the scanner reports per changed file, and you touched that file. Running tools/check_a11y.py src/gui/MainWindow.cpp on clean main gives the same 4. Warnings only, nothing to do here. There is no Copilot review on this PR.

Scope

build, check-windows and check-macos were still in progress when I looked, so this covers Static checks only — your PR body already flags that macOS and Windows weren't built locally, so those are worth a second look once they report.

🤖 Generated with Claude Code


🤖 aethersdr-agent · cost: $5.3324 · model: claude-opus-5

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

2 similar comments
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@ten9876 ten9876 changed the title feat(ctr2): opaque TCP proxy prototype + draft USB HID framing (RFC pending). Principle VI. feat(ctr2): CTR2 relay over Wi-Fi (TCP) and USB HID link v0. Principle VI. Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

2 similar comments
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@ten9876
ten9876 marked this pull request as ready for review October 2, 2026 22:37
@ten9876
ten9876 requested review from a team as code owners October 2, 2026 22:37

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.h Tuning::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 refuses 0.0.0.0 for 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) in onConnected() 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): BlockingQueuedConnection into the worker plus a bare m_thread->wait(), with the comment itself conceding "a few seconds". closeAfter() does the same at :266. This runs from MainWindow::closeEvent and from any stop(). The house pattern is a bounded wait(ms) with a qCWarning on expiry.
  • The upstream port is hardcoded to 4992 (Ctr2ProxyModel.cpp:250 and :317) while RadioInfo::port exists and RadioModel itself 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 in ConnectionPanel.cpp sets 4992, and the relay is gated to hasMultiClientSessions which only Flex sets true — so this is latent coupling, not a current defect. One-line fix: push lastRadioInfo().port through setAetherRadio().
  • 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 under src/ or tools/. 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_HIDAPI not 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_SOURCES compiles into aethercore (CMakeLists.txt:1577) and the define is target_compile_definitions(aethercore PUBLIC HAVE_HIDAPI) at :2747, so Ctr2ProxyModel.cpp's #ifdef sees it. Also checked that the applet is reachable: AppletPanel constructs it, appletCatalog() lists it, and setupCtr2Proxy() wires setModel() — no dead additions.
  • FrameReassembler against hostile reports. Tried length/packet-count disagreement (packets != packetsFor(length) rejects it), Datagram with length 2 to make m_partial[0..1] read out of bounds (the length >= 3 bound 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 above 0x04. Every error is sticky until reset() 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 calls notifyClosed(Side::A) from inside maybeFinishDrain()'s own closeGracefully(dst) call, which re-enters ByteRelay::finish(). It survives: m_closingGracefully is set before either close and m_finished guards every subsequent path, so there's no double-finish and no double CLOSED (the sendClosed = error wiring means a graceful close never sends a second one).
  • Byte loss across partial writes and backpressure. Walked pump()'s d.pending = chunk.mid(accepted) against the while (… && d.pending.isEmpty()) loop condition and flushPending()'s std::min(n, pending.size()) clamp — no double-count, no overwrite. When the loop exits on room <= 0 no continuation is scheduled, but resumption rides notifyBytesWritten(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, …) before deleteLater(), accepted sockets reparented to Session (so delete m_server can't free them), and ~Session deletes the relay before its socket children. The hidapi worker's invokeMethod(m_port, …, Queued) uses the port as context, so pending deliveries drop when it's destroyed. delete hid on a failed Ctr2UsbRelay::start() is not a double-free — every false return precedes m_port->setParent(this).
  • Attacked the tests rather than trusting them. byte_relay_test asserts byte-for-byte equality in both directions, budget ceilings, discardedA/B counts, 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, including check-macos and check-windows — so the "Linux only" caveat is about runtime validation; compilation is verified on three platforms. There is no Analyze (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 TxCoordinator bypass and the unauthenticated TCP listener (both ruled on in #6091 — and TxCoordinator.h:16-18 really 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

Comment thread src/core/Ctr2UsbRelay.cpp
Comment thread src/core/Ctr2UsbRelay.cpp Outdated
Comment thread src/core/Ctr2HidapiPort.cpp Outdated
Comment thread src/models/Ctr2ProxyModel.cpp Outdated
@ten9876 ten9876 self-assigned this Oct 3, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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's AnyIPv4 ephemeral 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 lcDevices logging 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_test fail 10 assertions; removing stop-on-radio-change made ctr2_proxy_model_test fail 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-review skill 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_test and ctr2_proxy_model_test targets also passed. Drove this build offscreen using isolated settings and explicit socket aethersdr-pr6090-gui, with TX disabled; connected only DEMO-0001, verified model: AetherSDR Demo, connected: true, transmitting: false, then disconnected and closed the instance. The widget tree reported State: Stopped and disabled Start with This radio does not accept another client alongside AetherSDR; after disconnect the reason became Connect 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 snapshot f2acec1e was committed at 2026-10-02T23:27:17Z. The CI workflow uses pull_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.

@ten9876 ten9876 removed their assignment Oct 3, 2026
ten9876 added a commit that referenced this pull request Oct 3, 2026
…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>
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@ten9876

ten9876 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks to both reviewers. Every item is addressed in 218ceb1.

Finding Fix Pinned by
Blocker 1 (both reviews): HELLO or fault keeps the old link's queued HID output Ctr2HidPort::discardQueued(), a generation fence that drops queued reports and ignores acknowledgements from before it. Called on HELLO, link faults and error ends; accounting reset. Spec and reference: after HELLO the device skips reports until a READY or CLOSED header (data reports never start with 0xFF), then resets its receiver. testRestartDiscardsQueuedOutput: held acks, a part-delivered message, only the new READY queued, empty budget, clean resync; 20 stalled restarts stay at 2 reports or fewer
Blocker 2: the next message reused the previous message's incomplete-message deadline Your suggested change: each completed message stops the timer, so a new message gets a fresh second while one message keeps its absolute deadline testEachMessageGetsItsOwnDeadline: completion plus next header in one batch; a trickled message still times out
Blocker 3 (and the bot's nit): Stop blocks the GUI on HID writes shutdown(finalReports) replaces close/closeAfter. The new Ctr2HidThreadPort cancels between writes, writes only CLOSED, and deletes itself when its thread finishes; nothing waits, not even with a timeout ctr2_hid_thread_port_test: 250 ms writes, shutdown() returns in under 50 ms, event loop keeps running, at most the in-flight write plus CLOSED
Blocker 4: unbounded CLOSED replies while waiting for HELLO One CLOSED per link episode; READY ignored while no link is up; hard cap on queued reports testControlOutputBoundedWhileWaiting: 1000 READYs answered with nothing, repeated faults and a second fault in one episode give one CLOSED
Nit: UDP socket bound to every interface Bound to the interface facing the radio –
Nit: radio port hardcoded to 4992 RadioInfo::port pushed through; a port change counts as a different radio ctr2_proxy_model_test
Nit: MIT notice says "and AetherSDR contributors" Names the single author only –
Nit: socket disclosure omits UDP tests.cmake and the PR body now list the relay's UDP socket and the loopback UDP peers –
Body: USB 16 KiB budget undisclosed; RFC body still mentions a typed radio address RFC #6091 and this PR's body updated; the design doc states the budget –

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.

ten9876 and others added 9 commits October 2, 2026 19:54
…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>
ten9876 and others added 6 commits October 2, 2026 19:54
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>
@ten9876
ten9876 force-pushed the ctr2-tcp-proxy-prototype branch from 218ceb1 to 345f9dd Compare October 3, 2026 02:57
@ten9876

ten9876 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

The branch is rebased onto current main (ad46155dc), so the review fixes referenced above as 218ceb1 are now 2b9c011. 345f9dd regenerates the aetherd touchpoint manifest, which had gone stale against the newer main and failed Static checks. Static checks now pass locally on the rebased tree, and the seven CTR2 suites pass.

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

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>
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@rfoust rfoust self-assigned this Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 7
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@rfoust rfoust left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@rfoust
rfoust merged commit 732b99a into main Oct 3, 2026
7 checks passed
@rfoust
rfoust deleted the ctr2-tcp-proxy-prototype branch October 3, 2026 04:07
ten9876 added a commit that referenced this pull request Oct 3, 2026
…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>
ten9876 added a commit that referenced this pull request Oct 4, 2026
…lay, Qt 6.12 (#6159)

## Summary

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

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

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

## Changelog

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

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

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

## Docs refresh, not just version strings

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

## Judgment calls flagged for the maintainer

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

## Scope

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

## Validation

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

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

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

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Lynovation

Copy link
Copy Markdown

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.
Lynn

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants