diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..29b763f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include bless py.typed diff --git a/bless/__init__.py b/bless/__init__.py index 65c6931..89cde08 100644 --- a/bless/__init__.py +++ b/bless/__init__.py @@ -13,7 +13,7 @@ # Service from bless.backends.corebluetooth.service import ( # noqa: F401 - BlessGATTServiceCoreBluetooth as BlessGATTService + BlessGATTServiceCoreBluetooth as BlessGATTService, ) # Characteristic Classes @@ -21,6 +21,11 @@ BlessGATTCharacteristicCoreBluetooth as BlessGATTCharacteristic, ) + # Descriptor Classes + from bless.backends.corebluetooth.descriptor import ( # noqa: F401 + BlessGATTDescriptorCoreBluetooth as BlessGATTDescriptor, + ) + elif sys.platform == "linux": # Server @@ -30,7 +35,7 @@ # Service from bless.backends.bluezdbus.service import ( # noqa: F401 - BlessGATTServiceBlueZDBus as BlessGATTService + BlessGATTServiceBlueZDBus as BlessGATTService, ) # Characteristic Classes @@ -52,7 +57,7 @@ # Service from bless.backends.winrt.service import ( # noqa: F401 - BlessGATTServiceWinRT as BlessGATTService + BlessGATTServiceWinRT as BlessGATTService, ) # Characteristic Classes @@ -60,21 +65,21 @@ BlessGATTCharacteristicWinRT as BlessGATTCharacteristic, ) -# type: ignore from bless.backends.attribute import ( # noqa: E402 F401 GATTAttributePermissions, ) -# type: ignore from bless.backends.characteristic import ( # noqa: E402 F401 GATTCharacteristicProperties, ) -# type: ignore from bless.backends.descriptor import ( # noqa: E402 F401 GATTDescriptorProperties, ) +from bless.backends.request import BlessGATTRequest # noqa: F401 +from bless.backends.session import BlessGATTSession # noqa: F401 + def check_test() -> bool: """ diff --git a/bless/backends/bluezdbus/characteristic.py b/bless/backends/bluezdbus/characteristic.py index df1f084..67d60a3 100644 --- a/bless/backends/bluezdbus/characteristic.py +++ b/bless/backends/bluezdbus/characteristic.py @@ -1,6 +1,6 @@ from uuid import UUID -from typing import Union, Optional, List, Dict, cast, TYPE_CHECKING, Literal +from typing import Dict, List, Literal, Optional, Set, TYPE_CHECKING, Union, cast from bleak.backends.characteristic import ( # type: ignore BleakGATTCharacteristic, @@ -11,6 +11,9 @@ from bless.backends.characteristic import ( BlessGATTCharacteristic, GATTCharacteristicProperties, + GATTReadCallback, + GATTWriteCallback, + GATTSubscribeCallback, ) from bless.backends.bluezdbus.dbus.characteristic import ( Flags, @@ -42,6 +45,7 @@ class BlessGATTCharacteristicBlueZDBus( """ BlueZ implementation of the BlessGATTCharacteristic """ + gatt: "BlueZGattCharacteristic" def __init__( @@ -50,6 +54,10 @@ def __init__( properties: GATTCharacteristicProperties, permissions: GATTAttributePermissions, value: Optional[bytearray], + on_read: Optional[GATTReadCallback] = None, + on_write: Optional[GATTWriteCallback] = None, + on_subscribe: Optional[GATTSubscribeCallback] = None, + on_unsubscribe: Optional[GATTSubscribeCallback] = None, ): """ Instantiates a new GATT Characteristic but is not yet assigned to any @@ -66,9 +74,31 @@ def __init__( Permissions that define the protection levels of the properties value : Optional[bytearray] The binary value of the characteristic + on_read : Optional[GATTReadCallback] + If defined, reads destined for this characteristic will be passed + to this function + on_write : Optional[GATTWriteCallback] + If defined, writes destined for this characteristic will be passed + to this function + on_subscribe : Optional[GATTSubscribeCallback] + If defined, subscriptions destined for this characteristic will be + passed to this function + on_unsubscribe : Optional[GATTSubscribeCallback] + If defined, unsubscriptions destined for this characteristic will + be passed to this function """ value = value if value is not None else bytearray(b"") - BlessGATTCharacteristic.__init__(self, uuid, properties, permissions, value) + BlessGATTCharacteristic.__init__( + self, + uuid, + properties, + permissions, + value, + on_read, + on_write, + on_subscribe, + on_unsubscribe, + ) self._value = value self._descriptors: Dict[int, BleakGATTDescriptor] = {} @@ -142,6 +172,10 @@ def description(self) -> str: """Description of this characteristic""" return f"Characteristic {self._uuid}" + @property + def subscribed_centrals(self) -> Set[str]: + return set(list(self.obj._subscribed_centrals.keys())) + def transform_flags_with_permissions( flag: Flags, permissions: GATTAttributePermissions diff --git a/bless/backends/bluezdbus/dbus/application.py b/bless/backends/bluezdbus/dbus/application.py index 7125c4a..870ed81 100644 --- a/bless/backends/bluezdbus/dbus/application.py +++ b/bless/backends/bluezdbus/dbus/application.py @@ -23,11 +23,13 @@ ) from bless.backends.bluezdbus.dbus.descriptor import BlueZGattDescriptor # type: ignore +from .session import NotifySession + LOGGER = logging.getLogger(__name__) ReadCallback = Callable[[BlueZGattCharacteristic, Dict[str, Any]], bytes] WriteCallback = Callable[[BlueZGattCharacteristic, bytes, Dict[str, Any]], None] -SubscribeCallback = Callable[[BlueZGattCharacteristic, Dict[str, Any]], None] +SubscribeCallback = Callable[[BlueZGattCharacteristic, NotifySession], None] class BlueZGattApplication(ServiceInterface): diff --git a/bless/backends/bluezdbus/dbus/characteristic.py b/bless/backends/bluezdbus/dbus/characteristic.py index 3b441e8..df67418 100644 --- a/bless/backends/bluezdbus/dbus/characteristic.py +++ b/bless/backends/bluezdbus/dbus/characteristic.py @@ -5,13 +5,15 @@ import bleak.backends.bluezdbus.defs as defs # type: ignore from dbus_next import DBusError # type: ignore +from dbus_next.aio import ProxyInterface # type: ignore from dbus_next.constants import PropertyAccess # type: ignore from dbus_next.service import ServiceInterface, method, dbus_property # type: ignore from dbus_next.signature import Variant # type: ignore from enum import Enum -from typing import List, TYPE_CHECKING, Any, Dict +from typing import List, TYPE_CHECKING, Any, Dict, cast from .descriptor import BlueZGattDescriptor, DescriptorFlags # type: ignore +from .device import Device1 from .session import NotifySession # type: ignore if TYPE_CHECKING: @@ -111,7 +113,7 @@ def NotifyAcquired(self) -> "b": # type: ignore # noqa: F821 return len(self._subscribed_centrals) > 0 @method() # noqa: F722 - def ReadValue(self, options: "a{sv}") -> "ay": # type: ignore # noqa: F722 F821 N802 E501 + async def ReadValue(self, options: "a{sv}") -> "ay": # type: ignore # noqa: F722 F821 N802 E501 """ Read the value of the characteristic. This is to be fully implemented at the application level @@ -126,6 +128,12 @@ def ReadValue(self, options: "a{sv}") -> "ay": # type: ignore # noqa: F722 F821 bytes The bytes that is the value of the characteristic """ + device_path: str = options["device"] + device_interface: ProxyInterface = Device1.get_device( + self._service.bus, device_path + ) + device: Device1 = cast(Device1, device_interface) + options["central_id"] = await device.get_address() f = self._service.app.Read if f is None: raise NotImplementedError() @@ -144,6 +152,12 @@ def WriteValue(self, value: "ay", options: "a{sv}"): # type: ignore # noqa options : Dict Some options for you to select from """ + device_path: str = options["device"] + device_interface: ProxyInterface = Device1.get_device( + self._service.bus, device_path + ) + device: Device1 = cast(Device1, device_interface) + options["central_id"] = device.get_address() f = self._service.app.Write if f is None: raise NotImplementedError() @@ -176,14 +190,14 @@ async def AcquireNotify(self, options: "a{sv}") -> "hq": # type: ignore # noqa f = self._service.app.StartNotify if f is None: raise NotImplementedError() - f(self, {"device": address}) + + f(self, session) self._subscribed_centrals[address] = session async def close_rx(): logger.debug("Closing RX") await asyncio.sleep(2) os.close(rx) - # asyncio.get_running_loop().call_soon_threadsafe(os.close, rx) asyncio.create_task(close_rx()) return [rx, mtu] @@ -194,7 +208,7 @@ async def ReleaseNotify(self, session: NotifySession): f = self._service.app.StopNotify if f is None: raise NotImplementedError() - f(self, {"device": address}) + f(self, session) del self._subscribed_centrals[address] @method() @@ -214,7 +228,7 @@ def StartNotify(self): # noqa: N802 f = self._service.app.StartNotify if f is None: raise NotImplementedError() - f(self, {}) + f(self, None) # type: ignore self._notifying_calls += 1 @method() @@ -229,7 +243,7 @@ async def StopNotify(self): # noqa: N802 f = self._service.app.StopNotify if f is None: raise NotImplementedError() - f(self, {}) + f(self, {}) # type: ignore self._notifying_calls -= 1 def update_value(self) -> None: diff --git a/bless/backends/bluezdbus/dbus/device.py b/bless/backends/bluezdbus/dbus/device.py index 857b9cf..8421401 100644 --- a/bless/backends/bluezdbus/dbus/device.py +++ b/bless/backends/bluezdbus/dbus/device.py @@ -1,5 +1,7 @@ from bleak.backends.bluezdbus.defs import DEVICE_INTERFACE +from dbus_next.aio import MessageBus, ProxyInterface, ProxyObject # type: ignore from dbus_next.constants import PropertyAccess # type: ignore +from dbus_next.introspection import Interface, Node # type: ignore from dbus_next.service import ServiceInterface, method, dbus_property # type: ignore @@ -145,3 +147,17 @@ def ServicesResolved(self) -> "a{say}": # type: ignore # noqa: F722 @dbus_property(access=PropertyAccess.READ) def AdvertisingFlags(self) -> "ay": # type: ignore # noqa: F722 F821 raise NotImplementedError + + @classmethod + def get_device(cls, bus: MessageBus, path: str) -> ProxyInterface: + # Query the device object + node: Node = Node.default(name=path) + device_iface: Interface = Device1().introspect() + node.interfaces.append(device_iface) + + object: ProxyObject = bus.get_proxy_object("org.bluez", path, node) + return object.get_interface(DEVICE_INTERFACE) + + # For typing + async def get_address(self) -> str: + raise NotImplementedError diff --git a/bless/backends/bluezdbus/dbus/session.py b/bless/backends/bluezdbus/dbus/session.py index 02b109e..31ae1e4 100644 --- a/bless/backends/bluezdbus/dbus/session.py +++ b/bless/backends/bluezdbus/dbus/session.py @@ -3,11 +3,8 @@ import logging import os -import bleak.backends.bluezdbus.defs as defs # type: ignore - from asyncio import AbstractEventLoop, Event -from dbus_next.aio import ProxyInterface, ProxyObject, MessageBus # type: ignore -from dbus_next.introspection import Interface, Node # type: ignore +from dbus_next.aio import ProxyInterface, MessageBus # type: ignore from select import poll, POLLHUP, POLLERR, POLLNVAL from socket import socket, socketpair, AF_UNIX, SOCK_SEQPACKET from typing import Callable, Coroutine, Optional, Union @@ -39,6 +36,14 @@ def __init__( self._tx: Optional[socket] = None self._device: Optional[ProxyInterface] = None + self._address: Optional[str] = None + + @property + def address(self) -> str: + if self._address is None: + raise Exception("NotifySession not started. Address property not obtained") + + return self._address def get_device(self) -> ProxyInterface: if self._device is None: @@ -63,16 +68,8 @@ async def watch_fd(self) -> None: self.close() async def start(self) -> int: - - # Query the device object - node: Node = Node.default(name=self.device_path) - device_iface: Interface = Device1().introspect() - node.interfaces.append(device_iface) - - object: ProxyObject = self.bus.get_proxy_object( - "org.bluez", self.device_path, node - ) - self._device = object.get_interface(defs.DEVICE_INTERFACE) + self._device = Device1.get_device(self.bus, self.device_path) + self._address = await self.get_device_address() # create a bluetooth socket pair self._tx, rx = socketpair(AF_UNIX, SOCK_SEQPACKET) diff --git a/bless/backends/bluezdbus/request.py b/bless/backends/bluezdbus/request.py new file mode 100644 index 0000000..9aa4b2a --- /dev/null +++ b/bless/backends/bluezdbus/request.py @@ -0,0 +1,31 @@ +from typing import Dict, Optional, cast +from ..request import BlessGATTRequest + + +class BlessGATTRequestBlueZ(BlessGATTRequest): + + @property + def options(self) -> Dict: + return cast(Dict, self.obj) + + @property + def central_id(self) -> str: + """ + Note, that the device returned within the options on + BlueZ is a DBus path to the device object. It is the + receving calls responsibility to use the DBus to resolve + the device address and populate this field + """ + return self.obj["central_id"] + + @property + def mtu(self) -> int: + return self.obj["mtu"] + + @property + def offset(self) -> int: + return self.obj["offset"] + + @property + def response_requested(self) -> Optional[bool]: + return None if "type" not in self.obj else (self.obj["type"] == "request") diff --git a/bless/backends/bluezdbus/server.py b/bless/backends/bluezdbus/server.py index fd02312..4741f05 100644 --- a/bless/backends/bluezdbus/server.py +++ b/bless/backends/bluezdbus/server.py @@ -16,10 +16,11 @@ from bless.backends.bluezdbus.dbus.application import ( # type: ignore BlueZGattApplication, ) -from bless.backends.bluezdbus.dbus.utils import get_adapter # type: ignore from bless.backends.bluezdbus.dbus.characteristic import ( # type: ignore BlueZGattCharacteristic, ) +from bless.backends.bluezdbus.dbus.session import NotifySession +from bless.backends.bluezdbus.dbus.utils import get_adapter # type: ignore from bless.backends.bluezdbus.service import BlessGATTServiceBlueZDBus @@ -29,6 +30,9 @@ from bless.backends.characteristic import ( # type: ignore GATTCharacteristicProperties, + GATTReadCallback, + GATTWriteCallback, + GATTSubscribeCallback, ) from bless.backends.descriptor import ( # type: ignore @@ -37,6 +41,9 @@ from bleak.uuids import normalize_uuid_str +from .request import BlessGATTRequestBlueZ +from .session import BlessGATTSessionBlueZ + class BlessServerBlueZDBus(BaseBlessServer): """ @@ -68,10 +75,10 @@ async def setup(self: "BlessServerBlueZDBus"): self.name, "org.bluez", self.bus, - self.read, - self.write, - self.subscribe, - self.unsubscribe, + self.__on_read, + self.__on_write, + self.__on_subscribe, + self.__on_unsubscribe, ) potential_adapter: Optional[ProxyObject] = await get_adapter( @@ -133,17 +140,6 @@ async def stop(self) -> bool: return True - async def is_connected(self) -> bool: - """ - Determine whether there are any connected peripheral devices - - Returns - ------- - bool - Whether any peripheral devices are connected - """ - return await self.app.is_connected() - async def is_advertising(self) -> bool: """ Determine whether the server is advertising @@ -181,6 +177,10 @@ async def add_new_characteristic( properties: GATTCharacteristicProperties, value: Optional[bytearray], permissions: GATTAttributePermissions, + on_read: Optional[GATTReadCallback] = None, + on_write: Optional[GATTWriteCallback] = None, + on_subscribe: Optional[GATTSubscribeCallback] = None, + on_unsubscribe: Optional[GATTSubscribeCallback] = None, ): """ Add a new characteristic to be associated with the server @@ -200,13 +200,34 @@ async def add_new_characteristic( permissions : int GATT Characteristic flags that define the permissions for the characteristic + on_read : Optional[GATTReadCallback] + If defined, reads destined for this characteristic will be passed + to this function + on_write : Optional[GATTWriteCallback] + If defined, writes destined for this characteristic will be passed + to this function + on_subscribe : Optional[GATTSubscribeCallback] + If defined, subscriptions destined for this characteristic will be + passed to this function + on_unsubscribe : Optional[GATTSubscribeCallback] + If defined, unsubscriptions destined for this characteristic will + be passed to this function """ await self.setup_task service: BlessGATTServiceBlueZDBus = cast( BlessGATTServiceBlueZDBus, self.services[str(UUID(service_uuid))] ) characteristic: BlessGATTCharacteristicBlueZDBus = ( - BlessGATTCharacteristicBlueZDBus(char_uuid, properties, permissions, value) + BlessGATTCharacteristicBlueZDBus( + char_uuid, + properties, + permissions, + value, + on_read, + on_write, + on_subscribe, + on_unsubscribe, + ) ) await characteristic.init(service) @@ -302,7 +323,9 @@ def update_value(self, service_uuid: str, char_uuid: str) -> bool: characteristic.update_value() return True - def read(self, char: BlueZGattCharacteristic, options: Dict[str, Any]) -> bytes: + def __on_read( + self, char: BlueZGattCharacteristic, options: Dict[str, Any] + ) -> bytes: """ Read request. This re-routes the the request incomming on the dbus to the server to @@ -320,11 +343,11 @@ def read(self, char: BlueZGattCharacteristic, options: Dict[str, Any]) -> bytes: bytes The value of the characteristic """ - return bytes(self.read_request(char.UUID, options)) + return bytes(self._on_read(char.UUID, BlessGATTRequestBlueZ(options))) - def write( + def __on_write( self, char: BlueZGattCharacteristic, value: bytes, options: Dict[str, Any] - ): + ) -> None: """ Write request. This function re-routes the write request sent from the @@ -338,9 +361,13 @@ def write( value : bytearray The value being requested to set """ - return self.write_request(char.UUID, bytearray(value), options) + return self.write_request( + char.UUID, bytearray(value), BlessGATTRequestBlueZ(options) + ) - def subscribe(self, char: BlueZGattCharacteristic, options: Dict[str, Any]): + def __on_subscribe( + self, char: BlueZGattCharacteristic, session: NotifySession + ) -> None: """ Subscribe request. This function re-routes the subscribe request sent from the @@ -352,10 +379,11 @@ def subscribe(self, char: BlueZGattCharacteristic, options: Dict[str, Any]): char : BlueZGattCharacteristic The characteristic object involved in the request """ - options["central_id"] = options.get("device") - return self.subscribe_request(char.UUID, options) + return self.subscribe_request(char.UUID, BlessGATTSessionBlueZ(session)) - def unsubscribe(self, char: BlueZGattCharacteristic, options: Dict[str, Any]): + def __on_unsubscribe( + self, char: BlueZGattCharacteristic, session: NotifySession + ) -> None: """ Unsubscribe request. This function re-routes the unsubscribe request sent from the @@ -367,5 +395,4 @@ def unsubscribe(self, char: BlueZGattCharacteristic, options: Dict[str, Any]): char : BlueZGattCharacteristic The characteristic object involved in the request """ - options["central_id"] = options.get("device") - return self.unsubscribe_request(char.UUID, options) + return self.unsubscribe_request(char.UUID, BlessGATTSessionBlueZ(session)) diff --git a/bless/backends/bluezdbus/session.py b/bless/backends/bluezdbus/session.py new file mode 100644 index 0000000..4d94197 --- /dev/null +++ b/bless/backends/bluezdbus/session.py @@ -0,0 +1,20 @@ +from typing import cast + +from .dbus.session import NotifySession + +from ..session import BlessGATTSession + + +class BlessGATTSessionBlueZ(BlessGATTSession): + + @property + def session(self) -> NotifySession: + return cast(NotifySession, self.obj) + + @property + def central_id(self) -> str: + return self.session.address + + @property + def mtu(self) -> int: + return self.session.mtu diff --git a/bless/backends/characteristic.py b/bless/backends/characteristic.py index 36d42e2..9cc821a 100644 --- a/bless/backends/characteristic.py +++ b/bless/backends/characteristic.py @@ -1,8 +1,9 @@ import abc +import logging from enum import Flag from uuid import UUID -from typing import List, Optional, Set, Union, TYPE_CHECKING, cast +from typing import Callable, List, Optional, Set, Union, TYPE_CHECKING, cast from bleak.backends.characteristic import ( # type: ignore BleakGATTCharacteristic, @@ -10,11 +11,19 @@ ) from .attribute import GATTAttributePermissions +from .request import BlessGATTRequest +from .session import BlessGATTSession if TYPE_CHECKING: from bless.backends.service import BlessGATTService from bless.backends.descriptor import BlessGATTDescriptor +logger = logging.getLogger(__name__) + +GATTReadCallback = Callable[["BlessGATTCharacteristic", BlessGATTRequest], bytearray] +GATTWriteCallback = Callable[["BlessGATTCharacteristic", bytes, BlessGATTRequest], None] +GATTSubscribeCallback = Callable[["BlessGATTCharacteristic", BlessGATTSession], None] + class GATTCharacteristicProperties(Flag): broadcast = 0x0001 @@ -67,6 +76,10 @@ def __init__( properties: GATTCharacteristicProperties, permissions: GATTAttributePermissions, value: Optional[bytearray], + on_read: Optional[GATTReadCallback] = None, + on_write: Optional[GATTWriteCallback] = None, + on_subscribe: Optional[GATTSubscribeCallback] = None, + on_unsubscribe: Optional[GATTSubscribeCallback] = None, ): """ Instantiates a new GATT Characteristic but is not yet assigned to any @@ -83,7 +96,25 @@ def __init__( Permissions that define the protection levels of the properties value : Optional[bytearray] The binary value of the characteristic + on_read : Optional[GATTReadCallback] + If defined, reads destined for this characteristic will be passed + to this function + on_write : Optional[GATTWriteCallback] + If defined, writes destined for this characteristic will be passed + to this function + on_subscribe : Optional[GATTSubscribeCallback] + If defined, subscriptions destined for this characteristic will be + passed to this function + on_unsubscribe : Optional[GATTSubscribeCallback] + If defined, unsubscriptions destined for this characteristic will + be passed to this function """ + self.on_read: Optional[GATTReadCallback] = on_read + self.on_write: Optional[GATTWriteCallback] = on_write + self.on_subscribe: Optional[GATTSubscribeCallback] = on_subscribe + self.on_unsubscribe: Optional[GATTSubscribeCallback] = on_unsubscribe + + logger.debug(f"With on_read: {on_read}") if type(uuid) is str: uuid_str: str = cast(str, uuid) uuid = UUID(uuid_str) @@ -94,7 +125,6 @@ def __init__( ) self._permissions: GATTAttributePermissions = permissions self._initial_value: Optional[bytearray] = value - self._subscribed_centrals: Set[str] = set() def __str__(self): """ @@ -133,10 +163,4 @@ def subscribed_centrals(self) -> Set[str]: """ Unique list of subscribed central IDs """ - return self._subscribed_centrals - - def add_subscription(self, central_id: str): - self._subscribed_centrals.add(central_id) - - def remove_subscription(self, central_id: str): - self._subscribed_centrals.discard(central_id) + raise NotImplementedError diff --git a/bless/backends/corebluetooth/characteristic.py b/bless/backends/corebluetooth/characteristic.py index accb403..32844fb 100644 --- a/bless/backends/corebluetooth/characteristic.py +++ b/bless/backends/corebluetooth/characteristic.py @@ -1,10 +1,11 @@ -from enum import Flag +import logging + from uuid import UUID -from typing import Union, Optional, Dict +from typing import Dict, Optional, Set, Union from CoreBluetooth import CBUUID, CBMutableCharacteristic # type: ignore -from bleak.backends.characteristic import ( # type: ignore +from bleak.backends.characteristic import ( BleakGATTCharacteristic, ) from bleak.backends.descriptor import BleakGATTDescriptor # type: ignore @@ -15,14 +16,12 @@ GATTCharacteristicProperties, GATTAttributePermissions, BlessGATTCharacteristic as BaseBlessGATTCharacteristic, + GATTReadCallback, + GATTWriteCallback, + GATTSubscribeCallback, ) - -class CBAttributePermissions(Flag): - readable = 0x1 - writeable = 0x2 - read_encryption_required = 0x4 - write_encryption_required = 0x8 +logger = logging.getLogger(__name__) class BlessGATTCharacteristicCoreBluetooth( @@ -38,6 +37,10 @@ def __init__( properties: GATTCharacteristicProperties, permissions: GATTAttributePermissions, value: Optional[bytearray], + on_read: Optional[GATTReadCallback] = None, + on_write: Optional[GATTWriteCallback] = None, + on_subscribe: Optional[GATTSubscribeCallback] = None, + on_unsubscribe: Optional[GATTSubscribeCallback] = None, ): """ Instantiates a new GATT Characteristic but is not yet assigned to any @@ -54,8 +57,35 @@ def __init__( Permissions that define the protection levels of the properties value : Optional[bytearray] The binary value of the characteristic + on_read : Optional[GATTReadCallback] + If defined, reads destined for this characteristic will be passed + to this function + on_write : Optional[GATTWriteCallback] + If defined, writes destined for this characteristic will be passed + to this function + on_subscribe : Optional[GATTSubscribeCallback] + If defined, subscriptions destined for this characteristic will be + passed to this function + on_unsubscribe : Optional[GATTSubscribeCallback] + If defined, unsubscriptions destined for this characteristic will + be passed to this function """ - BaseBlessGATTCharacteristic.__init__(self, uuid, properties, permissions, value) + BaseBlessGATTCharacteristic.__init__( + self, + uuid, + properties, + permissions, + value, + on_read, + on_write, + on_subscribe, + on_unsubscribe, + ) + if value is not None and on_read is not None: + logger.warning( + "On CoreBluetooth, " + + "callbacks are only triggered if the value is initialized to None" + ) self._descriptors: Dict[int, BleakGATTDescriptor] = {} self._cb_characteristic: Optional[CBMutableCharacteristic] = None @@ -120,3 +150,12 @@ def value(self, val: bytearray): if self._cb_characteristic is not None: cb_char: CBMutableCharacteristic = self._cb_characteristic cb_char.setValue_(val) + + @property + def subscribed_centrals(self) -> Set[str]: + return set( + [ + central.identifier().UUIDString() + for central in self.obj.subscribedCentrals() + ] + ) diff --git a/bless/backends/corebluetooth/peripheral_manager_delegate.py b/bless/backends/corebluetooth/peripheral_manager_delegate.py index 231425e..5d34cf0 100644 --- a/bless/backends/corebluetooth/peripheral_manager_delegate.py +++ b/bless/backends/corebluetooth/peripheral_manager_delegate.py @@ -356,18 +356,7 @@ def peripheralManager_central_didSubscribeToCharacteristic_( # noqa: N802 ) ) - # Get the MTU - mtu_value = None - max_update = getattr(central, "maximumUpdateValueLength", None) - if callable(max_update): - mtu_value = max_update() - else: - mtu_value = max_update - if mtu_value is not None and self.server is not None: - self.server._mtu = int(mtu_value) - - options = {"central_id": central_uuid} - self.get_callback("subscribe")(char_uuid, options) + self.get_callback("subscribe")(char_uuid, central) def peripheralManager_central_didUnsubscribeFromCharacteristic_( # noqa: N802 E501 self, @@ -383,8 +372,7 @@ def peripheralManager_central_didUnsubscribeFromCharacteristic_( # noqa: N802 E ) ) - options = {"central_id": central_uuid} - self.get_callback("unsubscribe")(char_uuid, options) + self.get_callback("unsubscribe")(char_uuid, central) def peripheralManagerIsReadyToUpdateSubscribers_( # noqa: N802 self, peripheral_manager: CBPeripheralManager @@ -394,8 +382,6 @@ def peripheralManagerIsReadyToUpdateSubscribers_( # noqa: N802 def peripheralManager_didReceiveReadRequest_( # noqa: N802 self, peripheral_manager: CBPeripheralManager, request: CBATTRequest ): - # This should probably be a callback to be handled by the user, to be - # implemented or given to the BleakServer LOGGER.debug( "Received read request from {} for characteristic {}".format( request.central().identifier().UUIDString(), @@ -403,14 +389,15 @@ def peripheralManager_didReceiveReadRequest_( # noqa: N802 ) ) request.setValue_( - self.get_callback("read")(request.characteristic().UUID().UUIDString()) + self.get_callback("read")( + request.characteristic().UUID().UUIDString(), request + ) ) peripheral_manager.respondToRequest_withResult_(request, CBATTErrorSuccess) def peripheralManager_didReceiveWriteRequests_( # noqa: N802 self, peripheral_manager: CBPeripheralManager, requests: List[CBATTRequest] ): - # Again, this should likely be moved to a callback LOGGER.debug("Receving write requests...") for request in requests: central: CBCentral = request.central() @@ -423,7 +410,7 @@ def peripheralManager_didReceiveWriteRequests_( # noqa: N802 value, ) ) - self.get_callback("write")(char.UUID().UUIDString(), value) + self.get_callback("write")(char.UUID().UUIDString(), value, request) peripheral_manager.respondToRequest_withResult_( requests[0], CBATTErrorSuccess diff --git a/bless/backends/corebluetooth/request.py b/bless/backends/corebluetooth/request.py new file mode 100644 index 0000000..251bdc5 --- /dev/null +++ b/bless/backends/corebluetooth/request.py @@ -0,0 +1,30 @@ +from CoreBluetooth import CBATTRequest # type: ignore +from typing import Optional, cast +from ..characteristic import GATTCharacteristicProperties +from ..request import BlessGATTRequest + + +class BlessGATTRequestCoreBluetooth(BlessGATTRequest): + + @property + def request(self) -> CBATTRequest: + return cast(CBATTRequest, self.obj) + + @property + def central_id(self) -> str: + return self.request.central().identifier().UUIDString() + + @property + def mtu(self) -> int: + return self.request.central().maximumUpdateValueLength() + + @property + def offset(self) -> int: + return self.request.offset() + + @property + def response_requested(self) -> Optional[bool]: + return not ( + self.request.characteristic().properties() + & GATTCharacteristicProperties.write_without_response.value + ) diff --git a/bless/backends/corebluetooth/server.py b/bless/backends/corebluetooth/server.py index b0aad26..7f62f17 100644 --- a/bless/backends/corebluetooth/server.py +++ b/bless/backends/corebluetooth/server.py @@ -7,12 +7,14 @@ from asyncio.events import AbstractEventLoop from CoreBluetooth import ( # type: ignore - CBService, - CBPeripheralManager, - CBMutableCharacteristic, - CBMutableDescriptor, CBAdvertisementDataLocalNameKey, CBAdvertisementDataServiceUUIDsKey, + CBATTRequest, + CBCentral, + CBMutableCharacteristic, + CBMutableDescriptor, + CBPeripheralManager, + CBService, CBUUID, ) @@ -38,7 +40,12 @@ ) from bless.backends.characteristic import ( GATTCharacteristicProperties, + GATTReadCallback, + GATTWriteCallback, + GATTSubscribeCallback, ) +from .request import BlessGATTRequestCoreBluetooth +from .session import BlessGATTSessionCoreBluetooth logger = logging.getLogger(name=__name__) @@ -71,10 +78,10 @@ def __init__(self, name: str, loop: Optional[AbstractEventLoop] = None, **kwargs ) = PeripheralManagerDelegate.alloc().init( self, { - "read": self.read_request, - "write": self.write_request, - "subscribe": self.subscribe_request, - "unsubscribe": self.unsubscribe_request, + "read": self.__on_read, + "write": self.__on_write, + "subscribe": self.__on_subscribe, + "unsubscribe": self.__on_unsubscribe, }, ) @@ -148,18 +155,6 @@ async def stop(self): """ await self.peripheral_manager_delegate.stop_advertising() - async def is_connected(self) -> bool: - """ - Determine whether there are any connected central devices - - Returns - ------- - bool - True if there are central devices that are connected - """ - n_subscriptions = len(self.peripheral_manager_delegate._central_subscriptions) - return n_subscriptions > 0 - async def is_advertising(self) -> bool: """ Determine whether the service is advertising @@ -197,6 +192,10 @@ async def add_new_characteristic( properties: GATTCharacteristicProperties, value: Optional[bytearray], permissions: GATTAttributePermissions, + on_read: Optional[GATTReadCallback] = None, + on_write: Optional[GATTWriteCallback] = None, + on_subscribe: Optional[GATTSubscribeCallback] = None, + on_unsubscribe: Optional[GATTSubscribeCallback] = None, ): """ Generate a new characteristic to be associated with the server @@ -215,12 +214,31 @@ async def add_new_characteristic( The initial value for the characteristic permissions : GATTAttributePermissions The permissions for the characteristic + on_read : Optional[GATTReadCallback] + If defined, reads destined for this characteristic will be passed + to this function + on_write : Optional[GATTWriteCallback] + If defined, writes destined for this characteristic will be passed + to this function + on_subscribe : Optional[GATTSubscribeCallback] + If defined, subscriptions destined for this characteristic will be + passed to this function + on_unsubscribe : Optional[GATTSubscribeCallback] + If defined, unsubscriptions destined for this characteristic will + be passed to this function """ service_uuid = str(UUID(service_uuid)) logger.debug("Creating a new characteristic with uuid: {}".format(char_uuid)) characteristic: BlessGATTCharacteristicCoreBluetooth = ( BlessGATTCharacteristicCoreBluetooth( - char_uuid, properties, permissions, value + char_uuid, + properties, + permissions, + value, + on_read, + on_write, + on_subscribe, + on_unsubscribe, ) ) @@ -298,3 +316,15 @@ def update_value(self, service_uuid: str, char_uuid: str) -> bool: ) return result + + def __on_read(self, uuid: str, request: CBATTRequest) -> bytearray: + return self._on_read(uuid, BlessGATTRequestCoreBluetooth(request)) + + def __on_write(self, uuid: str, value: bytearray, request: CBATTRequest) -> None: + return self._on_write(uuid, value, BlessGATTRequestCoreBluetooth(request)) + + def __on_subscribe(self, uuid: str, central: CBCentral) -> None: + return self._on_subscribe(uuid, BlessGATTSessionCoreBluetooth(central)) + + def __on_unsubscribe(self, uuid: str, central: CBCentral) -> None: + return self._on_unsubscribe(uuid, BlessGATTSessionCoreBluetooth(central)) diff --git a/bless/backends/corebluetooth/session.py b/bless/backends/corebluetooth/session.py new file mode 100644 index 0000000..d70cac1 --- /dev/null +++ b/bless/backends/corebluetooth/session.py @@ -0,0 +1,19 @@ +from CoreBluetooth import CBCentral # type: ignore +from typing import cast + +from ..session import BlessGATTSession + + +class BlessGATTSessionCoreBluetooth(BlessGATTSession): + + @property + def central(self) -> CBCentral: + return cast(CBCentral, self.obj) + + @property + def central_id(self) -> str: + return self.central.identifier().UUIDString() + + @property + def mtu(self) -> int: + return self.central.maximumUpdateValueLength() diff --git a/bless/backends/request.py b/bless/backends/request.py new file mode 100644 index 0000000..9a0dc74 --- /dev/null +++ b/bless/backends/request.py @@ -0,0 +1,58 @@ +from abc import abstractmethod +from typing import Any, Dict, Optional + + +class BlessGATTRequest: + + def __init__(self, obj: Any): + """ + Parameters + ---------- + obj : Any + The backend-specific request object + """ + self.obj: Any = obj + + def __str__(self) -> str: + return f"BlessGATTRequest({self.to_dict()})" + + def to_dict(self) -> Dict[str, Any]: + return { + "central_id": self.central_id, + "mtu": self.mtu, + "offset": self.offset, + "response_requested": self.response_requested, + } + + @property + @abstractmethod + def central_id(self) -> str: + """ + The id of the central that made the request + """ + raise NotImplementedError + + @property + @abstractmethod + def mtu(self) -> int: + """ + The maximum transfer unit + """ + raise NotImplementedError + + @property + @abstractmethod + def offset(self) -> int: + """ + The offset of the characteristic value to begin reading or writing from + in bytes + """ + raise NotImplementedError + + @property + @abstractmethod + def response_requested(self) -> Optional[bool]: + """ + Whether a response is requested for write requests + """ + raise NotImplementedError diff --git a/bless/backends/server.py b/bless/backends/server.py index 7abb2d4..98a8078 100644 --- a/bless/backends/server.py +++ b/bless/backends/server.py @@ -1,10 +1,11 @@ import abc import asyncio import logging +import coloredlogs # type: ignore from uuid import UUID from asyncio import AbstractEventLoop -from typing import Any, Callable, Dict, List, Optional, Set +from typing import Any, Dict, List, Optional, Set from bless.backends.service import BlessGATTService from bless.backends.advertisement import BlessAdvertisementData @@ -12,11 +13,17 @@ from bless.backends.characteristic import ( # type: ignore BlessGATTCharacteristic, GATTCharacteristicProperties, + GATTReadCallback, + GATTWriteCallback, + GATTSubscribeCallback, ) from bless.backends.descriptor import GATTDescriptorProperties # type: ignore +from bless.backends.session import BlessGATTSession +from bless.backends.request import BlessGATTRequest from bless.exceptions import BlessError +coloredlogs.install(level="DEBUG") LOGGER = logging.getLogger(__name__) @@ -30,10 +37,20 @@ class BaseBlessServer(abc.ABC): Used to manage services and characteristics that this server advertises """ - def __init__(self, loop: Optional[AbstractEventLoop] = None, **kwargs): + def __init__( + self, + loop: Optional[AbstractEventLoop] = None, + on_read: Optional[GATTReadCallback] = None, + on_write: Optional[GATTWriteCallback] = None, + on_subscribe: Optional[GATTSubscribeCallback] = None, + on_unsubscribe: Optional[GATTSubscribeCallback] = None, + **kwargs, + ): self.loop: AbstractEventLoop = loop if loop else asyncio.get_event_loop() - - self._callbacks: Dict[str, Callable[[Any], Any]] = {} + self.on_read: Optional[GATTReadCallback] = on_read + self.on_write: Optional[GATTWriteCallback] = on_write + self.on_subscribe: Optional[GATTSubscribeCallback] = on_subscribe + self.on_unsubscribe: Optional[GATTSubscribeCallback] = on_unsubscribe self.services: Dict[str, BlessGATTService] = {} self._mtu: Optional[int] = None @@ -80,7 +97,6 @@ async def stop(self) -> bool: """ raise NotImplementedError() - @abc.abstractmethod async def is_connected(self) -> bool: """ Determine whether there are any connected central devices @@ -90,7 +106,19 @@ async def is_connected(self) -> bool: bool Whether any peripheral devices are connected """ - raise NotImplementedError() + return ( + len( + set( + [ + cid + for service in self.services.values() + for characteristic in service.characteristics + for cid in characteristic.subscribed_centrals + ] + ) + ) + > 0 + ) @abc.abstractmethod async def is_advertising(self) -> bool: @@ -127,6 +155,10 @@ async def add_new_characteristic( properties: GATTCharacteristicProperties, value: Optional[bytearray], permissions: GATTAttributePermissions, + on_read: Optional[GATTReadCallback] = None, + on_write: Optional[GATTWriteCallback] = None, + on_subscribe: Optional[GATTSubscribeCallback] = None, + on_unsubscribe: Optional[GATTSubscribeCallback] = None, ): """ Add a new characteristic to be associated with the server @@ -145,6 +177,18 @@ async def add_new_characteristic( characteristic. Can be None if the characteristic is writable permissions : GATTAttributePermissions GATT flags that define the permissions for the characteristic + on_read : Optional[GATTReadCallback] + If defined, reads destined for this characteristic will be passed + to this function + on_write : Optional[GATTWriteCallback] + If defined, writes destined for this characteristic will be passed + to this function + on_subscribe : Optional[GATTSubscribeCallback] + If defined, subscriptions destined for this characteristic will be + passed to this function + on_unsubscribe : Optional[GATTSubscribeCallback] + If defined, unsubscriptions destined for this characteristic will + be passed to this function """ raise NotImplementedError() @@ -273,6 +317,10 @@ async def add_gatt(self, gatt_tree: Dict): char_info.get("Properties"), char_info.get("Value"), char_info.get("Permissions"), + char_info.get("OnRead"), + char_info.get("OnWrite"), + char_info.get("OnSubscribe"), + char_info.get("OnUnsubscribe"), ) descriptors = char_info.get("Descriptors") if isinstance(descriptors, dict): @@ -286,7 +334,7 @@ async def add_gatt(self, gatt_tree: Dict): desc_info.get("Permissions"), ) - def read_request(self, uuid: str, options: Optional[Dict] = None) -> bytearray: + def _on_read(self, uuid: str, request: BlessGATTRequest) -> bytearray: """ This function should be handed off to the subsequent backend bluetooth servers as a callback for incoming read requests on values for @@ -298,6 +346,8 @@ def read_request(self, uuid: str, options: Optional[Dict] = None) -> bytearray: uuid : str The string representation of the UUID for the characteristic whose value is to be read + request : BlessGATTRequest + The read request Returns ------- @@ -305,39 +355,77 @@ def read_request(self, uuid: str, options: Optional[Dict] = None) -> bytearray: A bytearray value that represents the value for the characteristic requested """ - if options is not None: - self._update_mtu_from_options(options) + LOGGER.debug(f"Read request\n\tuuid: {uuid}\n\trequest: {request}") characteristic: Optional[BlessGATTCharacteristic] = self.get_characteristic( uuid ) - if not characteristic: raise BlessError("Invalid characteristic: {}".format(uuid)) - return self.on_read(characteristic) + # handle MTU capture + self.mtu = request.mtu + + # Route to characteristic read + LOGGER.debug(f"on_read: {characteristic.on_read}") + if characteristic.on_read is not None: + LOGGER.debug("Characteristic Read!") + return characteristic.on_read(characteristic, request) + + # Route to server defined read + if self.on_read is not None: + return self.on_read(characteristic, request) - def write_request( - self, uuid: str, value: Any, options: Optional[Dict] = None - ) -> None: + # Generic handling + return characteristic.value + + def _on_write(self, uuid: str, value: Any, request: BlessGATTRequest) -> None: """ Obtain the characteristic to write and pass on to the user-defined on_write + Parameters + ---------- + uuid : str + The string representation of the UUID for the characteristic whose + value is to be written + value : Any + The value to write to the characteristic + request : BlessGATTRequest + The write request data + """ - if options is not None: - self._update_mtu_from_options(options) + LOGGER.debug(f"Write request\n\tuuid: {uuid}\n\trequest: {request}") characteristic: Optional[BlessGATTCharacteristic] = self.get_characteristic( uuid ) + if not characteristic: + raise BlessError("Invalid characteristic: {}".format(uuid)) + + # handle MTU capture + self.mtu = request.mtu + + # Route to characteristic write + if characteristic.on_write is not None: + return characteristic.on_write(characteristic, value, request) - self.on_write(characteristic, value) + # Route to server defined write + if self.on_write is not None: + return self.on_write(characteristic, value, request) - def subscribe_request(self, uuid: str, options: Optional[Dict] = None) -> None: + def _on_subscribe(self, uuid: str, session: BlessGATTSession) -> None: """ Obtain the characteristic to subscribe to and pass on to the user-defined on_subscribe + + Parameters + ---------- + uuid : str + The string representation of the UUID for the characteristic whose + value is to be subscribed to + session : BlessGATTSession + The session object """ - LOGGER.debug(f"Subscribe_request\n\tuuid: {uuid}\n\toptions: {options}") + LOGGER.debug(f"Subscribe request\n\tuuid: {uuid}\n\tsession: {session}") characteristic: Optional[BlessGATTCharacteristic] = self.get_characteristic( uuid ) @@ -345,19 +433,31 @@ def subscribe_request(self, uuid: str, options: Optional[Dict] = None) -> None: if characteristic is None: raise BlessError(f"Invalid characteristic: {uuid}") - if options is not None: - self._update_mtu_from_options(options) + # handle MTU capture + self.mtu = session.mtu - if options.get("central_id") is not None: - characteristic.add_subscription(options["central_id"]) + # Route to characteristic subscription + if characteristic.on_subscribe is not None: + return characteristic.on_subscribe(characteristic, session) - self.on_subscribe(characteristic) + # Route to server defined subscription + if self.on_subscribe is not None: + return self.on_subscribe(characteristic, session) - def unsubscribe_request(self, uuid: str, options: Optional[Dict] = None) -> None: + def _on_unsubscribe(self, uuid: str, session: BlessGATTSession) -> None: """ Obtain the characteristic to unsubscribe from and pass on to the user-defined on_unsubscribe + + Parameters + ---------- + uuid : str + The string representation of the UUID for the characteristic whose + value is to be unsubscribed from + session : BlessGATTSession + The session object """ + LOGGER.debug(f"Unsubscribe request\n\tuuid: {uuid}\n\tsession: {session}") characteristic: Optional[BlessGATTCharacteristic] = self.get_characteristic( uuid ) @@ -365,84 +465,44 @@ def unsubscribe_request(self, uuid: str, options: Optional[Dict] = None) -> None if characteristic is None: raise BlessError(f"Invalid characteristic: {uuid}") - if options is not None: - self._update_mtu_from_options(options) + # handle MTU capture + self.mtu = session.mtu - if options.get("central_id") is not None: - characteristic.remove_subscription(options["central_id"]) + # Route to characteristic unsubscription + if characteristic.on_unsubscribe is not None: + return characteristic.on_unsubscribe(characteristic, session) - self.on_unsubscribe(characteristic) + # Route to server defined unsubscription + if self.on_unsubscribe is not None: + return self.on_unsubscribe(characteristic, session) - @property - def on_read(self) -> Callable[[Any], Any]: + # Aliases for backwards compatibility + def read_request(self, uuid: str, request: BlessGATTRequest) -> bytearray: """ - Alias for `read_request_func`. + Alias for `_on_read` for backwards compatibility """ - func: Optional[Callable[[Any], Any]] = self._callbacks.get("read") - if func is not None: - return func - else: - raise BlessError("Server: Read Callback is undefined") + return self._on_read(uuid, request) - @on_read.setter - def on_read(self, func: Callable): + def write_request(self, uuid: str, value: Any, request: BlessGATTRequest) -> None: """ - Alias for `read_request_func`. + Alias for `_on_write` for backwards compatibility """ - self._callbacks["read"] = func + return self._on_write(uuid, value, request) - @property - def on_write(self) -> Callable: + def subscribe_request(self, uuid: str, session: BlessGATTSession) -> None: """ - Alias for `write_request_func`. + Alias for `_on_subscribe` for backwards compatibility """ - func: Optional[Callable[[Any], Any]] = self._callbacks.get("write") - if func is not None: - return func - else: - raise BlessError("Server: Write Callback is undefined") + return self._on_subscribe(uuid, session) - @on_write.setter - def on_write(self, func: Callable): + def unsubscribe_request(self, uuid: str, session: BlessGATTSession) -> None: """ - Alias for `write_request_func`. + Alias for `_on_unsubscribe` for backwards compatibility """ - self._callbacks["write"] = func + return self._on_unsubscribe(uuid, session) @property - def on_subscribe(self) -> Callable: - """ - Alias for `subscribe_request_func`. - """ - func: Optional[Callable[[Any], Any]] = self._callbacks.get("subscribe") - if func is not None: - return func - else: - raise BlessError("Server: Subscribe Callback is undefined") - - @on_subscribe.setter - def on_subscribe(self, func: Callable): - """ """ - self._callbacks["subscribe"] = func - - @property - def on_unsubscribe(self) -> Callable: - """ - Alias for `unsubscribe_request_func`. - """ - func: Optional[Callable[[Any], Any]] = self._callbacks.get("unsubscribe") - if func is not None: - return func - else: - raise BlessError("Server: Unsubscribe Callback is undefined") - - @on_unsubscribe.setter - def on_unsubscribe(self, func: Callable): - """ """ - self._callbacks["unsubscribe"] = func - - @property - def read_request_func(self) -> Callable[[Any], Any]: + def read_request_func(self) -> Optional[GATTReadCallback]: """ Return an instance of the function to handle incoming read requests @@ -453,7 +513,7 @@ def read_request_func(self) -> Callable[[Any], Any]: return self.on_read @read_request_func.setter - def read_request_func(self, func: Callable): + def read_request_func(self, func: GATTReadCallback): """ Set the function to handle incoming read requests @@ -464,7 +524,7 @@ def read_request_func(self, func: Callable): self.on_read = func @property - def write_request_func(self) -> Callable: + def write_request_func(self) -> Optional[GATTWriteCallback]: """ Return an instance of the function to handle incoming write requests @@ -475,7 +535,7 @@ def write_request_func(self) -> Callable: return self.on_write @write_request_func.setter - def write_request_func(self, func: Callable): + def write_request_func(self, func: GATTWriteCallback): """ Set the function to handle incoming write requests @@ -499,22 +559,6 @@ def mtu(self, value: Optional[int]): """ self._mtu = value - @staticmethod - def _coerce_mtu_value(value: Any) -> Optional[int]: - if value is None: - return None - if hasattr(value, "value"): - return BaseBlessServer._coerce_mtu_value(value.value) - try: - return int(value) - except (TypeError, ValueError): - return None - - def _update_mtu_from_options(self, options: Dict[str, Any]) -> None: - mtu_value = self._coerce_mtu_value(options.get("mtu")) - if mtu_value is not None: - self._mtu = mtu_value - @property def subscribed_centrals(self) -> Set[str]: """ @@ -536,12 +580,6 @@ def subscribed_clients(self) -> Set[str]: """ return self.subscribed_centrals - def _normalize_uuid(self, uuid: str) -> str: - try: - return str(UUID(uuid)) - except ValueError: - return uuid - @staticmethod def is_uuid(uuid: str) -> bool: """ diff --git a/bless/backends/session.py b/bless/backends/session.py new file mode 100644 index 0000000..ee2f815 --- /dev/null +++ b/bless/backends/session.py @@ -0,0 +1,43 @@ +from abc import abstractmethod +from typing import Any, Dict + + +class BlessGATTSession: + """ + Represents a session established between a central and a peripheral + during a subscription + """ + + def __init__(self, obj: Any): + """ + Parameters + ---------- + obj : Any + The backend-specific session object + """ + self.obj: Any = obj + + def __str__(self) -> str: + return f"BlessGATTSession({self.to_dict()})" + + def to_dict(self) -> Dict[str, Any]: + return { + "central_id": self.central_id, + "mtu": self.mtu, + } + + @property + @abstractmethod + def central_id(self) -> str: + """ + The central ID of this session + """ + raise NotImplementedError + + @property + @abstractmethod + def mtu(self) -> int: + """ + The maximum transfer unit + """ + raise NotImplementedError diff --git a/bless/backends/winrt/characteristic.py b/bless/backends/winrt/characteristic.py index 0015e4e..d72e1d6 100644 --- a/bless/backends/winrt/characteristic.py +++ b/bless/backends/winrt/characteristic.py @@ -1,6 +1,6 @@ import sys from uuid import UUID -from typing import Union, Optional, Dict +from typing import Dict, Optional, Union, Set from bleak.backends.characteristic import ( # type: ignore BleakGATTCharacteristic, @@ -33,6 +33,9 @@ from bless.backends.characteristic import ( BlessGATTCharacteristic as BaseBlessGATTCharacteristic, GATTCharacteristicProperties, + GATTReadCallback, + GATTWriteCallback, + GATTSubscribeCallback, ) @@ -49,6 +52,10 @@ def __init__( properties: GATTCharacteristicProperties, permissions: GATTAttributePermissions, value: Optional[bytearray], + on_read: Optional[GATTReadCallback] = None, + on_write: Optional[GATTWriteCallback] = None, + on_subscribe: Optional[GATTSubscribeCallback] = None, + on_unsubscribe: Optional[GATTSubscribeCallback] = None, ): """ Instantiates a new GATT Characteristic but is not yet assigned to any @@ -65,9 +72,31 @@ def __init__( Permissions that define the protection levels of the properties value : Optional[bytearray] The binary value of the characteristic + on_read : Optional[GATTReadCallback] + If defined, reads destined for this characteristic will be passed + to this function + on_write : Optional[GATTWriteCallback] + If defined, writes destined for this characteristic will be passed + to this function + on_subscribe : Optional[GATTSubscribeCallback] + If defined, subscriptions destined for this characteristic will be + passed to this function + on_unsubscribe : Optional[GATTSubscribeCallback] + If defined, unsubscriptions destined for this characteristic will + be passed to this function """ value = value if value is not None else bytearray(b"") - BaseBlessGATTCharacteristic.__init__(self, uuid, properties, permissions, value) + BaseBlessGATTCharacteristic.__init__( + self, + uuid, + properties, + permissions, + value, + on_read, + on_write, + on_subscribe, + on_unsubscribe, + ) self._value = value self._descriptors: Dict[int, BleakGATTDescriptor] = {} self._gatt_characteristic: Optional[GattLocalCharacteristic] = None @@ -176,3 +205,9 @@ def value(self) -> bytearray: def value(self, val: bytearray): """Set the value of the characteristic""" self._value = val + + @property + def subscribed_centrals(self) -> Set[str]: + return set( + [client.session.device_id.id for client in self.obj.subscribed_clients] + ) diff --git a/bless/backends/winrt/request.py b/bless/backends/winrt/request.py new file mode 100644 index 0000000..be6d80c --- /dev/null +++ b/bless/backends/winrt/request.py @@ -0,0 +1,56 @@ +import sys + +if sys.version_info >= (3, 12): + from winrt.windows.devices.bluetooth.genericattributeprofile import ( # type: ignore # noqa: E501 + GattSession, + GattReadRequest, + GattWriteRequest, + GattWriteOption + ) +else: + from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( # type: ignore # noqa: E501 + GattSession, + GattReadRequest, + GattWriteRequest, + GattWriteOption + ) +from typing import Optional, Tuple, Union, cast +from ..request import BlessGATTRequest + +GattRequest = Union[GattReadRequest, GattWriteRequest] +WinRTGattRequest = Tuple[GattSession, GattRequest] + + +class BlessGATTRequestWinRT(BlessGATTRequest): + + @property + def object_data(self) -> WinRTGattRequest: + return cast(WinRTGattRequest, self.obj) + + @property + def session(self) -> GattSession: + return cast(GattSession, self.object_data[0]) + + @property + def request(self) -> GattRequest: + return cast(GattRequest, self.object_data[1]) + + @property + def central_id(self) -> str: + return self.session.device_id.id + + @property + def mtu(self) -> int: + return self.session.max_pdu_size + + @property + def offset(self) -> int: + return self.request.offset + + @property + def response_requested(self) -> Optional[bool]: + return ( + None + if isinstance(self.request, GattReadRequest) + else (self.request.option == GattWriteOption.WRITE_WITH_RESPONSE) + ) diff --git a/bless/backends/winrt/server.py b/bless/backends/winrt/server.py index 274dc61..d369c99 100644 --- a/bless/backends/winrt/server.py +++ b/bless/backends/winrt/server.py @@ -5,7 +5,7 @@ from uuid import UUID from threading import Event from asyncio.events import AbstractEventLoop -from typing import Optional, List, Any, cast, Set +from typing import Any, Dict, List, Optional, Set, cast from bless.backends.server import BaseBlessServer # type: ignore from bless.backends.advertisement import BlessAdvertisementData @@ -14,6 +14,9 @@ ) from bless.backends.characteristic import ( # type: ignore GATTCharacteristicProperties, + GATTReadCallback, + GATTWriteCallback, + GATTSubscribeCallback, ) from bless.backends.descriptor import GATTDescriptorProperties from bless.backends.winrt.service import BlessGATTServiceWinRT @@ -22,9 +25,11 @@ ) from bless.backends.winrt.descriptor import BlessGATTDescriptorWinRT # type: ignore - from bless.backends.winrt.ble import BLEAdapter +from .request import BlessGATTRequestWinRT +from .session import BlessGATTSessionWinRT + # CLR imports # Import of Bleak CLR->UWP Bridge. # from BleakBridge import Bridge @@ -45,6 +50,7 @@ GattReadRequest, GattWriteRequestedEventArgs, GattWriteRequest, + GattSession, GattSubscribedClient, ) else: @@ -65,6 +71,7 @@ GattReadRequest, GattWriteRequestedEventArgs, GattWriteRequest, + GattSession, GattSubscribedClient, ) @@ -116,7 +123,7 @@ def __init__( self.name: str = name self._service_provider: Optional[GattServiceProvider] = None - self._subscribed_clients: List[GattSubscribedClient] = [] + self._subscribed_clients: Dict[str, Set[GattSubscribedClient]] = {} self._advertising: bool = False self._advertising_started: Event = Event() @@ -177,18 +184,6 @@ async def stop(self: "BlessServerWinRT"): service_provider.stop_advertising() self._advertising = False - async def is_connected(self) -> bool: - """ - Determine whether there are any connected peripheral devices - - Returns - ------- - bool - True if there are any central devices that have subscribed to our - characteristics - """ - return len(self._subscribed_clients) > 0 - async def is_advertising(self) -> bool: """ Determine whether the server is advertising @@ -259,6 +254,10 @@ async def add_new_characteristic( properties: GATTCharacteristicProperties, value: Optional[bytearray], permissions: GATTAttributePermissions, + on_read: Optional[GATTReadCallback] = None, + on_write: Optional[GATTWriteCallback] = None, + on_subscribe: Optional[GATTSubscribeCallback] = None, + on_unsubscribe: Optional[GATTSubscribeCallback] = None, ): """ Generate a new characteristic to be associated with the server @@ -276,6 +275,18 @@ async def add_new_characteristic( The initial value for the characteristic permissions : GATTAttributePermissions The permissions for the characteristic + on_read : Optional[GATTReadCallback] + If defined, reads destined for this characteristic will be passed + to this function + on_write : Optional[GATTWriteCallback] + If defined, writes destined for this characteristic will be passed + to this function + on_subscribe : Optional[GATTSubscribeCallback] + If defined, subscriptions destined for this characteristic will be + passed to this function + on_unsubscribe : Optional[GATTSubscribeCallback] + If defined, unsubscriptions destined for this characteristic will + be passed to this function """ service_uuid = str(UUID(service_uuid)) @@ -284,12 +295,24 @@ async def add_new_characteristic( BlessGATTServiceWinRT, self.services[service_uuid] ) characteristic: BlessGATTCharacteristicWinRT = BlessGATTCharacteristicWinRT( - char_uuid, properties, permissions, value + char_uuid, + properties, + permissions, + value, + on_read, + on_write, + on_subscribe, + on_unsubscribe, ) await characteristic.init(service) - characteristic.obj.add_read_requested(self.read_characteristic) - characteristic.obj.add_write_requested(self.write_characteristic) - characteristic.obj.add_subscribed_clients_changed(self.subscribe_characteristic) + + # All characteristics route through to: + # 1. Backend-specific `__on_` + # 2. Bless server `_on_` + # 3. User-defined `on_` + characteristic.obj.add_read_requested(self.__on_read) + characteristic.obj.add_write_requested(self.__on_write) + characteristic.obj.add_subscribed_clients_changed(self.__on_subscribe) service.add_characteristic(characteristic) async def add_new_descriptor( @@ -356,7 +379,7 @@ def update_value(self, service_uuid: str, char_uuid: str) -> bool: return True - def read_characteristic( + def __on_read( self, sender: GattLocalCharacteristic, args: GattReadRequestedEventArgs ): """ @@ -374,6 +397,11 @@ def read_characteristic( deferral: Optional[Deferral] = args.get_deferral() if deferral is None: return + + # Get the session + session: GattSession = args.session + + # Get the request object logger.debug("Getting request object {}".format(self)) request: GattReadRequest @@ -383,7 +411,12 @@ async def f(): asyncio.new_event_loop().run_until_complete(f()) logger.debug("Got request object {}".format(request)) - value: bytearray = self.read_request(str(sender.uuid), {}) + + # pass up to server-side callback + value: bytearray = self._on_read( + str(sender.uuid), BlessGATTRequestWinRT((session, request)) + ) + logger.debug(f"Current Characteristic value {value}") value = value if value is not None else b"\x00" writer: DataWriter = DataWriter() @@ -391,7 +424,7 @@ async def f(): request.respond_with_value(writer.detach_buffer()) deferral.complete() - def write_characteristic( + def __on_write( self, sender: GattLocalCharacteristic, args: GattWriteRequestedEventArgs ): """ @@ -409,6 +442,11 @@ def write_characteristic( deferral: Optional[Deferral] = args.get_deferral() if deferral is None: return + + # Get the session + session: GattSession = args.session + + # Get the request request: GattWriteRequest async def f(): @@ -417,6 +455,8 @@ async def f(): asyncio.new_event_loop().run_until_complete(f()) logger.debug("Request value: {}".format(request.value)) + + # extrac the bytarray value reader: Optional[DataReader] = DataReader.from_buffer(request.value) if reader is None: return @@ -425,9 +465,12 @@ async def f(): for n in range(0, n_bytes): next_byte: int = reader.read_byte() value.append(next_byte) - logger.debug("Written Value: {}".format(value)) - self.write_request(str(sender.uuid), value, {}) + + # Pass up to server + self._on_write( + str(sender.uuid), value, BlessGATTRequestWinRT((session, request)) + ) if request.option == GattWriteOption.WRITE_WITH_RESPONSE: request.respond() @@ -435,9 +478,13 @@ async def f(): logger.debug("Write Complete") deferral.complete() - def subscribe_characteristic(self, sender: GattLocalCharacteristic, args: Any): + def __on_subscribe(self, sender: GattLocalCharacteristic, args: Any): """ - Called when a characteristic is subscribed to + Called when a characteristic is subscribed to or unsubscribed from + + Because there is no "unsubscribe", we track when central devices come + and go to determine whether to call upstream subscribe or unsubscribe + callbacks Parameters ---------- @@ -451,38 +498,39 @@ def subscribe_characteristic(self, sender: GattLocalCharacteristic, args: Any): list([]) if sender.subscribed_clients is None else sender.subscribed_clients ) - prev_ids: Set[str] = { - str(client.session.device_id.id) for client in self._subscribed_clients - } + prev_ids: Set[str] = set( + [ + str(client.session.device_id.id) + for client in self._subscribed_clients.get(str(sender.uuid), set()) + ] + ) + new_ids: Set[str] = {str(client.session.device_id.id) for client in new_clients} - # Handle Callbacks - added = new_ids - prev_ids - removed = prev_ids - new_ids - logger.debug(f"Added: {added}") - logger.debug(f"removed: {removed}") - if added: - for cid in added: - self.subscribe_request(str(sender.uuid), {"central_id": cid}) - elif len(new_clients) > len(self._subscribed_clients): - self.subscribe_request(str(sender.uuid)) - - if removed: - for cid in removed: - self.unsubscribe_request(str(sender.uuid), {"central_id": cid}) - elif len(new_clients) < len(self._subscribed_clients): - self.unsubscribe_request(str(sender.uuid)) + # compute added and removed + added_ids: Set[str] = new_ids - prev_ids + removed_ids: Set[str] = prev_ids - new_ids + logger.debug(f"Added: {added_ids}") + logger.debug(f"removed: {removed_ids}") + + # convert to client objects + added_clients: List[GattSubscribedClient] = [ + client + for client in new_clients + if str(client.session.device_id.id) in added_ids + ] + removed_clients: List[GattSubscribedClient] = [ + client + for client in self._subscribed_clients[str(sender.uuid)] + if str(client.session.device_id.id) in removed_ids + ] + + # Handle Subscriptions + for client in added_clients: + self._on_subscribe(str(sender.uuid), BlessGATTSessionWinRT(client)) + + for client in removed_clients: + self._on_unsubscribe(str(sender.uuid), BlessGATTSessionWinRT(client)) # Update Subscribed Clients - self._subscribed_clients = new_clients - - # Process MTU - if self._subscribed_clients: - mtu_values = [ - int(client.max_pdu_size) - for client in self._subscribed_clients - if getattr(client, "max_pdu_size", None) is not None - ] - if mtu_values: - self._mtu = max(mtu_values) - logger.info("New device subscribed") + self._subscribed_clients[str(sender.uuid)] = set(new_clients) diff --git a/bless/backends/winrt/session.py b/bless/backends/winrt/session.py new file mode 100644 index 0000000..5309c88 --- /dev/null +++ b/bless/backends/winrt/session.py @@ -0,0 +1,21 @@ +from typing import cast +from winrt.windows.devices.bluetooth.genericattributeprofile import ( # type: ignore # noqa: E501 + GattSubscribedClient, +) + +from ..session import BlessGATTSession + + +class BlessGATTSessionWinRT(BlessGATTSession): + + @property + def subscribed_client(self) -> GattSubscribedClient: + return cast(GattSubscribedClient, self.obj) + + @property + def central_id(self) -> str: + return self.subscribed_client.session.device_id.id + + @property + def mtu(self) -> int: + return self.subscribed_client.session.max_pdu_size diff --git a/docs/backends/bluezdbus/index.rst b/docs/backends/bluezdbus/index.rst index 115fe6c..f728df7 100644 --- a/docs/backends/bluezdbus/index.rst +++ b/docs/backends/bluezdbus/index.rst @@ -21,3 +21,12 @@ DBus APIs. .. automodule:: bless.backends.bluezdbus.dbus.advertisement :members: + +.. automodule:: bless.backends.bluezdbus.request + :members: + +.. automodule:: bless.backends.bluezdbus.session + :members: + +.. automodule:: bless.backends.bluezdbus.dbus.session + :members: diff --git a/docs/backends/corebluetooth/index.rst b/docs/backends/corebluetooth/index.rst index 0ae37c3..d8498d4 100644 --- a/docs/backends/corebluetooth/index.rst +++ b/docs/backends/corebluetooth/index.rst @@ -15,3 +15,9 @@ Apple's CoreBluetooth APIs. .. automodule:: bless.backends.corebluetooth.descriptor :members: + +.. automodule:: bless.backends.corebluetooth.request + :members: + +.. automodule:: bless.backends.corebluetooth.session + :members: diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 259dc9d..639cb30 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -12,6 +12,8 @@ top-level `bless` package selects the backend based on the current platform. attribute characteristic descriptor + request + session service server bluezdbus/index diff --git a/docs/backends/request.rst b/docs/backends/request.rst new file mode 100644 index 0000000..fe01c0e --- /dev/null +++ b/docs/backends/request.rst @@ -0,0 +1,7 @@ +Requests +======== + +`bless.backends.request` defines the request wrapper used by backends. + +.. automodule:: bless.backends.request + :members: diff --git a/docs/backends/server.rst b/docs/backends/server.rst index 4d54817..331531f 100644 --- a/docs/backends/server.rst +++ b/docs/backends/server.rst @@ -4,6 +4,9 @@ Server Base `bless.backends.server` provides the abstract server interface used by all platform backends. +Callbacks are dispatched in priority order: characteristic-specific handlers, +then server-wide handlers, then default handling when no callbacks are defined. + .. automodule:: bless.backends.server :members: :undoc-members: diff --git a/docs/backends/session.rst b/docs/backends/session.rst new file mode 100644 index 0000000..ea350ca --- /dev/null +++ b/docs/backends/session.rst @@ -0,0 +1,7 @@ +Sessions +======== + +`bless.backends.session` defines the session wrapper used by backends. + +.. automodule:: bless.backends.session + :members: diff --git a/docs/backends/winrt/index.rst b/docs/backends/winrt/index.rst index aa52e0d..1566f64 100644 --- a/docs/backends/winrt/index.rst +++ b/docs/backends/winrt/index.rst @@ -15,3 +15,9 @@ Runtime Bluetooth APIs. .. automodule:: bless.backends.winrt.descriptor :members: + +.. automodule:: bless.backends.winrt.request + :members: + +.. automodule:: bless.backends.winrt.session + :members: diff --git a/docs/usage.rst b/docs/usage.rst index 6baa94d..f62f6e1 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -35,3 +35,15 @@ Create a server, add a GATT tree, then start advertising: loop = asyncio.get_event_loop() loop.run_until_complete(run(loop)) + +Callback Priority +================= + +Bless routes callbacks in the following order: + +1. Characteristic-specific callbacks (for example, `characteristic.on_read`) +2. Server-wide callbacks (for example, `server.on_read`) +3. Default handling if no callbacks are defined + +Default handling currently returns the characteristic value for reads. Writes, +subscriptions, and unsubscriptions are no-ops unless a callback is defined. diff --git a/examples/gattserver.py b/examples/gattserver.py index 6fd3867..b063162 100644 --- a/examples/gattserver.py +++ b/examples/gattserver.py @@ -3,9 +3,9 @@ characteristics """ -import sys -import logging import asyncio +import logging +import sys import threading from typing import Any, Dict, Union @@ -15,24 +15,28 @@ BlessGATTCharacteristic, GATTCharacteristicProperties, GATTAttributePermissions, + BlessGATTRequest, + BlessGATTSession, ) logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(name=__name__) -trigger: Union[asyncio.Event, threading.Event] -if sys.platform in ["darwin", "win32"]: - trigger = threading.Event() -else: - trigger = asyncio.Event() +trigger: Union[asyncio.Event, threading.Event] = ( + threading.Event() if sys.platform in ["darwin", "win32"] else asyncio.Event() +) -def on_read(characteristic: BlessGATTCharacteristic, **kwargs) -> bytearray: +def on_read( + characteristic: BlessGATTCharacteristic, request: BlessGATTRequest +) -> bytearray: logger.debug(f"Reading {characteristic.value}") return characteristic.value -def on_write(characteristic: BlessGATTCharacteristic, value: Any, **kwargs): +def on_write( + characteristic: BlessGATTCharacteristic, value: Any, request: BlessGATTRequest +): characteristic.value = value logger.debug(f"Char value set to {characteristic.value}") if characteristic.value == b"\x0f": @@ -40,14 +44,22 @@ def on_write(characteristic: BlessGATTCharacteristic, value: Any, **kwargs): trigger.set() -def on_subscribe(characteristic: BlessGATTCharacteristic, **kwargs): +def on_subscribe(characteristic: BlessGATTCharacteristic, session: BlessGATTSession): logger.debug(f"Subscribed to {characteristic.uuid}") -def on_unsubscribe(characteristic: BlessGATTCharacteristic, **kwargs): +def on_unsubscribe(characteristic: BlessGATTCharacteristic, session: BlessGATTSession): logger.debug(f"Unsubscribed from {characteristic.uuid}") +# Characteristic-specific handlers +def inc(c: BlessGATTCharacteristic, req: BlessGATTRequest) -> bytearray: + c.value = c.value if c.value is not None else bytearray("\x00") + n: int = int.from_bytes(bytes(c.value)) + c.value = bytearray((n + 1).to_bytes()) + return c.value + + async def run(loop): trigger.clear() @@ -71,16 +83,20 @@ async def run(loop): "bfc0c92f-317d-4ba9-976b-cc11ce77b4ca": { "Properties": GATTCharacteristicProperties.read, "Permissions": GATTAttributePermissions.readable, - "Value": bytearray(b"\x69"), + "Value": None, + "OnRead": inc, } }, } my_service_name = "Test Service" - server = BlessServer(name=my_service_name, loop=loop) - server.on_read = on_read - server.on_write = on_write - server.on_subscribe = on_subscribe - server.on_unsubscribe = on_unsubscribe + server = BlessServer( + name=my_service_name, + loop=loop, + on_read=on_read, + on_write=on_write, + on_subscribe=on_subscribe, + on_unsubscribe=on_unsubscribe, + ) await server.add_gatt(gatt) await server.start() @@ -90,12 +106,14 @@ async def run(loop): "Write '0xF' to the advertised characteristic: " + "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B" ) - if trigger.__module__ == "threading": + if isinstance(trigger, threading.Event): trigger.wait() else: await trigger.wait() + logger.info("Triggered... Waiting 2 seconds") await asyncio.sleep(2) - logger.debug("Updating") + + logger.debug("Updating characteristic") server.get_characteristic("51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B").value = bytearray( b"i" ) diff --git a/requirements.txt b/requirements.txt index 16ffea3..ad96b13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ twisted; platform_system == "Linux" dbus-next; platform_system == "Linux" git+https://github.com/gwangyi/pysetupdi; platform_system == "Windows" pywin32; platform_system == "Windows" +coloredlogs diff --git a/setup.py b/setup.py index f9b5ef2..6ff6b9c 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ include_package_data=True, install_requires=[ "bleak>=1.1.1", # Updated to require Bleak v1.1.1+ + "coloredlogs", "pywin32;platform_system=='Windows'", "dbus_next;platform_system=='Linux'", ( diff --git a/test/backends/test_server.py b/test/backends/test_server.py index bafbe5b..a4d5408 100644 --- a/test/backends/test_server.py +++ b/test/backends/test_server.py @@ -25,6 +25,8 @@ from bless.backends.characteristic import ( # noqa: E402 GATTCharacteristicProperties, ) +from bless.backends.request import BlessGATTRequest +from bless.backends.session import BlessGATTSession hardware_only = pytest.mark.skipif("os.environ.get('TEST_HARDWARE') != '1'") use_encrypted = os.environ.get("TEST_ENCRYPTED") is not None @@ -102,17 +104,29 @@ async def test_server(self): assert server.services[service_uuid].get_characteristic(char_uuid) # Set up read, write, and subscribe callbacks - def read(characteristic: BlessGATTCharacteristic) -> bytearray: + def read( + characteristic: BlessGATTCharacteristic, request: BlessGATTRequest + ) -> bytearray: + print(f"Read request: {request}") return characteristic.value - def write(characteristic: BlessGATTCharacteristic, value: bytearray): + def write( + characteristic: BlessGATTCharacteristic, + value: bytearray, + request: BlessGATTRequest, + ) -> None: + print(f"Write request: {request}") characteristic.value = value # type: ignore - def subscribe(characteristic: BlessGATTCharacteristic) -> None: - print("Subscribed") + def subscribe( + characteristic: BlessGATTCharacteristic, session: BlessGATTSession + ) -> None: + print(f"Subscribed to session: {session}") - def unsubscribe(characteristic: BlessGATTCharacteristic) -> None: - print("Unsubscribed") + def unsubscribe( + characteristic: BlessGATTCharacteristic, session: BlessGATTSession + ) -> None: + print(f"Unsubscribed to session: {session}") server.on_read = read server.on_write = write