11.12. Roluri concurente și conexiuni multiple

Paginile despre periferic și central prezintă fiecare câte un singur rol care deservește o singură conexiune la un moment dat. Aplicațiile reale sunt rareori atât de simple. O cameră poate publica un serviciu de senzor către un telefon în timp ce citește și valori de la o bandă de monitorizare a ritmului cardiac, sau poate accepta conexiuni de la două telefoane împerecheate simultan. API-ul aioble acceptă ambele modele, deoarece radioul multiplexează în spate, iar fiecare operațiune este deja o corutină – rulează mai multe corutine, iar lucrul se desfășoară în paralel pe o singură buclă de evenimente.

Această pagină adună modelele care apar frecvent.

11.12.1. Mai mulți clienți care se conectează la un singur periferic

Bucla simplă de periferic din Funcționarea ca periferic deservește un singur central conectat la un moment dat:

async def serve():
    while True:
        connection = await aioble.advertise(...)
        async with connection:
            await connection.disconnected()

Modelul care îi permite să accepte mai mult de un client este să declanșezi o sarcină per conexiune și să te întorci imediat în buclă la aioble.advertise(), astfel încât și următorul client să se poată conecta:

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))

Fiecare conexiune rulează în propria sarcină. Baza de date GATT este partajată – toți clienții văd aceleași servicii și caracteristici – dar starea per conexiune trăiește în interiorul sarcinii. Notificările ajung la fiecare client abonat atunci când write() este apelată cu send_update=True; transmiterile direcționate care ar trebui să ajungă doar la un singur client folosesc notify() / indicate() cu argumentul DeviceConnection specific.

Menține numărul de ramificări mic. Fiecare conexiune menținută consumă timp de radio, RAM și un slot în tabelul de conexiuni al controlerului, iar camera nu este proiectată să fie un hub pentru zeci de clienți. Două sau trei centrale (un telefon, o tabletă, un microcontroler companion) sunt perfect realizabile; proiectele care necesită mai mult aparțin unui gateway BLE adecvat, nu camerei.

11.12.2. Periferic și central în același timp

O cameră poate face publicitate propriului serviciu către un telefon, acționând în același timp și ca un central pentru un dispozitiv portabil. aioble nu are un comutator de „mod” – bucla de publicitate și bucla de scanare-și-conectare sunt pur și simplu corutine independente:

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())

Radioul împarte timpul între cele două roluri – o fereastră de scanare aici, o explozie de publicitate acolo, un eveniment de conexiune atunci când una dintre conexiunile oricăreia dintre părți este activă. Debitul fiecărui rol scade când ambele sunt active, deoarece radioul nu poate face literalmente două lucruri în același timp, dar pentru conversațiile cu lățime de bandă redusă pentru care a fost proiectat BLE, costul este de obicei invizibil.

Două aspecte practice de reținut:

  • Ambele roluri trebuie să fie în propria lor corutină. Apelarea aioble.scan() din interiorul sarcinii per client care gestionează un central conectat funcționează, dar blochează notificările acelui client până când scanarea se termină – rulează în schimb scanarea în propria sa sarcină.

  • Doar o singură scanare rulează la un moment dat. Dacă trebuie să scanezi din două locuri diferite, partajează iteratorul de scanare sau coordonează accesul; nu intra în două gestionare de context aioble.scan() în paralel.

11.12.3. Coordonarea mai multor conexiuni dintr-o singură sarcină

Atunci când mai multe conexiuni trebuie combinate într-o singură operațiune logică – de exemplu, camera comunică cu doi senzori simultan și raportează rezultatul abia după ce ambii au răspuns – primitivele standard asyncio se aplică direct. asyncio.gather() rulează corutinele per conexiune concurent și revine când toate s-au terminat; asyncio.wait_for() adaugă un termen limită.

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

Același model pe care îl folosește capitolul asyncio (Asyncio) pentru rețele – corutinele BLE se conectează la gather / wait_for / Event / Lock în același mod ca cele TCP.

11.12.4. Când un rol se termină per ciclu, iar celălalt nu

Un ciclu într-o cameră alimentată cu baterie ar putea arăta astfel:

  • Trezire.

  • Ca un central, citește valori proaspete de la o bandă de senzori împerecheată.

  • Ca un periferic, fă publicitate pentru ca un telefon să descarce măsurătorile zilei.

  • Când ambele sunt inactive, apelează aioble.stop() și intră în repaus.

Secvențierea este simplă cu două sarcini și 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