11.11. Canais L2CAP¶
O GATT é um modelo chave/valor. As operações que ele oferece (read, write, notify, indicate) movem um valor curto por vez, e o maior payload único que podem transportar é o que o MTU negociado permitir – alguns poucos centenas de bytes, na melhor das hipóteses. Isso funciona bem para leituras de sensores, registradores de comando e flags de status. Mas desmorona em kilobytes ou megabytes: dividir um blob longo em centenas de pequenas escritas custa idas e voltas em relação às quais o rádio é muito mais rápido.
Para fluxos de dados em massa – um quadro capturado que a câmera transmite para um celular, uma imagem de atualização over-the-air, uma exportação em lote de medições – o BLE oferece um caminho alternativo: o Logical Link Control and Adaptation Protocol, L2CAP. O L2CAP fica entre a camada de enlace e o GATT e permite que uma aplicação reivindique seu próprio canal orientado a conexão sobre o mesmo enlace de rádio. O canal é um caminho de bytes com controle de fluxo por créditos, com um MTU por pacote muito maior e sem o enquadramento GATT no meio.
11.11.1. Quando usar o L2CAP¶
Os canais L2CAP são a ferramenta certa quando:
A transferência tem mais do que algumas poucas centenas de bytes.
As duas pontas sabem que um canal L2CAP será usado (ele não é exposto no payload de anúncio; o cliente precisa saber, fora de banda, o número do protocol/service multiplexer, ou PSM, do canal).
A aplicação está disposta a abrir mão das comodidades do GATT: sem endereçamento embutido por UUID, sem descoberta pelo cliente através de aplicativos padrão, sem notificações.
O caso mais comum em aplicações baseadas em aioble é mover um blob binário entre duas peças de software que ambas conhecem a convenção de PSM – um protocolo personalizado câmera-para-celular, um par de câmeras openmv conversando entre si, um caminho interno de atualização de firmware sob o serviço GATT de um peripheral.
Para todo o resto, permaneça no GATT. Um status curto, um registrador de controle, uma leitura de sensor – todos esses pertencem a uma característica.
11.11.2. Estabelecendo um canal¶
O L2CAP é executado sobre uma aioble.DeviceConnection existente, portanto o fluxo de descoberta / anúncio / conexão do lado GAP é exatamente o mesmo do GATT. Uma vez que ambos os lados mantêm uma conexão, um lado escuta em um PSM e o outro lado se conecta a ele.
O PSM é apenas um pequeno inteiro. O Bluetooth SIG reserva a parte inferior da faixa para uso padronizado (0x0001-0x007F); para canais específicos da aplicação, use um número da faixa dinâmica (0x0080-0x00FF para PSMs fixos, 0x0040 em diante normalmente livres para uso personalizado). Ambos os lados precisam concordar com o valor de antemão.
O MTU em um canal L2CAP é a maior SDU única (Service Data Unit) que qualquer um dos lados entregará em um único send() – não o MTU do enlace BLE. O Aioble fragmenta payloads maiores automaticamente. O host BLE da câmera limita o MTU do L2CAP a 1017 bytes; 512 é um padrão sensato que deixa espaço em ambos os lados sem consumir RAM.
No lado do listener (por exemplo, a câmera como peripheral):
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()
No lado do conector (por exemplo, um celular ou 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() bloqueia até que o par se conecte (ou timeout_ms dispare); l2cap_connect() bloqueia até que o listener aceite (ou falhe). Ambos retornam uma aioble.L2CAPChannel – ela própria um gerenciador de contexto assíncrono que fecha o canal na saída.
11.11.3. Enviando e recebendo¶
As duas principais operações em um canal são send() (escreve bytes para o par) e recvinto() (lê para um buffer pré-alocado). Ambas são corrotinas.
send()fragmenta o buffer em blocos do tamanho do MTU e aguarda créditos de controle de fluxo da camada de enlace entre eles. Um envio longo é um únicoawaitdo ponto de vista da aplicação; internamente, ele pode enfileirar muitos pacotes e pausar sempre que os créditos de recepção do par se esgotarem.recvinto()preenche o buffer passado com o que estiver disponível (até o MTU do canal) e retorna a contagem de bytes. Aguarda se nada estiver disponível.available()retornaTruede forma síncrona se houver dados em buffer prontos – útil para fazer polling sem suspender.flush()aguarda até que qualquer envio pendente tenha sido totalmente transmitido ao controlador.
Os canais L2CAP são semelhantes a fluxos (stream) no sentido de que os bytes chegam em ordem e sem perda, mas os limites de um único send são preservados – cada SDU sai de um único recvinto. Isso é diferente do TCP, onde os limites de um send() podem se misturar entre múltiplas chamadas recv().
11.11.4. Tratamento de desconexão¶
O canal desaparece sob três condições: qualquer um dos lados chama disconnect(), a conexão GAP subjacente cai, ou a desconexão em nível de L2CAP chega. As operações ativas levantam aioble.L2CAPDisconnectedError. Como no lado GATT, isso aparece como uma exceção na corrotina que estava aguardando, e o bloco async with channel sai de forma limpa.
Se um canal se torna inalcançável por meio de uma desconexão em nível de GAP, a aplicação volta ao anúncio ou à varredura da mesma forma que faria para uma desconexão GATT.
11.11.5. Custo de memória¶
MTUs maiores e filas mais longas usam mais RAM em ambos os lados. Um MTU de 512 bytes mais um buffer de recepção por canal são cerca de 1 KB por canal – não algo gratuito em uma câmera pequena se vários canais estiverem abertos ao mesmo tempo. Mantenha um canal por conexão e escolha um MTU que corresponda ao tamanho esperado da mensagem; o padrão de um L2CAPChannel por DeviceConnection é suficiente para a maioria das aplicações.
O L2CAP é a válvula de segurança do BLE. O GATT é o que quase toda aplicação busca primeiro, e o restante dos exemplos de central / peripheral desta seção permanece no GATT. A API com sabor de canal é a resposta quando uma aplicação supera o modelo chave/valor.