@@ -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