From 250c1e33954754c9ec3baf97354f58c1834a667e Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Mon, 13 Jun 2022 16:44:57 -0700 Subject: [PATCH] address PR comments --- apps/pair.py | 34 +++++++++++------------ bumble/device.py | 70 +++++++++++++++++++++++++++++++++++++++++++++--- bumble/hci.py | 3 +++ bumble/host.py | 6 +++++ 4 files changed, 92 insertions(+), 21 deletions(-) diff --git a/apps/pair.py b/apps/pair.py index 73d71c8f..7f896299 100644 --- a/apps/pair.py +++ b/apps/pair.py @@ -63,16 +63,12 @@ class Delegate(PairingDelegate): # We already asked the peer return - # For classic, just use the address - if self.mode == 'classic': - self.peer_name = str(self.peer.connection.peer_address) + # Try to get the peer's name + if self.peer: + peer_name = await get_peer_name(self.peer, self.mode) + self.peer_name = f'{peer_name or ""} [{self.peer.connection.peer_address}]' else: - # Try to get the peer's name - if self.peer: - peer_name = await get_peer_name(self.peer) - self.peer_name = f'{peer_name or ""} [{self.peer.connection.peer_address}]' - else: - self.peer_name = '[?]' + self.peer_name = '[?]' async def accept(self): if self.prompt: @@ -107,7 +103,7 @@ class Delegate(PairingDelegate): print(color(f'### Pairing with {self.peer_name}', 'yellow')) print(color('###-----------------------------------', 'yellow')) while True: - response = await aioconsole.ainput(color(f'>>> Does the other device display {number:{digits}}? ', 'yellow')) + response = await aioconsole.ainput(color(f'>>> Does the other device display {number:0{digits}}? ', 'yellow')) response = response.lower().strip() if response == 'yes': return True @@ -144,14 +140,18 @@ class Delegate(PairingDelegate): # ----------------------------------------------------------------------------- -async def get_peer_name(peer): - services = await peer.discover_service(GATT_GENERIC_ACCESS_SERVICE) - if not services: - return None +async def get_peer_name(peer, mode): + if mode == 'classic': + return await peer.request_name() + else: + # Try to get the peer name from GATT + services = await peer.discover_service(GATT_GENERIC_ACCESS_SERVICE) + if not services: + return None - values = await peer.read_characteristics_by_uuid(GATT_DEVICE_NAME_CHARACTERISTIC, services[0]) - if values: - return values[0].decode('utf-8') + values = await peer.read_characteristics_by_uuid(GATT_DEVICE_NAME_CHARACTERISTIC, services[0]) + if values: + return values[0].decode('utf-8') # ----------------------------------------------------------------------------- diff --git a/bumble/device.py b/bumble/device.py index e56e34e3..6e06e168 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -137,6 +137,10 @@ class Peer: def get_characteristics_by_uuid(self, uuid, service = None): return self.gatt_client.get_characteristics_by_uuid(uuid, service) + # [Classic only] + async def request_name(self): + return await self.connection.request_remote_name() + def __str__(self): return f'{self.connection.peer_address} as {self.connection.role_name}' @@ -176,6 +180,7 @@ class Connection(CompositeEventEmitter): self.transport = transport self.peer_address = peer_address self.peer_resolvable_address = peer_resolvable_address + self.peer_name = None # Classic only self.role = role self.parameters = parameters self.encryption = 0 @@ -231,6 +236,10 @@ class Connection(CompositeEventEmitter): supervision_timeout ) + # [Classic only] + async def request_remote_name(self): + return await self.device.request_remote_name(self) + def __str__(self): return f'Connection(handle=0x{self.handle:04X}, role={self.role_name}, address={self.peer_address})' @@ -290,8 +299,7 @@ def with_connection_from_handle(function): @functools.wraps(function) def wrapper(self, connection_handle, *args, **kwargs): if (connection := self.lookup_connection(connection_handle)) is None: - logger.warn(f'no connection found for handle 0x{connection_handle:04X}') - return + raise ValueError('no connection for handle') return function(self, connection, *args, **kwargs) return wrapper @@ -303,7 +311,7 @@ def with_connection_from_address(function): for connection in self.connections.values(): if connection.peer_address == address: return function(self, connection, *args, **kwargs) - logger.warn(f'no connection found for address {address}') + raise ValueError('no connection for address') return wrapper @@ -1044,6 +1052,40 @@ class Device(CompositeEventEmitter): connection.remove_listener('connection_encryption_change', on_encryption_change) connection.remove_listener('connection_encryption_failure', on_encryption_failure) + # [Classic only] + async def request_remote_name(self, connection): + # Set up event handlers + pending_name = asyncio.get_running_loop().create_future() + + def on_remote_name(): + pending_name.set_result(connection.peer_name) + + def on_remote_name_failure(error_code): + pending_name.set_exception(HCI_Error(error_code)) + + connection.on('remote_name', on_remote_name) + connection.on('remote_name_failure', on_remote_name_failure) + + try: + result = await self.send_command( + HCI_Remote_Name_Request_Command( + bd_addr = connection.peer_address, + page_scan_repetition_mode = HCI_Remote_Name_Request_Command.R0, # TODO investigate other options + reserved = 0, + clock_offset = 0 # TODO investigate non-0 values + ) + ) + + if result.status != HCI_COMMAND_STATUS_PENDING: + logger.warn(f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}') + raise HCI_Error(result.status) + + # Wait for the result + return await pending_name + finally: + connection.remove_listener('remote_name', on_remote_name) + connection.remove_listener('remote_name_failure', on_remote_name_failure) + # [Classic only] @host_event_handler def on_link_key(self, bd_addr, link_key, key_type): @@ -1188,10 +1230,12 @@ class Device(CompositeEventEmitter): # Compute the authentication requirements authentication_requirements = ( + # No Bonding ( HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS, HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS ), + # General Bonding ( HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS, HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS @@ -1203,7 +1247,7 @@ class Device(CompositeEventEmitter): HCI_IO_Capability_Request_Reply_Command( bd_addr = connection.peer_address, io_capability = io_capability, - oob_data_present = 0x00, + oob_data_present = 0x00, # Not present authentication_requirements = authentication_requirements ) ) @@ -1272,6 +1316,24 @@ class Device(CompositeEventEmitter): HCI_User_Passkey_Request_Negative_Reply_Command(bd_addr=connection.peer_address) ) + # [Classic only] + @host_event_handler + @with_connection_from_address + def on_remote_name(self, connection, remote_name): + # Try to decode the name + try: + connection.peer_name = remote_name.decode('utf-8') + connection.emit('remote_name') + except UnicodeDecodeError as error: + logger.warning('peer name is not valid UTF-8') + connection.emit('remote_name_failure', error) + + # [Classic only] + @host_event_handler + @with_connection_from_address + def on_remote_name_failure(self, connection, error): + connection.emit('remote_name_failure', error) + @host_event_handler @with_connection_from_handle def on_connection_encryption_change(self, connection, encryption): diff --git a/bumble/hci.py b/bumble/hci.py index f301f3d6..8358e30a 100644 --- a/bumble/hci.py +++ b/bumble/hci.py @@ -1378,6 +1378,9 @@ class HCI_Remote_Name_Request_Command(HCI_Command): ''' See Bluetooth spec @ 7.1.19 Remote Name Request Command ''' + R0 = 0x00 + R1 = 0x01 + R2 = 0x02 # ----------------------------------------------------------------------------- diff --git a/bumble/host.py b/bumble/host.py index db58dc6d..da6ef556 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -593,3 +593,9 @@ class Host(EventEmitter): event.extended_inquiry_response, event.rssi ) + + def on_hci_remote_name_request_complete_event(self, event): + if event.status != HCI_SUCCESS: + self.emit('remote_name_failure', event.bd_addr, event.status) + else: + self.emit('remote_name', event.bd_addr, event.remote_name)