前两节中,我们首先直接用实际例程,体验了MicroPython中蓝牙BLE的实际运行效果,接着分析了其主要的程序代码逻辑,详见[MicroPython蓝牙BLE例程实操(一) ]和[MicroPython蓝牙BLE例程实操(二) ]。
其中,构建蓝牙BLE广播报文的函数——advertising_payload是在单独的模块(.py文件)中自己实现的。由此,也从另外一个角度反映出,MicroPython只提供了蓝牙BLE的“低阶”接口:“低阶”到广播报文都需要自己实现,而非系统模块集成。当然,考虑到MicoPython作为一个通用的平台,又运行于资源有限的微控制器上,其只负责最核心的功能,而将业务场景有关的代码实现剥离出来由用户自己实现,似乎也是无可厚非的了。
两个蓝牙设备想要建立连接,首先需要外设设备向外广播,然后中心设备才能搜索到该设备,再发起连接请求。外设设备的广播报文中包含设备的相关信息,比如设备名称,设备具有的服务UUID等等,中心设备可以根据这些信息决定其是不是自己关心的设备,以及要不要发起对该设备的连接请求。
基本的广播数据包格式如下:
每个广播数据包都是31字节,数据包中又分为有效(significant)数据部分和无效(non-significant)数据部分。其中,无效数据部分全为零,仅仅是为了凑够31字节而存在。而有效数据部分,又由若干个数据单元组成,每个数据单元的格式为:
1字节长度+n字节(通常也是1字节)类型+n字节类型特定数据
这里的类型,用于表明这个数据单元代表什么,比如标记(Flag),设备名,或者UUID等等。我们来看例程中是怎么实现的:
from micropython import const
import struct
import bluetooth
# Advertising payloads are repeated packets of the following form:
# 1 byte data length (N + 1)
# 1 byte type (see constants below)
# N bytes type-specific data
#常量定义
_ADV_TYPE_FLAGS = const(0x01)
_ADV_TYPE_NAME = const(0x09)
_ADV_TYPE_UUID16_COMPLETE = const(0x3)
_ADV_TYPE_UUID32_COMPLETE = const(0x5)
_ADV_TYPE_UUID128_COMPLETE = const(0x7)
_ADV_TYPE_UUID16_MORE = const(0x2)
_ADV_TYPE_UUID32_MORE = const(0x4)
_ADV_TYPE_UUID128_MORE = const(0x6)
_ADV_TYPE_APPEARANCE = const(0x19)
#构建广播数据包
def advertising_payload(limited_disc=False, br_edr=False, name=None, services=None, appearance=0):
payload = bytearray()
#构建广播数据单元
def _append(adv_type, value):
nonlocal payload
payload += struct.pack("BB", len(value) + 1, adv_type) + value
#标记类型数据单元
_append(
_ADV_TYPE_FLAGS,
struct.pack("B", (0x01 if limited_disc else 0x02) + (0x18 if br_edr else 0x04)),
)
#设备名类型数据单元
if name:
_append(_ADV_TYPE_NAME, name)
#服务UUID类型数据单元
if services:
for uuid in services:
b = bytes(uuid)
if len(b) == 2:
_append(_ADV_TYPE_UUID16_COMPLETE, b)
elif len(b) == 4:
_append(_ADV_TYPE_UUID32_COMPLETE, b)
elif len(b) == 16:
_append(_ADV_TYPE_UUID128_COMPLETE, b)
#外观类型数据单元
# See org.bluetooth.characteristic.gap.appearance.xml
if appearance:
_append(_ADV_TYPE_APPEARANCE, struct.pack("<h", appearance))
return payload
Python中struct模块对应C语言中的结构体,其可将结构体中的数据打包成字节串。比如上述代码中,struct.pack函数第一个参数为格式字符串,其表示该结构体是如何构建的:"B"表示其中包含一字节数据,而"BB"则表示其中包含两字节数据。上述代码中,定义了_append子函数用于构建一个一个的广播数据单元,其格式正好与前面提到的标准广播数据单元的格式相符合。
上述广播数据包中,包含的第一个数据单元为“标记(Flag)”类型数据单元。蓝牙BLE可用该类型数据单元表明设备是有限可发现(LE Limited Discoverable)还是普通可发现(General Discoverable),还可表明设备是支持双模(同时支持经典蓝牙和蓝牙BLE)还是不支持经典蓝牙(BR/EDR模式),仅支持蓝牙BLE。之后,依据参数传递情况决定是否加入设备名数据单元,服务UUID数据单元,和设备外观数据单元。
(注:所谓有限可发现是指该设备发送广播报文时,只是在每个周期中的一段时间之内广播,其余时间不广播。而常规可发现是指该设备没有时间限制,一直发送广播报文。)
设备名比较容易理解,上述服务UUID的定义中都有COMPLETE后缀,那么还有对应INCOMPLETE的类型吗?确实如此。如果该设备有两个服务,只广播了其中一个,那么就是INCOMPLETE的,否则,就是COMPLETE的。那外观(APPEARANCE)又是什么呢,其实就是告诉接收广播报文的设备,本设备应该用什么样的图标外观进行显示,是用一个耳机的图标,还是用一个HID键盘的图标等等。
如此看来,蓝牙规范确实详实啊,基本所有细节都有考虑!这也是其保证各种设备能够广泛兼容的一个措施:自定义功能特性越少,越能保证规范范围内各设备之间的兼容性。
该模块中还有其它函数如下:
#解析payload中指定adv_type的内容
def decode_field(payload, adv_type):
i = 0
result = []
while i + 1 < len(payload):
if payload[i + 1] == adv_type:
result.append(payload[i + 2 : i + payload[i] + 1])
i += 1 + payload[i]
return result
#解析设备名,以字符串形式返回
def decode_name(payload):
n = decode_field(payload, _ADV_TYPE_NAME)
return str(n[0], "utf-8") if n else ""
#解析服务
def decode_services(payload):
services = []
for u in decode_field(payload, _ADV_TYPE_UUID16_COMPLETE):
services.append(bluetooth.UUID(struct.unpack("<h", u)[0]))
for u in decode_field(payload, _ADV_TYPE_UUID32_COMPLETE):
services.append(bluetooth.UUID(struct.unpack("<d", u)[0]))
for u in decode_field(payload, _ADV_TYPE_UUID128_COMPLETE):
services.append(bluetooth.UUID(u))
return services
#构造单元测试函数,以验证本模块其它函数工作是否正常
def demo():
payload = advertising_payload(
name="micropython",
services=[bluetooth.UUID(0x181A), bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")],
)
print(payload)
print(decode_name(payload))
print(decode_services(payload))
#作为模块单独运行时的主入口
if __name__ == "__main__":
demo()
上述函数基本都是配合advertising_payload函数,用于验证其是否工作正常。如果直接运行该模块,__name__系统变量为__main__,从最后面的代码来看,则直接运行的是demo函数,其使用advertising_payload函数构造了一个广播报文,并进行各个域的解析,以验证所构造的报文是否正确。从单元测试的角度来讲,这也可以说是Python编程中一种比较常规的范式了。