Skip to content

Commit

Permalink
Merge pull request #387 from google/gbg/async-gatt-server
Browse files Browse the repository at this point in the history
support async read/write for characteristic values
  • Loading branch information
barbibulle committed Dec 29, 2023
2 parents 6810865 + f2925ca commit 09e5ea5
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 129 deletions.
8 changes: 4 additions & 4 deletions apps/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,7 @@ async def do_show_local_values(self):
if not service:
continue
values = [
attribute.read_value(connection)
await attribute.read_value(connection)
for connection in self.device.connections.values()
]
if not values:
Expand All @@ -796,11 +796,11 @@ async def do_show_local_values(self):
if not characteristic:
continue
values = [
attribute.read_value(connection)
await attribute.read_value(connection)
for connection in self.device.connections.values()
]
if not values:
values = [attribute.read_value(None)]
values = [await attribute.read_value(None)]

# TODO: future optimization: convert CCCD value to human readable string

Expand Down Expand Up @@ -944,7 +944,7 @@ async def do_local_write(self, params):

# send data to any subscribers
if isinstance(attribute, Characteristic):
attribute.write_value(None, value)
await attribute.write_value(None, value)
if attribute.has_properties(Characteristic.NOTIFY):
await self.device.gatt_server.notify_subscribers(attribute)
if attribute.has_properties(Characteristic.INDICATE):
Expand Down
64 changes: 53 additions & 11 deletions bumble/att.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,21 @@
from __future__ import annotations
import enum
import functools
import inspect
import struct
from typing import (
Any,
Awaitable,
Callable,
Dict,
List,
Optional,
Type,
Union,
TYPE_CHECKING,
)

from pyee import EventEmitter
from typing import Dict, Type, List, Protocol, Union, Optional, Any, TYPE_CHECKING

from bumble.core import UUID, name_or_number, ProtocolError
from bumble.hci import HCI_Object, key_with_value
Expand Down Expand Up @@ -722,12 +734,38 @@ class ATT_Handle_Value_Confirmation(ATT_PDU):


# -----------------------------------------------------------------------------
class ConnectionValue(Protocol):
def read(self, connection) -> bytes:
...
class AttributeValue:
'''
Attribute value where reading and/or writing is delegated to functions
passed as arguments to the constructor.
'''

def __init__(
self,
read: Union[
Callable[[Optional[Connection]], bytes],
Callable[[Optional[Connection]], Awaitable[bytes]],
None,
] = None,
write: Union[
Callable[[Optional[Connection], bytes], None],
Callable[[Optional[Connection], bytes], Awaitable[None]],
None,
] = None,
):
self._read = read
self._write = write

def read(self, connection: Optional[Connection]) -> Union[bytes, Awaitable[bytes]]:
return self._read(connection) if self._read else b''

def write(
self, connection: Optional[Connection], value: bytes
) -> Union[Awaitable[None], None]:
if self._write:
return self._write(connection, value)

def write(self, connection, value: bytes) -> None:
...
return None


# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -770,13 +808,13 @@ def from_string(cls, permissions_str: str) -> Attribute.Permissions:
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION

value: Union[str, bytes, ConnectionValue]
value: Union[bytes, AttributeValue]

def __init__(
self,
attribute_type: Union[str, bytes, UUID],
permissions: Union[str, Attribute.Permissions],
value: Union[str, bytes, ConnectionValue] = b'',
value: Union[str, bytes, AttributeValue] = b'',
) -> None:
EventEmitter.__init__(self)
self.handle = 0
Expand Down Expand Up @@ -806,7 +844,7 @@ def encode_value(self, value: Any) -> bytes:
def decode_value(self, value_bytes: bytes) -> Any:
return value_bytes

