11.10. Központként való működés

A beszélgetés másik oldala a központ – az az eszköz, amely hirdető perifériákat keres, kiválaszt egyet, amellyel kommunikál, kapcsolatot nyit, bejárja a távoli GATT adatbázist, és olvas vagy feliratkozik a rajta lévő jellemzőkre. Egy kamera, amely egy hordható érzékelőtől gyűjt méréseket, egy jeladót figyel, vagy egy társ mikrokontrollerrel kommunikál, központ.

A központi minta az aioble modulban négy szakaszon fut keresztül: keresés, kapcsolódás, felderítés, működtetés.

11.10.1. Keresés

Az aioble.scan() egy async kontextuskezelőt ad vissza, amely egyúttal egy async iterátorként is működik a felfedezett eszközök fölött. A tipikus felhasználás az, hogy addig keresünk, amíg egy érdekes eszköz meg nem jelenik, majd kilépünk az iterációból:

import aioble
import asyncio
import bluetooth

HR_SERVICE = bluetooth.UUID(0x180D)

async def find_heart_rate():
    async with aioble.scan(duration_ms=5000, active=True) as scanner:
        async for result in scanner:
            if HR_SERVICE in result.services():
                return result.device
    return None

A duration_ms=5000 korlátozza, hogy meddig fut a keresés; a duration_ms=0 örökké keres (amíg a kontextuskezelő ki nem lép). Az active=True keresési válaszokat kér, ami megduplázza az eszközönkénti adattartalom méretét egy kis további átvitel árán mindkét oldalról. A fennmaradó interval_us / window_us kulcsszó argumentumok a kereső saját rádió-kihasználtsági ciklusát hangolják, és ritkán változtatják meg az alapértelmezésekhez képest.

Minden egyes aioble.ScanResult elérhetővé teszi az eszköz címét, a legutóbbi RSSI értéket, a nyers hirdetési és keresési válasz bájtokat, valamint a szabványos mezőket elemző segédeszközöket:

  • result.device – egy aioble.Device, amely készen áll arra, hogy meghívják rajta a connect() metódust.

  • result.rssi – a vett jelerősség mutatója dBm-ben, hasznos a „válaszd ki a legközelebbit” logikához.

  • result.name() – a helyi név karakterlánc, vagy None, ha nincs hirdetve.

  • result.services() – egy bluetooth.UUID generátor minden szolgáltatáshoz, amelyet az eszköz hirdet.

  • result.manufacturer() – egy (company_id, data) rendezett párokból álló generátor a gyártóspecifikus mezőkhöz.

  • result.connectable – hogy a legutóbbi hirdetés kapcsolódható volt-e.

Ugyanaz a ScanResult újra kibocsátásra kerül, ahogy új hirdetési adatok érkeznek ugyanahhoz az eszközhöz, így egy passzív hallgató, amely csak korlátlan ideig szeretné követni az eszközöket, örökké futtathatja az async iterátort, és minden eseményre reagálhat.

11.10.2. Kapcsolódás

Miután egy céleszközt azonosítottunk, egy kapcsolat megnyitása egyetlen await

async def talk_to(device):
    connection = await device.connect()           # 10 s timeout
    async with connection:
        # ... do GATT work ...
        pass

Az aioble.Device.connect() átveszi a timeout_ms paramétert (mennyi ideig várjon a kapcsolat létrejöttére; alapértelmezés 10 mp), valamint a min_conn_interval_us / max_conn_interval_us paramétereket (a kért kapcsolatintervallum-tartomány a Kapcsolatok oldalról).

11.10.2.1. Újrakapcsolódás egy ismert partnerhez keresés nélkül

Miután egy kötés létezik egy partnerrel, a cím már ismert, és egy újabb keresés-és-választás kör elpazarolt rádióidő. Hozzunk létre közvetlenül egy aioble.Device objektumot a mentett címmel, és ugorjunk egyenesen a connect() hívásra:

import aioble

KITCHEN_CAM = aioble.Device(aioble.ADDR_PUBLIC,
                            "aa:bb:cc:dd:ee:ff")

