11.6. 服务与特征

一旦 GAP 让两台设备建立起一个开放连接,它上面的那一层——通用属性配置文件(Generic Attribute Profile,GATT)——就必须赋予流经该连接的字节以意义。BLE 在这里的选择有些不同寻常。TCP 暴露的是一条原始字节流,把如何分帧的工作留给应用自行设计;而 GATT 暴露的则是一个小型键/值数据库,由一方托管,另一方对其进行读取、写入或订阅。

应用设计者在 BLE 上花费的大部分时间,正是在思考这个数据库。摄像头向手机发布什么、它在远程传感器上监视什么、蓝牙键盘如何告诉主机按下了哪个键——这些全都是某处某个 GATT 数据库中的特征值。

11.6.1. 两条角色轴,而非一条

一个常见的困惑来源是:外设(peripheral)/ 中心(central)与服务端(server)/ 客户端(client)是两条相互独立的轴,而非同义词。

  • 外设中心是 GAP 角色,在建立连接时确定。外设进行广播并被连接;中心进行扫描并发起连接。这在链路建立的那一刻就已确定,之后不再改变。

  • 服务端客户端是 GATT 角色,针对每一次特征操作而定。服务端托管特征;客户端对它进行读取、写入或订阅。

规范将这两条轴解耦开来。外设通常是服务端(心率带发布它的读数),中心通常是客户端(手机读取这些读数),但 BLE 允许任意组合——外设可以在刚连接上的中心上发现某个特征,或者单个连接也可以同时在两端托管服务。

大多数摄像头应用都遵循传统配对方式(外设 + 服务端,或中心 + 客户端),因此在描述这种传统情形时,本节其余部分会把它们当作一条轴来对待。当这种区分确有意义时,会把两个术语都明确写出来。

11.6.2. 数据库内部

GATT 数据库是一棵树。叶子节点承载着实际的字节。分支节点则把相关的叶子归组为对人有意义的单元。

一棵树,顶层节点标记为 "GATT database"。其下有三个 Service 节点, 分别标记为 "Generic Access (0x1800)"、"Battery (0x180F)" 和 "Environmental Sensing (0x181A)"。每个 Service 都有子级 Characteristic 节点;Battery 服务有 "Battery Level (0x2A19)",其下有一个子级 Descriptor "CCCD"。Environmental Sensing 服务有 "Temperature (0x2A6E)" 和 "Humidity (0x2A6F)"。

一个 GATT 数据库。服务对特征进行归组;特征承载应用的字节;描述符则承载关于特征的元数据。

节点共有三种:

  • 服务(service)是一组相关值的逻辑分组。Bluetooth SIG 为常见用例发布了标准服务定义——电量用的 Battery Service、温度/湿度/气压用的 Environmental Sensing、心率监测器用的 Heart Rate——这样手机上的通用应用就能识别出它从未见过的服务。应用也可以自由地为 SIG 尚未标准化的事物定义自己的服务。

  • 特征(characteristic)是服务内部一个具名的值。Battery 服务只有一个特征——Battery Level,即一个字节的百分比。Environmental Sensing 则为温度、湿度、气压等分别设有特征。特征是 GATT 操作的单位——你读取一个特征、写入一个特征、订阅一个特征。

  • 描述符(descriptor)是附加在特征上的元数据。有些描述符是标准化的——其中最著名的就是 Client Characteristic Configuration Descriptor(CCCD),因为客户端正是通过向它写入来告诉服务端“在此特征上给我发送通知”。另一些描述符则是用户自定义的,承载着诸如呈现格式或扩展属性之类的内容。

GATT 服务端(通常是外设)在启动时声明一次它的数据库,运行期间该数据库不再改变。GATT 客户端(通常是中心)则在连接之后发现数据库中的内容——遍历这棵树,读取它所找到的服务的 UUID,再读取每个服务内部的特征。

11.6.3. UUID

每个服务、特征和描述符都有一个 UUID(Universally Unique IDentifier,全局唯一标识符),用于标识它是哪一类事物。UUID 有三种宽度:

  • 16 位。保留给 Bluetooth SIG 定义的标准。Battery Service 是 0x180F。Battery Level(一个特征)是 0x2A19。完整列表发布在 Bluetooth SIG 的已分配编号网站上:https://www.bluetooth.com/specifications/assigned-numbers/

  • 32 位。一种很少使用的中间方案。

  • 128 位。其他所有人都用这种——厂商或应用随机生成一个,用于自己的自定义服务或特征。定义自有协议的摄像头就属于这一类。

bluetooth.UUID 类接受三种宽度中的任意一种:

import bluetooth

BATTERY_SERVICE = bluetooth.UUID(0x180F)
CUSTOM_SERVICE = bluetooth.UUID("12345678-1234-5678-9abc-def012345678")

16 位 UUID 编码后只占用很小的广播载荷,这也是当存在标准服务时应优先选用它的原因之一——广播 0x180D(Heart Rate)的心率带只需两个字节,而一个自定义 UUID 则要花费十六个字节。对于不需要标准互操作性的应用来说,生成一个 128 位 UUID 才是正确答案。

11.6.4. SIG 标准化服务能为你带来什么

使用标准服务的理由很简单:现有应用已经知道如何与它通信。一台广播 Heart Rate 服务(0x180D)并暴露 Heart Rate Measurement 特征(0x2A37)的设备,无需任何人编写新代码就能与全世界每一款健身应用配合工作。而一台用自定义 UUID 重新实现相同数据的设备,则需要自己的配套应用和自己的协议文档。

标准确实是有代价的。每个特征内部的字节布局都是被规定好的——SIG 决定 Heart Rate Measurement 是一个单字节的标志位字段,后跟一个 8 位或 16 位的心率值,可选地再跟 R-R 间隔——符合规范的设备必须遵循这些布局。而自定义服务则不受这一约束。

对摄像头而言务实的答案是:当你拥有的那类数据存在对应的标准服务时(Battery Service、Environmental Sensing),就使用标准服务;对于任何特定于你应用的东西,则用 128 位 UUID 定义一个自定义服务。

11.6.5. 服务端与客户端对象

对于相同的概念性构建模块(服务、特征、描述符),每个 GATT 库都会暴露两组并行的对象: