11.11. Каналы L2CAP¶
GATT – это модель ключ/значение. Операции, которые она предлагает (чтение, запись, уведомление, индикация), передают одно короткое значение за раз, а наибольшая полезная нагрузка, которую они могут нести, ограничена согласованным MTU – в лучшем случае несколько сотен байтов. Это хорошо работает для показаний датчиков, командных регистров и флагов состояния. Но это разваливается на килобайтах или мегабайтах: разбиение длинного блоба на сотни мелких записей обходится в круговые задержки, на фоне которых радиомодуль работает гораздо быстрее.
Для потоков объёмных данных – захваченного кадра, который камера передаёт телефону, образа обновления по воздуху, пакетного экспорта измерений – BLE предлагает альтернативный путь: Logical Link Control and Adaptation Protocol (протокол управления и адаптации логического канала), L2CAP. L2CAP находится между канальным уровнем и GATT и позволяет приложению заявить свой собственный ориентированный на соединение канал поверх того же радиоканала. Канал представляет собой байтовый путь с управлением потоком по кредитам, гораздо большим MTU на пакет и без обрамления GATT посередине.
11.11.1. Когда использовать L2CAP¶
Каналы L2CAP – это правильный инструмент, когда:
Объём передачи превышает несколько сотен байтов.
Обе стороны заранее знают, что будет использоваться канал L2CAP (он не отображается в рекламной полезной нагрузке; клиент должен узнать номер protocol/service multiplexer, или 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 – разумное значение по умолчанию, оставляющее запас на обеих сторонах без излишнего расхода ОЗУ.
На стороне слушателя (например, камера как периферийное устройство):
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 и более длинные очереди используют больше ОЗУ на обеих сторонах. MTU в 512 байтов плюс приёмный буфер на канал составляют около 1 КБ на канал – что не бесплатно на маленькой камере, если несколько каналов открыты одновременно. Придерживайтесь одного канала на подключение и выбирайте MTU, соответствующий ожидаемому размеру сообщения; значения по умолчанию – один L2CAPChannel на DeviceConnection – достаточно для большинства приложений.
L2CAP – это предохранительный клапан BLE. GATT – то, к чему почти каждое приложение обращается в первую очередь, и остальные примеры центральных / периферийных устройств в этом разделе придерживаются GATT. API с каналами – это ответ на случай, когда приложение перерастает модель ключ/значение.