11.10. Vystupování jako central

Druhou stranou konverzace je central – zařízení, které vyhledává inzerující periferie, vybere jednu, s níž bude komunikovat, otevře připojení, projde vzdálenou GATT databázi a čte nebo se přihlásí k odběru jejích charakteristik. Kamera, která sbírá údaje z nositelného senzoru, naslouchá majáku nebo komunikuje s doprovodným mikrokontrolérem, je central.

Vzor centralu v aioble probíhá ve čtyřech fázích: vyhledání, připojení, objevení, operace.

11.10.1. Vyhledávání

aioble.scan() vrací asynchronní kontextový správce, který slouží zároveň jako asynchronní iterátor přes objevená zařízení. Typickým použitím je vyhledávat, dokud se neobjeví zařízení, o které máme zájem, a poté z iterace vystoupit:

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

duration_ms=5000 omezuje, jak dlouho vyhledávání běží; duration_ms=0 vyhledává navždy (dokud se kontextový správce neukončí). active=True vyžaduje odpovědi na vyhledávání (scan responses), což zdvojnásobuje velikost užitečného zatížení na zařízení za cenu malého dodatečného přenosu z obou stran. Zbývající klíčové argumenty interval_us / window_us ladí pracovní cyklus vlastního rádia vyhledávače a od výchozích hodnot se mění jen zřídka.

Každý aioble.ScanResult zpřístupňuje adresu zařízení, poslední RSSI, surové bajty inzerce a odpovědi na vyhledávání a pomocníky, kteří parsují standardní pole:

  • result.deviceaioble.Device připravený k volání connect().

  • result.rssi – indikátor síly přijímaného signálu v dBm, užitečný pro logiku „vyber nejbližší“.

  • result.name() – řetězec s lokálním jménem, nebo None, pokud není inzerován.

  • result.services() – generátor bluetooth.UUID pro každou službu, kterou zařízení inzeruje.

  • result.manufacturer() – generátor n-tic (company_id, data) pro pole specifická pro výrobce.

  • result.connectable – zda byla nejnovější inzerce připojitelná.

Tentýž ScanResult je opětovně vydán, jakmile pro stejné zařízení dorazí nová inzertní data, takže pasivní posluchač, který chce jen neomezeně sledovat zařízení, může asynchronní iterátor spustit navždy a reagovat na každou událost.

11.10.2. Připojování

Jakmile je cílové zařízení identifikováno, otevření připojení je jedno await

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

aioble.Device.connect() přijímá timeout_ms (jak dlouho čekat na navázání připojení; výchozí 10 s) a min_conn_interval_us / max_conn_interval_us (požadovaný rozsah intervalu připojení z Připojení).

11.10.2.1. Opětovné připojení ke známému protějšku bez vyhledávání

Jakmile s protějškem existuje vazba, adresa je již známa a další kolo vyhledávání a výběru je promarněný čas rádia. Sestrojte aioble.Device přímo s uloženou adresou a přejděte rovnou k connect()

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

Prvním argumentem je jeden z aioble.ADDR_PUBLIC (tovární adresa řadiče) nebo aioble.ADDR_RANDOM (generovaná statická nebo rozlišitelná privátní adresa); druhým je buď šestibajtová hodnota bytes, nebo hexadecimální řetězec oddělený dvojtečkami. Atributy addr_type a addr libovolného Device (např. získaného dříve z ScanResult) lze uložit a zde znovu předat.

Vrácený aioble.DeviceConnection je tím, na čem visí zbytek práce centralu. async with zajišťuje, že připojení bude při výstupu z bloku uzavřeno – při úspěchu, při zrušení nebo při jakékoli výjimce včetně aioble.DeviceDisconnectedError při odpojení protějšku.

Pokud central potřebuje větší hodnotu charakteristiky, než dovoluje výchozí 23bajtová MTU, je toto místo k jejímu vyjednání:

await connection.exchange_mtu(512)

(exchange_mtu() vrací skutečně vyjednanou MTU, což je minimum z požadované hodnoty a toho, co protějšek podporuje.)

11.10.3. Objevování

