11.10. Djelovanje u ulozi centrale

Druga strana razgovora je centrala – uređaj koji skenira periferije koje se oglašavaju, odabire jednu za razgovor, otvara vezu, prolazi kroz udaljenu GATT bazu podataka te čita ili se pretplaćuje na njezine karakteristike. Kamera koja prikuplja očitanja s nosivog senzora, sluša signalni odašiljač (beacon) ili razgovara s pratećim mikrokontrolerom je centrala.

Obrazac centrale u aioble prolazi kroz četiri faze: skeniranje, povezivanje, otkrivanje, rad.

11.10.1. Skeniranje

aioble.scan() vraća asinkroni upravitelj konteksta koji ujedno služi i kao asinkroni iterator nad otkrivenim uređajima. Tipična upotreba je skeniranje dok se ne pojavi uređaj od interesa, a zatim izlazak iz iteracije:

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 ograničava koliko dugo skeniranje traje; duration_ms=0 skenira zauvijek (dok upravitelj konteksta ne izađe). active=True traži odgovore na skeniranje, što udvostručuje veličinu sadržaja po uređaju uz cijenu malog dodatnog prijenosa s obje strane. Preostali ključni argumenti interval_us / window_us podešavaju radni ciklus radija samog skenera i rijetko se mijenjaju od zadanih vrijednosti.

Svaki aioble.ScanResult izlaže adresu uređaja, posljednji RSSI, sirove bajtove oglašavanja i odgovora na skeniranje te pomoćnike koji raščlanjuju standardna polja:

  • result.deviceaioble.Device spreman za poziv connect().

  • result.rssi – pokazatelj jakosti primljenog signala u dBm, koristan za logiku „odaberi najbliži”.

  • result.name() – niz lokalnog imena, ili None ako nije oglašeno.

  • result.services() – generator bluetooth.UUID za svaku uslugu koju uređaj oglašava.

  • result.manufacturer() – generator torki (company_id, data) za polja specifična za proizvođača.

  • result.connectable – je li najnovije oglašavanje bilo ono na koje se može povezati.

Isti ScanResult ponovno se isporučuje kako pristižu novi podaci oglašavanja za isti uređaj, pa pasivni slušatelj koji samo želi neograničeno pratiti uređaje može pokretati asinkroni iterator zauvijek i otpremati na svakom događaju.

11.10.2. Povezivanje

Nakon što je ciljni uređaj identificiran, otvaranje veze je jedan await

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

aioble.Device.connect() prima timeout_ms (koliko dugo čekati da se veza uspostavi; zadano 10 s) te min_conn_interval_us / max_conn_interval_us (traženi raspon intervala veze iz Veze).

11.10.2.1. Ponovno povezivanje s poznatim suparnikom bez skeniranja

Nakon što postoji veza s parnjakom, adresa je već poznata, a još jedan krug skeniranja i odabira je izgubljeno vrijeme radija. Izravno konstruirajte aioble.Device sa spremljenom adresom i preskočite ravno na 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

Prvi argument je jedan od aioble.ADDR_PUBLIC (tvornička adresa kontrolera) ili aioble.ADDR_RANDOM (generirana statična ili razrješiva privatna adresa); drugi je ili šestbajtna bytes vrijednost ili heksadecimalni niz odvojen dvotočkama. Atributi addr_type i addr bilo kojeg Device (npr. onog dobivenog ranije iz ScanResult) mogu se trajno pohraniti i ovdje ponovno proslijediti.

Vraćeni aioble.DeviceConnection je ono o čemu ovisi ostatak posla centrale. async with osigurava da se veza zatvori kad blok izađe – pri uspjehu, pri otkazivanju ili pri bilo kojoj iznimci uključujući aioble.DeviceDisconnectedError zbog odlaska parnjaka.

Ako centrali treba veća vrijednost karakteristike nego što zadani 23-bajtni MTU dopušta, ovo je mjesto za njezino dogovaranje:

await connection.exchange_mtu(512)

(exchange_mtu() vraća stvarno dogovoreni MTU, koji je minimum tražene vrijednosti i onoga što parnjak podržava.)

11.10.3. Otkrivanje

Otkrivanje prolazi kroz udaljenu GATT bazu podataka kako bi pronašlo usluge i karakteristike prema njihovim UUID-ovima. Postoje dvije inačice: ciljano (znate UUID i želite jednu određenu stvar) i iscrpno (želite sve).

