11.8. O módulo aioble

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

  • bluetooth – a ligação de baixo nível ao controlador BLE. Síncrono, orientado a eventos através de uma callback no estilo IRQ, e estruturado em torno de buffers de bytes, handles e as primitivas GATT básicas. Expõe o protocolo tal como é, não como as aplicações Python o querem consumir.

  • aioble – um invólucro de nível superior, escrito em Python sobre bluetooth, que transforma cada operação remota numa corrotina asyncio e cada objeto BLE (serviços, características, ligações, resultados de scan, canais L2CAP) numa classe Python ergonómica. Os scans tornam-se iteradores assíncronos; as ligações tornam-se gestores de contexto assíncronos; as notificações tornam-se aguardáveis.

11.8.1. Quando usar o módulo de nível inferior

bluetooth continua a ser a resposta certa em dois casos restritos:

  • Está a escrever o tipo de código com que aioble é construído – um novo padrão que precisa de controlo ao nível de IRQ sobre o protocolo.

  • Está a correr numa plataforma de hardware onde o pacote aioble não está disponível, e um invólucro fino em torno do controlador é a única opção.

Para todas as aplicações de câmara, aioble é a resposta certa.

11.8.2. Componentes de um programa aioble

Cada aplicação baseada em aioble tem um pequeno conjunto de partes móveis, independentemente dos papéis que desempenha.

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

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

  • Um ou mais papéis em execução simultânea. No lado periférico: um conjunto registado 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 papel como uma tarefa independente.

11.8.3. Um periférico mínimo

O programa aioble mais pequeno e útil – um periférico que publica uma única característica só de 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 mais do que ligar 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 de «rádio desligado» até «trabalho útil concluído».

11.8.4. Desligar o rádio

Numa câmara a bateria, o rádio BLE é o maior consumo discricionário do orçamento. Dois parâmetros importam.

O primeiro é implícito: aioble ativa o rádio no primeiro uso, e o rádio dorme entre os eventos agendados (rajadas de publicidade, eventos de ligação, janelas de scan) automaticamente. Escolher intervalos mais longos em aioble.advertise() / aioble.scan() e acordar um intervalo de ligação mais longo no momento de connect() mantém o rádio desligado durante uma proporção maior do tempo. A tabela de publicidade em Publicidade e pesquisa é 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 derruba tudo em curso – ligações abertas caem, scanners e anunciantes cancelam, canais L2CAP fecham. Corrotinas que estavam à espera nessas operações levantam as suas exceções habituais (DeviceDisconnectedError e semelhantes), que é o mecanismo de limpeza para o qual os blocos async with circundantes foram escritos. Chamar qualquer corrotina aioble depois disso ativa o rádio novamente a partir do zero.

O padrão típico para uma câmara sensor alimentada a bateria e periódica é:

  • Acordar num horário (temporizador, sensor de movimento, botão).

  • Executar a rajada de trabalho BLE – publicitar, aceitar uma ligação, enviar o valor, desligar.

  • Chamar aioble.stop() e dormir até ao próximo acordar.

11.8.5. O que o aioble não faz

aioble cobre deliberadamente GATT, GAP e L2CAP – as camadas que uma aplicação usa. Três componentes estão fora do âmbito:

  • Qualquer coisa abaixo da camada de ligação. A seleção de canais, o salto de frequência, os reconhecimentos de pacotes e a encriptação da camada de ligação acontecem todos dentro da porta BLE e do silício do controlador; aioble não expõe ganchos a esse nível.

  • Bluetooth Clássico. aioble é exclusivamente BLE. Ligações de áudio, RFCOMM, A2DP e outras funcionalidades de perfis clássicos não fazem parte da API.

  • Bluetooth Mesh. A camada de redes em malha do Bluetooth SIG (uma pilha separada sobre publicidade BLE) não está implementada na câmara. A câmara pode publicitar e observar, mas não pode participar nos papéis de retransmissão / amigo / proxy de uma rede em malha.

11.8.6. Exceções

Quatro tipos de exceção provêm de aioble. Cada um é lançado dentro de uma corrotina que estava a aguardar uma operação quando algo correu mal; os blocos async with desenrolam-se de forma limpa quando se propagam.

  • aioble.DeviceDisconnectedError – a ligação BLE ao par caiu enquanto uma operação GATT (read, write, notified, indicated, subscribe, exchange_mtu, …) estava em curso. Levantado dentro de qualquer corrotina que estava à espera. A exceção mais comum de longe; capturá-la em qualquer código que deva reconectar após perda.

  • aioble.GattError – uma operação GATT chegou ao par mas completou com um estado ATT não nulo (escrita com resposta rejeitada, indicação não reconhecida, leitura não permitida, …). O código de estado está no atributo _status da exceção.

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

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

As operações que têm um timeout_ms explícito (as chamadas de ligação / descoberta / leitura / escrita / emparelhamento, mais timeout() como invólucro) levantam adicionalmente asyncio.TimeoutError de asyncio quando o prazo expirar antes de a operação completar.