11.10. Toimiminen keskusyksikkönä

Keskustelun toinen osapuoli on keskusyksikkö (central) – laite, joka skannaa mainostavia oheislaitteita, valitsee yhden keskusteltavaksi, avaa yhteyden, käy läpi etäkohteen GATT-tietokannan ja lukee tai tilaa siinä olevia ominaisuuksia. Kamera, joka kerää lukemia puettavasta sensorista, kuuntelee majakkaa tai puhuu kumppanimikrokontrollerille, on keskusyksikkö.

Keskusyksikön kuvio aioble-moduulissa etenee neljän vaiheen kautta: skannaus, yhteyden muodostus, löytäminen, toiminta.

11.10.1. Skannaus

aioble.scan() palauttaa asynkronisen kontekstinhallintaobjektin, joka toimii myös asynkronisena iteraattorina löydettyihin laitteisiin. Tyypillinen käyttö on skannata, kunnes kiinnostava laite ilmaantuu, ja keskeyttää sitten iteraatio:

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 rajoittaa skannauksen kestoa; duration_ms=0 skannaa ikuisesti (kunnes kontekstinhallintaobjektista poistutaan). active=True pyytää skannausvastauksia, mikä kaksinkertaistaa laitekohtaisen hyötykuorman koon pienen lisälähetyksen kustannuksella molemmilta puolilta. Jäljelle jäävät interval_us / window_us -avainsana-argumentit säätävät skannerin oman radion työjaksoa, ja niitä muutetaan harvoin oletusarvoista.

Jokainen aioble.ScanResult paljastaa laitteen osoitteen, viimeisimmän RSSI:n, raa’at mainostus- ja skannausvastaustavut sekä apuvälineet, jotka jäsentävät vakiokentät:

  • result.deviceaioble.Device, joka on valmis connect()-kutsua varten.

  • result.rssi – vastaanotetun signaalin voimakkuuden ilmaisin dBm-yksikössä, hyödyllinen ”valitse lähin” -logiikkaan.

  • result.name() – paikallinen nimi -merkkijono tai None, jos sitä ei mainosteta.

  • result.services() – generaattori, joka tuottaa bluetooth.UUID-arvon jokaiselle palvelulle, jota laite mainostaa.

  • result.manufacturer() – generaattori, joka tuottaa (company_id, data) -monikkoja valmistajakohtaisille kentille.

  • result.connectable – onko viimeisin mainostus ollut yhdistettävissä oleva.

Sama ScanResult tuotetaan uudelleen, kun uutta mainostusdataa saapuu samalle laitteelle, joten passiivinen kuuntelija, joka haluaa vain seurata laitteita rajattomasti, voi ajaa asynkronista iteraattoria ikuisesti ja käsitellä jokaisen tapahtuman.

11.10.2. Yhteyden muodostaminen

Kun kohdelaite on tunnistettu, yhteyden avaaminen on yksi await

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

aioble.Device.connect() ottaa timeout_ms-parametrin (kuinka kauan odotetaan yhteyden muodostumista; oletus 10 s) sekä min_conn_interval_us / max_conn_interval_us -parametrit (pyydetty yhteysvälin alue sivulta Yhteydet).

11.10.2.1. Uudelleenyhdistäminen tunnettuun vertaiseen ilman skannausta

Kun sidos vertaisen kanssa on olemassa, osoite on jo tiedossa ja uusi skannaa-ja-valitse -kierros on hukattua radioaikaa. Rakenna aioble.Device suoraan tallennetulla osoitteella ja siirry suoraan connect()-kutsuun:

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

Ensimmäinen argumentti on jompikumpi aioble.ADDR_PUBLIC (ohjaimen tehdasosoite) tai aioble.ADDR_RANDOM (luotu staattinen tai ratkaistavissa oleva yksityinen osoite); toinen on joko kuusitavuinen bytes-arvo tai kaksoispisteillä erotettu heksamerkkijono. Minkä tahansa Device-objektin (esim. aiemmin ScanResult-objektista saadun) addr_type- ja addr-attribuutit voidaan tallentaa ja syöttää takaisin tähän.

Palautettu aioble.DeviceConnection on se, johon keskusyksikön loput työt nojaavat. async with varmistaa, että yhteys suljetaan lohkosta poistuttaessa – onnistuessa, peruutettaessa tai minkä tahansa poikkeuksen kohdalla, mukaan lukien aioble.DeviceDisconnectedError vertaisen poistuessa.

Jos keskusyksikkö tarvitsee suuremman ominaisuusarvon kuin oletusarvoinen 23-tavuinen MTU sallii, tämä on paikka neuvotella siitä:

await connection.exchange_mtu(512)

(exchange_mtu() palauttaa todellisuudessa neuvotellun MTU:n, joka on pyydetyn arvon ja vertaisen tukeman arvon pienempi.)

11.10.3. Löytäminen

Löytäminen käy läpi etäkohteen GATT-tietokannan löytääkseen palvelut ja ominaisuudet niiden UUID:iden perusteella. Tapoja on kaksi: kohdennettu (tiedät UUID:n ja haluat yhden tietyn asian) ja kattava (haluat kaiken).

