From bd767af4857022caab2f7ea51fe076ef0c384b45 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 29 Jun 2024 15:08:50 -0700 Subject: [PATCH] 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. --- meshtastic/ble_interface.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 4c24528..4357a4b 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -8,8 +8,9 @@ import time from threading import Thread from typing import Optional -from bleak import BleakClient, BleakScanner, BLEDevice import print_color +from bleak import BleakClient, BleakScanner, BLEDevice +from bleak.exc import BleakDBusError, BleakError from meshtastic.mesh_interface import MeshInterface @@ -62,14 +63,14 @@ class BLEInterface(MeshInterface): if not self.noProto: self._waitConnected(timeout=60.0) self.waitForConfig() - logging.debug("Mesh init finished") logging.debug("Register FROMNUM notify callback") self.client.start_notify(FROMNUM_UUID, self.from_num_handler) # We MUST run atexit (if we can) because otherwise (at least on linux) the BLE device is not disconnected # and future connection attempts will fail. (BlueZ kinda sucks) - self._exit_handler = atexit.register(self.close) + # Note: the on disconnected callback will call our self.close which will make us nicely wait for threads to exit + self._exit_handler = atexit.register(self.client.disconnect) def from_num_handler(self, _, b): # pylint: disable=C0116 """Handle callbacks for fromnum notify. @@ -143,7 +144,7 @@ class BLEInterface(MeshInterface): # Bleak docs recommend always doing a scan before connecting (even if we know addr) device = self.find_device(address) - client = BLEClient(device.address) + client = BLEClient(device.address, disconnected_callback=lambda _: self.close) client.connect() client.discover() return client @@ -153,11 +154,20 @@ class BLEInterface(MeshInterface): if self.should_read: self.should_read = False retries = 0 - while True: + while self._want_receive: try: b = bytes(self.client.read_gatt_char(FROMRADIO_UUID)) - except Exception as e: - raise BLEInterface.BLEError("Error reading BLE") from e + except BleakDBusError as e: + # Device disconnected probably, so end our read loop immediately + logging.debug(f"Device disconnected, shutting down {e}") + self._want_receive = False + except BleakError as e: + # We were definitely disconnected + if "Not connected" in str(e): + logging.debug(f"Device disconnected, shutting down {e}") + self._want_receive = False + else: + raise BLEInterface.BLEError("Error reading BLE") from e if not b: if retries < 5: time.sleep(0.1) @@ -188,7 +198,10 @@ class BLEInterface(MeshInterface): def close(self): atexit.unregister(self._exit_handler) - MeshInterface.close(self) + try: + MeshInterface.close(self) + except Exception as e: + logging.error(f"Error closing mesh interface: {e}") if self._want_receive: self.want_receive = False # Tell the thread we want it to stop