11.11. Kanály L2CAP

GATT je model klíč/hodnota. Operace, které nabízí (read, write, notify, indicate), přenášejí najednou jednu krátkou hodnotu a největší jednotlivý užitečný obsah, který mohou nést, je to, co umožňuje vyjednané MTU – v nejlepším případě několik stovek bajtů. To funguje dobře pro hodnoty ze senzorů, příkazové registry a stavové příznaky. Selhává to u kilobajtů či megabajtů: rozdělení dlouhého blobu na stovky malých zápisů stojí přenosy tam a zpět, oproti nimž je rádio mnohem rychlejší.

Pro toky hromadných dat – zachycený snímek, který kamera streamuje do telefonu, obraz pro aktualizaci přes vzduch, dávkový export měření – nabízí BLE alternativní cestu: Logical Link Control and Adaptation Protocol, L2CAP. L2CAP se nachází mezi linkovou vrstvou a GATT a umožňuje aplikaci nárokovat si vlastní kanál orientovaný na připojení nad stejným rádiovým spojem. Kanál je bajtová cesta s řízením toku pomocí kreditů, s mnohem větším MTU na paket a bez GATT rámcování uprostřed.

11.11.1. Kdy použít L2CAP

Kanály L2CAP jsou tím správným nástrojem, když:

  • Přenos je větší než několik stovek bajtů.

  • Oba konce vědí, že se použije kanál L2CAP (není to vystaveno v inzertním obsahu; klient musí znát číslo protocol/service multiplexer neboli PSM kanálu mimo pásmo).

  • Aplikace je ochotna vzdát se výhod GATT: žádná vestavěná adresovatelnost přes UUID, žádná zjistitelnost klientem přes standardní aplikace, žádná oznámení.

Nejběžnějším případem v aplikacích založených na aioble je přesun binárního blobu mezi dvěma programy, které oba znají konvenci PSM – vlastní protokol kamera-telefon, dvojice kamer openmv komunikujících mezi sebou, interní cesta pro aktualizaci firmware pod GATT službou periferie.

Pro vše ostatní zůstaňte u GATT. Krátký stav, řídicí registr, hodnota ze senzoru – to vše patří do charakteristiky.

11.11.2. Navázání kanálu

L2CAP běží nad existujícím aioble.DeviceConnection, takže tok zjišťování / inzerce / připojování na straně GAP je přesně stejný jako u GATT. Jakmile obě strany drží připojení, jedna strana naslouchá na PSM a druhá se k ní připojuje.

PSM je jen malé celé číslo. Bluetooth SIG rezervuje spodní část rozsahu pro standardizované použití (0x0001-0x007F); pro kanály specifické pro aplikaci použijte číslo z dynamického rozsahu (0x0080-0x00FF pro pevná PSM, 0x0040 a výše obvykle volné pro vlastní použití). Obě strany se musí na hodnotě předem dohodnout.

MTU na kanálu L2CAP je největší jednotlivá SDU (Service Data Unit), kterou kterákoli strana doručí v jednom send() – nikoli MTU BLE spoje. Aioble větší užitečné obsahy automaticky fragmentuje. BLE host kamery omezuje MTU L2CAP na 1017 bajtů; 512 je rozumná výchozí hodnota, která ponechává prostor na obou stranách bez spalování RAM.

Na straně naslouchajícího (např. kamera jako periferie):

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()

Na straně připojujícího (např. telefon nebo centrální zařízení):

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, dokud se protějšek nepřipojí (nebo nevyprší timeout_ms); l2cap_connect() blokuje, dokud naslouchající nepřijme (nebo neselže). Obě vracejí aioble.L2CAPChannel – sám o sobě asynchronní kontextový správce, který kanál při ukončení zavře.

11.11.3. Odesílání a příjem

Dvě hlavní operace na kanálu jsou send() (zapisuje bajty protějšku) a recvinto() (čte do předem alokovaného bufferu). Obě jsou korutiny.

  • send() fragmentuje buffer na části velikosti MTU a mezi nimi čeká na kredity řízení toku na linkové vrstvě. Dlouhé odeslání je z pohledu aplikace jediné await; interně může zařadit do fronty mnoho paketů a pozastavit se pokaždé, když protějšku dojdou přijímací kredity.

  • recvinto() naplní předaný buffer tím, co je k dispozici (až do MTU kanálu), a vrátí počet bajtů. Čeká, pokud není nic k dispozici.

  • available() vrací synchronně True, pokud jsou připravena bufferovaná data – užitečné pro dotazování bez pozastavení.

  • flush() čeká, dokud nebude jakékoli nevyřízené odeslání plně přeneseno do řadiče.

Kanály L2CAP jsou podobné proudu v tom smyslu, že bajty přicházejí v pořadí a bez ztrát, ale hranice jediného send jsou zachovány – každá SDU vychází z jediného recvinto. To je na rozdíl od TCP, kde se hranice jednoho send() mohou rozetřít napříč více voláními recv().

11.11.4. Zpracování odpojení

Kanál zaniká za tří podmínek: kterákoli strana zavolá disconnect(), podkladové GAP připojení vypadne, nebo dorazí odpojení na úrovni L2CAP. Aktivní operace vyvolají aioble.L2CAPDisconnectedError. Stejně jako na straně GATT se to projeví jako výjimka v korutině, která čekala, a blok async with channel se čistě ukončí.

Pokud se kanál stane nedosažitelným kvůli odpojení na úrovni GAP, aplikace se vrací zpět k inzerci nebo skenování stejným způsobem, jako by to udělala u odpojení GATT.

11.11.5. Paměťové náklady

Větší MTU a delší fronty využívají více RAM na obou stranách. MTU 512 bajtů plus přijímací buffer pro každý kanál představuje zhruba 1 KB na kanál – na malé kameře to není zadarmo, pokud je otevřeno několik kanálů najednou. Držte se jednoho kanálu na připojení a zvolte MTU odpovídající očekávané velikosti zpráv; výchozí jeden L2CAPChannel na DeviceConnection postačí pro většinu aplikací.

L2CAP je pojistný ventil BLE. GATT je to, po čem téměř každá aplikace sáhne jako první, a zbytek příkladů centrálního a periferního zařízení v této sekci se drží GATT. API ve stylu kanálů je odpovědí, když aplikace přeroste model klíč/hodnota.