Objevování prochází vzdálenou GATT databázi, aby našlo služby a charakteristiky podle jejich UUID. Existují dvě varianty: cílená (znáte UUID a chcete jednu konkrétní věc) a vyčerpávající (chcete vše).

Cílená – běžný případ:

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

aioble.DeviceConnection.service() a aioble.ClientService.characteristic() každá přijímá bluetooth.UUID a vrací odpovídající objekt (nebo None). Obě mají klíčový argument timeout_ms pro objevování, jehož výchozí hodnota je 2 s.

Vyčerpávající:

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

To je to, co dělají obecné aplikace pro průzkum Bluetooth – užitečné pro vývoj, méně už pro produkční kód, který ví, jaké UUID očekává.

11.10.3.1. Zkoumání toho, co charakteristika podporuje

Objevování vrací bitovou masku GATT vlastností, kterou protějšek inzeroval pro každou charakteristiku, jako properties. Bity jsou ty definované v GATT – read (0x02), write-without-response (0x04), write (0x08), notify (0x10), indicate (0x20) a další. Zkoumání bitové masky před vydáním operace umožňuje obecnému klientovi přizpůsobit se charakteristikám, jejichž schopnosti předem nezná:

_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

Produkční kód, který už zná GATT profil protějšku, to obvykle nepotřebuje – UUID byla zdokumentována předem. Obecní / průzkumní klienti (stránka nastavení procházející neznámé zařízení, hostitel pluginů) se na to spoléhají.

11.10.4. Operace

Jakmile central drží ClientCharacteristic, každá GATT operace je jedno volání korutiny:

  • Čtení. Vydejte GATT čtení a získejte zpět hodnotu:

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

    Dlouhá čtení (hodnoty větší než MTU) jsou zpracována transparentně.

  • Zápis. Odešlete novou hodnotu na server:

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

    response=True čeká na odpověď zápisu a vyvolá aioble.GattError, pokud server zápis odmítne. response=False je zápis bez odpovědi: fire-and-forget. response=None (výchozí) automaticky vybírá podle toho, co protějšek inzeroval.

  • Přihlášení k odběru. Povolte notifikace nebo indikace zápisem do CCCD charakteristiky:

    await char.subscribe(notify=True)
    

    Po návratu z tohoto volání může central čekat na příchozí přenosy.

  • Notifikováno / indikováno. Čekejte na další přenos ze serveru:

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

    timeout_ms=None (výchozí) čeká navždy; předejte celé číslo v milisekundách, chcete-li to po nějaké době vzdát.

Spojení všech čtyř dohromady dává kanonický central program „připoj se, přihlas se k odběru, streamuj“:

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

Celá věc má asi tucet řádků a pokrývá tok od „žádné běžící Bluetooth“ po „živý datový stream“. Iterátor vyhledávání odpovídá vzoru broadcaster/observer, connect otevírá GAP připojení, service / characteristic prochází GATT strom, subscribe zapisuje CCCD a notified čeká na přenosy.

11.10.5. Odpojení a opětovné připojení

Cokoli, co se přihodí rádiovému spojení, se projeví v korutině, která na něj čekala. aioble.DeviceDisconnectedError je signál, že protějšek odešel nebo se spustil dohledový timeout; výjimka ukončí jakékoli probíhající volání read(), write() nebo notified() a jakýkoli blok async with connection se čistě ukončí.

Central, který se má při ztrátě znovu připojit, obalí práci do vlastní vnější smyčky:

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. Ohraničení posloupnosti pomocí timeout()

Když má několik GATT operací v řadě dohromady proběhnout v rámci jednoho rozpočtu – nikoli každá jednotlivě se svým vlastním timeout_ms – použijte k jejich obalení aioble.DeviceConnection.timeout(). Vrácený kontextový správce zruší své tělo, pokud rozpočet uplyne (vyvolá asyncio.TimeoutError) nebo pokud se protějšek odpojí (vyvolá aioble.DeviceDisconnectedError):

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

Toto je čistší alternativa k obalování každého volání jednotlivě do asyncio.wait_for() a vyhýbá se falešným úspěchům, kdy každé volání splní svůj vlastní termín, ale posloupnost jako celek překročí limit. Předání timeout_ms=None do timeout() deaktivuje termín a ponechá aktivní pouze ochranu před odpojením.