Compare commits

..

2 Commits

Author SHA1 Message Date
Gilles Boccon-Gibod 07270240e3 fix merge issues 2023-12-26 11:33:15 -08:00
Gilles Boccon-Gibod d12b15b5d4 Merge/rebase 2023-12-26 11:25:54 -08:00
18 changed files with 1033 additions and 385 deletions
+4 -4
View File
@@ -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):
+37
View File
@@ -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
View File
@@ -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))
# -----------------------------------------------------------------------------
+8 -3
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+48 -60
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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):
+2 -2
View File
@@ -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:
+3 -3
View File
@@ -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:
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -48,8 +48,7 @@ from bumble.profiles.bap import (
PublishedAudioCapabilitiesService,
PublishedAudioCapabilitiesServiceProxy,
)
from tests.test_utils import TwoDevices
from .test_utils import TwoDevices
# -----------------------------------------------------------------------------
# Logging
+138
View File
@@ -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
View File
@@ -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())