forked from auracaster/bumble_mirror
Merge pull request #598 from google/gbg/gatt-class-adapter
Add a class-based GATT adapter
This commit is contained in:
@@ -757,13 +757,13 @@ class AttributeValue:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
read: Union[
|
read: Union[
|
||||||
Callable[[Optional[Connection]], bytes],
|
Callable[[Optional[Connection]], Any],
|
||||||
Callable[[Optional[Connection]], Awaitable[bytes]],
|
Callable[[Optional[Connection]], Awaitable[Any]],
|
||||||
None,
|
None,
|
||||||
] = None,
|
] = None,
|
||||||
write: Union[
|
write: Union[
|
||||||
Callable[[Optional[Connection], bytes], None],
|
Callable[[Optional[Connection], Any], None],
|
||||||
Callable[[Optional[Connection], bytes], Awaitable[None]],
|
Callable[[Optional[Connection], Any], Awaitable[None]],
|
||||||
None,
|
None,
|
||||||
] = None,
|
] = None,
|
||||||
):
|
):
|
||||||
@@ -822,13 +822,13 @@ class Attribute(EventEmitter):
|
|||||||
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
|
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
|
||||||
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
|
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
|
||||||
|
|
||||||
value: Union[bytes, AttributeValue]
|
value: Any
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
attribute_type: Union[str, bytes, UUID],
|
attribute_type: Union[str, bytes, UUID],
|
||||||
permissions: Union[str, Attribute.Permissions],
|
permissions: Union[str, Attribute.Permissions],
|
||||||
value: Union[str, bytes, AttributeValue] = b'',
|
value: Any = b'',
|
||||||
) -> None:
|
) -> None:
|
||||||
EventEmitter.__init__(self)
|
EventEmitter.__init__(self)
|
||||||
self.handle = 0
|
self.handle = 0
|
||||||
@@ -846,10 +846,6 @@ class Attribute(EventEmitter):
|
|||||||
else:
|
else:
|
||||||
self.type = attribute_type
|
self.type = attribute_type
|
||||||
|
|
||||||
# Convert the value to a byte array
|
|
||||||
if isinstance(value, str):
|
|
||||||
self.value = bytes(value, 'utf-8')
|
|
||||||
else:
|
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
def encode_value(self, value: Any) -> bytes:
|
def encode_value(self, value: Any) -> bytes:
|
||||||
@@ -893,6 +889,8 @@ class Attribute(EventEmitter):
|
|||||||
else:
|
else:
|
||||||
value = self.value
|
value = self.value
|
||||||
|
|
||||||
|
self.emit('read', connection, value)
|
||||||
|
|
||||||
return self.encode_value(value)
|
return self.encode_value(value)
|
||||||
|
|
||||||
async def write_value(self, connection: Connection, value_bytes: bytes) -> None:
|
async def write_value(self, connection: Connection, value_bytes: bytes) -> None:
|
||||||
|
|||||||
@@ -28,12 +28,15 @@ import functools
|
|||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
from typing import (
|
from typing import (
|
||||||
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
Dict,
|
Dict,
|
||||||
Iterable,
|
Iterable,
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
Sequence,
|
Sequence,
|
||||||
|
SupportsBytes,
|
||||||
|
Type,
|
||||||
Union,
|
Union,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
@@ -41,6 +44,7 @@ from typing import (
|
|||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.core import BaseBumbleError, UUID
|
from bumble.core import BaseBumbleError, UUID
|
||||||
from bumble.att import Attribute, AttributeValue
|
from bumble.att import Attribute, AttributeValue
|
||||||
|
from bumble.utils import ByteSerializable
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bumble.gatt_client import AttributeProxy
|
from bumble.gatt_client import AttributeProxy
|
||||||
@@ -343,7 +347,7 @@ class Service(Attribute):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
uuid: Union[str, UUID],
|
uuid: Union[str, UUID],
|
||||||
characteristics: List[Characteristic],
|
characteristics: Iterable[Characteristic],
|
||||||
primary=True,
|
primary=True,
|
||||||
included_services: Iterable[Service] = (),
|
included_services: Iterable[Service] = (),
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -362,7 +366,7 @@ class Service(Attribute):
|
|||||||
)
|
)
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.included_services = list(included_services)
|
self.included_services = list(included_services)
|
||||||
self.characteristics = characteristics[:]
|
self.characteristics = list(characteristics)
|
||||||
self.primary = primary
|
self.primary = primary
|
||||||
|
|
||||||
def get_advertising_data(self) -> Optional[bytes]:
|
def get_advertising_data(self) -> Optional[bytes]:
|
||||||
@@ -393,7 +397,7 @@ class TemplateService(Service):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
characteristics: List[Characteristic],
|
characteristics: Iterable[Characteristic],
|
||||||
primary: bool = True,
|
primary: bool = True,
|
||||||
included_services: Iterable[Service] = (),
|
included_services: Iterable[Service] = (),
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -490,7 +494,7 @@ class Characteristic(Attribute):
|
|||||||
uuid: Union[str, bytes, UUID],
|
uuid: Union[str, bytes, UUID],
|
||||||
properties: Characteristic.Properties,
|
properties: Characteristic.Properties,
|
||||||
permissions: Union[str, Attribute.Permissions],
|
permissions: Union[str, Attribute.Permissions],
|
||||||
value: Union[str, bytes, CharacteristicValue] = b'',
|
value: Any = b'',
|
||||||
descriptors: Sequence[Descriptor] = (),
|
descriptors: Sequence[Descriptor] = (),
|
||||||
):
|
):
|
||||||
super().__init__(uuid, permissions, value)
|
super().__init__(uuid, permissions, value)
|
||||||
@@ -525,7 +529,11 @@ class CharacteristicDeclaration(Attribute):
|
|||||||
|
|
||||||
characteristic: Characteristic
|
characteristic: Characteristic
|
||||||
|
|
||||||
def __init__(self, characteristic: Characteristic, value_handle: int) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
characteristic: Characteristic,
|
||||||
|
value_handle: int,
|
||||||
|
) -> None:
|
||||||
declaration_bytes = (
|
declaration_bytes = (
|
||||||
struct.pack('<BH', characteristic.properties, value_handle)
|
struct.pack('<BH', characteristic.properties, value_handle)
|
||||||
+ characteristic.uuid.to_pdu_bytes()
|
+ characteristic.uuid.to_pdu_bytes()
|
||||||
@@ -705,7 +713,7 @@ class MappedCharacteristicAdapter(PackedCharacteristicAdapter):
|
|||||||
'''
|
'''
|
||||||
Adapter that packs/unpacks characteristic values according to a standard
|
Adapter that packs/unpacks characteristic values according to a standard
|
||||||
Python `struct` format.
|
Python `struct` format.
|
||||||
The adapted `read_value` and `write_value` methods return/accept aa dictionary which
|
The adapted `read_value` and `write_value` methods return/accept a dictionary which
|
||||||
is packed/unpacked according to format, with the arguments extracted from the
|
is packed/unpacked according to format, with the arguments extracted from the
|
||||||
dictionary by key, in the same order as they occur in the `keys` parameter.
|
dictionary by key, in the same order as they occur in the `keys` parameter.
|
||||||
'''
|
'''
|
||||||
@@ -735,6 +743,24 @@ class UTF8CharacteristicAdapter(CharacteristicAdapter):
|
|||||||
return value.decode('utf-8')
|
return value.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class SerializableCharacteristicAdapter(CharacteristicAdapter):
|
||||||
|
'''
|
||||||
|
Adapter that converts any class to/from bytes using the class'
|
||||||
|
`to_bytes` and `__bytes__` methods, respectively.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, characteristic, cls: Type[ByteSerializable]):
|
||||||
|
super().__init__(characteristic)
|
||||||
|
self.cls = cls
|
||||||
|
|
||||||
|
def encode_value(self, value: SupportsBytes) -> bytes:
|
||||||
|
return bytes(value)
|
||||||
|
|
||||||
|
def decode_value(self, value: bytes) -> Any:
|
||||||
|
return self.cls.from_bytes(value)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Descriptor(Attribute):
|
class Descriptor(Attribute):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -28,7 +28,17 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import struct
|
import struct
|
||||||
from typing import List, Tuple, Optional, TypeVar, Type, Dict, Iterable, TYPE_CHECKING
|
from typing import (
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
TypeVar,
|
||||||
|
Type,
|
||||||
|
Union,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
@@ -68,6 +78,7 @@ from bumble.gatt import (
|
|||||||
GATT_REQUEST_TIMEOUT,
|
GATT_REQUEST_TIMEOUT,
|
||||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
Characteristic,
|
Characteristic,
|
||||||
|
CharacteristicAdapter,
|
||||||
CharacteristicDeclaration,
|
CharacteristicDeclaration,
|
||||||
CharacteristicValue,
|
CharacteristicValue,
|
||||||
IncludedServiceDeclaration,
|
IncludedServiceDeclaration,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
@@ -28,10 +29,11 @@ from bumble.device import Connection
|
|||||||
from bumble.att import ATT_Error
|
from bumble.att import ATT_Error
|
||||||
from bumble.gatt import (
|
from bumble.gatt import (
|
||||||
Characteristic,
|
Characteristic,
|
||||||
DelegatedCharacteristicAdapter,
|
SerializableCharacteristicAdapter,
|
||||||
|
PackedCharacteristicAdapter,
|
||||||
TemplateService,
|
TemplateService,
|
||||||
CharacteristicValue,
|
CharacteristicValue,
|
||||||
PackedCharacteristicAdapter,
|
UTF8CharacteristicAdapter,
|
||||||
GATT_AUDIO_INPUT_CONTROL_SERVICE,
|
GATT_AUDIO_INPUT_CONTROL_SERVICE,
|
||||||
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
||||||
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
||||||
@@ -154,9 +156,6 @@ class AudioInputState:
|
|||||||
attribute=self.attribute_value, value=bytes(self)
|
attribute=self.attribute_value, value=bytes(self)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_read(self, _connection: Optional[Connection]) -> bytes:
|
|
||||||
return bytes(self)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GainSettingsProperties:
|
class GainSettingsProperties:
|
||||||
@@ -173,7 +172,7 @@ class GainSettingsProperties:
|
|||||||
(gain_settings_unit, gain_settings_minimum, gain_settings_maximum) = (
|
(gain_settings_unit, gain_settings_minimum, gain_settings_maximum) = (
|
||||||
struct.unpack('BBB', data)
|
struct.unpack('BBB', data)
|
||||||
)
|
)
|
||||||
GainSettingsProperties(
|
return GainSettingsProperties(
|
||||||
gain_settings_unit, gain_settings_minimum, gain_settings_maximum
|
gain_settings_unit, gain_settings_minimum, gain_settings_maximum
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -186,9 +185,6 @@ class GainSettingsProperties:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_read(self, _connection: Optional[Connection]) -> bytes:
|
|
||||||
return bytes(self)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AudioInputControlPoint:
|
class AudioInputControlPoint:
|
||||||
@@ -321,21 +317,14 @@ class AudioInputDescription:
|
|||||||
audio_input_description: str = "Bluetooth"
|
audio_input_description: str = "Bluetooth"
|
||||||
attribute_value: Optional[CharacteristicValue] = None
|
attribute_value: Optional[CharacteristicValue] = None
|
||||||
|
|
||||||
@classmethod
|
def on_read(self, _connection: Optional[Connection]) -> str:
|
||||||
def from_bytes(cls, data: bytes):
|
return self.audio_input_description
|
||||||
return cls(audio_input_description=data.decode('utf-8'))
|
|
||||||
|
|
||||||
def __bytes__(self) -> bytes:
|
async def on_write(self, connection: Optional[Connection], value: str) -> None:
|
||||||
return self.audio_input_description.encode('utf-8')
|
|
||||||
|
|
||||||
def on_read(self, _connection: Optional[Connection]) -> bytes:
|
|
||||||
return self.audio_input_description.encode('utf-8')
|
|
||||||
|
|
||||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
|
||||||
assert connection
|
assert connection
|
||||||
assert self.attribute_value
|
assert self.attribute_value
|
||||||
|
|
||||||
self.audio_input_description = value.decode('utf-8')
|
self.audio_input_description = value
|
||||||
await connection.device.notify_subscribers(
|
await connection.device.notify_subscribers(
|
||||||
attribute=self.attribute_value, value=value
|
attribute=self.attribute_value, value=value
|
||||||
)
|
)
|
||||||
@@ -375,26 +364,29 @@ class AICSService(TemplateService):
|
|||||||
self.audio_input_state, self.gain_settings_properties
|
self.audio_input_state, self.gain_settings_properties
|
||||||
)
|
)
|
||||||
|
|
||||||
self.audio_input_state_characteristic = DelegatedCharacteristicAdapter(
|
self.audio_input_state_characteristic = SerializableCharacteristicAdapter(
|
||||||
Characteristic(
|
Characteristic(
|
||||||
uuid=GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
uuid=GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
||||||
properties=Characteristic.Properties.READ
|
properties=Characteristic.Properties.READ
|
||||||
| Characteristic.Properties.NOTIFY,
|
| Characteristic.Properties.NOTIFY,
|
||||||
permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
|
permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
|
||||||
value=CharacteristicValue(read=self.audio_input_state.on_read),
|
value=self.audio_input_state,
|
||||||
),
|
),
|
||||||
encode=lambda value: bytes(value),
|
AudioInputState,
|
||||||
)
|
)
|
||||||
self.audio_input_state.attribute_value = (
|
self.audio_input_state.attribute_value = (
|
||||||
self.audio_input_state_characteristic.value
|
self.audio_input_state_characteristic.value
|
||||||
)
|
)
|
||||||
|
|
||||||
self.gain_settings_properties_characteristic = DelegatedCharacteristicAdapter(
|
self.gain_settings_properties_characteristic = (
|
||||||
|
SerializableCharacteristicAdapter(
|
||||||
Characteristic(
|
Characteristic(
|
||||||
uuid=GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
uuid=GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
||||||
properties=Characteristic.Properties.READ,
|
properties=Characteristic.Properties.READ,
|
||||||
permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
|
permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
|
||||||
value=CharacteristicValue(read=self.gain_settings_properties.on_read),
|
value=self.gain_settings_properties,
|
||||||
|
),
|
||||||
|
GainSettingsProperties,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -402,7 +394,7 @@ class AICSService(TemplateService):
|
|||||||
uuid=GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
|
uuid=GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
|
||||||
properties=Characteristic.Properties.READ,
|
properties=Characteristic.Properties.READ,
|
||||||
permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
|
permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
|
||||||
value=audio_input_type,
|
value=bytes(audio_input_type, 'utf-8'),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.audio_input_status_characteristic = Characteristic(
|
self.audio_input_status_characteristic = Characteristic(
|
||||||
@@ -412,18 +404,14 @@ class AICSService(TemplateService):
|
|||||||
value=bytes([self.audio_input_status]),
|
value=bytes([self.audio_input_status]),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.audio_input_control_point_characteristic = DelegatedCharacteristicAdapter(
|
self.audio_input_control_point_characteristic = Characteristic(
|
||||||
Characteristic(
|
|
||||||
uuid=GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
|
uuid=GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
|
||||||
properties=Characteristic.Properties.WRITE,
|
properties=Characteristic.Properties.WRITE,
|
||||||
permissions=Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
|
permissions=Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
|
||||||
value=CharacteristicValue(
|
value=CharacteristicValue(write=self.audio_input_control_point.on_write),
|
||||||
write=self.audio_input_control_point.on_write
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.audio_input_description_characteristic = DelegatedCharacteristicAdapter(
|
self.audio_input_description_characteristic = UTF8CharacteristicAdapter(
|
||||||
Characteristic(
|
Characteristic(
|
||||||
uuid=GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
|
uuid=GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
|
||||||
properties=Characteristic.Properties.READ
|
properties=Characteristic.Properties.READ
|
||||||
@@ -469,8 +457,8 @@ class AICSServiceProxy(ProfileServiceProxy):
|
|||||||
)
|
)
|
||||||
):
|
):
|
||||||
raise gatt.InvalidServiceError("Audio Input State Characteristic not found")
|
raise gatt.InvalidServiceError("Audio Input State Characteristic not found")
|
||||||
self.audio_input_state = DelegatedCharacteristicAdapter(
|
self.audio_input_state = SerializableCharacteristicAdapter(
|
||||||
characteristic=characteristics[0], decode=AudioInputState.from_bytes
|
characteristics[0], AudioInputState
|
||||||
)
|
)
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
@@ -481,9 +469,8 @@ class AICSServiceProxy(ProfileServiceProxy):
|
|||||||
raise gatt.InvalidServiceError(
|
raise gatt.InvalidServiceError(
|
||||||
"Gain Settings Attribute Characteristic not found"
|
"Gain Settings Attribute Characteristic not found"
|
||||||
)
|
)
|
||||||
self.gain_settings_properties = PackedCharacteristicAdapter(
|
self.gain_settings_properties = SerializableCharacteristicAdapter(
|
||||||
characteristics[0],
|
characteristics[0], GainSettingsProperties
|
||||||
'BBB',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
@@ -494,10 +481,7 @@ class AICSServiceProxy(ProfileServiceProxy):
|
|||||||
raise gatt.InvalidServiceError(
|
raise gatt.InvalidServiceError(
|
||||||
"Audio Input Status Characteristic not found"
|
"Audio Input Status Characteristic not found"
|
||||||
)
|
)
|
||||||
self.audio_input_status = PackedCharacteristicAdapter(
|
self.audio_input_status = PackedCharacteristicAdapter(characteristics[0], 'B')
|
||||||
characteristics[0],
|
|
||||||
'B',
|
|
||||||
)
|
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
characteristics := service_proxy.get_characteristics_by_uuid(
|
characteristics := service_proxy.get_characteristics_by_uuid(
|
||||||
@@ -517,4 +501,4 @@ class AICSServiceProxy(ProfileServiceProxy):
|
|||||||
raise gatt.InvalidServiceError(
|
raise gatt.InvalidServiceError(
|
||||||
"Audio Input Description Characteristic not found"
|
"Audio Input Description Characteristic not found"
|
||||||
)
|
)
|
||||||
self.audio_input_description = characteristics[0]
|
self.audio_input_description = UTF8CharacteristicAdapter(characteristics[0])
|
||||||
|
|||||||
@@ -276,10 +276,7 @@ class BroadcastReceiveState:
|
|||||||
subgroups: List[SubgroupInfo]
|
subgroups: List[SubgroupInfo]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytes(cls, data: bytes) -> Optional[BroadcastReceiveState]:
|
def from_bytes(cls, data: bytes) -> BroadcastReceiveState:
|
||||||
if not data:
|
|
||||||
return None
|
|
||||||
|
|
||||||
source_id = data[0]
|
source_id = data[0]
|
||||||
_, source_address = hci.Address.parse_address_preceded_by_type(data, 2)
|
_, source_address = hci.Address.parse_address_preceded_by_type(data, 2)
|
||||||
source_adv_sid = data[8]
|
source_adv_sid = data[8]
|
||||||
@@ -357,7 +354,7 @@ class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
|
|||||||
SERVICE_CLASS = BroadcastAudioScanService
|
SERVICE_CLASS = BroadcastAudioScanService
|
||||||
|
|
||||||
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy
|
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy
|
||||||
broadcast_receive_states: List[gatt.DelegatedCharacteristicAdapter]
|
broadcast_receive_states: List[gatt.SerializableCharacteristicAdapter]
|
||||||
|
|
||||||
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
||||||
self.service_proxy = service_proxy
|
self.service_proxy = service_proxy
|
||||||
@@ -381,8 +378,8 @@ class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
|
|||||||
"Broadcast Receive State characteristic not found"
|
"Broadcast Receive State characteristic not found"
|
||||||
)
|
)
|
||||||
self.broadcast_receive_states = [
|
self.broadcast_receive_states = [
|
||||||
gatt.DelegatedCharacteristicAdapter(
|
gatt.SerializableCharacteristicAdapter(
|
||||||
characteristic, decode=BroadcastReceiveState.from_bytes
|
characteristic, BroadcastReceiveState
|
||||||
)
|
)
|
||||||
for characteristic in characteristics
|
for characteristic in characteristics
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ class DeviceInformationService(TemplateService):
|
|||||||
):
|
):
|
||||||
characteristics = [
|
characteristics = [
|
||||||
Characteristic(
|
Characteristic(
|
||||||
uuid, Characteristic.Properties.READ, Characteristic.READABLE, field
|
uuid,
|
||||||
|
Characteristic.Properties.READ,
|
||||||
|
Characteristic.READABLE,
|
||||||
|
bytes(field, 'utf-8'),
|
||||||
)
|
)
|
||||||
for (field, uuid) in (
|
for (field, uuid) in (
|
||||||
(manufacturer_name, GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
|
(manufacturer_name, GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from ..gatt import (
|
|||||||
TemplateService,
|
TemplateService,
|
||||||
Characteristic,
|
Characteristic,
|
||||||
CharacteristicValue,
|
CharacteristicValue,
|
||||||
|
SerializableCharacteristicAdapter,
|
||||||
DelegatedCharacteristicAdapter,
|
DelegatedCharacteristicAdapter,
|
||||||
PackedCharacteristicAdapter,
|
PackedCharacteristicAdapter,
|
||||||
)
|
)
|
||||||
@@ -150,15 +151,14 @@ class HeartRateService(TemplateService):
|
|||||||
body_sensor_location=None,
|
body_sensor_location=None,
|
||||||
reset_energy_expended=None,
|
reset_energy_expended=None,
|
||||||
):
|
):
|
||||||
self.heart_rate_measurement_characteristic = DelegatedCharacteristicAdapter(
|
self.heart_rate_measurement_characteristic = SerializableCharacteristicAdapter(
|
||||||
Characteristic(
|
Characteristic(
|
||||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||||
Characteristic.Properties.NOTIFY,
|
Characteristic.Properties.NOTIFY,
|
||||||
0,
|
0,
|
||||||
CharacteristicValue(read=read_heart_rate_measurement),
|
CharacteristicValue(read=read_heart_rate_measurement),
|
||||||
),
|
),
|
||||||
# pylint: disable=unnecessary-lambda
|
HeartRateService.HeartRateMeasurement,
|
||||||
encode=lambda value: bytes(value),
|
|
||||||
)
|
)
|
||||||
characteristics = [self.heart_rate_measurement_characteristic]
|
characteristics = [self.heart_rate_measurement_characteristic]
|
||||||
|
|
||||||
@@ -204,9 +204,8 @@ class HeartRateServiceProxy(ProfileServiceProxy):
|
|||||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC
|
||||||
):
|
):
|
||||||
self.heart_rate_measurement = DelegatedCharacteristicAdapter(
|
self.heart_rate_measurement = SerializableCharacteristicAdapter(
|
||||||
characteristics[0],
|
characteristics[0], HeartRateService.HeartRateMeasurement
|
||||||
decode=HeartRateService.HeartRateMeasurement.from_bytes,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.heart_rate_measurement = None
|
self.heart_rate_measurement = None
|
||||||
|
|||||||
@@ -24,17 +24,19 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from typing import (
|
from typing import (
|
||||||
Awaitable,
|
|
||||||
Set,
|
|
||||||
TypeVar,
|
|
||||||
List,
|
|
||||||
Tuple,
|
|
||||||
Callable,
|
|
||||||
Any,
|
Any,
|
||||||
|
Awaitable,
|
||||||
|
Callable,
|
||||||
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
|
Protocol,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
@@ -487,3 +489,16 @@ class OpenIntEnum(enum.IntEnum):
|
|||||||
obj._value_ = value
|
obj._value_ = value
|
||||||
obj._name_ = f"{cls.__name__}[{value}]"
|
obj._name_ = f"{cls.__name__}[{value}]"
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class ByteSerializable(Protocol):
|
||||||
|
"""
|
||||||
|
Type protocol for classes that can be instantiated from bytes and serialized
|
||||||
|
to bytes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes) -> Self: ...
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes: ...
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ async def keyboard_device(device, command):
|
|||||||
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
||||||
Characteristic.Properties.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
'Bumble',
|
bytes('Bumble', 'utf-8'),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ async def main() -> None:
|
|||||||
'486F64C6-4B5F-4B3B-8AFF-EDE134A8446A',
|
'486F64C6-4B5F-4B3B-8AFF-EDE134A8446A',
|
||||||
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
'hello',
|
bytes('hello', 'utf-8'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
319
examples/run_gatt_with_adapters.py
Normal file
319
examples/run_gatt_with_adapters.py
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
# Copyright 2024 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Imports
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
from __future__ import annotations
|
||||||
|
import asyncio
|
||||||
|
import dataclasses
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from typing import Any, List, Union
|
||||||
|
|
||||||
|
from bumble.device import Connection, Device, Peer
|
||||||
|
from bumble import transport
|
||||||
|
from bumble import gatt
|
||||||
|
from bumble import hci
|
||||||
|
from bumble import core
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
SERVICE_UUID = core.UUID("50DB505C-8AC4-4738-8448-3B1D9CC09CC5")
|
||||||
|
CHARACTERISTIC_UUID_BASE = "D901B45B-4916-412E-ACCA-0000000000"
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class CustomSerializableClass:
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes) -> CustomSerializableClass:
|
||||||
|
return cls(*struct.unpack(">II", data))
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
return struct.pack(">II", self.x, self.y)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class CustomClass:
|
||||||
|
a: int
|
||||||
|
b: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def decode(cls, data: bytes) -> CustomClass:
|
||||||
|
return cls(*struct.unpack(">II", data))
|
||||||
|
|
||||||
|
def encode(self) -> bytes:
|
||||||
|
return struct.pack(">II", self.a, self.b)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def client(device: Device, address: hci.Address) -> None:
|
||||||
|
print(f'=== Connecting to {address}...')
|
||||||
|
connection = await device.connect(address)
|
||||||
|
print('=== Connected')
|
||||||
|
|
||||||
|
# Discover all characteristics.
|
||||||
|
peer = Peer(connection)
|
||||||
|
print("*** Discovering services and characteristics...")
|
||||||
|
await peer.discover_all()
|
||||||
|
print("*** Discovery complete")
|
||||||
|
|
||||||
|
service = peer.get_services_by_uuid(SERVICE_UUID)[0]
|
||||||
|
characteristics = []
|
||||||
|
for index in range(1, 9):
|
||||||
|
characteristics.append(
|
||||||
|
service.get_characteristics_by_uuid(
|
||||||
|
CHARACTERISTIC_UUID_BASE + f"{index:02X}"
|
||||||
|
)[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read all characteristics as raw bytes.
|
||||||
|
for characteristic in characteristics:
|
||||||
|
value = await characteristic.read_value()
|
||||||
|
print(f"### {characteristic} = {value} ({value.hex()})")
|
||||||
|
|
||||||
|
# Static characteristic with a bytes value.
|
||||||
|
c1 = characteristics[0]
|
||||||
|
c1_value = await c1.read_value()
|
||||||
|
print(f"@@@ C1 {c1} value = {c1_value} (type={type(c1_value)})")
|
||||||
|
await c1.write_value("happy π day".encode("utf-8"))
|
||||||
|
|
||||||
|
# Static characteristic with a string value.
|
||||||
|
c2 = gatt.UTF8CharacteristicAdapter(characteristics[1])
|
||||||
|
c2_value = await c2.read_value()
|
||||||
|
print(f"@@@ C2 {c2} value = {c2_value} (type={type(c2_value)})")
|
||||||
|
await c2.write_value("happy π day")
|
||||||
|
|
||||||
|
# Static characteristic with a tuple value.
|
||||||
|
c3 = gatt.PackedCharacteristicAdapter(characteristics[2], ">III")
|
||||||
|
c3_value = await c3.read_value()
|
||||||
|
print(f"@@@ C3 {c3} value = {c3_value} (type={type(c3_value)})")
|
||||||
|
await c3.write_value((2001, 2002, 2003))
|
||||||
|
|
||||||
|
# Static characteristic with a named tuple value.
|
||||||
|
c4 = gatt.MappedCharacteristicAdapter(
|
||||||
|
characteristics[3], ">III", ["f1", "f2", "f3"]
|
||||||
|
)
|
||||||
|
c4_value = await c4.read_value()
|
||||||
|
print(f"@@@ C4 {c4} value = {c4_value} (type={type(c4_value)})")
|
||||||
|
await c4.write_value({"f1": 4001, "f2": 4002, "f3": 4003})
|
||||||
|
|
||||||
|
# Static characteristic with a serializable value.
|
||||||
|
c5 = gatt.SerializableCharacteristicAdapter(
|
||||||
|
characteristics[4], CustomSerializableClass
|
||||||
|
)
|
||||||
|
c5_value = await c5.read_value()
|
||||||
|
print(f"@@@ C5 {c5} value = {c5_value} (type={type(c5_value)})")
|
||||||
|
await c5.write_value(CustomSerializableClass(56, 57))
|
||||||
|
|
||||||
|
# Static characteristic with a delegated value.
|
||||||
|
c6 = gatt.DelegatedCharacteristicAdapter(
|
||||||
|
characteristics[5], encode=CustomClass.encode, decode=CustomClass.decode
|
||||||
|
)
|
||||||
|
c6_value = await c6.read_value()
|
||||||
|
print(f"@@@ C6 {c6} value = {c6_value} (type={type(c6_value)})")
|
||||||
|
await c6.write_value(CustomClass(6, 7))
|
||||||
|
|
||||||
|
# Dynamic characteristic with a bytes value.
|
||||||
|
c7 = characteristics[6]
|
||||||
|
c7_value = await c7.read_value()
|
||||||
|
print(f"@@@ C7 {c7} value = {c7_value} (type={type(c7_value)})")
|
||||||
|
await c7.write_value(bytes.fromhex("01020304"))
|
||||||
|
|
||||||
|
# Dynamic characteristic with a string value.
|
||||||
|
c8 = gatt.UTF8CharacteristicAdapter(characteristics[7])
|
||||||
|
c8_value = await c8.read_value()
|
||||||
|
print(f"@@@ C8 {c8} value = {c8_value} (type={type(c8_value)})")
|
||||||
|
await c8.write_value("howdy")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def dynamic_read(selector: str) -> Union[bytes, str]:
|
||||||
|
if selector == "bytes":
|
||||||
|
print("$$$ Returning random bytes")
|
||||||
|
return random.randbytes(7)
|
||||||
|
elif selector == "string":
|
||||||
|
print("$$$ Returning random string")
|
||||||
|
return random.randbytes(7).hex()
|
||||||
|
|
||||||
|
raise ValueError("invalid selector")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def dynamic_write(selector: str, value: Any) -> None:
|
||||||
|
print(f"$$$ Received[{selector}]: {value} (type={type(value)})")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def on_characteristic_read(characteristic: gatt.Characteristic, value: Any) -> None:
|
||||||
|
"""Event listener invoked when a characteristic is read."""
|
||||||
|
print(f"<<< READ: {characteristic} -> {value} ({type(value)})")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def on_characteristic_write(characteristic: gatt.Characteristic, value: Any) -> None:
|
||||||
|
"""Event listener invoked when a characteristic is written."""
|
||||||
|
print(f"<<< WRITE: {characteristic} <- {value} ({type(value)})")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def main() -> None:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: run_gatt_with_adapters.py <transport-spec> [<bluetooth-address>]")
|
||||||
|
print("example: run_gatt_with_adapters.py usb:0 E1:CA:72:48:C4:E8")
|
||||||
|
return
|
||||||
|
|
||||||
|
async with await transport.open_transport(sys.argv[1]) as hci_transport:
|
||||||
|
# Create a device to manage the host
|
||||||
|
device = Device.with_hci(
|
||||||
|
"Bumble",
|
||||||
|
hci.Address("F0:F1:F2:F3:F4:F5"),
|
||||||
|
hci_transport.source,
|
||||||
|
hci_transport.sink,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Static characteristic with a bytes value.
|
||||||
|
c1 = gatt.Characteristic(
|
||||||
|
CHARACTERISTIC_UUID_BASE + "01",
|
||||||
|
gatt.Characteristic.Properties.READ | gatt.Characteristic.Properties.WRITE,
|
||||||
|
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||||
|
b'hello',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Static characteristic with a string value.
|
||||||
|
c2 = gatt.UTF8CharacteristicAdapter(
|
||||||
|
gatt.Characteristic(
|
||||||
|
CHARACTERISTIC_UUID_BASE + "02",
|
||||||
|
gatt.Characteristic.Properties.READ
|
||||||
|
| gatt.Characteristic.Properties.WRITE,
|
||||||
|
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||||
|
'hello',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Static characteristic with a tuple value.
|
||||||
|
c3 = gatt.PackedCharacteristicAdapter(
|
||||||
|
gatt.Characteristic(
|
||||||
|
CHARACTERISTIC_UUID_BASE + "03",
|
||||||
|
gatt.Characteristic.Properties.READ
|
||||||
|
| gatt.Characteristic.Properties.WRITE,
|
||||||
|
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||||
|
(1007, 1008, 1009),
|
||||||
|
),
|
||||||
|
">III",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Static characteristic with a named tuple value.
|
||||||
|
c4 = gatt.MappedCharacteristicAdapter(
|
||||||
|
gatt.Characteristic(
|
||||||
|
CHARACTERISTIC_UUID_BASE + "04",
|
||||||
|
gatt.Characteristic.Properties.READ
|
||||||
|
| gatt.Characteristic.Properties.WRITE,
|
||||||
|
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||||
|
{"f1": 3007, "f2": 3008, "f3": 3009},
|
||||||
|
),
|
||||||
|
">III",
|
||||||
|
["f1", "f2", "f3"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Static characteristic with a serializable value.
|
||||||
|
c5 = gatt.SerializableCharacteristicAdapter(
|
||||||
|
gatt.Characteristic(
|
||||||
|
CHARACTERISTIC_UUID_BASE + "05",
|
||||||
|
gatt.Characteristic.Properties.READ
|
||||||
|
| gatt.Characteristic.Properties.WRITE,
|
||||||
|
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||||
|
CustomSerializableClass(11, 12),
|
||||||
|
),
|
||||||
|
CustomSerializableClass,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Static characteristic with a delegated value.
|
||||||
|
c6 = gatt.DelegatedCharacteristicAdapter(
|
||||||
|
gatt.Characteristic(
|
||||||
|
CHARACTERISTIC_UUID_BASE + "06",
|
||||||
|
gatt.Characteristic.Properties.READ
|
||||||
|
| gatt.Characteristic.Properties.WRITE,
|
||||||
|
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||||
|
CustomClass(1, 2),
|
||||||
|
),
|
||||||
|
encode=CustomClass.encode,
|
||||||
|
decode=CustomClass.decode,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dynamic characteristic with a bytes value.
|
||||||
|
c7 = gatt.Characteristic(
|
||||||
|
CHARACTERISTIC_UUID_BASE + "07",
|
||||||
|
gatt.Characteristic.Properties.READ | gatt.Characteristic.Properties.WRITE,
|
||||||
|
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||||
|
gatt.CharacteristicValue(
|
||||||
|
read=lambda connection: dynamic_read("bytes"),
|
||||||
|
write=lambda connection, value: dynamic_write("bytes", value),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dynamic characteristic with a string value.
|
||||||
|
c8 = gatt.UTF8CharacteristicAdapter(
|
||||||
|
gatt.Characteristic(
|
||||||
|
CHARACTERISTIC_UUID_BASE + "08",
|
||||||
|
gatt.Characteristic.Properties.READ
|
||||||
|
| gatt.Characteristic.Properties.WRITE,
|
||||||
|
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||||
|
gatt.CharacteristicValue(
|
||||||
|
read=lambda connection: dynamic_read("string"),
|
||||||
|
write=lambda connection, value: dynamic_write("string", value),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
characteristics: List[
|
||||||
|
Union[gatt.Characteristic, gatt.CharacteristicAdapter]
|
||||||
|
] = [c1, c2, c3, c4, c5, c6, c7, c8]
|
||||||
|
|
||||||
|
# Listen for read and write events.
|
||||||
|
for characteristic in characteristics:
|
||||||
|
characteristic.on(
|
||||||
|
"read",
|
||||||
|
lambda _, value, c=characteristic: on_characteristic_read(c, value),
|
||||||
|
)
|
||||||
|
characteristic.on(
|
||||||
|
"write",
|
||||||
|
lambda _, value, c=characteristic: on_characteristic_write(c, value),
|
||||||
|
)
|
||||||
|
|
||||||
|
device.add_service(gatt.Service(SERVICE_UUID, characteristics)) # type: ignore
|
||||||
|
|
||||||
|
# Get things going
|
||||||
|
await device.power_on()
|
||||||
|
|
||||||
|
# Connect to a peer
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
await client(device, hci.Address(sys.argv[2]))
|
||||||
|
else:
|
||||||
|
await device.start_advertising(auto_restart=True)
|
||||||
|
|
||||||
|
await hci_transport.source.wait_for_termination()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||||
|
asyncio.run(main())
|
||||||
@@ -28,6 +28,7 @@ from bumble.profiles.aics import (
|
|||||||
AudioInputState,
|
AudioInputState,
|
||||||
AICSServiceProxy,
|
AICSServiceProxy,
|
||||||
GainMode,
|
GainMode,
|
||||||
|
GainSettingsProperties,
|
||||||
AudioInputStatus,
|
AudioInputStatus,
|
||||||
AudioInputControlPointOpCode,
|
AudioInputControlPointOpCode,
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
@@ -82,7 +83,12 @@ async def test_init_service(aics_client: AICSServiceProxy):
|
|||||||
gain_mode=GainMode.MANUAL,
|
gain_mode=GainMode.MANUAL,
|
||||||
change_counter=0,
|
change_counter=0,
|
||||||
)
|
)
|
||||||
assert await aics_client.gain_settings_properties.read_value() == (1, 0, 255)
|
assert (
|
||||||
|
await aics_client.gain_settings_properties.read_value()
|
||||||
|
== GainSettingsProperties(
|
||||||
|
gain_settings_unit=1, gain_settings_minimum=0, gain_settings_maximum=255
|
||||||
|
)
|
||||||
|
)
|
||||||
assert await aics_client.audio_input_status.read_value() == (
|
assert await aics_client.audio_input_status.read_value() == (
|
||||||
AudioInputStatus.ACTIVE
|
AudioInputStatus.ACTIVE
|
||||||
)
|
)
|
||||||
@@ -481,12 +487,12 @@ async def test_set_automatic_gain_mode_when_automatic_only(
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_audio_input_description_initial_value(aics_client: AICSServiceProxy):
|
async def test_audio_input_description_initial_value(aics_client: AICSServiceProxy):
|
||||||
description = await aics_client.audio_input_description.read_value()
|
description = await aics_client.audio_input_description.read_value()
|
||||||
assert description.decode('utf-8') == "Bluetooth"
|
assert description == "Bluetooth"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_audio_input_description_write_and_read(aics_client: AICSServiceProxy):
|
async def test_audio_input_description_write_and_read(aics_client: AICSServiceProxy):
|
||||||
new_description = "Line Input".encode('utf-8')
|
new_description = "Line Input"
|
||||||
|
|
||||||
await aics_client.audio_input_description.write_value(new_description)
|
await aics_client.audio_input_description.write_value(new_description)
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import pytest
|
import pytest
|
||||||
|
from typing_extensions import Self
|
||||||
from unittest.mock import AsyncMock, Mock, ANY
|
from unittest.mock import AsyncMock, Mock, ANY
|
||||||
|
|
||||||
from bumble.controller import Controller
|
from bumble.controller import Controller
|
||||||
@@ -31,6 +33,7 @@ from bumble.gatt import (
|
|||||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||||
CharacteristicAdapter,
|
CharacteristicAdapter,
|
||||||
|
SerializableCharacteristicAdapter,
|
||||||
DelegatedCharacteristicAdapter,
|
DelegatedCharacteristicAdapter,
|
||||||
PackedCharacteristicAdapter,
|
PackedCharacteristicAdapter,
|
||||||
MappedCharacteristicAdapter,
|
MappedCharacteristicAdapter,
|
||||||
@@ -310,7 +313,7 @@ async def test_attribute_getters():
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_CharacteristicAdapter():
|
async def test_CharacteristicAdapter() -> None:
|
||||||
# Check that the CharacteristicAdapter base class is transparent
|
# Check that the CharacteristicAdapter base class is transparent
|
||||||
v = bytes([1, 2, 3])
|
v = bytes([1, 2, 3])
|
||||||
c = Characteristic(
|
c = Characteristic(
|
||||||
@@ -329,67 +332,94 @@ async def test_CharacteristicAdapter():
|
|||||||
assert c.value == v
|
assert c.value == v
|
||||||
|
|
||||||
# Simple delegated adapter
|
# Simple delegated adapter
|
||||||
a = DelegatedCharacteristicAdapter(
|
delegated = DelegatedCharacteristicAdapter(
|
||||||
c, lambda x: bytes(reversed(x)), lambda x: bytes(reversed(x))
|
c, lambda x: bytes(reversed(x)), lambda x: bytes(reversed(x))
|
||||||
)
|
)
|
||||||
|
|
||||||
value = await a.read_value(None)
|
delegated_value = await delegated.read_value(None)
|
||||||
assert value == bytes(reversed(v))
|
assert delegated_value == bytes(reversed(v))
|
||||||
|
|
||||||
v = bytes([3, 4, 5])
|
delegated_value2 = bytes([3, 4, 5])
|
||||||
await a.write_value(None, v)
|
await delegated.write_value(None, delegated_value2)
|
||||||
assert a.value == bytes(reversed(v))
|
assert delegated.value == bytes(reversed(delegated_value2))
|
||||||
|
|
||||||
# Packed adapter with single element format
|
# Packed adapter with single element format
|
||||||
v = 1234
|
packed_value_ref = 1234
|
||||||
pv = struct.pack('>H', v)
|
packed_value_bytes = struct.pack('>H', packed_value_ref)
|
||||||
c.value = v
|
c.value = packed_value_ref
|
||||||
a = PackedCharacteristicAdapter(c, '>H')
|
packed = PackedCharacteristicAdapter(c, '>H')
|
||||||
|
|
||||||
value = await a.read_value(None)
|
packed_value_read = await packed.read_value(None)
|
||||||
assert value == pv
|
assert packed_value_read == packed_value_bytes
|
||||||
c.value = None
|
c.value = b''
|
||||||
await a.write_value(None, pv)
|
await packed.write_value(None, packed_value_bytes)
|
||||||
assert a.value == v
|
assert packed.value == packed_value_ref
|
||||||
|
|
||||||
# Packed adapter with multi-element format
|
# Packed adapter with multi-element format
|
||||||
v1 = 1234
|
v1 = 1234
|
||||||
v2 = 5678
|
v2 = 5678
|
||||||
pv = struct.pack('>HH', v1, v2)
|
packed_multi_value_bytes = struct.pack('>HH', v1, v2)
|
||||||
c.value = (v1, v2)
|
c.value = (v1, v2)
|
||||||
a = PackedCharacteristicAdapter(c, '>HH')
|
packed_multi = PackedCharacteristicAdapter(c, '>HH')
|
||||||
|
|
||||||
value = await a.read_value(None)
|
packed_multi_read_value = await packed_multi.read_value(None)
|
||||||
assert value == pv
|
assert packed_multi_read_value == packed_multi_value_bytes
|
||||||
c.value = None
|
packed_multi.value = b''
|
||||||
await a.write_value(None, pv)
|
await packed_multi.write_value(None, packed_multi_value_bytes)
|
||||||
assert a.value == (v1, v2)
|
assert packed_multi.value == (v1, v2)
|
||||||
|
|
||||||
# Mapped adapter
|
# Mapped adapter
|
||||||
v1 = 1234
|
v1 = 1234
|
||||||
v2 = 5678
|
v2 = 5678
|
||||||
pv = struct.pack('>HH', v1, v2)
|
packed_mapped_value_bytes = struct.pack('>HH', v1, v2)
|
||||||
mapped = {'v1': v1, 'v2': v2}
|
mapped = {'v1': v1, 'v2': v2}
|
||||||
c.value = mapped
|
c.value = mapped
|
||||||
a = MappedCharacteristicAdapter(c, '>HH', ('v1', 'v2'))
|
packed_mapped = MappedCharacteristicAdapter(c, '>HH', ('v1', 'v2'))
|
||||||
|
|
||||||
value = await a.read_value(None)
|
packed_mapped_read_value = await packed_mapped.read_value(None)
|
||||||
assert value == pv
|
assert packed_mapped_read_value == packed_mapped_value_bytes
|
||||||
c.value = None
|
c.value = b''
|
||||||
await a.write_value(None, pv)
|
await packed_mapped.write_value(None, packed_mapped_value_bytes)
|
||||||
assert a.value == mapped
|
assert packed_mapped.value == mapped
|
||||||
|
|
||||||
# UTF-8 adapter
|
# UTF-8 adapter
|
||||||
v = 'Hello π'
|
string_value = 'Hello π'
|
||||||
ev = v.encode('utf-8')
|
string_value_bytes = string_value.encode('utf-8')
|
||||||
c.value = v
|
c.value = string_value
|
||||||
a = UTF8CharacteristicAdapter(c)
|
string_c = UTF8CharacteristicAdapter(c)
|
||||||
|
|
||||||
value = await a.read_value(None)
|
string_read_value = await string_c.read_value(None)
|
||||||
assert value == ev
|
assert string_read_value == string_value_bytes
|
||||||
c.value = None
|
c.value = b''
|
||||||
await a.write_value(None, ev)
|
await string_c.write_value(None, string_value_bytes)
|
||||||
assert a.value == v
|
assert string_c.value == string_value
|
||||||
|
|
||||||
|
# Class adapter
|
||||||
|
class BlaBla:
|
||||||
|
def __init__(self, a: int, b: int) -> None:
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes) -> Self:
|
||||||
|
a, b = struct.unpack(">II", data)
|
||||||
|
return cls(a, b)
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
return struct.pack(">II", self.a, self.b)
|
||||||
|
|
||||||
|
class_value = BlaBla(3, 4)
|
||||||
|
class_value_bytes = struct.pack(">II", 3, 4)
|
||||||
|
c.value = class_value
|
||||||
|
class_c = SerializableCharacteristicAdapter(c, BlaBla)
|
||||||
|
|
||||||
|
class_read_value = await class_c.read_value(None)
|
||||||
|
assert class_read_value == class_value_bytes
|
||||||
|
c.value = b''
|
||||||
|
await class_c.write_value(None, class_value_bytes)
|
||||||
|
assert isinstance(c.value, BlaBla)
|
||||||
|
assert c.value.a == 3
|
||||||
|
assert c.value.b == 4
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user