forked from auracaster/bumble_mirror
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07270240e3 | |||
| d12b15b5d4 |
+4
-4
@@ -777,7 +777,7 @@ class ConsoleApp:
|
||||
if not service:
|
||||
continue
|
||||
values = [
|
||||
await attribute.read_value(connection)
|
||||
attribute.read_value(connection)
|
||||
for connection in self.device.connections.values()
|
||||
]
|
||||
if not values:
|
||||
@@ -796,11 +796,11 @@ class ConsoleApp:
|
||||
if not characteristic:
|
||||
continue
|
||||
values = [
|
||||
await attribute.read_value(connection)
|
||||
attribute.read_value(connection)
|
||||
for connection in self.device.connections.values()
|
||||
]
|
||||
if not values:
|
||||
values = [await attribute.read_value(None)]
|
||||
values = [attribute.read_value(None)]
|
||||
|
||||
# TODO: future optimization: convert CCCD value to human readable string
|
||||
|
||||
@@ -944,7 +944,7 @@ class ConsoleApp:
|
||||
|
||||
# send data to any subscribers
|
||||
if isinstance(attribute, Characteristic):
|
||||
await attribute.write_value(None, value)
|
||||
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):
|
||||
|
||||
@@ -24,6 +24,10 @@ from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
from bumble.colors import color
|
||||
from bumble.core import name_or_number
|
||||
from bumble.hci import (
|
||||
HCI_READ_LOCAL_EXTENDED_FEATURES_COMMAND,
|
||||
HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND,
|
||||
HCI_Read_Local_Extended_Features_Command,
|
||||
HCI_Read_Local_Supported_Features_Command,
|
||||
map_null_terminated_utf8_string,
|
||||
HCI_SUCCESS,
|
||||
HCI_LE_SUPPORTED_FEATURES_NAMES,
|
||||
@@ -58,6 +62,36 @@ def command_succeeded(response):
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_common_info(host):
|
||||
if host.supports_command(HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND):
|
||||
response = await host.send_command(HCI_Read_Local_Supported_Features_Command())
|
||||
if response.return_parameters.status == HCI_SUCCESS:
|
||||
print()
|
||||
print(color('LMP Features:', 'yellow'))
|
||||
# TODO: support printing discrete enum values
|
||||
print(' ', response.return_parameters.lmp_features.hex())
|
||||
|
||||
if host.supports_command(HCI_READ_LOCAL_EXTENDED_FEATURES_COMMAND):
|
||||
response = await host.send_command(
|
||||
HCI_Read_Local_Extended_Features_Command(page_number=0)
|
||||
)
|
||||
if response.return_parameters.status == HCI_SUCCESS:
|
||||
if response.return_parameters.max_page_number > 0:
|
||||
print()
|
||||
print(color('Extended LMP Features:', 'yellow'))
|
||||
|
||||
for page in range(1, response.return_parameters.max_page_number + 1):
|
||||
response = await host.send_command(
|
||||
HCI_Read_Local_Extended_Features_Command(page_number=page)
|
||||
)
|
||||
|
||||
if response.return_parameters.status == HCI_SUCCESS:
|
||||
# TODO: support printing discrete enum values
|
||||
print(f' Page {page}:')
|
||||
print(' ', response.return_parameters.extended_lmp_features.hex())
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_classic_info(host):
|
||||
if host.supports_command(HCI_READ_BD_ADDR_COMMAND):
|
||||
@@ -162,6 +196,9 @@ async def async_main(transport):
|
||||
)
|
||||
print(color(' LMP Subversion:', 'green'), host.local_version.lmp_subversion)
|
||||
|
||||
# Get the common info
|
||||
await get_common_info(host)
|
||||
|
||||
# Get the Classic info
|
||||
await get_classic_info(host)
|
||||
|
||||
|
||||
+15
-15
@@ -17,31 +17,25 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
from bumble.controller import Controller
|
||||
import click
|
||||
|
||||
from bumble.controller import Controller, Options
|
||||
from bumble.link import LocalLink
|
||||
from bumble.transport import open_transport_or_link
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main():
|
||||
if len(sys.argv) != 3:
|
||||
print(
|
||||
'Usage: controllers.py <hci-transport-1> <hci-transport-2> '
|
||||
'[<hci-transport-3> ...]'
|
||||
)
|
||||
print('example: python controllers.py pty:ble1 pty:ble2')
|
||||
return
|
||||
|
||||
async def async_main(extended_advertising, transport_names):
|
||||
# Create a local link to attach the controllers to
|
||||
link = LocalLink()
|
||||
|
||||
# Create a transport and controller for all requested names
|
||||
transports = []
|
||||
controllers = []
|
||||
for index, transport_name in enumerate(sys.argv[1:]):
|
||||
options = Options(extended_advertising=extended_advertising)
|
||||
for index, transport_name in enumerate(transport_names):
|
||||
transport = await open_transport_or_link(transport_name)
|
||||
transports.append(transport)
|
||||
controller = Controller(
|
||||
@@ -49,6 +43,7 @@ async def async_main():
|
||||
host_source=transport.source,
|
||||
host_sink=transport.sink,
|
||||
link=link,
|
||||
options=options,
|
||||
)
|
||||
controllers.append(controller)
|
||||
|
||||
@@ -61,9 +56,14 @@ async def async_main():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(async_main())
|
||||
@click.command()
|
||||
@click.option(
|
||||
'--extended-advertising', is_flag=True, help="Enable extended advertising"
|
||||
)
|
||||
@click.argument('transports', nargs=-1, required=True)
|
||||
def main(extended_advertising, transports):
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
asyncio.run(async_main(extended_advertising, transports))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -253,7 +253,7 @@ class Relay:
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
def main():
|
||||
async def async_main():
|
||||
# Check the Python version
|
||||
if sys.version_info < (3, 6, 1):
|
||||
print('ERROR: Python 3.6.1 or higher is required')
|
||||
@@ -280,8 +280,13 @@ def main():
|
||||
|
||||
# Start a relay
|
||||
relay = Relay(args.port)
|
||||
asyncio.get_event_loop().run_until_complete(relay.start())
|
||||
asyncio.get_event_loop().run_forever()
|
||||
async with relay.start():
|
||||
await asyncio.Future()
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
def main():
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
+11
-53
@@ -25,21 +25,9 @@
|
||||
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
|
||||
@@ -734,38 +722,12 @@ class ATT_Handle_Value_Confirmation(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AttributeValue:
|
||||
'''
|
||||
Attribute value where reading and/or writing is delegated to functions
|
||||
passed as arguments to the constructor.
|
||||
'''
|
||||
class ConnectionValue(Protocol):
|
||||
def read(self, connection) -> bytes:
|
||||
...
|
||||
|
||||
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)
|
||||
|
||||
return None
|
||||
def write(self, connection, value: bytes) -> None:
|
||||
...
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -808,13 +770,13 @@ class Attribute(EventEmitter):
|
||||
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
|
||||
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
|
||||
|
||||
value: Union[bytes, AttributeValue]
|
||||
value: Union[str, bytes, ConnectionValue]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attribute_type: Union[str, bytes, UUID],
|
||||
permissions: Union[str, Attribute.Permissions],
|
||||
value: Union[str, bytes, AttributeValue] = b'',
|
||||
value: Union[str, bytes, ConnectionValue] = b'',
|
||||
) -> None:
|
||||
EventEmitter.__init__(self)
|
||||
self.handle = 0
|
||||
@@ -844,7 +806,7 @@ class Attribute(EventEmitter):
|
||||
def decode_value(self, value_bytes: bytes) -> Any:
|
||||
return value_bytes
|
||||
|
||||
async def read_value(self, connection: Optional[Connection]) -> bytes:
|
||||
def read_value(self, connection: Optional[Connection]) -> bytes:
|
||||
if (
|
||||
(self.permissions & self.READ_REQUIRES_ENCRYPTION)
|
||||
and connection is not None
|
||||
@@ -870,8 +832,6 @@ class Attribute(EventEmitter):
|
||||
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
|
||||
@@ -881,7 +841,7 @@ class Attribute(EventEmitter):
|
||||
|
||||
return self.encode_value(value)
|
||||
|
||||
async def write_value(self, connection: Connection, value_bytes: bytes) -> None:
|
||||
def write_value(self, connection: Connection, value_bytes: bytes) -> None:
|
||||
if (
|
||||
self.permissions & self.WRITE_REQUIRES_ENCRYPTION
|
||||
) and not connection.encryption:
|
||||
@@ -904,9 +864,7 @@ class Attribute(EventEmitter):
|
||||
|
||||
if hasattr(self.value, 'write'):
|
||||
try:
|
||||
result = self.value.write(connection, value)
|
||||
if inspect.isawaitable(result):
|
||||
await result
|
||||
self.value.write(connection, value) # pylint: disable=not-callable
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
|
||||
+665
-110
File diff suppressed because it is too large
Load Diff
+48
-60
@@ -23,28 +23,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import enum
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from typing import (
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
from typing import Optional, Sequence, Iterable, List, Union
|
||||
|
||||
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
|
||||
from .colors import color
|
||||
from .core import UUID, get_dict_key_by_value
|
||||
from .att import Attribute
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -534,43 +522,56 @@ class CharacteristicDeclaration(Attribute):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CharacteristicValue(AttributeValue):
|
||||
"""Same as AttributeValue, for backward compatibility"""
|
||||
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 CharacteristicAdapter:
|
||||
'''
|
||||
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.
|
||||
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.
|
||||
'''
|
||||
|
||||
read_value: Callable
|
||||
write_value: Callable
|
||||
|
||||
def __init__(self, characteristic: Union[Characteristic, AttributeProxy]):
|
||||
def __init__(self, characteristic):
|
||||
self.wrapped_characteristic = characteristic
|
||||
self.subscribers: Dict[
|
||||
Callable, Callable
|
||||
] = {} # Map from subscriber to proxy subscriber
|
||||
self.subscribers = {} # Map from subscriber to proxy subscriber
|
||||
|
||||
if isinstance(characteristic, Characteristic):
|
||||
self.read_value = self.read_encoded_value
|
||||
self.write_value = self.write_encoded_value
|
||||
else:
|
||||
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:
|
||||
self.read_value = self.read_encoded_value
|
||||
self.write_value = self.write_encoded_value
|
||||
|
||||
if hasattr(self.wrapped_characteristic, 'subscribe'):
|
||||
self.subscribe = self.wrapped_subscribe
|
||||
|
||||
if hasattr(self.wrapped_characteristic, 'unsubscribe'):
|
||||
self.unsubscribe = self.wrapped_unsubscribe
|
||||
|
||||
def __getattr__(self, name):
|
||||
@@ -589,13 +590,11 @@ class CharacteristicAdapter:
|
||||
else:
|
||||
setattr(self.wrapped_characteristic, name, value)
|
||||
|
||||
async def read_encoded_value(self, connection):
|
||||
return self.encode_value(
|
||||
await self.wrapped_characteristic.read_value(connection)
|
||||
)
|
||||
def read_encoded_value(self, connection):
|
||||
return self.encode_value(self.wrapped_characteristic.read_value(connection))
|
||||
|
||||
async def write_encoded_value(self, connection, value):
|
||||
return await self.wrapped_characteristic.write_value(
|
||||
def write_encoded_value(self, connection, value):
|
||||
return self.wrapped_characteristic.write_value(
|
||||
connection, self.decode_value(value)
|
||||
)
|
||||
|
||||
@@ -730,24 +729,13 @@ 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={value_str})'
|
||||
f'value={self.read_value(None).hex()})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ClientCharacteristicConfigurationBits(enum.IntFlag):
|
||||
'''
|
||||
See Vol 3, Part G - 3.3.3.3 - Table 3.11 Client Characteristic Configuration bit
|
||||
|
||||
+21
-29
@@ -31,9 +31,9 @@ import struct
|
||||
from typing import List, Tuple, Optional, TypeVar, Type, Dict, Iterable, TYPE_CHECKING
|
||||
from pyee import EventEmitter
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID
|
||||
from bumble.att import (
|
||||
from .colors import color
|
||||
from .core import UUID
|
||||
from .att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
@@ -60,7 +60,7 @@ from bumble.att import (
|
||||
ATT_Write_Response,
|
||||
Attribute,
|
||||
)
|
||||
from bumble.gatt import (
|
||||
from .gatt import (
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
GATT_MAX_ATTRIBUTE_VALUE_SIZE,
|
||||
@@ -74,7 +74,6 @@ from bumble.gatt import (
|
||||
Descriptor,
|
||||
Service,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
@@ -380,7 +379,7 @@ class Server(EventEmitter):
|
||||
|
||||
# Get or encode the value
|
||||
value = (
|
||||
await attribute.read_value(connection)
|
||||
attribute.read_value(connection)
|
||||
if value is None
|
||||
else attribute.encode_value(value)
|
||||
)
|
||||
@@ -423,7 +422,7 @@ class Server(EventEmitter):
|
||||
|
||||
# Get or encode the value
|
||||
value = (
|
||||
await attribute.read_value(connection)
|
||||
attribute.read_value(connection)
|
||||
if value is None
|
||||
else attribute.encode_value(value)
|
||||
)
|
||||
@@ -651,8 +650,7 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
async def on_att_find_by_type_value_request(self, connection, request):
|
||||
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
|
||||
'''
|
||||
@@ -660,13 +658,13 @@ class Server(EventEmitter):
|
||||
# Build list of returned attributes
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
attributes = []
|
||||
async for attribute in (
|
||||
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 (await attribute.read_value(connection)) == request.attribute_value
|
||||
and attribute.read_value(connection) == request.attribute_value
|
||||
and pdu_space_available >= 4
|
||||
):
|
||||
# TODO: check permissions
|
||||
@@ -704,8 +702,7 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
async def on_att_read_by_type_request(self, connection, request):
|
||||
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
|
||||
'''
|
||||
@@ -728,7 +725,7 @@ class Server(EventEmitter):
|
||||
and pdu_space_available
|
||||
):
|
||||
try:
|
||||
attribute_value = await attribute.read_value(connection)
|
||||
attribute_value = 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
|
||||
@@ -770,15 +767,14 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
async def on_att_read_request(self, connection, request):
|
||||
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 = await attribute.read_value(connection)
|
||||
value = attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
@@ -796,15 +792,14 @@ class Server(EventEmitter):
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
async def on_att_read_blob_request(self, connection, request):
|
||||
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 = await attribute.read_value(connection)
|
||||
value = attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
@@ -841,8 +836,7 @@ class Server(EventEmitter):
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
async def on_att_read_by_group_type_request(self, connection, request):
|
||||
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
|
||||
'''
|
||||
@@ -870,7 +864,7 @@ class Server(EventEmitter):
|
||||
):
|
||||
# No need to catch permission errors here, since these attributes
|
||||
# must all be world-readable
|
||||
attribute_value = await attribute.read_value(connection)
|
||||
attribute_value = 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:
|
||||
@@ -909,8 +903,7 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
async def on_att_write_request(self, connection, request):
|
||||
def on_att_write_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
|
||||
'''
|
||||
@@ -943,13 +936,12 @@ class Server(EventEmitter):
|
||||
return
|
||||
|
||||
# Accept the value
|
||||
await attribute.write_value(connection, request.attribute_value)
|
||||
attribute.write_value(connection, request.attribute_value)
|
||||
|
||||
# Done
|
||||
self.send_response(connection, ATT_Write_Response())
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
async def on_att_write_command(self, connection, request):
|
||||
def on_att_write_command(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
|
||||
'''
|
||||
@@ -967,7 +959,7 @@ class Server(EventEmitter):
|
||||
|
||||
# Accept the value
|
||||
try:
|
||||
await attribute.write_value(connection, request.attribute_value)
|
||||
attribute.write_value(connection, request.attribute_value)
|
||||
except Exception as error:
|
||||
logger.exception(f'!!! ignoring exception: {error}')
|
||||
|
||||
|
||||
+8
-4
@@ -3266,7 +3266,9 @@ class HCI_Read_Local_Supported_Commands_Command(HCI_Command):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command()
|
||||
@HCI_Command.command(
|
||||
return_parameters_fields=[('status', STATUS_SPEC), ('lmp_features', 8)]
|
||||
)
|
||||
class HCI_Read_Local_Supported_Features_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.4.3 Read Local Supported Features Command
|
||||
@@ -3279,7 +3281,7 @@ class HCI_Read_Local_Supported_Features_Command(HCI_Command):
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('page_number', 1),
|
||||
('maximum_page_number', 1),
|
||||
('max_page_number', 1),
|
||||
('extended_lmp_features', 8),
|
||||
],
|
||||
)
|
||||
@@ -3448,7 +3450,9 @@ class HCI_LE_Set_Advertising_Parameters_Command(HCI_Command):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command()
|
||||
@HCI_Command.command(
|
||||
return_parameters_fields=[('status', STATUS_SPEC), ('tx_power_level', 1)]
|
||||
)
|
||||
class HCI_LE_Read_Advertising_Physical_Channel_Tx_Power_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.6 LE Read Advertising Physical Channel Tx Power Command
|
||||
@@ -5829,7 +5833,7 @@ class HCI_Inquiry_Result_With_RSSI_Event(HCI_Event):
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
('page_number', 1),
|
||||
('maximum_page_number', 1),
|
||||
('max_page_number', 1),
|
||||
('extended_lmp_features', 8),
|
||||
]
|
||||
)
|
||||
|
||||
+2
-2
@@ -428,8 +428,8 @@ class Host(AbortableEventEmitter):
|
||||
and self.acl_packets_in_flight < self.hc_total_num_le_acl_data_packets
|
||||
):
|
||||
packet = self.acl_packet_queue.pop()
|
||||
self.send_hci_packet(packet)
|
||||
self.acl_packets_in_flight += 1
|
||||
self.send_hci_packet(packet)
|
||||
|
||||
def supports_command(self, command):
|
||||
# Find the support flag position for this command
|
||||
@@ -568,7 +568,7 @@ class Host(AbortableEventEmitter):
|
||||
else:
|
||||
logger.warning(
|
||||
color(
|
||||
'!!! {total_packets} completed but only '
|
||||
f'!!! {total_packets} completed but only '
|
||||
f'{self.acl_packets_in_flight} in flight'
|
||||
)
|
||||
)
|
||||
|
||||
+32
-18
@@ -95,11 +95,21 @@ class LocalLink:
|
||||
def on_address_changed(self, controller):
|
||||
pass
|
||||
|
||||
def send_advertising_data(self, sender_address, data):
|
||||
def send_advertising_data(self, sender_address, data, scan_response):
|
||||
# Send the advertising data to all controllers, except the sender
|
||||
for controller in self.controllers:
|
||||
if controller.random_address != sender_address:
|
||||
controller.on_link_advertising_data(sender_address, data)
|
||||
controller.on_link_advertising_data(sender_address, data, scan_response)
|
||||
|
||||
def send_extended_advertising_data(
|
||||
self, sender_address, event_type, data, scan_response
|
||||
):
|
||||
# Send the advertising data to all controllers, except the sender
|
||||
for controller in self.controllers:
|
||||
if controller.random_address != sender_address:
|
||||
controller.on_link_extended_advertising_data(
|
||||
sender_address, event_type, data, scan_response
|
||||
)
|
||||
|
||||
def send_acl_data(self, sender_controller, destination_address, transport, data):
|
||||
# Send the data to the first controller with a matching address
|
||||
@@ -151,30 +161,34 @@ class LocalLink:
|
||||
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
||||
|
||||
def on_disconnection_complete(
|
||||
self, central_address, peripheral_address, disconnect_command
|
||||
self, initiator_address, peer_address, disconnect_command
|
||||
):
|
||||
# Find the controller that initiated the disconnection
|
||||
if not (central_controller := self.find_controller(central_address)):
|
||||
if not (initiator_controller := self.find_controller(initiator_address)):
|
||||
logger.warning('!!! Initiating controller not found')
|
||||
return
|
||||
|
||||
# Disconnect from the first controller with a matching address
|
||||
if peripheral_controller := self.find_controller(peripheral_address):
|
||||
peripheral_controller.on_link_central_disconnected(
|
||||
central_address, disconnect_command.reason
|
||||
if peer_controller := self.find_controller(peer_address):
|
||||
peer_controller.on_link_peer_disconnected(
|
||||
initiator_address, disconnect_command.reason
|
||||
)
|
||||
|
||||
central_controller.on_link_peripheral_disconnection_complete(
|
||||
initiator_controller.on_link_initiated_disconnection_complete(
|
||||
disconnect_command, HCI_SUCCESS
|
||||
)
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
def disconnect(self, initiator_address, peer_address, disconnect_command):
|
||||
logger.debug(
|
||||
f'$$$ DISCONNECTION {central_address} -> '
|
||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||
f'$$$ DISCONNECTION {initiator_address} -> '
|
||||
f'{peer_address}: reason = {disconnect_command.reason}'
|
||||
)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
self.on_disconnection_complete,
|
||||
initiator_address,
|
||||
peer_address,
|
||||
disconnect_command,
|
||||
)
|
||||
args = [central_address, peripheral_address, disconnect_command]
|
||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def on_connection_encrypted(
|
||||
@@ -360,11 +374,11 @@ class RemoteLink:
|
||||
|
||||
async def on_left_received(self, address):
|
||||
if address in self.central_connections:
|
||||
self.controller.on_link_peripheral_disconnected(Address(address))
|
||||
self.controller.on_link_connection_lost(Address(address))
|
||||
self.central_connections.remove(address)
|
||||
|
||||
if address in self.peripheral_connections:
|
||||
self.controller.on_link_central_disconnected(
|
||||
self.controller.on_link_peer_disconnected(
|
||||
address, HCI_CONNECTION_TIMEOUT_ERROR
|
||||
)
|
||||
self.peripheral_connections.remove(address)
|
||||
@@ -384,7 +398,7 @@ class RemoteLink:
|
||||
async def on_advertisement_message_received(self, sender, advertisement):
|
||||
try:
|
||||
self.controller.on_link_advertising_data(
|
||||
Address(sender), bytes.fromhex(advertisement)
|
||||
Address(sender), bytes.fromhex(advertisement), b''
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('exception')
|
||||
@@ -424,7 +438,7 @@ class RemoteLink:
|
||||
# Notify the controller
|
||||
params = parse_parameters(message)
|
||||
reason = int(params.get('reason', str(HCI_CONNECTION_TIMEOUT_ERROR)))
|
||||
self.controller.on_link_central_disconnected(Address(sender), reason)
|
||||
self.controller.on_link_peer_disconnected(Address(sender), reason)
|
||||
|
||||
# Forget the connection
|
||||
if sender in self.peripheral_connections:
|
||||
@@ -471,7 +485,7 @@ class RemoteLink:
|
||||
async def send_advertising_data_to_relay(self, data):
|
||||
await self.send_targeted_message('*', f'advertisement:{data.hex()}')
|
||||
|
||||
def send_advertising_data(self, _, data):
|
||||
def send_advertising_data(self, _, data, scan_response):
|
||||
self.execute(partial(self.send_advertising_data_to_relay, data))
|
||||
|
||||
async def send_acl_data_to_relay(self, peer_address, data):
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from bumble import l2cap
|
||||
from ..core import AdvertisingData
|
||||
@@ -67,7 +67,7 @@ class AshaService(TemplateService):
|
||||
self.emit('volume', connection, value[0])
|
||||
|
||||
# Handler for audio control commands
|
||||
def on_audio_control_point_write(connection: Optional[Connection], value):
|
||||
def on_audio_control_point_write(connection: Connection, value):
|
||||
logger.info(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
||||
opcode = value[0]
|
||||
if opcode == AshaService.OPCODE_START:
|
||||
|
||||
@@ -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 @@ class AseStateMachine(gatt.Characteristic):
|
||||
# Readonly. Do nothing in the setter.
|
||||
pass
|
||||
|
||||
def on_read(self, _: Optional[device.Connection]) -> bytes:
|
||||
def on_read(self, _: device.Connection) -> bytes:
|
||||
return self.value
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@@ -152,7 +152,7 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
||||
|
||||
super().__init__(characteristics)
|
||||
|
||||
def on_sirk_read(self, _connection: Optional[device.Connection]) -> bytes:
|
||||
def on_sirk_read(self, _connection: device.Connection) -> bytes:
|
||||
if self.set_identity_resolving_key_type == SirkType.PLAINTEXT:
|
||||
return bytes([SirkType.PLAINTEXT]) + self.set_identity_resolving_key
|
||||
else:
|
||||
|
||||
+6
-3
@@ -280,14 +280,17 @@ class AsyncRunner:
|
||||
def wrapper(*args, **kwargs):
|
||||
coroutine = func(*args, **kwargs)
|
||||
if queue is None:
|
||||
# Spawn the coroutine as a task
|
||||
# Create a task to run the coroutine
|
||||
async def run():
|
||||
try:
|
||||
await coroutine
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception in wrapper:", "red"))
|
||||
logger.warning(
|
||||
f'{color("!!! Exception in wrapper:", "red")} '
|
||||
f'{traceback.format_exc()}'
|
||||
)
|
||||
|
||||
AsyncRunner.spawn(run())
|
||||
asyncio.create_task(run())
|
||||
else:
|
||||
# Queue the coroutine to be awaited by the work queue
|
||||
queue.enqueue(coroutine)
|
||||
|
||||
+1
-2
@@ -48,8 +48,7 @@ from bumble.profiles.bap import (
|
||||
PublishedAudioCapabilitiesService,
|
||||
PublishedAudioCapabilitiesServiceProxy,
|
||||
)
|
||||
from tests.test_utils import TwoDevices
|
||||
|
||||
from .test_utils import TwoDevices
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# Copyright 2023 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
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import pytest
|
||||
from typing import List, Optional
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from bumble.device import Connection, Device
|
||||
from bumble.host import Host
|
||||
from bumble.link import LocalLink
|
||||
from bumble.controller import Controller
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_CONNECTION_TERMINATED_BY_LOCAL_HOST_ERROR,
|
||||
HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR,
|
||||
)
|
||||
from bumble.transport import AsyncPipeSink
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class TwoDevices:
|
||||
connections: List[Optional[Connection]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.connections = [None, None]
|
||||
|
||||
self.link = LocalLink()
|
||||
self.controllers = [
|
||||
Controller('C1', link=self.link),
|
||||
Controller('C2', link=self.link),
|
||||
]
|
||||
self.devices = [
|
||||
Device(
|
||||
address=Address('F0:F1:F2:F3:F4:F5'),
|
||||
host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])),
|
||||
),
|
||||
Device(
|
||||
address=Address('F5:F4:F3:F2:F1:F0'),
|
||||
host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])),
|
||||
),
|
||||
]
|
||||
|
||||
self.paired = [None, None]
|
||||
|
||||
def on_connection(self, which, connection):
|
||||
self.connections[which] = connection
|
||||
connection.on(
|
||||
'disconnection', lambda reason: self.on_disconnection(which, reason)
|
||||
)
|
||||
|
||||
def on_disconnection(self, which, _):
|
||||
self.connections[which] = None
|
||||
|
||||
async def setup(self):
|
||||
self.devices[0].on(
|
||||
'connection', lambda connection: self.on_connection(0, connection)
|
||||
)
|
||||
self.devices[1].on(
|
||||
'connection', lambda connection: self.on_connection(1, connection)
|
||||
)
|
||||
|
||||
await self.devices[0].power_on()
|
||||
await self.devices[1].power_on()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_self_connection():
|
||||
two_devices = TwoDevices()
|
||||
await two_devices.setup()
|
||||
|
||||
await two_devices.devices[0].connect(two_devices.devices[1].random_address)
|
||||
|
||||
assert two_devices.connections[0] is not None
|
||||
assert two_devices.connections[1] is not None
|
||||
|
||||
mock0 = MagicMock()
|
||||
mock1 = MagicMock()
|
||||
two_devices.connections[0].once('disconnection', mock0)
|
||||
two_devices.connections[1].once('disconnection', mock1)
|
||||
await two_devices.connections[0].disconnect(
|
||||
HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR
|
||||
)
|
||||
mock0.assert_called_once_with(HCI_CONNECTION_TERMINATED_BY_LOCAL_HOST_ERROR)
|
||||
mock1.assert_called_once_with(
|
||||
HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR
|
||||
)
|
||||
|
||||
assert two_devices.connections[0] is None
|
||||
assert two_devices.connections[1] is None
|
||||
|
||||
await two_devices.devices[0].connect(two_devices.devices[1].random_address)
|
||||
|
||||
assert two_devices.connections[0] is not None
|
||||
assert two_devices.connections[1] is not None
|
||||
|
||||
mock0 = MagicMock()
|
||||
mock1 = MagicMock()
|
||||
two_devices.connections[0].once('disconnection', mock0)
|
||||
two_devices.connections[1].once('disconnection', mock1)
|
||||
await two_devices.connections[1].disconnect(
|
||||
HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR
|
||||
)
|
||||
mock1.assert_called_once_with(HCI_CONNECTION_TERMINATED_BY_LOCAL_HOST_ERROR)
|
||||
mock0.assert_called_once_with(
|
||||
HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR
|
||||
)
|
||||
|
||||
assert two_devices.connections[0] is None
|
||||
assert two_devices.connections[1] is None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run_test_controller():
|
||||
await test_self_connection()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(run_test_controller())
|
||||
+31
-76
@@ -20,10 +20,11 @@ import logging
|
||||
import os
|
||||
import struct
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, Mock, ANY
|
||||
from unittest.mock import 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
|
||||
@@ -119,9 +120,9 @@ async def test_characteristic_encoding():
|
||||
Characteristic.READABLE,
|
||||
123,
|
||||
)
|
||||
x = await c.read_value(None)
|
||||
x = c.read_value(None)
|
||||
assert x == bytes([123])
|
||||
await c.write_value(None, bytes([122]))
|
||||
c.write_value(None, bytes([122]))
|
||||
assert c.value == 122
|
||||
|
||||
class FooProxy(CharacteristicProxy):
|
||||
@@ -151,22 +152,7 @@ async def test_characteristic_encoding():
|
||||
bytes([123]),
|
||||
)
|
||||
|
||||
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]
|
||||
)
|
||||
service = Service('3A657F47-D34F-46B3-B1EC-698E29B6B829', [characteristic])
|
||||
server.add_service(service)
|
||||
|
||||
await client.power_on()
|
||||
@@ -198,13 +184,6 @@ async def test_characteristic_encoding():
|
||||
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):
|
||||
@@ -306,8 +285,7 @@ async def test_attribute_getters():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_CharacteristicAdapter():
|
||||
def test_CharacteristicAdapter():
|
||||
# Check that the CharacteristicAdapter base class is transparent
|
||||
v = bytes([1, 2, 3])
|
||||
c = Characteristic(
|
||||
@@ -318,11 +296,11 @@ async def test_CharacteristicAdapter():
|
||||
)
|
||||
a = CharacteristicAdapter(c)
|
||||
|
||||
value = await a.read_value(None)
|
||||
value = a.read_value(None)
|
||||
assert value == v
|
||||
|
||||
v = bytes([3, 4, 5])
|
||||
await a.write_value(None, v)
|
||||
a.write_value(None, v)
|
||||
assert c.value == v
|
||||
|
||||
# Simple delegated adapter
|
||||
@@ -330,11 +308,11 @@ async def test_CharacteristicAdapter():
|
||||
c, lambda x: bytes(reversed(x)), lambda x: bytes(reversed(x))
|
||||
)
|
||||
|
||||
value = await a.read_value(None)
|
||||
value = a.read_value(None)
|
||||
assert value == bytes(reversed(v))
|
||||
|
||||
v = bytes([3, 4, 5])
|
||||
await a.write_value(None, v)
|
||||
a.write_value(None, v)
|
||||
assert a.value == bytes(reversed(v))
|
||||
|
||||
# Packed adapter with single element format
|
||||
@@ -343,10 +321,10 @@ async def test_CharacteristicAdapter():
|
||||
c.value = v
|
||||
a = PackedCharacteristicAdapter(c, '>H')
|
||||
|
||||
value = await a.read_value(None)
|
||||
value = a.read_value(None)
|
||||
assert value == pv
|
||||
c.value = None
|
||||
await a.write_value(None, pv)
|
||||
a.write_value(None, pv)
|
||||
assert a.value == v
|
||||
|
||||
# Packed adapter with multi-element format
|
||||
@@ -356,10 +334,10 @@ async def test_CharacteristicAdapter():
|
||||
c.value = (v1, v2)
|
||||
a = PackedCharacteristicAdapter(c, '>HH')
|
||||
|
||||
value = await a.read_value(None)
|
||||
value = a.read_value(None)
|
||||
assert value == pv
|
||||
c.value = None
|
||||
await a.write_value(None, pv)
|
||||
a.write_value(None, pv)
|
||||
assert a.value == (v1, v2)
|
||||
|
||||
# Mapped adapter
|
||||
@@ -370,10 +348,10 @@ async def test_CharacteristicAdapter():
|
||||
c.value = mapped
|
||||
a = MappedCharacteristicAdapter(c, '>HH', ('v1', 'v2'))
|
||||
|
||||
value = await a.read_value(None)
|
||||
value = a.read_value(None)
|
||||
assert value == pv
|
||||
c.value = None
|
||||
await a.write_value(None, pv)
|
||||
a.write_value(None, pv)
|
||||
assert a.value == mapped
|
||||
|
||||
# UTF-8 adapter
|
||||
@@ -382,49 +360,27 @@ async def test_CharacteristicAdapter():
|
||||
c.value = v
|
||||
a = UTF8CharacteristicAdapter(c)
|
||||
|
||||
value = await a.read_value(None)
|
||||
value = a.read_value(None)
|
||||
assert value == ev
|
||||
c.value = None
|
||||
await a.write_value(None, ev)
|
||||
a.write_value(None, ev)
|
||||
assert a.value == v
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_CharacteristicValue():
|
||||
def test_CharacteristicValue():
|
||||
b = bytes([1, 2, 3])
|
||||
|
||||
async def read_value(connection):
|
||||
return b
|
||||
|
||||
c = CharacteristicValue(read=read_value)
|
||||
x = await c.read(None)
|
||||
c = CharacteristicValue(read=lambda _: b)
|
||||
x = c.read(None)
|
||||
assert x == b
|
||||
|
||||
m = Mock()
|
||||
c = CharacteristicValue(write=m)
|
||||
result = []
|
||||
c = CharacteristicValue(
|
||||
write=lambda connection, value: result.append((connection, value))
|
||||
)
|
||||
z = object()
|
||||
c.write(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)
|
||||
assert result == [(z, b)]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1005,18 +961,12 @@ Descriptor(handle=0x0009, type=UUID-16:2902 (Client Characteristic Configuration
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
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()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1155,4 +1105,9 @@ 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())
|
||||
|
||||
Reference in New Issue
Block a user