diff --git a/apps/console.py b/apps/console.py index 9a529dd2..5d046366 100644 --- a/apps/console.py +++ b/apps/console.py @@ -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: @@ -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 @@ -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): diff --git a/bumble/att.py b/bumble/att.py index db8d2baa..2bec4eae 100644 --- a/bumble/att.py +++ b/bumble/att.py @@ -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 @@ -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 # ----------------------------------------------------------------------------- @@ -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 @@ -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 @@ -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 @@ -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: @@ -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 diff --git a/bumble/gatt.py b/bumble/gatt.py index 5e270244..71c01f43 100644 --- a/bumble/gatt.py +++ b/bumble/gatt.py @@ -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 # ----------------------------------------------------------------------------- @@ -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): @@ -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) ) @@ -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 = '' + 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 diff --git a/bumble/gatt_server.py b/bumble/gatt_server.py index eca11ce4..d574d520 100644 --- a/bumble/gatt_server.py +++ b/bumble/gatt_server.py @@ -31,9 +31,9 @@ from typing import List, Tuple, Optional, TypeVar, Type, Dict, Iterable, TYPE_CHECKING from pyee import EventEmitter -from .colors import color -from .core import UUID -from .att import ( +from bumble.colors import color +from bumble.core import UUID +from bumble.att import ( ATT_ATTRIBUTE_NOT_FOUND_ERROR, ATT_ATTRIBUTE_NOT_LONG_ERROR, ATT_CID, @@ -60,7 +60,7 @@ ATT_Write_Response, Attribute, ) -from .gatt import ( +from bumble.gatt import ( GATT_CHARACTERISTIC_ATTRIBUTE_TYPE, GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR, GATT_MAX_ATTRIBUTE_VALUE_SIZE, @@ -74,6 +74,7 @@ Descriptor, Service, ) +from bumble.utils import AsyncRunner if TYPE_CHECKING: from bumble.device import Device, Connection @@ -379,7 +380,7 @@ async def notify_subscriber( # Get or encode the value value = ( - attribute.read_value(connection) + await attribute.read_value(connection) if value is None else attribute.encode_value(value) ) @@ -422,7 +423,7 @@ async def indicate_subscriber( # Get or encode the value value = ( - attribute.read_value(connection) + await attribute.read_value(connection) if value is None else attribute.encode_value(value) ) @@ -650,7 +651,8 @@ def on_att_find_information_request(self, connection, request): self.send_response(connection, response) - def on_att_find_by_type_value_request(self, connection, request): + @AsyncRunner.run_in_task() + async def on_att_find_by_type_value_request(self, connection, request): ''' See Bluetooth spec Vol 3, Part F - 3.4.3.3 Find By Type Value Request ''' @@ -658,13 +660,13 @@ def on_att_find_by_type_value_request(self, connection, request): # Build list of returned attributes pdu_space_available = connection.att_mtu - 2 attributes = [] - for attribute in ( + async for attribute in ( attribute for attribute in self.attributes if attribute.handle >= request.starting_handle and attribute.handle <= request.ending_handle and attribute.type == request.attribute_type - and attribute.read_value(connection) == request.attribute_value + and (await attribute.read_value(connection)) == request.attribute_value and pdu_space_available >= 4 ): # TODO: check permissions @@ -702,7 +704,8 @@ def on_att_find_by_type_value_request(self, connection, request): self.send_response(connection, response) - def on_att_read_by_type_request(self, connection, request): + @AsyncRunner.run_in_task() + async def on_att_read_by_type_request(self, connection, request): ''' See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request ''' @@ -725,7 +728,7 @@ def on_att_read_by_type_request(self, connection, request): and pdu_space_available ): try: - attribute_value = attribute.read_value(connection) + attribute_value = await attribute.read_value(connection) except ATT_Error as error: # If the first attribute is unreadable, return an error # Otherwise return attributes up to this point @@ -767,14 +770,15 @@ def on_att_read_by_type_request(self, connection, request): self.send_response(connection, response) - def on_att_read_request(self, connection, request): + @AsyncRunner.run_in_task() + async def on_att_read_request(self, connection, request): ''' See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request ''' if attribute := self.get_attribute(request.attribute_handle): try: - value = attribute.read_value(connection) + value = await attribute.read_value(connection) except ATT_Error as error: response = ATT_Error_Response( request_opcode_in_error=request.op_code, @@ -792,14 +796,15 @@ def on_att_read_request(self, connection, request): ) self.send_response(connection, response) - def on_att_read_blob_request(self, connection, request): + @AsyncRunner.run_in_task() + async def on_att_read_blob_request(self, connection, request): ''' See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request ''' if attribute := self.get_attribute(request.attribute_handle): try: - value = attribute.read_value(connection) + value = await attribute.read_value(connection) except ATT_Error as error: response = ATT_Error_Response( request_opcode_in_error=request.op_code, @@ -836,7 +841,8 @@ def on_att_read_blob_request(self, connection, request): ) self.send_response(connection, response) - def on_att_read_by_group_type_request(self, connection, request): + @AsyncRunner.run_in_task() + async def on_att_read_by_group_type_request(self, connection, request): ''' See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request ''' @@ -864,7 +870,7 @@ def on_att_read_by_group_type_request(self, connection, request): ): # No need to catch permission errors here, since these attributes # must all be world-readable - attribute_value = attribute.read_value(connection) + attribute_value = await attribute.read_value(connection) # Check the attribute value size max_attribute_size = min(connection.att_mtu - 6, 251) if len(attribute_value) > max_attribute_size: @@ -903,7 +909,8 @@ def on_att_read_by_group_type_request(self, connection, request): self.send_response(connection, response) - def on_att_write_request(self, connection, request): + @AsyncRunner.run_in_task() + async def on_att_write_request(self, connection, request): ''' See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request ''' @@ -936,12 +943,13 @@ def on_att_write_request(self, connection, request): return # Accept the value - attribute.write_value(connection, request.attribute_value) + await attribute.write_value(connection, request.attribute_value) # Done self.send_response(connection, ATT_Write_Response()) - def on_att_write_command(self, connection, request): + @AsyncRunner.run_in_task() + async def on_att_write_command(self, connection, request): ''' See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command ''' @@ -959,7 +967,7 @@ def on_att_write_command(self, connection, request): # Accept the value try: - attribute.write_value(connection, request.attribute_value) + await attribute.write_value(connection, request.attribute_value) except Exception as error: logger.exception(f'!!! ignoring exception: {error}') diff --git a/bumble/profiles/asha_service.py b/bumble/profiles/asha_service.py index 412b28a1..acbc47e3 100644 --- a/bumble/profiles/asha_service.py +++ b/bumble/profiles/asha_service.py @@ -18,7 +18,7 @@ # ----------------------------------------------------------------------------- import struct import logging -from typing import List +from typing import List, Optional from bumble import l2cap from ..core import AdvertisingData @@ -67,7 +67,7 @@ def on_volume_write(connection, value): self.emit('volume', connection, value[0]) # Handler for audio control commands - def on_audio_control_point_write(connection: Connection, value): + def on_audio_control_point_write(connection: Optional[Connection], value): logger.info(f'--- AUDIO CONTROL POINT Write:{value.hex()}') opcode = value[0] if opcode == AshaService.OPCODE_START: diff --git a/bumble/profiles/bap.py b/bumble/profiles/bap.py index 4785997b..dd57f01c 100644 --- a/bumble/profiles/bap.py +++ b/bumble/profiles/bap.py @@ -114,7 +114,7 @@ class SamplingFrequency(enum.IntEnum): '''Bluetooth Assigned Numbers, Section 6.12.5.1 - Sampling Frequency''' # fmt: off - FREQ_8000 = 0x01 + FREQ_8000 = 0x01 FREQ_11025 = 0x02 FREQ_16000 = 0x03 FREQ_22050 = 0x04 @@ -430,7 +430,7 @@ class AseResponseCode(enum.IntEnum): REJECTED_METADATA = 0x0B INVALID_METADATA = 0x0C INSUFFICIENT_RESOURCES = 0x0D - UNSPECIFIED_ERROR = 0x0E + UNSPECIFIED_ERROR = 0x0E class AseReasonCode(enum.IntEnum): @@ -1066,7 +1066,7 @@ def value(self, _new_value): # Readonly. Do nothing in the setter. pass - def on_read(self, _: device.Connection) -> bytes: + def on_read(self, _: Optional[device.Connection]) -> bytes: return self.value def __str__(self) -> str: diff --git a/bumble/profiles/csip.py b/bumble/profiles/csip.py index c82b413a..cb17f48f 100644 --- a/bumble/profiles/csip.py +++ b/bumble/profiles/csip.py @@ -152,7 +152,7 @@ def __init__( super().__init__(characteristics) - def on_sirk_read(self, _connection: device.Connection) -> bytes: + def on_sirk_read(self, _connection: Optional[device.Connection]) -> bytes: if self.set_identity_resolving_key_type == SirkType.PLAINTEXT: return bytes([SirkType.PLAINTEXT]) + self.set_identity_resolving_key else: diff --git a/bumble/utils.py b/bumble/utils.py index 81e150cc..552140b1 100644 --- a/bumble/utils.py +++ b/bumble/utils.py @@ -280,17 +280,14 @@ def decorator(func): def wrapper(*args, **kwargs): coroutine = func(*args, **kwargs) if queue is None: - # Create a task to run the coroutine + # Spawn the coroutine as a task async def run(): try: await coroutine except Exception: - logger.warning( - f'{color("!!! Exception in wrapper:", "red")} ' - f'{traceback.format_exc()}' - ) + logger.exception(color("!!! Exception in wrapper:", "red")) - asyncio.create_task(run()) + AsyncRunner.spawn(run()) else: # Queue the coroutine to be awaited by the work queue queue.enqueue(coroutine) diff --git a/tests/bap_test.py b/tests/bap_test.py index d9d12596..bc223c14 100644 --- a/tests/bap_test.py +++ b/tests/bap_test.py @@ -48,7 +48,8 @@ PublishedAudioCapabilitiesService, PublishedAudioCapabilitiesServiceProxy, ) -from .test_utils import TwoDevices +from tests.test_utils import TwoDevices + # ----------------------------------------------------------------------------- # Logging diff --git a/tests/gatt_test.py b/tests/gatt_test.py index 85b40a98..19dff2f2 100644 --- a/tests/gatt_test.py +++ b/tests/gatt_test.py @@ -20,11 +20,10 @@ import os import struct import pytest -from unittest.mock import Mock, ANY +from unittest.mock import AsyncMock, Mock, ANY from bumble.controller import Controller from bumble.gatt_client import CharacteristicProxy -from bumble.gatt_server import Server from bumble.link import LocalLink from bumble.device import Device, Peer from bumble.host import Host @@ -120,9 +119,9 @@ def decode_value(self, value_bytes): Characteristic.READABLE, 123, ) - x = c.read_value(None) + x = await c.read_value(None) assert x == bytes([123]) - c.write_value(None, bytes([122])) + await c.write_value(None, bytes([122])) assert c.value == 122 class FooProxy(CharacteristicProxy): @@ -152,7 +151,22 @@ def decode_value(self, value_bytes): bytes([123]), ) - service = Service('3A657F47-D34F-46B3-B1EC-698E29B6B829', [characteristic]) + async def async_read(connection): + return 0x05060708 + + async_characteristic = PackedCharacteristicAdapter( + Characteristic( + '2AB7E91B-43E8-4F73-AC3B-80C1683B47F9', + Characteristic.Properties.READ, + Characteristic.READABLE, + CharacteristicValue(read=async_read), + ), + '>I', + ) + + service = Service( + '3A657F47-D34F-46B3-B1EC-698E29B6B829', [characteristic, async_characteristic] + ) server.add_service(service) await client.power_on() @@ -184,6 +198,13 @@ def decode_value(self, value_bytes): await async_barrier() assert characteristic.value == bytes([50]) + c2 = peer.get_characteristics_by_uuid(async_characteristic.uuid) + assert len(c2) == 1 + c2 = c2[0] + cd2 = PackedCharacteristicAdapter(c2, ">I") + cd2v = await cd2.read_value() + assert cd2v == 0x05060708 + last_change = None def on_change(value): @@ -285,7 +306,8 @@ async def test_attribute_getters(): # ----------------------------------------------------------------------------- -def test_CharacteristicAdapter(): +@pytest.mark.asyncio +async def test_CharacteristicAdapter(): # Check that the CharacteristicAdapter base class is transparent v = bytes([1, 2, 3]) c = Characteristic( @@ -296,11 +318,11 @@ def test_CharacteristicAdapter(): ) a = CharacteristicAdapter(c) - value = a.read_value(None) + value = await a.read_value(None) assert value == v v = bytes([3, 4, 5]) - a.write_value(None, v) + await a.write_value(None, v) assert c.value == v # Simple delegated adapter @@ -308,11 +330,11 @@ def test_CharacteristicAdapter(): c, lambda x: bytes(reversed(x)), lambda x: bytes(reversed(x)) ) - value = a.read_value(None) + value = await a.read_value(None) assert value == bytes(reversed(v)) v = bytes([3, 4, 5]) - a.write_value(None, v) + await a.write_value(None, v) assert a.value == bytes(reversed(v)) # Packed adapter with single element format @@ -321,10 +343,10 @@ def test_CharacteristicAdapter(): c.value = v a = PackedCharacteristicAdapter(c, '>H') - value = a.read_value(None) + value = await a.read_value(None) assert value == pv c.value = None - a.write_value(None, pv) + await a.write_value(None, pv) assert a.value == v # Packed adapter with multi-element format @@ -334,10 +356,10 @@ def test_CharacteristicAdapter(): c.value = (v1, v2) a = PackedCharacteristicAdapter(c, '>HH') - value = a.read_value(None) + value = await a.read_value(None) assert value == pv c.value = None - a.write_value(None, pv) + await a.write_value(None, pv) assert a.value == (v1, v2) # Mapped adapter @@ -348,10 +370,10 @@ def test_CharacteristicAdapter(): c.value = mapped a = MappedCharacteristicAdapter(c, '>HH', ('v1', 'v2')) - value = a.read_value(None) + value = await a.read_value(None) assert value == pv c.value = None - a.write_value(None, pv) + await a.write_value(None, pv) assert a.value == mapped # UTF-8 adapter @@ -360,27 +382,49 @@ def test_CharacteristicAdapter(): c.value = v a = UTF8CharacteristicAdapter(c) - value = a.read_value(None) + value = await a.read_value(None) assert value == ev c.value = None - a.write_value(None, ev) + await a.write_value(None, ev) assert a.value == v # ----------------------------------------------------------------------------- -def test_CharacteristicValue(): +@pytest.mark.asyncio +async def test_CharacteristicValue(): b = bytes([1, 2, 3]) - c = CharacteristicValue(read=lambda _: b) - x = c.read(None) + + async def read_value(connection): + return b + + c = CharacteristicValue(read=read_value) + x = await c.read(None) assert x == b - result = [] - c = CharacteristicValue( - write=lambda connection, value: result.append((connection, value)) - ) + m = Mock() + c = CharacteristicValue(write=m) z = object() c.write(z, b) - assert result == [(z, b)] + m.assert_called_once_with(z, b) + + +# ----------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_CharacteristicValue_async(): + b = bytes([1, 2, 3]) + + async def read_value(connection): + return b + + c = CharacteristicValue(read=read_value) + x = await c.read(None) + assert x == b + + m = AsyncMock() + c = CharacteristicValue(write=m) + z = object() + await c.write(z, b) + m.assert_called_once_with(z, b) # ----------------------------------------------------------------------------- @@ -961,12 +1005,18 @@ async def test_server_string(): # ----------------------------------------------------------------------------- async def async_main(): + test_UUID() + test_ATT_Error_Response() + test_ATT_Read_By_Group_Type_Request() await test_read_write() await test_read_write2() await test_subscribe_notify() await test_unsubscribe() await test_characteristic_encoding() await test_mtu_exchange() + await test_CharacteristicValue() + await test_CharacteristicValue_async() + await test_CharacteristicAdapter() # ----------------------------------------------------------------------------- @@ -1105,9 +1155,4 @@ def test_get_attribute_group(): # ----------------------------------------------------------------------------- if __name__ == '__main__': logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) - test_UUID() - test_ATT_Error_Response() - test_ATT_Read_By_Group_Type_Request() - test_CharacteristicValue() - test_CharacteristicAdapter() asyncio.run(async_main())