ROUTING_APP with errorReason NONE is an ACK (success), not an error.
Only print failure message for non-NONE error reasons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, receiving a ROUTING_APP packet in response to a traceroute
would cause the function to attempt parsing it as a RouteDiscovery
payload, resulting in a crash or silent failure followed by a timeout.
This mirrors the error handling already present in onResponseTelemetry
and onResponseWaypoint, but displays the specific errorReason instead
of a hardcoded firmware version message, since traceroute is a
diagnostic tool where the exact failure reason is more valuable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If connect() or waitForConfig() raises during __init__ (handshake timeout,
bad stream, config error), the reader thread started by connect() keeps
running and the underlying stream/socket stays open — but the caller never
receives a reference to the half-initialized instance, so they cannot call
close() themselves. The leak compounds on every retry from a caller's
reconnect loop.
Fix: wrap connect() + waitForConfig() in try/except; call self.close() on
any exception before re-raising. Also guard close() against RuntimeError
from joining an unstarted reader thread (happens when close() runs from
a failed __init__ before connect() could spawn it).
Discovered while debugging a real-world Meshtastic firmware crash where
a passive logger's retrying TCPInterface() calls against a node with
250-entry NodeDB produced a reconnect storm — every retry triggered a
full config+NodeDB dump on the node, compounding heap pressure, which
then exposed null-deref bugs in Router::perhapsDecode / MeshService
(firmware side fixed in meshtastic/firmware#10226 and #10229). The
client-side leak is independent of those firmware bugs and worth fixing
on its own.