11.12. Rôles concurrents et connexions multiples¶
Les pages consacrées au périphérique et au central présentent chacune un seul rôle servant une seule connexion à la fois. Les applications réelles sont rarement aussi simples. Une caméra peut publier un service de capteur vers un téléphone tout en lisant des valeurs depuis une ceinture de fréquence cardiaque, ou accepter des connexions de deux téléphones appairés simultanément. L’API aioble prend en charge ces deux schémas car la radio multiplexe en arrière-plan et chaque opération est déjà une coroutine – exécutez davantage de coroutines, et le travail se déroule en parallèle sur une seule boucle d’événements.
Cette page rassemble les schémas qui se présentent couramment.
11.12.1. Plusieurs clients se connectant à un seul périphérique¶
La boucle de périphérique simple présentée sur Agir comme périphérique sert un seul central connecté à la fois :
async def serve():
while True:
connection = await aioble.advertise(...)
async with connection:
await connection.disconnected()
Le schéma qui lui permet d’accepter plus d’un client consiste à lancer une tâche par connexion et à revenir immédiatement en boucle vers aioble.advertise() afin que le client suivant puisse lui aussi se connecter
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))
Chaque connexion s’exécute dans sa propre tâche. La base de données GATT est partagée – tous les clients voient les mêmes services et caractéristiques – mais l’état propre à chaque connexion réside dans la tâche. Les notifications sont envoyées à tous les clients abonnés lorsque write() est appelée avec send_update=True ; les envois dirigés qui ne doivent atteindre qu’un seul client utilisent notify() / indicate() avec l’argument DeviceConnection spécifique.
Gardez la diffusion réduite. Chaque connexion maintenue coûte du temps radio, de la RAM et un emplacement dans la table de connexions du contrôleur, et la caméra n’est pas conçue pour servir de concentrateur à des dizaines de clients. Deux ou trois centraux (un téléphone, une tablette, un microcontrôleur compagnon) restent tout à fait à portée ; les conceptions nécessitant davantage relèvent d’une véritable passerelle BLE plutôt que de la caméra.
11.12.2. Périphérique et central simultanément¶
Une caméra peut annoncer son propre service à un téléphone tout en agissant aussi comme central vis-à-vis d’un objet connecté. aioble ne dispose pas d’un commutateur de « mode » – la boucle d’annonce et la boucle de scan-et-connexion sont simplement des coroutines indépendantes
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 partage le temps entre les deux rôles – une fenêtre de scan ici, une rafale d’annonce là, un événement de connexion lorsqu’une connexion de l’un ou l’autre côté est active. Le débit de chaque rôle diminue lorsque les deux sont actifs car la radio ne peut littéralement pas faire deux choses à la fois, mais pour les échanges à faible bande passante pour lesquels le BLE a été conçu, ce coût est généralement imperceptible.
Deux points pratiques à garder à l’esprit :
Chaque rôle doit se trouver dans sa propre coroutine. Appeler
aioble.scan()depuis la tâche par client qui gère un central connecté fonctionne, mais bloque les notifications de ce client jusqu’à la fin du scan – exécutez plutôt le scan dans sa propre tâche.Un seul scan s’exécute à la fois. Si vous devez scanner depuis deux endroits différents, partagez l’itérateur de scan ou coordonnez les accès ; n’entrez pas en parallèle dans deux gestionnaires de contexte
aioble.scan().
11.12.3. Coordonner plusieurs connexions depuis une seule tâche¶
Lorsque plusieurs connexions doivent être combinées en une seule opération logique – par exemple, la caméra communique avec deux capteurs simultanément et ne rapporte le résultat qu’après que les deux ont répondu – les primitives standard asyncio s’appliquent directement. asyncio.gather() exécute les coroutines par connexion de manière concurrente et retourne lorsqu’elles sont toutes terminées ; asyncio.wait_for() ajoute une échéance.
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
C’est le même schéma que celui utilisé par le chapitre asyncio (Asyncio) pour le réseau – les coroutines BLE s’intègrent à gather / wait_for / Event / Lock exactement comme le font celles du TCP.
11.12.4. Lorsqu’un rôle se termine à chaque cycle et que l’autre non¶
Un cycle dans une caméra alimentée par batterie pourrait ressembler à ceci :
Réveil.
En tant que central, lire les valeurs fraîches depuis une ceinture de capteur appairée.
En tant que périphérique, annoncer pour qu’un téléphone télécharge les mesures de la journée.
Lorsque les deux sont inactifs, appeler
aioble.stop()et passer en veille.
Le séquencement est simple avec deux tâches et 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