11.8. O módulo aioble

A especificação Bluetooth Core fornece um vocabulário que se mapeia para dois módulos MicroPython.

  • bluetooth – a ligação de baixo nível com o controlador BLE. Síncrona, orientada a eventos através de um callback no estilo de IRQ, e estruturada em torno de buffers de bytes, handles e as primitivas GATT puras. Ela expõe o protocolo como ele é, não como as aplicações Python querem consumi-lo.

  • aioble – um wrapper de nível mais alto, escrito em Python sobre o bluetooth, que transforma cada operação remota em uma corrotina asyncio e cada objeto BLE (serviços, características, conexões, resultados de varredura, canais L2CAP) em uma classe Python ergonômica. As varreduras se tornam iteradores assíncronos; as conexões se tornam gerenciadores de contexto assíncronos; as notificações se tornam aguardáveis (awaitable).

11.8.1. Quando recorrer ao módulo de nível mais baixo

bluetooth ainda é a resposta certa para dois casos restritos:

  • Você está escrevendo o tipo de código a partir do qual o próprio aioble é construído – um novo padrão que precisa de controle em nível de IRQ sobre o protocolo.

  • Você está rodando em uma plataforma de hardware onde o pacote aioble não está disponível, e uma camada fina em torno do controlador é a única opção.

Para toda aplicação de câmera, aioble é a resposta certa.

11.8.2. Partes de um programa aioble

Toda aplicação baseada em aioble tem um pequeno conjunto de partes móveis, independentemente das funções que ela desempenha.

  • Um loop de eventos asyncio de longa duração. Tudo em aioble é uma corrotina, então a aplicação é estruturada como uma ou mais tarefas em um único loop de eventos. Consulte Asyncio para detalhes sobre o loop, as tarefas e as exceções.

  • Um rádio ligado. aioble ativa o rádio BLE implicitamente no primeiro uso, mas ele também pode ser controlado explicitamente com aioble.config() (que repassa para bluetooth.BLE.config() após garantir que o rádio está ativo) e desligado com aioble.stop().

  • Uma ou mais funções em andamento ao mesmo tempo. No lado peripheral: um conjunto registrado de serviços GATT (consulte aioble.register_services()) e uma corrotina aioble.advertise() em execução. No lado central: um iterador aioble.scan() em execução ou um aioble.Device.connect() pendente. O rádio multiplexa o trabalho; a aplicação vê cada função como uma tarefa independente.

11.8.3. Um peripheral mínimo

O menor programa aioble útil – um peripheral que anuncia uma única característica somente leitura – é curto:

import aioble
import asyncio
import bluetooth

SERVICE_UUID = bluetooth.UUID(0x181A)            # Environmental Sensing
TEMP_UUID = bluetooth.UUID(0x2A6E)               # Temperature

service = aioble.Service(SERVICE_UUID)
temp = aioble.Characteristic(service, TEMP_UUID, read=True)
aioble.register_services(service)

async def main():
    while True:
        conn = await aioble.advertise(
            interval_us=250000,
            name="openmv-temp",
            services=[SERVICE_UUID],
        )
        async with conn:
            await conn.disconnected()

asyncio.run(main())

Um central que não faz nada além de conectar e ler uma vez é igualmente curto:

import aioble
import asyncio
import bluetooth

SERVICE_UUID = bluetooth.UUID(0x181A)
TEMP_UUID = bluetooth.UUID(0x2A6E)

async def main():
    device = None
    async with aioble.scan(duration_ms=5000, active=True) as scanner:
        async for result in scanner:
            if SERVICE_UUID in result.services():
                device = result.device
                break
    if device is None:
        return

    async with await device.connect() as conn:
        service = await conn.service(SERVICE_UUID)
        char = await service.characteristic(TEMP_UUID)
        print(await char.read())

asyncio.run(main())

Ambos os programas têm cerca de quinze linhas e cobrem todo o fluxo desde “o rádio está desligado” até “trabalho útil concluído”.

11.8.4. Desligando o rádio

