diff --git a/apps/console.py b/apps/console.py index 744d7da..93bb6ae 100644 --- a/apps/console.py +++ b/apps/console.py @@ -311,7 +311,7 @@ class ConsoleApp: rssi = '' if self.connection_rssi is None else rssi_bar(self.connection_rssi) if self.device: - if self.device.is_connecting: + if self.device.is_le_connecting: connection_state = 'CONNECTING' elif self.connected_peer: connection = self.connected_peer.connection @@ -574,7 +574,7 @@ class ConsoleApp: self.show_error('connection timed out') async def do_disconnect(self, params): - if self.device.connecting: + if self.device.is_le_connecting: await self.device.cancel_connection() else: if not self.connected_peer: @@ -877,9 +877,9 @@ class ScanResult: else: type_color = colors.cyan - name = self.ad_data.get(AdvertisingData.COMPLETE_LOCAL_NAME) + name = self.ad_data.get(AdvertisingData.COMPLETE_LOCAL_NAME, raw=True) if name is None: - name = self.ad_data.get(AdvertisingData.SHORTENED_LOCAL_NAME) + name = self.ad_data.get(AdvertisingData.SHORTENED_LOCAL_NAME, raw=True) if name: # Convert to string try: diff --git a/bumble/core.py b/bumble/core.py index 3d4ef41..302ee6a 100644 --- a/bumble/core.py +++ b/bumble/core.py @@ -769,17 +769,20 @@ class AdvertisingData: def ad_data_to_object(ad_type, ad_data): if ad_type in { AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, - AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS + AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, + AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS }: return AdvertisingData.uuid_list_to_objects(ad_data, 2) elif ad_type in { AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS, - AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS + AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS, + AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS }: return AdvertisingData.uuid_list_to_objects(ad_data, 4) elif ad_type in { AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, - AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS + AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, + AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS }: return AdvertisingData.uuid_list_to_objects(ad_data, 16) elif ad_type == AdvertisingData.SERVICE_DATA_16_BIT_UUID: @@ -790,11 +793,24 @@ class AdvertisingData: return (UUID.from_bytes(ad_data[:16]), ad_data[16:]) elif ad_type in { AdvertisingData.SHORTENED_LOCAL_NAME, - AdvertisingData.COMPLETE_LOCAL_NAME + AdvertisingData.COMPLETE_LOCAL_NAME, + AdvertisingData.URI }: return ad_data.decode("utf-8") - elif ad_type == AdvertisingData.TX_POWER_LEVEL: + elif ad_type in { + AdvertisingData.TX_POWER_LEVEL, + AdvertisingData.FLAGS + }: return ad_data[0] + elif ad_type in { + AdvertisingData.APPEARANCE, + AdvertisingData.ADVERTISING_INTERVAL + }: + return struct.unpack(' 0: + future = self.classic_pending_accepts[Address.ANY].pop(0) + future.set_result((bd_addr, class_of_device, link_type)) + + # device configuration is set to accept any incoming connection + elif self.classic_accept_any: + self.host.send_command_sync( + HCI_Accept_Connection_Request_Command( + bd_addr = bd_addr, + role = 0x01 # Remain the peripheral + ) + ) + + # reject incoming connection + else: + self.host.send_command_sync( + HCI_Reject_Connection_Request_Command( + bd_addr = bd_addr, + reason = HCI_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES_ERROR + ) + ) + @host_event_handler @with_connection_from_handle def on_disconnection(self, connection, reason): @@ -1772,9 +1949,13 @@ class Device(CompositeEventEmitter): @host_event_handler @AsyncRunner.run_in_task() async def on_inquiry_complete(self): - if self.discovering: + if self.auto_restart_inquiry: # Inquire again - await self.start_discovery() + await self.start_discovery(auto_restart=True) + else: + self.auto_restart_inquiry = True + self.discovering = False + self.emit('inquiry_complete') @host_event_handler @with_connection_from_handle @@ -1899,21 +2080,32 @@ class Device(CompositeEventEmitter): # [Classic only] @host_event_handler - @with_connection_from_address - def on_remote_name(self, connection, remote_name): + @try_with_connection_from_address + def on_remote_name(self, connection, address, remote_name): # Try to decode the name try: - connection.peer_name = remote_name.decode('utf-8') - connection.emit('remote_name') + remote_name = remote_name.decode('utf-8') + if connection: + connection.peer_name = remote_name + connection.emit('remote_name') + else: + self.emit('remote_name', address, remote_name) except UnicodeDecodeError as error: logger.warning('peer name is not valid UTF-8') - connection.emit('remote_name_failure', error) + if connection: + connection.emit('remote_name_failure', error) + else: + self.emit('remote_name_failure', address, error) + # [Classic only] @host_event_handler - @with_connection_from_address - def on_remote_name_failure(self, connection, error): - connection.emit('remote_name_failure', error) + @try_with_connection_from_address + def on_remote_name_failure(self, connection, address, error): + if connection: + connection.emit('remote_name_failure', error) + else: + self.emit('remote_name_failure', address, error) @host_event_handler @with_connection_from_handle diff --git a/bumble/hci.py b/bumble/hci.py index af26374..d4cf7cc 100644 --- a/bumble/hci.py +++ b/bumble/hci.py @@ -1652,6 +1652,16 @@ class Address: ADDRESS_TYPE_SPEC = {'size': 1, 'mapper': lambda x: Address.address_type_name(x)} + @classmethod + @property + def ANY(cls): + return cls(b"\xff\xff\xff\xff\xff\xff", cls.PUBLIC_DEVICE_ADDRESS) + + @classmethod + @property + def NIL(cls): + return cls(b"\x00\x00\x00\x00\x00\x00", cls.PUBLIC_DEVICE_ADDRESS) + @staticmethod def address_type_name(address_type): return name_or_number(Address.ADDRESS_TYPE_NAMES, address_type) @@ -1935,6 +1945,17 @@ class HCI_Accept_Connection_Request_Command(HCI_Command): ''' +# ----------------------------------------------------------------------------- +@HCI_Command.command([ + ('bd_addr', Address.parse_address), + ('reason', {'size': 1, 'mapper': HCI_Constant.error_name}) +]) +class HCI_Reject_Connection_Request_Command(HCI_Command): + ''' + See Bluetooth spec @ 7.1.9 Reject Connection Request Command + ''' + + # ----------------------------------------------------------------------------- @HCI_Command.command([ ('bd_addr', Address.parse_address), diff --git a/bumble/host.py b/bumble/host.py index 20a92bf..32b2194 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -176,6 +176,9 @@ class Host(EventEmitter): if check_result: if type(response.return_parameters) is int: status = response.return_parameters + elif type(response.return_parameters) is bytes: + # return parameters first field is a one byte status code + status = response.return_parameters[0] else: status = response.return_parameters.status @@ -344,13 +347,12 @@ class Host(EventEmitter): # Classic only def on_hci_connection_request_event(self, event): - # For now, just accept everything - # TODO: delegate the decision - self.send_command_sync( - HCI_Accept_Connection_Request_Command( - bd_addr = event.bd_addr, - role = 0x01 # Remain the peripheral - ) + # Notify the listeners + self.emit( + 'connection_request', + event.bd_addr, + event.class_of_device, + event.link_type, ) def on_hci_le_connection_complete_event(self, event): @@ -645,3 +647,6 @@ class Host(EventEmitter): self.emit('remote_name_failure', event.bd_addr, event.status) else: self.emit('remote_name', event.bd_addr, event.remote_name) + + def on_hci_remote_host_supported_features_notification_event(self, event): + self.emit('remote_host_supported_features', event.bd_addr, event.host_supported_features) diff --git a/tests/core_test.py b/tests/core_test.py index f4bdd83..fa397db 100644 --- a/tests/core_test.py +++ b/tests/core_test.py @@ -24,19 +24,19 @@ def test_ad_data(): ad = AdvertisingData.from_bytes(data) ad_bytes = bytes(ad) assert(data == ad_bytes) - assert(ad.get(AdvertisingData.COMPLETE_LOCAL_NAME) is None) - assert(ad.get(AdvertisingData.TX_POWER_LEVEL) == bytes([123])) - assert(ad.get(AdvertisingData.COMPLETE_LOCAL_NAME, return_all=True) == []) - assert(ad.get(AdvertisingData.TX_POWER_LEVEL, return_all=True) == [bytes([123])]) + assert(ad.get(AdvertisingData.COMPLETE_LOCAL_NAME, raw=True) is None) + assert(ad.get(AdvertisingData.TX_POWER_LEVEL, raw=True) == bytes([123])) + assert(ad.get(AdvertisingData.COMPLETE_LOCAL_NAME, return_all=True, raw=True) == []) + assert(ad.get(AdvertisingData.TX_POWER_LEVEL, return_all=True, raw=True) == [bytes([123])]) data2 = bytes([2, AdvertisingData.TX_POWER_LEVEL, 234]) ad.append(data2) ad_bytes = bytes(ad) assert(ad_bytes == data + data2) - assert(ad.get(AdvertisingData.COMPLETE_LOCAL_NAME) is None) - assert(ad.get(AdvertisingData.TX_POWER_LEVEL) == bytes([123])) - assert(ad.get(AdvertisingData.COMPLETE_LOCAL_NAME, return_all=True) == []) - assert(ad.get(AdvertisingData.TX_POWER_LEVEL, return_all=True) == [bytes([123]), bytes([234])]) + assert(ad.get(AdvertisingData.COMPLETE_LOCAL_NAME, raw=True) is None) + assert(ad.get(AdvertisingData.TX_POWER_LEVEL, raw=True) == bytes([123])) + assert(ad.get(AdvertisingData.COMPLETE_LOCAL_NAME, return_all=True, raw=True) == []) + assert(ad.get(AdvertisingData.TX_POWER_LEVEL, return_all=True, raw=True) == [bytes([123]), bytes([234])]) # ----------------------------------------------------------------------------- diff --git a/tests/device_test.py b/tests/device_test.py index cd72c4c..acf4446 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -158,16 +158,23 @@ async def test_device_connect_parallel(): d1.host.set_packet_sink(Sink(d1_flow())) d2.host.set_packet_sink(Sink(d2_flow())) - [c1, c2] = await asyncio.gather(*[ + [c01, c02, a10, a20, a01] = await asyncio.gather(*[ asyncio.create_task(d0.connect(d1.public_address, transport=BT_BR_EDR_TRANSPORT)), asyncio.create_task(d0.connect(d2.public_address, transport=BT_BR_EDR_TRANSPORT)), + asyncio.create_task(d1.accept(peer_address=d0.public_address)), + asyncio.create_task(d2.accept()), + asyncio.create_task(d0.accept(peer_address=d1.public_address)), ]) - assert type(c1) == Connection - assert type(c2) == Connection + assert type(c01) == Connection + assert type(c02) == Connection + assert type(a10) == Connection + assert type(a20) == Connection + assert type(a01) == Connection - assert c1.handle == 0x100 - assert c2.handle == 0x101 + assert c01.handle == a10.handle and c01.handle == 0x100 + assert c02.handle == a20.handle and c02.handle == 0x101 + assert a01 == c01 # -----------------------------------------------------------------------------