11.4. Rozgłaszanie i skanowanie

Dwa urządzenia BLE, które nigdy wcześniej się nie spotkały, muszą się najpierw odnaleźć. Sieć rozwiązuje to, przydzielając każdemu urządzeniu adres ze wspólnej puli i pozwalając każdej ze stron dotrzeć do drugiej przez routery. BLE nie ma routerów, nie ma wspólnej puli i – między większością par urządzeń – nie ma w ogóle żadnej wcześniejszej relacji. Generic Access Profile (GAP) rozwiązuje wykrywanie wzorcem rozgłaszania i nasłuchiwania. Jedna strona rozgłasza – transmituje krótki pakiet na trzech kanałach rozgłoszeniowych w regularnych odstępach, opisując, kim jest. Druga strona skanuje – przeczesuje te same trzy kanały, nasłuchując tych pakietów.

GAP definiuje wokół tego wzorca cztery role, każda będąca określoną kombinacją rozgłaszania i nasłuchiwania.

11.4.1. Cztery role GAP

Macierz dwa na dwa. Wiersze są oznaczone "rozgłasza" i "nie rozgłasza". Kolumny są oznaczone "akceptuje połączenia" i "nie akceptuje połączeń". Cztery komórki zawierają nazwy ról: Peripheral, Broadcaster, Central, Observer.

Cztery role GAP. Oś pionowa to to, czy urządzenie rozgłasza; oś pozioma to to, czy akceptuje (lub inicjuje) połączenia.

  • Peripheral rozgłasza pakiety mówiące „jestem tutaj i możesz się ze mną połączyć”. Gdy inne urządzenie otwiera połączenie, peripheral przestaje rozgłaszać i zaczyna obsługiwać żądania GATT. Pasy do pomiaru tętna, termometry i większość kamer pełniących rolę czujników działają jako peripheral.

  • Central skanuje w poszukiwaniu peripheral, wybiera jeden i inicjuje połączenie. Po połączeniu komunikuje się przez GATT jako klient. Telefony, laptopy i kamery działające jako kolektory danych są central.

  • Broadcaster rozgłasza, ale nigdy nie akceptuje połączeń. Jego ładunek rozgłoszeniowy jest danymi – nie ma z czym się łączyć. iBeacons i większość beaconów wykrywających obecność w sklepach to broadcaster.

  • Observer skanuje w poszukiwaniu tych rozgłoszeń i odczytuje ładunek, również nigdy się nie łącząc. Kamera, która nasłuchuje pobliskich beaconów i reaguje na to, co usłyszy, jest observer.

Pojedyncze urządzenie może pełnić więcej niż jedną rolę jednocześnie – kamera może być peripheral publikującym własny stan oraz central łączącym się z pobliskim czujnikiem. Radio multipleksuje pracę.

11.4.2. Co zawiera pakiet rozgłoszeniowy

Pakiet rozgłoszeniowy jest mały: 31 bajtów ładunku lub 62, jeśli rozgłaszający publikuje również scan response, o który skanery mogą poprosić na żądanie. Ładunek to lista krótkich, typowanych pól:

  • Flags. Możliwość połączenia lub jej brak, ogólna / ograniczona wykrywalność.

  • Local name. Krótki, przyjazny dla człowieka ciąg znaków – nazwa, którą system operacyjny telefonu lub laptopa wyświetla w swoim menu Bluetooth.

  • Service UUIDs. Lista identyfikatorów usług GATT, które urządzenie hostuje, dzięki czemu skaner może rozpoznać odpowiednie peripheral bez wcześniejszego łączenia się. Pas do pomiaru tętna rozgłasza 0x180D – standardowy UUID usługi Heart-Rate – a aplikacja do pomiaru tętna na telefonie wie już z samego tego, że warto się z urządzeniem połączyć.

  • Appearance. 16-bitowa wartość z listy przypisanych numerów Bluetooth (czujnik, ogólne media, ogólny zegarek, …) – wskazówka dla central, co wyświetlić.

  • Manufacturer-specific data. Dowolne bajty poprzedzone identyfikatorem firmy. iBeacons używają tego pola do przenoszenia swojego UUID, major i minor; aplikacje niestandardowe mogą umieścić tu, co tylko zechcą.

Ładunki rozgłoszeniowe są ciasne. Limit 31 bajtów sprawia, że wybór, co uwzględnić, to realna decyzja projektowa – długa, czytelna dla człowieka nazwa może szybko nie zostawić miejsca na UUID usług. API aioble.advertise() przyjmuje każde z tych pól jako argument nazwany i składa bajty za Ciebie, automatycznie przelewając nadmiar do scan response, jeśli główny pakiet się zapełni.

11.4.3. Skanowanie aktywne i pasywne

Skaner może działać pasywnie, gdy nasłuchuje pakietów rozgłoszeniowych i parsuje to, co napłynie, lub aktywnie, gdy dodatkowo wysyła scan request do każdego rozgłaszającego i parsuje zwrócony scan response.

Skanowanie pasywne widzi tylko początkowy pakiet rozgłoszeniowy (do 31 bajtów). Skanowanie aktywne podwaja tę wartość – scan response to kolejne 31 bajtów, które peripheral może wykorzystać na pola, które się nie zmieściły. Skanowanie aktywne kosztuje też energię po obu stronach, ponieważ skaner transmituje, a rozgłaszający transmituje dodatkowy pakiet, więc jest to wybór, a nie ustawienie domyślne.

