11.11. Canais L2CAP

O GATT é um modelo chave/valor. As operações que oferece (leitura, escrita, notificação, indicação) movem um valor curto de cada vez, e a maior carga útil singular que conseguem transportar é aquela que o MTU negociado permite – algumas centenas de bytes, no melhor caso. Isto funciona bem para leituras de sensores, registos de comandos e sinalizadores de estado. Falha com kilobytes ou megabytes: dividir uma mancha longa em centenas de escritas pequenas custa percursos de ida e volta que o rádio consegue fazer muito mais rapidamente.

Para fluxos de dados em massa – um fotograma capturado que a câmara transmite para um telemóvel, 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 situa-se entre a camada de ligação e o GATT e permite que uma aplicação reivindique o seu próprio canal orientado à ligação sobre a mesma ligação de rádio. O canal é um caminho de bytes controlado por créditos de fluxo, com um MTU por pacote muito maior e sem enquadramento GATT no meio.

11.11.1. Quando usar L2CAP

Os canais L2CAP são a ferramenta adequada quando:

  • A transferência tem mais de algumas centenas de bytes.

  • Ambas as extremidades sabem que será utilizado um canal L2CAP (não está exposto na carga útil de publicidade; o cliente tem de conhecer o número do protocol/service multiplexer, ou PSM, do canal fora de banda).

  • A aplicação está disposta a prescindir das conveniências do GATT: sem endereçabilidade por UUID incorporada, sem capacidade de descoberta pelo cliente através de aplicações padrão, sem notificações.

O caso mais comum em aplicações baseadas em aioble é mover uma mancha binária entre dois componentes de software que conhecem a convenção PSM – um protocolo personalizado câmara-para-telemóvel, um par de câmaras openmv a comunicar entre si, um caminho interno de atualização de firmware sob o serviço GATT de um periférico.

Para tudo o resto, fique no GATT. Um estado curto, um registo de controlo, uma leitura de sensor – todos esses pertencem a uma característica.

11.11.2. Estabelecer um canal

O L2CAP corre em cima de uma aioble.DeviceConnection existente, pelo que o fluxo de descoberta / publicidade / ligação do lado GAP é exatamente o mesmo que para o GATT. Quando ambos os lados têm uma ligação, um lado fica à escuta num PSM e o outro lado liga-se a ele.

O PSM é apenas um pequeno inteiro. O Bluetooth SIG reserva a parte inferior do intervalo para uso normalizado (0x0001-0x007F); para canais específicos de aplicação, use um número do intervalo dinâmico (0x0080-0x00FF para PSMs fixos, 0x0040 em diante tipicamente livre para uso personalizado). Ambos os lados têm de concordar no valor previamente.

O MTU num canal L2CAP é a maior SDU (Service Data Unit) singular que qualquer lado entregará numa send() – não o MTU da ligação BLE. O Aioble fragmenta automaticamente cargas úteis maiores. O host BLE da câmara limita o MTU L2CAP a 1017 bytes; 512 é um valor predefinido sensato que deixa margem em ambos os lados sem desperdiçar RAM.

No lado do ouvinte (p. ex., a 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()

No lado do conector (p. ex., um telemóvel 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 ligue (ou timeout_ms expire); l2cap_connect() bloqueia até que o ouvinte aceite (ou falhe). Ambos devolvem um aioble.L2CAPChannel – que é em si um gestor de contexto assíncrono que fecha o canal ao sair.

11.11.3. Enviar e receber

As duas operações principais num 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 controlo de fluxo da camada de ligação entre eles. Um envio longo é um único await da perspetiva da aplicação; internamente pode colocar muitos pacotes em fila e pausar sempre que os créditos de receção do par se esgotem.

  • recvinto() preenche o buffer passado com o que estiver disponível (até ao MTU do canal) e devolve a contagem de bytes. Aguarda se não houver nada disponível.

  • available() devolve True de forma síncrona se houver dados em buffer prontos – útil para sondagem sem suspensão.

  • flush() aguarda até que qualquer envio pendente tenha sido totalmente transmitido ao controlador.

Os canais L2CAP são semelhantes a fluxos no sentido em que os bytes chegam por ordem e sem perdas, mas os limites de um único send são preservados – cada SDU sai de um único recvinto. Isto é diferente do TCP, onde os limites de um send() podem distribuir-se por múltiplas chamadas recv().

11.11.4. Tratamento de desligamento

O canal desaparece em três condições: qualquer um dos lados chama disconnect(), a ligação GAP subjacente cai, ou chega o desligamento a nível L2CAP. Operações ativas levantam aioble.L2CAPDisconnectedError. Tal como no lado GATT, isto surge como uma exceção na corrotina que estava à espera, e o bloco async with channel sai de forma limpa.

Se um canal ficar inacessível devido a um desligamento ao nível GAP, a aplicação volta a publicitar ou a fazer scan da mesma forma que faria para um desligamento 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 receção por canal representa cerca de 1 KB por canal – não é gratuito numa câmara pequena se vários canais estiverem abertos ao mesmo tempo. Mantenha-se num canal por ligação e escolha um MTU que corresponda ao tamanho esperado das mensagens; a predefiniçã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 é aquilo a que quase todas as aplicações recorrem em primeiro lugar, e os exemplos central/periférico do resto desta secção ficam no GATT. A API orientada a canais é a resposta quando uma aplicação ultrapassa o modelo chave/valor.