From 6df89f54a79f8a012b8d1040e6339147c9ef7caf Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 29 Jun 2024 09:10:20 -0700 Subject: [PATCH] fix BLE scan with latest Bleak --- .vscode/launch.json | 8 +++ meshtastic/__main__.py | 8 +-- meshtastic/ble_interface.py | 117 +++++++++++++++++++----------------- 3 files changed, 73 insertions(+), 60 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 911eb4d..c1bb9ef 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,14 @@ "module": "meshtastic", "justMyCode": false, "args": ["--debug", "--ble", "24:62:AB:DD:DF:3A"] + }, + { + "name": "meshtastic BLE scan", + "type": "python", + "request": "launch", + "module": "meshtastic", + "justMyCode": false, + "args": ["--debug", "--ble-scan"] }, { "name": "meshtastic admin", diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index e6c2362..8bebf41 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1041,12 +1041,8 @@ def common(): subscribe() if args.ble_scan: logging.debug("BLE scan starting") - client = BLEInterface(None, debugOut=logfile, noProto=args.noproto) - try: - for x in client.scan(): - print(f"Found: name='{x[1].local_name}' address='{x[0].address}'") - finally: - client.close() + for x in BLEInterface.scan(): + print(f"Found: name='{x.name}' address='{x.address}'") meshtastic.util.our_exit("BLE scan finished", 0) return elif args.ble: diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 0624db6..9067878 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -1,13 +1,13 @@ """Bluetooth interface """ -import logging -import time -import struct import asyncio -from threading import Thread, Event +import logging +import struct +import time +from threading import Event, Thread from typing import Optional -from bleak import BleakScanner, BleakClient, BLEDevice +from bleak import BleakClient, BleakScanner, BLEDevice from meshtastic.mesh_interface import MeshInterface from meshtastic.util import our_exit @@ -20,25 +20,32 @@ FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453" class BLEInterface(MeshInterface): """MeshInterface using BLE to connect to devices""" + class BLEError(Exception): """An exception class for BLE errors""" + def __init__(self, message): self.message = message super().__init__(self.message) - class BLEState(): # pylint: disable=C0115 + class BLEState: # pylint: disable=C0115 THREADS = False BLE = False MESH = False - - def __init__(self, address: Optional[str], noProto: bool = False, debugOut = None, noNodes: bool = False): + def __init__( + self, + address: Optional[str], + noProto: bool = False, + debugOut=None, + noNodes: bool = False, + ): self.state = BLEInterface.BLEState() self.should_read = False logging.debug("Threads starting") - self._receiveThread = Thread(target = self._receiveFromRadioImpl) + self._receiveThread = Thread(target=self._receiveFromRadioImpl) self._receiveThread_started = Event() self._receiveThread_stopped = Event() self._receiveThread.start() @@ -56,10 +63,12 @@ class BLEInterface(MeshInterface): raise e logging.debug("Mesh init starting") - MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto, noNodes = noNodes) + MeshInterface.__init__( + self, debugOut=debugOut, noProto=noProto, noNodes=noNodes + ) self._startConfig() if not self.noProto: - self._waitConnected(timeout = 60.0) + self._waitConnected(timeout=60.0) self.waitForConfig() self.state.MESH = True logging.debug("Mesh init finished") @@ -67,53 +76,55 @@ class BLEInterface(MeshInterface): logging.debug("Register FROMNUM notify callback") self.client.start_notify(FROMNUM_UUID, self.from_num_handler) - - async def from_num_handler(self, _, b): # pylint: disable=C0116 - from_num = struct.unpack(' list[BLEDevice]: + @staticmethod + def scan() -> list[BLEDevice]: """Scan for available BLE devices.""" with BLEClient() as client: - response = client.discover( - return_adv = True, - service_uuids=[SERVICE_UUID] - ) + response = client.discover(return_adv=True, service_uuids=[SERVICE_UUID]) devices = response.values() # bleak sometimes returns devices we didn't ask for, so filter the response # to only return true meshtastic devices # d[0] is the device. d[1] is the advertisement data - devices = list(filter(lambda d: SERVICE_UUID in d[1].service_uuids, devices)) + devices = list( + filter(lambda d: SERVICE_UUID in d[1].service_uuids, devices) + ) return list(map(lambda d: d[0], devices)) - def find_device(self, address: Optional[str]) -> BLEDevice: - """Find a device by address""" - addressed_devices = self.scan() + """Find a device by address.""" + addressed_devices = BLEInterface.scan() if address: - addressed_devices = list(filter(lambda x: address == x.name or address == x.address, addressed_devices)) + addressed_devices = list( + filter( + lambda x: address == x.name or address == x.address, + addressed_devices, + ) + ) if len(addressed_devices) == 0: - raise BLEInterface.BLEError(f"No Meshtastic BLE peripheral with identifier or address '{address}' found. Try --ble-scan to find it.") + raise BLEInterface.BLEError( + f"No Meshtastic BLE peripheral with identifier or address '{address}' found. Try --ble-scan to find it." + ) if len(addressed_devices) > 1: - raise BLEInterface.BLEError(f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found.") + raise BLEInterface.BLEError( + f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found." + ) return addressed_devices[0] - def _sanitize_address(address): # pylint: disable=E0213 - "Standardize BLE address by removing extraneous characters and lowercasing" - return address \ - .replace("-", "") \ - .replace("_", "") \ - .replace(":", "") \ - .lower() + def _sanitize_address(address): # pylint: disable=E0213 + "Standardize BLE address by removing extraneous characters and lowercasing." + return address.replace("-", "").replace("_", "").replace(":", "").lower() def connect(self, address: Optional[str] = None): - "Connect to a device by address" + "Connect to a device by address." # Bleak docs recommend always doing a scan before connecting (even if we know addr) device = self.find_device(address) @@ -121,7 +132,6 @@ class BLEInterface(MeshInterface): client.connect() return client - def _receiveFromRadioImpl(self): self._receiveThread_started.set() while self._receiveThread_started.is_set(): @@ -146,12 +156,11 @@ class BLEInterface(MeshInterface): b = toRadio.SerializeToString() if b: logging.debug(f"TORADIO write: {b.hex()}") - self.client.write_gatt_char(TORADIO_UUID, b, response = True) + self.client.write_gatt_char(TORADIO_UUID, b, response=True) # Allow to propagate and then make sure we read time.sleep(0.1) self.should_read = True - def close(self): if self.state.MESH: MeshInterface.close(self) @@ -165,10 +174,11 @@ class BLEInterface(MeshInterface): self.client.close() -class BLEClient(): +class BLEClient: """Client for managing connection to a BLE device""" - def __init__(self, address = None, **kwargs): - self._eventThread = Thread(target = self._run_event_loop) + + def __init__(self, address=None, **kwargs): + self._eventThread = Thread(target=self._run_event_loop) self._eventThread_started = Event() self._eventThread_stopped = Event() self._eventThread.start() @@ -180,47 +190,46 @@ class BLEClient(): self.bleak_client = BleakClient(address, **kwargs) - - def discover(self, **kwargs): # pylint: disable=C0116 + def discover(self, **kwargs): # pylint: disable=C0116 return self.async_await(BleakScanner.discover(**kwargs)) - def pair(self, **kwargs): # pylint: disable=C0116 + def pair(self, **kwargs): # pylint: disable=C0116 return self.async_await(self.bleak_client.pair(**kwargs)) - def connect(self, **kwargs): # pylint: disable=C0116 + def connect(self, **kwargs): # pylint: disable=C0116 return self.async_await(self.bleak_client.connect(**kwargs)) - def disconnect(self, **kwargs): # pylint: disable=C0116 + def disconnect(self, **kwargs): # pylint: disable=C0116 self.async_await(self.bleak_client.disconnect(**kwargs)) - def read_gatt_char(self, *args, **kwargs): # pylint: disable=C0116 + def read_gatt_char(self, *args, **kwargs): # pylint: disable=C0116 return self.async_await(self.bleak_client.read_gatt_char(*args, **kwargs)) - def write_gatt_char(self, *args, **kwargs): # pylint: disable=C0116 + def write_gatt_char(self, *args, **kwargs): # pylint: disable=C0116 self.async_await(self.bleak_client.write_gatt_char(*args, **kwargs)) - def start_notify(self, *args, **kwargs): # pylint: disable=C0116 + def start_notify(self, *args, **kwargs): # pylint: disable=C0116 self.async_await(self.bleak_client.start_notify(*args, **kwargs)) - def close(self): # pylint: disable=C0116 + def close(self): # pylint: disable=C0116 self.async_run(self._stop_event_loop()) self._eventThread_stopped.wait(5) def __enter__(self): return self - + def __exit__(self, _type, _value, _traceback): self.close() - def async_await(self, coro, timeout = None): # pylint: disable=C0116 + def async_await(self, coro, timeout=None): # pylint: disable=C0116 return self.async_run(coro).result(timeout) - def async_run(self, coro): # pylint: disable=C0116 + def async_run(self, coro): # pylint: disable=C0116 return asyncio.run_coroutine_threadsafe(coro, self._eventLoop) def _run_event_loop(self): # I don't know if the event loop can be initialized in __init__ so silencing pylint - self._eventLoop = asyncio.new_event_loop() # pylint: disable=W0201 + self._eventLoop = asyncio.new_event_loop() # pylint: disable=W0201 self._eventThread_started.set() try: self._eventLoop.run_forever()