11.12. Rinnakkaiset roolit ja useat yhteydet

Peripheral- ja central-sivut esittelevät kumpikin yhden roolin, joka palvelee yhtä yhteyttä kerrallaan. Todelliset sovellukset ovat harvoin näin yksinkertaisia. Kamera voi julkaista sensoripalvelun puhelimelle samalla kun se lukee arvoja sykevyöstä, tai hyväksyä yhteyksiä kahdesta samanaikaisesti paritetusta puhelimesta. aioble-API tukee molempia malleja, koska radio multipleksoi taustalla ja jokainen operaatio on jo valmiiksi korutiini – aja lisää korutiineja, niin työ tapahtuu rinnakkain yhdessä tapahtumasilmukassa.

Tämä sivu kokoaa yhteen useimmin esiintyvät mallit.

11.12.1. Useita asiakkaita yhteen peripheraaliin

Yksinkertainen peripheral-silmukka sivulla Oheislaitteena toimiminen palvelee yhtä yhdistettyä central-laitetta kerrallaan:

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

Malli, joka antaa sen hyväksyä useamman kuin yhden asiakkaan, on käynnistää yhteyskohtainen tehtävä ja palata heti takaisin aioble.advertise()-kutsuun, jotta seuraavakin asiakas voi yhdistyä:

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

Jokainen yhteys ajetaan omassa tehtävässään. GATT-tietokanta on jaettu – kaikki asiakkaat näkevät samat palvelut ja ominaisuudet – mutta yhteyskohtainen tila elää tehtävän sisällä. Ilmoitukset menevät jokaiselle tilanneelle asiakkaalle, kun write() kutsutaan parametrilla send_update=True; suunnatut työnnöt, joiden tulee tavoittaa vain yksi asiakas, käyttävät notify() / indicate() -metodeja tietyllä DeviceConnection-argumentilla.

Pidä haaroitus pienenä. Jokainen ylläpidetty yhteys kuluttaa radioaikaa, RAM-muistia ja paikan ohjaimen yhteystaulukossa, eikä kameraa ole suunniteltu kymmenien asiakkaiden keskittimeksi. Kaksi tai kolme central-laitetta (puhelin, tabletti, oheismikro-ohjain) ovat hyvin saavutettavissa; suunnitelmat, jotka tarvitsevat enemmän, kuuluvat varsinaiseen BLE-yhdyskäytävään eikä kameraan.

11.12.2. Peripheral ja central yhtä aikaa

Kamera voi mainostaa omaa palveluaan puhelimelle samalla kun se myös toimii central-laitteena puettavaa laitetta kohtaan. aioble-moduulissa ei ole ”tila”-kytkintä – mainossilmukka ja skannaa-ja-yhdistä-silmukka ovat vain itsenäisiä korutiineja:

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

Radio jakaa aikaa kahden roolin välillä – skannausikkuna täällä, mainospurske tuolla, yhteystapahtuma kun jompikumpi puolen yhteyksistä on aktiivinen. Kummankin roolin läpäisykyky laskee, kun molemmat ovat aktiivisia, koska radio ei kirjaimellisesti voi tehdä kahta asiaa yhtä aikaa, mutta niissä matalan kaistanleveyden keskusteluissa, joita varten BLE suunniteltiin, kustannus on yleensä näkymätön.

Kaksi käytännön asiaa muistettavaksi:

  • Molempien roolien on oltava omassa korutiinissaan. aioble.scan()-kutsuminen yhdistettyä central-laitetta käsittelevän asiakaskohtaisen tehtävän sisältä toimii, mutta estää kyseisen asiakkaan ilmoitukset, kunnes skannaus valmistuu – aja skannaus sen sijaan omassa tehtävässään.

  • Vain yksi skannaus on käynnissä kerrallaan. Jos sinun täytyy skannata kahdesta eri paikasta, jaa skanneri-iteraattori tai koordinoi käyttöä; älä siirry kahteen aioble.scan()-kontekstinhallintaan rinnakkain.

11.12.3. Useiden yhteyksien koordinointi yhdestä tehtävästä

Kun useat yhteydet on yhdistettävä yhdeksi loogiseksi operaatioksi – esimerkiksi kamera keskustelee kahden sensorin kanssa yhtä aikaa ja raportoi tuloksen vasta, kun molemmat ovat vastanneet – vakiintuneet asyncio-primitiivit pätevät suoraan. asyncio.gather() ajaa yhteyskohtaiset korutiinit rinnakkain ja palaa, kun kaikki ovat valmistuneet; asyncio.wait_for() lisää määräajan.

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

Sama malli, jota asyncio-luku (Asyncio) käyttää verkkoyhteyksiin – BLE-korutiinit liittyvät gather / wait_for / Event / Lock -primitiiveihin samalla tavalla kuin TCP-korutiinit.

11.12.4. Kun toinen rooli valmistuu jaksoittain ja toinen ei

Akkukäyttöisen kameran jakso voisi näyttää tältä:

  • Herää.

  • Lue central-laitteena tuoreet arvot paritetusta sensorivyöstä.

  • Mainosta peripheraalina, jotta puhelin voi ladata päivän mittaukset.

  • Kun molemmat ovat lepotilassa, kutsu aioble.stop() ja nuku.

Jaksotus on suoraviivaista kahdella tehtävällä ja yhdellä asyncio.Event-oliolla:

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