diff --git a/.vscode/launch.json b/.vscode/launch.json index aca86dde..e2534db4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,15 @@ "request": "launch", "module": "meshtastic", "justMyCode": false, - "args": ["--debug", "--ble", "24:62:AB:DD:DF:3A"] + "args": ["--ble", "Meshtastic_9f6e"] + }, + { + "name": "meshtastic BLE scan", + "type": "python", + "request": "launch", + "module": "meshtastic", + "justMyCode": false, + "args": ["--debug", "--ble-scan"] }, { "name": "meshtastic admin", @@ -76,6 +84,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 781f6bf9..173d6e07 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1042,16 +1042,11 @@ 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: - client = BLEInterface(args.ble, 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( @@ -1119,8 +1114,10 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse group.add_argument( "--ble", - 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 diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 1c12758d..69cb9a3b 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -1,141 +1,173 @@ """Bluetooth interface """ +import asyncio +import atexit import logging -import time import struct -import asyncio -from threading import Thread, Event +import time +from threading import Thread from typing import Optional -from bleak import BleakScanner, BleakClient +import print_color # type: ignore[import-untyped] +from bleak import BleakClient, BleakScanner, BLEDevice +from bleak.exc import BleakDBusError, BleakError 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" FROMRADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002" 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""" - def __init__(self, message): - self.message = message - super().__init__(self.message) - - class BLEState(): # pylint: disable=C0115 - THREADS = False - BLE = False - MESH = False - + """MeshInterface using BLE to connect to devices.""" - def __init__(self, address: Optional[str], noProto: bool = False, debugOut = None, noNodes: bool = False): - self.state = BLEInterface.BLEState() - - if not address: - return + class BLEError(Exception): + """An exception class for BLE errors.""" + + def __init__( + self, + address: Optional[str], + noProto: bool = False, + debugOut=None, + noNodes: bool = False, + ): + MeshInterface.__init__( + self, debugOut=debugOut, noProto=noProto, noNodes=noNodes + ) self.should_read = False logging.debug("Threads starting") - self._receiveThread = Thread(target = self._receiveFromRadioImpl) - 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}") - self.client = self.connect(address) - self.state.BLE = True + logging.debug(f"BLE connecting to: {address if address else 'any'}") + self.client: Optional[BLEClient] = self.connect(address) logging.debug("BLE connected") except BLEInterface.BLEError as e: self.close() - our_exit(e.message, 1) - return + 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) + logging.debug("Mesh configure starting") 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") 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) + # 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) - async def from_num_handler(self, _, b): # pylint: disable=C0116 - from_num = struct.unpack(' list[BLEDevice]: + """Scan for available BLE devices.""" with BLEClient() as client: - return [ - (x[0], x[1]) for x in (client.discover( - return_adv = True, - service_uuids = [ SERVICE_UUID ] - )).values() - ] + logging.info("Scanning for BLE devices (takes 10 seconds)...") + response = client.discover( + timeout=10, 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) + ) + return list(map(lambda d: d[0], devices)) + + 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 in (x.name, 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] - def find_device(self, address): - "Find a device by address" - meshtastic_devices = self.scan() + def _sanitize_address(address): # pylint: disable=E0213 + "Standardize BLE address by removing extraneous characters and lowercasing." + return address.replace("-", "").replace("_", "").replace(":", "").lower() - 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)) + def connect(self, address: Optional[str] = None) -> "BLEClient": + "Connect to a device by address." - 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] - - 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): - "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 = BLEClient(device.address, disconnected_callback=lambda _: self.close) client.connect() - try: - client.pair() - except NotImplementedError: - # Some bluetooth backends do not require explicit pairing. - # See Bleak docs for details on this. - pass + client.discover() return client - def _receiveFromRadioImpl(self): - self._receiveThread_started.set() - while self._receiveThread_started.is_set(): + while self._want_receive: if self.should_read: self.should_read = False retries = 0 - while True: - b = bytes(self.client.read_gatt_char(FROMRADIO_UUID)) + while self._want_receive: + try: + b = bytes(self.client.read_gatt_char(FROMRADIO_UUID)) + 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) @@ -145,40 +177,52 @@ def _receiveFromRadioImpl(self): logging.debug(f"FROMRADIO read: {b.hex()}") self._handleFromRadio(b) else: - time.sleep(0.1) - self._receiveThread_stopped.set() + time.sleep(0.01) def _sendToRadioImpl(self, toRadio): 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=True + ) # FIXME: or False? + # search Bleak src for org.bluez.Error.InProgress + except Exception as 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): - if self.state.MESH: + atexit.unregister(self._exit_handler) + try: MeshInterface.close(self) + except Exception as e: + logging.error(f"Error closing mesh interface: {e}") - if self.state.THREADS: - self._receiveThread_started.clear() - self._receiveThread_stopped.wait(5) + if self._want_receive: + self.want_receive = False # Tell the thread we want it to stop + self._receiveThread.join() + self._receiveThread = None - if self.state.BLE: + if self.client: self.client.disconnect() self.client.close() + self.client = None -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) - self._eventThread_started = Event() - self._eventThread_stopped = Event() + + def __init__(self, address=None, **kwargs): + self._eventLoop = asyncio.new_event_loop() + self._eventThread = Thread( + target=self._run_event_loop, name="BLEClient", daemon=True + ) self._eventThread.start() - self._eventThread_started.wait(1) if not address: logging.debug("No address provided - only discover method will work.") @@ -186,31 +230,30 @@ def __init__(self, address = None, **kwargs): 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) + self._eventThread.join() def __enter__(self): return self @@ -218,21 +261,17 @@ def __enter__(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._eventThread_started.set() try: self._eventLoop.run_forever() finally: self._eventLoop.close() - self._eventThread_stopped.set() async def _stop_event_loop(self): self._eventLoop.stop() diff --git a/poetry.lock b/poetry.lock index 29e130e0..3331326f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" -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"}, +python-versions = "<4.0,>=3.8" +files = [ + {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.104.1" +version = "6.104.2" description = "A library for property-based testing" optional = false python-versions = ">=3.8" files = [ - {file = "hypothesis-6.104.1-py3-none-any.whl", hash = "sha256:a0a898fa78ecaefe76ad248901dc274e598f29198c6015b3053f7f7827670e0e"}, - {file = "hypothesis-6.104.1.tar.gz", hash = "sha256:4033898019a6149823d2feeb8d214921b4ac2d342a05d6b02e40a3ca4be07eea"}, + {file = "hypothesis-6.104.2-py3-none-any.whl", hash = "sha256:8b52b7e2462e552c75b819495d5cb6251a2b840accc79cf2ce52588004c915d9"}, + {file = "hypothesis-6.104.2.tar.gz", hash = "sha256:6f2a1489bc8fe1c87ffd202707319b66ec46b2bc11faf6e0161e957b8b9b1eab"}, ] [package.dependencies] @@ -762,6 +756,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.2" @@ -857,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] @@ -1551,4 +1556,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 dc1f2aed..730ca20d 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"