async def talk_to_kitchen():
    async with await KITCHEN_CAM.connect() as connection:
        # ... GATT work ...
        pass

Az első argumentum az aioble.ADDR_PUBLIC (egy vezérlő gyári címe) vagy az aioble.ADDR_RANDOM (egy generált statikus vagy feloldható privát cím) egyike; a második vagy egy hatbájtos bytes érték, vagy egy kettősponttal elválasztott hexadecimális karakterlánc. Bármely Device (pl. egy korábban egy ScanResult objektumból kapott) addr_type és addr attribútuma megőrizhető és visszatáplálható ide.

A visszaadott aioble.DeviceConnection az, amire a központ további munkája támaszkodik. Az async with biztosítja, hogy a kapcsolat lezáruljon, amikor a blokk kilép – sikeresség, megszakítás vagy bármely kivétel esetén, beleértve a partner távozásából eredő aioble.DeviceDisconnectedError kivételt is.

Ha a központnak nagyobb jellemzőértékre van szüksége, mint amit az alapértelmezett 23 bájtos MTU megenged, ez az a hely, ahol egyeztetni kell róla:

await connection.exchange_mtu(512)

(Az exchange_mtu() a ténylegesen egyeztetett MTU-t adja vissza, amely a kért érték és a partner által támogatott érték közül a kisebbik.)

11.10.3. Felderítés

A felderítés bejárja a távoli GATT adatbázist, hogy megtalálja a szolgáltatásokat és jellemzőket az UUID-jük alapján. Két változata van: célzott (ismered az UUID-t, és egy konkrét dolgot szeretnél) és kimerítő (mindent szeretnél).

Célzott – a gyakori eset:

service = await connection.service(HR_SERVICE)
if service is None:
    return                                        # no such service

char = await service.characteristic(HR_MEASUREMENT)
if char is None:
    return                                        # no such characteristic

Az aioble.DeviceConnection.service() és az aioble.ClientService.characteristic() egyaránt átvesz egy bluetooth.UUID értéket, és visszaadja az illeszkedő objektumot (vagy None értéket). Mindkettő rendelkezik egy felderítésenkénti timeout_ms kulcsszóval, amely alapértelmezetten 2 mp.

Kimerítő:

async for service in connection.services():
    print("service:", service.uuid)
    async for char in service.characteristics():
        print("  characteristic:", char.uuid, "properties:", hex(char.properties))

Ezt teszik az általános Bluetooth-felfedező alkalmazások – hasznos a fejlesztéshez, kevésbé az olyan éles kódhoz, amely tudja, hogy milyen UUID-ket vár.

11.10.3.1. Annak vizsgálata, hogy egy jellemző mit támogat

A felderítés a peer által minden egyes jellemzőhöz hirdetett GATT tulajdonság-bitmaszkot adja vissza properties formájában. A bitek a GATT által definiáltak – olvasás (0x02), válasz nélküli írás (0x04), írás (0x08), értesítés (0x10), jelzés (0x20) és társaik. A bitmaszk vizsgálata egy művelet kiadása előtt lehetővé teszi, hogy egy általános kliens alkalmazkodjon az olyan jellemzőkhöz, amelyek képességeit előzetesen nem ismeri:

_PROP_READ = const(0x02)
_PROP_NOTIFY = const(0x10)

char = await service.characteristic(STATUS_UUID)
if char.properties & _PROP_NOTIFY:
    await char.subscribe(notify=True)
    value = await char.notified()
elif char.properties & _PROP_READ:
    value = await char.read()
else:
    value = None                                  # nothing the client can do

Az éles kódnak, amely már ismeri a partner GATT profilját, általában nincs erre szüksége – az UUID-k előzetesen dokumentálva voltak. Az általános / felfedező kliensek (egy beállítási oldal, amely bejár egy ismeretlen eszközt, egy bővítmény-gazda) támaszkodnak rá.

11.10.4. Működtetés

