BLE学习(2) - BLE应用层profile解析

profile架构

profile文件是应用层的直观体现,应用层由多个profile构成。
1个profile由多个服务(service)组成,而1个服务由多个特性(attribute)组成。
在这里插入图片描述
profile: 相当于一个大容器,容器里面包含了很多独立的服务。
服务: 一个服务属于某一大类功能,
特性:属于某一类服务的某一个具体功能。
打个比方,现在有个profile文件,里面包含电池电量监测功能、心率功能、温湿度传感器功能等。
而这些电池电量监测功能、心率功能、温湿度传感器功能等就是一个个具体的服务。
温湿度传感器服务可以包含温度读取、湿度读取等小功能,这一个个小功能就相当于一个个特性。
profile和服务都只是一个容器,我们真正操作去读写的是里面的特性值。

属性(Attribute)

属性的实质就是一条数据,也是BLE读写操作的最小单元。
BLE中规范了属性的数据组成结构,如下:
在这里插入图片描述

Attribute Handle

属性句柄是指向属性实体的指针,对端设备可以通过属性句柄来读写该属性下的值。
属性句柄值的范围为:0x0000 ~ 0xFFFF。

Attribute Type

属性类型的实质就是由已经规划好的特殊UUID值表示的,协议栈根据属性类型的UUID值来区分当前属性是属于服务项还是特性等。
BLE的属性类型主要有4大类:
在这里插入图片描述
Primary Service(首要服务项):0x2800
Secondary Service(次要服务项):0x2801
Include(包含服务项):0x2802
Characteristic(特性):0x2803

蓝牙SIG组规范了各类UUID的取值范围如下:
0x1800 – 0x26FF :服务项类型
0x2700 – 0x27FF :单位
0x2800 – 0x28FF :属性类型,比如该属性是属于服务还是特性
0x2900 – 0x29FF :描述符类型,是对特性的补充描述,比如客户端配置描述符CCCD = 0X2902
0x2A00 – 0x7FFF :特性值类型,表示值属性的UUID,这个是由开发者自行定义的
这样说可能还是有点不太好理解,我们对应下图可以看的更清晰些:
在这里插入图片描述

Attribute Value

属性值,就是该属性包含的数据有效负载。

Attribute Permissions

属性权限,它规定了本属性的读写权限,可以保护Attribute Value值。
权限属性:
访问权限(Access Permission)-只读,只写,读写
禁止访问权限(No Access)-禁止读或者写
加密权限(Encryption Permission)-加密,不加密
认证权限(Authentication Permission)-需要认证,无需认证。认证是指相互确认对方身份。完成认证流程的两个设备,双方建立信任关系,二者之间的通信通道即可认为是安全的。BLE中,认证过程就是配对。
授权权限(Authorization Permission)-需要授权,无需授权
签名权限(Signed Permission)-签名后才能读或写,这个用得比较少

Attribute Properties

Attribute Properties描述了值属性的操作方式,它在申明属性中被赋值给value,用来展示值属性具有的读写操作:
ESP_GATT_CHAR_PROP_BIT_READ: 可以读取特性
ESP_GATT_CHAR_PROP_BIT_WRITE: 特性可写
ESP_GATT_CHAR_PROP_BIT_NOTIFY: 特性可以通知值的变化

同时拥有权限(Attribute Permissions)和Attribute Properties似乎是多余的。
但是Attribute Properties是用来向客户端展示的信息,它更多的是提醒客户端该服务器特性是否支持读写请求。
从这个意义上来说,Attribute Properties充当客户端正确访问服务器资源的提示。
而权限(Attribute Permissions)是授予客户端读取或写入该属性的授权。例如,如果客户端尝试写入它没有写入权限的属性,即使设置了写入特性,服务器也会拒绝该请求。

实列分析profile

以下是esp32官方例程 ble_spp_server(串口透传)中的一段代码:

enum{
    SPP_IDX_SVC,
    SPP_IDX_SPP_DATA_RECV_CHAR,
    SPP_IDX_SPP_DATA_RECV_VAL,
    SPP_IDX_SPP_DATA_NOTIFY_CHAR,
    SPP_IDX_SPP_DATA_NTY_VAL,
    SPP_IDX_SPP_DATA_CLIENT_CFG,
    SPP_IDX_SPP_DATA_SRVR_CFG,
    SPP_IDX_NB,
};

