Skip to content

Commit

Permalink
BREAKING: change notification callback argument
Browse files Browse the repository at this point in the history
This is a breaking change that changes the first argument of the
notification callback from the handle to the BleakGATTCharacteristic
object.

This has been a commonly reported problem by users (so much so it is
in our troubleshooting guide which can now be removed) and #759 has
received positive feedback for the breaking change.

It is likely that most users don't use the first argument anyway, so
in those cases, this won't actually be a breaking change.

Fixes: #759
  • Loading branch information
dlech committed Sep 22, 2022
1 parent 548166f commit be97338
Show file tree
Hide file tree
Showing 13 changed files with 50 additions and 76 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
25 changes: 14 additions & 11 deletions bleak/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
__email__ = "henrik.blidh@gmail.com"

import asyncio
import functools
import inspect
import logging
import os
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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(
Expand Down
11 changes: 6 additions & 5 deletions bleak/backends/bluezdbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bleak/backends/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 6 additions & 4 deletions bleak/backends/corebluetooth/PeripheralDelegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,6 +23,7 @@
)

from ...exc import BleakError
from ..client import NotifyCallback

# logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -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] = {}

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion bleak/backends/p4android/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
27 changes: 11 additions & 16 deletions bleak/backends/winrt/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -36,6 +35,7 @@
GattSession,
GattSessionStatus,
GattSessionStatusChangedEventArgs,
GattValueChangedEventArgs,
GattWriteOption,
)
from bleak_winrt.windows.devices.enumeration import (
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
30 changes: 0 additions & 30 deletions docs/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------------------
Expand Down
2 changes: 1 addition & 1 deletion examples/async_callback_with_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions examples/enable_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions examples/sensortag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
4 changes: 2 additions & 2 deletions examples/two_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
3 changes: 2 additions & 1 deletion examples/uart_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit be97338

Please sign in to comment.