Miután a központ birtokol egy ClientCharacteristic objektumot, minden egyes GATT művelet egyetlen korutin-hívás:

  • Olvasás. Adj ki egy GATT olvasást, és kapd vissza az értéket:

    value = await char.read()
    print("value:", value)
    

    A hosszú olvasások (az MTU-nál nagyobb értékek) átláthatóan kezelődnek.

  • Írás. Küldj egy új értéket a szervernek:

    await char.write(b"\\x01")
    

    A response=True egy írásválaszra vár, és aioble.GattError kivételt dob, ha a szerver elutasítja az írást. A response=False válasz nélküli írás: tűzd-és-felejtsd. A response=None (az alapértelmezés) automatikusan választ aszerint, hogy a partner mit hirdetett.

  • Feliratkozás. Engedélyezd az értesítéseket vagy jelzéseket a jellemző CCCD-jébe való írással:

    await char.subscribe(notify=True)
    

    Miután ez visszatér, a központ várhatja a beérkező leküldéseket.

  • Értesített / jelzett. Várj a szerver következő leküldésére:

    while True:
        data = await char.notified()
        print("push:", data)
    

    A timeout_ms=None (az alapértelmezés) örökké vár; adj át egy egész számot ezredmásodpercben, hogy egy idő után feladja.

A négy összeillesztése megadja a kanonikus „kapcsolódás, feliratkozás, adatfolyam” központi programot:

async def stream_heart_rate():
    async with aioble.scan(duration_ms=5000, active=True) as scanner:
        async for result in scanner:
            if HR_SERVICE in result.services():
                device = result.device
                break
        else:
            return

    async with await device.connect() as connection:
        service = await connection.service(HR_SERVICE)
        char = await service.characteristic(HR_MEASUREMENT)
        await char.subscribe(notify=True)
        while connection.is_connected():
            data = await char.notified()
            print("hr push:", data)

asyncio.run(stream_heart_rate())

Az egész körülbelül egy tucat sor, és lefedi a folyamatot a „nincs futó Bluetooth” állapottól az „élő adatfolyam” állapotig. A keresési iterátor megfelel a sugárzó/megfigyelő mintának, a connect megnyitja a GAP kapcsolatot, a service / characteristic bejárja a GATT fát, a subscribe írja a CCCD-t, a notified pedig a leküldésekre vár.

11.10.5. Lecsatlakozások és újrakapcsolódás

Bármi, ami a rádiókapcsolattal történik, abban a korutinban jelenik meg, amely rá várt. Az aioble.DeviceDisconnectedError az a jelzés, hogy a partner távozott, vagy a felügyeleti időtúllépés bekövetkezett; a kivétel megszakít bármely folyamatban lévő read(), write() vagy notified() hívást, és bármely async with connection blokk tisztán kilép.

Egy központ, amelynek veszteség esetén újra kell kapcsolódnia, a munkát a saját külső ciklusába csomagolja:

async def keep_streaming():
    while True:
        try:
            await stream_heart_rate()
        except aioble.DeviceDisconnectedError:
            print("disconnected, retrying...")
            await asyncio.sleep(2)

11.10.5.1. Egy szekvencia zárójelezése a timeout() használatával

Amikor több egymás utáni GATT műveletnek mind egyetlen kereten belül kell befejeződnie – nem egyenként a saját timeout_ms paraméterén –, használd az aioble.DeviceConnection.timeout() metódust ezek becsomagolásához. A visszaadott kontextuskezelő megszakítja a törzsét, ha a keret letelik (asyncio.TimeoutError kivételt dobva), vagy ha a partner lecsatlakozik (aioble.DeviceDisconnectedError kivételt dobva):

async with await device.connect() as connection:
    try:
        with connection.timeout(2000):                    # 2 s for the whole block
            service = await connection.service(HR_SERVICE)
            char = await service.characteristic(HR_MEASUREMENT)
            await char.subscribe(notify=True)
    except asyncio.TimeoutError:
        print("discovery + subscribe took too long")

Ez a tisztább alternatívája annak, hogy minden egyes hívást külön-külön becsomagoljunk egy asyncio.wait_for() hívásba, és elkerüli a hamis sikereket, ahol minden hívás teljesíti a saját határidejét, de a szekvencia egésze túllépi azt. A timeout_ms=None átadása az timeout() metódusnak letiltja a határidőt, és csak a lecsatlakozási őrt hagyja aktívan.