diff --git a/bumble/device.py b/bumble/device.py index 30763e97..563bbf87 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -1856,6 +1856,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 @@ -4009,14 +4010,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 ) @@ -4147,6 +4162,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 41fbabd9..9d43fcec 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 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() # -----------------------------------------------------------------------------