14.8. The aioble module

The Bluetooth Core specification gives a vocabulary that maps onto two MicroPython modules.

  • bluetooth – the low-level binding to the BLE controller. Synchronous, event-driven through an IRQ-style callback, and structured around byte buffers, handles, and the bare GATT primitives. It exposes the protocol as it is, not as Python applications want to consume it.

  • aioble – a higher-level wrapper, written in Python on top of bluetooth, that turns every remote operation into an asyncio coroutine and every BLE object (services, characteristics, connections, scan results, L2CAP channels) into an ergonomic Python class. Scans become async iterators; connections become async context managers; notifications become awaitable.

14.8.1. When to reach for the lower-level module

bluetooth is still the right answer for two narrow cases:

  • You are writing the kind of code aioble itself is built out of – a new pattern that needs IRQ-level control over the protocol.

  • You are running on a hardware target where the aioble package is not available, and a thin shim around the controller is the only option.

For every camera application, aioble is the right answer.

14.8.2. Pieces of an aioble program

Every aioble-based application has a small set of moving parts, regardless of which roles it plays.

  • A long-running asyncio event loop. Everything in aioble is a coroutine, so the application is structured as one or more tasks on a single event loop. See Asyncio for the loop, tasks, and exceptions in detail.

  • A radio that is on. aioble activates the BLE radio implicitly on first use, but it can also be controlled explicitly with aioble.config() (which forwards to bluetooth.BLE.config() after ensuring the radio is up) and shut down with aioble.stop().

  • One or more roles in flight at once. On the peripheral side: a registered set of GATT services (see aioble.register_services()) and a running aioble.advertise() coroutine. On the central side: a running aioble.scan() iterator or a pending aioble.Device.connect(). The radio multiplexes the work; the application sees each role as an independent task.

14.8.3. A minimal peripheral

The smallest useful aioble program – a peripheral that advertises a single read-only characteristic – is short:

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

A central that does nothing more than connect and read once is similarly short:

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

Both programs are about fifteen lines and they cover the whole flow from “radio is off” to “useful work done”.

14.8.4. Turning the radio off

On a battery-powered camera the BLE radio is the biggest discretionary draw on the budget. Two knobs matter.

The first is implicit: aioble activates the radio on first use, and the radio sleeps between scheduled events (advertising bursts, connection events, scan windows) automatically. Picking longer intervals on aioble.advertise() / aioble.scan() and agreeing on a longer connection interval at connect() time keeps the radio off proportionally more of the time. The advertising table in Advertising and scanning is the practical guide here.

The second is explicit shutdown:

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() deactivates the underlying BLE radio and tears down anything in flight – open connections drop, scanners and advertisers cancel, L2CAP channels close. Coroutines that were waiting on those operations raise their usual exceptions (DeviceDisconnectedError and friends), which is the cleanup mechanism the surrounding async with blocks were written for. Calling any aioble coroutine afterwards activates the radio again from cold.

The typical pattern for a periodic battery-powered sensor cam is:

  • Wake on a schedule (timer, motion sensor, button).

  • Run the burst of BLE work – advertise, accept a connection, push the value, disconnect.

  • Call aioble.stop() and sleep until the next wake.

14.8.5. What aioble does not do

aioble deliberately covers GATT, GAP, and L2CAP – the layers an application uses. Three pieces are out of scope:

  • Anything below the link layer. Channel selection, frequency hopping, packet acknowledgements, and link-layer encryption all happen inside the BLE port and the controller silicon; aioble does not expose hooks at that level.

  • Classic Bluetooth. aioble is BLE-only. Audio links, RFCOMM, A2DP, and other classic-profile features are not part of the API.

  • Bluetooth Mesh. The Bluetooth SIG’s mesh networking layer (a separate stack on top of BLE advertising) is not implemented on the camera. The cam can advertise and observe, but it cannot participate in a mesh network’s relay / friend / proxy roles.

14.8.6. Exceptions

Four exception types come out of aioble. Each fires from inside a coroutine that was awaiting an operation when something went wrong; async with blocks unwind cleanly when they propagate.

  • aioble.DeviceDisconnectedError – the BLE link to the peer dropped while a GATT operation (read, write, notified, indicated, subscribe, exchange_mtu, …) was in flight. Raised inside whichever coroutine was waiting. The most common exception by far; catch it in any code that should reconnect on loss.

  • aioble.GattError – a GATT operation reached the peer but completed with a non-zero ATT status (write-with-response rejected, indicate not acknowledged, read-not-permitted, …). The status code is on the exception’s _status attribute.

  • aioble.L2CAPDisconnectedError – the L2CAP channel dropped while a send(), recvinto(), or flush() was in flight. Either side may have closed the channel, or the underlying GAP connection went away.

  • aioble.L2CAPConnectionError – raised by l2cap_connect() when the listener refused or the controller failed the channel setup. The Bluetooth status code is the first positional argument.

Operations that take an explicit timeout_ms (the connect / discovery / read / write / pair calls, plus timeout() as a wrapper) additionally raise asyncio.TimeoutError from asyncio when the deadline elapses before the operation completes.