From 2e3aeb86487a6417fa627965e782056785c87bdb Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Wed, 5 Jun 2024 16:29:31 -0700 Subject: [PATCH 1/2] support out of order advertising set termination / connection events --- bumble/device.py | 31 ++++++++++++++++++++++++++++--- bumble/host.py | 4 ++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/bumble/device.py b/bumble/device.py index f9e6b9d7..3007b995 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -1574,6 +1574,7 @@ def __init__( # Extended advertising. self.extended_advertising_sets: Dict[int, AdvertisingSet] = {} + self.connecting_extended_advertising_sets: Dict[int, AdvertisingSet] = {} # Legacy advertising. # The advertising and scan response data, as well as the advertising interval @@ -3605,14 +3606,28 @@ def on_advertising_set_termination( ) return - if not (connection := self.lookup_connection(connection_handle)): - logger.warning(f'no connection for handle 0x{connection_handle:04x}') + if connection := self.lookup_connection(connection_handle): + # We have already received the connection complete event. + self._complete_le_extended_advertising_connection( + connection, advertising_set + ) return + # Associate the connection handle with the advertising set, the connection + # will complete later. + logger.debug( + f'the connection with handle {connection_handle:04X} will complete later' + ) + self.connecting_extended_advertising_sets[connection_handle] = advertising_set + + def _complete_le_extended_advertising_connection( + self, connection: Connection, advertising_set: AdvertisingSet + ) -> None: # Update the connection address. connection.self_address = ( advertising_set.random_address - if advertising_set.advertising_parameters.own_address_type + if advertising_set.random_address is not None + and advertising_set.advertising_parameters.own_address_type in (OwnAddressType.RANDOM, OwnAddressType.RESOLVABLE_OR_RANDOM) else self.public_address ) @@ -3743,6 +3758,16 @@ def on_connection( if role == HCI_CENTRAL_ROLE or not self.supports_le_extended_advertising: # We can emit now, we have all the info we need self._emit_le_connection(connection) + return + + if role == HCI_PERIPHERAL_ROLE and self.supports_le_extended_advertising: + if advertising_set := self.connecting_extended_advertising_sets.pop( + connection_handle, None + ): + # We have already received the advertising set termination event. + self._complete_le_extended_advertising_connection( + connection, advertising_set + ) @host_event_handler def on_connection_failure(self, transport, peer_address, error_code): diff --git a/bumble/host.py b/bumble/host.py index 64b66688..18de1bcc 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -787,6 +787,10 @@ def on_hci_le_enhanced_connection_complete_event(self, event): # Just use the same implementation as for the non-enhanced event for now self.on_hci_le_connection_complete_event(event) + def on_hci_le_enhanced_connection_complete_v2_event(self, event): + # Just use the same implementation as for the v1 event for now + self.on_hci_le_enhanced_connection_complete_event(event) + def on_hci_connection_complete_event(self, event): if event.status == hci.HCI_SUCCESS: # Create/update the connection From df5fc2ddfe2d142a82ff2c513c159bda2ce82d75 Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Wed, 12 Jun 2024 10:13:57 -0700 Subject: [PATCH 2/2] add test --- tests/device_test.py | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/tests/device_test.py b/tests/device_test.py index ac0c96b1..b5df89ab 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -301,9 +301,7 @@ async def test_legacy_advertising_connection(own_address_type): else: assert device.lookup_connection(0x0001).self_address == device.random_address - # For unknown reason, read_phy() in on_connection() would be killed at the end of - # test, so we force scheduling here to avoid an warning. - await asyncio.sleep(0.0001) + await async_barrier() # ----------------------------------------------------------------------------- @@ -384,9 +382,41 @@ async def test_extended_advertising_connection(own_address_type): else: assert device.lookup_connection(0x0001).self_address == device.random_address - # For unknown reason, read_phy() in on_connection() would be killed at the end of - # test, so we force scheduling here to avoid an warning. - await asyncio.sleep(0.0001) + await async_barrier() + + +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize( + 'own_address_type,', + (OwnAddressType.PUBLIC, OwnAddressType.RANDOM), +) +@pytest.mark.asyncio +async def test_extended_advertising_connection_out_of_order(own_address_type): + device = Device(host=mock.AsyncMock(spec=Host)) + peer_address = Address('F0:F1:F2:F3:F4:F5') + advertising_set = await device.create_advertising_set( + advertising_parameters=AdvertisingParameters(own_address_type=own_address_type) + ) + device.on_advertising_set_termination( + HCI_SUCCESS, + advertising_set.advertising_handle, + 0x0001, + 0, + ) + device.on_connection( + 0x0001, + BT_LE_TRANSPORT, + peer_address, + BT_PERIPHERAL_ROLE, + ConnectionParameters(0, 0, 0), + ) + + if own_address_type == OwnAddressType.PUBLIC: + assert device.lookup_connection(0x0001).self_address == device.public_address + else: + assert device.lookup_connection(0x0001).self_address == device.random_address + + await async_barrier() # -----------------------------------------------------------------------------