Em uma câmera alimentada por bateria, o rádio BLE é o maior consumo discricionário do orçamento. Dois controles importam.

O primeiro é implícito: aioble ativa o rádio no primeiro uso, e o rádio dorme entre eventos agendados (rajadas de anúncio, eventos de conexão, janelas de varredura) automaticamente. Escolher intervalos mais longos em aioble.advertise() / aioble.scan() e concordar com um intervalo de conexão mais longo no momento do connect() mantém o rádio desligado proporcionalmente por mais tempo. A tabela de anúncio em Anúncio e varredura é o guia prático aqui.

O segundo é o desligamento explícito

import aioble

await do_burst_of_ble_work()
aioble.stop()                             # radio deactivated; in-flight tasks unwound
await asyncio.sleep(60)                   # sleep with the radio off
# ... next aioble call brings the radio back up automatically

aioble.stop() desativa o rádio BLE subjacente e encerra qualquer coisa em andamento – conexões abertas caem, varredores e anunciantes são cancelados, canais L2CAP fecham. As corrotinas que estavam aguardando essas operações levantam suas exceções usuais (DeviceDisconnectedError e afins), que é o mecanismo de limpeza para o qual os blocos async with ao redor foram escritos. Chamar qualquer corrotina aioble depois disso ativa o rádio novamente do zero.

O padrão típico para uma câmera de sensor periódica alimentada por bateria é:

  • Acordar segundo um cronograma (timer, sensor de movimento, botão).

  • Executar a rajada de trabalho BLE – anunciar, aceitar uma conexão, enviar o valor, desconectar.

  • Chamar aioble.stop() e dormir até o próximo despertar.

11.8.5. O que o aioble não faz

aioble cobre deliberadamente GATT, GAP e L2CAP – as camadas que uma aplicação utiliza. Três peças estão fora de escopo:

  • Qualquer coisa abaixo da camada de enlace. Seleção de canal, salto de frequência, confirmações de pacotes e criptografia em nível de enlace acontecem todas dentro da porta BLE e do silício do controlador; aioble não expõe ganchos nesse nível.

  • Bluetooth clássico. aioble é exclusivo para BLE. Enlaces de áudio, RFCOMM, A2DP e outros recursos de perfis clássicos não fazem parte da API.

  • Bluetooth Mesh. A camada de rede em malha do Bluetooth SIG (uma pilha separada sobre o anúncio BLE) não está implementada na câmera. A câmera pode anunciar e observar, mas não pode participar das funções de relay / friend / proxy de uma rede em malha.

11.8.6. Exceções

Quatro tipos de exceção saem do aioble. Cada uma é disparada de dentro de uma corrotina que estava aguardando uma operação quando algo deu errado; os blocos async with se desfazem de forma limpa quando elas se propagam.

  • aioble.DeviceDisconnectedError – o enlace BLE com o par caiu enquanto uma operação GATT (read, write, notified, indicated, subscribe, exchange_mtu, …) estava em andamento. Levantada dentro de qualquer corrotina que estivesse aguardando. De longe, a exceção mais comum; capture-a em qualquer código que deva reconectar em caso de perda.

  • aioble.GattError – uma operação GATT chegou ao par, mas foi concluída com um status ATT diferente de zero (write-with-response rejeitado, indicate não confirmado, read-not-permitted, …). O código de status está no atributo _status da exceção.

  • aioble.L2CAPDisconnectedError – o canal L2CAP caiu enquanto um send(), recvinto() ou flush() estava em andamento. Qualquer um dos lados pode ter fechado o canal, ou a conexão GAP subjacente desapareceu.

  • aioble.L2CAPConnectionError – levantada por l2cap_connect() quando o listener recusou ou o controlador falhou na configuração do canal. O código de status do Bluetooth é o primeiro argumento posicional.

Operações que recebem um timeout_ms explícito (as chamadas de connect / discovery / read / write / pair, além de timeout() como wrapper) levantam adicionalmente asyncio.TimeoutError do asyncio quando o prazo expira antes de a operação ser concluída.