蓝牙4.0标准新增了Low Energy标准部分,带了两个全新的核心协议:ATT (Attribute Protocol)和GATT (Generic Attribute Profile)。这两个协议主要规范了低功耗部分,所有低功耗(LE)相关profile都应该使用它们。但是这两个协议也可以供经典蓝牙(Bluetooth (BR/EDR))使用。
概述
ATT是一个应用层线路协议(wire application protocol),GATT在ATT的基础上,规定了在service中使用ATT的方法。所有LE profile都必须基于GATT协议,所以所有的LE service都使用了ATT作为应用层协议。
规定profile使用这两个协议有以下好处:
- 开发和实现新的LE profile时更为便捷,因为不需要重新开发线路协议。
- ATT对LE设备进行了专门的优化:使用更少的字节传输数据,而且使用了定长结构(fixed-size structures)的PDU。
- ATT/GATT结构简单,意味着固件层可以分担部分ATT/GATT的实现,减少了微控制器在软件层面上的麻烦。
- 软件协议栈方面,ATT/GATT may be implemented only once in stack itself, saving applications from the trouble.(待翻译)
- 有一些profiles不适合使用ATT/GATT作为应用协议,但在ATT通道中存在另外一条L2CAP线路,可以用其实现profile-specific协议。
接下来我们更深入地了解这两个协议。
ATT: Attribute Protocol
属性(attribute)是ATT的基石。一个attribute由三种元素组成:
- 一个16位的句柄(handle)
- 一个UUID,定义了attribute的**类型**
- 一个定长的值(value)
从ATT的角度来看,value的内容是未知(amorphous)的,value只是一个定长的字节数组。value的意义完全由UUID决定,而且ATT不会检查value的长度是否与UUID匹配。attribute的handle具有唯一性,仅用作区分不用的attribute(因为可能有很多不同的attribute拥有相同的UUID)。ATT不定义任何UUID(的含义),全部由GATT或者高层profile来规定。
ATT server负责存储attribute。ATT client不存储attribute,client通过ATT线路协议读写server上attribute的value。每个attribute可带权限认证功能。认证内容存储在value里面,并由高层协议定义。ATT本身不了解value的内容,而且不会尝试解析value来进行权限认证。认证功能由GATT负责。
ATT线路协议有一些很好的特性,例如可以通过UUID来搜索attribute,通过指定handle的范围来批量获得attribute等,所以client端不需要事先知道handle具体值,高层协议也不需要硬编码handle相关部分。
但handle的值对于给定的设备来说应该是稳定的。这样子client端可以对其进行缓存,以在第一次发现设备后,使用更短的数据包(消耗更少的电量)来获取attribute。高层协议规定了server端的attribute发生了变化时,如何将消息通知到client端(例如固件升级完成通知)。
大多数ATT协议都是基于简单的client-server模式:client负责请求,server负责应答。但ATT还有两个特性:notification和indication。这意味着server可以主动发起请求,通知client端某个attribute发生了变化,使client端不用轮询attribute。
ATT线路协议不会发送value的长度,其通常隐含在PDU的大小里。client应该知道如何通过UUID解析value的结构。不发送value的长度可以节省(数据包)字节数。这对于LE来说很重要,因为在LE里MTU(maximum transmission unit)仅为23字节。小MTU长度不利于传输数据量大的attribute。因此ATT定义了”read long” 和 “write long” 操作,通过数据块来传输attribute。ATT兼容链路MTU,而且不限制包大小为 lowest-common denominator。例如,一个40字节的arrtibute请求在LE系统中通过read long操作进行,但也可以通过经典蓝牙来进行传输,因为后者的最小MTU是48字节。
ATT非常通用,给高层协议留下了许多发挥空间。同时ATT将一些问题留给高层协议去解决。例如,一个设备如何提供多种服务?每个设备仅有一个handle空间(即handle在某个设备上是唯一的),多种服务必须以某种方式共用这个空间。
幸运地,我们有GATT。它规范和扩展了attribute的用法。
GATT: Generic Attribute Profile
GATT是所有高层LE协议的基础。它定义了attribute是如何组成服务的。
一个GATT服务始于UUID为0x2800的attribute,直到下一个UUID为0x2800的attribute为止。范围内的所有attribute都是属于该服务的。
例如,一台有三种服务的设备拥有如下所示的attribute布局:
Handle | UUID | Description |
0x0100 | 0x2800 | Service A definition |
… | … | Service details |
0x0150 | 0x2800 | Service B definition |
… | … | Service details |
0x0300 | 0x2800 | Service C definition |
… | … | Service details |
attribute并不知道自己属于哪一个service。GATT负责通过查找UUID为0x2800的attribute来划分service的handle范围。于是在这种情况下,handle变得非常重要。在例子中,属于service B的attribute的handle范围肯定落在0x0151和0x02ff之中。UUID0x2800定义了主服务,0x2801定义的是次服务。主服务包含次服务。
那么,我如何知道一个service是温度检测,keyfob还是GPS呢?通过读取该attribute的value。service attribute的value的值是一个UUID,代表该service的类型。我们可以看到,每个service含有两个UUID:一个是attribute的UUID,另一个是储存于value中。
如例子所示,假设标识温度计service的 UUID是0x1816:
Handle | UUID | Description | Value |
0x0100 | 0x2800 | Thermometer service definition | UUID 0x1816 |
… | … | Service details | … |
0x0150 | 0x2800 | Service B definition | 0x18xx |
… | … | Service details | … |
0x0300 | 0x2800 | Service C definition | 0x18xx |
… | … | Service details | … |
听起来有点奇怪:两个UUID确定一种service?没错,这是GATT的特性之一。其中UUID为0x2800的attribute作为service的起始标志,该attribute的value中的UUID标志着该service的具体类型。所以一个client可以在不知道具体类型情况下读取到所有GATT提供的service。
每个GATT service都包含一个或多个characteristic(特性)。这些characteristic负责存储service的数据和访问权限。
例如,一个温度计(service)一般会有一个只读的“温度”characteristic,和一个可读写的“日期时间”characteristic:
Handle | UUID | Description | Value |
0x0100 | 0x2800 | Thermometer service definition | UUID 0x1816 |
0x0101 | 0x2803 | Characteristic: temperature | UUID 0x2A2B Value handle: 0x0102 |
0x0102 | 0x2A2B | Temperature value | 20 degrees |
0x0110 | 0x2803 | Characteristic: date/time | UUID 0x2A08 Value handle: 0x0111 |
0x0111 | 0x2A08 | Date/Time | 1/1/1980 12:00 |
首先,一个service拥有多个characteristic,GATT通过类似发现service的方法(寻找标记attribute)来确定某一characteristic以及它的的handle范围。主attribute的UUID为0x2803,和service一样,拥有两个UUID:一个(0x2803)用于标识characteristic,另一个(例如标识温度计的0x2A2B)用于标识characteristic的内容(类型)。
每个characteristic至少包含两个UUID:主UUID和value中UUID。通过主UUID可以知道value对应的attribute的handle和UUID,这在某种程度上提供了双重检查的可能(?)。
value的格式完全由UUID决定。所以,如果client知道如何解析value中的0x2A08,那么它就知道如何解释任意一个服务中value为0x2A08的characteristic。另一方面来看,如果client不知道如何解析一个value中的UUID的characteristic,那么它就可以安全地忽略它(?)。
Characteristic descriptors
除了value之外,我们可以在characteristic的附加attribute里获取到其它信息。在GATT里,这些附加的attribute称为descriptor。
例如,当我们我们需要明确温度计的计量单位时,可以通过添加一个descriptor来实现:
Handle | UUID | Description | Value |
0x0100 | 0x2800 | Thermometer service definition | UUID 0x1816 |
0x0101 | 0x2803 | Characteristic: temperature | UUID 0x2A2B Value handle: 0x0102 |
0x0102 | 0x2A2B | Temperature value | 20 degrees |
0x0104 | 0x2A1F | Descriptor: unit | Celsius |
0x0110 | 0x2803 | Characteristic: date/time | UUID 0x2A08 Value handle: 0x0111 |
0x0111 | 0x2A08 | Date/Time | 1/1/1980 12:00 |
GATT “知道” 值为0x0104的handle是一个属于0x0101 characteristic的descriptor是因为:
- 它不是一个value attribute,因为value attribute的handle被指定为0x0102;而且
- 它的handle落在.0x010F之中(两个characteristic之间)。
desctiptor的value根据其UUID进行解析。在例子中, desctiptor的UUID是0x2A1F。client可以自动忽略未知的UUID,这给日后扩展service带来了很大的方便(不需要修改旧的client代码)。
每个service都可以自定义 desctiptor,但GATT已经预定义了一系列常用的 desctiptor:
- 数值类型的格式以及表达方式
- 可读性描述
- 有效范围
- 扩展属性(?)
以及其他。其中一个很重要的descriptor是client characteristic configuration。
Client Characteristic Configuration descriptor (CCC descriptor)
这个descriptor的UUID为0x2902,有一个可读写的16位value。这意味着它可以作为一个bitmap(?)。
像其他descriptor一样,CCC不是client-side,而是server-side的。但是不同的是,server需要为每个绑定的client维护一个独立的value实体,client只可以读取它自己的那一份。因此称之为CCC。
CCC的头两位被GATT占用,用于配置attribute的notification和indication.剩下的位供其他功能使用,但目前还是处于保留的状态。
由于ATT拥有notification能力,所以client不需要通过轮询来获取更新。通过设置CCC,server会在该characteristic发生变化的时候通知client。这使得某些service变得有更有意义(例如温度计)。一个配置了CCC的温度计service如下表所示:
Handle | UUID | Description | Value |
0x0100 | 0x2800 | Thermometer service definition | UUID 0x1816 |
0x0101 | 0x2803 | Characteristic: temperature | UUID 0x2A2B Value handle: 0x0102 |
0x0102 | 0x2A2B | Temperature value | 20 degrees |
0x0104 | 0x2A1F | Descriptor: unit | Celsius |
0x0105 | 0x2902 | Client characteristic configuration descriptor | 0x0000 |
0x0110 | 0x2803 | Characteristic: date/time | UUID 0x2A08 Value handle: 0x0111 |
0x0111 | 0x2A08 | Date/Time | 1/1/1980 12:00 |
跟之前说的一样,GATT知道CCC属于温度chraracteristic是因为其handle落在0x0102..0x010F范围之间,而且知道它是一个CCC因为其UUID为0x2902。
Service discovery in Low Energy
因为GATT中,所有service的建立细节都在ATT之上,所有不需要像经典蓝牙那样的服务发现协议(SDP)。ATT协议包含了所有的东西:服务发现,characteristics发现,读写value,以及其他。