Ciljano – uobičajeni slučaj:

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() i aioble.ClientService.characteristic() svaki primaju bluetooth.UUID i vraćaju odgovarajući objekt (ili None). Oba imaju ključnu riječ timeout_ms po otkrivanju koja je zadano 2 s.

Iscrpno:

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 ono što rade generičke aplikacije za istraživanje Bluetootha – korisno za razvoj, manje za produkcijski kod koji zna koje UUID-ove očekuje.

11.10.3.1. Pregledavanje onoga što karakteristika podržava

Otkrivanje vraća bitmasku GATT svojstava koju je parnjak oglasio za svaku karakteristiku kao properties. Bitovi su oni definirani u GATT-u – read (0x02), write-without-response (0x04), write (0x08), notify (0x10), indicate (0x20) i drugi. Pregledavanje bitmaske prije izdavanja operacije omogućuje generičkom klijentu prilagodbu karakteristikama čije mogućnosti ne poznaje unaprijed:

_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

Produkcijskom kodu koji već poznaje GATT profil parnjaka ovo obično nije potrebno – UUID-ovi su dokumentirani unaprijed. Generički / istraživački klijenti (stranica postavki koja prolazi nepoznatim uređajem, host za dodatke) oslanjaju se na njega.

11.10.4. Rad

Nakon što centrala drži ClientCharacteristic, svaka GATT operacija je jedan poziv korutine:

  • Čitanje. Izdaj GATT čitanje i vrati vrijednost:

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

    Duga čitanja (vrijednosti veće od MTU-a) obrađuju se transparentno.

  • Pisanje. Pošalji novu vrijednost poslužitelju:

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

    response=True čeka na odgovor pisanja i podiže aioble.GattError ako poslužitelj odbije pisanje. response=False je pisanje bez odgovora: pošalji i zaboravi. response=None (zadano) automatski bira na temelju onoga što je parnjak oglasio.

  • Pretplata. Omogući obavijesti ili indikacije pisanjem u CCCD karakteristike:

    await char.subscribe(notify=True)
    

    Nakon što se ovo vrati, centrala može čekati na dolazne potiske.

  • Obaviješteno / indicirano. Čekaj na sljedeći potisak od poslužitelja:

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

    timeout_ms=None (zadano) čeka zauvijek; proslijedite cijeli broj u milisekundama za odustajanje nakon nekog vremena.

Spajanje sva četiri zajedno daje kanonski program centrale „poveži se, pretplati se, primaj tok”:

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

Cijela stvar je nekoliko desetaka redaka i pokriva tijek od „nema pokrenutog Bluetootha” do „podaci uživo teku”. Iterator skeniranja odgovara obrascu odašiljač/promatrač, connect otvara GAP vezu, service / characteristic prolaze kroz GATT stablo, subscribe zapisuje CCCD, a notified čeka na potiske.

11.10.5. Prekidi veze i ponovno povezivanje

Sve što se dogodi radijskoj vezi izlazi na površinu u korutini koja je na nju čekala. aioble.DeviceDisconnectedError je signal da je parnjak otišao ili da je istekao nadzorni timeout; iznimka prekida bilo koji poziv read(), write() ili notified() koji je bio u tijeku, a svaki async with connection blok uredno izlazi.

Centrala koja bi se trebala ponovno povezati pri gubitku omata posao u vlastitu vanjsku petlju:

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. Omeđivanje slijeda s timeout()

Kada bi nekoliko uzastopnih GATT operacija sve trebalo dovršiti unutar jednog proračuna – a ne svaka pojedinačno na vlastitom timeout_ms – upotrijebite aioble.DeviceConnection.timeout() da ih omotate. Vraćeni upravitelj konteksta otkazuje svoje tijelo ako proračun istekne (podižući asyncio.TimeoutError) ili ako se parnjak odspoji (podižući 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")

Ovo je čišća alternativa omatanju svakog poziva pojedinačno u asyncio.wait_for() i izbjegava lažne uspjehe gdje svaki poziv ispunjava vlastiti rok, ali slijed kao cjelina prekorači. Prosljeđivanje timeout_ms=None u timeout() onemogućuje rok i ostavlja aktivnim samo zaštitu od odspajanja.