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.
requestConfig was assuming that the order of fields in meshtastic.LocalConfig
matches the order of enum values in AdminMessage.ConfigType. This is true for
'device', 'position', etc. but is NOT true for 'security' due to the intervening
'version' field.
Look up LocalConfig fields by name, not index, to prevent this error in the future.
LocalConfig.security was introduced in https://github.com/meshtastic/protobufs/pull/553
This fixes two issues that caused BLE connections to hang on macOS
when not using the --debug flag:
1. Race condition in BLEClient event loop initialization
- The event loop thread was started but asyncio operations were
submitted before the loop was actually running
- Added threading.Event synchronization to ensure the event loop
is running before any operations are submitted
- The ready signal is sent from within the loop via call_soon()
to guarantee the loop is truly active
2. CoreBluetooth callback delivery on macOS
- On macOS, CoreBluetooth requires occasional I/O operations for
callbacks to be properly delivered to the main thread
- Without --debug, no I/O was happening, causing callbacks to
never be processed and operations to hang indefinitely
- Added sys.stdout.flush() call before waiting for async results
to trigger the necessary I/O
The --debug flag masked these issues because:
- Debug logging introduces timing delays that let the event loop start
- Logger I/O triggers the necessary callback delivery mechanism
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>