11.12. Funções simultâneas e múltiplas ligações¶
As páginas sobre periférico e central mostram cada uma um único papel a servir uma única ligação de cada vez. As aplicações reais raramente são tão simples. Uma câmara pode publicar um serviço de sensor para um telemóvel ao mesmo tempo que lê valores de uma fita cardíaca, ou aceitar ligações de dois telemóveis emparelhados em simultâneo. A API aioble suporta ambos os padrões porque o rádio multiplexa por baixo e cada operação já é uma corrotina – execute mais corrotinas e o trabalho acontece em paralelo num único ciclo de eventos.
Esta página reúne os padrões que surgem.
11.12.1. Múltiplos clientes a ligar a um periférico¶
O ciclo de periférico simples em Agir como periférico serve um central ligado de cada vez:
async def serve():
while True:
connection = await aioble.advertise(...)
async with connection:
await connection.disconnected()
O padrão que lhe permite aceitar mais do que um cliente é lançar uma tarefa por ligação e voltar imediatamente a aioble.advertise() para que o próximo cliente também possa ligar:
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 ligação corre na sua própria tarefa. A base de dados GATT é partilhada – todos os clientes veem os mesmos serviços e características – mas o estado por ligação vive dentro da tarefa. As notificações vão para todos os clientes subscritos quando write() é chamado com send_update=True; os envios direcionados que devem chegar apenas a um cliente usam notify() / indicate() com o argumento DeviceConnection específico.
Mantenha o fan-out pequeno. Cada ligação mantida consome tempo de rádio, RAM e um lugar na tabela de ligações do controlador, e a câmara não foi concebida para ser um hub para dezenas de clientes. Dois ou três centrais (um telemóvel, um tablet, um microcontrolador complementar) estão bem dentro do alcance; os designs que precisam de mais pertencem a um gateway BLE adequado e não à câmara.
11.12.2. Periférico e central ao mesmo tempo¶
Uma câmara pode anunciar o seu próprio serviço a um telemóvel enquanto também age como central para um dispositivo vestível. aioble não tem um interruptor de «modo» – o ciclo de anúncio e o ciclo de scan-e-ligação são apenas corrotinas independentes:
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())
O rádio partilha o tempo entre os dois papéis – uma janela de scan aqui, uma rajada de publicidade ali, um evento de ligação quando uma das ligações de qualquer lado está ativa. O débito em cada papel diminui quando ambos estão ativos porque o rádio literalmente não consegue fazer duas coisas ao mesmo tempo, mas para as conversas de baixa largura de banda para as quais o BLE foi concebido, o custo é normalmente invisível.
Dois aspetos práticos a ter em mente:
Ambos os papéis precisam de estar na sua própria corrotina. Chamar
aioble.scan()dentro da tarefa por cliente que trata de um central ligado funciona, mas bloqueia as notificações desse cliente até o scan terminar – execute o scan na sua própria tarefa.Apenas um scan corre de cada vez. Se precisar de fazer scan a partir de dois locais diferentes, partilhe o iterador de scan ou coordene o acesso; não entre em dois gestores de contexto
aioble.scan()em paralelo.
11.12.3. Coordenar múltiplas ligações a partir de uma única tarefa¶
Quando várias ligações precisam de ser combinadas numa única operação lógica – por exemplo, a câmara fala com dois sensores ao mesmo tempo e só reporta o resultado depois de ambos terem respondido – os primitivos padrão de asyncio aplicam-se diretamente. asyncio.gather() executa as corrotinas por ligação concorrentemente e retorna quando todas terminaram; asyncio.wait_for() adiciona um prazo.
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
O mesmo padrão que o capítulo de asyncio (Asyncio) usa para redes – as corrotinas BLE ligam-se a gather / wait_for / Event / Lock da mesma forma que as TCP.
11.12.4. Quando um papel termina por ciclo e o outro não¶
Um ciclo numa câmara alimentada por bateria pode ter este aspeto:
Acordar.
Como central, ler valores frescos de uma fita de sensor emparelhada.
Como periférico, anunciar para um telemóvel descarregar as medições do dia.
Quando ambos estiverem inativos, chamar
aioble.stop()e adormecer.
A sequência é simples com duas tarefas e um 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