Merge pull request #616 from meshtastic/ble-logging

Adds support for ble logging characteristic
This commit is contained in:
Ian McEwen
2024-06-30 09:33:05 -07:00
committed by GitHub
5 changed files with 223 additions and 165 deletions

18
.vscode/launch.json vendored
View File

@@ -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",

View File

@@ -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

View File

@@ -1,141 +1,173 @@
"""Bluetooth interface
"""
import logging
import time
import struct
import asyncio
from threading import Thread, Event
import atexit
import logging
import struct
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"""
"""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)
"""An exception class for BLE errors."""
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):
self.state = BLEInterface.BLEState()
if not address:
return
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
logging.debug("Mesh init starting")
MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto, noNodes = noNodes)
self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler)
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('<I', bytes(b))[0]
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("<I", bytes(b))[0]
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_color.print(log_radio, color="cyan", end=None)
elif log_radio.startswith("INFO"):
print_color.print(log_radio, color="white", end=None)
elif log_radio.startswith("WARN"):
print_color.print(log_radio, color="yellow", end=None)
elif log_radio.startswith("ERROR"):
print_color.print(log_radio, color="red", end=None)
else:
print_color.print(log_radio, end=None)
def scan(self):
"Scan for available BLE devices"
@staticmethod
def scan() -> 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()
def find_device(self, address):
"Find a device by address"
meshtastic_devices = self.scan()
# 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))
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 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.")
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]
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):
"Connect to a device by address"
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)
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 @@ class BLEInterface(MeshInterface):
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 @@ 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)
self._eventThread.join()
def __enter__(self):
return self
@@ -218,21 +261,17 @@ class BLEClient():
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()

91
poetry.lock generated
View File

@@ -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.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"

View File

@@ -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"