11.12. Roles concurrentes y conexiones múltiples¶
Las páginas de periférico y central muestran cada una un único rol que atiende una sola conexión a la vez. Las aplicaciones reales rara vez son así de simples. Una cámara puede publicar un servicio de sensor a un teléfono al mismo tiempo que lee valores de una banda de frecuencia cardíaca, o aceptar conexiones de dos teléfonos emparejados simultáneamente. La API de aioble admite ambos patrones porque la radio multiplexa por debajo y cada operación ya es una corrutina: ejecuta más corrutinas y el trabajo ocurre en paralelo en un único bucle de eventos.
Esta página recopila los patrones que suelen aparecer.
11.12.1. Varios clientes conectándose a un solo periférico¶
El sencillo bucle de periférico de Actuar como periférico atiende un central conectado a la vez:
async def serve():
while True:
connection = await aioble.advertise(...)
async with connection:
await connection.disconnected()
El patrón que le permite aceptar más de un cliente consiste en lanzar una tarea por conexión y volver inmediatamente a aioble.advertise() para que el siguiente cliente también pueda conectarse:
async def handle_client(connection):
async with connection:
# ... per-client work: subscribe their CCCDs,
# push notifications, await writes ...
await connection.disconnected()
async def serve():
while True:
connection = await aioble.advertise(
interval_us=250000,
name="openmv-env",
services=[ENV_SERVICE],
)
asyncio.create_task(handle_client(connection))
Cada conexión se ejecuta en su propia tarea. La base de datos GATT es compartida – todos los clientes ven los mismos servicios y características – pero el estado por conexión vive dentro de la tarea. Las notificaciones llegan a todos los clientes suscritos cuando se llama a write() con send_update=True; los envíos dirigidos que solo deben alcanzar a un cliente usan notify() / indicate() con el argumento DeviceConnection específico.
Mantén el número de conexiones simultáneas reducido. Cada conexión mantenida cuesta tiempo de radio, RAM y un espacio en la tabla de conexiones del controlador, y la cámara no está diseñada para ser un concentrador de docenas de clientes. Dos o tres centrales (un teléfono, una tableta, un microcontrolador acompañante) están perfectamente a su alcance; los diseños que necesitan más corresponden a una pasarela BLE adecuada en lugar de a la cámara.
11.12.2. Periférico y central al mismo tiempo¶
Una cámara puede anunciar su propio servicio a un teléfono mientras también actúa como central frente a un dispositivo ponible. aioble no tiene un interruptor de «modo»: el bucle de anuncio y el bucle de escaneo y conexión son simplemente corrutinas independientes:
async def be_peripheral():
while True:
connection = await aioble.advertise(
interval_us=250000,
name="openmv-hub",
services=[ENV_SERVICE],
)
asyncio.create_task(handle_client(connection))
async def be_central():
while True:
sensor = await find_sensor()
if sensor is None:
await asyncio.sleep(5)
continue
try:
async with await sensor.connect() as conn:
await stream_from_sensor(conn)
except aioble.DeviceDisconnectedError:
pass
async def main():
await asyncio.gather(be_peripheral(), be_central())
asyncio.run(main())
La radio comparte el tiempo entre los dos roles – una ventana de escaneo aquí, una ráfaga de anuncio allá, un evento de conexión cuando alguna de las conexiones de cualquiera de los lados está activa. El rendimiento de cada rol disminuye cuando ambos están activos porque la radio no puede hacer literalmente dos cosas a la vez, pero para las conversaciones de bajo ancho de banda para las que se diseñó BLE, el coste suele ser imperceptible.
Dos cuestiones prácticas a tener en cuenta:
Ambos roles deben estar en su propia corrutina. Llamar a
aioble.scan()desde dentro de la tarea por cliente que gestiona un central conectado funciona, pero bloquea las notificaciones de ese cliente hasta que el escaneo termina – en su lugar, ejecuta el escaneo en su propia tarea.Solo se ejecuta un escaneo a la vez. Si necesitas escanear desde dos lugares distintos, comparte el iterador de escaneo o coordina el acceso; no entres en dos gestores de contexto
aioble.scan()en paralelo.
11.12.3. Coordinar varias conexiones desde una sola tarea¶
Cuando varias conexiones deben combinarse en una única operación lógica – por ejemplo, la cámara habla con dos sensores a la vez y solo informa del resultado después de que ambos hayan respondido – las primitivas estándar de asyncio se aplican directamente. asyncio.gather() ejecuta las corrutinas por conexión de forma concurrente y retorna cuando todas han terminado; asyncio.wait_for() añade un plazo límite.
async def read_pair():
async with await sensor_a.connect() as a:
async with await sensor_b.connect() as b:
value_a, value_b = await asyncio.gather(
read_value(a, A_SERVICE, A_CHAR),
read_value(b, B_SERVICE, B_CHAR),
)
return value_a, value_b
El mismo patrón que el capítulo de asyncio (Asyncio) usa para la red – las corrutinas BLE se integran con gather / wait_for / Event / Lock igual que lo hacen las de TCP.
11.12.4. Cuando un rol termina por ciclo y el otro no¶
Un ciclo en una cámara alimentada por batería podría tener este aspecto:
Despertar.
Como central, leer valores frescos de una banda de sensor emparejada.
Como periférico, anunciarse para que un teléfono descargue las mediciones del día.
Cuando ambos están inactivos, llamar a
aioble.stop()y dormir.
La secuenciación es sencilla con dos tareas y un asyncio.Event:
phone_done = asyncio.Event()
async def serve_phone():
connection = await aioble.advertise(
interval_us=250000,
name="openmv-hub",
services=[ENV_SERVICE],
)
async with connection:
await stream_measurements(connection)
phone_done.set()
async def read_strap():
async with await strap.connect() as conn:
await pull_fresh_values(conn)
async def cycle():
await asyncio.gather(read_strap(), serve_phone())
aioble.stop() # radio off until next wake