diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dd0423c6..4bbc5ed4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,7 @@ Changed * Deprecated ``BleakClient.set_disconnected_callback()``. * Deprecated ``BleakClient.get_services()``. * Refactored common code in ``BleakClient.start_notify()``. +* (BREAKING) Changed notification callback argument from ``int`` to ``BleakGattCharacteristic``. Fixes #759. Fixed ----- diff --git a/bleak/__init__.py b/bleak/__init__.py index 22f1f98c..95a2d3b1 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -8,6 +8,7 @@ __email__ = "henrik.blidh@gmail.com" import asyncio +import functools import inspect import logging import os @@ -492,14 +493,16 @@ async def write_gatt_char( async def start_notify( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - callback: Callable[[int, bytearray], Union[None, Awaitable[None]]], + callback: Callable[ + [BleakGATTCharacteristic, bytearray], Union[None, Awaitable[None]] + ], **kwargs, ) -> None: """ Activate notifications/indications on a characteristic. - Callbacks must accept two inputs. The first will be a integer handle of - the characteristic generating the data and the second will be a ``bytearray``. + Callbacks must accept two inputs. The first will be the characteristic + and the second will be a ``bytearray`` containing the data received. .. code-block:: python @@ -521,14 +524,6 @@ def callback(sender: int, data: bytearray): if not self.is_connected: raise BleakError("Not connected") - if inspect.iscoroutinefunction(callback): - - def wrapped_callback(s, d): - asyncio.ensure_future(callback(s, d)) - - else: - wrapped_callback = callback - if not isinstance(char_specifier, BleakGATTCharacteristic): characteristic = self.services.get_characteristic(char_specifier) else: @@ -537,6 +532,14 @@ def wrapped_callback(s, d): if not characteristic: raise BleakError(f"Characteristic {char_specifier} not found!") + if inspect.iscoroutinefunction(callback): + + def wrapped_callback(data): + asyncio.ensure_future(callback(characteristic, data)) + + else: + wrapped_callback = functools.partial(callback, characteristic) + await self._backend.start_notify(characteristic, wrapped_callback, **kwargs) async def stop_notify( diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 19da9972..3d40779b 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -25,7 +25,7 @@ from .characteristic import BleakGATTCharacteristicBlueZDBus from .manager import get_global_bluez_manager from .scanner import BleakScannerBlueZDBus -from .utils import assert_reply, extract_service_handle_from_path +from .utils import assert_reply from .version import BlueZFeatures logger = logging.getLogger(__name__) @@ -71,7 +71,7 @@ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): # used to ensure device gets disconnected if event loop crashes self._disconnect_monitor_event: Optional[asyncio.Event] = None # map of characteristic D-Bus object path to notification callback - self._notification_callbacks: Dict[str, Callable] = {} + self._notification_callbacks: Dict[str, NotifyCallback] = {} # used to override mtu_size property self._mtu_size: Optional[int] = None @@ -146,9 +146,10 @@ def on_connected_changed(connected: bool) -> None: disconnecting_event.set() def on_value_changed(char_path: str, value: bytes) -> None: - if char_path in self._notification_callbacks: - handle = extract_service_handle_from_path(char_path) - self._notification_callbacks[char_path](handle, bytearray(value)) + callback = self._notification_callbacks.get(char_path) + + if callback: + callback(bytearray(value)) watcher = manager.add_device_watcher( self._device_path, on_connected_changed, on_value_changed diff --git a/bleak/backends/client.py b/bleak/backends/client.py index b9395dc7..654ebb7f 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -18,7 +18,7 @@ from .characteristic import BleakGATTCharacteristic from .device import BLEDevice -NotifyCallback = Callable[[int, bytearray], None] +NotifyCallback = Callable[[bytearray], None] class BaseBleakClient(abc.ABC): diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index a6bdf870..3fa90c02 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -9,7 +9,7 @@ import asyncio import itertools import logging -from typing import Callable, Any, Dict, Iterable, NewType, Optional +from typing import Any, Dict, Iterable, NewType, Optional import async_timeout import objc @@ -23,6 +23,7 @@ ) from ...exc import BleakError +from ..client import NotifyCallback # logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) @@ -60,7 +61,7 @@ def initWithPeripheral_(self, peripheral: CBPeripheral): self._descriptor_write_futures: Dict[int, asyncio.Future] = {} self._characteristic_notify_change_futures: Dict[int, asyncio.Future] = {} - self._characteristic_notify_callbacks: Dict[int, Callable[[str, Any], Any]] = {} + self._characteristic_notify_callbacks: Dict[int, NotifyCallback] = {} self._read_rssi_futures: Dict[NSUUID, asyncio.Future] = {} @@ -204,7 +205,7 @@ async def write_descriptor(self, descriptor: CBDescriptor, value: NSData) -> Non @objc.python_method async def start_notifications( - self, characteristic: CBCharacteristic, callback: Callable[[str, Any], Any] + self, characteristic: CBCharacteristic, callback: NotifyCallback ) -> None: c_handle = characteristic.handle() if c_handle in self._characteristic_notify_callbacks: @@ -366,8 +367,9 @@ def did_update_value_for_characteristic( if not future: if error is None: notify_callback = self._characteristic_notify_callbacks.get(c_handle) + if notify_callback: - notify_callback(c_handle, bytearray(value)) + notify_callback(bytearray(value)) return if error is not None: diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index 1969b393..3a19a183 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -552,7 +552,7 @@ def onServicesDiscovered(self, status): @java_method("(I[B)V") def onCharacteristicChanged(self, handle, value): self._loop.call_soon_threadsafe( - self._client._subscriptions[handle], handle, bytearray(value.tolist()) + self._client._subscriptions[handle], bytearray(value.tolist()) ) @java_method("(II[B)V") diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 5de34530..06cb29ae 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -10,8 +10,7 @@ import sys import uuid import warnings -from functools import wraps -from typing import Any, Callable, Dict, List, Optional, Sequence, Union, cast +from typing import Any, Dict, List, Optional, Sequence, Union, cast import async_timeout @@ -36,6 +35,7 @@ GattSession, GattSessionStatus, GattSessionStatusChangedEventArgs, + GattValueChangedEventArgs, GattWriteOption, ) from bleak_winrt.windows.devices.enumeration import ( @@ -763,8 +763,15 @@ async def start_notify( "characteristic does not support notifications or indications" ) - fcn = _notification_wrapper(callback, asyncio.get_running_loop()) - event_handler_token = winrt_char.add_value_changed(fcn) + loop = asyncio.get_running_loop() + + def handle_value_changed( + sender: GattCharacteristic, args: GattValueChangedEventArgs + ): + value = bytearray(args.characteristic_value) + return loop.call_soon_threadsafe(callback, value) + + event_handler_token = winrt_char.add_value_changed(handle_value_changed) self._notification_callbacks[characteristic.handle] = event_handler_token try: @@ -817,15 +824,3 @@ async def stop_notify( event_handler_token = self._notification_callbacks.pop(characteristic.handle) characteristic.obj.remove_value_changed(event_handler_token) - - -def _notification_wrapper(func: Callable, loop: asyncio.AbstractEventLoop): - @wraps(func) - def notification_parser(sender: Any, args: Any): - # Return only the UUID string representation as sender. - # Also do a conversion from System.Bytes[] to bytearray. - value = bytearray(args.characteristic_value) - - return loop.call_soon_threadsafe(func, sender.attribute_handle, value) - - return notification_parser diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index ceac2a66..6fb71296 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -109,36 +109,6 @@ Python:: asyncio.run(find_all_devices_services()) ------------------------------------------ -Pass more parameters to a notify callback ------------------------------------------ - -If you need a way to pass more parameters to the notify callback, please use -``functools.partial`` to pass in more arguments. - -Issue #759 might fix this in the future. - -Python:: - - from functools import partial - - from bleak import BleakClient - - - def my_notification_callback_with_client_input( - client: BleakClient, sender: int, data: bytearray - ): - """Notification callback with client awareness""" - print( - f"Notification from device with address {client.address} and characteristic with handle {client.services.get_characteristic(sender)}. Data: {data}" - ) - - # [...] - - await client.start_notify( - char_specifier, partial(my_notification_callback_with_client_input, client) - ) - ------------------------- Capture Bluetooth Traffic ------------------------- diff --git a/examples/async_callback_with_queue.py b/examples/async_callback_with_queue.py index 4b64d168..bc241048 100644 --- a/examples/async_callback_with_queue.py +++ b/examples/async_callback_with_queue.py @@ -28,7 +28,7 @@ async def run_ble_client(address: str, char_uuid: str, queue: asyncio.Queue): - async def callback_handler(sender, data): + async def callback_handler(_, data): await queue.put((time.time(), data)) async with BleakClient(address) as client: diff --git a/examples/enable_notifications.py b/examples/enable_notifications.py index f02e8b75..ce293f78 100644 --- a/examples/enable_notifications.py +++ b/examples/enable_notifications.py @@ -14,6 +14,7 @@ import platform from bleak import BleakClient +from bleak.backends.characteristic import BleakGATTCharacteristic # you can change these to match your device or override them from the command line @@ -25,9 +26,9 @@ ) -def notification_handler(sender, data): +def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray): """Simple notification handler which prints the data received.""" - print("{0}: {1}".format(sender, data)) + print(f"{characteristic.description}: {data}") async def main(address, char_uuid): diff --git a/examples/sensortag.py b/examples/sensortag.py index fc2b68a4..40333ef9 100644 --- a/examples/sensortag.py +++ b/examples/sensortag.py @@ -132,8 +132,8 @@ async def main(address): battery_level = await client.read_gatt_char(BATTERY_LEVEL_UUID) print("Battery Level: {0}%".format(int(battery_level[0]))) - async def notification_handler(sender, data): - print("{0}: {1}".format(sender, data)) + async def notification_handler(characteristic, data): + print(f"{characteristic.description}: {data}") # Turn on the red light on the Sensor Tag by writing to I/O Data and I/O Config. write_value = bytearray([0x01]) diff --git a/examples/two_devices.py b/examples/two_devices.py index e0fcb198..59bd524d 100644 --- a/examples/two_devices.py +++ b/examples/two_devices.py @@ -8,8 +8,8 @@ notify_uuid = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0xFFE1) -def callback(sender, data): - print(sender, data) +def callback(characteristic, data): + print(characteristic, data) async def connect_to_device(address): diff --git a/examples/uart_service.py b/examples/uart_service.py index 8d71f12c..37dea7e7 100644 --- a/examples/uart_service.py +++ b/examples/uart_service.py @@ -13,6 +13,7 @@ from typing import Iterator from bleak import BleakClient, BleakScanner +from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData @@ -57,7 +58,7 @@ def handle_disconnect(_: BleakClient): for task in asyncio.all_tasks(): task.cancel() - def handle_rx(_: int, data: bytearray): + def handle_rx(_: BleakGATTCharacteristic, data: bytearray): print("received:", data) async with BleakClient(device, disconnected_callback=handle_disconnect) as client: