蓝牙解析(part10):BLE ATT/GATT

本部分是从各位前辈的学习经验中,总结过来的,希望对初学者有益。

从蓝牙Spec 4.0开始,推出了低功耗(BLE)规范,BLE的协议可分为Bluetooth Application和Bluetooth Core两大部分,而Bluetooth Core又包含BLE Controller和BLE Host两部分,整体架构如下图所示。本章节,先来看一下Host部分中的两个核心协议:ATT(Attribute Protocol)和GATT(Generic Attribute Protocol).

ble_stack

1. 总纲

这两个协议主要目标是BLE,但是也可以运行在传统蓝牙上(BR/EDR).

ATT提供了一种无线应用协议,GATT基于ATT协议,相当于ATT的framewrok层,而所有的BLE profile又基于GATT。同时ATT/GATT定义在host中,即协议栈里面, 而profiles则定义在应用层,这样的结构决定了ATT/GATT要实现基本而common的功能实现,而profiles来完善各具特色的具体应用功能。

BLE分层使用这两个协议的好处是:

  1. 对于软件实现的协议栈来说,ATT/GATT在协议栈里实现,省去了应用的麻烦。
  2. 开发和实现新的BLE profile更加容易,因为不需要从头实现wire protocol。
  3. ATT针对BLE 设备进行了特别的优化:使用尽可能少的字节,因此可能在存储中使用定长结构来生成PDU。
  4. ATT/GATT的简单意味着固件可能提供某种程度的协议支持,省去了微处理器软件的麻烦。
  5. 即使有的场景下,ATT/GATT不够理想。也可以在L2CAP连接上实现平行于ATTchannel的协议。

2. ATT: Attribute Protocol

该协议将数据以“Attribute(属性)”的形式抽象出来,并提供一些方法,供远端设备(remote device)读取、修改这些属性的值(Attribute value)。ATT协议的唯一基础是属性。每个属性由三个元素构成:

  1. one 16bits handle;
  2. one 16bits/32bits/128bitsUUID来定义属性的类型;
  3. 确定长度的属性值

在ATT中,属性值可以是任意长度的byte数组。属性值的实际意义依赖于UUID,而且ATT并不会检查属性值长度是否与给定的UUID定义一致。

Handle是用来唯一识别属性的数字,因为在一个BLE 设备中可能存在多个属性具有相同的UUID。

ATT协议本身没有定义任何UUID。这部分工作留给了GATT和上层协议。

和属性相关的还有读写权限。读写权限存在属性值里,由高层协议确定。ATT本身不会关心,也不会试图解释属性值来确定权限。这部分工作也留给了GATT和上层协议。

ATT有一些良好的特征,比如通过UUID来搜索属性,通过handle区间范围来获取所有区间内的属性,因此client不需要提前获得handle的值,也不需要高层协议硬编码这些值。

但是在特定的设备上handle的取值最好保持不变,这样的话client能够缓冲信息。在第一个discovery以后,client能够使用缓冲信息,这样能够减少传输的包数量,也能够节约能量。如果服务端的属性布局已经发生了变换,高层协议应该能够”暗示”client,比如固件升级。

ATT有两个角色,Client和Server,大多数情况下ATT协议都是纯C/S架构,即server存储属性,client什么也不存储,client主动发起请求读写server端的属性,server被动响应。但是服务端也有通知的能力,在服务端属性发生变化时,server能够通知client,这样避免了client不停的poll

ATT协议不会显式发送属性值的长度,只能从PDU长度里面获得。因此client最好能够知道某种UUID类型所代表的属性的精确结构。

不发送属性值长度,是为了减少发送的字节,因为LE的MTU只有23bytes

23bytes的MTU对于较长的属性值来说是个麻烦。因此不得采用“read long”,”write long“这样的操作。

ATT是如此通用,意味着高层协议有太多工作要做。过度的自由也会带来问题,比如:如果一个设备提供多个服务怎么办?对每一个设备只有一个ATT handle空间,多个服务不得不共享同一份空间。

幸运地是,我们还有GATT,它为我们提供了属性用法,并解除了这些限制。

3. GATT:Generic Attribute Profile

GATT是所有LE顶层协议的基础。它定义了怎么把一堆ATT属性分组成为有意义的服务。
纵向看,GATT Profile包含一个或多个GATT Services, 每个GATT service又包含一个或多个GATT Characteristics, 同时每个Characteristic又对应一个或多个GATT Descriptors。

3.1 GATT service

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能够找到所有的服务而不需要知道服务的具体定义。


3.2 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的话,也可以选择忽略。

3.3 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



3.4 Service discovery in Low Energy

因为GATT中所有的服务细节通过ATT来描述,所以不需要像BR/EDR那样设置专门的服务发现协议。ATT负责一切:发现服务,查找特征,读写值等等。

3.5 GATT and vanilla 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。这是规范不运行的,但是开发者可以。


3.6 Notification and Indication

通知和暗示使得server可以发送消息给client。这样客户端不需要poll server来获取新的数据。

另外,典型的GATT server是“小的“外设,像非常需要节能的传感器之类。因此,外设的LE 设备不能发起连接。那么通知怎么发送呢?

在BLE协议栈,如果server有数据发送,它就进入广播模式,并且发送一些信号。每个profile定义了广播时长和频率。时长和频率应该根据使用场景进行了节能和及时性的权衡。

处于中心模式的设备随时处于监听模式。当它监听到广播后,如果发现广播设备是认识的(配对过或者白名单中的),就会向外设发起连接。

连接建立以后,GATT通信能够进行,通知得以发送。所以典型的序列是:1,server发送广播 2,client连接 3,server通知

如果没有更多的数据发送,server和client就会超时断开。最佳超时时间依赖于用例;如果服务不会频繁发送通知并且没有实时性要求的话,可以立马断开。因为BLE重连是非常快的。

典型的GATT server是外设设备,但是不是必须的。也可以外设做client,center做server。在这种场景下,client想要读写数据的时候,需要先进入广播模式。

猜你喜欢

转载自blog.csdn.net/z497544849/article/details/53924227