11.11. Kanały L2CAP

GATT jest modelem klucz/wartość. Operacje, które oferuje (read, write, notify, indicate), przenoszą jedną krótką wartość naraz, a największy pojedynczy ładunek, jaki mogą przenieść, to tyle, na ile pozwala wynegocjowany MTU – w najlepszym razie kilkaset bajtów. Sprawdza się to dobrze w przypadku odczytów sensorów, rejestrów poleceń i flag stanu. Zawodzi przy kilobajtach lub megabajtach: podział długiej plamy (blob) na setki małych zapisów kosztuje cykle wymiany, w których radio jest znacznie szybsze.

Dla przepływów danych masowych – przechwyconej ramki, którą kamera strumieniuje do telefonu, obrazu aktualizacji bezprzewodowej, wsadowego eksportu pomiarów – BLE oferuje alternatywną ścieżkę: Logical Link Control and Adaptation Protocol, czyli L2CAP. L2CAP znajduje się pomiędzy warstwą łącza a GATT i pozwala aplikacji zająć własny kanał zorientowany na połączenie na tym samym łączu radiowym. Kanał jest ścieżką bajtów z kontrolą przepływu opartą na kredytach, ze znacznie większym MTU na pakiet i bez ramkowania GATT pośrodku.

11.11.1. Kiedy używać L2CAP

Kanały L2CAP są właściwym narzędziem, gdy:

  • Transfer obejmuje więcej niż kilkaset bajtów.

  • Obie strony wiedzą, że zostanie użyty kanał L2CAP (nie jest on ujawniany w ładunku rozgłaszania; klient musi znać numer protocol/service multiplexer kanału, czyli PSM, pozyskany poza pasmem).

  • Aplikacja jest gotowa zrezygnować z udogodnień GATT: brak wbudowanej adresowalności po UUID, brak wykrywalności po stronie klienta przez standardowe aplikacje, brak powiadomień.

Najczęstszym przypadkiem w aplikacjach opartych na aioble jest przenoszenie binarnej plamy (blob) między dwoma fragmentami oprogramowania, które oba znają konwencję PSM – niestandardowy protokół kamera-telefon, para kamer openmv rozmawiających ze sobą, wewnętrzna ścieżka aktualizacji oprogramowania układowego pod usługą GATT urządzenia peryferyjnego.

We wszystkich pozostałych przypadkach pozostań przy GATT. Krótki status, rejestr sterujący, odczyt sensora – wszystko to należy do charakterystyki.

11.11.2. Ustanawianie kanału

L2CAP działa na wierzchu istniejącego aioble.DeviceConnection, więc przepływ wykrywania / rozgłaszania / łączenia po stronie GAP jest dokładnie taki sam jak dla GATT. Gdy obie strony utrzymują połączenie, jedna strona nasłuchuje na PSM, a druga się z nią łączy.

PSM to po prostu mała liczba całkowita. Bluetooth SIG rezerwuje dolną część zakresu do standardowego użytku (0x0001-0x007F); dla kanałów specyficznych dla aplikacji użyj liczby z zakresu dynamicznego (0x0080-0x00FF dla stałych PSM, 0x0040 i wzwyż zwykle wolne do niestandardowego użytku). Obie strony muszą wcześniej uzgodnić wartość.

MTU na kanale L2CAP to największa pojedyncza jednostka SDU (Service Data Unit), jaką którakolwiek ze stron dostarczy w jednym send() – a nie MTU łącza BLE. Aioble automatycznie fragmentuje większe ładunki. Host BLE kamery ogranicza MTU L2CAP do 1017 bajtów; 512 to rozsądna wartość domyślna, która pozostawia zapas po obu stronach bez nadmiernego zużycia pamięci RAM.

Po stronie nasłuchującej (np. kamera jako urządzenie peryferyjne):

async def serve_l2cap(connection, image_bytes):
    channel = await connection.l2cap_accept(psm=0x80, mtu=512)
    async with channel:
        # image_bytes is a bytearray -- e.g. csi0.snapshot().bytearray()
        # or a compressed JPEG buffer. send() fragments into MTU-sized
        # chunks automatically and awaits flow-control credits between.
        await channel.send(image_bytes)
        await channel.flush()

