Skip to content

Commit bd767af

Browse files
committed
A better way to ensure BLE disconnects:
It turns out that Bleak is kinda racey. If we call disconnect() and then immediately close() the disconnect may or may not actually happen (probably because it was merely queued for dbus). So instead: When we want to close the BLEInterface we call disconnect() and then in a preregistered 'on disconnect' handler we actually close down our interface/datastructures.
1 parent 6194e41 commit bd767af

1 file changed

Lines changed: 21 additions & 8 deletions

File tree

meshtastic/ble_interface.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from threading import Thread
99
from typing import Optional
1010

11-
from bleak import BleakClient, BleakScanner, BLEDevice
1211
import print_color
12+
from bleak import BleakClient, BleakScanner, BLEDevice
13+
from bleak.exc import BleakDBusError, BleakError
1314

1415
from meshtastic.mesh_interface import MeshInterface
1516

@@ -62,14 +63,14 @@ def __init__(
6263
if not self.noProto:
6364
self._waitConnected(timeout=60.0)
6465
self.waitForConfig()
65-
logging.debug("Mesh init finished")
6666

6767
logging.debug("Register FROMNUM notify callback")
6868
self.client.start_notify(FROMNUM_UUID, self.from_num_handler)
6969

7070
# We MUST run atexit (if we can) because otherwise (at least on linux) the BLE device is not disconnected
7171
# and future connection attempts will fail. (BlueZ kinda sucks)
72-
self._exit_handler = atexit.register(self.close)
72+
# Note: the on disconnected callback will call our self.close which will make us nicely wait for threads to exit
73+
self._exit_handler = atexit.register(self.client.disconnect)
7374

7475
def from_num_handler(self, _, b): # pylint: disable=C0116
7576
"""Handle callbacks for fromnum notify.
@@ -143,7 +144,7 @@ def connect(self, address: Optional[str] = None):
143144

144145
# Bleak docs recommend always doing a scan before connecting (even if we know addr)
145146
device = self.find_device(address)
146-
client = BLEClient(device.address)
147+
client = BLEClient(device.address, disconnected_callback=lambda _: self.close)
147148
client.connect()
148149
client.discover()
149150
return client
@@ -153,11 +154,20 @@ def _receiveFromRadioImpl(self):
153154
if self.should_read:
154155
self.should_read = False
155156
retries = 0
156-
while True:
157+
while self._want_receive:
157158
try:
158159
b = bytes(self.client.read_gatt_char(FROMRADIO_UUID))
159-
except Exception as e:
160-
raise BLEInterface.BLEError("Error reading BLE") from e
160+
except BleakDBusError as e:
161+
# Device disconnected probably, so end our read loop immediately
162+
logging.debug(f"Device disconnected, shutting down {e}")
163+
self._want_receive = False
164+
except BleakError as e:
165+
# We were definitely disconnected
166+
if "Not connected" in str(e):
167+
logging.debug(f"Device disconnected, shutting down {e}")
168+
self._want_receive = False
169+
else:
170+
raise BLEInterface.BLEError("Error reading BLE") from e
161171
if not b:
162172
if retries < 5:
163173
time.sleep(0.1)
@@ -188,7 +198,10 @@ def _sendToRadioImpl(self, toRadio):
188198

189199
def close(self):
190200
atexit.unregister(self._exit_handler)
191-
MeshInterface.close(self)
201+
try:
202+
MeshInterface.close(self)
203+
except Exception as e:
204+
logging.error(f"Error closing mesh interface: {e}")
192205

193206
if self._want_receive:
194207
self.want_receive = False # Tell the thread we want it to stop

0 commit comments

Comments
 (0)