Refactor LE emulation with LL and Air Interface

This commit is contained in:
Josh Wu
2025-11-27 21:40:43 +08:00
parent d2a4c2a8e4
commit a84f0279b1
8 changed files with 731 additions and 656 deletions

View File

@@ -285,10 +285,12 @@ async def test_legacy_advertising():
async def test_legacy_advertising_disconnection(auto_restart):
devices = TwoDevices()
for controller in devices.controllers:
controller.le_features = bytes.fromhex('ffffffffffffffff')
controller.le_features |= hci.LeFeatureMask.LE_EXTENDED_ADVERTISING
for dev in devices:
await dev.power_on()
await devices[0].start_advertising(auto_restart=auto_restart)
await devices[0].start_advertising(
auto_restart=auto_restart, advertising_interval_min=1.0
)
connecion = await devices[1].connect(devices[0].random_address)
await connecion.disconnect()
@@ -343,12 +345,15 @@ async def test_extended_advertising_connection(own_address_type):
for dev in devices:
await dev.power_on()
advertising_set = await devices[0].create_advertising_set(
advertising_parameters=AdvertisingParameters(own_address_type=own_address_type)
advertising_parameters=AdvertisingParameters(
own_address_type=own_address_type, primary_advertising_interval_min=1.0
)
)
await asyncio.wait_for(
devices[1].connect(advertising_set.random_address or devices[0].public_address),
_TIMEOUT,
)
await async_barrier()
# Advertising set should be terminated after connected.
assert not advertising_set.enabled
@@ -376,7 +381,7 @@ async def test_extended_advertising_connection(own_address_type):
async def test_extended_advertising_connection_out_of_order(own_address_type):
devices = TwoDevices()
device = devices[0]
devices.controllers[0].le_features = bytes.fromhex('ffffffffffffffff')
devices.controllers[0].le_features |= hci.LeFeatureMask.LE_EXTENDED_ADVERTISING
await device.power_on()
advertising_set = await device.create_advertising_set(
advertising_parameters=AdvertisingParameters(own_address_type=own_address_type)

View File

@@ -69,7 +69,7 @@ from bumble.host import Host
from bumble.link import LocalLink
from bumble.transport.common import AsyncPipeSink
from .test_utils import async_barrier
from .test_utils import Devices, TwoDevices, async_barrier
# -----------------------------------------------------------------------------
@@ -160,7 +160,8 @@ async def test_characteristic_encoding():
def decode_value(self, value_bytes):
return value_bytes[0]
[client, server] = LinkedDevices().devices[:2]
devices = await TwoDevices.create_with_connection()
[client, server] = devices
characteristic = Characteristic(
'FDB159DB-036C-49E3-B3DB-6325AC750806',
@@ -189,9 +190,7 @@ async def test_characteristic_encoding():
)
server.add_service(service)
await client.power_on()
await server.power_on()
connection = await client.connect(server.random_address)
connection = devices.connections[0]
peer = Peer(connection)
await peer.discover_services()
@@ -279,7 +278,8 @@ async def test_characteristic_encoding():
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_attribute_getters():
[client, server] = LinkedDevices().devices[:2]
devices = await TwoDevices.create_with_connection()
[client, server] = devices
characteristic_uuid = UUID('FDB159DB-036C-49E3-B3DB-6325AC750806')
characteristic = Characteristic(
@@ -629,39 +629,11 @@ async def test_CharacteristicValue_async():
m.assert_called_once_with(z, b)
# -----------------------------------------------------------------------------
class LinkedDevices:
def __init__(self):
self.connections = [None, None, None]
self.link = LocalLink()
self.controllers = [
Controller('C1', link=self.link),
Controller('C2', link=self.link),
Controller('C3', link=self.link),
]
self.devices = [
Device(
address='F0:F1:F2:F3:F4:F5',
host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])),
),
Device(
address='F1:F2:F3:F4:F5:F6',
host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])),
),
Device(
address='F2:F3:F4:F5:F6:F7',
host=Host(self.controllers[2], AsyncPipeSink(self.controllers[2])),
),
]
self.paired = [None, None, None]
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_read_write():
[client, server] = LinkedDevices().devices[:2]
devices = await TwoDevices.create_with_connection()
[client, server] = devices
characteristic1 = Characteristic(
'FDB159DB-036C-49E3-B3DB-6325AC750806',
@@ -694,9 +666,7 @@ async def test_read_write():
)
server.add_services([service1])
await client.power_on()
await server.power_on()
connection = await client.connect(server.random_address)
connection = devices.connections[0]
peer = Peer(connection)
await peer.discover_services()
@@ -740,7 +710,8 @@ async def test_read_write():
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_read_write2():
[client, server] = LinkedDevices().devices[:2]
devices = await TwoDevices.create_with_connection()
[client, server] = devices
v = bytes([0x11, 0x22, 0x33, 0x44])
characteristic1 = Characteristic(
@@ -753,9 +724,7 @@ async def test_read_write2():
service1 = Service('3A657F47-D34F-46B3-B1EC-698E29B6B829', [characteristic1])
server.add_services([service1])
await client.power_on()
await server.power_on()
connection = await client.connect(server.random_address)
connection = devices.connections[0]
peer = Peer(connection)
await peer.discover_services()
@@ -785,7 +754,8 @@ async def test_read_write2():
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_subscribe_notify():
[client, server] = LinkedDevices().devices[:2]
devices = await TwoDevices.create_with_connection()
[client, server] = devices
characteristic1 = Characteristic(
'FDB159DB-036C-49E3-B3DB-6325AC750806',
@@ -855,9 +825,7 @@ async def test_subscribe_notify():
server.on('characteristic_subscription', on_characteristic_subscription)
await client.power_on()
await server.power_on()
connection = await client.connect(server.random_address)
connection = devices.connections[0]
peer = Peer(connection)
await peer.discover_services()
@@ -1006,7 +974,8 @@ async def test_subscribe_notify():
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_unsubscribe():
[client, server] = LinkedDevices().devices[:2]
devices = await TwoDevices.create_with_connection()
[client, server] = devices
characteristic1 = Characteristic(
'FDB159DB-036C-49E3-B3DB-6325AC750806',
@@ -1032,9 +1001,7 @@ async def test_unsubscribe():
mock2 = Mock()
characteristic2.on('subscription', mock2)
await client.power_on()
await server.power_on()
connection = await client.connect(server.random_address)
connection = devices.connections[0]
peer = Peer(connection)
await peer.discover_services()
@@ -1094,7 +1061,8 @@ async def test_unsubscribe():
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_discover_all():
[client, server] = LinkedDevices().devices[:2]
devices = await TwoDevices.create_with_connection()
[client, server] = devices
characteristic1 = Characteristic(
'FDB159DB-036C-49E3-B3DB-6325AC750806',
@@ -1120,9 +1088,7 @@ async def test_discover_all():
service2 = Service('1111', [])
server.add_services([service1, service2])
await client.power_on()
await server.power_on()
connection = await client.connect(server.random_address)
connection = devices.connections[0]
peer = Peer(connection)
await peer.discover_all()
@@ -1146,7 +1112,10 @@ async def test_discover_all():
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_mtu_exchange():
[d1, d2, d3] = LinkedDevices().devices[:3]
devices = Devices(3)
for dev in devices:
await dev.power_on()
[d1, d2, d3] = devices
d3.gatt_server.max_mtu = 100
@@ -1160,11 +1129,15 @@ async def test_mtu_exchange():
await d2.power_on()
await d3.power_on()
await d3.start_advertising(advertising_interval_min=1.0)
d1_connection = await d1.connect(d3.random_address)
await async_barrier()
assert len(d3_connections) == 1
assert d3_connections[0] is not None
await d3.start_advertising(advertising_interval_min=1.0)
d2_connection = await d2.connect(d3.random_address)
await async_barrier()
assert len(d3_connections) == 2
assert d3_connections[1] is not None
@@ -1233,7 +1206,8 @@ Got: BROADCAST,HELLO"""
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_server_string():
[_, server] = LinkedDevices().devices[:2]
devices = await TwoDevices.create_with_connection()
[_, server] = devices
characteristic = Characteristic(
'FDB159DB-036C-49E3-B3DB-6325AC750806',
@@ -1422,7 +1396,8 @@ def test_get_attribute_group():
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_get_characteristics_by_uuid():
[client, server] = LinkedDevices().devices[:2]
devices = await TwoDevices.create_with_connection()
[client, server] = devices
characteristic1 = Characteristic(
'1234',
@@ -1447,9 +1422,7 @@ async def test_get_characteristics_by_uuid():
server.add_services([service1, service2])
await client.power_on()
await server.power_on()
connection = await client.connect(server.random_address)
connection = devices.connections[0]
peer = Peer(connection)
await peer.discover_services()
@@ -1472,7 +1445,8 @@ async def test_get_characteristics_by_uuid():
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_write_return_error():
[client, server] = LinkedDevices().devices[:2]
devices = await TwoDevices.create_with_connection()
[client, server] = devices
on_write = Mock(side_effect=ATT_Error(error_code=ErrorCode.VALUE_NOT_ALLOWED))
characteristic = Characteristic(
@@ -1484,9 +1458,7 @@ async def test_write_return_error():
service = Service('ABCD', [characteristic])
server.add_service(service)
await client.power_on()
await server.power_on()
connection = await client.connect(server.random_address)
connection = devices.connections[0]
async with Peer(connection) as peer:
c = peer.get_characteristics_by_uuid(uuid=UUID('1234'))[0]

View File

@@ -35,7 +35,7 @@ from bumble.smp import (
OobLegacyContext,
)
from .test_utils import TwoDevices
from .test_utils import TwoDevices, async_barrier
# -----------------------------------------------------------------------------
# Logging
@@ -56,12 +56,14 @@ async def test_self_disconnection():
two_devices = TwoDevices()
await two_devices.setup_connection()
await two_devices.connections[0].disconnect()
await async_barrier()
assert two_devices.connections[0] is None
assert two_devices.connections[1] is None
two_devices = TwoDevices()
await two_devices.setup_connection()
await two_devices.connections[1].disconnect()
await async_barrier()
assert two_devices.connections[0] is None
assert two_devices.connections[1] is None
@@ -80,7 +82,8 @@ async def test_self_classic_connection(responder_role):
two_devices.devices[1].classic_enabled = True
# Start
await two_devices.setup_connection()
for dev in two_devices.devices:
await dev.power_on()
# Connect the two devices
await asyncio.gather(
@@ -418,8 +421,9 @@ async def test_self_smp_over_classic():
two_devices.devices[1].classic_enabled = True
# Connect the two devices
await two_devices.devices[0].power_on()
await two_devices.devices[1].power_on()
for dev in two_devices.devices:
await dev.power_on()
await asyncio.gather(
two_devices.devices[0].connect(
two_devices.devices[1].public_address, transport=PhysicalTransport.BR_EDR

View File

@@ -16,6 +16,7 @@
# Imports
# -----------------------------------------------------------------------------
import asyncio
import functools
from typing import Optional
from typing_extensions import Self
@@ -30,39 +31,34 @@ from bumble.transport.common import AsyncPipeSink
# -----------------------------------------------------------------------------
class TwoDevices:
class Devices:
connections: list[Optional[Connection]]
def __init__(self) -> None:
self.connections = [None, None]
def __init__(self, num_devices: int) -> None:
self.connections = [None for _ in range(num_devices)]
self.link = LocalLink()
addresses = ['F0:F1:F2:F3:F4:F5', 'F5:F4:F3:F2:F1:F0']
addresses = [":".join([f"F{i}"] * 6) for i in range(num_devices)]
self.controllers = [
Controller('C1', link=self.link, public_address=addresses[0]),
Controller('C2', link=self.link, public_address=addresses[1]),
Controller(f'C{i+i}', link=self.link, public_address=addresses[i])
for i in range(num_devices)
]
self.devices = [
Device(
address=Address(addresses[0]),
host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])),
),
Device(
address=Address(addresses[1]),
host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])),
),
address=Address(addresses[i]),
host=Host(self.controllers[i], AsyncPipeSink(self.controllers[i])),
)
for i in range(num_devices)
]
self.devices[0].on(
'connection', lambda connection: self.on_connection(0, connection)
)
self.devices[1].on(
'connection', lambda connection: self.on_connection(1, connection)
)
for i in range(num_devices):
self.devices[i].on(
self.devices[i].EVENT_CONNECTION,
functools.partial(self.on_connection, i),
)
self.paired = [
asyncio.get_event_loop().create_future(),
asyncio.get_event_loop().create_future(),
asyncio.get_event_loop().create_future() for _ in range(num_devices)
]
def on_connection(self, which, connection):
@@ -77,19 +73,26 @@ class TwoDevices:
async def setup_connection(self) -> None:
# Start
await self.devices[0].power_on()
await self.devices[1].power_on()
for dev in self.devices:
await dev.power_on()
# Connect the two devices
await self.devices[0].connect(self.devices[1].random_address)
# Check the post conditions
assert self.connections[0] is not None
assert self.connections[1] is not None
# Connect devices
for dev in self.devices[1:]:
connection_future = asyncio.get_running_loop().create_future()
dev.once(dev.EVENT_CONNECTION, connection_future.set_result)
await dev.start_advertising(advertising_interval_min=1.0)
await self.devices[0].connect(dev.random_address)
await connection_future
def __getitem__(self, index: int) -> Device:
return self.devices[index]
# -----------------------------------------------------------------------------
class TwoDevices(Devices):
def __init__(self) -> None:
super().__init__(2)
@classmethod
async def create_with_connection(cls: type[Self]) -> Self:
devices = cls()