11.12. Gleichzeitige Rollen und mehrere Verbindungen¶
Die Seiten zu Peripheral und Central zeigen jeweils eine einzelne Rolle, die zu einem Zeitpunkt eine einzelne Verbindung bedient. Reale Anwendungen sind selten so einfach. Eine Kamera kann einen Sensordienst an ein Telefon veröffentlichen, während sie gleichzeitig Werte von einem Herzfrequenzgurt liest, oder Verbindungen von zwei gleichzeitig gekoppelten Telefonen annehmen. Die aioble-API unterstützt beide Muster, weil das Funkmodul darunter multiplext und jede Operation bereits eine Coroutine ist – starte mehr Coroutinen, und die Arbeit geschieht parallel auf einer einzigen Ereignisschleife.
Diese Seite sammelt die Muster, die dabei auftreten.
11.12.1. Mehrere Clients verbinden sich mit einem Peripheral¶
Die einfache Peripheral-Schleife auf Als Peripheriegerät agieren bedient jeweils ein verbundenes Central:
async def serve():
while True:
connection = await aioble.advertise(...)
async with connection:
await connection.disconnected()
Das Muster, das es erlaubt, mehr als einen Client anzunehmen, besteht darin, eine Task pro Verbindung zu starten und sofort zu aioble.advertise() zurückzukehren, damit sich auch der nächste Client verbinden kann:
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))
Jede Verbindung läuft in ihrer eigenen Task. Die GATT-Datenbank wird gemeinsam genutzt – alle Clients sehen dieselben Dienste und Charakteristiken –, aber der Zustand pro Verbindung lebt innerhalb der Task. Benachrichtigungen gehen an jeden abonnierten Client, wenn write() mit send_update=True aufgerufen wird; gezielte Pushes, die nur einen Client erreichen sollen, verwenden notify() / indicate() mit dem spezifischen DeviceConnection-Argument.
Halte die Auffächerung klein. Jede gehaltene Verbindung kostet Funkzeit, RAM und einen Platz in der Verbindungstabelle des Controllers, und die Kamera ist nicht dafür ausgelegt, ein Hub für Dutzende von Clients zu sein. Zwei oder drei Centrals (ein Telefon, ein Tablet, ein begleitender Mikrocontroller) liegen gut im Bereich des Möglichen; Designs, die mehr benötigen, gehören auf ein richtiges BLE-Gateway und nicht auf die Cam.
11.12.2. Peripheral und Central gleichzeitig¶
Eine Kamera kann ihren eigenen Dienst an ein Telefon bewerben, während sie gleichzeitig als Central für ein Wearable agiert. aioble hat keinen „Modus“-Schalter – die Advertise-Schleife und die Scan-and-Connect-Schleife sind einfach unabhängige Coroutinen:
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())
Das Funkmodul teilt sich die Zeit zwischen den beiden Rollen – ein Scan-Fenster hier, ein Advertising-Burst dort, ein Verbindungsereignis, wenn eine der Verbindungen beider Seiten aktiv ist. Der Durchsatz jeder Rolle sinkt, wenn beide aktiv sind, weil das Funkmodul nicht buchstäblich zwei Dinge auf einmal tun kann, aber für die Konversationen mit geringer Bandbreite, für die BLE entworfen wurde, sind die Kosten meist unsichtbar.
Zwei praktische Dinge sind zu beachten:
Beide Rollen müssen sich in ihrer eigenen Coroutine befinden. Der Aufruf von
aioble.scan()aus der Task pro Client heraus, die ein verbundenes Central bearbeitet, funktioniert zwar, blockiert aber die Benachrichtigungen dieses Clients, bis der Scan abgeschlossen ist – führe das Scannen stattdessen in einer eigenen Task aus.Es läuft immer nur ein Scan gleichzeitig. Wenn du von zwei verschiedenen Stellen aus scannen musst, teile den Scan-Iterator oder koordiniere den Zugriff; betrete nicht zwei
aioble.scan()-Kontextmanager parallel.
11.12.3. Mehrere Verbindungen von einer Task aus koordinieren¶
Wenn mehrere Verbindungen zu einer logischen Operation kombiniert werden müssen – zum Beispiel spricht die Kamera mit zwei Sensoren gleichzeitig und meldet das Ergebnis erst, nachdem beide geantwortet haben –, lassen sich die Standard-asyncio-Primitive direkt anwenden. asyncio.gather() führt die Coroutinen pro Verbindung nebenläufig aus und kehrt zurück, wenn alle fertig sind; asyncio.wait_for() fügt eine Frist hinzu.
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
Dasselbe Muster, das das asyncio-Kapitel (Asyncio) für Netzwerke verwendet – BLE-Coroutinen lassen sich genauso in gather / wait_for / Event / Lock einklinken wie TCP-Coroutinen.
11.12.4. Wenn eine Rolle pro Zyklus fertig ist und die andere nicht¶
Ein Zyklus in einer batteriebetriebenen Cam könnte so aussehen:
Aufwachen.
Als Central frische Werte von einem gekoppelten Sensorgurt lesen.
Als Peripheral für ein Telefon bewerben, damit dieses die Messungen des Tages herunterladen kann.
Wenn beide untätig sind,
aioble.stop()aufrufen und schlafen.
Die Abfolge ist mit zwei Tasks und einem asyncio.Event unkompliziert:
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