Po stronie łączącej się (np. telefon lub urządzenie centralne):

async def open_l2cap(connection, total_bytes):
    channel = await connection.l2cap_connect(psm=0x80, mtu=512)
    async with channel:
        image_bytes = bytearray(total_bytes)
        view = memoryview(image_bytes)
        received = 0
        while received < total_bytes:
            n = await channel.recvinto(view[received:])
            if n == 0:
                break
            received += n
        return image_bytes

l2cap_accept() blokuje, dopóki nie połączy się partner (albo nie upłynie timeout_ms); l2cap_connect() blokuje, dopóki strona nasłuchująca nie zaakceptuje (albo nie zawiedzie). Obie zwracają aioble.L2CAPChannel – sam będący asynchronicznym menedżerem kontekstu, który zamyka kanał przy wyjściu.

11.11.3. Wysyłanie i odbieranie

Dwie główne operacje na kanale to send() (zapisuje bajty do partnera) oraz recvinto() (odczytuje do wstępnie zaalokowanego bufora). Obie są korutynami.

  • send() fragmentuje bufor na fragmenty o rozmiarze MTU i oczekuje pomiędzy nimi na kredyty kontroli przepływu warstwy łącza. Z perspektywy aplikacji długie wysłanie to jeden await; wewnętrznie może ono kolejkować wiele pakietów i wstrzymywać się za każdym razem, gdy kredyty odbiorcze partnera się wyczerpią.

  • recvinto() wypełnia przekazany bufor tym, co jest dostępne (do MTU kanału) i zwraca liczbę bajtów. Oczekuje, jeśli nic nie jest dostępne.

  • available() zwraca synchronicznie True, jeśli gotowe są zbuforowane dane – przydatne do odpytywania bez zawieszania.

  • flush() oczekuje, dopóki wszystkie zaległe wysyłki nie zostaną w pełni przesłane do kontrolera.

Kanały L2CAP są podobne do strumieni w tym sensie, że bajty docierają w kolejności i bez strat, ale granice pojedynczego sendzachowywane – każda SDU wychodzi z pojedynczego recvinto. Różni się to od TCP, gdzie granice jednego send() mogą rozmazywać się na wiele wywołań recv().

11.11.4. Obsługa rozłączeń

Kanał zanika w trzech przypadkach: którakolwiek ze stron wywołuje disconnect(), leżące pod spodem połączenie GAP zostaje zerwane lub nadchodzi rozłączenie na poziomie L2CAP. Aktywne operacje zgłaszają aioble.L2CAPDisconnectedError. Podobnie jak po stronie GATT, objawia się to jako wyjątek w korutynie, która oczekiwała, a blok async with channel wychodzi w czysty sposób.

Jeśli kanał staje się nieosiągalny w wyniku rozłączenia na poziomie GAP, aplikacja wraca do rozgłaszania lub skanowania w taki sam sposób jak przy rozłączeniu GATT.

11.11.5. Koszt pamięci

Większe MTU i dłuższe kolejki zużywają więcej pamięci RAM po obu stronach. MTU o rozmiarze 512 bajtów plus bufor odbiorczy na kanał to około 1 KB na kanał – co nie jest darmowe na małej kamerze, jeśli kilka kanałów jest otwartych jednocześnie. Trzymaj się jednego kanału na połączenie i wybierz MTU pasujące do oczekiwanego rozmiaru wiadomości; domyślna wartość jednego L2CAPChannel na DeviceConnection wystarcza w większości aplikacji.

L2CAP jest zaworem bezpieczeństwa BLE. GATT jest tym, po co niemal każda aplikacja sięga najpierw, a pozostałe przykłady urządzeń centralnych / peryferyjnych w tej sekcji trzymają się GATT. API w stylu kanałowym jest odpowiedzią, gdy aplikacja przerasta model klucz/wartość.