11.9. Djelovanje kao periferija

Najčešći BLE obrazac na strani kamere jest djelovanje kao periferija – objaviti malu GATT bazu podataka, oglašavati svoje postojanje, prihvatiti vezu s telefona ili pratećeg uređaja te slati vrijednosti onome tko je na drugom kraju.

11.9.1. Izgradnja GATT baze podataka

Prvo što periferija radi pri pokretanju – još i prije nego što uključi radio – jest izgradnja baze podataka koju namjerava izložiti, stvaranje objekata za svaku uslugu i karakteristiku te zatim registracija cijelog skupa:

import aioble
import bluetooth

ENV_SERVICE = bluetooth.UUID(0x181A)              # Environmental Sensing
TEMP_UUID = bluetooth.UUID(0x2A6E)                # Temperature
HUMID_UUID = bluetooth.UUID(0x2A6F)               # Humidity

env = aioble.Service(ENV_SERVICE)
temp_char = aioble.Characteristic(
    env, TEMP_UUID,
    read=True, notify=True, initial=b"\\x00\\x00",
)
humid_char = aioble.Characteristic(
    env, HUMID_UUID,
    read=True, notify=True, initial=b"\\x00\\x00",
)

aioble.register_services(env)

Svaka aioble.Characteristic pripaja se svojoj uslugi jednostavno tako da se konstruira s uslugom kao prvim argumentom. Booleovi imenovani argumenti (read, write, write_no_response, notify, indicate) odabiru koje će GATT operacije klijentu biti dopušteno izvoditi; prosljeđivanje False (zadana vrijednost) znači da bit svojstva nije postavljen.

aioble.register_services() predaje sastavljeno stablo GATT poslužitelju. Mora se pozvati jednom, prije nego što ijedan aioble.advertise() započne; ponovni poziv zamjenjuje prethodnu bazu podataka.

11.9.2. Oglašavanje

Kad je baza podataka uspostavljena, oglašavanje je jedan poziv korutine koji čeka na vezu:

async def serve_one():
    connection = await aioble.advertise(
        interval_us=250000,
        name="openmv-env",
        services=[ENV_SERVICE],
        appearance=0x0540,           # Generic Sensor
    )

Imenovani argumenti izravno se preslikavaju na polja korisnog sadržaja oglašavanja. name je polje lokalnog naziva; services je popis UUID-ova usluga koje uređaj nudi (skener na strani telefona može filtrirati po njima); appearance je naznaka iz standardnih 16-bitnih vrijednosti izgleda koja omogućuje središtu (central) prikaz razumne ikone. Podaci specifični za proizvođača prosljeđuju se putem manufacturer=(company_id, data_bytes).

Nekoliko manje uobičajenih ključnih riječi pokriva ostatak prostora zastavica oglašavanja:

  • connectable=False – način rada samo s emitiranjem (veza se nikad ne prihvaća). Pravi izbor za korisne sadržaje u stilu odašiljača (beacon).

  • limited_disc=True – koristi zastavicu ograničene vidljivosti umjesto opće vidljivosti; neki operacijski sustavi to dvoje različito tretiraju u svom sučelju za uparivanje.

  • adv_data / resp_data – sirovi bajtovi ako aplikaciji treba potpuna kontrola nad rasporedom.

  • timeout_ms – odustani nakon fiksnog vremena. Zadano je oglašavanje zauvijek.

Kada se središte (central) poveže, aioble.advertise() vraća rezultirajuću aioble.DeviceConnection. Periferija u ovom trenutku prestaje s oglašavanjem.

11.9.3. Posluživanje jednog klijenta

Glavna petlja periferije obično izgleda ovako:

async def serve():
    while True:
        connection = await aioble.advertise(
            interval_us=250000,
            name="openmv-env",
            services=[ENV_SERVICE],
        )
        print("connected:", connection.device.addr_hex())
        async with connection:
            await connection.disconnected()
        print("disconnected; advertising again")

asyncio.run(serve())

async with connection čini čišćenje pri prekidu veze automatskim. disconnected() je korutina koja se suspendira dok bilo koja strana ne prekine vezu – čist način da periferija nastavi posluživati dok središte (central) ne ode, a zatim se vrati na oglašavanje za sljedeći krug.

11.9.4. Ažuriranje karakteristike