def read_value(self, connection: Optional[Connection]) -> bytes:
async def read_value(self, connection: Optional[Connection]) -> bytes:
if (
(self.permissions & self.READ_REQUIRES_ENCRYPTION)
and connection is not None
Expand All @@ -832,6 +870,8 @@ def read_value(self, connection: Optional[Connection]) -> bytes:
if hasattr(self.value, 'read'):
try:
value = self.value.read(connection)
if inspect.isawaitable(value):
value = await value
except ATT_Error as error:
raise ATT_Error(
error_code=error.error_code, att_handle=self.handle
Expand All @@ -841,7 +881,7 @@ def read_value(self, connection: Optional[Connection]) -> bytes:

return self.encode_value(value)

def write_value(self, connection: Connection, value_bytes: bytes) -> None:
async def write_value(self, connection: Connection, value_bytes: bytes) -> None:
if (
self.permissions & self.WRITE_REQUIRES_ENCRYPTION
) and not connection.encryption:
Expand All @@ -864,7 +904,9 @@ def write_value(self, connection: Connection, value_bytes: bytes) -> None:

if hasattr(self.value, 'write'):
try:
self.value.write(connection, value) # pylint: disable=not-callable
result = self.value.write(connection, value)
if inspect.isawaitable(result):
await result
except ATT_Error as error:
raise ATT_Error(
error_code=error.error_code, att_handle=self.handle
Expand Down
110 changes: 61 additions & 49 deletions bumble/gatt.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,28 @@
# Imports
# -----------------------------------------------------------------------------
from __future__ import annotations
import asyncio
import enum
import functools
import logging
import struct
from typing import Optional, Sequence, Iterable, List, Union

from .colors import color
from .core import UUID, get_dict_key_by_value
from .att import Attribute
from typing import (
Callable,
Dict,
Iterable,
List,
Optional,
Sequence,
Union,
TYPE_CHECKING,
)

from bumble.colors import color
from bumble.core import UUID
from bumble.att import Attribute, AttributeValue

if TYPE_CHECKING:
from bumble.gatt_client import AttributeProxy
from bumble.device import Connection


# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -522,56 +534,43 @@ def __str__(self) -> str:


# -----------------------------------------------------------------------------
class CharacteristicValue:
'''
Characteristic value where reading and/or writing is delegated to functions
passed as arguments to the constructor.
'''

def __init__(self, read=None, write=None):
self._read = read
self._write = write

def read(self, connection):
return self._read(connection) if self._read else b''

def write(self, connection, value):
if self._write:
self._write(connection, value)
class CharacteristicValue(AttributeValue):
"""Same as AttributeValue, for backward compatibility"""


# -----------------------------------------------------------------------------
class CharacteristicAdapter:
'''
An adapter that can adapt any object with `read_value` and `write_value`
methods (like Characteristic and CharacteristicProxy objects) by wrapping
those methods with ones that return/accept encoded/decoded values.
Objects with async methods are considered proxies, so the adaptation is one
where the return value of `read_value` is decoded and the value passed to
`write_value` is encoded. Other objects are considered local characteristics
so the adaptation is one where the return value of `read_value` is encoded
and the value passed to `write_value` is decoded.
If the characteristic has a `subscribe` method, it is wrapped with one where
the values are decoded before being passed to the subscriber.
An adapter that can adapt Characteristic and AttributeProxy objects
by wrapping their `read_value()` and `write_value()` methods with ones that
return/accept encoded/decoded values.
For proxies (i.e used by a GATT client), the adaptation is one where the return
value of `read_value()` is decoded and the value passed to `write_value()` is
encoded. The `subscribe()` method, is wrapped with one where the values are decoded
before being passed to the subscriber.
For local values (i.e hosted by a GATT server) the adaptation is one where the
return value of `read_value()` is encoded and the value passed to `write_value()`
is decoded.
'''

def __init__(self, characteristic):
read_value: Callable
write_value: Callable

def __init__(self, characteristic: Union[Characteristic, AttributeProxy]):
self.wrapped_characteristic = characteristic
self.subscribers = {} # Map from subscriber to proxy subscriber
self.subscribers: Dict[
Callable, Callable
] = {} # Map from subscriber to proxy subscriber

if asyncio.iscoroutinefunction(
characteristic.read_value
) and asyncio.iscoroutinefunction(characteristic.write_value):
self.read_value = self.read_decoded_value
self.write_value = self.write_decoded_value
else:
if isinstance(characteristic, Characteristic):
self.read_value = self.read_encoded_value
self.write_value = self.write_encoded_value

if hasattr(self.wrapped_characteristic, 'subscribe'):
else:
self.read_value = self.read_decoded_value
self.write_value = self.write_decoded_value
self.subscribe = self.wrapped_subscribe

if hasattr(self.wrapped_characteristic, 'unsubscribe'):
self.unsubscribe = self.wrapped_unsubscribe

def __getattr__(self, name):
Expand All @@ -590,11 +589,13 @@ def __setattr__(self, name, value):
else:
setattr(self.wrapped_characteristic, name, value)

def read_encoded_value(self, connection):
return self.encode_value(self.wrapped_characteristic.read_value(connection))
async def read_encoded_value(self, connection):
return self.encode_value(
await self.wrapped_characteristic.read_value(connection)
)

def write_encoded_value(self, connection, value):
return self.wrapped_characteristic.write_value(
async def write_encoded_value(self, connection, value):
return await self.wrapped_characteristic.write_value(
connection, self.decode_value(value)
)

Expand Down Expand Up @@ -729,13 +730,24 @@ class Descriptor(Attribute):
'''

def __str__(self) -> str:
if isinstance(self.value, bytes):
value_str = self.value.hex()
elif isinstance(self.value, CharacteristicValue):
value = self.value.read(None)
if isinstance(value, bytes):
value_str = value.hex()
else:
value_str = '<async>'
else:
value_str = '<...>'
return (
f'Descriptor(handle=0x{self.handle:04X}, '
f'type={self.type}, '
f'value={self.read_value(None).hex()})'
f'value={value_str})'
)


# -----------------------------------------------------------------------------
class ClientCharacteristicConfigurationBits(enum.IntFlag):
'''
See Vol 3, Part G - 3.3.3.3 - Table 3.11 Client Characteristic Configuration bit
Expand Down

0 comments on commit 09e5ea5

Please sign in to comment.