Repository navigation
fix(hl2): discover on every local interface - #6009
Conversation
|
One gap, for #5569 specifically: this fixes discovery and carries the replying interface into the stream. The manual-IP path the issue is about also goes through That matches the reporter's note that the Explicit-source workaround works. Without a change there, "connect by IP in Auto mode" can still pick the wrong interface on a multi-homed machine. Happy to send that half separately if you would rather keep this PR to discovery. ON8ST 🤖 Generated with Claude Code |
|
Thanks for identifying this gap. The manual-IP path is now covered in In Auto mode, the HL2 probe binds one UDP socket per active local IPv4 address, sends the directed probe from each socket, and retains the local address of the socket that receives the valid HL2 reply. That session address is passed through the HL2 connect request and automatic reconnect path, so the Metis stream stays on the same interface. Explicit source selection remains unchanged, and Flex/ANAN paths are untouched. Validation after the change:
|
|
Follow-up fix in 334b780: manual source selection is now preserved during interactive IP editing and connection. The saved per-radio route is restored only for startup or an explicitly selected recent address, so choosing a specific interface cannot be silently reset to Auto. Validation: local Windows AetherSDR build succeeds; hl2_metis_protocol_test, hl2_hardware_options_test, and hl2_receivers_test all pass; git diff --check passes. The commit is SSH-signed and pushed to the PR branch. |
|
The local test package was also corrected: the Qt runtime dependencies were deployed next to AetherSDR.exe, including Qt6OpenGLWidgets.dll. The rebuilt executable now launches successfully as AetherSDR v26.9.5 and remains responsive. This was a packaging/runtime issue only; no source changes were needed for the deployment fix. |
|
Thanks. I read e0196c2 and 334b780 at 334b780, and the gap I raised is closed:
What I have not done is run this on our own HL2, which sits on a routed subnet behind a gateway from this Mac. That is a code reading, not a test. 73, ON8ST |
|
@dacacioa, this no longer merges onto Merging 73, ON8ST |
ten9876
left a comment
There was a problem hiding this comment.
Reviewed head 334b78036a542f8f30471864a4ad5556190fc405.
Issue fit: The implementation addresses #5569's multi-interface failure: broadcast discovery binds per IPv4 interface, manual Auto probes all candidates, and the observed local address reaches the stream socket and reconnect request. The explicit-source reporter workaround supports the diagnosis. I could not independently reproduce the multi-NIC Windows/hardware exchange; the seam violation below must be addressed before merging.
Scope
| File/group | Change | Claimed? | Verdict |
|---|---|---|---|
Hl2Discovery.{h,cpp} |
Refresh per-interface sockets and directed/global probes | Yes | Direct issue fit |
MetisClient.{h,cpp}, Hl2Backend.cpp |
Probe source selection and stream bind | Yes | Direct issue fit |
RadioModel.cpp |
Pass source through initial/reconnect parameters | Yes | Needed behavior; new family branch violates standing seam notice |
ConnectionPanel.cpp probe |
Auto candidate fan-out, retain winning source | Yes | Direct issue fit |
ConnectionPanel.cpp address-edit/profile logic |
Preserve explicit selection while typing, across families | Not disclosed as cross-family behavior | Issue-related; update scope claim |
All three authored commits concern discovery/source selection. No added tests, new dependency, capability flag, or changelog edit.
Blocker
- Do not add a family-string branch above the backend seam —
src/models/RadioModel.cpp:693–700addsfamily.compare("hl2", …)to inject a backend-specific parameter.docs/agents/backends.md, standing notice #5554, explicitly says: “no newusesFlexCommandPlane()/ family-string branches” before submission in this territory. Existing neighboring family branches are tracked debt, not authorization to grow it. Preserve the source address through neutral connection metadata, with interpretation inHl2Backend, or obtain an explicit maintainer-approved seam design. All gates are green because none detects this semantic family check; that does not satisfy the standing notice.
Nits / limits (non-blocking)
src/gui/ConnectionPanel.cpp:2685–2694,2756–2763: the body says Flex and ANAN connection paths are untouched, but these shared address-edit/profile handlers are unconditional. They now preserve an explicit source instead of restoring another address's profile during an interactive edit. Disclose the cross-family scope and verify editing A→B versus selecting B from recents. This is a scope/verification limitation, not an independently reproduced GUI regression.src/core/backends/hl2/Hl2Discovery.cpp:277–299: broadcast results still do not fillRadioInfo::sessionBindAddress. The current backend can discover a source again during its receiver-count probe, but that is a second discovery, not preservation of the original winning broadcast path as the summary implies. Carry the observed source or narrow the claim; ensure a knownboardMaxRxdoes not skip the only remaining source-discovery opportunity.src/core/backends/hl2/MetisClient.cpp:264–267: the first idle socket may consume the entire remaining timeout before later sockets are inspected. The manual probe already uses bounded 10 ms waits. Apply equivalent bounded/fair polling here if prompt connection matters on machines with many adapters; this latency concern is reasoned from code, not reproduced on the reporter's host.
Verification and attacks
- No added/modified test owns sockets; no synthetic radio peer was created. Read explicit-bind failure, no candidate, bind failure, malformed/non-HL2 reply, in-use reply, socket replacement/stop, and reconnect paths. The manual in-use refusal and explicit bind-error reporting survive.
- Built and ran the head's actual socket-free
hl2_metis_protocol_testwith GCC 16.2.1/C++20. It passes; it checks codec compatibility, not interface routing. The existing tests listed in the body do not exercise the changed multi-interface lifecycle. - All four engine boundary, touchpoint, capability-record and command-plane gates pass on merge base and head with byte-identical output. No manifest/baseline was weakened; the family-check blocker is invisible to these gates. Read the backend thread contract; the new local-address discovery still uses the existing blocking I/O-thread invocation.
- No full-app/bridge run: the key claims require HL2 replies and a multi-homed host; SimBackend cannot establish that routing behavior. Windows/real-radio results remain author/reporter evidence. An isolated refusal/routing-policy test would be appropriate; a fake firmware peer would not.
- Automated code-review skill unavailable; independent manual audit and focused codec execution performed.
CI and recommendation
Request changes for the new above-seam family branch. Current check rollup is empty. Earliest historical run is 2026-09-29T17:46:37Z, before main's 2026-10-04T18:26:40Z; those runs do not establish today's merge compatibility, and non-strict branch protection does not force a refresh. Resolve the seam routing and correct the scope claims, then obtain required checks and reporter confirmation on the dedicated Ethernet path.
— Jeremy KK7GWY + Codex
ten9876
left a comment
There was a problem hiding this comment.
Independent re-review at head 334b78036a542f8f30471864a4ad5556190fc405. Same head as the earlier ten9876 + Codex review. I re-checked each of its findings: one is confirmed, two of its nits don't hold, and there is a new merge blocker.
Issue fit: yes, for the mechanism. #5569's root cause is a wildcard-bound probe that leaves via the route table's adapter rather than the HL2's link-local NIC. The reporter confirmed it in the thread: picking Ethernet 3 explicitly in Advanced fixes it. This PR does in Auto what that workaround does by hand, in three places:
- the broadcast sweep, with one socket per local IPv4 address;
- the manual-IP probe;
- the connect-time unicast probe, which also carries the winning local address into the stream socket's bind (
MetisClient.cpp:369-374).
Scope
| File/group | Change | Claimed? | Verdict |
|---|---|---|---|
Hl2Discovery.{h,cpp} |
Per-interface sockets rebuilt every sweep; directed and global broadcast | Yes | In scope |
MetisClient.{h,cpp}, Hl2Backend.cpp |
Per-interface discover(), Params::localAddress, stream bind |
Yes | In scope |
RadioModel.cpp |
Passes sessionBindAddress as hl2.localAddress on connect, reconnect and wake |
Yes | Needed, but the shape is a blocker (below) |
ConnectionPanel.cpp HL2 probe |
Auto probes from every candidate and keeps the winning socket's address | Yes | In scope |
ConnectionPanel.cpp onManualIpChanged / probeRadio |
Keeps an explicit source while typing; stops re-applying the saved profile on interactive connects | Partly: "explicit source selection remains unchanged" | Shared by every family's manual connect, and the body says Flex/ANAN are untouched. Disclose it. |
All three commits are on-topic and signed. No tests were added, and there's no CHANGELOG entry.
Blockers
-
This no longer merges with
main.git merge-tree origin/main <head>reports conflicts insrc/core/backends/hl2/Hl2Backend.cpp,src/gui/ConnectionPanel.cppandsrc/models/RadioModel.cpp.RadioModel::populateFamilyParamsin particular has moved under #5919 and the ANAN work. Rebase. The fix for blocker 2 changes that same function anyway. -
New family-string branch above the seam. This is the earlier review's blocker, and I confirm it.
src/models/RadioModel.cpp:693-702addsfamily.compare("hl2", …)to decide whether to forward a bind address.docs/agents/backends.md(standing notice #5554) says: "no newusesFlexCommandPlane()/ family-string branches". The existing neighbouring branches are tracked debt, not permission to add more.A "local interface that reached the radio" is not HL2-specific:
RadioInfo::sessionBindAddressis already family-neutral and the Flex probe fills it. So forward it without asking which family. For example, add a typed field toRadioConnectRequest(IRadioBackend.h:43) and set it unconditionally:struct RadioConnectRequest { QString host; quint16 port = 0; QString serial; // when a family identifies radios by serial QVariantMap params; // family-specific extras (namespaced by the backend) RadioSerialIdentity serialIdentity; // Local IPv4 address the probe reached the radio from; null = let the OS route. QHostAddress localBindAddress; };
Then
req.localBindAddress = info.sessionBindAddress;(andm_lastInfo.…/selectedRadio.…at the other two call sites).Hl2Backend::connectRadioreadsrequest.localBindAddressinstead ofparams["hl2.localAddress"]. Adding a field to the seam struct is itself a maintainer call. Keeping a params key but inserting it unconditionally would also clear the branch.
Nits (non-blocking)
- Two of the earlier review's nits don't hold.
- "Broadcast results don't carry the interface." True of
Hl2Discovery, but it doesn't matter: production never suppliesboardMaxRx. Only three tests set it (hl2_gain_restore_test.cpp:173,hl2_gain_split_test.cpp:113,hl2_slice_meter_lifecycle_test.cpp:78). So every real connect runs the per-interface unicastdiscover()(Hl2Backend.cpp:2796-2810), and that fillsmp.localAddressfrom the socket that got the reply. A broadcast-found radio is bound correctly too. - "The first idle socket eats the timeout." The total doesn't change: the old single-socket loop also ran to
timeoutMscollecting replies, and later sockets' datagrams are buffered and read on the next 1 ms pass. Connect latency is the same 400 ms as before.
- "Broadcast results don't carry the interface." True of
- Two families now sweep subnets two different ways. #6146 (ANAN, ruled acceptable as-is) uses one
AnyIPv4socket plus a directed broadcast per subnet, with the target list as a pure function and a socket-free test of it. This PR rebuilds a bound socket per address on every sweep (Hl2Discovery.cpp:179,212). Per-address binding is what makes the unicast probe and the stream work on the reporter's host, so keep it there. For the broadcast sweep, #6146's shape would be simpler and testable. Aligning the two is worth a follow-up. - Repeated interface walks. The NIC walk is hand-rolled in
Hl2Discovery::refreshSockets()andMetisClient::discover()(MetisClient.cpp:226) with the same filter asNetworkPathResolver::enumerateIpv4Candidates()insrc/core, which the ConnectionPanel half already uses (ConnectionPanel.cpp:3119).sweepNow()also callsQNetworkInterface::allInterfaces()once per socket per sweep to find that socket's broadcast address. That's cheap on Linux and noticeably slower on Windows. Carry theQNetworkAddressEntryfromrefreshSockets()instead. - Global broadcast from every socket. It goes out once per local address per sweep (
Hl2Discovery.cpp:207). On Linux a limited broadcast follows the routing table regardless of the source bind, so N copies leave the default interface with N different source addresses. That's harmless noise, but noise on every sweep. - Lost diagnostic.
ConnectionPanel.cpp:3164droppederrorString()from the Auto bind-failure message. Keep the last socket's error, as the send path does. - No tests were added. The per-interface target derivation can be tested without a socket (see #6146's pattern). A fake HL2 peer would not be acceptable.
Verification and what I tried to break
-
Socket preflight: no test was added or modified.
hl2_metis_protocol_testopens no sockets, and it passes at this head (it checks the codec, not routing). -
Built and ran the head offscreen, with isolated settings and TX disabled, and never connected to anything. Sampled the process's UDP sockets with
ss -uanpevery 4 s for 32 s:- one discovery socket per local IPv4 address (
192.168.50.236:*,100.86.176.80:*on the tailscale /32); - a fresh ephemeral port on each sweep;
- the count stayed at two the whole time, with no wildcard HL2 socket.
The per-sweep rebuild doesn't leak. Flex discovery on the shared
0.0.0.0:4992was unaffected:connect liststill showed the LAN FLEX and the demo. - one discovery socket per local IPv4 address (
-
Couldn't verify: a positive HL2 discovery or connect, because there's no HL2 on this LAN and canon forbids a synthetic peer. The multi-NIC Windows behaviour remains the reporter's evidence.
-
Read for regressions: the explicit-bind failure path, the in-use (
0x03) refusal, the non-HL2 board-id gate, and the reconnect path. All are preserved in the restructured probe loop. -
CI: the earliest run on this head (2026-09-29) predates
main's current tip, and the conflicts above confirm the green is stale.
Recommendation: request changes. Rebase, and route the bind address through a family-neutral field instead of the hl2 branch. The core mechanism is right, and it's exactly what the reporter's manual workaround proved.
— Jeremy KK7GWY + Claude
|
Maintainer ruling: pass the discovered interface address to the connect through a new family-neutral field on — Jeremy KK7GWY + Claude |
334b780 to
bec8b27
Compare
|
Rebased onto the latest upstream/main (3342761) and resolved the merge conflicts. The connection route is now carried through the family-neutral Validation:
|
|
Review follow-up completed.
There are no unresolved inline review threads remaining. GitHub reports the PR as mergeable. |
|
Thanks for sticking with this one, @dacacioa — the rebase onto the family-neutral What's red at
|
| Job | What it builds | Result |
|---|---|---|
| build (Linux) | cmake --build build — whole tree |
❌ failed at Build |
| check-macos | cmake --build build — whole tree |
❌ failed at Build |
| check-windows | --target AetherSDR + 4 named test exes |
✅ passed |
| Static checks | — | ✅ passed |
| Sanitizer option configures | — | ✅ passed |
- Linux: https://github.com/aethersdr/AetherSDR/actions/runs/37354821025/job/112076320721
- macOS: https://github.com/aethersdr/AetherSDR/actions/runs/37354821025/job/112076320475
I could not read the Build step's log text from here, so I won't quote you a compiler error I haven't seen — the step log on either job page has the file and line. But the shape of the failure narrows it a lot.
The useful part: it's almost certainly not in the app target
Look at what each job actually builds (.github/workflows/ci.yml):
build(Linux) andcheck-macosboth run plaincmake --build build— the whole tree, app plus every test target (a couple of hundred executables).check-windowsrunscmake --build build -j … --target AetherSDR aether-dv-waveform thumbdv_queue_test dstar_model_test digital_voice_waveform_process_test cross_needle_meter_test— deliberately narrow.
Both full-tree jobs fail; the only narrow-target job passes. Two different toolchains (GCC on the Linux CI image, Apple Clang on macOS) fail the same way, so this isn't a GCC quirk or a platform API difference — it's a translation unit that the AetherSDR target doesn't include and your local build therefore never compiled. Your validation was cmake --build build-5569 --target AetherSDR, which is exactly the blind spot.
To reproduce, drop --target:
cmake --build build # no --target: this is what both red jobs run
ctest --test-dir build --output-on-failure
(The two red jobs also pass -DENABLE_NVIDIA_AFX=ON -DAETHER_GPU_SPECTRUM=ON; the missing --target is the discriminator, so don't fight those flags on Windows if they don't configure for you.)
The test targets that compile your changed sources directly — only ever built by the full tree — are the first places to look:
connection_panel_size_test— compilessrc/gui/ConnectionPanel.cpp(tests/tests.cmake:6443)startup_autoconnect_lockout_test— also compilessrc/gui/ConnectionPanel.cpp(tests/tests.cmake:6479)app_settings_safety_test— compilessrc/core/backends/hl2/Hl2Discovery.cpp(tests/tests.cmake:2043)
Beyond those, anything that includes MetisClient.h, Hl2Discovery.h or IRadioBackend.h now sees your new struct members, so a test that builds one of those types positionally would break there.
Dead ends you can skip
Checked at your head commit so you don't spend time on them:
- Not a broken lane. Linux
buildis green on currentmain(035a8583). - Not base drift. Merge base is
3342761;mainhasn't touched any of your eight files since, so the merged tree compiles the same sources you're reading. - Not a missing include in the changed files. All five changed
.cppfiles syntax-check clean atbec8b27against Clang/Qt 6.11, as do the ConnectionPanel and backend-seam test TUs. (std::vectorwithout<vector>inConnectionPanel.cppis fine —qmetatype.h/qdebug.hpull it in.) #include <QHostAddress>insrc/core/backends/IRadioBackend.his safe.aethercorelinksQt6::NetworkPUBLIC (CMakeLists.txt:2386at your head), so every consumer of that header already has QtNetwork's include path.- The generated seam-probe table is still in sync —
python3 tools/gen_seam_probe_table.py --checkreports "up to date (51 signals)" on your branch, so adding the field toRadioConnectRequestdidn't drift it (consistent with Static checks passing).
Reviewer comments
The task surfaced no GitHub Copilot findings on this PR. @on8st's manual-IP gap looks closed by the per-interface probe in probeHermesLite2(), and the maintainer ruling (family-neutral field on RadioConnectRequest, no "hl2" family-string branch in RadioModel.cpp) is implemented as asked. CI is the only thing in the way.
One non-blocking observation
Hl2Discovery::refreshSockets() closes and deleteLater()s every socket at the top of each sweep, then rebinds. The rebind gets fresh ephemeral ports, so a reply that arrives after the next sweep has started comes back to a port nobody is listening on and is dropped. kMissedSweepsBeforeLost = 3 absorbs most of that, but a radio on a slow or loaded path could flap present/lost. Keeping a socket whose local address hasn't changed, and adding/removing only on real interface changes, would avoid it. Separate from the CI failure — just cheaper to fix now than after merge.
(For the record, the new &mp reference capture in Hl2Backend.cpp is fine: that invokeMethod is Qt::BlockingQueuedConnection, so the lambda completes before mp goes out of scope.)
Paste the compiler error here if it isn't obvious from the log and I'll dig in with you. Thanks again for the real-hardware validation — the multi-homed case only ever gets fixed by someone who has the setup to see it.
— AetherClaude
🤖 aethersdr-agent · cost: $10.3870 · model: claude-opus-5
|
CI follow-up on c3e3442: The previous Linux and macOS failures were caused by Local validation after the fix:
The new GitHub Actions runs are currently marked |
jensenpat
left a comment
There was a problem hiding this comment.
Reviewed head c3e344275d4c882f9eae8dd5b109ea55b05f91de.
Issue fit: Yes. #5569 is a multi-homed Windows host whose Hermes-Lite 2 sits on a link-local adapter the route table does not select. The reporter confirmed that choosing Ethernet 3 explicitly connects. This PR does that selection in Auto: one discovery socket per up IPv4 address, a manual probe from every enumerateIpv4Candidates() address (link-local included), and the winning local address carried on RadioConnectRequest::localBindAddress into the Metis stream bind and reconnect.
Maintainer ruling: The 2026-10-05 ruling asked for a family-neutral field on RadioConnectRequest and removal of the new hl2 family-string check. That is what landed. populateFamilyParams sets localBindAddress for every family; Hl2Backend::connectRadio is the only consumer. The pre-existing makeBackend, ANAN, and Icom family branches are unchanged.
Scope
| File/group | Change | Claimed? | Verdict |
|---|---|---|---|
Hl2Discovery |
Per-interface sockets, directed and global broadcast, rebuilt each sweep | Yes | In scope |
MetisClient, Hl2Backend |
Per-interface unicast discover; stream bind to localAddress |
Yes | In scope |
IRadioBackend.h, RadioModel |
Neutral localBindAddress; no new family branch |
Yes, maintainer-ruled | In scope |
ConnectionPanel probe |
Auto fan-out; keep the winning source | Yes | In scope |
ConnectionPanel edit/profile |
Preserve an explicit source while typing, all families | Disclosed in the updated body | In scope, shared UI |
tests/tests.cmake |
Qt6::Network on hl2_telemetry_source_test |
Yes, the CI compile fix | In scope |
No CHANGELOG entry. No new test. Flex, ANAN, Icom, RTL, and sim backend sources are untouched. ANAN's manual probe is still a wildcard bind; that is outside #5569.
Approve. No code blocker. Merge waits on required checks: the runs for this SHA are action_required and have not executed. The prior CHANGES_REQUESTED reviews are on 334b78036, before the rebase and the neutral field.
Nits (non-blocking, left in the body so they do not open threads)
- Broadcast
RadioInfostill leavessessionBindAddressempty. Production connect does not passboardMaxRx, soHl2Backendstill runs the per-interface unicastdiscover()and binds the stream to the socket that answered. A later caller that suppliesboardMaxRxwould skip that probe and fall back toAnyIPv4. MetisClient::discoverwaits the remaining timeout on the first idle socket before reading the others. The reply is still collected; connect latency can be the full 400 ms when the winning adapter is not first. The manual probe already polls at 10 ms.- The Auto "could not open a UDP socket" message no longer includes
errorString(). The Explicit path still does. MetisClient::startstill comments that the bind is a wildcard.
Verification
- Diff matches head
c3e34427against base33427611(9 files).git merge-treewith currentorigin/main(121340c0, 20 commits ahead) reports no conflicts; GitHub reports MERGEABLE. - The Linux and macOS failure at
bec8b27ishl2_telemetry_source_testnot findingQHostAddressafterIRadioBackend.hstarted including it, while that target linked only Qt Core. This head addsQt6::Network. An include-closure check of currentorigin/maintest targets found no other target that compiles the seam header withoutQt6::Network,aethercore, oraetherdesktop_support. - No socket-owning test was added or modified.
hl2_telemetry_source_teststays a pure function. I did not build or run it on this SHA; the previous full CI run is the evidence for the compile break, and this SHA has no completed checks. - I did not reproduce the multi-NIC Windows exchange. The author reports a real HL2 on the dedicated Ethernet path. No synthetic firmware peer.
Follow-up (not a merge prerequisite)
A socket-free test of the per-interface target list, in the style of #6146, would pin directed-broadcast selection without a fake HL2. The Windows link-local path still needs the reporter's host.
Superseded by c3e3442: rebased onto main and the bind address now rides the family-neutral RadioConnectRequest::localBindAddress, which is the maintainer ruling from 2026-10-05. The hl2 family-string branch is gone.
… and re-take the Stable snapshot. Principle VIII. (#6396) ## Summary This brings the user guide (docs.aethersdr.com) up to date with everything in v26.10.2 and re-takes the Stable snapshot so the default site shows it. Docs only, apart from the TCI page prose in `tools/docs/gen_reference.py`; no application source, test or CI changes. Every page was audited against the 106 PRs in the release. Every fact that was added or kept was checked against the source at `f753dbfc4`, the v26.10.2 release commit, not taken from the PR bodies alone. ## What changed - **Menu paths.** - AetherModem, Callsign Lookup, AetherMap and FreeDV Reporter are on **Tools**. They are created on View and then moved to Tools (`MainWindow_Menus.cpp:1519-1520`, `:1569-1575`); the style guide's example says **Tools → AetherMap...**. - **Show live CW contacts** is on **View**. - Waveforms is on **Tools**, not File. - The VUDU applet button was called TXDSP. - Labels now match the source's `...` vs `…`. - **Gaps from this cycle.** - **Windows and Network Diagnostics:** Windows Firewall rules and the Network Diagnostics Windows Firewall page (#6289); the `aether.network` and `aether.nvafx` log categories (#6289, #6308). - **Remote access:** Tailscale shim revocation semantics (#6325). - **Sleep, App Nap and power throttling (#6195):** per-OS notes. - **Hermes-Lite 2:** state restore, RX audio at unity, CW at TUNE level, multi-interface discovery and a discovery troubleshooting entry (#6259, #6254, #5975, #6009, #6293). - **ANAN-G2:** AGC-T range, a "What is remembered" section, and supply voltage and link status (#6282, #6129, #6137, #6211). - **NB2** on the HL2 and ANAN-G2 (#5966). - **DVK:** WAV import requirements, the slot menu without Delete, status text and licensing (#6247, #6262, #6251). - **Copy Assist** from unprocessed audio (#6216). - **Live CW contacts** replace the decoder's contact card (#6268). - **Status-bar indicators** work from the keyboard (#6267). - **Volume:** controllers, hotkeys and TCI `volume` follow the active output (#6129, #6260). - **Peripherals:** the KPA1500 (#5905), CTR2 link extensions and the AetherKnob (#6338), TMate 2 udev (#6250), MIDI RADE Modem and FDV modes (#6031, #6038). - **RTL-SDR:** live PPM (#6082). - **Icom:** COMP and the MK2 squelch scale (#6174, #6184). - **Known issues.** Entries whose GitHub issues are closed are removed (each checked with `gh issue view`). As decided, this includes three closed today without a demonstrated fix: #5107, #4824 and #5665. - **US spelling.** `docs-style-guide.md` asked for British spelling; it now asks for US, by the maintainer's decision, and every page is converted. The generated TCI pages are converted at their source, the prose strings in `tools/docs/gen_reference.py`. Code spans, links and URLs were left alone. UI labels were checked against the source strings, and none of them are British. Two in-page anchors and two Log Analyzer rule anchors (`#bnr-is-grayed-out-…`) follow the renamed headings. - **Stable snapshot.** `snapshot_stable.py 26.10.2` was re-run on the release tree with these corrections, before `main` was merged in, so Stable carries no post-release docs. `docs/` also has `main`'s post-release pages, in US spelling. Stable alone keeps the IC-9700 waterfall-rate entry (#5996): #6402 fixed it after the 26.10.2 tag, so 26.10.2 still has it. The PDF manual already attached to the v26.10.2 release keeps the tag's docs. ## Validation - `npm ci && npm run build` in `docs/user`: success. This is the broken-link and broken-anchor check. - `node tools/docs/test_log_rules.mjs`: 707 checks passed for 45 rules. - `python3 tools/docs/gen_reference.py --check`: up to date (8 pages). - `git diff --check`: clean. - Menu paths on a build of this branch (offscreen, isolated settings, no radio), `menu list`: `Tools → AetherModem...`, `Tools → Callsign Lookup...`, `Tools → AetherMap...`, `Tools → FreeDV Reporter...`, `Tools → Waveforms...`, `View → Show live CW contacts`. ## Screenshots now out of date (not recaptured) These need a recapture pass on the radio: - **Old panadapter title strip (#6386):** `panadapter`, `main-window`, `first-session-*`, `multi-slice-two-slices`, `split-pair`, `tnf-markers`, `offscreen-vfo-indicator`, `pan-3d-stacked-trace`, `default-light-theme`. - **Pre-canon windows (#6334, #6339, #6341, #6372):** `aetherrx*`, `aethertx*`, `aethermodem-aprs`, `bnr-panel`, `connect-dialog`, `connect-by-ip-card`, `connect-icom-rows`, `connect-smartlink-card`, `rx-agc-calibration`. - **Other:** - `title-bar` (lowercase tab states, #6281); - `menu-help` (#6294); - `display-panel` (#6371); - `support-diagnostics-dialog`; - `waveforms-dialog` and `tailnet-remote-access` (show shim 0.4.0); - `about-window` (v26.10.1); - `radio-setup-qrz`. ## Found during the audit, not fixed here - **App string bug:** `TailnetShimDialog.cpp:628` tells the user to "Connect remotely with Connect → Manual"; the real path is **File → Connect to Radio... → Connect by IP**. - **ANAN AGC-T tooltips:** the RX applet and VFO tooltips show the slider position as dB, which is wrong on ANAN. #6282 lists this as a follow-up. - **MIDI Master Volume on non-Flex radios:** it is refused while PC Audio is off, whereas hotkeys, FlexControl and HID drive the line out. The docs describe the code as it is. - **RTL mode names:** `rtl-sdr.md` writes "FM-N" and "CW-R", but the source strings are `FMN` and `CWR`. Left as is until someone confirms what the mode menu displays. - **IC-705 COMP readback:** inferred from the MK2 (#6174) and not verified on an IC-705. - **Not yet documented:** the #6230 Radio Health row "Forward responses skipped", and the Store/MSIX package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
Fixes #5569.
Hermes-Lite 2 discovery and manual-IP connection now walk the active local IPv4 interfaces instead of relying on a wildcard UDP bind. Each interface gets its own probe socket; the interface that receives the valid HL2 response is retained for the Metis stream and automatic reconnects. Directed and global broadcast behavior for normal discovery is preserved.
This addresses the reported case where the HL2 is connected through one Ethernet interface on a multi-homed Windows host, while the route table selects another adapter for AetherSDR.
Constitution principle honored
Principle XI — Fixes Are Demonstrated. The change was built locally, exercised against the Hermes-Lite 2 discovery path, and validated with the relevant HL2 tests before submission.
Scope
The Flex and ANAN protocol/backend implementations are unchanged. The shared ConnectionPanel source-selection and manual-IP handling is intentionally used by all radio families; this preserves explicit source selection consistently and is not an HL2-specific protocol change.
Test plan
cmake --build build-5569 --target AetherSDR)hl2_metis_protocol_test,hl2_hardware_options_test, andhl2_receivers_testChecklist
Good "git" signature)AppSettingscalls — use nested-JSON-under-one-key (Principle V)MeterSmoother(no meter UI changed)