11.8. Модуль aioble

Спецификация Bluetooth Core даёт словарь терминов, который отображается на два модуля MicroPython.

  • bluetoothнизкоуровневая привязка к контроллеру BLE. Синхронная, управляемая событиями через callback в стиле IRQ и построенная вокруг байтовых буферов, дескрипторов и голых примитивов GATT. Она предоставляет протокол как он есть, а не так, как хотят его использовать приложения на Python.

  • aioble – более высокоуровневая обёртка, написанная на Python поверх bluetooth, которая превращает каждую удалённую операцию в корутину asyncio, а каждый объект BLE (сервисы, характеристики, подключения, результаты сканирования, каналы L2CAP) – в эргономичный класс Python. Сканирования становятся асинхронными итераторами; подключения становятся асинхронными контекстными менеджерами; уведомления становятся ожидаемыми.

11.8.1. Когда обращаться к низкоуровневому модулю

bluetooth по-прежнему остаётся правильным выбором в двух узких случаях:

  • Вы пишете тот тип кода, из которого построен сам aioble – новый паттерн, требующий управления протоколом на уровне IRQ.

  • Вы работаете на аппаратной платформе, где пакет aioble недоступен, и тонкая прослойка вокруг контроллера – единственный вариант.

Для любого приложения камеры aioble – правильный выбор.

11.8.2. Составные части программы на aioble

Каждое приложение на основе aioble имеет небольшой набор подвижных частей, независимо от того, какие роли оно играет.

  • Долгоживущий цикл событий asyncio. Всё в aioble является корутиной, поэтому приложение структурировано как одна или несколько задач в одном цикле событий. Подробнее о цикле, задачах и исключениях см. Asyncio.

  • Включённый радиомодуль. aioble неявно активирует радиомодуль BLE при первом использовании, но им также можно управлять явно с помощью aioble.config() (которая перенаправляет вызов в bluetooth.BLE.config() после того, как убедится, что радиомодуль включён) и отключать с помощью aioble.stop().

  • Одна или несколько ролей в работе одновременно. На стороне периферийного устройства: зарегистрированный набор GATT-сервисов (см. aioble.register_services()) и запущенная корутина aioble.advertise(). На стороне центрального устройства: запущенный итератор aioble.scan() или ожидающий aioble.Device.connect(). Радиомодуль мультиплексирует работу; приложение видит каждую роль как независимую задачу.

11.8.3. Минимальное периферийное устройство

Наименьшая полезная программа на aioble – периферийное устройство, рекламирующее одну характеристику только для чтения – коротка:

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

Центральное устройство, которое не делает ничего, кроме как подключается и читает один раз, столь же коротко:

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

Обе программы занимают около пятнадцати строк и охватывают весь процесс от «радиомодуль выключен» до «полезная работа выполнена».

11.8.4. Выключение радиомодуля

На камере с питанием от батареи радиомодуль BLE – самый крупный произвольный потребитель в энергобюджете. Важны две регулировки.

Первая неявная: aioble активирует радиомодуль при первом использовании, а радиомодуль автоматически засыпает между запланированными событиями (всплесками рекламы, событиями подключения, окнами сканирования). Выбор более длительных интервалов в aioble.advertise() / aioble.scan() и согласование более длительного интервала подключения во время connect() пропорционально дольше держит радиомодуль выключенным. Таблица рекламы в Реклама и сканирование – практическое руководство по этому вопросу.

Вторая – явное выключение:

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() деактивирует лежащий в основе радиомодуль BLE и разрушает всё, что находится в работе – открытые подключения разрываются, сканеры и рекламодатели отменяются, каналы L2CAP закрываются. Корутины, которые ожидали этих операций, возбуждают свои обычные исключения (DeviceDisconnectedError и аналогичные), что является механизмом очистки, для которого были написаны окружающие блоки async with. Вызов любой корутины aioble после этого снова активирует радиомодуль с нуля.

Типичный паттерн для периодической сенсорной камеры с питанием от батареи таков:

  • Пробуждение по расписанию (таймер, датчик движения, кнопка).

  • Выполнение всплеска работы BLE – реклама, приём подключения, отправка значения, отключение.

  • Вызов aioble.stop() и сон до следующего пробуждения.

11.8.5. Чего aioble не делает

aioble сознательно охватывает GATT, GAP и L2CAP – уровни, которые использует приложение. Три части находятся вне области охвата:

  • Всё, что ниже канального уровня. Выбор канала, скачкообразная перестройка частоты, подтверждения пакетов и шифрование канального уровня – всё это происходит внутри порта BLE и кремния контроллера; aioble не предоставляет хуков на этом уровне.

  • Классический Bluetooth. aioble работает только с BLE. Аудиоканалы, RFCOMM, A2DP и другие возможности классических профилей не являются частью API.

  • Bluetooth Mesh. Уровень ячеистой сети Bluetooth SIG (отдельный стек поверх рекламы BLE) не реализован на камере. Камера может рекламировать и наблюдать, но она не может участвовать в ролях ретранслятора / друга / прокси ячеистой сети.

11.8.6. Исключения

Из aioble выходят четыре типа исключений. Каждое возникает изнутри корутины, которая ожидала операцию, когда что-то пошло не так; блоки async with корректно сворачиваются при их распространении.

  • aioble.DeviceDisconnectedError – BLE-канал к пиру был разорван, пока выполнялась операция GATT (read, write, notified, indicated, subscribe, exchange_mtu, …). Возбуждается внутри той корутины, которая ожидала. Безусловно, самое частое исключение; перехватывайте его в любом коде, который должен переподключаться при потере.

  • aioble.GattError – операция GATT достигла пира, но завершилась с ненулевым статусом ATT (запись с ответом отклонена, индикация не подтверждена, чтение не разрешено, …). Код статуса находится в атрибуте _status исключения.

  • aioble.L2CAPDisconnectedError – канал L2CAP был разорван, пока выполнялись send(), recvinto() или flush(). Канал мог закрыть любая из сторон, либо исчезло лежащее в основе GAP-подключение.

  • aioble.L2CAPConnectionError – возбуждается методом l2cap_connect(), когда слушатель отказал или контроллер не смог настроить канал. Код статуса Bluetooth является первым позиционным аргументом.

Операции, принимающие явный timeout_ms (вызовы connect / discovery / read / write / pair, а также timeout() в качестве обёртки), дополнительно возбуждают asyncio.TimeoutError из asyncio, когда крайний срок истекает до завершения операции.