You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
While a radio is actively connected via IP (the routed / VPN / "Connect by IP" path), RadioDiscovery keeps a 5-second background re-bind loop running forever. The log shows a continuous cycle of:
RadioDiscovery: no packets yet, re-binding socket
RadioDiscovery: listening on UDP 4992
every 5 seconds, even though we already have a healthy TCP session to the radio. Discovery is never stopped or quiesced on connect, and the re-bind loop has no termination condition other than "first UDP broadcast packet received" — which by design never happens on a routed/VPN connection.
Reproduced on a FLEX-8400M connected over IP. The same condition occurs on macOS whenever Local Network privacy consent silently drops inbound broadcasts (a case the code already comments on).
Root cause
Discovery (src/core/RadioDiscovery.{h,cpp}) is a UDP broadcast listener on port 4992. Its lifecycle:
onConnectionStateChanged(true) (MainWindow.cpp:8355) — the successful-connect handler — never touches discovery. There is no m_discovery.stopListening() (or pause) anywhere on the connect path. Confirmed: the only startListening/stopListening call sites in the whole app are launch, shutdown, and the retry button.
It is armed in startListening() on every bind whenever no packet has ever arrived:
if (!m_receivedAny) {
m_rebindTimer.start();
}
…and the only thing that stops it is the first inbound broadcast, in onReadyRead():
if (!m_receivedAny) {
m_receivedAny = true;
m_rebindTimer.stop(); // <- the sole exit from the loop
}
Note there is no retry cap on the re-bind loop (unlike the bind-failure path, which gives up after MAX_BIND_RETRIES = 15). Re-bind is unbounded — see the constants at RadioDiscovery.h:100-104.
Why it loops forever specifically when connected via IP
"…populate mf_enable and connected client info even when there's no discovery broadcast (direct IP / VPN / routed path)."
So on a routed/VPN connection, UDP broadcasts never reach the client by definition → m_receivedAny stays false → the 5-second re-bind loop runs indefinitely in the background, closing and re-binding the UDP socket forever, while we hold a perfectly healthy TCP connection to the radio. That is the loop in the log.
The discovery subsystem and the active connection are completely independent and do not coordinate — discovery has no idea we've connected (by IP or otherwise), so it keeps "searching."
Answers to the design questions this raised
When do we use discovery, and over what transport? Discovery = UDP broadcast listener on port 4992 (RadioDiscovery). It exists to (a) populate the connection panel's local-radio list, (b) auto-connect to the last radio by serial (MainWindow.cpp:1537), and (c) refresh Multi-Flex / other-GUI-client info. The three connection transports are otherwise separate: local (discovery-assisted, TCP), routed / Connect-by-IP (direct TCP probe, no discovery), and WAN/SmartLink (TLS + cloud lookup). Discovery is broadcast-only and is not the connection mechanism for any of them — it just feeds the picker.
Why am I "using discovery" while connected by IP? You aren't, for the session — it's just never told to stop. It runs in parallel and, on a routed link, spins the re-bind loop forever because broadcasts can't arrive.
When do we shut discovery down? Only at app exit and momentarily on manual retry. Never on connect, and never when connecting by IP. The re-bind sub-loop never self-terminates unless a broadcast is received.
Impact
Continuous log spam every 5 s for the entire session.
Continuous, pointless close() + re-bind() of a UDP socket every 5 s (wasted syscalls / churn) for the whole time you're connected by IP.
Misleading: looks like the app "can't find the radio" while it's fully connected and working.
The amber "Searching for radio…" discovery indicator state (TitleBar::setDiscovering) is driven separately and cleared by heartbeats, so the UI doesn't show it — but the underlying subsystem is still thrashing.
Proposed fix (options)
The safe, clearly-correct part is bounding/stopping the re-bind loop so it cannot run forever:
Stop the re-bind churn on connect. In onConnectionStateChanged(true), tell RadioDiscovery we're connected so it stops the re-bind timer (and optionally goes passive). Resume normal discovery on disconnect. This directly fixes the reported loop.
Cap the re-bind attempts the same way the bind-failure path is capped (MAX_BIND_RETRIES), so even when we never connect, discovery stops thrashing after N tries instead of forever.
Suppress re-bind for routed/isRouted connections — when the active radio was reached by direct IP/VPN, broadcasts cannot arrive by design, so the "keep re-binding until first packet" heuristic should not apply at all.
The smaller open design question (for maintainer direction): while connected, should discovery (a) fully stop, or (b) keep passively listening (no re-bind churn) so the connection panel can still refresh Multi-Flex / other-client info and support auto-reconnect? Option (b) preserves the most behavior; option (a) is simplest. Either way, the unbounded 5-second re-bind loop should not run while connected.
Notes
Purely client-side networking — no radio-authoritative state involved.
The core fix (stop/cap the re-bind loop; it's a missing lifecycle guard with a clear root cause) falls within the autonomous-fix boundaries in CLAUDE.md. The "fully stop vs. passively listen while connected" choice is the only part that wants maintainer confirmation.
Summary
While a radio is actively connected via IP (the routed / VPN / "Connect by IP" path),
RadioDiscoverykeeps a 5-second background re-bind loop running forever. The log shows a continuous cycle of:every 5 seconds, even though we already have a healthy TCP session to the radio. Discovery is never stopped or quiesced on connect, and the re-bind loop has no termination condition other than "first UDP broadcast packet received" — which by design never happens on a routed/VPN connection.
Reproduced on a FLEX-8400M connected over IP. The same condition occurs on macOS whenever Local Network privacy consent silently drops inbound broadcasts (a case the code already comments on).
Root cause
Discovery (
src/core/RadioDiscovery.{h,cpp}) is a UDP broadcast listener on port 4992. Its lifecycle:MainWindow.cpp:4530-4531:MainWindow.cpp:5310) or during a manual "Retry Discovery" (MainWindow.cpp:1491-1493).onConnectionStateChanged(true)(MainWindow.cpp:8355) — the successful-connect handler — never touches discovery. There is nom_discovery.stopListening()(or pause) anywhere on the connect path. Confirmed: the onlystartListening/stopListeningcall sites in the whole app are launch, shutdown, and the retry button.The re-bind loop itself, in
RadioDiscovery.cpp:24-28:It is armed in
startListening()on every bind whenever no packet has ever arrived:…and the only thing that stops it is the first inbound broadcast, in
onReadyRead():Note there is no retry cap on the re-bind loop (unlike the bind-failure path, which gives up after
MAX_BIND_RETRIES = 15). Re-bind is unbounded — see the constants atRadioDiscovery.h:100-104.Why it loops forever specifically when connected via IP
"Connect by IP" is the routed/VPN/manual path:
ConnectionPanel::probeRadio()opens a direct TCP socket toip:4992(ConnectionPanel.cpp:1323) and tags the resultinfo.isRouted = true(ConnectionPanel.cpp:1367). The panel's own UI text spells out the precondition (ConnectionPanel.cpp:458-461):and the probe code repeats it (
ConnectionPanel.cpp:1463-1466):So on a routed/VPN connection, UDP broadcasts never reach the client by definition →
m_receivedAnystaysfalse→ the 5-second re-bind loop runs indefinitely in the background, closing and re-binding the UDP socket forever, while we hold a perfectly healthy TCP connection to the radio. That is the loop in the log.The discovery subsystem and the active connection are completely independent and do not coordinate — discovery has no idea we've connected (by IP or otherwise), so it keeps "searching."
Answers to the design questions this raised
RadioDiscovery). It exists to (a) populate the connection panel's local-radio list, (b) auto-connect to the last radio by serial (MainWindow.cpp:1537), and (c) refresh Multi-Flex / other-GUI-client info. The three connection transports are otherwise separate: local (discovery-assisted, TCP), routed / Connect-by-IP (direct TCP probe, no discovery), and WAN/SmartLink (TLS + cloud lookup). Discovery is broadcast-only and is not the connection mechanism for any of them — it just feeds the picker.Impact
close()+ re-bind()of a UDP socket every 5 s (wasted syscalls / churn) for the whole time you're connected by IP.TitleBar::setDiscovering) is driven separately and cleared by heartbeats, so the UI doesn't show it — but the underlying subsystem is still thrashing.Proposed fix (options)
The safe, clearly-correct part is bounding/stopping the re-bind loop so it cannot run forever:
onConnectionStateChanged(true), tellRadioDiscoverywe're connected so it stops the re-bind timer (and optionally goes passive). Resume normal discovery on disconnect. This directly fixes the reported loop.MAX_BIND_RETRIES), so even when we never connect, discovery stops thrashing after N tries instead of forever.isRoutedconnections — when the active radio was reached by direct IP/VPN, broadcasts cannot arrive by design, so the "keep re-binding until first packet" heuristic should not apply at all.The smaller open design question (for maintainer direction): while connected, should discovery (a) fully stop, or (b) keep passively listening (no re-bind churn) so the connection panel can still refresh Multi-Flex / other-client info and support auto-reconnect? Option (b) preserves the most behavior; option (a) is simplest. Either way, the unbounded 5-second re-bind loop should not run while connected.
Notes
CLAUDE.md. The "fully stop vs. passively listen while connected" choice is the only part that wants maintainer confirmation.