11.8. El módulo aioble

La especificación Bluetooth Core ofrece un vocabulario que se asigna a dos módulos de MicroPython.

  • bluetooth – el enlace de bajo nivel con el controlador BLE. Síncrono, dirigido por eventos a través de una función de retorno (callback) al estilo de una IRQ, y estructurado en torno a búferes de bytes, manejadores y las primitivas GATT en crudo. Expone el protocolo tal como es, no como las aplicaciones Python quieren consumirlo.

  • aioble – un envoltorio de más alto nivel, escrito en Python sobre bluetooth, que convierte cada operación remota en una corrutina de asyncio y cada objeto BLE (servicios, características, conexiones, resultados de escaneo, canales L2CAP) en una clase de Python ergonómica. Los escaneos se convierten en iteradores asíncronos; las conexiones se convierten en gestores de contexto asíncronos; las notificaciones se vuelven esperables.

11.8.1. Cuándo recurrir al módulo de más bajo nivel

bluetooth sigue siendo la respuesta adecuada para dos casos concretos:

  • Estás escribiendo el tipo de código del que el propio aioble está construido – un nuevo patrón que necesita control a nivel de IRQ sobre el protocolo.

  • Estás ejecutando en un destino de hardware donde el paquete aioble no está disponible, y una fina capa de adaptación alrededor del controlador es la única opción.

Para toda aplicación de cámara, aioble es la respuesta adecuada.

11.8.2. Piezas de un programa aioble

Toda aplicación basada en aioble tiene un pequeño conjunto de partes móviles, independientemente de los roles que desempeñe.

  • Un bucle de eventos de asyncio de larga duración. Todo en aioble es una corrutina, por lo que la aplicación se estructura como una o más tareas en un único bucle de eventos. Consulta Asyncio para conocer en detalle el bucle, las tareas y las excepciones.

  • Una radio encendida. aioble activa la radio BLE de forma implícita en el primer uso, pero también puede controlarse explícitamente con aioble.config() (que reenvía a bluetooth.BLE.config() tras asegurar que la radio está activa) y apagarse con aioble.stop().

  • Uno o más roles en marcha a la vez. En el lado del periférico: un conjunto registrado de servicios GATT (consulta aioble.register_services()) y una corrutina aioble.advertise() en ejecución. En el lado del central: un iterador aioble.scan() en ejecución o un aioble.Device.connect() pendiente. La radio multiplexa el trabajo; la aplicación ve cada rol como una tarea independiente.

11.8.3. Un periférico mínimo

El programa aioble útil más pequeño – un periférico que anuncia una única característica de solo lectura – es corto:

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())

Un central que no hace más que conectarse y leer una vez es igualmente corto:

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 programas tienen unas quince líneas y cubren todo el flujo desde «la radio está apagada» hasta «trabajo útil hecho».

11.8.4. Apagar la radio

En una cámara alimentada por batería, la radio BLE es el mayor consumo discrecional del presupuesto. Dos perillas importan.

La primera es implícita: aioble activa la radio en el primer uso, y la radio duerme entre eventos programados (ráfagas de anuncio, eventos de conexión, ventanas de escaneo) automáticamente. Elegir intervalos más largos en aioble.advertise() / aioble.scan() y acordar un intervalo de conexión más largo en el momento de connect() mantiene la radio apagada proporcionalmente más tiempo. La tabla de anuncios en Anuncio y escaneo es la guía práctica aquí.

La segunda es el apagado 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() desactiva la radio BLE subyacente y desmantela todo lo que esté en marcha – las conexiones abiertas caen, los escáneres y los anunciantes se cancelan, los canales L2CAP se cierran. Las corrutinas que estaban esperando esas operaciones lanzan sus excepciones habituales (DeviceDisconnectedError y compañía), que es el mecanismo de limpieza para el que se escribieron los bloques async with circundantes. Llamar a cualquier corrutina de aioble después activa la radio de nuevo desde cero.

El patrón típico para una cámara de sensor periódica alimentada por batería es:

  • Despertar según un horario (temporizador, sensor de movimiento, botón).

  • Ejecutar la ráfaga de trabajo BLE – anunciar, aceptar una conexión, enviar el valor, desconectar.

  • Llamar a aioble.stop() y dormir hasta el siguiente despertar.

11.8.5. Lo que aioble no hace

aioble cubre deliberadamente GATT, GAP y L2CAP – las capas que usa una aplicación. Tres piezas quedan fuera de su alcance:

  • Cualquier cosa por debajo de la capa de enlace. La selección de canales, el salto de frecuencia, los acuses de recibo de paquetes y el cifrado a nivel de enlace ocurren todos dentro del puerto BLE y el silicio del controlador; aioble no expone ganchos a ese nivel.

  • Bluetooth clásico. aioble es solo BLE. Los enlaces de audio, RFCOMM, A2DP y otras funciones de perfil clásico no forman parte de la API.

  • Bluetooth Mesh. La capa de red en malla del Bluetooth SIG (una pila independiente sobre el anuncio BLE) no está implementada en la cámara. La cámara puede anunciarse y observar, pero no puede participar en los roles de relevo / amigo / proxy de una red en malla.

11.8.6. Excepciones

Del aioble salen cuatro tipos de excepción. Cada una se dispara desde dentro de una corrutina que estaba esperando una operación cuando algo salió mal; los bloques async with se desenrollan limpiamente cuando se propagan.

  • aioble.DeviceDisconnectedError – el enlace BLE con el par cayó mientras una operación GATT (read, write, notified, indicated, subscribe, exchange_mtu, …) estaba en marcha. Se lanza dentro de la corrutina que estuviera esperando. Es con diferencia la excepción más común; captúrala en cualquier código que deba reconectarse ante una pérdida.

  • aioble.GattError – una operación GATT alcanzó al par pero se completó con un estado ATT distinto de cero (escritura con respuesta rechazada, indicación no reconocida, lectura no permitida, …). El código de estado está en el atributo _status de la excepción.

  • aioble.L2CAPDisconnectedError – el canal L2CAP cayó mientras un send(), recvinto() o flush() estaba en marcha. Cualquiera de los lados puede haber cerrado el canal, o la conexión GAP subyacente desapareció.

  • aioble.L2CAPConnectionError – lanzada por l2cap_connect() cuando el oyente rechazó la conexión o el controlador falló al configurar el canal. El código de estado de Bluetooth es el primer argumento posicional.

Las operaciones que toman un timeout_ms explícito (las llamadas de conexión / descubrimiento / lectura / escritura / emparejamiento, más timeout() como envoltorio) lanzan además asyncio.TimeoutError de asyncio cuando el plazo se cumple antes de que la operación se complete.