11.7. Operacje GATT

Charakterystyka po prostu znajduje się w bazie danych GATT jako nazwana wartość. Użyteczną czyni ją niewielki, dobrze zdefiniowany zestaw operacji, jakie może na niej wykonać klient. Każda charakterystyka deklaruje, które operacje obsługuje, w postaci maski bitowej właściwości (property bitmask) – serwer, który nie ma nic do udostępnienia, może opublikować wartość tylko do odczytu, rejestr sterujący może być tylko do zapisu, a sensor strumieniujący aktualizacje ustawi bit powiadomień. Klient odkrywa tę maskę bitową podczas wykrywania i jej przestrzega.

Pięć operacji to odczyt (read), zapis (write), zapis bez odpowiedzi (write without response), powiadomienie (notify) i wskazanie (indicate). Dzielą się na dwie grupy – pobieranie (klient pyta) i wypychanie (serwer wysyła).

11.7.1. Pobieranie: odczyt i zapis

Te dwie operacje są najprostsze i wyglądają dokładnie jak wywołania funkcji.

  • Odczyt. Klient prosi o bieżącą wartość, serwer ją odsyła. Jedna pełna wymiana, klient otrzymuje bajty, jakie serwer ustawił dla tej charakterystyki, a serwer nie dowiaduje się niczego o tym, kto dokonał odczytu.

  • Zapis. Klient wysyła nowe bajty, serwer je przechowuje (i opcjonalnie uruchamia logikę aplikacji na nowej wartości). Istnieją dwie odmiany:

    • Zapis z odpowiedzią – serwer potwierdza, zgłaszając ewentualny błąd aplikacji przy niezerowym statusie. Niezawodny, jedna pełna wymiana.

    • Zapis bez odpowiedzi – serwer po cichu przechowuje bajty; klient nie otrzymuje żadnego potwierdzenia. Szybszy (brak pełnej wymiany oczekującej na potwierdzenie) i przydatny przy strumieniowaniu, kosztem tego, że o błędach można się dowiedzieć tylko poprzez odczyt zwrotny innym kanałem.

W aioble API po stronie klienta ukrywa ten wybór za jedną metodą aioble.ClientCharacteristic.write() ze słowem kluczowym response (True / False / None w celu automatycznego wyboru na podstawie tego, co rozgłasza druga strona).

11.7.2. Wypychanie: powiadomienie i wskazanie

Model pobierania jest niewłaściwy dla danych z sensorów. Pasek do pomiaru tętna, który telefon musiałby odpytywać co sekundę, marnowałby baterię na sto niepotrzebnych zdarzeń radiowych; pasek, który wypycha wartość tylko wtedy, gdy ma nowy odczyt, jest tym, o co w BLE chodzi w pierwszej kolejności.

GATT rozwiązuje to za pomocą operacji inicjowanych przez serwer. Klient subskrybuje charakterystykę; od tego momentu za każdym razem, gdy serwer aktualizuje wartość, nowa wartość jest wypychana przez łącze do klienta. Dwie odmiany:

  • Powiadomienie. Wyślij i zapomnij. Serwer kolejkuje powiadomienie, warstwa łącza transmituje je podczas kolejnego zdarzenia połączenia, a klient je odbiera. Na poziomie GATT nie ma potwierdzenia; normalna retransmisja warstwy łącza obsługuje utratę pakietów po stronie radia, ale aplikacja nie widzi żadnego potwierdzenia, że wartość została przetworzona.

  • Wskazanie. Serwer wysyła powiadomienie oraz czeka na potwierdzenie klienta na poziomie GATT, zanim wyśle następne. Jedno wskazanie naraz. Stosowane, gdy serwer musi wiedzieć, że klient faktycznie zobaczył wartość – charakterystyka alarmu krytycznego, potwierdzenie konfiguracji.

Dwa umieszczone obok siebie diagramy serwera i klienta. Po lewej klient wysyła "read", a serwer odpowiada wartością. Trzy odczyty pod rząd, każdy jako para strzałek. Po prawej klient wysyła pojedyncze "subscribe", po czym serwer wypycha trzy pakiety "notify" w wybranych przez siebie momentach, bez żadnego żądania klienta pomiędzy nimi.

Pobieranie (read) kontra wypychanie (notify). Przy powiadomieniach klient subskrybuje raz, a serwer wypycha nowe wartości za każdym razem, gdy ulegają zmianie.

Subskrybowanie odbywa się przez zapis do deskryptora dołączonego do charakterystyki – Client Characteristic Configuration Descriptor (CCCD, 0x2902). Zapisanie 0x0001 włącza powiadomienia, 0x0002 włącza wskazania, a 0x0000 wyłącza oba. Metoda aioble.ClientCharacteristic.subscribe() wykonuje ten zapis za Ciebie, korzystając z flag słów kluczowych notify=True i indicate=True.

Po subskrypcji klient oczekuje na przychodzące wypchnięcia za pomocą notified() oraz indicated() – obie to korutyny asynchroniczne, które wstrzymują się do nadejścia kolejnego wypchnięcia.

11.7.3. MTU rządzi rozmiarem ładunku

Każda operacja jest ograniczona przez wynegocjowane MTU, na którym połączenie ustaliło się w momencie nawiązania łącza. Domyślne MTU to 23 bajty, co po nagłówku GATT pozostawia 20 bajtów na bajty wartości charakterystyki. Cokolwiek większego musi albo zmieścić się w większym MTU (wynegocjowanym w górę przez aioble.DeviceConnection.exchange_mtu(), do 512 bajtów na kamerze), albo zostać podzielone na wiele charakterystyk lub wiele powiadomień.

Inicjowane przez klienta odczyty i zapisy wartości większych niż MTU są obsługiwane w tle przez długie procedury GATT (Read Long / Prepare-Write + Execute-Write); aioble uruchamia je w sposób przezroczysty, więc wywołanie read() / write() z nadmiarową wartością kosztuje jedynie więcej pełnych wymian. Inicjowane przez serwer powiadomienia i wskazania nie są fragmentowane – jedno wypchnięcie jest ograniczone przez MTU, a aplikacja sama dzieli cokolwiek większego na wiele powiadomień albo całkowicie rezygnuje z GATT.

W przypadku naprawdę dużych transferów – przechwyconej ramki, partii pomiarów, blobu oprogramowania układowego – właściwym rozwiązaniem jest zwykle całkowite zejście z GATT i użycie zamiast tego kanału L2CAP (zobacz Kanały L2CAP).

11.7.4. Obie strony w skrócie

Pięć operacji udostępnia się inaczej po każdej ze stron połączenia: