11.8. aioble 模块¶
Bluetooth Core 规范提供了一套术语,它们对应到两个 MicroPython 模块。
bluetooth——到 BLE 控制器的底层绑定。它是同步的,通过 IRQ 风格的回调进行事件驱动,并围绕字节缓冲区、句柄和裸 GATT 原语来组织。它将协议原样暴露出来,而不是以 Python 应用希望使用的方式呈现。aioble——一个更高层的封装,用 Python 编写在bluetooth之上,它将每个远程操作变成一个asyncio协程,并把每个 BLE 对象(服务、特征、连接、扫描结果、L2CAP 通道)变成符合人体工学的 Python 类。扫描变成异步迭代器;连接变成异步上下文管理器;通知变成可等待对象。
11.8.1. 何时使用底层模块¶
在两种狭窄的情形下,bluetooth 仍然是正确的选择:
对于每一个摄像头应用而言,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——应用所使用的那些层。有三块内容不在其范围之内:
11.8.6. 异常¶
aioble 会抛出四种异常类型。每种都来自某个在出错时正在等待某操作的协程;当它们传播时,async with 代码块会干净地展开退出。
aioble.DeviceDisconnectedError——在某个 GATT 操作(read、write、notified、indicated、subscribe、exchange_mtu等)进行过程中,到对端的 BLE 链路断开了。它在正在等待的那个协程内部抛出。这是迄今最常见的异常;在任何应在连接丢失时重连的代码中都要捕获它。aioble.GattError——某个 GATT 操作到达了对端,但以非零的 ATT 状态完成(带响应的写入被拒绝、指示未被确认、读取不被允许等)。状态码在该异常的_status属性上。aioble.L2CAPDisconnectedError——在某个send()、recvinto()或flush()进行过程中,L2CAP 通道断开了。可能是任一侧关闭了通道,或者底层的 GAP 连接消失了。aioble.L2CAPConnectionError——当监听端拒绝或控制器未能完成通道建立时,由l2cap_connect()抛出。Bluetooth 状态码是第一个位置参数。
接受显式 timeout_ms 的操作(连接 / 发现 / 读 / 写 / 配对调用,以及作为包装器的 timeout())在操作完成前截止期限到期时,还会从 asyncio 抛出 asyncio.TimeoutError。