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 层的确认。每次只能有一条指示在途。当服务端需要确知客户端确实看到了该值时使用——例如一个关键报警特征、一次配置确认。
拉取(读)与推送(通知)的对比。使用通知时,客户端只需订阅一次,服务端便会在数值变化时随时推送新值。¶
订阅是通过向附加在特征上的一个描述符写入数据来完成的——即 客户端特征配置描述符(Client Characteristic Configuration Descriptor)(CCCD,0x2902)。写入 0x0001 启用通知,写入 0x0002 启用指示,写入 0x0000 则两者都禁用。aioble.ClientCharacteristic.subscribe() 方法会替你执行这次写入,并提供 notify=True 和 indicate=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. 两侧一览¶
这五种操作在连接的每一侧暴露方式各不相同:
在 服务端(即常见布局中的外围设备)上:
aioble.Characteristic.read()——从 GATT 数据库中读取当前的本地值(即“客户端会看到什么”的服务端侧)。aioble.Characteristic.write()——更新本地值,并可选地将该更新推送给每一个已订阅的客户端。aioble.Characteristic.notify()/indicate()——向某个特定客户端发送一次推送。aioble.Characteristic.written()——等待来自任一客户端的下一次传入写入。aioble.Characteristic.on_read()——在客户端读取时同步调用的回调,可用于按需计算一个值。
在 客户端(即常见布局中的中央设备)上:
aioble.ClientCharacteristic.read()——向服务端请求当前值。aioble.ClientCharacteristic.write()——发送一个新值,可带或不带响应。aioble.ClientCharacteristic.subscribe()——启用 / 禁用通知和指示。aioble.ClientCharacteristic.notified()/indicated()——等待下一次推送。