11.11. Канали L2CAP#
GATT — це модель «ключ/значення». Операції, які вона надає (читання, запис, сповіщення, індикація), переміщують одне коротке значення за раз, а найбільший одиничний пакет, який вони можуть переносити, визначається узгодженим значенням MTU — у кращому разі кілька сотень байт. Це добре працює для показань датчиків, регістрів команд і статусних прапорів. Але на кілобайтах чи мегабайтах усе розсипається: розбиття довгої плями на сотні малих записів коштує зайвих обмінів, а радіоканал значно швидший за це.
Для потоків об’ємних даних — захопленого кадру, який камера передає на телефон, образу оновлення прошивки по повітрю, пакетного експорту вимірювань — BLE пропонує альтернативний шлях: Logical Link Control and Adaptation Protocol, L2CAP. L2CAP знаходиться між канальним рівнем і GATT та дозволяє застосунку зайняти власний з’єднано-орієнтований канал поверх того самого радіозв’язку. Канал є байтовим шляхом з управлінням потоком на основі кредитів, набагато більшим MTU на пакет і без GATT-фреймінгу посередині.
11.11.1. Коли використовувати L2CAP#
Канали L2CAP — правильний інструмент, коли:
Обсяг передачі перевищує кілька сотень байт.
Обидва кінці знають, що буде використовуватися канал L2CAP (він не відображається в рекламному пакеті; клієнт має знати мультиплексор протоколу/сервісу каналу, або PSM, заздалегідь).
Застосунок готовий відмовитися від зручностей GATT: немає вбудованої адресації за UUID, немає виявлення клієнтами через стандартні застосунки, немає сповіщень.
Найпоширенішим випадком у застосунках на основі aioble є передача двійкової плями між двома програмними компонентами, які обидва знають про конвенцію PSM — власний протокол камера-до-телефону, пара камер openmv, що спілкуються між собою, внутрішній шлях оновлення мікропрограми під GATT-сервісом периферійного пристрою.
Для всього іншого залишайтеся на GATT. Короткий статус, регістр керування, показання датчика — все це належить характеристиці.
11.11.2. Встановлення каналу#
L2CAP працює поверх наявного aioble.DeviceConnection, тому процес виявлення/реклами/підключення на стороні GAP точно такий самий, як і для GATT. Як тільки обидві сторони мають підключення, одна сторона слухає на PSM, інша — підключається до нього.
PSM — це просто мале ціле число. Bluetooth SIG резервує нижній діапазон для стандартизованого використання (0x0001-0x007F); для каналів конкретного застосунку використовуйте число з динамічного діапазону (0x0080-0x00FF для фіксованих PSM, 0x0040 і далі зазвичай вільні для власного використання). Обидві сторони мають заздалегідь погодити значення.
MTU на каналі L2CAP — це найбільший одиничний SDU (Service Data Unit), який кожна зі сторін доставить за один send() — не MTU BLE-зв’язку. Aioble автоматично фрагментує більші пакети. BLE-хост камери обмежує MTU L2CAP до 1017 байт; 512 — розумний за замовчуванням, що залишає місце з обох боків без зайвих витрат RAM.
На стороні слухача (наприклад, камера як периферійний пристрій):
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()
На стороні з’єднувача (наприклад, телефон або центральний пристрій):
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() блокується до підключення партнера (або спрацьовування timeout_ms); l2cap_connect() блокується до прийняття слухачем (або невдачі). Обидва повертають aioble.L2CAPChannel — сам по собі асинхронний контекстний менеджер, який закриває канал при виході.
11.11.3. Надсилання та отримання#
Дві основні операції на каналі: send() (записує байти до партнера) і recvinto() (читає в попередньо виділений буфер). Обидві є корутинами.
send()фрагментує буфер на частини розміром MTU і очікує між ними кредити управління потоком канального рівня. Тривале відправлення — це одинawaitз точки зору застосунку; внутрішньо воно може ставити в чергу багато пакетів і призупинятися, коли кредити прийому партнера вичерпуються.recvinto()заповнює переданий буфер тим, що є (до MTU каналу), і повертає кількість байт. Чекає, якщо нічого не доступно.available()синхронно повертаєTrue, якщо є буферизовані дані — корисно для опитування без призупинення.flush()чекає, поки будь-яке незавершене надсилання не буде повністю передано до контролера.
Канали L2CAP подібні до потоків у тому сенсі, що байти надходять по порядку і без втрат, але межі одного send зберігаються — кожен SDU виходить з одного recvinto. Це відрізняється від TCP, де межі одного send() можуть розмиватися в кількох викликах recv().
11.11.4. Обробка від’єднання#
Канал зникає за трьох умов: будь-яка сторона викликає disconnect(), базове GAP-з’єднання обривається, або надходить відключення на рівні L2CAP. Активні операції генерують виняток aioble.L2CAPDisconnectedError. Як і на стороні GATT, це проявляється як виняток у корутині, яка очікувала, і блок async with channel завершується чисто.
Якщо канал стає недосяжним через відключення на рівні GAP, застосунок повертається до реклами або сканування так само, як при відключенні GATT.
11.11.5. Витрати пам’яті#
Більші MTU і довші черги використовують більше RAM з обох боків. MTU 512 байт плюс буфер прийому на канал займає близько 1 КБ на канал — це не безкоштовно на маленькій камері, якщо одночасно відкрито кілька каналів. Тримайтеся одного каналу на з’єднання і вибирайте MTU, який відповідає очікуваному розміру повідомлення; типово одного L2CAPChannel на DeviceConnection достатньо для більшості застосунків.
L2CAP — це запобіжний клапан BLE. GATT — це те, до чого майже кожен застосунок звертається першим, і решта прикладів центрального/периферійного пристрою в цьому розділі залишаються на GATT. API на основі каналів — це відповідь, коли застосунок переростає модель «ключ/значення».