- 下面内容来自GPT的解析,留作BLE的学习
一、各个函数的功能
1. bas_init()
ble_service_t *bas_init(const ble_service_config_t *config, const bas_battery_info_t *info);
功能:
- 初始化电池服务(BAS)。
- 注册电池服务的GATT特性和描述符。
- 设置初始的电池电量值。
主要步骤:
- 创建服务对象:分配并初始化
bat_service_t
结构体,用于存储服务相关的数据。 - 设置服务回调:将相关事件处理函数(如连接、读写请求、清理等)绑定到服务中。
- 计算属性数量:根据传入的
config
和info
来计算服务中包含的属性数量。 - 添加GATT服务:通过调用
ble_gatts_add_service()
添加一个GATT服务。 - 添加特性和描述符:
- 电池电量特性:支持读取和通知,添加到服务中。
- 客户端特性配置描述符(CCC):用于管理通知的启用和禁用状态。
- 如果
info
存在,还会添加内容展示格式描述符(CPF)。
- 注册服务:通过调用
ble_gatts_register_service()
将服务注册到GATT数据库中。 - 设置初始电池电量值:使用
ble_gatts_set_value()
函数确保电池电量特性有一个初始值。
2. handle_connected_evt()
static void handle_connected_evt(ble_service_t *svc, const ble_evt_gap_connected_t *evt);
功能:
- 处理设备连接事件。
- 检查是否需要发送电池电量通知。
主要步骤:
- 获取当前电池电量值:从GATT服务器获取当前的电池电量值。
- 比较存储的电量值:检查存储在
ble_storage
中的电池电量值是否与当前值不同。 - 发送通知:如果值不同,则调用
notify_level()
函数发送电池电量通知。
3. handle_read_req()
static void handle_read_req(ble_service_t *svc, const ble_evt_gatts_read_req_t *evt);
功能:
- 处理读请求事件。
- 返回请求的属性值。
主要步骤:
- 检查请求的句柄:根据请求的句柄,判断客户端请求的是哪个属性。
- 读取描述符的值:如果请求的是CCC描述符,则从存储中获取当前配置并返回。
- 确认读操作:调用
ble_gatts_read_cfm()
函数发送读操作的确认消息,并返回对应的值。
4. handle_write_req()
static void handle_write_req(ble_service_t *svc, const ble_evt_gatts_write_req_t *evt);
功能:
- 处理写请求事件。
- 更新请求的属性值。
主要步骤:
- 检查请求的句柄:判断客户端要写入的属性是哪一个。
- 处理写入操作:调用
do_bl_ccc_write()
函数处理对CCC描述符的写入请求。 - 确认写操作:调用
ble_gatts_write_cfm()
函数发送写操作的确认消息,并返回操作结果。
5. do_bl_ccc_write()
static att_error_t do_bl_ccc_write(bat_service_t *bas, uint16_t conn_idx, uint16_t offset, uint16_t length, const uint8_t *value);
功能:
- 处理客户端对CCC描述符的写入请求。
- 更新存储中的配置。
主要步骤:
- 检查偏移和长度:确认写入操作的偏移和数据长度是否有效。
- 解析CCC值:将客户端传入的值转换为16位的配置值。
- 更新存储:调用
ble_storage_put_u32()
将新的CCC配置值存储起来。
6. notify_level()
static void notify_level(ble_service_t *svc, uint16_t conn_idx, uint8_t level);
功能:
- 发送电池电量通知。
主要步骤:
- 检查通知是否启用:从存储中读取CCC值,检查是否启用了通知。
- 发送通知:如果启用了通知,调用
ble_gatts_send_event()
向客户端发送电池电量通知。
7. bas_notify_level()
void bas_notify_level(ble_service_t *svc, uint16_t conn_idx);
功能:
- 主动向指定连接设备发送电池电量通知。
主要步骤:
- 获取当前电池电量值:从GATT服务器获取当前的电池电量值。
- 发送通知:调用
notify_level()
函数发送电池电量通知。 - 更新存储:将新的电池电量值存储到
ble_storage
中。
8. bas_set_level()
void bas_set_level(ble_service_t *svc, uint8_t level, bool notify);
功能:
- 设置电池电量值,并根据需要发送通知。
主要步骤:
- 验证电池电量值:确保电池电量值在有效范围内(0到100)。
- 更新GATT值:将新的电池电量值写入到GATT服务器中。
- 通知连接的设备:如果
notify
参数为true
,则向所有连接的设备发送通知。 - 更新存储:将新的电池电量值存储到
ble_storage
中。
9. cleanup()
static void cleanup(ble_service_t *svc);
功能:
- 清理电池服务资源。
主要步骤:
- 删除存储:删除与电池电量和CCC描述符相关的存储数据。
- 释放内存:释放服务结构体的内存。
二、完整代码
/**
****************************************************************************************
*
* @file bas.c
*
* @brief Battery Service sample implementation
*
* Copyright (C) 2015-2021 Dialog Semiconductor.
* This computer program includes Confidential, Proprietary Information
* of Dialog Semiconductor. All Rights Reserved.
*
****************************************************************************************
*/
#if defined(CONFIG_USE_BLE_SERVICES)
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include "osal.h"
#include "ble_att.h"
#include "ble_bufops.h"
#include "ble_common.h"
#include "ble_gatt.h"
#include "ble_gatts.h"
#include "ble_storage.h"
#include "ble_uuid.h"
#include "svc_defines.h"
#include "bas.h"
#define UUID_BATTERY_LEVEL (0x2A19)
typedef struct {
ble_service_t svc;
// handles
uint16_t bl_val_h;
uint16_t bl_ccc_h;
} bat_service_t;
static att_error_t do_bl_ccc_write(bat_service_t *bas, uint16_t conn_idx, uint16_t offset,
uint16_t length, const uint8_t *value)
{
uint16_t ccc;
if (offset) {
return ATT_ERROR_ATTRIBUTE_NOT_LONG;
}
if (length != sizeof(ccc)) {
return ATT_ERROR_APPLICATION_ERROR;
}
ccc = get_u16(value);
ble_storage_put_u32(conn_idx, bas->bl_ccc_h, ccc, true);
return ATT_ERROR_OK;
}
static void notify_level(ble_service_t *svc, uint16_t conn_idx, uint8_t level);
static void handle_connected_evt(ble_service_t *svc, const ble_evt_gap_connected_t *evt)
{
bat_service_t *bas = (bat_service_t *) svc;
uint8_t level = 0x00;
uint16_t level_len = sizeof(level);
uint8_t prev_level;
ble_error_t err;
ble_gatts_get_value(bas->bl_val_h, &level_len, &level);
prev_level = level;
err = ble_storage_get_u8(evt->conn_idx, bas->bl_val_h, &prev_level);
if (BLE_STATUS_OK == err && prev_level != level) {
notify_level(svc, evt->conn_idx, level);
}
ble_storage_put_u32(evt->conn_idx, bas->bl_val_h, level, true);
}
static void handle_read_req(ble_service_t *svc, const ble_evt_gatts_read_req_t *evt)
{
bat_service_t *bas = (bat_service_t *) svc;
if (evt->handle == bas->bl_ccc_h) {
uint16_t ccc = 0x0000;
ble_storage_get_u16(evt->conn_idx, bas->bl_ccc_h, &ccc);
// we're little-endian, ok to write directly from uint16_t
ble_gatts_read_cfm(evt->conn_idx, evt->handle, ATT_ERROR_OK, sizeof(ccc), &ccc);
} else {
ble_gatts_read_cfm(evt->conn_idx, evt->handle, ATT_ERROR_READ_NOT_PERMITTED, 0, NULL);
}
}
static void handle_write_req(ble_service_t *svc, const ble_evt_gatts_write_req_t *evt)
{
bat_service_t *bas = (bat_service_t *) svc;
att_error_t status = ATT_ERROR_ATTRIBUTE_NOT_FOUND;
if (evt->handle == bas->bl_ccc_h) {
status = do_bl_ccc_write(bas, evt->conn_idx, evt->offset, evt->length, evt->value);
goto done;
}
done:
ble_gatts_write_cfm(evt->conn_idx, evt->handle, status);
}
static void cleanup(ble_service_t *svc)
{
bat_service_t *bas = (bat_service_t *) svc;
ble_storage_remove_all(bas->bl_ccc_h);
ble_storage_remove_all(bas->bl_val_h);
OS_FREE(bas);
}
ble_service_t *bas_init(const ble_service_config_t *config, const bas_battery_info_t *info)
{
uint16_t num_descr;
uint16_t num_attr;
uint16_t cpf_h = 0;
bat_service_t *bas;
att_uuid_t uuid;
uint8_t level = 0;
bas = OS_MALLOC(sizeof(*bas));
memset(bas, 0, sizeof(*bas));
bas->svc.connected_evt = handle_connected_evt;
bas->svc.read_req = handle_read_req;
bas->svc.write_req = handle_write_req;
bas->svc.cleanup = cleanup;
// Content Presentation Format descriptor present if 'info' is set
num_descr = (info ? 2 : 1);
num_attr = ble_service_get_num_attr(config, 1, num_descr);
ble_uuid_create16(UUID_SERVICE_BAS, &uuid);
ble_gatts_add_service(&uuid, GATT_SERVICE_PRIMARY, num_attr);
ble_service_config_add_includes(config);
ble_uuid_create16(UUID_BATTERY_LEVEL, &uuid);
ble_gatts_add_characteristic(&uuid, GATT_PROP_READ | GATT_PROP_NOTIFY,
ble_service_config_elevate_perm(ATT_PERM_READ, config),
1, 0, NULL, &bas->bl_val_h);
ble_uuid_create16(UUID_GATT_CLIENT_CHAR_CONFIGURATION, &uuid);
ble_gatts_add_descriptor(&uuid, ATT_PERM_RW, 2, 0, &bas->bl_ccc_h);
if (info) {
ble_uuid_create16(UUID_GATT_CHAR_PRESENTATION_FORMAT, &uuid);
ble_gatts_add_descriptor(&uuid,
ble_service_config_elevate_perm(ATT_PERM_READ, config),
7, 0, &cpf_h);
}
ble_gatts_register_service(&bas->svc.start_h, &bas->bl_val_h, &bas->bl_ccc_h, &cpf_h, 0);
/* Set initial value for battery level so we always have proper characteristic value set. */
ble_gatts_set_value(bas->bl_val_h, sizeof(level), &level);
if (info) {
uint8_t cpf_val[7];
uint8_t *p = cpf_val;
put_u8_inc(&p, 0x04); // Format=unsigned 8-bit integer
put_u8_inc(&p, 0x00); // Exponent=0
put_u16_inc(&p, 0x27AD); // Unit=percentage
put_u8_inc(&p, info->name_space);
put_u16_inc(&p, info->descriptor);
// Content Presentation Format descriptor has static value
ble_gatts_set_value(cpf_h, sizeof(cpf_val), cpf_val);
}
bas->svc.end_h = bas->svc.start_h + num_attr;
ble_service_add(&bas->svc);
return &bas->svc;
}
static void notify_level(ble_service_t *svc, uint16_t conn_idx, uint8_t level)
{
bat_service_t *bas = (bat_service_t *) svc;
uint16_t ccc = 0x0000;
ble_storage_get_u16(conn_idx, bas->bl_ccc_h, &ccc);
if (!(ccc & GATT_CCC_NOTIFICATIONS)) {
return;
}
ble_gatts_send_event(conn_idx, bas->bl_val_h, GATT_EVENT_NOTIFICATION, sizeof(level), &level);
}
void bas_notify_level(ble_service_t *svc, uint16_t conn_idx)
{
bat_service_t *bas = (bat_service_t *) svc;
uint8_t level = 0x00;
uint16_t level_len = sizeof(level);
ble_gatts_get_value(bas->bl_val_h, &level_len, &level);
notify_level(svc, conn_idx, level);
ble_storage_put_u32(conn_idx, bas->bl_val_h, level, true);
}
void bas_set_level(ble_service_t *svc, uint8_t level, bool notify)
{
bat_service_t *bas = (bat_service_t *) svc;
uint8_t prev_level = 0x00;
uint16_t prev_level_len = sizeof(prev_level);
uint8_t num_conn;
uint16_t *conn_idx;
if (level > 100) {
return;
}
ble_gatts_get_value(bas->bl_val_h, &prev_level_len, &prev_level);
if (level == prev_level) {
return;
}
ble_gatts_set_value(bas->bl_val_h, sizeof(level), &level);
/*
* for each connected device we need to:
* - notify new value, if requested by caller
* - put new value to storage to use when device is reconnected
*/
ble_gap_get_connected(&num_conn, &conn_idx);
while (num_conn--) {
if (notify) {
notify_level(svc, conn_idx[num_conn], level);
}
ble_storage_put_u32(conn_idx[num_conn], bas->bl_val_h, level, true);
}
if (conn_idx) {
OS_FREE(conn_idx);
}
}
#endif /* defined(CONFIG_USE_BLE_SERVICES) */
三、逐行代码解析
1. bas_init()
ble_service_t *bas_init(const ble_service_config_t *config, const bas_battery_info_t *info)
ble_service_t *bas_init(...)
: 这是电池服务的初始化函数,返回一个指向ble_service_t
类型的指针。该指针代表初始化后的电池服务实例。
uint16_t num_descr;
uint16_t num_attr;
uint16_t cpf_h = 0;
bat_service_t *bas;
att_uuid_t uuid;
uint8_t level = 0;
uint16_t num_descr, num_attr, cpf_h = 0;
: 声明和初始化本地变量,用于存储描述符数量、属性数量,以及CPF描述符的句柄。bat_service_t *bas;
: 声明一个指向bat_service_t
结构体的指针,代表电池服务实例。att_uuid_t uuid;
: 声明一个UUID结构体变量,用于存储服务和特性的UUID。uint8_t level = 0;
: 初始化电池电量值为0。
bas = OS_MALLOC(sizeof(*bas));
memset(bas, 0, sizeof(*bas));
OS_MALLOC(sizeof(*bas));
: 动态分配内存,为bat_service_t
结构体分配足够的空间。memset(bas, 0, sizeof(*bas));
: 将分配的内存区域清零,初始化服务实例结构体的所有成员为0。
bas->svc.connected_evt = handle_connected_evt;
bas->svc.read_req = handle_read_req;
bas->svc.write_req = handle_write_req;
bas->svc.cleanup = cleanup;
bas->svc.connected_evt = handle_connected_evt;
: 将连接事件处理函数handle_connected_evt
赋值给服务实例的connected_evt
成员。bas->svc.read_req = handle_read_req;
: 将读请求处理函数handle_read_req
赋值给服务实例的read_req
成员。bas->svc.write_req = handle_write_req;
: 将写请求处理函数handle_write_req
赋值给服务实例的write_req
成员。bas->svc.cleanup = cleanup;
: 将清理函数cleanup
赋值给服务实例的cleanup
成员。
num_descr = (info ? 2 : 1);
num_descr = (info ? 2 : 1);
: 如果info
不为空,表示需要添加两个描述符(CCC和CPF);否则只添加一个(CCC)。
num_attr = ble_service_get_num_attr(config, 1, num_descr);
num_attr = ble_service_get_num_attr(config, 1, num_descr);
: 计算服务中总共包含的属性数量(1个特性加上描述符)。-
ble_service_get_num_attr
是一个用于计算 BLE 服务中属性(Attributes)数量的函数。在 BLE 服务中,属性通常包括特性(Characteristic)和描述符(Descriptor),这些属性需要在服务初始化时注册到 GATT 表中。函数作用
ble_service_get_num_attr
函数通过传递的ble_service_config_t
配置结构来计算服务所需的总属性数量。这个数量通常用于在注册服务时告诉 GATT 表这个服务会占用多少属性空间。参数含义
- config(
const ble_service_config_t *config
):指向服务配置结构体的指针,包含了服务的安全级别、包含的服务数量等信息。- 如果
config
为NULL
,说明服务不依赖任何配置,函数将根据默认值来计算属性数量。 - 如果
config
不为空,函数将考虑配置中的服务类型、安全级别、包含的服务等来计算总的属性数量。
- 如果
-
情况分析
-
config
为空 (NULL
) 的情况:- 函数可能会使用默认的属性数量。这意味着它只计算服务本身的属性数量,不会包括任何额外的包含服务或其他特殊配置。比如,仅计算服务的特性和描述符数量。
-
config
不为空的情况:- 函数将依据
config
中的内容计算属性数量。这可能包括:- 任何包含的服务 (
num_includes
)。 - 服务的安全级别 (
sec_level
)。 - 特殊的描述符或特性。
- 任何包含的服务 (
- 函数将依据
ble_uuid_create16(UUID_SERVICE_BAS, &uuid);
ble_uuid_create16(UUID_SERVICE_BAS, &uuid);
: 使用16位UUID创建电池服务的UUID,并将其存储在uuid
变量中
ble_gatts_add_service(&uuid, GATT_SERVICE_PRIMARY, num_attr);
ble_gatts_add_service(&uuid, GATT_SERVICE_PRIMARY, num_attr);
: 将电池服务添加到GATT数据库中,指定为主服务,并指定属性数量。
ble_service_config_add_includes(config);
ble_service_config_add_includes(config);
: 添加包含的服务(如果有)。
ble_uuid_create16(UUID_BATTERY_LEVEL, &uuid);
ble_gatts_add_characteristic(&uuid, GATT_PROP_READ | GATT_PROP_NOTIFY,
ble_service_config_elevate_perm(ATT_PERM_READ, config),
1, 0, NULL, &bas->bl_val_h);
ble_uuid_create16(UUID_BATTERY_LEVEL, &uuid);
: 使用16位UUID创建电池电量特性的UUID。ble_gatts_add_characteristic(...)
: 将电池电量特性添加到服务中,特性支持读取和通知,并设置权限,最后将特性的句柄存储在bl_val_h
中。
ble_uuid_create16(UUID_GATT_CLIENT_CHAR_CONFIGURATION, &uuid);
ble_gatts_add_descriptor(&uuid, ATT_PERM_RW, 2, 0, &bas->bl_ccc_h);
ble_uuid_create16(UUID_GATT_CLIENT_CHAR_CONFIGURATION, &uuid);
: 使用16位UUID创建客户端特性配置描述符(CCC)的UUID。ble_gatts_add_descriptor(...)
: 将CCC描述符添加到服务中,设置读写权限,并将描述符的句柄存储在bl_ccc_h
中。GATT_PROP_NOTIFY
:- 这一标志表示该特性(Characteristic)支持通知功能。通知是一种单向的通信机制,即服务器可以主动向客户端发送数据,而无需客户端请求(如读取请求)。通知的使用在 BLE 应用中非常常见,特别是在传感器数据、状态更新等实时性要求较高的场景中。
-
CCC(客户端特性配置描述符)与通知属性的关系
- CCC(Client Characteristic Configuration,客户端特性配置描述符):
- CCC 是一个特殊的描述符,用来配置客户端是否希望接收来自特性值的通知或指示。当特性支持通知(或指示)时,必须为该特性添加 CCC 描述符。客户端可以通过写入 CCC 来启用或禁用通知。
- 关系:
- 如果特性支持
GATT_PROP_NOTIFY
属性,那么通常会自动添加一个 CCC 描述符,以允许客户端配置是否启用通知。如果没有 CCC 描述符,客户端将无法启用通知功能,即使特性支持通知。 - CCC 描述符是 16 位值,其中最低位(0x0001)表示是否启用通知。当客户端写入 0x0001 到 CCC 描述符时,通知就会被启用,服务器可以开始向客户端发送通知。
- 如果特性支持
-
在
NRF Connect
上,当 GATT 特性支持GATT_PROP_NOTIFY
(通知属性)时,特性旁边会显示一个小铃铛图标。这意味着该特性支持通知。用户可以点击这个图标来启用或禁用对该特性的通知。当启用通知时,设备会自动将特性值的变化推送给客户端,而不需要客户端轮询读取。启用通知后,客户端会收到该特性值的更新数据,这些数据会显示在特性值旁边。
CCC
(Client Characteristic Configuration)描述符用于管理通知和指示功能的启用与禁用。当你在NRF Connect
上点击那个小铃铛图标时,实际上是向设备写入了CCC
描述符的值,启用或禁用了该特性的通知功能。
if (info) {
ble_uuid_create16(UUID_GATT_CHAR_PRESENTATION_FORMAT, &uuid);
ble_gatts_add_descriptor(&uuid,
ble_service_config_elevate_perm(ATT_PERM_READ, config),
7, 0, &cpf_h);
}
if (info)
: 如果info
不为空,表示需要添加内容展示格式描述符(CPF)。ble_uuid_create16(UUID_GATT_CHAR_PRESENTATION_FORMAT, &uuid);
: 使用16位UUID创建CPF描述符的UUID。ble_gatts_add_descriptor(...)
: 将CPF描述符添加到服务中,设置读取权限,并将描述符的句柄存储在cpf_h
中。
ble_gatts_register_service(&bas->svc.start_h, &bas->bl_val_h, &bas->bl_ccc_h, &cpf_h, 0);
ble_gatts_register_service(...)
: 注册电池服务,将特性和描述符的句柄传递给GATT服务器,并获取服务的开始句柄。- 在
ble_gatts_register_service()
函数调用的最后一个参数0
,表示的是服务注册时的终止参数,用来告诉函数注册的属性列表已经结束。这个参数通常为0
或者NULL
,根据具体的函数原型和需求来决定。
ble_gatts_set_value(bas->bl_val_h, sizeof(level), &level);
ble_gatts_set_value(...)
: 设置电池电量特性的初始值为0,以确保服务初始化时该特性具有有效值。
if (info) {
uint8_t cpf_val[7];
uint8_t *p = cpf_val;
put_u8_inc(&p, 0x04); // Format=unsigned 8-bit integer
put_u8_inc(&p, 0x00); // Exponent=0
put_u16_inc(&p, 0x27AD); // Unit=percentage
put_u8_inc(&p, info->name_space);
put_u16_inc(&p, info->descriptor);
// Content Presentation Format descriptor has static value
ble_gatts_set_value(cpf_h, sizeof(cpf_val), cpf_val);
}
if (info)
: 如果info
不为空,设置CPF描述符的值。put_u8_inc(&p, 0x04);
: 设置格式为无符号8位整数。put_u8_inc(&p, 0x00);
: 设置指数为0。put_u16_inc(&p, 0x27AD);
: 设置单位为百分比。put_u8_inc(&p, info->name_space);
: 设置命名空间。put_u16_inc(&p, info->descriptor);
: 设置描述符。ble_gatts_set_value(cpf_h, sizeof(cpf_val), cpf_val);
: 将设置好的CPF值写入描述符。
bas->svc.end_h = bas->svc.start_h + num_attr;
bas->svc.end_h = bas->svc.start_h + num_attr;
: 计算服务的结束句柄。
ble_service_add(&bas->svc);
ble_service_add(&bas->svc);
: 将初始化后的服务添加到BLE堆栈中,使其可用。
return &bas->svc;
return &bas->svc;
: 返回初始化完成的服务实例指针。
大致流程:
gatts_add_service
→ble_service_config_add_includes
→gatts_add_characteristic
→ble_gatts_add_descriptor
→ble_gatts_register_service
→ble_service_add;
2. handle_connected_evt()
static void handle_connected_evt(ble_service_t *svc, const ble_evt_gap_connected_t *evt)
{
bat_service_t *bas = (bat_service_t *) svc;
uint8_t level = 0x00;
uint16_t level_len = sizeof(level);
uint8_t prev_level;
ble_error_t err;
ble_gatts_get_value(bas->bl_val_h, &level_len, &level);
prev_level = level;
err = ble_storage_get_u8(evt->conn_idx, bas->bl_val_h, &prev_level);
if (BLE_STATUS_OK == err && prev_level != level) {
notify_level(svc, evt->conn_idx, level);
}
ble_storage_put_u32(evt->conn_idx, bas->bl_val_h, level, true);
}
这个函数确保设备连接后,连接的设备可以接收到最新的电池电量信息,并且在电量发生变化时得到通知。
-
获取当前电池电量值: 函数首先从 GATT 特性句柄
bl_val_h
中读取当前的电池电量值。 -
检查是否需要发送通知: 它从存储中获取该连接设备上次保存的电池电量值,并将其与当前值进行比较。如果值发生了变化(即当前值与存储的值不同),则会通过调用
notify_level
函数向连接设备发送电池电量更新通知。 -
更新存储中的电池电量值: 最后,无论电池电量是否发生变化,都会将当前电池电量值保存到存储中,以便下次连接时使用。
3. handle_read_req()
static void handle_read_req(ble_service_t *svc, const ble_evt_gatts_read_req_t *evt)
{
bat_service_t *bas = (bat_service_t *) svc;
if (evt->handle == bas->bl_ccc_h) {
uint16_t ccc = 0x0000;
ble_storage_get_u16(evt->conn_idx, bas->bl_ccc_h, &ccc);
// we're little-endian, ok to write directly from uint16_t
ble_gatts_read_cfm(evt->conn_idx, evt->handle, ATT_ERROR_OK, sizeof(ccc), &ccc);
} else {
ble_gatts_read_cfm(evt->conn_idx, evt->handle, ATT_ERROR_READ_NOT_PERMITTED, 0, NULL);
}
}
-
- 这个函数是当设备接收到客户端的读请求(GATT read request)时,处理该请求并返回相应的数据或错误。
-
类型转换:
bat_service_t *bas = (bat_service_t *) svc;
:将svc
转换为bat_service_t
类型的指针,这样可以访问电池服务特定的属性和句柄。
-
检查请求的句柄:
if (evt->handle == bas->bl_ccc_h)
:检查客户端请求的句柄是否等于bl_ccc_h
(即Client Characteristic Configuration Descriptor, CCC的句柄)。CCC通常用于存储客户端是否开启通知或指示功能。
-
从存储中获取数据:
ble_storage_get_u16(evt->conn_idx, bas->bl_ccc_h, &ccc);
:从持久化存储中读取与当前连接(conn_idx
)和CCC句柄(bl_ccc_h
)相关联的16位数据(ccc
),并将其存储到变量ccc
中。这个数据通常代表客户端配置的通知/指示状态。
-
确认读请求:
ble_gatts_read_cfm(evt->conn_idx, evt->handle, ATT_ERROR_OK, sizeof(ccc), &ccc);
:确认读请求,并将ccc
的值返回给客户端。ATT_ERROR_OK
表示读请求成功,sizeof(ccc)
表示返回数据的长度,&ccc
是要返回的数据指针。
-
处理其他情况:
else
块用于处理读请求的句柄不是bl_ccc_h
的情况。在这种情况下,返回ATT_ERROR_READ_NOT_PERMITTED
错误,表示读操作不被允许。
4. handle_write_req()
static void handle_write_req(ble_service_t *svc, const ble_evt_gatts_write_req_t *evt)
{
bat_service_t *bas = (bat_service_t *) svc;
att_error_t status = ATT_ERROR_ATTRIBUTE_NOT_FOUND;
if (evt->handle == bas->bl_ccc_h) {
status = do_bl_ccc_write(bas, evt->conn_idx, evt->offset, evt->length, evt->value);
goto done;
}
done:
ble_gatts_write_cfm(evt->conn_idx, evt->handle, status);
}
-
函数目的:
- 这个函数的作用是在设备接收到客户端的写请求(GATT write request)时,根据客户端请求的句柄(
handle
)确定要写入的数据类型,并调用相应的处理函数来执行写操作。完成后,返回写入操作的结果给客户端。
- 这个函数的作用是在设备接收到客户端的写请求(GATT write request)时,根据客户端请求的句柄(
-
类型转换:
bat_service_t *bas = (bat_service_t *) svc;
:将svc
转换为bat_service_t
类型的指针,这样可以访问电池服务特定的属性和句柄。
-
初始化写入状态:
att_error_t status = ATT_ERROR_ATTRIBUTE_NOT_FOUND;
:初始化status
为ATT_ERROR_ATTRIBUTE_NOT_FOUND
,表示默认情况下找不到要写入的属性。如果后续没有找到匹配的句柄,这个错误状态将返回给客户端。
-
检查请求的句柄:
if (evt->handle == bas->bl_ccc_h)
:检查客户端请求的句柄是否等于bl_ccc_h
(即Client Characteristic Configuration Descriptor, CCC的句柄)。CCC通常用于存储客户端是否开启通知或指示功能。
-
调用写入处理函数:
status = do_bl_ccc_write(bas, evt->conn_idx, evt->offset, evt->length, evt->value);
:如果请求的句柄是bl_ccc_h
,则调用do_bl_ccc_write
函数来处理这个写请求。这个函数负责处理CCC的写操作,比如启用或禁用通知功能。执行后,status
将被更新为写入操作的结果。
-
结束写入处理:
goto done;
:一旦处理了写请求,跳转到done
标签,准备发送写入确认。
-
确认写入请求:
ble_gatts_write_cfm(evt->conn_idx, evt->handle, status);
:向客户端确认写入请求,conn_idx
是连接索引,handle
是请求的句柄,status
是写入操作的结果。如果成功,status
会是ATT_ERROR_OK
,否则会返回相应的错误状态。
5. do_bl_ccc_write()
static att_error_t do_bl_ccc_write(bat_service_t *bas, uint16_t conn_idx, uint16_t offset,
uint16_t length, const uint8_t *value)
{
uint16_t ccc;
if (offset) {
return ATT_ERROR_ATTRIBUTE_NOT_LONG;
}
if (length != sizeof(ccc)) {
return ATT_ERROR_APPLICATION_ERROR;
}
ccc = get_u16(value);
ble_storage_put_u32(conn_idx, bas->bl_ccc_h, ccc, true);
return ATT_ERROR_OK;
}
-
函数参数:
bat_service_t *bas
: 指向电池服务 (Battery Service
) 结构体的指针,用于访问相关句柄和数据。uint16_t conn_idx
: 连接索引,用于标识当前的连接。uint16_t offset
: 写入操作的偏移量。如果客户端支持分段写入,这个值会大于0。uint16_t length
: 客户端请求写入的数据长度。const uint8_t *value
: 指向客户端写入的数据的指针。
-
偏移量检查:
if (offset)
: 如果偏移量不为0,返回ATT_ERROR_ATTRIBUTE_NOT_LONG
错误。- 解释: CCC 描述符的值一般为 2 字节(
uint16_t
),通常不支持分段写入,因此偏移量应为0。如果不是,则返回错误,表示这个属性不支持分段写入。
-
长度检查:
if (length != sizeof(ccc))
: 检查请求写入的数据长度是否等于 2 字节(sizeof(ccc)
)。- 解释: CCC 描述符的大小固定为 2 字节(
uint16_t
)。如果客户端传输的数据长度不为 2 字节,返回ATT_ERROR_APPLICATION_ERROR
,表示应用层错误。
-
获取写入的值:
ccc = get_u16(value);
: 将客户端写入的 2 字节数据转换为uint16_t
类型的ccc
值。- 解释:
get_u16(value)
函数用于将字节数组转换为 16 位无符号整数。
-
存储写入的值:
ble_storage_put_u32(conn_idx, bas->bl_ccc_h, ccc, true);
: 将转换后的ccc
值存储到非易失性存储中。- 解释:
ble_storage_put_u32
函数会将ccc
值与连接索引conn_idx
以及 CCC 描述符句柄bl_ccc_h
关联并保存。这确保了即使设备重启,CCC 配置依然保留。
-
返回成功状态:
return ATT_ERROR_OK;
: 如果所有步骤都成功执行,返回ATT_ERROR_OK
表示写入操作成功。
6. notify_level()
static void notify_level(ble_service_t *svc, uint16_t conn_idx, uint8_t level)
{
bat_service_t *bas = (bat_service_t *) svc;
uint16_t ccc = 0x0000;
ble_storage_get_u16(conn_idx, bas->bl_ccc_h, &ccc);
if (!(ccc & GATT_CCC_NOTIFICATIONS)) {
return;
}
ble_gatts_send_event(conn_idx, bas->bl_val_h, GATT_EVENT_NOTIFICATION, sizeof(level), &level);
}
-
函数参数:
ble_service_t *svc
: 指向通用 BLE 服务结构体的指针,通过它可以访问具体服务的数据。uint16_t conn_idx
: 当前连接的索引,用于标识特定的设备连接。uint8_t level
: 电池电量的值。
-
获取特定服务的数据:
bat_service_t *bas = (bat_service_t *) svc;
: 将通用的ble_service_t
类型指针转换为特定的bat_service_t
(电池服务)类型指针,以便访问电池服务相关的句柄和数据。
-
获取 CCC 值:
ble_storage_get_u16(conn_idx, bas->bl_ccc_h, &ccc);
: 从非易失性存储中获取与连接索引conn_idx
和 CCC 描述符句柄bl_ccc_h
相关联的 CCC 值,并将其存储在ccc
变量中。- 解释: 这个步骤是为了检查客户端是否启用了电池电量特征的通知功能。
-
检查通知是否启用:
if (!(ccc & GATT_CCC_NOTIFICATIONS)) { return; }
: 检查ccc
值中是否设置了通知位 (GATT_CCC_NOTIFICATIONS
)。- 解释: 如果通知位没有被设置,函数会立即返回,不发送通知。这意味着客户端没有启用通知功能。
-
发送通知:
ble_gatts_send_event(conn_idx, bas->bl_val_h, GATT_EVENT_NOTIFICATION, sizeof(level), &level);
: 如果通知功能已启用,使用ble_gatts_send_event
函数发送一个包含当前电池电量的通知到客户端。- 解释:
ble_gatts_send_event
会通过当前连接索引conn_idx
将电池电量值level
发送给启用了通知的客户端。
notify_level
函数的作用是检查特定客户端连接是否启用了电池电量特征的通知功能。如果启用了通知功能,函数会将当前的电池电量值作为通知发送给客户端。这个过程确保了只有在客户端明确启用通知时,才会发送相关的数据更新。
7. bas_notify_level()
void bas_notify_level(ble_service_t *svc, uint16_t conn_idx)
{
bat_service_t *bas = (bat_service_t *) svc;
uint8_t level = 0x00;
uint16_t level_len = sizeof(level);
ble_gatts_get_value(bas->bl_val_h, &level_len, &level);
notify_level(svc, conn_idx, level);
ble_storage_put_u32(conn_idx, bas->bl_val_h, level, true);
}
-
函数参数:
ble_service_t *svc
: 指向通用 BLE 服务结构体的指针,代表当前的电池服务实例。uint16_t conn_idx
: 当前连接的索引,用于标识特定的设备连接。
-
获取特定服务的数据:
bat_service_t *bas = (bat_service_t *) svc;
: 将通用的ble_service_t
类型指针转换为具体的bat_service_t
(电池服务)类型指针,以便访问电池服务相关的数据和句柄。
-
初始化电池电量值:
uint8_t level = 0x00;
: 初始化电池电量值为0x00
。uint16_t level_len = sizeof(level);
: 设置level_len
为电池电量值的长度(通常是 1 字节)。
-
获取当前电池电量值:
ble_gatts_get_value(bas->bl_val_h, &level_len, &level);
: 从 GATT 服务器获取与电池电量特征相关联的当前电池电量值,并将其存储在level
变量中。- 解释: 这个步骤读取存储在 GATT 数据库中的电池电量值,以便在后续步骤中使用。
-
发送电池电量通知:
notify_level(svc, conn_idx, level);
: 调用notify_level
函数,检查当前连接的客户端是否启用了通知功能,如果启用,则将当前电池电量值level
发送给客户端。- 解释: 这一步根据客户端的配置发送电池电量通知,使客户端能够实时了解电池电量的变化。
-
存储当前电池电量值:
ble_storage_put_u32(conn_idx, bas->bl_val_h, level, true);
: 将当前电池电量值存储到非易失性存储中,以便在将来可以从存储中检索该值。- 解释: 这一步确保电池电量值被持久化存储,允许设备在断开连接或重启后保留该信息。
8. bas_set_level()
void bas_set_level(ble_service_t *svc, uint8_t level, bool notify)
{
bat_service_t *bas = (bat_service_t *) svc;
uint8_t prev_level = 0x00;
uint16_t prev_level_len = sizeof(prev_level);
uint8_t num_conn;
uint16_t *conn_idx;
if (level > 100) {
return;
}
ble_gatts_get_value(bas->bl_val_h, &prev_level_len, &prev_level);
if (level == prev_level) {
return;
}
ble_gatts_set_value(bas->bl_val_h, sizeof(level), &level);
/*
* for each connected device we need to:
* - notify new value, if requested by caller
* - put new value to storage to use when device is reconnected
*/
ble_gap_get_connected(&num_conn, &conn_idx);
while (num_conn--) {
if (notify) {
notify_level(svc, conn_idx[num_conn], level);
}
ble_storage_put_u32(conn_idx[num_conn], bas->bl_val_h, level, true);
}
if (conn_idx) {
OS_FREE(conn_idx);
}
}
-
函数参数:
ble_service_t *svc
: 指向通用 BLE 服务结构体的指针,代表当前的电池服务实例。uint8_t level
: 要设置的新电池电量值(范围是 0-100)。bool notify
: 如果为true
,表示需要通知已连接的设备新设置的电量值。
-
获取具体服务的指针:
bat_service_t *bas = (bat_service_t *) svc;
: 将通用的ble_service_t
类型指针转换为具体的bat_service_t
(电池服务)类型指针,以便访问电池服务相关的数据和句柄。
-
检查电量值的有效性:
if (level > 100) { return; }
: 如果电量值超出有效范围(0-100),函数立即返回,不做任何操作。
-
获取之前的电量值:
ble_gatts_get_value(bas->bl_val_h, &prev_level_len, &prev_level);
: 从 GATT 服务器获取当前的电量值并存储在prev_level
中。
-
检查新值是否与之前的值相同:
if (level == prev_level) { return; }
: 如果新电量值与之前的值相同,则函数直接返回,不进行任何操作。
-
更新 GATT 服务器中的电量值:
ble_gatts_set_value(bas->bl_val_h, sizeof(level), &level);
: 更新 GATT 服务器中的电池电量值为新设置的值。
-
处理每个已连接的设备:
-
ble_gap_get_connected(&num_conn, &conn_idx);
: 获取当前已连接设备的数量和连接索引。 -
循环遍历每个已连接的设备:
if (notify) { notify_level(svc, conn_idx[num_conn], level); }
: 如果notify
为true
,调用notify_level
函数,向当前设备发送电量值通知。ble_storage_put_u32(conn_idx[num_conn], bas->bl_val_h, level, true);
: 将新设置的电量值存储到非易失性存储中,以便在设备重新连接时使用。
-
-
释放连接索引内存:
if (conn_idx) { OS_FREE(conn_idx); }
: 如果连接索引不为空,释放之前分配的内存。
9. cleanup()
static void cleanup(ble_service_t *svc)
{
bat_service_t *bas = (bat_service_t *) svc;
ble_storage_remove_all(bas->bl_ccc_h);
ble_storage_remove_all(bas->bl_val_h);
OS_FREE(bas);
}
bas->bl_ccc_h
:移除与客户端特性配置(CCC)描述符相关的所有数据。bas->bl_val_h
:移除与电池电量值相关的所有数据。
这确保了当服务被清理时,任何与设备的连接状态、通知状态等相关的数据都不会残留。- 释放为
bat_service_t
结构体分配的内存,防止内存泄漏。
四、知识点
在 BLE(蓝牙低功耗)中,描述符(Descriptor)是附加在特性(Characteristic)上的元数据,用于提供有关该特性额外的信息。描述符可以描述特性的用途、格式、单位或允许客户端设备对特性进行配置或订阅通知等。
BLE 描述符的作用和类型
描述符主要用于以下几个用途:
表示格式(Characteristic Presentation Format,CPF):
- 用于描述特性值的格式、单位、指数等,使客户端能够正确地解释特性值。
用户描述符(Characteristic User Description,CUD):
- 允许为特性提供一个人类可读的字符串描述,告诉用户该特性表示什么。例如,“Battery Level”可以作为一个电池电量特性的用户描述符。
客户端特性配置描述符(Client Characteristic Configuration Descriptor,CCCD):
- 用于配置特性是否支持通知(Notification)或指示(Indication)。客户端可以通过写入此描述符来启用或禁用特性的通知或指示。
服务特性配置描述符(Server Characteristic Configuration Descriptor,SCCD):
- 用于配置服务器端的行为,如广播特性值。
范围描述符(Characteristic Aggregate Format,CAF):
- 用于表示多个特性值的组合。
示例
在 BLE 电池服务中,CCCD 描述符允许客户端设备选择是否接收来自服务器的电池电量变化的通知。用户描述符可以告诉用户该特性表示电池电量,而 CPF 描述符则详细说明电池电量是如何表示的(例如,以百分比的形式,范围是 0-100%)。
CPF
在BLE(蓝牙低功耗)中,CPF 代表 Characteristic Presentation Format(特性表示格式)。它是一种描述符,用于描述如何在客户端设备上表示特性的值。这包括特性的格式、单位、指数和其他与表示有关的信息。
具体字段
CPF 描述符通常包括以下几个字段:
- Format (格式):指定特性值的格式。例如,
0x04
表示无符号 8 位整数(uint8_t)。- Exponent (指数):用于表示特性值的实际值与存储值之间的比例。它的作用类似于科学计数法中的指数部分。例如,
0x00
表示没有指数缩放。- Unit (单位):表示特性值的物理单位。单位代码是基于国际单位制(SI)的,例如,
0x27AD
表示百分比。- Namespace (命名空间):指定描述符的命名空间标识符,用于解释单位字段。例如,
0x01
表示 Bluetooth SIG 命名空间。- Description (描述符):提供额外的信息或标识特性值的描述性文本或 ID。
作用
CPF 描述符的主要作用是在客户端(如手机或平板电脑)读取特性值时,告诉客户端如何解释该特性值。例如,如果特性值表示温度,CPF 描述符可以告诉客户端该值是摄氏度、华氏度还是开尔文。
示例
在电池服务中,CPF 描述符可能会用来表示电池电量是一个百分比值,并指定它是一个无符号 8 位整数,单位是百分比。通过这种方式,客户端设备可以正确显示电池电量的数值。
总体来说,CPF 是为了确保特性值在不同设备之间的一致性表示,使得设备可以正确地理解和展示特性值。