11.7. GATT 操作

一个特征(characteristic)在 GATT 数据库中只是作为一个命名的值存在。真正让它有用的,是客户端可以对它执行的那一小组定义良好的操作。每个特征都通过一个 属性位掩码(property bitmask) 声明它支持哪些操作——没有内容要暴露的服务端可以发布一个只读值,控制寄存器可能是只写的,而流式推送更新的传感器则会设置 notify 位。客户端在发现过程中获取该位掩码并加以遵守。

这五种操作是 读(read)写(write)无响应写(write without response)通知(notify)指示(indicate)。它们分为两组——拉取(客户端发起请求)和推送(服务端主动发送)。

11.7.1. 拉取:读和写

这两种是最简单的,看起来就和函数调用一模一样。

  • 读。 客户端请求当前值,服务端将其回送。一次往返,客户端拿到服务端为该特征设置的字节内容,而服务端不会得知是谁读取了它。

  • 写。 客户端发送新字节,服务端将其存储(并可选地对新值运行应用逻辑)。存在两种形式:

    • 带响应写 ——服务端进行确认,并在状态非零时引发相应的应用错误。可靠,一次往返。

    • 无响应写 ——服务端静默地存储这些字节;客户端完全不会收到任何确认。更快(无需等待确认的往返),适用于流式传输,代价是只能通过旁路回读才能发现错误。

aioble 中,客户端侧 API 将这一选择隐藏在单一的 aioble.ClientCharacteristic.write() 方法之后,通过一个 response 关键字控制(True / False / None,根据对端所通告的能力自动选择)。

11.7.2. 推送:通知和指示

拉取模型对传感器数据是不合适的。一条心率带如果需要手机每秒轮询一次,就会在上百次无用的无线电事件上耗尽电量;而只在有了新读数时才推送数值的方式,正是 BLE 一开始的意义所在。

GATT 用 服务端发起 的操作解决了这个问题。客户端 订阅(subscribe) 一个特征;自此之后,每当服务端更新该值,新值就会通过链路推送给客户端。有两种形式:

  • 通知。 发后即忘。服务端将一条通知排入队列,链路层在下一个连接事件期间将其发送,客户端予以接收。在 GATT 层没有任何确认;链路层正常的重传机制会处理无线电侧的丢包,但应用程序不会看到该值已被处理的任何确认。

  • 指示。 服务端发送一条通知,并且 在发送下一条之前等待客户端在 GATT 层的确认。每次只能有一条指示在途。当服务端需要确知客户端确实看到了该值时使用——例如一个关键报警特征、一次配置确认。

服务端与客户端的两幅并排示意图。左侧, 客户端发送 "read",服务端以该值回复。 连续三次读取,每对箭头对应一次。右侧, 客户端只发送一次 "subscribe",随后 服务端在它选择的时刻推送三个 "notify" 数据包, 其间没有任何客户端请求。

拉取(读)与推送(通知)的对比。使用通知时,客户端只需订阅一次,服务端便会在数值变化时随时推送新值。

订阅是通过向附加在特征上的一个描述符写入数据来完成的——即 客户端特征配置描述符(Client Characteristic Configuration Descriptor)(CCCD,0x2902)。写入 0x0001 启用通知,写入 0x0002 启用指示,写入 0x0000 则两者都禁用。aioble.ClientCharacteristic.subscribe() 方法会替你执行这次写入,并提供 notify=Trueindicate=True 关键字标志。

订阅之后,客户端使用 notified()indicated() 等待传入的推送——两者都是异步协程,会一直挂起直到下一次推送到达。

11.7.3. MTU 决定有效载荷大小

每一种操作都受到连接在链路建立时敲定的协商 MTU 的约束。默认 MTU 为 23 字节,在扣除 GATT 头部之后留给特征值字节的只有 20 字节。任何大于此的内容要么必须装入一个更大的 MTU(通过 aioble.DeviceConnection.exchange_mtu() 向上协商,在摄像头上最高可达 512 字节),要么必须拆分到多个特征或多条通知中。

由客户端发起的、对大于 MTU 的值的读写,会由 GATT 的 长(long) 流程在幕后处理(Read Long / Prepare-Write + Execute-Write);aioble 透明地运行这些流程,因此以超大值调用 read() / write() 只是会多花一些往返而已。由服务端发起的通知和指示则 不会 被分片——一次推送以 MTU 为上限,应用程序需自行将任何更大的内容拆分为多条通知,或者干脆不使用 GATT。

对于真正的大数据传输——一帧捕获的图像、一批测量数据、一个固件 blob——正确的做法通常是彻底脱离 GATT,转而使用 L2CAP 信道(参见 L2CAP 通道)。

11.7.4. 两侧一览

这五种操作在连接的每一侧暴露方式各不相同: