• 蓝牙BLE: ATT协议层中属性(Attribute)


    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-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的真实值。

    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)

    服务项这种类型本身并不包含数据,仅仅相当于是一个容器,用来容纳特征值。特征值用于保存用户数据,但它也有自己的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[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的使能情况。

    关于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 0-5:操作属性的方法
    bit 6:Command 标识位
    bit 7:Authentication Signature标识位
    Attribute Parameters:
    如果Attribute Opcode中身份验证签名标记位为0,则X = 1;
    如果Attribute Opcode中身份验证签名标记位为1,则X = 13;
    Authentication Signature:
    属性操作码和属性参数的可选身份验证签名

    参考链接:

    1. 蓝牙BLE实用教程

    2. BLE协议栈-ATT

    3. BLE ATT和GATT小结

    4. BLE 5 协议栈-属性协议层(ATT)

  • 相关阅读:
    条款十四 在资源管理类中小心copying行为
    条款八 别让异常逃离析构函数
    条款五 了解C++默默的编写并调用的哪些函数
    volatile——C++关键字(转)
    C++ auto_ptr(转)
    条款十三 以对象管理资源
    优秀文章收集(更新中..)
    条款十一 在operator = 中处理"自我赋值"
    TCP协议疑难杂症全景解析(转)
    大四的迷茫
  • 原文地址:https://www.cnblogs.com/yongdaimi/p/11984359.html
Copyright © 2020-2023  润新知