#define CHAR_DECLARATION_SIZE   (sizeof(uint8_t))
#define ESP_GATT_UUID_SPP_DATA_RECEIVE      0x2BE0
#define ESP_GATT_UUID_SPP_DATA_NOTIFY       0x2BE1
static const uint16_t spp_service_uuid = 0x1877;
static const uint16_t primary_service_uuid = ESP_GATT_UUID_PRI_SERVICE; //0X2800
static const uint16_t character_declaration_uuid = ESP_GATT_UUID_CHAR_DECLARE; //0X2803
static const uint16_t character_client_config_uuid = ESP_GATT_UUID_CHAR_CLIENT_CONFIG; // 0X2902
static const uint16_t character_srvr_config_uuid = ESP_GATT_UUID_CHAR_SRVR_CONFIG; //0X2903

static const uint8_t char_prop_read_notify = ESP_GATT_CHAR_PROP_BIT_NOTIFY;
static const uint8_t char_prop_read_write = ESP_GATT_CHAR_PROP_BIT_WRITE_NR;
///SPP Service - data receive characteristic, read&write without response
static const uint16_t spp_data_receive_uuid = ESP_GATT_UUID_SPP_DATA_RECEIVE; //0X2BE0
static const uint8_t  spp_data_receive_val[20] = {0x00};

///SPP Service - data notify characteristic, notify&read
static const uint16_t spp_data_notify_uuid = ESP_GATT_UUID_SPP_DATA_NOTIFY; //0X2BEE
static const uint8_t  spp_data_notify_val[20] = {0x00};
static const uint8_t  spp_data_notify_ccc[2] = {0x00, 0x00};

///Full HRS Database Description - Used to add attributes into the database
static const esp_gatts_attr_db_t spp_gatt_db[SPP_IDX_NB] =
{
    //SPP -  Service Declaration
    [SPP_IDX_SVC]                      	=
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
    sizeof(spp_service_uuid), sizeof(spp_service_uuid), (uint8_t *)&spp_service_uuid}},

    //SPP -  data receive characteristic Declaration
    [SPP_IDX_SPP_DATA_RECV_CHAR]            =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
    CHAR_DECLARATION_SIZE,CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_write}},

    //SPP -  data receive characteristic Value
    [SPP_IDX_SPP_DATA_RECV_VAL]             	=
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&spp_data_receive_uuid, ESP_GATT_PERM_WRITE,
    SPP_DATA_MAX_LEN,sizeof(spp_data_receive_val), (uint8_t *)spp_data_receive_val}},

    //SPP -  data notify characteristic Declaration
    [SPP_IDX_SPP_DATA_NOTIFY_CHAR]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
    CHAR_DECLARATION_SIZE,CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_notify}},

    //SPP -  data notify characteristic Value
    [SPP_IDX_SPP_DATA_NTY_VAL]   =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&spp_data_notify_uuid, ESP_GATT_PERM_READ,
    SPP_DATA_MAX_LEN, sizeof(spp_data_notify_val), (uint8_t *)spp_data_notify_val}},

    //SPP -  data characteristic - Client Characteristic Configuration Descriptor
    [SPP_IDX_SPP_DATA_CLIENT_CFG]         =
    {{ESP_GATT_AUTO_RSP},{ESP_UUID_LEN_16,(uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
    sizeof(spp_data_notify_ccc),sizeof(spp_data_notify_ccc), (uint8_t *)spp_data_notify_ccc}},

    [SPP_IDX_SPP_DATA_SRVR_CFG]         =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_srvr_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
    sizeof(uint16_t),sizeof(spp_data_notify_ccc), (uint8_t *)spp_data_notify_ccc}},
};

结构体esp_gatts_attr_db_t 定义如下:

 typedef struct
 {
     uint16_t uuid_length;              /*!< UUID length */
     uint8_t  *uuid_p;                  /*!< UUID value */
     uint16_t perm;                     /*!< Attribute permission */
     uint16_t max_length;               /*!< Maximum length of the element*/
     uint16_t length;                   /*!< Current length of the element*/
     uint8_t  *value;                   /*!< Element value array*/
 } esp_attr_desc_t;
 
typedef struct
{
#define ESP_GATT_RSP_BY_APP             0
#define ESP_GATT_AUTO_RSP               1
    uint8_t auto_rsp;
} esp_attr_control_t;

typedef struct
{
    esp_attr_control_t      attr_control;                   /*!< The attribute control type */
    esp_attr_desc_t         att_desc;                       /*!< The attribute type */
} esp_gatts_attr_db_t;