Periferija ažurira lokalnu GATT bazu podataka pomoću aioble.Characteristic.write()

temp_char.write(b"\\x9a\\x09")              # 24.58 deg C as sint16, 0.01 units

To mijenja vrijednost koju bi sljedeći read od bilo kojeg klijenta vratio. Sam po sebi, on ne gura novu vrijednost – pretplaćeni klijent neće vidjeti ništa dok ili klijent ne ispita stanje ili periferija ne pošalje izričitu obavijest.

Strana koja gura podatke jedna je ključna riječ u istom pozivu:

temp_char.write(temp_bytes, send_update=True)

send_update=True obavještava (ili indicira) svakog klijenta koji se pretplatio na ovu karakteristiku. Većina koda u stilu senzora živi u zadatku po vezi koji u petlji čita senzor i upisuje vrijednost s send_update=True otprilike svake sekunde:

async def stream_temperature(connection):
    while connection.is_connected():
        temp_char.write(encode_temperature(read_sensor()), send_update=True)
        await asyncio.sleep(1)

async def serve():
    while True:
        connection = await aioble.advertise(
            interval_us=250000,
            name="openmv-env",
            services=[ENV_SERVICE],
        )
        async with connection:
            asyncio.create_task(stream_temperature(connection))
            await connection.disconnected()

Ako biste radije usmjerili obavijest prema jednom određenom klijentu umjesto prema cijelom pretplaćenom skupu (recimo odgovor privatan za vezu na naredbu tog klijenta), aioble.Characteristic.notify() i indicate() primaju argument DeviceConnection i neobavezni korisni sadržaj.

11.9.5. Primanje upisa

Drugi smjer – klijent koji upisuje u karakteristiku – postaje dostupan kada se karakteristika konstruira s write=True ili write_no_response=True. Periferija čeka sljedeći upis pomoću aioble.Characteristic.written()

cmd_char = aioble.Characteristic(env, CMD_UUID, write=True, capture=True)

async def handle_commands():
    while True:
        connection, data = await cmd_char.written()
        print("command from", connection.device.addr_hex(), "=", data)

Bez capture=True, written() vraća samo vezu koja upisuje; nova vrijednost živi u pozadinskom međuspremniku karakteristike, a aplikacija je dohvaća pomoću read(). Ako drugi upis stigne prije nego što aplikacija pročita prvi, druga vrijednost prepisuje prvu u međuspremniku i izvorna se vrijednost gubi – written() i dalje budi aplikaciju, ali samo jednom po „postoji nešto novo”, a ne jednom po upisu.

Ključna riječ capture=True to ispravlja. Svaki dolazni upis dodaje se u red na razini modula, a written() vraća n-torku (connection, data) za svaki pojedinačni upis – aplikacijska petlja vidi svaki točno jednom, redoslijedom dolaska. Dvije praktične posljedice:

  • Red je ograničen i dijeli se među svim karakteristikama na uređaju s omogućenim hvatanjem (capture). Kratki naleti uzastopnih upisa se toleriraju; trajno prepunjavanje (upisi stižu brže nego što ih aplikacija prazni) tiho odbacuje najstarije stavke u redu, a navalni promet na jednoj karakteristici može izbaciti stavke na čekanju iz druge.

  • Odaberite capture=True za upise u stilu naredbi gdje je svaka vrijednost važna. Ostavite ga isključenim za karakteristike u stilu stanja gdje je samo posljednja vrijednost od interesa.

Ako bi čitanje od klijenta trebalo biti odgovoreno kodom koji se izvodi na zahtjev umjesto statičkom vrijednošću, nadjačajte on_read(). Metoda se poziva sinkrono kada stigne čitanje; vratite 0 da biste dopustili čitanje (poslat će se trenutna vrijednost iz write()) ili ATT kôd pogreške različit od nule da biste ga odbili:

import time

_ATT_ERR_READ_NOT_PERMITTED = const(0x02)
_MIN_READ_INTERVAL_MS = const(1000)            # at most once per second

class TempChar(aioble.Characteristic):
    _last_read_ms = 0

    def on_read(self, connection):
        now = time.ticks_ms()
        if time.ticks_diff(now, self._last_read_ms) < _MIN_READ_INTERVAL_MS:
            return _ATT_ERR_READ_NOT_PERMITTED
        self._last_read_ms = now
        self.write(encode_temperature(read_sensor()))
        return 0

