forked from auracaster/bumble_mirror
Add bumble-console --device-config support for gatt services
This PR adds support for bumble-console to be preloaded with gatt services via `--device-config`. This PR also adds some type annotations
This commit is contained in:
@@ -58,6 +58,12 @@ def padded_bytes(buffer, size):
|
||||
return buffer + bytes(padding_size)
|
||||
|
||||
|
||||
def get_dict_key_by_value(dictionary, value):
|
||||
for key, val in dictionary.items():
|
||||
if val == value:
|
||||
return key
|
||||
return None
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Exceptions
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -135,7 +141,7 @@ class UUID:
|
||||
else:
|
||||
uuid_str = uuid_str_or_int
|
||||
if len(uuid_str) != 32 and len(uuid_str) != 8 and len(uuid_str) != 4:
|
||||
raise ValueError('invalid UUID format')
|
||||
raise ValueError(f"invalid UUID format: {uuid_str}")
|
||||
self.uuid_bytes = bytes(reversed(bytes.fromhex(uuid_str)))
|
||||
self.name = name
|
||||
|
||||
|
||||
@@ -540,6 +540,7 @@ class DeviceConfiguration:
|
||||
)
|
||||
self.irk = bytes(16) # This really must be changed for any level of security
|
||||
self.keystore = None
|
||||
self.gatt_services = []
|
||||
|
||||
def load_from_dict(self, config):
|
||||
# Load simple properties
|
||||
@@ -556,6 +557,7 @@ class DeviceConfiguration:
|
||||
self.classic_accept_any = config.get('classic_accept_any', self.classic_accept_any)
|
||||
self.connectable = config.get('connectable', self.connectable)
|
||||
self.discoverable = config.get('discoverable', self.discoverable)
|
||||
self.gatt_services = config.get('gatt_services', self.gatt_services)
|
||||
|
||||
# Load or synthesize an IRK
|
||||
irk = config.get('irk')
|
||||
@@ -589,7 +591,7 @@ def with_connection_from_handle(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, connection_handle, *args, **kwargs):
|
||||
if (connection := self.lookup_connection(connection_handle)) is None:
|
||||
raise ValueError('no connection for handle')
|
||||
raise ValueError(f"no connection for handle: 0x{connection_handle:04x}")
|
||||
return function(self, connection, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@@ -726,6 +728,26 @@ class Device(CompositeEventEmitter):
|
||||
self.connectable = config.connectable
|
||||
self.classic_accept_any = config.classic_accept_any
|
||||
|
||||
for service in config.gatt_services:
|
||||
characteristics = []
|
||||
for characteristic in service.get("characteristics", []):
|
||||
descriptors = []
|
||||
for descriptor in characteristic.get("descriptors", []):
|
||||
new_descriptor = Descriptor(
|
||||
descriptor_type=descriptor["descriptor_type"],
|
||||
permissions=descriptor["permission"],
|
||||
)
|
||||
descriptors.append(new_descriptor)
|
||||
new_characteristic = Characteristic(
|
||||
uuid=characteristic["uuid"],
|
||||
properties=characteristic["properties"],
|
||||
permissions=int(characteristic["permissions"], 0),
|
||||
descriptors=descriptors,
|
||||
)
|
||||
characteristics.append(new_characteristic)
|
||||
new_service = Service(uuid=service["uuid"], characteristics=characteristics)
|
||||
self.gatt_server.add_service(new_service)
|
||||
|
||||
# If a name is passed, override the name from the config
|
||||
if name:
|
||||
self.name = name
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import enum
|
||||
import types
|
||||
@@ -187,7 +188,7 @@ class Service(Attribute):
|
||||
See Vol 3, Part G - 3.1 SERVICE DEFINITION
|
||||
'''
|
||||
|
||||
def __init__(self, uuid, characteristics, primary=True):
|
||||
def __init__(self, uuid, characteristics: list[Characteristic], primary=True):
|
||||
# Convert the uuid to a UUID object if it isn't already
|
||||
if type(uuid) is str:
|
||||
uuid = UUID(uuid)
|
||||
@@ -256,10 +257,21 @@ class Characteristic(Attribute):
|
||||
if properties & p
|
||||
])
|
||||
|
||||
def __init__(self, uuid, properties, permissions, value = b'', descriptors = []):
|
||||
@staticmethod
|
||||
def string_to_properties(properties_str: str):
|
||||
return functools.reduce(
|
||||
lambda x, y: x | get_dict_key_by_value(Characteristic.PROPERTY_NAMES, y),
|
||||
properties_str.split(","),
|
||||
0,
|
||||
)
|
||||
|
||||
def __init__(self, uuid, properties, permissions, value = b'', descriptors: list[Descriptor] = []):
|
||||
super().__init__(uuid, permissions, value)
|
||||
self.uuid = self.type
|
||||
self.properties = properties
|
||||
if type(properties) is str:
|
||||
self.properties = Characteristic.string_to_properties(properties)
|
||||
else:
|
||||
self.properties = properties
|
||||
self.descriptors = descriptors
|
||||
|
||||
def get_descriptor(self, descriptor_type):
|
||||
|
||||
@@ -60,6 +60,9 @@ class Server(EventEmitter):
|
||||
self.indication_semaphores = defaultdict(lambda: asyncio.Semaphore(1))
|
||||
self.pending_confirmations = defaultdict(lambda: None)
|
||||
|
||||
def __str__(self):
|
||||
return "\n".join(map(str, self.attributes))
|
||||
|
||||
def send_gatt_pdu(self, connection_handle, pdu):
|
||||
self.device.send_l2cap_pdu(connection_handle, ATT_CID, pdu)
|
||||
|
||||
@@ -87,7 +90,7 @@ class Server(EventEmitter):
|
||||
# Add this attribute to the list
|
||||
self.attributes.append(attribute)
|
||||
|
||||
def add_service(self, service):
|
||||
def add_service(self, service: Service):
|
||||
# Add the service attribute to the DB
|
||||
self.add_attribute(service)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user