我们先关注一下下图的红框:
在这里插入图片描述
一个特性(Attribute) 的组成实际是由声明属性 + 值属性 + 可选的CCCD组成的。

现在分析第1组数据:

    [SPP_IDX_SVC]                      	=
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
    sizeof(spp_service_uuid), sizeof(spp_service_uuid), (uint8_t *)&spp_service_uuid}},

ESP_GATT_AUTO_RSP: 这个是设置GATTT自动回复的选项,这里我们先不做解释
参考esp_attr_desc_t结构体可以知道:
ESP_UUID_LEN_16: 对应变量uuid_length,即uuid长度为16位
primary_service_uuid:对应变量uuid_p,即指向uuid值的指针,在代码中定义如下:

static const uint16_t primary_service_uuid = ESP_GATT_UUID_PRI_SERVICE; //0X2800

uuid_length和uuid_p这两个变量,其实就是定义了 Attribute Type,我们上面说过 Attribute Type实际就是由已经规划好的特殊uuid值决定的。那么现在UUID值为0x2800,参考UUID规划可知,这个协议栈已经定义好的UUID值表示首要服务类型。

ESP_GATT_PERM_READ:对应变量perm,即这个首要服务的权限为仅读权限。
第一个sizeof(spp_service_uuid): 对应变量max_length,即value支持的最大长度
第二个sizeof(spp_service_uuid): 对应变量length,即value的实际长度
spp_service_uuid:对应变量value的值,赋值如下:

static const uint16_t spp_service_uuid = 0x1877;

在服务申明时,value表示该服务的UUID值为0x1877。

到此,这组数据分析完毕。
这组数据放在此结构体数组的首位置,其作用是申明一个服务的开始,并设置服务UUID的值,那么遇到下一个服务申明之前的Attribute 都是对此服务内的特性进行描述和设置。

接下来分析第2组数据:

    //SPP -  data receive characteristic Declaration
    [SPP_IDX_SPP_DATA_RECV_CHAR]            =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
    CHAR_DECLARATION_SIZE,CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_write}},

同样参考esp_attr_desc_t结构体:
ESP_UUID_LEN_16: 对应变量uuid_length,即uuid长度为16位
character_declaration_uuid:对应变量uuid_p,即指向uuid值的指针,在代码中定义如下:

static const uint16_t character_declaration_uuid = ESP_GATT_UUID_CHAR_DECLARE; //0X2803

参考第1组数据的分析,我们可以得出这是一个特性申明属性。
ESP_GATT_PERM_READ: 表示这个特性值的权限设置成仅读。
第一个CHAR_DECLARATION_SIZE: 对应变量max_length,即value支持的最大长度为1byte
第二个CHAR_DECLARATION_SIZE: 对应变量length,即value的实际长度为1byte
注意,接下来的变量value含义与第1组数据的服务申明不同:
在特性申明时,value用作该特性的读写申明(Attribute Properties),也就是我们上面说过的展示给客户端该特性具有的读写操作的一种提示信息。

static const uint8_t char_prop_read_write = ESP_GATT_CHAR_PROP_BIT_WRITE_NR;

那么根据char_prop_read_write 的值,提示了客户端此特性具有无回应写的特性。

接下来分析第3组数据:

    //SPP -  data receive characteristic Value
    [SPP_IDX_SPP_DATA_RECV_VAL]             	=
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&spp_data_receive_uuid, ESP_GATT_PERM_WRITE,
    SPP_DATA_MAX_LEN,sizeof(spp_data_receive_val), (uint8_t *)spp_data_receive_val}},

ESP_UUID_LEN_16: 对应变量uuid_length,即uuid长度为16位
spp_data_receive_uuid:对应变量uuid_p,即指向uuid值的指针,在代码中定义如下:

static const uint16_t spp_data_receive_uuid = ESP_GATT_UUID_SPP_DATA_RECEIVE; //0X2BE0

UUID值为0X2BE0,我们参考上面的UUID值规划: 0x2A00 – 0x7FFF :表示特性值类型
说明这是一个特性值属性。
ESP_GATT_PERM_WRITE: 表示这个特性值的权限设置成写。
SPP_DATA_MAX_LEN:表示最大支持的写入数据长度
sizeof(spp_data_receive_val): 对应变量length,即写缓冲区的buffer大小
spp_data_receive_val: 表示写缓冲区的buffer
所以,我们也应该注意到,特性值属性的变量value含义与上面两组数据的vaule含义又不同!
至此,首要服务的第一个特性,包含申明属性+值属性,跟第2组数据和第3组数据就对应起来了。由于此特性在申明的时候它的特性描述是无回应写ESP_GATT_CHAR_PROP_BIT_WRITE_NR,不支持通知和指示,故没有CCCD属性。