W API aioble ustawienie active=True w aioble.scan() przełącza tryb, a każdy ScanResult udostępnia połączone adv_data plus resp_data, a także funkcje pomocnicze takie jak result.name() i result.services(), które ukrywają parsowanie na poziomie bajtów.

Informacja

Atrybuty adv_data i resp_data to surowe ładunki rozgłoszeniowe i scan response (bytes). Funkcje pomocnicze – name(), services(), manufacturer() – obejmują typowe standardowe pola i są właściwym wyborem w 99% przypadków. Po surowe bajty sięgaj tylko wtedy, gdy potrzebujesz pola producenta, którego funkcje pomocnicze nie parsują (adresy URL Eddystone, UUID/major/minor iBeacon, niestandardowe typy rozgłoszeń). Układ bajtów jest standardowy TLV: każde pole to length, type, value....

11.4.4. Interwał rozgłaszania

To, jak często peripheral rozgłasza, jest kompromisem między mocą a opóźnieniem wykrywania. Rozgłoszenia wysyłane co 20 ms są wychwytywane przez skaner niemal natychmiast, ale utrzymują radio zajęte i rozładowują baterię; rozgłoszenia co sekundę zużywają niemal zero energii, ale sprawiają, że skaner wolniej zauważa urządzenie.

interval_us w aioble.advertise() ustawia interwał w mikrosekundach:

  • 20 000 do 100 000 us (20 ms - 100 ms) – szybkie parowanie, aplikacja oczekuje szybkiej odpowiedzi, urządzenie podłączone do zasilania.

  • 250 000 do 1 000 000 us (250 ms - 1 s) – rozsądna wartość domyślna dla peripheral zasilanego bateryjnie, który chce być wykrywalny bez nadmiernego zużywania energii.

  • Powyżej 1 000 000 us – powolne rozgłaszanie w tle, beacony wysyłające aktualizację pozycji co kilka sekund.

Strona skanera ma własne pokrętła – aioble.scan() przyjmuje interval_us i window_us (jak często skaner budzi swoje radio i jak długo nasłuchuje za każdym razem). Wartości domyślne są w porządku; jedyna typowa zmiana to ustawienie obu równych dla ciągłego skanowania, gdy bateria nie jest problemem.

11.4.5. Wzorce bezpołączeniowe – broadcaster i observer

Strony Działanie jako urządzenie peryferyjne i Działanie jako central omawiają połączeniową postać API – gdzie peripheral akceptuje połączenie, a obie strony wymieniają dane przez GATT. Druga postać jest bezpołączeniowa: broadcaster transmituje ładunek jako rozgłoszenie, a dowolny observer w zasięgu może go odczytać, nigdy się nie łącząc. Beacony, czujniki obecności i jednokierunkowa telemetria należą właśnie tutaj.

Broadcaster to aioble.advertise() z connectable=False. Dane specyficzne dla producenta przenoszą ładunek:

import aioble
import asyncio
import struct

_COMPANY_ID = const(0xFFFF)                # 0xFFFF is "no specific vendor"

async def beacon():
    seq = 0
    while True:
        seq = (seq + 1) & 0xFFFF
        payload = struct.pack("<H", seq)
        await aioble.advertise(
            interval_us=500000,
            connectable=False,
            name="openmv-beacon",
            manufacturer=(_COMPANY_ID, payload),
            timeout_ms=1000,                # one cycle, then loop
        )

asyncio.run(beacon())

Słowo kluczowe timeout_ms kończy wywołanie advertise po sekundzie; pętla zewnętrzna ponawia je z kolejnym numerem sekwencji, dzięki czemu nasłuchujący widzą świeże dane. Flaga connectable=False jest tym, co nadaje rozgłoszeniu charakter broadcaster – kamera nie odpowie na żądanie połączenia, nawet jeśli takie nadejdzie.

Observer to odpowiadający mu skaner tylko do odczytu. Uruchamia aioble.scan() w nieskończoność, parsuje napływające rozgłoszenia i nigdy nie wywołuje connect()

import aioble
import asyncio

_COMPANY_ID = const(0xFFFF)

async def watch():
    async with aioble.scan(duration_ms=0, active=False) as scanner:
        async for result in scanner:
            for company, data in result.manufacturer(filter=_COMPANY_ID):
                print(result.device.addr_hex(),
                      "rssi", result.rssi, "data", data)

asyncio.run(watch())

duration_ms=0 skanuje do momentu wyjścia z menedżera kontekstu; active=False utrzymuje własne radio observer w ciszy (brak żądań scan response) dla najniższego poboru mocy. Argument filter= w manufacturer() odrzuca każde rozgłoszenie, które nie pasuje do identyfikatora firmy, więc pętla uruchamia się tylko dla ruchu broadcaster.

11.4.6. Od wykrycia do połączenia

Gdy central wybierze peripheral, z którym chce się komunikować, przestaje nasłuchiwać, wysyła connect request na kanale rozgłoszeniowym, którego peripheral ostatnio używał, i obie strony przechodzą do przeskakujących kanałów danych warstwy łącza. Peripheral zazwyczaj przestaje w tym momencie rozgłaszać. Co dzieje się dalej – parametry połączenia, wykrywanie GATT, czas życia łącza – znajdziesz na stronie Połączenia.