Skip to content

Commit 520bed7

Browse files
backends: bluez: use AcquireNotify when possible
Using StartNotify in the BlueZ backend has a similar issue as the CoreBluetooth backend in that we get the same signal that the characteristic value has changed for both notifications and reads and there isn't a way to tell the difference. Using AcquireNotify instead of StartNotify creates a pipe that only receives notifications for a single characteristic. Having this separate communication channel makes it known that the change was a notification and not a read. As a bonus, this should be more performant since it avoids a D-Bus signal on every notification (although we didn't actually measure it).
1 parent ea5fe45 commit 520bed7

File tree

2 files changed

+113
-22
lines changed

2 files changed

+113
-22
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ Added
1717

1818
Changed
1919
-------
20-
* Raise new ``BleakBluetoothNotAvailableError`` when Bluetooth is not supported, turned off or permission is denied.
20+
* Use AcquireNotify rather than StartNotify for Linux backend on supported characteristics
2121
* Allow multiple calls to ``disconnect()`` on Windows to align behaviour over all backends.
22+
* Raise new ``BleakBluetoothNotAvailableError`` when Bluetooth is not supported, turned off or permission is denied.
2223

2324
Fixed
2425
-----

bleak/backends/bluezdbus/client.py

Lines changed: 111 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ def __init__(
103103
self._disconnect_monitor_event: Optional[asyncio.Event] = None
104104
# map of characteristic D-Bus object path to notification callback
105105
self._notification_callbacks: dict[str, NotifyCallback] = {}
106+
# map of characteristic D-Bus path to AcquireNotify file descriptor
107+
self._notification_fds: dict[str, int] = {}
106108

107109
# used to override mtu_size property
108110
self._mtu_size: Optional[int] = None
@@ -904,6 +906,39 @@ async def write_gatt_descriptor(
904906
"Write Descriptor %s | %s: %s", descriptor.handle, descriptor.obj[0], data
905907
)
906908

909+
def _register_notify_fd_reader(
910+
self, char_path: str, fd: int, callback: NotifyCallback
911+
) -> None:
912+
loop = asyncio.get_running_loop()
913+
914+
def on_data():
915+
try:
916+
data = os.read(fd, 1024)
917+
if not data:
918+
raise RuntimeError("Unexpected EOF on notification file handle")
919+
except Exception as e:
920+
logger.debug(
921+
"AcquireNotify: Read error on fd %d: %s. Notifications have been stopped.",
922+
fd,
923+
e,
924+
)
925+
try:
926+
loop.remove_reader(fd)
927+
except RuntimeError:
928+
# Run loop is closed
929+
pass
930+
try:
931+
os.close(fd)
932+
except OSError:
933+
# Bad file descriptor
934+
pass
935+
self._notification_fds.pop(char_path, None)
936+
return
937+
938+
callback(bytearray(data))
939+
940+
loop.add_reader(fd, on_data)
941+
907942
@override
908943
async def start_notify(
909944
self,
@@ -914,20 +949,50 @@ async def start_notify(
914949
"""
915950
Activate notifications/indications on a characteristic.
916951
"""
917-
self._notification_callbacks[characteristic.obj[0]] = callback
918-
919952
assert self._bus is not None
920953

921-
reply = await self._bus.call(
922-
Message(
923-
destination=defs.BLUEZ_SERVICE,
924-
path=characteristic.obj[0],
925-
interface=defs.GATT_CHARACTERISTIC_INTERFACE,
926-
member="StartNotify",
927-
)
954+
# If using StartNotify and calling a read on the same
955+
# characteristic, BlueZ will return the response as
956+
# both a notification and read, duplicating the message.
957+
# Using AcquireNotify on supported characteristics avoids this.
958+
# However, using the preferred AcquireNotify requires that devices
959+
# correctly indicate "notify" and/or "indicate" properties. If they
960+
# don't, we fall back to StartNotify.
961+
use_notify_acquire = "NotifyAcquired" in characteristic.obj[1]
962+
logger.debug(
963+
'using "%s" for notifications on characteristic %d',
964+
"AcquireNotify" if use_notify_acquire else "StartNotify",
965+
characteristic.handle,
928966
)
929-
assert reply
930-
assert_reply(reply)
967+
if use_notify_acquire:
968+
reply = await self._bus.call(
969+
Message(
970+
destination=defs.BLUEZ_SERVICE,
971+
path=characteristic.obj[0],
972+
interface=defs.GATT_CHARACTERISTIC_INTERFACE,
973+
member="AcquireNotify",
974+
body=[{}],
975+
signature="a{sv}",
976+
)
977+
)
978+
assert reply
979+
assert_reply(reply)
980+
981+
unix_fd = reply.unix_fds[0]
982+
self._notification_fds[characteristic.obj[0]] = unix_fd
983+
self._register_notify_fd_reader(characteristic.obj[0], unix_fd, callback)
984+
else:
985+
self._notification_callbacks[characteristic.obj[0]] = callback
986+
reply = await self._bus.call(
987+
Message(
988+
destination=defs.BLUEZ_SERVICE,
989+
path=characteristic.obj[0],
990+
interface=defs.GATT_CHARACTERISTIC_INTERFACE,
991+
member="StartNotify",
992+
)
993+
)
994+
assert reply
995+
assert_reply(reply)
931996

932997
@override
933998
async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None:
@@ -942,15 +1007,40 @@ async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None:
9421007

9431008
assert self._bus is not None
9441009

945-
reply = await self._bus.call(
946-
Message(
947-
destination=defs.BLUEZ_SERVICE,
948-
path=characteristic.obj[0],
949-
interface=defs.GATT_CHARACTERISTIC_INTERFACE,
950-
member="StopNotify",
1010+
if "NotifyAcquired" in characteristic.obj[1]:
1011+
logger.debug(
1012+
"Closing notification fd for characteristic %d", characteristic.handle
9511013
)
952-
)
953-
assert reply
954-
assert_reply(reply)
1014+
fd = self._notification_fds.pop(characteristic.obj[0], None)
9551015

956-
self._notification_callbacks.pop(characteristic.obj[0], None)
1016+
if fd is None:
1017+
logger.debug(
1018+
"No notification fd found for characteristic %d",
1019+
characteristic.handle,
1020+
)
1021+
else:
1022+
loop = asyncio.get_running_loop()
1023+
try:
1024+
loop.remove_reader(fd)
1025+
except RuntimeError:
1026+
# Run loop is closed
1027+
pass
1028+
try:
1029+
os.close(fd)
1030+
except OSError as e:
1031+
logger.debug("Failed to remove file descriptor %d: %s", fd, e)
1032+
else:
1033+
logger.debug(
1034+
"Calling StopNotify for characteristic %d", characteristic.handle
1035+
)
1036+
reply = await self._bus.call(
1037+
Message(
1038+
destination=defs.BLUEZ_SERVICE,
1039+
path=characteristic.obj[0],
1040+
interface=defs.GATT_CHARACTERISTIC_INTERFACE,
1041+
member="StopNotify",
1042+
)
1043+
)
1044+
assert reply
1045+
assert_reply(reply)
1046+
self._notification_callbacks.pop(characteristic.obj[0], None)

0 commit comments

Comments
 (0)