temp_char = TempChar(env, TEMP_UUID, read=True)

Povratni poziv uzorkuje senzor i ažurira vrijednost karakteristike neposredno prije nego što GATT stog posluži čitanje, tako da klijent uvijek vidi svježe podatke. Ograničenje brzine sprječava klijenta da napada senzor brže nego što se može uzorkovati – svako čitanje unutar jednosekundnog razdoblja hlađenja vraća se kao ATT pogreška Read Not Permitted umjesto zastarjele vrijednosti.

11.9.5.1. Veći pozadinski međuspremnici – BufferedCharacteristic

Pozadinski međuspremnik za uobičajenu Characteristic širok je 20 bajtova – praktična granica pri zadanom MTU-u od 23 bajta. Klijent koji upiše više od toga u uobičajenu karakteristiku dobiva svoju vrijednost skraćenu. Za veće dolazne vrijednosti ili za stavljanje u red uzastopnih upisa koje će aplikacijska petlja kasnije sustići, deklarirajte karakteristiku kao BufferedCharacteristic i unaprijed odaberite veličinu međuspremnika:

blob = aioble.BufferedCharacteristic(
    service, BLOB_UUID,
    max_len=512, append=True,
    write=True, capture=True,
)

async def receive_blob():
    while True:
        connection, chunk = await blob.written()
        handle_chunk(connection, chunk)

Dvije postavke razlikuju je od obične Characteristic:

  • max_len je veličina pozadinskog međuspremnika u bajtovima. Odaberite je tako da odgovara najvećem pojedinačnom upisu koji se od klijenta očekuje (nakon pregovaranja o MTU-u).

  • append=True čini da uzastopni upisi dodaju u međuspremnik umjesto da prepisuju – korisno za primanje vrijednosti koja stiže kroz nekoliko upisa (dijelovi ažuriranja firmwarea, retci dnevnika). Uz append=False međuspremnik se ponaša kao normalna karakteristika, samo širi.

Sve ostale zastavice konstruktora (read, write, notify, indicate, capture, initial) prosljeđuju se nepromijenjene osnovnoj karakteristici.

11.9.6. Standardne usluge i UUID-ovi koje dodjeljuje SIG

Pridržavanje UUID-ova iz dodijeljenih brojeva (0x180F za Battery Service, 0x181A za Environmental Sensing, 0x180D za Heart Rate i tako dalje) znači da generički Bluetooth izbornik telefona ili bilo koja aplikacija skenera treće strane može prepoznati svrhu uređaja bez ikakvog prilagođenog koda klijenta. Raspored bajtova unutar svake standardne karakteristike također je fiksiran specifikacijom – Battery Level (0x2A19) je jedan bajt 0..100; Temperature (0x2A6E) je little-endian sint16 u jedinicama od 0,01 °C. Za aplikacije koje se ne uklapaju u standardnu uslugu, generirajte 128-bitni UUID jednom i koristite ga u svim uslugama i karakteristikama uređaja.

Periferija koja objavljuje samo prilagođene UUID-ove i dalje je u redu – samo joj treba prilagođena klijentska aplikacija koja zna za te UUID-ove.

Napomena

BLE vrijednosti su little-endian posvuda – GATT specifikacija, svaka standardna karakteristika, svako polje oglašavanja. Višebajtni cijeli brojevi idu na vezu prvo s najnižim bajtom. Prefiks < u nizovima formata modula struct upravo je ono što želite za kodiranje/dekodiranje ("<h", "<H", "<I", …); korištenje zadanog izvornog redoslijeda bajtova na little-endian MCU-u zasad slučajno radi, ali izričito pisanje < je sigurna navika.

11.9.7. Radio koji stoji iza svega

Radio je uključen onog trenutka kada ga prva aioble korutina dotakne. Dok središte (central) nije povezano, periferija provodi vrijeme prebacujući se između kratkih naleta oglašavanja i spavanja; nakon veze slijedi dogovoreni interval veze. Periferija plaća mali energetski trošak po oglasu, pa je izbor interval_us u aioble.advertise() najizravnija postavka koju periferija ima za zamjenu kašnjenja otkrivanja za trajanje baterije.