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", "request": "launch",
"module": "meshtastic", "module": "meshtastic",
"justMyCode": false, "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", "name": "meshtastic admin",
@@ -76,6 +84,14 @@
"justMyCode": true, "justMyCode": true,
"args": ["--debug", "--info"] "args": ["--debug", "--info"]
}, },
{
"name": "meshtastic debug BLE",
"type": "python",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
"args": ["--debug", "--ble", "--info"]
},
{ {
"name": "meshtastic debug set region", "name": "meshtastic debug set region",
"type": "python", "type": "python",

View File

@@ -1042,16 +1042,11 @@ def common():
subscribe() subscribe()
if args.ble_scan: if args.ble_scan:
logging.debug("BLE scan starting") logging.debug("BLE scan starting")
client = BLEInterface(None, debugOut=logfile, noProto=args.noproto) for x in BLEInterface.scan():
try: print(f"Found: name='{x.name}' address='{x.address}'")
for x in client.scan():
print(f"Found: name='{x[1].local_name}' address='{x[0].address}'")
finally:
client.close()
meshtastic.util.our_exit("BLE scan finished", 0) meshtastic.util.our_exit("BLE scan finished", 0)
return
elif args.ble: 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: elif args.host:
try: try:
client = meshtastic.tcp_interface.TCPInterface( client = meshtastic.tcp_interface.TCPInterface(
@@ -1119,8 +1114,10 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse
group.add_argument( group.add_argument(
"--ble", "--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, default=None,
const="any"
) )
return parser return parser

View File

@@ -1,141 +1,173 @@
"""Bluetooth interface """Bluetooth interface
""" """
import logging
import time
import struct
import asyncio import asyncio
from threading import Thread, Event import atexit
import logging
import struct
import time
from threading import Thread
from typing import Optional 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.mesh_interface import MeshInterface
from meshtastic.util import our_exit
SERVICE_UUID = "6ba1b218-15a8-461f-9fa8-5dcae273eafd" SERVICE_UUID = "6ba1b218-15a8-461f-9fa8-5dcae273eafd"
TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7" TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7"
FROMRADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002" FROMRADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002"
FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453" FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453"
LOGRADIO_UUID = "6c6fd238-78fa-436b-aacf-15c5be1ef2e2"
class BLEInterface(MeshInterface): class BLEInterface(MeshInterface):
"""MeshInterface using BLE to connect to devices""" """MeshInterface using BLE to connect to devices."""
class BLEError(Exception): class BLEError(Exception):
"""An exception class for BLE errors""" """An exception class for BLE errors."""
def __init__(self, message):
self.message = message
super().__init__(self.message)
class BLEState(): # pylint: disable=C0115 def __init__(
THREADS = False self,
BLE = False address: Optional[str],
MESH = False 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() MeshInterface.__init__(
self, debugOut=debugOut, noProto=noProto, noNodes=noNodes
if not address: )
return
self.should_read = False self.should_read = False
logging.debug("Threads starting") logging.debug("Threads starting")
self._receiveThread = Thread(target = self._receiveFromRadioImpl) self._want_receive = True
self._receiveThread_started = Event() self._receiveThread: Optional[Thread] = Thread(
self._receiveThread_stopped = Event() target=self._receiveFromRadioImpl, name="BLEReceive", daemon=True
)
self._receiveThread.start() self._receiveThread.start()
self._receiveThread_started.wait(1)
self.state.THREADS = True
logging.debug("Threads running") logging.debug("Threads running")
try: 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.client: Optional[BLEClient] = self.connect(address)
self.state.BLE = True
logging.debug("BLE connected") logging.debug("BLE connected")
except BLEInterface.BLEError as e: except BLEInterface.BLEError as e:
self.close() self.close()
our_exit(e.message, 1) raise e
return
logging.debug("Mesh init starting") self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler)
MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto, noNodes = noNodes)
logging.debug("Mesh configure starting")
self._startConfig() self._startConfig()
if not self.noProto: if not self.noProto:
self._waitConnected(timeout = 60.0) self._waitConnected(timeout=60.0)
self.waitForConfig() self.waitForConfig()
self.state.MESH = True
logging.debug("Mesh init finished")
logging.debug("Register FROMNUM notify callback") logging.debug("Register FROMNUM notify callback")
self.client.start_notify(FROMNUM_UUID, self.from_num_handler) 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 def from_num_handler(self, _, b): # pylint: disable=C0116
from_num = struct.unpack('<I', bytes(b))[0] """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}") logging.debug(f"FROMNUM notify: {from_num}")
self.should_read = True 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): @staticmethod
"Scan for available BLE devices" def scan() -> list[BLEDevice]:
"""Scan for available BLE devices."""
with BLEClient() as client: with BLEClient() as client:
return [ logging.info("Scanning for BLE devices (takes 10 seconds)...")
(x[0], x[1]) for x in (client.discover( response = client.discover(
return_adv = True, timeout=10, return_adv=True, service_uuids=[SERVICE_UUID]
service_uuids = [ SERVICE_UUID ] )
)).values()
]
devices = response.values()
def find_device(self, address): # bleak sometimes returns devices we didn't ask for, so filter the response
"Find a device by address" # to only return true meshtastic devices
meshtastic_devices = self.scan() # 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)) def find_device(self, address: Optional[str]) -> BLEDevice:
# If nothing is found try on the address """Find a device by address."""
if len(addressed_devices) == 0:
addressed_devices = list(filter( addressed_devices = BLEInterface.scan()
lambda x: BLEInterface._sanitize_address(address) == BLEInterface._sanitize_address(x[0].address),
meshtastic_devices)) if address:
addressed_devices = list(
filter(
lambda x: address in (x.name, x.address),
addressed_devices,
)
)
if len(addressed_devices) == 0: 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: if len(addressed_devices) > 1:
raise BLEInterface.BLEError(f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found.") raise BLEInterface.BLEError(
return addressed_devices[0][0] f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found."
)
return addressed_devices[0]
def _sanitize_address(address): # pylint: disable=E0213 def _sanitize_address(address): # pylint: disable=E0213
"Standardize BLE address by removing extraneous characters and lowercasing" "Standardize BLE address by removing extraneous characters and lowercasing."
return address \ return address.replace("-", "").replace("_", "").replace(":", "").lower()
.replace("-", "") \
.replace("_", "") \
.replace(":", "") \
.lower()
def connect(self, address): def connect(self, address: Optional[str] = None) -> "BLEClient":
"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) device = self.find_device(address)
client = BLEClient(device.address) client = BLEClient(device.address, disconnected_callback=lambda _: self.close)
client.connect() client.connect()
try: client.discover()
client.pair()
except NotImplementedError:
# Some bluetooth backends do not require explicit pairing.
# See Bleak docs for details on this.
pass
return client return client
def _receiveFromRadioImpl(self): def _receiveFromRadioImpl(self):
self._receiveThread_started.set() while self._want_receive:
while self._receiveThread_started.is_set():
if self.should_read: if self.should_read:
self.should_read = False self.should_read = False
retries = 0 retries = 0
while True: while self._want_receive:
b = bytes(self.client.read_gatt_char(FROMRADIO_UUID)) 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 not b:
if retries < 5: if retries < 5:
time.sleep(0.1) time.sleep(0.1)
@@ -145,40 +177,52 @@ class BLEInterface(MeshInterface):
logging.debug(f"FROMRADIO read: {b.hex()}") logging.debug(f"FROMRADIO read: {b.hex()}")
self._handleFromRadio(b) self._handleFromRadio(b)
else: else:
time.sleep(0.1) time.sleep(0.01)
self._receiveThread_stopped.set()
def _sendToRadioImpl(self, toRadio): def _sendToRadioImpl(self, toRadio):
b = toRadio.SerializeToString() b = toRadio.SerializeToString()
if b: if b:
logging.debug(f"TORADIO write: {b.hex()}") 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 # Allow to propagate and then make sure we read
time.sleep(0.1) time.sleep(0.01)
self.should_read = True self.should_read = True
def close(self): def close(self):
if self.state.MESH: atexit.unregister(self._exit_handler)
try:
MeshInterface.close(self) MeshInterface.close(self)
except Exception as e:
logging.error(f"Error closing mesh interface: {e}")
if self.state.THREADS: if self._want_receive:
self._receiveThread_started.clear() self.want_receive = False # Tell the thread we want it to stop
self._receiveThread_stopped.wait(5) self._receiveThread.join()
self._receiveThread = None
if self.state.BLE: if self.client:
self.client.disconnect() self.client.disconnect()
self.client.close() self.client.close()
self.client = None
class BLEClient(): class BLEClient:
"""Client for managing connection to a BLE device""" """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_started = Event() self._eventLoop = asyncio.new_event_loop()
self._eventThread_stopped = Event() self._eventThread = Thread(
target=self._run_event_loop, name="BLEClient", daemon=True
)
self._eventThread.start() self._eventThread.start()
self._eventThread_started.wait(1)
if not address: if not address:
logging.debug("No address provided - only discover method will work.") logging.debug("No address provided - only discover method will work.")
@@ -186,31 +230,30 @@ class BLEClient():
self.bleak_client = BleakClient(address, **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)) 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)) 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)) 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)) 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)) 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)) 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)) 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.async_run(self._stop_event_loop())
self._eventThread_stopped.wait(5) self._eventThread.join()
def __enter__(self): def __enter__(self):
return self return self
@@ -218,21 +261,17 @@ class BLEClient():
def __exit__(self, _type, _value, _traceback): def __exit__(self, _type, _value, _traceback):
self.close() 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) 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) return asyncio.run_coroutine_threadsafe(coro, self._eventLoop)
def _run_event_loop(self): 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: try:
self._eventLoop.run_forever() self._eventLoop.run_forever()
finally: finally:
self._eventLoop.close() self._eventLoop.close()
self._eventThread_stopped.set()
async def _stop_event_loop(self): async def _stop_event_loop(self):
self._eventLoop.stop() self._eventLoop.stop()

