Skip to content

Commit

Permalink
Add prefer_notify option to gatt_client subscribe()
Browse files Browse the repository at this point in the history
If characteristic supports Notify and Indicate, the prefer_notify option
will subscribe with Notify if True or Indicate if False.

If characteristic only supports one property, Notify or Indicate, that
mode will be selected, regardless of the prefer_notify setting.

Tested with a characteristic that supports both Notify and Indicate and
verified that prefer_notify sets the desired mode.
  • Loading branch information
mogenson committed Nov 11, 2022
1 parent ee54df2 commit c51a4b1
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 34 deletions.
4 changes: 2 additions & 2 deletions bumble/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,8 @@ async def discover_descriptors(self, characteristic = None, start_handle = None,
async def discover_attributes(self):
return await self.gatt_client.discover_attributes()

async def subscribe(self, characteristic, subscriber=None):
return await self.gatt_client.subscribe(characteristic, subscriber)
async def subscribe(self, characteristic, subscriber=None, prefer_notify=True):
return await self.gatt_client.subscribe(characteristic, subscriber, prefer_notify)

async def unsubscribe(self, characteristic, subscriber=None):
return await self.gatt_client.unsubscribe(characteristic, subscriber)
Expand Down
63 changes: 34 additions & 29 deletions bumble/gatt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,17 @@
import asyncio
import logging
import struct

from colors import color

from .core import ProtocolError, TimeoutError
from .hci import *
from .att import *
from .gatt import (
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
GATT_REQUEST_TIMEOUT,
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
Characteristic,
ClientCharacteristicConfigurationBits
)
from .core import InvalidStateError, ProtocolError, TimeoutError
from .gatt import (GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE, GATT_REQUEST_TIMEOUT,
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE, Characteristic,
ClientCharacteristicConfigurationBits)
from .hci import *

# -----------------------------------------------------------------------------
# Logging
Expand Down Expand Up @@ -115,7 +112,7 @@ def get_descriptor(self, descriptor_type):
async def discover_descriptors(self):
return await self.client.discover_descriptors(self)

async def subscribe(self, subscriber=None):
async def subscribe(self, subscriber=None, prefer_notify=True):
if subscriber is not None:
if subscriber in self.subscribers:
# We already have a proxy subscriber
Expand All @@ -129,7 +126,7 @@ def on_change(value):
self.subscribers[subscriber] = on_change
subscriber = on_change

return await self.client.subscribe(self, subscriber)
return await self.client.subscribe(self, subscriber, prefer_notify)

async def unsubscribe(self, subscriber=None):
if subscriber in self.subscribers:
Expand Down Expand Up @@ -547,7 +544,7 @@ async def discover_attributes(self):

return attributes

async def subscribe(self, characteristic, subscriber=None):
async def subscribe(self, characteristic, subscriber=None, prefer_notify=True):
# If we haven't already discovered the descriptors for this characteristic, do it now
if not characteristic.descriptors_discovered:
await self.discover_descriptors(characteristic)
Expand All @@ -558,23 +555,31 @@ async def subscribe(self, characteristic, subscriber=None):
logger.warning('subscribing to characteristic with no CCCD descriptor')
return

# Set the subscription bits and select the subscriber set
bits = ClientCharacteristicConfigurationBits.DEFAULT
subscriber_sets = []
if characteristic.properties & Characteristic.NOTIFY:
bits |= ClientCharacteristicConfigurationBits.NOTIFICATION
subscriber_sets.append(self.notification_subscribers.setdefault(characteristic.handle, set()))
if characteristic.properties & Characteristic.INDICATE:
bits |= ClientCharacteristicConfigurationBits.INDICATION
subscriber_sets.append(self.indication_subscribers.setdefault(characteristic.handle, set()))
def characteristic_is_notify(characteristic, prefer_notify):
if (
characteristic.properties & (Characteristic.NOTIFY | Characteristic.INDICATE)
) == (Characteristic.NOTIFY | Characteristic.INDICATE):
return True if prefer_notify else False
elif characteristic.properties & Characteristic.NOTIFY:
return True
elif characteristic.properties & Characteristic.INDICATE:
return False
else:
raise InvalidStateError('characteristic is not notify or indicate')

bits, subscribers = (
(ClientCharacteristicConfigurationBits.NOTIFICATION, self.notification_subscribers)
if characteristic_is_notify(characteristic, prefer_notify)
else (ClientCharacteristicConfigurationBits.INDICATION, self.indication_subscribers)
)

# Add subscribers to the sets
for subscriber_set in subscriber_sets:
if subscriber is not None:
subscriber_set.add(subscriber)
# Add the characteristic as a subscriber, which will result in the characteristic
# emitting an 'update' event when a notification or indication is received
subscriber_set.add(characteristic)
subscriber_set = subscribers.setdefault(characteristic.handle, set())
if subscriber is not None:
subscriber_set.add(subscriber)
# Add the characteristic as a subscriber, which will result in the characteristic
# emitting an 'update' event when a notification or indication is received
subscriber_set.add(characteristic)

await self.write_value(cccd, struct.pack('<H', bits), with_response=True)

Expand Down
28 changes: 25 additions & 3 deletions tests/gatt_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,14 +612,25 @@ def on_c2_update(value):
await async_barrier()
assert not c2._called

c3._called = False
c3._called_2 = False
c3._called_3 = False
c3._last_update = None
c3._last_update_2 = None
c3._last_update_3 = None

def on_c3_update(value):
c3._called = True
c3._last_update = value

def on_c3_update_2(value):
def on_c3_update_2(value): # for notify
c3._called_2 = True
c3._last_update_2 = value

def on_c3_update_3(value): # for indicate
c3._called_3 = True
c3._last_update_3 = value

c3.on('update', on_c3_update)
await peer.subscribe(c3, on_c3_update_2)
await async_barrier()
Expand All @@ -629,22 +640,33 @@ def on_c3_update_2(value):
assert c3._last_update == characteristic3.value
assert c3._called_2
assert c3._last_update_2 == characteristic3.value
assert not c3._called_3

c3._called = False
c3._called_2 = False
c3._called_3 = False
await peer.unsubscribe(c3)
await peer.subscribe(c3, on_c3_update_3, prefer_notify=False)
await async_barrier()
characteristic3.value = bytes([1, 2, 3])
await server.indicate_subscriber(characteristic3._last_subscription[0], characteristic3)
await async_barrier()
assert c3._called
assert c3._last_update == characteristic3.value
assert c3._called_2
assert c3._last_update_2 == characteristic3.value
assert not c3._called_2
assert c3._called_3
assert c3._last_update_3 == characteristic3.value

c3._called = False
c3._called_2 = False
c3._called_3 = False
await peer.unsubscribe(c3)
await server.notify_subscriber(characteristic3._last_subscription[0], characteristic3)
await server.indicate_subscriber(characteristic3._last_subscription[0], characteristic3)
await async_barrier()
assert not c3._called
assert not c3._called_2
assert not c3._called_3


# -----------------------------------------------------------------------------
Expand Down

0 comments on commit c51a4b1

Please sign in to comment.