11.11. Canales L2CAP¶
GATT es un modelo de clave/valor. Las operaciones que ofrece (lectura, escritura, notificación, indicación) mueven un único valor corto a la vez, y la mayor carga útil individual que pueden transportar es la que permita el MTU negociado – unos pocos cientos de bytes como mucho. Eso funciona bien para lecturas de sensores, registros de comandos y banderas de estado. Se desmorona con kilobytes o megabytes: dividir un blob largo en cientos de escrituras pequeñas cuesta idas y vueltas en las que la radio es mucho más lenta.
Para flujos de datos masivos – un fotograma capturado que la cámara transmite a un teléfono, una imagen de actualización por aire, una exportación en lote de mediciones – BLE ofrece una vía alternativa: el Logical Link Control and Adaptation Protocol, L2CAP. L2CAP se sitúa entre la capa de enlace y GATT y permite que una aplicación reclame su propio canal orientado a conexión sobre el mismo enlace de radio. El canal es una vía de bytes con control de flujo por créditos, con un MTU por paquete mucho mayor y sin el entramado de GATT en medio.
11.11.1. Cuándo usar L2CAP¶
Los canales L2CAP son la herramienta adecuada cuando:
La transferencia es de más de unos pocos cientos de bytes.
Ambos extremos saben que se usará un canal L2CAP (no se expone en la carga útil del anuncio; el cliente tiene que conocer el número del protocol/service multiplexer, o PSM, del canal por otra vía).
La aplicación está dispuesta a renunciar a las ventajas de GATT: sin direccionamiento integrado por UUID, sin posibilidad de que las apps estándar descubran al cliente, sin notificaciones.
El caso más común en aplicaciones basadas en aioble es mover un blob binario entre dos piezas de software que conocen la convención del PSM – un protocolo personalizado de cámara a teléfono, un par de cámaras openmv hablando entre sí, una vía interna de actualización de firmware bajo el servicio GATT de un periférico.
Para todo lo demás, quédate con GATT. Un estado corto, un registro de control, una lectura de sensor – todo eso pertenece a una característica.
11.11.2. Establecer un canal¶
L2CAP se ejecuta sobre una aioble.DeviceConnection existente, por lo que el flujo de descubrimiento / anuncio / conexión del lado GAP es exactamente el mismo que para GATT. Una vez que ambos lados tienen una conexión, un lado escucha en un PSM y el otro lado se conecta a él.
El PSM es simplemente un entero pequeño. El Bluetooth SIG reserva la parte baja del rango para uso estandarizado (0x0001-0x007F); para canales específicos de aplicación usa un número del rango dinámico (0x0080-0x00FF para PSM fijos, 0x0040 en adelante normalmente libre para uso personalizado). Ambos lados deben acordar el valor de antemano.
El MTU de un canal L2CAP es la mayor SDU individual (Service Data Unit) que cualquiera de los lados entregará en un único send() – no el MTU del enlace BLE. Aioble fragmenta automáticamente las cargas útiles más grandes. El host BLE de la cámara limita el MTU de L2CAP a 1017 bytes; 512 es un valor predeterminado sensato que deja margen en ambos lados sin consumir demasiada RAM.
En el lado del oyente (p. ej. la cámara como periférico):
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()
En el lado del conector (p. ej. un teléfono o central):
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() se bloquea hasta que el par se conecta (o se dispara timeout_ms); l2cap_connect() se bloquea hasta que el oyente acepta (o falla). Ambos retornan un aioble.L2CAPChannel – a su vez un gestor de contexto asíncrono que cierra el canal al salir.
11.11.3. Envío y recepción¶
Las dos operaciones principales sobre un canal son send() (escribe bytes al par) y recvinto() (lee en un búfer preasignado). Ambas son corrutinas.
send()fragmenta el búfer en trozos del tamaño del MTU y espera créditos de control de flujo de la capa de enlace entre ellos. Un envío largo es un únicoawaitdesde la perspectiva de la aplicación; internamente puede encolar muchos paquetes y pausarse cuando se agotan los créditos de recepción del par.recvinto()llena el búfer pasado con lo que esté disponible (hasta el MTU del canal) y retorna el recuento de bytes. Espera si no hay nada disponible.available()retornaTruede forma síncrona si hay datos en el búfer listos – útil para sondear sin suspender.flush()espera hasta que cualquier envío pendiente se haya transmitido por completo al controlador.
Los canales L2CAP son similares a flujos en el sentido de que los bytes llegan en orden y sin pérdidas, pero los límites de un único send se preservan – cada SDU sale de un único recvinto. Eso es distinto de TCP, donde los límites de un send() pueden mezclarse a lo largo de varias llamadas recv().
11.11.4. Gestión de la desconexión¶
El canal desaparece en tres condiciones: que cualquiera de los lados llame a disconnect(), que la conexión GAP subyacente caiga, o que llegue la desconexión a nivel de L2CAP. Las operaciones activas lanzan aioble.L2CAPDisconnectedError. Como en el lado GATT, esto aflora como una excepción en la corrutina que estaba esperando, y el bloque async with channel sale limpiamente.
Si un canal se vuelve inalcanzable por una desconexión a nivel de GAP, la aplicación vuelve a anunciarse o a escanear de la misma manera que lo haría ante una desconexión GATT.
11.11.5. Coste de memoria¶
Los MTU más grandes y las colas más largas usan más RAM en ambos lados. Un MTU de 512 bytes más un búfer de recepción por canal son alrededor de 1 KB por canal – no es gratuito en una cámara pequeña si hay varios canales abiertos a la vez. Limítate a un canal por conexión y elige un MTU acorde al tamaño de mensaje esperado; el valor predeterminado de un L2CAPChannel por DeviceConnection es suficiente para la mayoría de las aplicaciones.
L2CAP es la válvula de seguridad de BLE. GATT es lo que casi toda aplicación elige primero, y el resto de los ejemplos de central / periférico de esta sección se ciñen a GATT. La API orientada a canales es la respuesta cuando una aplicación supera el modelo de clave/valor.