ATT(Attribute Protocol)属性层是GATT和GAP的基础,它定义了BLE协议栈上层的数据结构和组织方式。
属性(Attribute)概念是ATT层的核心,ATT层定义了属性的内容,规定了访问属性的方法和权限。以编程的眼光来看,属性是一个数据结构,它包括了数据类型和数据值,就如同C语言结构体的概念,开发者可以设计独特的结构,来描述外部世界实体。
属性包括三种类型:服务项、特征值和描述符。三者之间存在树状包含关系,服务项包含一个或多个特征值,特征值包含一个或多个描述符,多个服务项组织在一起,构成属性规范(Attribute Profile)。对于常用的属性规范,比如体重计、心率计,SIG(蓝牙技术联盟)做了具体定义,这样的话,只要BLE主从设备均遵守某个Profile来进行设计,那么二者就能够优雅的通信。
ATT层相关的东西与开发者比较近,易于理解,但是章节内容图表较少,阐述偏多。
一. 属性的组成(数据结构)
属性主要由以下四部分组成:属性句柄(Attribute Handler)、属性类型(Attribute Type)、属性值(Attribute Value)、属性权限(Attribute Permissions)。
1.1 属性句柄
属性句柄(Attribute Handle)犹如指向属性实体的指针,对端设备可通过属性句柄来访问该属性,它是一个2字节长度的十六进制码,起始于0x0001,在系统初始化时候,各个属性的句柄逐步加一,最大不超过0xFFFF。
1.2 属性类型
作用是用以区分当前属性是服务项或是特征值等,它用UUID来表示。UUID(universally unique identifier,通用唯一识别码)是一个软件构建标准,并非BLE独有的概念,一个合法的UUID,一定是随机的、全球唯一的,不应该出现两个相同的UUID(出现了,就说明它们俩是同一个UUID)。标准的UUID是一串16字节十六进制字符串,如f6257d37-34e5-41dd-8f40-e308210498b4,在网上可以方便的生成一个UUID。
BLE的属性类型是有限的,有四个大类:
- Primary Service(首要服务项)
- Secondary Service(次要服务项)
- Include(包含服务项)
- Characteristic(特征值)
这些属性类型分别对应了指定的UUID,BLE对这些UUID与属性类型的映射关系做了规定:
- 0x1800 – 0x26FF :服务项类型
- 0x2700 – 0x27FF :单位
- 0x2800 – 0x28FF :属性类型
- 0x2900 – 0x29FF :描述符类型
- 0x2A00 – 0x7FFF :特征值类型
假如UUID=0x1800,就表示它是一个首要服务项。
UUID是16个字节的字符串,为什么这里只使用了2字节?
因为这些是常用的UUID,为了减少传输的数据量,BLE协议做了一个转换约定,给定一个固定的16字节模板,只设置2个字节为变化量,其他为常量,2字节的UUID在系统内部会被替换,进而转换成标准的16字节UUID。UUID模板为:
0000XXXX----00805F9B34FB
其中从左数第3、4个字节“XXXX”就是变化位,其他为固定位。如:UUID=0x2A00在系统内部会转换成00002A00-0000-1000-8000-00805F9B34FB。
反之,如果一个特征值的UUID是16字节的,在系统内部它的属性类型也可能写成第3、4字节组成的双字节,比如UUID=1234ABCD-0000-1000-8000-00805F9B34FB,它的属性类型在内部表示为ABCD。主机端扫描到该属性类型,会将其当做是“用户自定义”的类型,然后从其他位置获取该UUID的真实值。
1.3 属性值
用于存放数据。如果该属性是服务项类型或者是特征值声明类型,那么它的属性值就是UUID等信息。如果是普通的特征值,则属性值是用户的数据。属性值需要预留空间以保存用户数据。为了方便理解,我们可以将属性值的空间看做I2C的数据空间,操作特征值里的用户数据,就是对那块内存空间进行读写。
1.4 属性权限
属性权限主要有以下四种:
- 访问权限(Access Permission)- 只读、只写、读写
- 加密权限(Encryption Permission) – 加密、不加密
- 认证权限(Authentication Permission) – 需要认证、无需认证
- 授权权限(Authorization Permission) – 需要授权、无需授权
访问(Access)权限好理解,如果是只读权限,就不能对其写数据,其他类似。
加密(Encryption)权限也好理解,就是对数据进行加密。
认证(Authentication)是指相互确认对方身份。完成认证流程的两个设备,双方建立信任关系,二者之间的通信通道即可以认为是安全的。BLE中,“认证”过程就是配对。
授权(Authorization)是指对授信设备开放权利。
认证和授权功能容易混淆,其英文拼写也很相似。从上面的概念上看,授权要求设备必须是可信任的,因此授权的管控等级要高于认证——认证的设备未必被授权,授权的设备一定是认证的。理解二者关系,需要引入一个概念:Trusted Device(可信任设备)一个没有经过认证的设备,被称为Unknown Device(未知设备);经过了认证该设备会在绑定信息中被标记为Untrusted,被称为Untrusted Device(不可信设备);经过了认证,并且在绑定信息中被标记为Trusted的设备被称为Trusted Device(可信设备)。
授权要求设备为Trusted Device(可信任设备)。在实际使用中,经过配对以后设备即为Untrusted Device——认证,在代码中调用API可以设置设备为Trusted Device——授权。
二. 属性的种类和分组(属性的层级)
属性大致可以分为三种类型:服务项、特征值和描述符。它们的层级关系为:最*为Profile, 下面是多个服务项(Service), 服务项下面是多个特征值(Characteristic), 特征值下面是多个描述符(Descriptor)。
每个设备都包含以下必要的特征值和服务项:
PROFILE
- Generic Access Service(Primary Service)
- Device Name(Characteristic)
- Appearance(Characteristic)
- Generic Attribute Service(Primary Service)
- Service Changed(Characteristic)
- CCCD(Descriptor)
- Service Changed(Characteristic)
服务项这种类型本身并不包含数据,仅仅相当于是一个容器,用来容纳特征值。特征值用于保存用户数据,但它也有自己的UUID, 有点像C语言中的变量int var=0xFF,整形变量var携带了用户数据0xFF,但是它自身还有地址信息(&var),因此在使用时需要先定义再赋值两个步骤。类似的,在处理特征值所携带的用户数据之前,需要先对特征值自身进行声明。
特征值在系统中的表达形式是:声明 + 特征值属性。比如Device Name,它在GATT数据库中表示方式是:
Characteristic 声明: 0x0002, 0x2803, access_property, 0x2A00
Characteristic 项: 0x0003, 0x2A00, access_property, data
其中第一列双字节数代表句柄,第二列双字节数代表属性类型(UUID),0x2803表示该项为“特征值的声明”,0x2A00表示该特征值是Device Name。在第一行特征值声明中,最后一项的属性值就是该特征值的UUID。可以注意到,声明里面的属性值为0x2A00,特征值自己的属性类型也是0x2A00,显然信息冗余了。原因是,假如特征值的UUID为自定义的16字节UUID,在特征值的属性类型中,只能存放2个字节,而UUID的真实值,就存放在特征值声明的属性值项中。
BLE的属性体系在系统中以GattDB表示,即属性数据库。打开CyBle_gatt.c文件,找到cyBle_gattDB变量,如下图
const CYBLE_GATTS_DB_T cyBle_gattDB[0x10u] = {
{ 0x0001u, 0x2800u /* Primary service */, 0x00000001u /* */, 0x0007u, {{0x1800u, NULL}} },
{ 0x0002u, 0x2803u /* Characteristic */, 0x00000201u /* rd */, 0x0003u, {{0x2A00u, NULL}} },
{ 0x0003u, 0x2A00u /* Device Name */, 0x00000201u /* rd */, 0x0003u, {{0x0009u, (void *)&cyBle_attValuesLen[]}} },
{ 0x0004u, 0x2803u /* Characteristic */, 0x00000201u /* rd */, 0x0005u, {{0x2A01u, NULL}} },
{ 0x0005u, 0x2A01u /* Appearance */, 0x00000201u /* rd */, 0x0005u, {{0x0002u, (void *)&cyBle_attValuesLen[]}} },
{ 0x0006u, 0x2803u /* Characteristic */, 0x00000201u /* rd */, 0x0007u, {{0x2A04u, NULL}} },
{ 0x0007u, 0x2A04u /* Peripheral Preferred Connection Par */, 0x00000201u /* rd */, 0x0007u, {{0x0008u, (void *)&cyBle_attValuesLen[]}} },
{ 0x0008u, 0x2800u /* Primary service */, 0x00000001u /* */, 0x000Bu, {{0x1801u, NULL}} },
{ 0x0009u, 0x2803u /* Characteristic */, 0x00002201u /* rd,ind */, 0x000Bu, {{0x2A05u, NULL}} },
{ 0x000Au, 0x2A05u /* Service Changed */, 0x00002201u /* rd,ind */, 0x000Bu, {{0x0004u, (void *)&cyBle_attValuesLen[]}} },
{ 0x000Bu, 0x2902u /* Client Characteristic Configuration */, 0x00000A04u /* rd,wr */, 0x000Bu, {{0x0002u, (void *)&cyBle_attValuesLen[]}} },
{ 0x000Cu, 0x2800u /* Primary service */, 0x00000001u /* */, 0x0010u, {{0xCBBBu, NULL}} },
{ 0x000Du, 0x2803u /* Characteristic */, 0x00001A01u /* rd,wr,ntf */, 0x0010u, {{0xCBB1u, NULL}} },
{ 0x000Eu, 0xCBB1u /* Custom Buffer */, 0x00011A04u /* rd,wr,ntf */, 0x0010u, {{0x00C8u, (void *)&cyBle_attValuesLen[]}} },
{ 0x000Fu, 0x2901u /* Custom Descriptor */, 0x00010001u /* */, 0x000Fu, {{0x001Cu, (void *)&cyBle_attValuesLen[]}} },
{ 0x0010u, 0x2902u /* Client Characteristic Configuration */, 0x00010A04u /* rd,wr */, 0x0010u, {{0x0002u, (void *)&cyBle_attValuesLen[]}} },
};
通过注释可以看到,每个特征值都有声明和特征值项两部分组成。
由于各个属性的句柄是递增的,因此属性的声明顺序会影响句柄的计算。
描述符是特征值的补充信息,挂载在特征值之下,它可以开辟一段数据空间以携带数据,客户端可以像操作特征值一样对其进行读写,但是描述符弱于特征值,它不具备Notify/Write等读写属性,远不如特征值灵活。有一种描述符叫CCCD(Client Characteristic Configuration Description),当对特征值设置Notify或Indication时,用以控制Notify和Indication的使能情况。
关于gattDB,值得注意的一点是,gattDB是BLE协议栈在内存中开辟的一段专有区域,它会在特定的时候写入Flash进行保存,并在启动时读取出来回写到内存中去。并非所有的BLE数据通信是操作gattDB!比如手机向BLE设备发送一串字符串,从机收到这串字符串并打印出来,这个过程中,这个字符串并没有写入gattDB。那么,即使开发者限制某个特征值为认证、只读等权限,仍然不能阻止这个字符串的发送和打印,因为特征值的权限,都是针对gattDB而言的。进一步,假如BLE从机收到字符串后,希望写入gattDB进行保存,那么就会触发权限问题。以往有开发者说为什么我勾选了属性需要认证,但是配对失败后仍然能够看到手机发来的数据,原因就在这里,就是手机端传来的数据没有涉及到gattDB,当尝试写入gattDB的时候,就会发现报错了。
三. ATT PDU(属性协议)
在ATT层协议框架内,拥有一组属性的设备称为服务端(Server),读写该属性值的设备称为客户端(Client),Server和Client通过ATT PDU进行交互。属性协议共有6种:
属性PDU | 方向 | 触发响应 |
---|---|---|
Command | Client -> Server | – |
Request | Client -> Server | Response |
Response | Server -> Client | – |
Notification | Server -> Client | – |
Indication | Server -> Client | Confirmation |
Confirmation | Client -> Server | – |
它们的区别如下:
客户端发送Request,服务器需要返回一个Response,表明服务器收到了。
服务器发送Indication,客户端需要返回一个Confirmation,表明客户端收到了。
以上两种方式,均是单线程操作,即下一个Request/Indication操作需要在上一个操作收到Response/Confirmation之后才能开始。
客户端发送Command,服务器无需任何返回。
服务器发送Notification,客户端无需任何返回。
因此Command和Notification是不可靠的通信。当通信环境不佳,客户端频繁发送Command,可能发生服务器接收不到或丢弃的情况,Notification也类似。
PDU的具体格式定义如下:
参数说明:
Opcode:
bit -:操作属性的方法
bit :Command 标识位
bit :Authentication Signature标识位
Attribute Parameters:
如果Attribute Opcode中身份验证签名标记位为0,则X = ;
如果Attribute Opcode中身份验证签名标记位为1,则X = ;
Authentication Signature:
属性操作码和属性参数的可选身份验证签名
参考链接:
1. 蓝牙BLE实用教程
2. BLE协议栈-ATT