Kohdennettu – yleinen tapaus:

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() ja aioble.ClientService.characteristic() ottavat kumpikin bluetooth.UUID-arvon ja palauttavat vastaavan objektin (tai None). Molemmilla on löytämiskohtainen timeout_ms-avainsana, jonka oletus on 2 s.

Kattava:

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

Tämä on sitä, mitä yleiskäyttöiset Bluetooth-tutkimussovellukset tekevät – hyödyllistä kehitystyössä, vähemmän tuotantokoodissa, joka tietää mitä UUID:itä se odottaa.

11.10.3.1. Sen tarkastelu, mitä ominaisuus tukee

Löytäminen palauttaa GATT-ominaisuusbittimaskin, jonka vertainen mainosti kullekin ominaisuudelle, muodossa properties. Bitit ovat GATT:n määrittelemiä – luku (0x02), kirjoitus ilman vastausta (0x04), kirjoitus (0x08), ilmoitus (0x10), indikaatio (0x20) ja vastaavat. Bittimaskin tarkastelu ennen operaation suorittamista antaa yleiskäyttöisen asiakkaan mukautua ominaisuuksiin, joiden kykyjä se ei tunne etukäteen:

_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

Tuotantokoodi, joka jo tuntee vertaisen GATT-profiilin, ei yleensä tarvitse tätä – UUID:t oli dokumentoitu etukäteen. Yleiskäyttöiset / tutkivat asiakkaat (asetussivu, joka käy läpi tuntemattoman laitteen, lisäosaisäntä) nojaavat siihen.

11.10.4. Toiminta

Kun keskusyksikkö pitää hallussaan ClientCharacteristic-objektia, jokainen GATT-operaatio on yksi korutiinikutsu:

  • Luku. Suorita GATT-luku ja saa arvo takaisin:

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

    Pitkät luvut (MTU:ta suuremmat arvot) käsitellään läpinäkyvästi.

  • Kirjoitus. Lähetä uusi arvo palvelimelle:

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

    response=True odottaa kirjoitusvastausta ja nostaa aioble.GattError-poikkeuksen, jos palvelin hylkää kirjoituksen. response=False on kirjoitus ilman vastausta: lähetä-ja-unohda. response=None (oletus) valitsee automaattisesti sen perusteella, mitä vertainen mainosti.

  • Tilaus. Ota ilmoitukset tai indikaatiot käyttöön kirjoittamalla ominaisuuden CCCD:hen:

    await char.subscribe(notify=True)
    

    Tämän palauduttua keskusyksikkö voi odottaa saapuvia työntöjä.

  • Ilmoitettu / indikoitu. Odota seuraavaa työntöä palvelimelta:

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

    timeout_ms=None (oletus) odottaa ikuisesti; anna kokonaisluku millisekunteina luovuttaaksesi jonkin ajan kuluttua.

Neljän yhdistäminen antaa kanonisen ”yhdistä, tilaa, suoratoista” -keskusyksikköohjelman:

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

Koko juttu on noin kymmenkunta riviä ja kattaa kulun ”ei Bluetoothia käynnissä” -tilasta ”live-data suoratoistaa” -tilaan. Skannausiteraattori vastaa lähettäjä/tarkkailija-kuviota, connect avaa GAP-yhteyden, service / characteristic käy läpi GATT-puun, subscribe kirjoittaa CCCD:n ja notified odottaa työntöjä.

11.10.5. Yhteyden katkeamiset ja uudelleenyhdistäminen

Kaikki, mitä radioyhteydelle tapahtuu, tulee esiin siinä korutiinissa, joka oli odottamassa sitä. aioble.DeviceDisconnectedError on signaali siitä, että vertainen poistui tai valvonta-aikakatkaisu laukesi; poikkeus lopettaa sen read()-, write()- tai notified()-kutsun, joka oli kesken, ja mikä tahansa async with connection -lohko poistuu siististi.

Keskusyksikkö, jonka pitäisi yhdistää uudelleen katkeamisen yhteydessä, käärii työn omaan ulompaan silmukkaansa:

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. Sekvenssin rajaaminen timeout()-funktiolla

Kun useiden peräkkäisten GATT-operaatioiden tulisi kaikkien valmistua yhden budjetin sisällä – ei kunkin erikseen omalla timeout_ms-ajallaan – käytä aioble.DeviceConnection.timeout()-metodia niiden käärimiseen. Palautettu kontekstinhallintaobjekti peruuttaa runkonsa, jos budjetti kuluu loppuun (nostaen asyncio.TimeoutError-poikkeuksen) tai jos vertainen katkaisee yhteyden (nostaen aioble.DeviceDisconnectedError-poikkeuksen):

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

Tämä on siistimpi vaihtoehto kunkin kutsun käärimiselle erikseen asyncio.wait_for()-funktioon ja välttää viräuttavat onnistumiset, joissa jokainen kutsu täyttää oman aikarajansa mutta sekvenssi kokonaisuutena ylittää sen. timeout_ms=None -arvon antaminen timeout()-metodille poistaa aikarajan käytöstä ja jättää vain yhteyden katkeamissuojan aktiiviseksi.