1. GATT
GATT-Generic Attribute profle-通用属性配置文件。GATT层是传输真正数据所在的层。包括了一个数据传输和存储架构以及其基本操作。GATT用来规范attribute中的数据内容,并运用group(分组)的概念对attribute进行分类管理。没有GATT,BLE协议栈也能跑。但互联互通就会出问题,也正是因为有了GATT和各种各样的应用profile,BLE摆脱了Zigbee等无线协议的兼容性困境,成为了出货量最大的2.4G无线通信产品。
GATT定义了两类角色:服务端(server)和客户端(client),GATT角色无需和GAP角色绑定,但是可能由更高层的规范进行指定。
除了GAP定义角色外(GAP定义广播者,扫描者,外围设备,中央设备),BLE还定义了另外2种角色:GATT服务端和GATT客户端。这两种角色完全独立于GAP的角色。提供数据的设备被称为GATT服务端,访问GATT服务端而获得数据的设备被称为GATT客户端。即一个设备可以同时作为客户端和服务端。通常情况下,外围设备为服务端,提供数据。手机作为客户端,访问数据。
一个GATT服务器通过一个称为属性表 的表格组织数据,这些数据就是用于真正发送的数据。
2.属性ATT
属性协议(Attribute Protocol)简称ATT。是GATT和GAP的基础。
就属性来说,一个属性其实就是一条数据,属性是BLE数据提供单元,也是蓝牙空中传播数据的最上层,BLE开发过程中接触最多的就是这一层。
一个属性包含了句柄、UUID、值。数据的结构如下图:
2.1 Attribute Handle
属性句柄 (Attribute Handle)占2个字节 ,犹如指向属性实体的指针,对端设备可以通过属性句柄来访问该属性。
属性句柄值的范围:0x0000 ~ 0xFFFF。
2.2 Attribute Type
属性类型(Attribute type)是用来区分当前属性是服务项还是特征值等,一般用UUID来表示。
UUID(universally unique identifier,通用唯一识别码)是一个软件构建标准,并非BLE独有的概念,一个合法的UUID,一定是随机的,全球唯一的,不应该出现两个相同的UUID。但是在一个GATT表中可能有许多属性,这些属性可能有相同的UUID。
BLE的属性类型主要有四个大类
Primary Service(首要服务项):0x2800
Secondary Service(次要服务项):0x2801
Include(包含服务项):0x2802
Characteristic(特征值):0x2803
常用的UUID:
Characteristic Extended Properties(特性扩展性质):0x2900
Characteristic User Description(特性用户描述):0x2901
Client Characteristic Configuration(客户端特性配置):0x2902
Server Characteristic Configuration(服务器特性配置):0x2903
Characteristic Presentation Format(特性表示格式):0x2904
Characteristic Aggregate Format(特性聚合格式):0x2905
客户端特性配置描述符:支持通知或指示的特性必须使用该描述符
取值范围
0x1800 -- 0x26FF :服务项类型
0x2700 -- 0x27FF :单位
0x2800 -- 0x28FF :属性类型
0x2900 -- 0x29FF :描述符类型
0x2A00 -- 0x7FFF :特征值类型
长度
UUID即可以是2字节也可以是16字节。因为一些常用的UUID,为了减少传输的数据量,BLE协议做了一个转换约定。给定一个固定的16字节模板,只设置2个字节为变化量,其他为常量。2字节的UUID在系统内部会被替换,进而转换成保准的16字节的UUID。
UUID模板为:
而2字节的UUID则替代上边的XXXX。例如心率测量特性使用0X2A37作为它的16位UUID,因此它完整的16字节的UUID为:0x00002A37-0000-1000-8000-008005F9B34FB。
蓝牙技术联盟所用的基本UUID不能用于任何定制的属性、服务和特性。对于定制的属性,必须使用完整的16字节UUID。
2.3 Attribute Value
属性值,就是该条属性数据包含的有效负载。
2.4 Attribute Permissions
属性权限/属性许可,作用是保护Attribute Value。
Attribute Permissions不会直接在空中包中体现,而是隐含在ATT命令的操作结果中。
权限属性:
访问权限(Access Permission)-只读,只写,读写
禁止访问权限(No Access)-禁止读或者写
加密权限(Encryption Permission)-加密,不加密
认证权限(Authentication Permission)-需要认证,无需认证。认证是指相互确认对方身份。完成认证流程的两个设备,双方建立信任关系,二者之间的通信通道即可认为是安全的。BLE中,认证过程就是配对。
授权权限(Authorization Permission)-需要授权,无需授权
签名权限(Signed Permission)-签名后才能读或写,这个用得比较少
认证和授权功能很容易混淆,其英文拼写也很相似。从上面的概念上看,授权要求设备必须是可信任的,因此授权的管控等级要高于认证--认证的设备未必被授权,授权的设备一定是认证的。理解二者关系后,需要引入一个概念:Trusted Device(可信任设备)一个没有经过认证的设备,被称为Unknown Device(未知设备);经过了认证后,该设备会在绑定设备信息中被标记为Untrusted,被称为Untrusted Device(不可信设备);经过了认证,并且在绑定信息中被标记为Trusted的设备被称为Trusted Device(可信设备)。
授权要求设备为Trusted Device(可信任设备)。在实际使用中,经过配对以后设备即为Untrusted Device--认证,在代码中调用API可以设置设备为Trusted Device--授权。
大部分的空中操作事件都是采用句柄来进行的,因为句柄能够唯一识别各个属性。如何使用特性依据他的性质,特性的性质包括:
写
没有回应的写
读
通知:客户端发请求服务端,不需要服务端回复一个响应
指示:客户端发请求给服务端,需要服务端回复一个响应。
写和没有回应的写
这两个权限允许GATT客户端写一个值到GATT服务端的一个特性中。他们之间不同的地方在与没有回应的写事件没有任何应用层上的确认或回应。
读
读性质标明一个GATT客户端可以读取在GATT服务端中特性的值。
通知和指示
通知和指示性质允许GATT服务端在其某个特性改变的时候对GATT客户端进行提醒。通知和指示之间不同之处在与指示有应用层上的确认,而通知没有。
注:Client和Server之间是通过ATT PDU来通讯的,ATT PDU主要包括4类:读、写、通知和指示。如果一个命令需要response,那么会在相应的指令后面加上request。如果一个命令只需要ACK而不需要response,那么后面就不会带request。这里要特别强调一点,BLE所有命令都是"必达"的,也就是说每个命令发出去之后,会立刻等ACK信息,如果收到了ACK包,发起方认为命令发送完成。否则发起方会一直重发该命令,直到超时导致BLE连接断开。换句话说,只要BLE没有断开,那么发送的数据包,一定会被对方收到。
那既然每个ATT命令都必达对方,那么为什么还需要request呢?如果一个命令带有request后缀,那么发起方就可以收到命令的response包,这个response包在应用层是有回调事件的,而ACK包在应用层是没有回调事件的。所以采用request/response方式,应用层可以按顺序地发送一些数据包,这个在很多应用场景中是非常有用的。相反,如果对应用层数据包的顺序没有要求,那么就可以不使用request/response形式。另外request/response有一个副作用:大大降低通信的吞吐率。因为request/response必须在不同的连接间隔中出现。也就是说,如果在间隔1中发送了一个request命令,那么response包必须在间隔2或者稍后间隔中回复,而不能再间隔1中回复。这就导致两个连接间隔最多只能发一个数据包,而不带quest后缀的ATT命令就没有这个问题,在同一个连接间隔中,可以同时发送多个数据包,这样就大大提供数据的吞吐率。
常用的带request的命令:所有读命令,写,指示等。而常用的不带request的命令有没有回应的写,通知等。
2.5 特性
一个特性至少包含2个属性:一个属性用于声明,一个属性用于存放属性的值。
所有通过GATT服务传输的数据必须映射成一系列的特性,可以把特性中的这些数据看成是一个个捆绑起来的数据。每个特性就是一个自我包容而独立的数据点。例如,如果几个数据总是一起变化,那么可以把它们集中在一个特性里。
2.6 描述符
任何在特性中的属性不是定义为属性值,就是为描述符。描述符是一个额外的属性以提供更多的特性信息。它提供一个人类可识别的特性描述的实例。
然而,有一个特别的描述符需要特别注意:客户端特性配置表述符(Client Characteristic Configuration Descriptor,CCCD),这个描述符是给任何支持通知和指示功能的特性额外增加的。在CCCD中写入"1"使能通知功能,写入"2"使能指示功能,写入"0"同时禁止通知和指示功能。
2.7 服务
一个服务包含一个或多个特性,这些特性是逻辑上相关的集合体。
GATT服务一般包含几块具有相关性的功能,比如特定传感器的读取和设置,人机接口的输入和输出。组织具有相关的特性到服务中即实用又有效。因为它使得逻辑上和用户数据上的边界变得更加清晰,同时它也有助于不同应用程序间代码的重用。GATT基于SIG蓝牙技术联盟官方设计,SIG建议根据用户自己的规范设计自己的profile。
2.8 profile(数据配置文件)
一个profile文件可以包含一个或多个服务,一个profile文件包含需要的服务信息或者是与设备如何交互的配置文件选项信息。设备的GAP和GATT的角色都可能在数据的交换过程中改变,因此,这个文件应该包含广播的种类、所使用的连接间隔、所需的安全等级等信息。
注:一个Profile中的属性表不能包含另一个属性表。
2.9 标准的定制服务和特性
SIG蓝牙技术联盟已经定义了一些profile、服务、特性和根据协议栈的GATT层定义的属性。但是协议栈中只实现了一部分应用的BLE服务。那就意味着,只要协议栈支持GATT,就可能为一个应用建立一个它需要的profile和服务。既然在一个应用中可以支持profile和服务,那么就可以在这个应用中建立一个个人定制的服务。
每个设备都包含以下必要的特征值和服务项:
PROFILE
Generic Access Service(Primary Service)
Device Name(Characteristic)
Apperance(Characteristic)
Generic Attribute Service(Primary Service)
Service Changed(Characteristic)
CCCD(Descriptor)
服务项这种类型本身并不包含数据,仅仅相当于是一个容器,用来容纳特征值。特征值用来保存用户数据,但是它也有自己的UUID。在处理特征值所携带的用户数据之前,需要先对特征值自身进行声明。
特征值在系统中的表达形式是:声明 + 特征值属性。比如上边Generic Access Service服务项的Device Name特征值,它在GATT数据库中表示方式是:
Characteristic 声明: 0x0002, 0x2803, access_property, 0x2A00
Characteristic 项: 0x0003, 0x2A00, access_property, data
其中,第一列2个字节代表属性句柄。第二列2个字节代表属性类型(UUID)。0x2803表示该项为"特征值的声明"。0x2A00表示该特征值是Device Name。在第一行特征值声明中,最后一项的属性值就是该特征值的UUID。可以看到,第一行声明里面的属性值0x2A00,而第二行属性值自己的属性类型(UUID)也是0x2A00,信息冗余了。原因是,假如特征值的UUID为自定义的16字节UUID,在特征值的属性类型(UUID)中,只能存放2个字节,而16字节的UUID全称,就只能存放在特征值声明的属性值项中。例如:
Characteristic 声明: 0x0002, 0x2803, access_property, 0x0000ABCD1212efde1523785feabcd123
Characteristic 项: 0x0003, 0xABCD, access_property, data
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[0]}} },
{ 0x0004u, 0x2803u /* Characteristic */, 0x00000201u /* rd */, 0x0005u, {{0x2A01u, NULL}} },
{ 0x0005u, 0x2A01u /* Appearance */, 0x00000201u /* rd */, 0x0005u, {{0x0002u, (void *)&cyBle_attValuesLen[1]}} },
{ 0x0006u, 0x2803u /* Characteristic */, 0x00000201u /* rd */, 0x0007u, {{0x2A04u, NULL}} },
{ 0x0007u, 0x2A04u /* Peripheral Preferred Connection Par */, 0x00000201u /* rd */, 0x0007u, {{0x0008u, (void *)&cyBle_attValuesLen[2]}} },
{ 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[3]}} },
{ 0x000Bu, 0x2902u /* Client Characteristic Configuration */, 0x00000A04u /* rd,wr */, 0x000Bu, {{0x0002u, (void *)&cyBle_attValuesLen[4]}} },
{ 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[5]}} },
{ 0x000Fu, 0x2901u /* Custom Descriptor */, 0x00010001u /* */, 0x000Fu, {{0x001Cu, (void *)&cyBle_attValuesLen[6]}} },
{ 0x0010u, 0x2902u /* Client Characteristic Configuration */, 0x00010A04u /* rd,wr */, 0x0010u, {{0x0002u, (void *)&cyBle_attValuesLen[7]}} },
};
通过注释可以看到,每个特征值都有声明和特征值项两部分组成。
由于每个属性的句柄是递增的,因此属性的声明顺序会影响句柄的计算。
描述符是特征值的补充信息,挂载在特征值之下,它可以开辟一段数据空间以携带数据,客户端可以像操作特征值一样对其进行读写,但是描述符弱于特征值,它不具备Notify/Write等读写属性,远不如特征值灵活。有一种描述符叫CCCD(Client Characteristic Configuration Description),当对特征值设置Notify或Indication时,用以控制Notify和Indication的使能情况。