ATT(Attribute Protocol)属性层是GATT和GAP的基础,它定义了BLE协议栈上层的数据结构和组织方式。属性(Attribute)概念是ATT层的核心,ATT层定义了属性的内容,规定了访问属性的方法和权限。以编程的眼光来看,属性是一个数据结构,它包括了数据类型和数据值,就如同C语言结构体的概念,开发者可以设计独特的结构,来描述外部世界实体。属性包括三种类型:服务项、特征值和描述符。三者之间存在树状包含关系,服务项包含一个或多个特征值,特征值包含一个或多个描述符,多个服务项组织在一起,构成属性规范(Attribute Profile)。对于常用的属性规范,比如体重计、心率计,BLE协会做了具体定义,这样的话,只要BLE主从设备均遵守某个Profile来进行设计,那么二者就能够优雅的通信。ATT层相关的东西与开发者比较近,易于理解,但是章节内容图表较少,阐述偏多。
属性数据结构
属性数据结构包含:句柄、类型、属性值、权限。
属性句柄(Attribute Handle)是一个2字节的十六进制码,起始于0x0001,在系统初始化时候,各个属性的句柄逐步加一,最大不超过0xFFFF。这个跟软件编程中的句柄的概念类似,就是某个属性值的查询地址。属性类型(Attribute Type)用以区分当前属性是服务项或是特征值等,它用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-0000-1000-8000-00805F9B34FB
其中从左数第3、4个字节“XXXX”就是变化位,其他为固定位。即UUID=0x2A00在系统内部会转换成00002A00-0000-1000-8000-00805F9B34FB。反之,如果一个特征值的UUID是16字节的,在系统内部它的属性类型也可能写成第3、4字节组成的双字节,比如UUID=1234ABCD-0000-1000-8000-00805F9B34FB,它的属性类型在内部表示为ABCD。主机端扫描到该属性类型,会将其当做是“用户自定义”的类型,然后从其他位置获取该UUID的真实值。属性值(Attribute Value)是存放数据的地方。如果是服务项或者特征值声明,该数据为UUID等信息,如果是普通的特征值,该数据则是用户的数据。属性值需要预留空间以保存用户数据。为了方便理解,我们可以将属性值的空间看做I2C的数据空间,操作特征值里的用户数据,就是对那块内存空间进行读写。权限(Permission)花样比较多:
- 访问权限(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)
服务项本身并不包含数据,仅仅相当于一个容器,容纳特征值。特征值中承载用户数据,同时特征值自身也有一个数据(UUID)。这有点像C语言中的变量int var=0xFF,整形变量var携带了用户数据0xFF,但是它自身还有地址信息(&var),因此在使用时需要先定义再赋值两个步骤。类似的,在处理特征值所携带的用户数据之前,需要先对特征值自身进行声明。特征值在系统中的表达形式是:声明 + 特征值属性。比如Device Name,它在GATT数据库中表示方式是:
Characteristic 声明: 0x0002, 0x2803, access_property, 0x2A00Characteristic 项: 0x0003, 0x2A00, access_property, data
其中第一列双字节数代表句柄,第二列双字节数代表属性类型(UUID),0x2803表示该项为“特征值的声明”,0x2A00表示该特征值是Device Name。在第一行特征值声明中,最后一项的属性值就是该特征值的UUID。可以注意到,声明里面的属性值为0x2A00,特征值自己的属性类型也是0x2A00,显然信息冗余了。原因是,假如特征值的UUID为自定义的16字节UUID,在特征值的属性类型中,只能存放2个字节,而UUID的真实值,就存放在特征值声明的属性值项中。BLE的属性体系在系统中以GattDB表示,即属性数据库。在Creator中,打开CyBle_gatt.c文件,找到cyBle_gattDB变量,如下图:
通过注释可以看到,每个特征值都有声明和特征值项两部分组成。由于各个属性的句柄是递增的,因此属性的声明顺序会影响句柄的计算。描述符是特征值的补充信息,挂载在特征值之下,它可以开辟一段数据空间以携带数据,客户端可以像操作特征值一样对其进行读写,但是描述符弱于特征值,它不具备Notify/Write等读写属性,远不如特征值灵活。有一种描述符叫CCCD(Client Characteristic Configuration Description),当对特征值设置Notify或Indication时,用以控制Notify和Indication的使能情况。关于gattDB,值得注意的一点是,gattDB是BLE协议栈在内存中开辟的一段专有区域,它会在特定的时候写入Flash进行保存,并在启动时读取出来回写到内存中去。并非所有的BLE数据通信是操作gattDB!比如手机向BLE设备发送一串字符串,从机收到这串字符串并打印出来,这个过程中,这个字符串并没有写入gattDB。那么,即使开发者限制某个特征值为认证、只读等权限,仍然不能阻止这个字符串的发送和打印,因为特征值的权限,都是针对gattDB而言的。进一步,假如BLE从机收到字符串后,希望写入gattDB进行保存,那么就会触发权限问题。以往有开发者说为什么我勾选了属性需要认证,但是配对失败后仍然能够看到手机发来的数据,原因就在这里,就是手机端传来的数据没有涉及到gattDB,当尝试写入gattDB的时候,就会发现报错了。
属性协议
属性协议规定了客户端(Client)和服务器(Server)之间通信的方式,共六种:
- Request(请求)
- Response(响应)
- Command(命令)
- Indication(指示)
- Confirmation(确认)
- Notification(通知)
客户端发送Request,服务器需要返回一个Response,表明服务器收到了。服务器发送Indication,客户端需要返回一个Confirmation,表明客户端收到了。以上两种方式,均是单线程操作,即下一个Request/Indication操作需要在上一个操作收到Response/Confirmation之后才能开始。客户端发送Command,服务器无需任何返回。服务器发送Notification,客户端无需任何返回。因此Command和Notification是不可靠的通信。当通信环境不佳,客户端频繁发送Command,可能发生服务器接收不到或丢弃的情况,Notification也类似。