11.14. Podsumowanie

Przeszliśmy przez Bluetooth Low Energy od warstwy radiowej aż po API Pythona używane do jego obsługi:

  • Motywacja – BLE to odpowiedź, gdy kamera chce porozmawiać z czymś znajdującym się w pobliżu bez żadnej infrastruktury pomiędzy nimi. Telefon w tym samym pomieszczeniu, urządzenie noszone na nadgarstku, beacon na ścianie. Krótki zasięg, brak sieci do dołączenia, niemal zerowy pobór mocy.

  • Radio – 2,4 GHz, 40 kanałów: trzy do rozgłaszania, 37 dla danych połączenia, przeskakiwanych według pseudolosowej sekwencji z adaptacyjnym unikaniem zaszumionych kanałów. Krótkie pakiety, radia przeważnie uśpione.

  • Warstwa łącza – ramkowanie pakietów, adresowanie, harmonogramowanie połączeń, retransmisja i szyfrowanie warstwy łącza. Nic z tego nie jest konfigurowane z poziomu Pythona; wszystko to przejawia się w parametrach połączenia i w MTU.

  • Generic Access Profile (GAP) – wykrywanie i zarządzanie połączeniami. Cztery role: peripheral i broadcaster (rozgłaszają), central i observer (skanują). Ładunki rozgłoszeniowe niosą lokalną nazwę, UUID-y usług, appearance oraz dane specyficzne dla producenta – 31 bajtów plus opcjonalna 31-bajtowa odpowiedź na skan. Interwał połączenia, opóźnienie urządzenia peryferyjnego oraz limit czasu nadzoru określają, jak zachowuje się otwarte połączenie.

  • Generic Attribute Profile (GATT) – drzewo usług, z których każda przechowuje charakterystyki, a każda z nich opcjonalnie deskryptory, identyfikowane przez UUID-y (16-bitowe dla standardów Bluetooth-SIG, 128-bitowe dla niestandardowych). Pięć operacji: read i write (pobieranie, inicjowane przez klienta), notify i indicate (wypychanie, inicjowane przez serwer, subskrybowane przez Client Characteristic Configuration Descriptor). Rozmiar ładunku jest ograniczony przez wynegocjowane MTU.

  • API Pythonaaioble zamienia każdy wzorzec BLE w korutynę asyncio. Urządzenie peryferyjne to aioble.advertise() w pętli po połączeniach, z obiektami Service / Characteristic budowanymi raz i zatwierdzanymi przez aioble.register_services(). Urządzenie centralne to aioble.scan(), by znaleźć peera, connect(), by otworzyć łącze, service() i characteristic(), by przejść po zdalnym drzewie GATT, a następnie read() / write() / subscribe() / notified() do faktycznych danych. Rozłączenia ujawniają się jako aioble.DeviceDisconnectedError wewnątrz korutyny, która oczekiwała.

  • Kanały L2CAP – furtka awaryjna dla masowych strumieni bajtów, które nie pasują do modelu klucz/wartość GATT. aioble.DeviceConnection.l2cap_accept() / l2cap_connect() otwierają kanał per aplikacja na bazie połączenia GAP, z wysyłaniem / odbieraniem sterowanym przepływem kredytowym i większym MTU, niż GATT jest w stanie przenieść.

  • Parowanie i szyfrowanie – łącza BLE są domyślnie publiczne. aioble.DeviceConnection.pair() inicjuje wymianę kluczy, która tworzy zaszyfrowane łącze; bond=True (wartość domyślna) utrwala klucze, dzięki czemu kolejne połączenia pomijają uzgadnianie. Bez mitm=True i użytecznej zdolności IO szyfrowanie chroni przed pasywnymi podsłuchującymi, ale nie przed aktywnym przekierowaniem podczas pierwotnego parowania.

To wystarczy, aby pisać aplikacje kamery, które publikują status jako urządzenie peryferyjne, odczytują dane z sensorów jako urządzenie centralne, wypychają wartości na żywo do telefonu przez BLE, zabezpieczają łącze krokiem parowania i wiązania oraz – w rzadkim przypadku transferu masowego – schodzą z GATT do kanału L2CAP.

11.14.1. Rozwiązywanie problemów

