[翻译]ATT和GATT概述

蓝牙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是因为:

  1. 它不是一个value attribute,因为value attribute的handle被指定为0x0102;而且
  2. 它的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,以及其他。