From 62f16d34d4915d8e79b300cc9d3acea984f9c6bd Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sun, 23 Jun 2024 11:56:44 -0700 Subject: [PATCH 01/13] fix BLE scan and connect to work with latest bleak --- .vscode/launch.json | 8 +++++++ meshtastic/__main__.py | 8 ++++++- meshtastic/ble_interface.py | 46 ++++++++++++++++++------------------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index aca86dd..911eb4d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -76,6 +76,14 @@ "justMyCode": true, "args": ["--debug", "--info"] }, + { + "name": "meshtastic debug BLE", + "type": "python", + "request": "launch", + "module": "meshtastic", + "justMyCode": true, + "args": ["--debug", "--ble", "--info"] + }, { "name": "meshtastic debug set region", "type": "python", diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index fa71adc..fb12a0c 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1050,7 +1050,7 @@ def common(): meshtastic.util.our_exit("BLE scan finished", 0) return elif args.ble: - client = BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes) + client = BLEInterface(args.ble_dest, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes) elif args.host: try: client = meshtastic.tcp_interface.TCPInterface( @@ -1119,6 +1119,12 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse group.add_argument( "--ble", help="The BLE device address or name to connect to", + action="store_true", + ) + + group.add_argument( + "--ble-dest", + help="The BLE device address or name to connect to", default=None, ) diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 1c12758..4b8ddeb 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -7,7 +7,7 @@ import asyncio from threading import Thread, Event from typing import Optional -from bleak import BleakScanner, BleakClient +from bleak import BleakScanner, BleakClient, BLEDevice from meshtastic.mesh_interface import MeshInterface from meshtastic.util import our_exit @@ -35,9 +35,6 @@ class BLEInterface(MeshInterface): def __init__(self, address: Optional[str], noProto: bool = False, debugOut = None, noNodes: bool = False): self.state = BLEInterface.BLEState() - if not address: - return - self.should_read = False logging.debug("Threads starting") @@ -54,10 +51,9 @@ class BLEInterface(MeshInterface): self.client = self.connect(address) self.state.BLE = True logging.debug("BLE connected") - except BLEInterface.BLEError as e: + # except BLEInterface.BLEError as e: + finally: self.close() - our_exit(e.message, 1) - return logging.debug("Mesh init starting") MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto, noNodes = noNodes) @@ -78,33 +74,35 @@ class BLEInterface(MeshInterface): self.should_read = True - def scan(self): - "Scan for available BLE devices" + def scan(self) -> list[BLEDevice]: + """Scan for available BLE devices.""" with BLEClient() as client: - return [ - (x[0], x[1]) for x in (client.discover( + response = client.discover( return_adv = True, - service_uuids = [ SERVICE_UUID ] - )).values() - ] + 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)) + return list(map(lambda d: d[0], devices)) - def find_device(self, address): - "Find a device by address" - meshtastic_devices = self.scan() + def find_device(self, address: Optional[str]) -> BLEDevice: + """Find a device by address""" + addressed_devices = self.scan() - addressed_devices = list(filter(lambda x: address in (x[1].local_name, x[0].name), meshtastic_devices)) - # If nothing is found try on the address - if len(addressed_devices) == 0: - addressed_devices = list(filter( - lambda x: BLEInterface._sanitize_address(address) == BLEInterface._sanitize_address(x[0].address), - meshtastic_devices)) + if address: + 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.") if len(addressed_devices) > 1: raise BLEInterface.BLEError(f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found.") - return addressed_devices[0][0] + return addressed_devices[0] def _sanitize_address(address): # pylint: disable=E0213 "Standardize BLE address by removing extraneous characters and lowercasing" From 81266e756b2920d2f2f428fdc3d6ed03049de43c Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sun, 23 Jun 2024 13:52:32 -0700 Subject: [PATCH 02/13] fixes to make Bleak (BLE) work better --- meshtastic/__main__.py | 4 ++-- meshtastic/ble_interface.py | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index fb12a0c..e6c2362 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1122,11 +1122,11 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse action="store_true", ) - group.add_argument( + outer.add_argument( "--ble-dest", help="The BLE device address or name to connect to", default=None, - ) + ) return parser diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 4b8ddeb..0624db6 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -51,9 +51,9 @@ class BLEInterface(MeshInterface): self.client = self.connect(address) self.state.BLE = True logging.debug("BLE connected") - # except BLEInterface.BLEError as e: - finally: + except BLEInterface.BLEError as e: self.close() + raise e logging.debug("Mesh init starting") MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto, noNodes = noNodes) @@ -112,17 +112,13 @@ class BLEInterface(MeshInterface): .replace(":", "") \ .lower() - def connect(self, address): + def connect(self, address: Optional[str] = None): "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) client = BLEClient(device.address) client.connect() - try: - client.pair() - except NotImplementedError: - # Some bluetooth backends do not require explicit pairing. - # See Bleak docs for details on this. - pass return client @@ -212,7 +208,7 @@ class BLEClient(): def __enter__(self): return self - + def __exit__(self, _type, _value, _traceback): self.close() From 897adfb8c2786bfc846762282b78547a99dd6b79 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 29 Jun 2024 09:41:06 -0500 Subject: [PATCH 03/13] Adds support for ble logging characteristic --- meshtastic/ble_interface.py | 18 ++++++++++++++++++ poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 1c12758..d1c9fd5 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -6,6 +6,7 @@ import struct import asyncio from threading import Thread, Event from typing import Optional +from print_color import print from bleak import BleakScanner, BleakClient @@ -16,6 +17,8 @@ SERVICE_UUID = "6ba1b218-15a8-461f-9fa8-5dcae273eafd" TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7" FROMRADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002" FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453" +LOGRADIO_UUID = "6c6fd238-78fa-436b-aacf-15c5be1ef2e2" + class BLEInterface(MeshInterface): @@ -70,6 +73,7 @@ class BLEInterface(MeshInterface): logging.debug("Register FROMNUM notify callback") self.client.start_notify(FROMNUM_UUID, self.from_num_handler) + self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler) async def from_num_handler(self, _, b): # pylint: disable=C0116 @@ -77,6 +81,20 @@ class BLEInterface(MeshInterface): logging.debug(f"FROMNUM notify: {from_num}") self.should_read = True + async def log_radio_handler(self, _, b): # pylint: disable=C0116 + log_radio = b.decode('utf-8').replace('\n', '') + if log_radio.startswith("DEBUG"): + print(log_radio, color="cyan", end=None) + elif log_radio.startswith("INFO"): + print(log_radio, color="white", end=None) + elif log_radio.startswith("WARN"): + print(log_radio, color="yellow", end=None) + elif log_radio.startswith("ERROR"): + print(log_radio, color="red", end=None) + else: + print(log_radio, end=None) + + self.should_read = False def scan(self): "Scan for available BLE devices" diff --git a/poetry.lock b/poetry.lock index dd7228f..915c63f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -762,6 +762,17 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "print-color" +version = "0.4.6" +description = "A simple package to print in color to the terminal" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "print_color-0.4.6-py3-none-any.whl", hash = "sha256:494bd1cdb84daf481f0e63bd22b3c32f7d52827d8f5d9138a96bb01ca8ba9299"}, + {file = "print_color-0.4.6.tar.gz", hash = "sha256:d3aafc1666c8d31a85fffa6ee8e4f269f5d5e338d685b4e6179915c71867c585"}, +] + [[package]] name = "protobuf" version = "5.27.1" @@ -1552,4 +1563,4 @@ tunnel = [] [metadata] lock-version = "2.0" python-versions = "^3.8,<3.13" -content-hash = "8548a8b432a3f62db158f5b35254b05b2599aafe75ef12100471937fd4603e3c" +content-hash = "8e82c70af84ffd1525ece9c446bf06c9a1a1235cdf3bb6c563413daf389de353" diff --git a/pyproject.toml b/pyproject.toml index dc1f2ae..730ca20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ pyyaml = "^6.0.1" pypubsub = "^4.0.3" bleak = "^0.21.1" packaging = "^24.0" +print-color = "^0.4.6" [tool.poetry.group.dev.dependencies] hypothesis = "^6.103.2" From 6df89f54a79f8a012b8d1040e6339147c9ef7caf Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 29 Jun 2024 09:10:20 -0700 Subject: [PATCH 04/13] 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() From adbfb328b201bea5da8777007568a7583d9e9016 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 29 Jun 2024 09:29:29 -0700 Subject: [PATCH 05/13] threads should aways be named (for debugability) --- meshtastic/ble_interface.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 9067878..9b9b602 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -19,10 +19,10 @@ FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453" class BLEInterface(MeshInterface): - """MeshInterface using BLE to connect to devices""" + """MeshInterface using BLE to connect to devices.""" class BLEError(Exception): - """An exception class for BLE errors""" + """An exception class for BLE errors.""" def __init__(self, message): self.message = message @@ -54,7 +54,7 @@ class BLEInterface(MeshInterface): logging.debug("Threads running") try: - logging.debug(f"BLE connecting to: {address}") + logging.debug(f"BLE connecting to: {address if address else 'any'}") self.client = self.connect(address) self.state.BLE = True logging.debug("BLE connected") @@ -178,7 +178,7 @@ class BLEClient: """Client for managing connection to a BLE device""" def __init__(self, address=None, **kwargs): - self._eventThread = Thread(target=self._run_event_loop) + self._eventThread = Thread(target=self._run_event_loop, name="BLEClient") self._eventThread_started = Event() self._eventThread_stopped = Event() self._eventThread.start() From dc50a60b016552a7e5513a6096ec762925b52d1c Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 29 Jun 2024 09:35:42 -0700 Subject: [PATCH 06/13] BLE debugging --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c1bb9ef..ff2f013 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "module": "meshtastic", "justMyCode": false, - "args": ["--debug", "--ble", "24:62:AB:DD:DF:3A"] + "args": ["--debug", "--ble", "--ble-dest", "Meshtastic_9f6e"] }, { "name": "meshtastic BLE scan", From 9f015f499ae2692d54ea0dc7295629997a6fc364 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 29 Jun 2024 10:18:37 -0700 Subject: [PATCH 07/13] wow Bleak now seems pretty buggy to me (compared to 2 yrs ago) --- meshtastic/ble_interface.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index d1ca6ca..fbe77a9 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -26,10 +26,7 @@ class BLEInterface(MeshInterface): class BLEError(Exception): """An exception class for BLE errors.""" - - def __init__(self, message): - self.message = message - super().__init__(self.message) + pass class BLEState: # pylint: disable=C0115 THREADS = False @@ -118,15 +115,21 @@ class BLEInterface(MeshInterface): def find_device(self, address: Optional[str]) -> BLEDevice: """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, + # Bleak scan is buggy (only on linux?) Try a few times + for _ in range(5): + addressed_devices = BLEInterface.scan() + + if address: + addressed_devices = list( + filter( + lambda x: address == x.name or address == x.address, + addressed_devices, + ) ) - ) + # We finally found something? + if len(addressed_devices) > 0: + break if len(addressed_devices) == 0: raise BLEInterface.BLEError( @@ -158,7 +161,10 @@ class BLEInterface(MeshInterface): self.should_read = False retries = 0 while True: - b = bytes(self.client.read_gatt_char(FROMRADIO_UUID)) + try: + b = bytes(self.client.read_gatt_char(FROMRADIO_UUID)) + except Exception as e: + raise BLEInterface.BLEError("Error reading BLE") from e if not b: if retries < 5: time.sleep(0.1) @@ -175,7 +181,10 @@ 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) + try: + self.client.write_gatt_char(TORADIO_UUID, b, response=False) + except Exception as e: + raise BLEInterface.BLEError("Error writing BLE") from e # Allow to propagate and then make sure we read time.sleep(0.1) self.should_read = True From 898018ebf35df827d600a05ab5bb12ee098cdaa7 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 29 Jun 2024 13:52:46 -0700 Subject: [PATCH 08/13] yay Bleak works again on Linux! --- .vscode/launch.json | 2 +- meshtastic/ble_interface.py | 43 ++++----- poetry.lock | 187 +++++++++++++++++------------------- 3 files changed, 112 insertions(+), 120 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ff2f013..6c9c892 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "module": "meshtastic", "justMyCode": false, - "args": ["--debug", "--ble", "--ble-dest", "Meshtastic_9f6e"] + "args": ["--ble", "--ble-dest", "Meshtastic_9f6e"] }, { "name": "meshtastic BLE scan", diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index fbe77a9..96293d0 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -45,7 +45,7 @@ class BLEInterface(MeshInterface): self.should_read = False logging.debug("Threads starting") - self._receiveThread = Thread(target=self._receiveFromRadioImpl) + self._receiveThread = Thread(target=self._receiveFromRadioImpl, name="BLEReceive", daemon=True) self._receiveThread_started = Event() self._receiveThread_stopped = Event() self._receiveThread.start() @@ -62,6 +62,8 @@ class BLEInterface(MeshInterface): self.close() raise e + self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler) + logging.debug("Mesh init starting") MeshInterface.__init__( self, debugOut=debugOut, noProto=noProto, noNodes=noNodes @@ -75,9 +77,10 @@ class BLEInterface(MeshInterface): logging.debug("Register FROMNUM notify callback") self.client.start_notify(FROMNUM_UUID, self.from_num_handler) - self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler) - async def from_num_handler(self, _, b): # pylint: disable=C0116 + def from_num_handler(self, _, b): # pylint: disable=C0116 + """Handle callbacks for fromnum notify. + Note: this method does not need to be async because it is just setting a bool.""" from_num = struct.unpack(" list[BLEDevice]: """Scan for available BLE devices.""" with BLEClient() as client: - response = client.discover(return_adv=True, service_uuids=[SERVICE_UUID]) + logging.info("Scanning for BLE devices (takes 10 seconds)...") + response = client.discover(timeout=10, return_adv=True, service_uuids=[SERVICE_UUID]) devices = response.values() @@ -116,20 +118,15 @@ class BLEInterface(MeshInterface): def find_device(self, address: Optional[str]) -> BLEDevice: """Find a device by address.""" - # Bleak scan is buggy (only on linux?) Try a few times - for _ in range(5): - addressed_devices = BLEInterface.scan() + addressed_devices = BLEInterface.scan() - if address: - addressed_devices = list( - filter( - lambda x: address == x.name or address == x.address, - addressed_devices, - ) + if address: + addressed_devices = list( + filter( + lambda x: address == x.name or address == x.address, + addressed_devices, ) - # We finally found something? - if len(addressed_devices) > 0: - break + ) if len(addressed_devices) == 0: raise BLEInterface.BLEError( @@ -152,6 +149,7 @@ class BLEInterface(MeshInterface): device = self.find_device(address) client = BLEClient(device.address) client.connect() + client.discover() return client def _receiveFromRadioImpl(self): @@ -182,11 +180,12 @@ class BLEInterface(MeshInterface): if b: logging.debug(f"TORADIO write: {b.hex()}") try: - self.client.write_gatt_char(TORADIO_UUID, b, response=False) + self.client.write_gatt_char(TORADIO_UUID, b, response=True) # FIXME: or False? + # search Bleak src for org.bluez.Error.InProgress except Exception as e: - raise BLEInterface.BLEError("Error writing BLE") from e + raise BLEInterface.BLEError("Error writing BLE (are you in the 'bluetooth' user group? did you enter the pairing PIN on your computer?)") from e # Allow to propagate and then make sure we read - time.sleep(0.1) + time.sleep(0.01) self.should_read = True def close(self): @@ -206,7 +205,7 @@ class BLEClient: """Client for managing connection to a BLE device""" def __init__(self, address=None, **kwargs): - self._eventThread = Thread(target=self._run_event_loop, name="BLEClient") + self._eventThread = Thread(target=self._run_event_loop, name="BLEClient", daemon=True) self._eventThread_started = Event() self._eventThread_stopped = Event() self._eventThread.start() diff --git a/poetry.lock b/poetry.lock index 915c63f..3331326 100644 --- a/poetry.lock +++ b/poetry.lock @@ -57,13 +57,13 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "autopep8" -version = "2.3.0" +version = "2.3.1" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" optional = false python-versions = ">=3.8" files = [ - {file = "autopep8-2.3.0-py2.py3-none-any.whl", hash = "sha256:b716efa70cbafbf4a2c9c5ec1cabfa037a68f9e30b04c74ffa5864dd49b8f7d2"}, - {file = "autopep8-2.3.0.tar.gz", hash = "sha256:5cfe45eb3bef8662f6a3c7e28b7c0310c7310d340074b7f0f28f9810b44b7ef4"}, + {file = "autopep8-2.3.1-py2.py3-none-any.whl", hash = "sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d"}, + {file = "autopep8-2.3.1.tar.gz", hash = "sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda"}, ] [package.dependencies] @@ -307,45 +307,39 @@ toml = ["tomli"] [[package]] name = "dbus-fast" -version = "2.21.3" +version = "2.22.1" description = "A faster version of dbus-next" optional = false -python-versions = "<4.0,>=3.7" +python-versions = "<4.0,>=3.8" files = [ - {file = "dbus_fast-2.21.3-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:828f2a337eac4c3b24b43ab4edc8d8bc656f558a4f07aa2b173e007ce093bd49"}, - {file = "dbus_fast-2.21.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b379ed7ef0d174480e41a5f1dde3392d974e618bb91e5fbfa06396c24d3c80fc"}, - {file = "dbus_fast-2.21.3-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:990d60e9796fa142e16af331e53d91aaa94dfbcf37b474c1d6caf61310fcc5ee"}, - {file = "dbus_fast-2.21.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9d0bbfa7cdb440f13d58e13625344b918b70ff0ccddc20ddd9c0ebf3e5a765dd"}, - {file = "dbus_fast-2.21.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0d4f459ba4fa394e3ba22a7421055878953aa92efd01e3a1d5216519c6b1586c"}, - {file = "dbus_fast-2.21.3-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:61d20cecc3efdc0e75bb7d5f4ae18929559003644b32945bfaa93b7e06cd94b6"}, - {file = "dbus_fast-2.21.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d53f5b24c732af5ae9c7e88fc9ba687ce2a785c63dcea3b9c984619f1bdcf71a"}, - {file = "dbus_fast-2.21.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b5ef802b2b7e5dbebdfa338a0278e5212a6073c26764c75f3e373e2a9b01797c"}, - {file = "dbus_fast-2.21.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:251d46d0d7cbed0d9b9eac2f91f6669893db9b87e19defb99f9a85579c2f786a"}, - {file = "dbus_fast-2.21.3-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:0665d8cb179f0b8fff23e63592c1f454fdaa4ae44a4263a7a7b7df8d834b3f71"}, - {file = "dbus_fast-2.21.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29ca29609a31f816c315844ed41b81247e3114261d26e5ee1dcc85bf5c046a36"}, - {file = "dbus_fast-2.21.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b520792549e8b2b1e4c8777492783ba81065bd02e16e4390e2b299bf33f1feea"}, - {file = "dbus_fast-2.21.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f44d2ea35daefac7ad1ede65695fde18526fb38f9ec0aadf108f629bb6c87293"}, - {file = "dbus_fast-2.21.3-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:50aa62f63de3e591d739b4925816b84f4169e9086701a2722a5e7a1f6f273bc0"}, - {file = "dbus_fast-2.21.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8fad077a989b002602aa192cfa95b89b3e40c5fa6da7740f42a87488bdbed6f"}, - {file = "dbus_fast-2.21.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87e8db4ea5023024a638826321039497dcbc7e70583bd33743eac2d8e69ca4fb"}, - {file = "dbus_fast-2.21.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0c342d8b33079c550ea575344d53807f6ae6464b1a5f6f9e0523fae979198872"}, - {file = "dbus_fast-2.21.3-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:41d6f81a5226e90f1bde95ce90a63430f58aea0c300f034b4055a7bfae187031"}, - {file = "dbus_fast-2.21.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d76b512cc8db4ebdfb7879d7cae42ee0adc362671bc0a4f55df5f4ebe547602d"}, - {file = "dbus_fast-2.21.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9fdbe2b22668f4021e909e65fa6a25bca1ab08294a35c600af95ba06a2f2d101"}, - {file = "dbus_fast-2.21.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:638c4b64159f8a3567e38705246bd1a2625d8c9adbb7ffa23a6a2ec2dfd40db0"}, - {file = "dbus_fast-2.21.3-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:dadc4bdcbe808f0d1750f951b3b4211763f280116714cb9749ebae2262bdc49c"}, - {file = "dbus_fast-2.21.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83e0a28e04218493ebd66c1f2a5290203ffff924ec01b37c5128ba1fa9731255"}, - {file = "dbus_fast-2.21.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:846733011edad8c0125f2b1148783c8d2ae162419707bb7e2bf08a26040939d8"}, - {file = "dbus_fast-2.21.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:07213240465c3c7306705ad512c983ada45ef222d2eecf3d7ab19f397b02de0d"}, - {file = "dbus_fast-2.21.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:8bfea9007a654adc5c16d43d124fded0c788fdb2a6e2c470fcfd7d0076bda87e"}, - {file = "dbus_fast-2.21.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5b202ffd4314c82f68b2431d928d596c45def381c018832003045f19ed857a"}, - {file = "dbus_fast-2.21.3-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:19091565dd9b5db9b3fa82459361c459387c01b11a656f36cab6a73284300c8c"}, - {file = "dbus_fast-2.21.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa74eb299ec88319a6a46c9b59aeebf9782378d9724913bcb3fb746a3222f70a"}, - {file = "dbus_fast-2.21.3-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:2bb2a659f31e1af87a3c4e41af3af69cb5a2bb4a335b35d8d6e80b43e8aed8e9"}, - {file = "dbus_fast-2.21.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56bf648a329257b127ee05667451e929c50ada7117737d14341a5399ca7860e1"}, - {file = "dbus_fast-2.21.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4bb07da46377b7affe648ce34ac42fb3409e87b40b55d64f0fd23512e583ce46"}, - {file = "dbus_fast-2.21.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b07d22e167b0af834344bd1c8619b702b823d8989d6884fc9719c6e871c413f5"}, - {file = "dbus_fast-2.21.3.tar.gz", hash = "sha256:8d0f0f61d007c1316ce79cde35ed52c0ce8ce229fd0f0bf8c9af2013ab4516a7"}, + {file = "dbus_fast-2.22.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f19c08fc0ab5f0e209e008f4646bb0624eacb96fb54367ea36e450aacfe289f"}, + {file = "dbus_fast-2.22.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:714c5bca7d1ae20557a5857fdb3022ff0a3f5ef2e14379eae0403940882a4d72"}, + {file = "dbus_fast-2.22.1-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:ac004b0f6a7f7b58ae7488f12463df68199546a8d71085379b5eed17ae012905"}, + {file = "dbus_fast-2.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a54533ee4b30a2c062078c02d10c5a258fc10eac51a0b85cfdd7f690f1d6285f"}, + {file = "dbus_fast-2.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cadf90548aaf336820e0b7037b0f0f46b9836ac0f2c6af0f494b00fe6bc23929"}, + {file = "dbus_fast-2.22.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e213b0252f97d6a9ceb97cd2d84ddac0d998b8dd15bdca051def181a666b6a"}, + {file = "dbus_fast-2.22.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6497859da721041dbf7615aab1cae666e5c0a169fca80032ab2fd8b03f7730f5"}, + {file = "dbus_fast-2.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a3ba17d91a32b53f8e16b40e7f948260847f3e8fbbbf83872dafe44b38a1ae42"}, + {file = "dbus_fast-2.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2b7f32e765051817d58e3242697b47cfe5def086181ad1087c9bc70e2db48004"}, + {file = "dbus_fast-2.22.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beebe8cbd0cd90d24b757c4aad617fcfa77f2e654287bc80b11c0e4964891c22"}, + {file = "dbus_fast-2.22.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b72ebd07ac873906f1001cb6eb75e864e30cb6cdcce17afe79939987b0a28b5"}, + {file = "dbus_fast-2.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c73e3b59de2b6e7447b1c3d26ccd307838d05c6a85bcc9eac7bc990bb843cc92"}, + {file = "dbus_fast-2.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dcb333f56ebb0de5cf3aa8affb9c492bd821e252d704dcce444a379c0513c6be"}, + {file = "dbus_fast-2.22.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2980b92493698f80910b3521d685ce230f94d93deac0bcf33f2082ce551b8ac5"}, + {file = "dbus_fast-2.22.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d88f7f1d4124feb4418f5d9efe359661e2f38e89f6c31539d998e3769f7f7b3"}, + {file = "dbus_fast-2.22.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:bf198217013b068fe610b1d5ce7ce53e15b993625331d2c83f53be5744c0be40"}, + {file = "dbus_fast-2.22.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f90017ba2c95dba4c1e417850d3c735d5eb464cbe0ebfb5d49cc0e95e7d916d2"}, + {file = "dbus_fast-2.22.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e6d2cd04da08a9d21be68faa4d23123a2f4cb5cef3406cc1a2ef900507b1c0"}, + {file = "dbus_fast-2.22.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2735f9cc9e6692b0bb114c48580709af824a16ea791922f628c265aa05f183a"}, + {file = "dbus_fast-2.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b709a9eaaae542d0d883c5a2f147c0cbe7ef29262ec0bf90f5a5945e76786c39"}, + {file = "dbus_fast-2.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7e7924d5042de42dcdc6be942d2f6cf1f187cf7a4ae2902b68431ea856ef654c"}, + {file = "dbus_fast-2.22.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e15b15c0bdef24f86a5940539ba68d0920d58b96cca8543fbda9189cb144fb13"}, + {file = "dbus_fast-2.22.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f70821ac238e3fa0f5a6ae4e99054d57261743f5d5516e43226f2bec0065a3d"}, + {file = "dbus_fast-2.22.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e56f6f0976aa953a2a5c71817e9ceecace6dd6a2a23dc64622025701005bf15"}, + {file = "dbus_fast-2.22.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6f894fe9b60374dc20c43bdf7a5b4a81e2db963433815a9d6ceaaeb51cba801"}, + {file = "dbus_fast-2.22.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0934118cc2e4f777d785df923b139f253ba3019469ec1f90eb8a5e4c12fff0ce"}, + {file = "dbus_fast-2.22.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994931d9bc57166a9e16ae71cb93133fa87f35d57125d741a92a1f4e56cade28"}, + {file = "dbus_fast-2.22.1.tar.gz", hash = "sha256:aa75dfb5bc7ba42f53391ae503ca5a21bd133e74ebb09965013ba23bdffc9a0e"}, ] [[package]] @@ -390,13 +384,13 @@ test = ["pytest (>=6)"] [[package]] name = "hypothesis" -version = "6.103.2" +version = "6.104.2" description = "A library for property-based testing" optional = false python-versions = ">=3.8" files = [ - {file = "hypothesis-6.103.2-py3-none-any.whl", hash = "sha256:629b7cdeca8c225933739f99879caba21949000d2c919c8b4585e01048b3bc73"}, - {file = "hypothesis-6.103.2.tar.gz", hash = "sha256:83504e31e90a0d7d6e8eb93e51525dc1a48d79c932a50ad6035e29f8295328cd"}, + {file = "hypothesis-6.104.2-py3-none-any.whl", hash = "sha256:8b52b7e2462e552c75b819495d5cb6251a2b840accc79cf2ce52588004c915d9"}, + {file = "hypothesis-6.104.2.tar.gz", hash = "sha256:6f2a1489bc8fe1c87ffd202707319b66ec46b2bc11faf6e0161e957b8b9b1eab"}, ] [package.dependencies] @@ -405,10 +399,10 @@ exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.54)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.4)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1)"] +all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.55)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.4)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1)"] cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] -crosshair = ["crosshair-tool (>=0.0.54)", "hypothesis-crosshair (>=0.0.4)"] +crosshair = ["crosshair-tool (>=0.0.55)", "hypothesis-crosshair (>=0.0.4)"] dateutil = ["python-dateutil (>=1.4)"] django = ["django (>=3.2)"] dpcontracts = ["dpcontracts (>=0.4)"] @@ -434,13 +428,13 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.2.0" +version = "8.0.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.2.0-py3-none-any.whl", hash = "sha256:04e4aad329b8b948a5711d394fa8759cb80f009225441b4f2a02bd4d8e5f426c"}, - {file = "importlib_metadata-7.2.0.tar.gz", hash = "sha256:3ff4519071ed42740522d494d04819b666541b9752c43012f85afb2cc220fcc6"}, + {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, + {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, ] [package.dependencies] @@ -609,38 +603,38 @@ files = [ [[package]] name = "mypy" -version = "1.10.0" +version = "1.10.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, - {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, - {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, - {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, - {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, - {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, - {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, - {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, - {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, - {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, - {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, - {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, - {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, - {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, - {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, - {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, - {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, - {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, - {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, + {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, + {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, + {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, + {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, + {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, + {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, + {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, + {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, + {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, ] [package.dependencies] @@ -775,22 +769,22 @@ files = [ [[package]] name = "protobuf" -version = "5.27.1" +version = "5.27.2" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-5.27.1-cp310-abi3-win32.whl", hash = "sha256:3adc15ec0ff35c5b2d0992f9345b04a540c1e73bfee3ff1643db43cc1d734333"}, - {file = "protobuf-5.27.1-cp310-abi3-win_amd64.whl", hash = "sha256:25236b69ab4ce1bec413fd4b68a15ef8141794427e0b4dc173e9d5d9dffc3bcd"}, - {file = "protobuf-5.27.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4e38fc29d7df32e01a41cf118b5a968b1efd46b9c41ff515234e794011c78b17"}, - {file = "protobuf-5.27.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:917ed03c3eb8a2d51c3496359f5b53b4e4b7e40edfbdd3d3f34336e0eef6825a"}, - {file = "protobuf-5.27.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:ee52874a9e69a30271649be88ecbe69d374232e8fd0b4e4b0aaaa87f429f1631"}, - {file = "protobuf-5.27.1-cp38-cp38-win32.whl", hash = "sha256:7a97b9c5aed86b9ca289eb5148df6c208ab5bb6906930590961e08f097258107"}, - {file = "protobuf-5.27.1-cp38-cp38-win_amd64.whl", hash = "sha256:f6abd0f69968792da7460d3c2cfa7d94fd74e1c21df321eb6345b963f9ec3d8d"}, - {file = "protobuf-5.27.1-cp39-cp39-win32.whl", hash = "sha256:dfddb7537f789002cc4eb00752c92e67885badcc7005566f2c5de9d969d3282d"}, - {file = "protobuf-5.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:39309898b912ca6febb0084ea912e976482834f401be35840a008da12d189340"}, - {file = "protobuf-5.27.1-py3-none-any.whl", hash = "sha256:4ac7249a1530a2ed50e24201d6630125ced04b30619262f06224616e0030b6cf"}, - {file = "protobuf-5.27.1.tar.gz", hash = "sha256:df5e5b8e39b7d1c25b186ffdf9f44f40f810bbcc9d2b71d9d3156fee5a9adf15"}, + {file = "protobuf-5.27.2-cp310-abi3-win32.whl", hash = "sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38"}, + {file = "protobuf-5.27.2-cp310-abi3-win_amd64.whl", hash = "sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505"}, + {file = "protobuf-5.27.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5"}, + {file = "protobuf-5.27.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b"}, + {file = "protobuf-5.27.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e"}, + {file = "protobuf-5.27.2-cp38-cp38-win32.whl", hash = "sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863"}, + {file = "protobuf-5.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6"}, + {file = "protobuf-5.27.2-cp39-cp39-win32.whl", hash = "sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca"}, + {file = "protobuf-5.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce"}, + {file = "protobuf-5.27.2-py3-none-any.whl", hash = "sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470"}, + {file = "protobuf-5.27.2.tar.gz", hash = "sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714"}, ] [[package]] @@ -868,13 +862,13 @@ setuptools = ">=42.0.0" [[package]] name = "pylint" -version = "3.2.3" +version = "3.2.5" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.2.3-py3-none-any.whl", hash = "sha256:b3d7d2708a3e04b4679e02d99e72329a8b7ee8afb8d04110682278781f889fa8"}, - {file = "pylint-3.2.3.tar.gz", hash = "sha256:02f6c562b215582386068d52a30f520d84fdbcf2a95fc7e855b816060d048b60"}, + {file = "pylint-3.2.5-py3-none-any.whl", hash = "sha256:32cd6c042b5004b8e857d727708720c54a676d1e22917cf1a2df9b4d4868abd6"}, + {file = "pylint-3.2.5.tar.gz", hash = "sha256:e9b7171e242dcc6ebd0aaa7540481d1a72860748a0a7816b8fe6cf6c80a6fe7e"}, ] [package.dependencies] @@ -1109,7 +1103,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1167,13 +1160,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "setuptools" -version = "70.1.0" +version = "70.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, - {file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, + {file = "setuptools-70.1.1-py3-none-any.whl", hash = "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95"}, + {file = "setuptools-70.1.1.tar.gz", hash = "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650"}, ] [package.extras] @@ -1229,13 +1222,13 @@ files = [ [[package]] name = "types-protobuf" -version = "5.26.0.20240422" +version = "5.27.0.20240626" description = "Typing stubs for protobuf" optional = false python-versions = ">=3.8" files = [ - {file = "types-protobuf-5.26.0.20240422.tar.gz", hash = "sha256:e6074178109f97efe9f0b20a035ba61d7c3b03e867eb47d254d2b2ab6a805e36"}, - {file = "types_protobuf-5.26.0.20240422-py3-none-any.whl", hash = "sha256:e4dc2554d342501d5aebc3c71203868b51118340e105fc190e3a64ca1be43831"}, + {file = "types-protobuf-5.27.0.20240626.tar.gz", hash = "sha256:683ba14043bade6785e3f937a7498f243b37881a91ac8d81b9202ecf8b191e9c"}, + {file = "types_protobuf-5.27.0.20240626-py3-none-any.whl", hash = "sha256:688e8f7e8d9295db26bc560df01fb731b27a25b77cbe4c1ce945647f7024f5c1"}, ] [[package]] From 532ca54ba4a1494f12a78ec300b4588769df4f2f Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 29 Jun 2024 14:25:01 -0700 Subject: [PATCH 09/13] fix bug: we were never calling BLE.disconnect() which... on linux breaks all but the first connection attempts. Also remove unneeded event stuff and arbitrary timeouts, better just to use thread.join() --- meshtastic/ble_interface.py | 91 ++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 96293d0..0b5c8b7 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -1,14 +1,15 @@ """Bluetooth interface """ import asyncio +import atexit import logging import struct import time -from threading import Event, Thread +from threading import Thread from typing import Optional -from print_color import print from bleak import BleakClient, BleakScanner, BLEDevice +from print_color import print from meshtastic.mesh_interface import MeshInterface from meshtastic.util import our_exit @@ -20,18 +21,13 @@ FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453" LOGRADIO_UUID = "6c6fd238-78fa-436b-aacf-15c5be1ef2e2" - class BLEInterface(MeshInterface): """MeshInterface using BLE to connect to devices.""" class BLEError(Exception): """An exception class for BLE errors.""" - pass - class BLEState: # pylint: disable=C0115 - THREADS = False - BLE = False - MESH = False + pass def __init__( self, @@ -40,23 +36,23 @@ class BLEInterface(MeshInterface): debugOut=None, noNodes: bool = False, ): - self.state = BLEInterface.BLEState() + MeshInterface.__init__( + self, debugOut=debugOut, noProto=noProto, noNodes=noNodes + ) self.should_read = False logging.debug("Threads starting") - self._receiveThread = Thread(target=self._receiveFromRadioImpl, name="BLEReceive", daemon=True) - self._receiveThread_started = Event() - self._receiveThread_stopped = Event() + self._want_receive = True + self._receiveThread: Optional[Thread] = Thread( + target=self._receiveFromRadioImpl, name="BLEReceive", daemon=True + ) self._receiveThread.start() - self._receiveThread_started.wait(1) - self.state.THREADS = True logging.debug("Threads running") try: logging.debug(f"BLE connecting to: {address if address else 'any'}") - self.client = self.connect(address) - self.state.BLE = True + self.client: Optional[BLEClient] = self.connect(address) logging.debug("BLE connected") except BLEInterface.BLEError as e: self.close() @@ -64,29 +60,30 @@ class BLEInterface(MeshInterface): self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler) - logging.debug("Mesh init starting") - MeshInterface.__init__( - self, debugOut=debugOut, noProto=noProto, noNodes=noNodes - ) + logging.debug("Mesh configure starting") self._startConfig() if not self.noProto: self._waitConnected(timeout=60.0) self.waitForConfig() - self.state.MESH = True 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) + def from_num_handler(self, _, b): # pylint: disable=C0116 """Handle callbacks for fromnum notify. - Note: this method does not need to be async because it is just setting a bool.""" + Note: this method does not need to be async because it is just setting a bool. + """ from_num = struct.unpack(" Date: Sat, 29 Jun 2024 14:37:50 -0700 Subject: [PATCH 10/13] fix linter warnings --- meshtastic/__main__.py | 3 +-- meshtastic/ble_interface.py | 17 +++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 8bebf41..7458e66 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1044,7 +1044,6 @@ def common(): 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: client = BLEInterface(args.ble_dest, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes) elif args.host: @@ -1122,7 +1121,7 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse "--ble-dest", help="The BLE device address or name to connect to", default=None, - ) + ) return parser diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 0b5c8b7..4c24528 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -9,10 +9,9 @@ from threading import Thread from typing import Optional from bleak import BleakClient, BleakScanner, BLEDevice -from print_color import print +import print_color from meshtastic.mesh_interface import MeshInterface -from meshtastic.util import our_exit SERVICE_UUID = "6ba1b218-15a8-461f-9fa8-5dcae273eafd" TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7" @@ -27,8 +26,6 @@ class BLEInterface(MeshInterface): class BLEError(Exception): """An exception class for BLE errors.""" - pass - def __init__( self, address: Optional[str], @@ -85,15 +82,15 @@ class BLEInterface(MeshInterface): async def log_radio_handler(self, _, b): # pylint: disable=C0116 log_radio = b.decode("utf-8").replace("\n", "") if log_radio.startswith("DEBUG"): - print(log_radio, color="cyan", end=None) + print_color.print(log_radio, color="cyan", end=None) elif log_radio.startswith("INFO"): - print(log_radio, color="white", end=None) + print_color.print(log_radio, color="white", end=None) elif log_radio.startswith("WARN"): - print(log_radio, color="yellow", end=None) + print_color.print(log_radio, color="yellow", end=None) elif log_radio.startswith("ERROR"): - print(log_radio, color="red", end=None) + print_color.print(log_radio, color="red", end=None) else: - print(log_radio, end=None) + print_color.print(log_radio, end=None) @staticmethod def scan() -> list[BLEDevice]: @@ -122,7 +119,7 @@ class BLEInterface(MeshInterface): if address: addressed_devices = list( filter( - lambda x: address == x.name or address == x.address, + lambda x: address in (x.name, x.address), addressed_devices, ) ) From bd767af4857022caab2f7ea51fe076ef0c384b45 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 29 Jun 2024 15:08:50 -0700 Subject: [PATCH 11/13] 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 From 9004f1ed57155fcbc040016918fca6a17d657781 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 29 Jun 2024 15:12:11 -0700 Subject: [PATCH 12/13] fixes for type checker --- meshtastic/ble_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 4357a4b..69cb9a3 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -8,7 +8,7 @@ import time from threading import Thread from typing import Optional -import print_color +import print_color # type: ignore[import-untyped] from bleak import BleakClient, BleakScanner, BLEDevice from bleak.exc import BleakDBusError, BleakError @@ -139,7 +139,7 @@ class BLEInterface(MeshInterface): "Standardize BLE address by removing extraneous characters and lowercasing." return address.replace("-", "").replace("_", "").replace(":", "").lower() - def connect(self, address: Optional[str] = None): + def connect(self, address: Optional[str] = None) -> "BLEClient": "Connect to a device by address." # Bleak docs recommend always doing a scan before connecting (even if we know addr) From 5c2851db858deba184fdd3bc3c6de8f19024d6cf Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sun, 30 Jun 2024 06:46:39 -0700 Subject: [PATCH 13/13] nargs='?' is awesome! get rid of ble_dest. --- .vscode/launch.json | 2 +- meshtastic/__main__.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6c9c892..e2534db 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "module": "meshtastic", "justMyCode": false, - "args": ["--ble", "--ble-dest", "Meshtastic_9f6e"] + "args": ["--ble", "Meshtastic_9f6e"] }, { "name": "meshtastic BLE scan", diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index e58910a..173d6e0 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1046,7 +1046,7 @@ def common(): print(f"Found: name='{x.name}' address='{x.address}'") meshtastic.util.our_exit("BLE scan finished", 0) elif args.ble: - client = BLEInterface(args.ble_dest, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes) + client = BLEInterface(args.ble if args.ble != "any" else None, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes) elif args.host: try: client = meshtastic.tcp_interface.TCPInterface( @@ -1114,14 +1114,10 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse group.add_argument( "--ble", - help="The BLE device address or name to connect to", - action="store_true", - ) - - outer.add_argument( - "--ble-dest", - help="The BLE device address or name to connect to", + help="Connect to a BLE device, optionally specifying a device name (defaults to 'any')", + nargs="?", default=None, + const="any" ) return parser