我们继续分析第4组数据:

  //SPP -  data notify characteristic Declaration
    [SPP_IDX_SPP_DATA_NOTIFY_CHAR]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
    CHAR_DECLARATION_SIZE,CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_notify}},

先确定Attribute 类型, character_declaration_uuid的定义如下:

static const uint16_t character_declaration_uuid = ESP_GATT_UUID_CHAR_DECLARE; //0X2803

跟第2组数据一样的类型,2803在UUID规范里面表示的是特性申明属性。
那么,根据我们分析第2组时说过的,在特性申明时,value用作申明该特性的读写特性(Attribute Properties),也就是我们上面说过的展示给客户端读写特性的一种提示。
char_prop_read_notify定义如下:

static const uint8_t char_prop_read_notify = ESP_GATT_CHAR_PROP_BIT_NOTIFY;

说明这是一个支持通知的特性。
那么通知特性就必须带CCCD描述了。
这里第5组数据就是特性值属性,跟第3组一样的分析。
我们重点来看下通知特性多出的CCCD描述,即第6组数据:

    //SPP -  data characteristic - Client Characteristic Configuration Descriptor
    [SPP_IDX_SPP_DATA_CLIENT_CFG]         =
    {{ESP_GATT_AUTO_RSP},{ESP_UUID_LEN_16,(uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
    sizeof(spp_data_notify_ccc),sizeof(spp_data_notify_ccc), (uint8_t *)spp_data_notify_ccc}},

character_client_config_uuid定义如下:

static const uint16_t character_client_config_uuid = ESP_GATT_UUID_CHAR_CLIENT_CONFIG; // 0X2902

根据UUID规划,0X2902在描述符类型这个UUID范围,而0x2902在BLE协议栈规范里面被定义为客户端配置描述符。
spp_data_notify_ccc定义如下:

static const uint8_t  spp_data_notify_ccc[2] = {0x00, 0x00};

我们可以读取或写入数据到这个buffer里面。
我们需要特别注意的是:0X2902这个客户端特性配置表述符(Client Characteristic Configuration Descriptor,CCCD),这个描述符是给任何支持通知和指示功能的特性额外增加的。在CCCD中写入“1”使能通知功能,写入“2”使能指示功能,写入”0“同时禁止通知和指示功能。这也是为啥带通知和指示的特性需要带上CCCD的原因。

这里我自己也存在疑问:为啥官方例程中spp_data_notify_ccc这个buffer的长度是2byte?spp_data_notify_ccc内的值应该就是写"1"或写"2"或写"0",去开启指示和通知,应该是1byte才对。

上面说了这么多,我们现在总结一下:

  1. 关于value的含义
  • 在服务申明时,value表示服务UUID值
  • 在特征属性申明时,value表示给客户端展示的读写特性
  • 在特征属性值时,vaule表示特征值的操作buffer
  • 在特征属性描述符CCCD中,value充当通知或指示的使能开关
  1. 关于上述结构体的剖析图:
    服务申明
  2. 此结构体定义了1个首要服务项;
  3. 首要服务项包含两个特性
  4. 特性1包含特性申明+特性值,特性2由于带通知操作,包含特性声明+特性值+特性描述(CCCD)

本例程是串口透传服务实例代码,综合例程来看:
我们profile文件包含1个服务,为串口透传服务。串口透传服务包含两个特性,1个是串口发送,1个是串口接收。
串口接收特性为特性1,此特性的特性值具有ESP_GATT_PERM_WRITE,即写权限,通过客户端写数据到特性值。
串口发送特性为特性2,此特性通过通知操作,当蓝牙芯片串口有数据需要发送时,将数据写入特性值内,并通过Notify通知方式,将数据主动发送给客户端。
注意: 在此例程蓝牙芯片充当从机角色。一般情况下,数据的交互都是由主机主动发起的,主机通过读、写操作与从机进行通讯。
当从机需要主动发送数据到主机时,只能通过通知(notify)或指示(indicate)的方式,其中通知是无需主机回应的,而指示在发送后,会等待主机回应。故我们在此选择了通知的方式向主机主动发送数据

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值