Awarie BLE to przeważnie niezgodności między tym, czego oczekują obie strony, a inspektor po stronie telefonu to najszybszy sposób, by zobaczyć, czyje oczekiwania są błędne. Standardowym narzędziem jest nRF Connect for Mobile (Nordic Semiconductor, darmowe na Androida i iOS): skanuje, łączy się, przechodzi po bazie danych GATT, odczytuje i zapisuje charakterystyki oraz subskrybuje powiadomienia – dzięki czemu zachowanie po stronie kamery można testować w izolacji, bez pisania jakiejkolwiek aplikacji towarzyszącej.

Typowe tryby awarii:

  • „Moje urządzenie pojawia się w skanerze, ale nie chce się połączyć.” Najczęściej pakiet rozgłoszeniowy ma connectable=False (tryb broadcaster) albo poprzednie połączenie jest nadal otwarte, a kamera jest już za aioble.advertise(). Dodaj instrukcje print wokół wywołania advertise, aby to potwierdzić.

  • „exchange_mtu(512) wykonało się, ale moje powiadomienia nadal są ograniczone do 20 bajtów.” Wynegocjowane MTU to min(local, peer) – telefon lub biblioteka centralna mogły nie zażądać większego MTU po swojej stronie, w którym to przypadku połączenie pozostaje na 23. Sprawdź mtu po zwróceniu przez exchange_mtu(). Zwróć też uwagę, że exchange_mtu() działa tylko raz na połączenie; wywołaj je przed pierwszą dużą operacją.

  • „Parowanie kończy się ogólnym błędem.” Dwaj zwykli winowajcy: niezgodność zdolności IO (żądanie mitm=True na kamerze deklarującej io=3 / brak wejścia, brak wyjścia – nie ma sposobu na potwierdzenie kodu numerycznego, więc silnik parowania rezygnuje) oraz drastycznie błędny czas zegara ściennego na kamerze, gdy peer go wymaga. Ustaw zegar za pomocą ntptime.settime() przed pierwszą próbą parowania.

  • „Powiadomienia nigdy nie docierają do klienta.” Dwie rzeczy do sprawdzenia, po kolei: (a) czy charakterystyka została zadeklarowana z notify=True? – bit właściwości musi być ustawiony po stronie serwera; (b) czy klient wywołał subscribe()? – bez zapisania Client Characteristic Configuration Descriptor (CCCD) serwerowi przekazywana jest informacja, że żaden klient nie chce powiadomień, i po cichu je odrzuca.

  • „Rozgłaszana nazwa jest obcięta lub jej brak.” Ładunek rozgłoszeniowy ma 31 bajtów, a pola flags + service-UUID + appearance zabierają po kilka bajtów z góry. Długie name= plus kilka UUID-ów usług powoduje przepełnienie. Albo skróć nazwę, albo użyj aktywnego skanowania, aby odpowiedź na skan (kolejne 31 bajtów) przeniosła nadmiar. nRF Connect pokazuje obie połówki osobno, co sprawia, że podział jest oczywisty.

  • „Połączenie L2CAP natychmiast zgłasza wyjątek.” Zwykle niezgodność PSM – obie strony muszą uzgodnić ten sam numer PSM poza pasmem. L2CAPConnectionError niesie kod statusu Bluetooth jako swój pierwszy argument; status 2 („PSM not supported”) to wyraźna wskazówka.

  • „Związane połączenia nadal wyzwalają pełne uzgadnianie parowania przy każdym ponownym połączeniu.” aioble.security.load_secrets() nie zostało wywołane przy starcie. Bez niego zapisane klucze są w pamięci flash, ale nigdy nie są wczytywane do pamięci, więc tożsamość peera jest nieznana i parowanie za każdym razem przebiega od zera.

Gdy wszystko inne zawiedzie, niskopoziomowy moduł bluetooth udostępnia wywołanie zwrotne IRQ, które uruchamia się dla każdego zdarzenia bazowego; krótkie zasubskrybowanie go i wypisywanie zdarzeń to odpowiednik śledzenia Wireshark po stronie kamery.

11.14.2. Korzystanie z tej dokumentacji referencyjnej w przyszłości

Traktuj rozdziały o Bluetooth jako materiał referencyjny; powracanie po dokładny układ ładunku rozgłoszeniowego urządzenia peryferyjnego lub po przebieg skanowania i subskrybowania po stronie urządzenia centralnego jest zamierzonym sposobem użycia. Strony referencyjne aioble — Asynchroniczne BLE i bluetooth — niskopoziomowa obsługa Bluetooth wymieniają każdą metodę, flagę i stałą w jednym miejscu, gdy pytanie brzmi po prostu „jaka jest dokładna nazwa tego wywołania”.