91
poetry.lock generated
View File

@@ -307,45 +307,39 @@ toml = ["tomli"]
[[package]] [[package]]
name = "dbus-fast" name = "dbus-fast"
version = "2.21.3" version = "2.22.1"
description = "A faster version of dbus-next" description = "A faster version of dbus-next"
optional = false optional = false
python-versions = "<4.0,>=3.7" python-versions = "<4.0,>=3.8"
files = [ 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.22.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f19c08fc0ab5f0e209e008f4646bb0624eacb96fb54367ea36e450aacfe289f"},
{file = "dbus_fast-2.21.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b379ed7ef0d174480e41a5f1dde3392d974e618bb91e5fbfa06396c24d3c80fc"}, {file = "dbus_fast-2.22.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:714c5bca7d1ae20557a5857fdb3022ff0a3f5ef2e14379eae0403940882a4d72"},
{file = "dbus_fast-2.21.3-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:990d60e9796fa142e16af331e53d91aaa94dfbcf37b474c1d6caf61310fcc5ee"}, {file = "dbus_fast-2.22.1-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:ac004b0f6a7f7b58ae7488f12463df68199546a8d71085379b5eed17ae012905"},
{file = "dbus_fast-2.21.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9d0bbfa7cdb440f13d58e13625344b918b70ff0ccddc20ddd9c0ebf3e5a765dd"}, {file = "dbus_fast-2.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a54533ee4b30a2c062078c02d10c5a258fc10eac51a0b85cfdd7f690f1d6285f"},
{file = "dbus_fast-2.21.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0d4f459ba4fa394e3ba22a7421055878953aa92efd01e3a1d5216519c6b1586c"}, {file = "dbus_fast-2.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cadf90548aaf336820e0b7037b0f0f46b9836ac0f2c6af0f494b00fe6bc23929"},
{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.22.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e213b0252f97d6a9ceb97cd2d84ddac0d998b8dd15bdca051def181a666b6a"},
{file = "dbus_fast-2.21.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d53f5b24c732af5ae9c7e88fc9ba687ce2a785c63dcea3b9c984619f1bdcf71a"}, {file = "dbus_fast-2.22.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6497859da721041dbf7615aab1cae666e5c0a169fca80032ab2fd8b03f7730f5"},
{file = "dbus_fast-2.21.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b5ef802b2b7e5dbebdfa338a0278e5212a6073c26764c75f3e373e2a9b01797c"}, {file = "dbus_fast-2.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a3ba17d91a32b53f8e16b40e7f948260847f3e8fbbbf83872dafe44b38a1ae42"},
{file = "dbus_fast-2.21.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:251d46d0d7cbed0d9b9eac2f91f6669893db9b87e19defb99f9a85579c2f786a"}, {file = "dbus_fast-2.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2b7f32e765051817d58e3242697b47cfe5def086181ad1087c9bc70e2db48004"},
{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.22.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beebe8cbd0cd90d24b757c4aad617fcfa77f2e654287bc80b11c0e4964891c22"},
{file = "dbus_fast-2.21.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29ca29609a31f816c315844ed41b81247e3114261d26e5ee1dcc85bf5c046a36"}, {file = "dbus_fast-2.22.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b72ebd07ac873906f1001cb6eb75e864e30cb6cdcce17afe79939987b0a28b5"},
{file = "dbus_fast-2.21.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b520792549e8b2b1e4c8777492783ba81065bd02e16e4390e2b299bf33f1feea"}, {file = "dbus_fast-2.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c73e3b59de2b6e7447b1c3d26ccd307838d05c6a85bcc9eac7bc990bb843cc92"},
{file = "dbus_fast-2.21.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f44d2ea35daefac7ad1ede65695fde18526fb38f9ec0aadf108f629bb6c87293"}, {file = "dbus_fast-2.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dcb333f56ebb0de5cf3aa8affb9c492bd821e252d704dcce444a379c0513c6be"},
{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.22.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2980b92493698f80910b3521d685ce230f94d93deac0bcf33f2082ce551b8ac5"},
{file = "dbus_fast-2.21.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8fad077a989b002602aa192cfa95b89b3e40c5fa6da7740f42a87488bdbed6f"}, {file = "dbus_fast-2.22.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d88f7f1d4124feb4418f5d9efe359661e2f38e89f6c31539d998e3769f7f7b3"},
{file = "dbus_fast-2.21.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87e8db4ea5023024a638826321039497dcbc7e70583bd33743eac2d8e69ca4fb"}, {file = "dbus_fast-2.22.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:bf198217013b068fe610b1d5ce7ce53e15b993625331d2c83f53be5744c0be40"},
{file = "dbus_fast-2.21.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0c342d8b33079c550ea575344d53807f6ae6464b1a5f6f9e0523fae979198872"}, {file = "dbus_fast-2.22.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f90017ba2c95dba4c1e417850d3c735d5eb464cbe0ebfb5d49cc0e95e7d916d2"},
{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.22.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e6d2cd04da08a9d21be68faa4d23123a2f4cb5cef3406cc1a2ef900507b1c0"},
{file = "dbus_fast-2.21.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d76b512cc8db4ebdfb7879d7cae42ee0adc362671bc0a4f55df5f4ebe547602d"}, {file = "dbus_fast-2.22.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2735f9cc9e6692b0bb114c48580709af824a16ea791922f628c265aa05f183a"},
{file = "dbus_fast-2.21.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9fdbe2b22668f4021e909e65fa6a25bca1ab08294a35c600af95ba06a2f2d101"}, {file = "dbus_fast-2.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b709a9eaaae542d0d883c5a2f147c0cbe7ef29262ec0bf90f5a5945e76786c39"},
{file = "dbus_fast-2.21.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:638c4b64159f8a3567e38705246bd1a2625d8c9adbb7ffa23a6a2ec2dfd40db0"}, {file = "dbus_fast-2.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7e7924d5042de42dcdc6be942d2f6cf1f187cf7a4ae2902b68431ea856ef654c"},
{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.22.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e15b15c0bdef24f86a5940539ba68d0920d58b96cca8543fbda9189cb144fb13"},
{file = "dbus_fast-2.21.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83e0a28e04218493ebd66c1f2a5290203ffff924ec01b37c5128ba1fa9731255"}, {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.21.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:846733011edad8c0125f2b1148783c8d2ae162419707bb7e2bf08a26040939d8"}, {file = "dbus_fast-2.22.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e56f6f0976aa953a2a5c71817e9ceecace6dd6a2a23dc64622025701005bf15"},
{file = "dbus_fast-2.21.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:07213240465c3c7306705ad512c983ada45ef222d2eecf3d7ab19f397b02de0d"}, {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.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.22.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0934118cc2e4f777d785df923b139f253ba3019469ec1f90eb8a5e4c12fff0ce"},
{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.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.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.22.1.tar.gz", hash = "sha256:aa75dfb5bc7ba42f53391ae503ca5a21bd133e74ebb09965013ba23bdffc9a0e"},
{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"},
] ]
[[package]] [[package]]
@@ -390,13 +384,13 @@ test = ["pytest (>=6)"]
[[package]] [[package]]
name = "hypothesis" name = "hypothesis"
version = "6.104.1" version = "6.104.2"
description = "A library for property-based testing" description = "A library for property-based testing"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "hypothesis-6.104.1-py3-none-any.whl", hash = "sha256:a0a898fa78ecaefe76ad248901dc274e598f29198c6015b3053f7f7827670e0e"}, {file = "hypothesis-6.104.2-py3-none-any.whl", hash = "sha256:8b52b7e2462e552c75b819495d5cb6251a2b840accc79cf2ce52588004c915d9"},
{file = "hypothesis-6.104.1.tar.gz", hash = "sha256:4033898019a6149823d2feeb8d214921b4ac2d342a05d6b02e40a3ca4be07eea"}, {file = "hypothesis-6.104.2.tar.gz", hash = "sha256:6f2a1489bc8fe1c87ffd202707319b66ec46b2bc11faf6e0161e957b8b9b1eab"},
] ]
[package.dependencies] [package.dependencies]
@@ -762,6 +756,17 @@ files = [
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"] 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]] [[package]]
name = "protobuf" name = "protobuf"
version = "5.27.2" version = "5.27.2"
@@ -857,13 +862,13 @@ setuptools = ">=42.0.0"
[[package]] [[package]]
name = "pylint" name = "pylint"
version = "3.2.3" version = "3.2.5"
description = "python code static checker" description = "python code static checker"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "pylint-3.2.3-py3-none-any.whl", hash = "sha256:b3d7d2708a3e04b4679e02d99e72329a8b7ee8afb8d04110682278781f889fa8"}, {file = "pylint-3.2.5-py3-none-any.whl", hash = "sha256:32cd6c042b5004b8e857d727708720c54a676d1e22917cf1a2df9b4d4868abd6"},
{file = "pylint-3.2.3.tar.gz", hash = "sha256:02f6c562b215582386068d52a30f520d84fdbcf2a95fc7e855b816060d048b60"}, {file = "pylint-3.2.5.tar.gz", hash = "sha256:e9b7171e242dcc6ebd0aaa7540481d1a72860748a0a7816b8fe6cf6c80a6fe7e"},
] ]
[package.dependencies] [package.dependencies]
@@ -1551,4 +1556,4 @@ tunnel = []
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8,<3.13" 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" pypubsub = "^4.0.3"
bleak = "^0.21.1" bleak = "^0.21.1"
packaging = "^24.0" packaging = "^24.0"
print-color = "^0.4.6"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
hypothesis = "^6.103.2" hypothesis = "^6.103.2"