英文原文URL:https://epx.com.br/artigos/bluetooth_gatt.php
因为看了这篇英文文档介绍Bluetooth ATT/GATT很详细,所以翻译了一下,供参考!
蓝牙4.0版本推出了低功耗规范,引入了两个核心协议:ATT(Attribute Protocol)和GATT(Generic Attribute Protocol).这两个协议主要目标是BLE,但是也可以运行在传统蓝牙上(BR/EDR).
Overview
ATT是wire application protocol(无线应用层连接协议?),GATT基于ATT协议。所有的BLE profile一定基于GATT。也就是所有的BLE服务都使用ATT作为应用协议。
锁定BLE使用这两个协议的好处是:
1, 开发和实现新的BLE profile更加容易,因为不需要从头实现wire protocol。
2, ATT针对BLE 设备进行了特别的优化:使用尽可能少的字节,因此可能在存储中使用定长结构来生成PDU。
3, ATT/GATT的简单意味着固件可能提供某种程度的协议支持,省去了微处理器软件的麻烦。
4, 对于软件实现的协议栈来说,ATT/GATT在协议栈里实现,省去了应用的麻烦。
5, 即使有的场景下,ATT/GATT不够理想。也可以在L2CAP连接上实现平行于ATTchannel的协议。
ATT: Attribute Protocol
ATT协议的唯一基础是属性。每个属性由三个元素构成:
1,一个16bit handle;
2,一个UUID来定义属性的类型;
3,确定长度的属性值
在ATT中,属性值可以是任意长度的byte数组。属性值的实际意义依赖于UUID,而且ATT并不会检查属性值长度是否与给定的UUID定义一致。
Handle是用来唯一识别属性的数字,因为在一个BLE 设备中可能存在多个属性具有相同的UUID。
ATT协议本身没有定义任何UUID。这部分工作留给了GATT和上层协议。
ATT server存储属性。ATT client什么也不存储,它使用ATT协议来读写server端的属性。
和属性相关的还有读写权限。读写权限存在属性值里,由高层协议确定。ATT本身不会关心,也不会试图解释属性值来确定权限。这部分工作也留给了GATT和上层协议。
ATT有一些良好的特征,比如通过UUID来搜索属性,通过handle区间范围来获取所有区间内的属性,因此client不需要提前获得handle的值,也不需要高层协议硬编码这些值。
但是在特定的设备上handle的取值最好保持不变,这样的话client能够缓冲信息。在第一个discovery以后,client能够使用缓冲信息,这样能够减少传输的包数量,也能够节约能量。如果服务端的属性布局已经发生了变换,高层协议应该能够”暗示”client,比如固件升级。
大多数情况下ATT协议都是纯C/S架构,client发起请求,server响应。但是服务端也有通知的能力,在服务端属性发生变化时,server能够通知client,这样避免了client不停的poll。
ATT协议不会显式发送属性值的长度,只能从PDU长度里面获得。因此client最好能够知道某种UUID类型所代表的属性的精确结构。
不发送属性值长度,是为了减少发送的字节,因为LE的MTU只有23bytes。
23bytes的MTU对于较长的属性值来说是个麻烦。因此不得采用“read long”,”write long“这样的操作。
ATT是如此通用,意味着高层协议有太多工作要做。过度的自由也会带来问题,比如:如果一个设备提供多个服务怎么办?对每一个设备只有一个ATT handle空间,多个服务不得不共享同一份空间。
幸运地是,我们还有GATT,它为我们提供了属性用法,并解除了这些限制。
GATT:Generic Attribute Profile
GATT是所有LE顶层协议的基础。它定义了怎么把一堆ATT属性分组成为有意义的服务。
GATT services
GATT service的基础是UUID值为0x2800的属性。所有跟在这个属性后面的属性都属于这个属性定义的服务,直到另一个0x2800属性出现。
比如说,一个设备里面的三个属性布局如下:
Handle | UUID | Description |
---|---|---|
0x0100 | 0x2800 | Service A definition |
... | ... | Service details |
0x0150 | 0x2800 | Service B definition |
... | ... | Service details |
0x0300 | 0x2800 | Service C definition |
... | ... | Service details |
每一个属性不知道它自己属于哪个服务,GATT需要根据0x2800属性作为标记来识别出哪个属性属于哪个服务。
按照这个定义,handle值就有意义了。在上面的例子中,属于service B的属性handle必须位于0x0151和0x02ff之间。
UUID 0x2800定义了primary服务,
也可以使用0x2801来定义secondary服务。
Secondary服务表示包含于primary服务。
然后我们怎么能知道一个服务是温度计,智能钥匙或者GPS?答案是通过读取属性值。服务属值包含了一个UUID,通过这个UUID区分服务。
因此,每个属性定义事实上包含了两个UUID,0x2800或者0x2801作为属性UUID,另外一个属性值里面存储的UUID。后面这个UUID是服务ID。
举个例子:
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 | ... |
在图中, thermometer service的UUID是0x1816。
是不是感觉怪怪的?两个UUID定义一个服务?这是GATT/ATT分层方式导致的后果。
UUID0x2800被GATT用来寻找服务定义边界。一旦找到了边界,属性值,也就是第二个UUID用来指定服务。这样client能够找到所有的服务而不需要知道服务的具体定义。
GATT service characteristics
每一个服务有几个特征。特征存储了有用的值以及权限。
比如,一个温度计可能有只读的温度特征,也可能有可读写的时间戳。
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 |
每一个服务可能有几个特征,这些特征也是通过路碑属性来发现的。
主特征的UUID是0x2803,然后主特征的属性值用来定义特征。比如图中 0x2803用来找到特征,0x2A2B用来找到特征包含的信息。
每一个特征至少包含两个属性,主属性0x2803和真正的值属性。主属性知道属性值的handle和UUID。这能够进行一定程度的交叉检测。
特征值的真正格式是由UUID决定的。因此,如果客户端知道如何解释UUID为 0x2A08的特征值,就能够从包含这个特征任何服务里面读取日期和时间。当然如果客户端不知道如何解释这个UUID的话,也可以选择忽略。
Characteristic descriptors
除了特征值,我们也可以为每个特征增加更多的属性。在GATT语法里,这个额外的属性成为描述符。
举个例子子,我们也许需要指定温度的计量单位。
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知道handle 0x0104是特征0x0101的描述符,因为:
1, 他不是特征的值,因为特征值的handle应该是0x0102
2, 他的handle落在了0x0103-0x010f之间,因此也不属于下一个特征。
描述符值的意义依赖于属性UUID。例子中,描述符的UUID是0x2A1F,客户端如果不能识别这个UUId,他可以选择忽略。这样可以实现向下兼容。
每个服务可能定义自己的描述符,但是GATT已经定义了能够覆盖大多数情况的标准描述符,比如:
数值格式和表示;
人类可读的描述;
合理范围扩展属性等等。其中特别重要的描述符是client characteristic configuration。
Client Characteristic Configurationdescriptor
Client Characteristic Configurationdescriptor的UUID是0x2902,具有一个16bit的可读写值,作为一个bitmap来使用。
这个属性被server用来存储和代表每个已经绑定的client的独立实例,每个client只能看到它自己的拷贝。
前两个bit被GATT用来定义通知和暗示。其他bit暂时未使用。
通过设置CCC,client能够让server在特征发生改变时得到通知。比如包含了CCC的属性布局如下:
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 |
Servicediscovery in Low Energy
因为GATT中所有的服务细节通过ATT来描述,所以不需要像BR/EDR那样设置专门的服务发现协议。ATT负责一切:发现服务,查找特征,读写值等等。
GATT andvanilla Bluetooth
GATT也可以工作在传统蓝牙上面,但是规范规定传统蓝牙仍然使用SDP发送服务,即使通过GATT来进行实际数据交换。
这样的好处是在双模设备上不用设置标识来识别LE-only服务。如果一个服务只能通过GATT发现,就是LE-only。如果能够通过GATT和SDP发现,就是双模。
如果一个profile通过GATT来进行数据交换,并且是双模的,它必须首先发布SDP record。然后这个服务通过SDP来发现,然后通过GATT来查找特征。
当然,现在没有双模的profile。以前的profile是BR/EDR only,并且没有适配到GATT;LE-only只有LE。
如果想要测试GATT而没有LE硬件,可以修改蓝牙协议栈来使BR/EDR可以进行GATT discovery。这是规范不运行的,但是开发者可以。
Notificationsversus connections
通知和暗示使得server可以发送消息给client。这样客户端不需要pollserver来获取新的数据。
另外,典型的GATT server是“小的“外设,像非常需要节能的传感器之类。因此,外设的LE 设备不能发起连接。那么通知怎么发送呢?
在BLE协议栈,如果server有数据发送,它就进入广播模式,并且发送一些信号。每个profile定义了广播时长和频率。时长和频率应该根据使用场景进行了节能和及时性的权衡。
处于中心模式的设备随时处于监听模式。当它监听到广播后,如果发现广播设备是认识的(配对过或者白名单中的),就会向外设发起连接。
连接建立以后,GATT通信能够进行,通知得以发送。所以典型的序列是:1,server发送广播 2,client连接 3server通知
如果没有更多的数据发送,server和client就会超时断开。最佳超时时间依赖于用例;如果服务不会频繁发送通知并且没有实时性要求的话,可以立马断开。因为BLE重连是非常快的。
典型的GATT server是外设设备,但是不是必须的。也可以外设做client,center做server。在这种场景下,client想要读写数据的时候,需要先进入广播模式。