GATT Server Service Table 示例
示例说明
本示例展示如何在 ESP32 上创建 GATT Server 并注册服务表,配置特征及其属性,允许客户端进行读写操作和订阅通知。通过该示例,可以学习如何定义服务与特征、处理客户端请求,以及实现数据的实时更新和通知机制。
该示例需要与 GATT Client 示例配合执行,以实现完整的 BLE 通信流程。
示例适用于 BLE 外设的数据采集、状态报告或控制指令接收等场景,如智能手环、蓝牙音箱或各类传感器设备。开发者可通过该示例快速掌握 GATT Server 的基本实现方法,为构建自定义 BLE 服务提供参考。
运行方法
示例完整代码见 gatt server service table 示例。运行前的配置说明、构建与烧录流程详见示例目录下的 README.md 文件。
如需自定义配置项及查看默认值,可参考文档中的 宏定义说明 部分。
头文件说明
本示例所使用的头文件涵盖了 FreeRTOS 任务管理、系统工具模块、蓝牙控制器与协议栈接口以及 BLE GATT 服务相关 API 等功能模块,构建了 BLE 初始化、Profile 管理、事件处理与数据交互的核心功能。
各头文件按功能分类如下:
FreeRTOS:提供基本任务调度、任务管理与事件组同步机制。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
系统工具:用于系统初始化、日志打印以及非易失性存储的初始化。
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
蓝牙协议栈:用于管理蓝牙控制器的初始化与释放、启停协议栈,以及获取本地蓝牙设备信息。
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
BLE GAP 接口:负责广播、扫描等设备发现与连接管理功能。
#include "esp_gap_ble_api.h"
BLE GATT 接口:提供 GATT Server 功能及本示例的 GATT 属性表定义文件,用于集中定义服务、特征值和描述符,并配置其 UUID、权限及属性以处理客户端读写请求。
#include "esp_gatts_api.h"
#include "gatts_table_creat_demo.h"
#include "esp_gatt_common_api.h"
宏定义说明
本示例中涉及的宏定义主要用于配置 BLE GATT Profile、服务实例、特征值及广播参数,方便在多 Profile 场景下统一管理、减少硬编码,并提高代码可维护性。
宏定义按功能分类如下:
GATT Profile 基本参数:用于统一管理 Profile 数量、索引、应用 ID 及服务实例 ID。
定义 GATT Profile 的数量,方便在多 Profile 应用中统一管理数量和索引。此示例中配置为 1。
#define PROFILE_NUM 1
设置各个 Profile 在 Profile 数组中的索引,用于访问指定 Profile 的数据结构,避免硬编码索引值。当存在多个 Profile 时,应为每个 Profile 设定一个索引值,从 0 开始递增,且不能重复。
#define PROFILE_APP_IDX 0
备注
硬编码指将数值直接写死在代码里,修改时需在多处修改代码,容易遗漏。使用宏定义或常量代替,就可以只改一处来影响所有引用的地方,例如设备名、UUID 等。
设置应用 ID,用于区分不同 Profile,方便协议栈管理。此示例中设置为 0x55。当存在多个 Profile 时,应为每个 Profile 设定一个应用 ID,取值可为任意不重复的整数或十六进制数。
#define ESP_APP_ID 0x55
设置服务实例 ID,当同一 Profile 存在多个相同 UUID 的服务时,应为每个服务设置一个实例 ID,从 0 开始递增,且不能重复,用于区分实例。
#define SVC_INST_ID 0
特征值与缓冲区:用于限制特征值大小及缓冲区容量,防止数据超出内存范围。
设置特征值最大长度,防止客户端写入的数据长度超出缓冲区引发溢出。此示例中设置为 500 字节。
#define GATTS_DEMO_CHAR_VAL_LEN_MAX 500
设置预写缓冲区最大长度,用于 “准备写” 操作的缓冲区限制,避免内存过大占用。
#define PREPARE_BUF_MAX_SIZE 1024
标明特征声明所占字节数,在构建 GATT 属性表时用于分配内存。
#define CHAR_DECLARATION_SIZE (sizeof(uint8_t))
广播相关配置:用于设置设备名称及广播/扫描响应的配置状态标志。
设置设备名称为 ESP_GATTS_DEMO,用于在广播中标识设备,避免直接在代码中硬编码字符串。
#define SAMPLE_DEVICE_NAME "ESP_GATTS_DEMO"
定义广播数据配置状态标志位,当该 bit 置 1 时表示广播数据配置完成,防止广播在配置未完成时启动。
#define ADV_CONFIG_FLAG (1 << 0)
定义扫描响应数据配置状态标志位,当该 bit 置 1 时表示扫描响应数据配置完成,防止扫描在配置未完成时启动。
#define SCAN_RSP_CONFIG_FLAG (1 << 1)
全局变量说明
本示例中定义的全局变量主要用于管理 BLE 广播配置、GATT Profile 实例、连接状态以及属性数据等核心信息。通过这些全局变量,示例能够在不同回调函数之间共享状态,处理客户端请求,并维护 GATT 服务的完整性。
其中部分内容已在 通用步骤 文档中详细介绍,对于重复内容此处不再赘述,仅重点说明本示例中的关键实现与差异部分。
广播配置:
GATT Profile 及连接信息:
heart_rate_handle_table[HRS_IDX_NB]
:用于存储 GATT 数据库中各个特征和描述符的句柄,方便后续读写操作。此示例中仅包含一个 profile,如有多个 profile,则需为每个 profile 分别定义句柄表。其中HRS_IDX_NB
是一个宏或枚举值,表示服务中 GATT 属性(服务、特征、描述符)的总数。
prepare_type_env_t
/prepare_write_env
:定义了一个prepare_type_env_t
结构体用于缓冲 准备写 操作的分片数据,并通过prepare_write_env
实例存储当前 GATT 服务器接收的片段及其长度。
gatts_profile_inst
:创建 GATT Server 实例结构体。
heart_rate_profile_tab
:gatts_profile_inst
结构体类型的数组,每个数组元素表示一个 Server Profile 实例。本实例仅包含一个 Profile,若存在多个 Profile,需要按需一一进行配置。示例中只定义了两个结构体成员,用于事件回调和接口管理,其余成员在本示例中未使用,因此结构体定义时省略。
UUID 及属性配置
UUID 和标志位 作用
声明
自定义服务 UUID,用于标识服务内容。
GATTS_SERVICE_UUID_TEST
自定义特征 UUID,用于标识特征内容。
GATTS_CHAR_UUID_TEST_A
GATTS_CHAR_UUID_TEST_B
GATTS_CHAR_UUID_TEST_C
主服务声明 UUID,标识属性类型。
primary_service_uuid
特征声明 UUID,标识属性类型。
character_declaration_uuid
客户端特征配置 UUID,标识属性类型。
character_client_config_uuid
特征的属性标志位(读、写和通知)
char_prop_read
char_prop_write
char_prop_read_write_notify
CCC Descriptor 的初始值,2字节,0x0000 代表禁用通知
heart_measurement_ccc
特征值的初始内容
char_value
备注
在 BLE(蓝牙低功耗)协议中,UUID(通用唯一标识符)用于区分服务和特征。
通常,一个属性包含两种 UUID,其中声明 UUID 标识类型,具体 UUID 标识内容。以服务 UUID 为例:
主服务 UUID:唯一标识某个具体的服务类型。
服务声明 UUID:标识“这是一个服务声明”,是 GATT 协议中的一个元信息。
特征和描述符也遵循相同逻辑。
GATT Attribute Table 定义:定义 属性表,用于集中存储服务、特征及描述符等属性,便于统一管理和访问。
属性索引在示例头文件
gatts_table_creat_demo.h
中定义,方便统一管理和访问。使用
gatt_db[]
数组定义所有 GATT 属性,包括服务声明、特征声明、特征值和可选描述符。示例中定义的属性如下:
主服务:心率测量服务。
特征 A:用于心率值或控制命令。
特征 A 描述符:客户端特征配置描述符(CCC),用于控制特征 A 的通知开关。
特征 B:用于提供静态信息或配置参数。
特征 C:用于接收客户端命令或数据。
功能函数说明
gap_event_handler()
gap_event_handler()
是 BLE GAP 事件回调函数,用于处理广播、扫描响应以及连接参数更新等 事件。
备注
事件回调函数是在程序运行时用来“接收通知”的函数,当特定事件发生时会被自动调用,使程序能够及时响应并处理这些事件。
static void gap_event_handler( esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param );
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
事件类型,例如广播开始、扫描完成、连接参数更新等。 |
可根据事件类型在回调中进行不同处理。 |
|
指向事件参数的结构体指针,包含该事件的详细信息。 |
不同事件对应不同的联合体成员,例如广播数据、扫描结果、连接信息等。 |
该函数根据不同事件执行对应的处理逻辑:
事件 |
|
触发时机 |
执行逻辑 |
---|---|---|---|
广播数据设置完成 |
|
完成广播数据配置后触发。 |
清除广播数据配置完成标志位,成功清除后 启动广播。 |
扫描响应数据设置完成。 |
|
完成扫描响应数据配置后触发。 |
清除扫描响应配置完成标志位,成功清除后 启动广播。 |
广播启动完成 |
|
成功启动广播后触发。 |
判断广播状态,并输出对应日志,标明启动成功或失败。 |
广播停止 |
|
成功停止广播后触发。 |
判断广播状态,并输出对应日志,标明停止成功或失败。 |
更新连接参数 |
|
连接建立后,连接双方协商新的连接参数时触发。 |
输出日志,包括状态、连接间隔、延迟和超时时间。 |
备注
启动广播前会通过条件编译判断广播数据包的配置方式,并发送对应正确的数据包。
example_prepare_write_event_env()
example_prepare_write_event_env()
用于处理 BLE GATT 的 Prepare Write 事件,即客户端分段写入特征值时服务器对每段数据的处理逻辑。
void example_prepare_write_event_env( esp_gatt_if_t gatts_if,
prepare_type_env_t *prepare_write_env,
esp_ble_gatts_cb_param_t *param );
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
GATT 服务器访问接口。 |
由协议栈分配,用于区分不同的 GATT Profile 服务实例,在事件回调中识别对应的 Profile。 |
|
指向保存分段写入数据的结构体。 |
存储 Prepare Write 缓冲区和已写长度。 |
|
指向事件参数的结构体指针,包含该事件的详细信息。 |
不同事件对应不同的联合体成员,例如广播数据、扫描结果、连接信息等。 |
函数执行逻辑如下:
打印日志,显示当前写入的属性句柄和写入数据长度。
初始化写入状态为成功。
检查写入偏移量和总长度是否合法:
如果偏移量大于缓冲区最大值,修改写入状态为
ESP_GATT_INVALID_OFFSET
,代表无效偏移。如果偏移量和数据长度超过缓冲区最大值,修改写入状态为
ESP_GATT_INVALID_ATTR_LEN
,代表无效属性长度。
如果写入状态正常且缓冲区尚未分配:
动态分配内存,大小为
PREPARE_BUF_MAX_SIZE
,用于存放分段写入的数据。初始化已写长度为 0。
如果内存分配失败,打印日志并记录错误状态。
判断客户端是否需要写入响应,如果需要:
如果写入状态异常,直接返回,不执行缓存写入。
将客户端写入的数据复制到 Prepare Write 缓冲区的对应偏移位置,并更新已写入数据长度。
example_exec_write_event_env()
example_exec_write_event_env()
用于处理 BLE GATT 的 Execute Write 事件,即客户端完成分段写入后,服务器根据客户端指示决定是否真正写入或取消写入。
void example_exec_write_event_env( prepare_type_env_t *prepare_write_env,
esp_ble_gatts_cb_param_t *param );
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
指向保存分段写入数据的结构体。 |
存储 Prepare Write 缓冲区和已写长度。 |
|
指向事件参数的结构体指针,包含该事件的详细信息。 |
不同事件对应不同的联合体成员,例如广播数据、扫描结果、连接信息等。 |
函数执行逻辑如下:
判断客户端是否发送执行写入命令并且缓冲区是否存在数据。
如果需要执行写入且缓冲区不为空,则打印缓冲区中存储的所有写入数据。
如果客户端取消写入或者缓冲区为空,打印日志表示 Prepare Write 被取消。
备注
示例中仅执行日志输出,实际应用中可在此处理写入逻辑,例如写入非易失性存储或更新特征值。
如果缓冲区存在,释放动态分配的内存,避免内存泄漏,并且将指针置空,防止悬空指针使用。
重置已写长度为 0,为下次分段写入做准备。
gatts_profile_event_handler()
gatts_profile_event_handler()
是 GATT Server Profile 的回调处理函数,用于处理 BLE GATT Server 在运行过程中产生的各种事件,例如注册、读写请求、连接、断开等。
static void gatts_profile_event_handler( esp_gatts_cb_event_t event,
esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param );
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
事件类型,例如注册、读写、连接、断开等。 |
可根据事件类型在回调中进行不同处理。 |
|
GATT 服务器访问接口。 |
由协议栈分配,用于区分不同的 GATT Profile 服务实例,在事件回调中识别对应的 Profile。 |
|
指向事件参数的结构体指针,包含该事件的详细信息。 |
不同事件对应不同的联合体成员,例如广播数据、扫描结果、连接信息等。 |
该函数根据不同事件类型执行对应的处理逻辑:
注册事件:
ESP_GATTS_REG_EVT
,应用注册完成时触发。
设置本地蓝牙设备的 名称,如果设置失败,打印错误日志。
根据条件编译选择数据包配置模式,设置广播数据和扫描响应数据,并标记配置完成。
创建 GATT 属性表,如果创建失败,打印错误日志。
GATT 属性表创建完成后会触发
ESP_GATTS_CREAT_ATTR_TAB_EVT
事件。
读取事件:
ESP_GATTS_READ_EVT
,客户端对某个特征值发起读请求时触发。
打印日志,显示该事件触发。
写入事件:
ESP_GATTS_WRITE_EVT
,客户端写入某个特征值时触发。
执行 Prepare Write 事件:
ESP_GATTS_EXEC_WRITE_EVT
,客户端发起“执行写”操作(prepare write 之后)时触发。
打印日志并调用任务函数
example_exec_write_event_env()
处理 缓存数据执行或取消操作。
MTU 更新事件:
ESP_GATTS_MTU_EVT
,服务器或客户端修改 MTU 值后(包括协商完成)触发。
打印新的 MTU 值。
服务器确认事件:
ESP_GATTS_CONF_EVT
,服务器向客户端发送响应或通知/指示后触发。
打印确认状态和属性句柄。
服务启动完成事件:
ESP_GATTS_START_EVT
,服务成功启动时触发。
打印启动状态和服务句柄。
客户端连接事件:
ESP_GATTS_CONNECT_EVT
,客户端连接到 GATT Server 时触发。
打印连接 ID 和客户端蓝牙地址。
初始化连接参数并复制客户端蓝牙地址。
设置连接参数:连接延迟,连接间隔以及连接超时时间。
向客户端请求 更新 连接参数。
客户端断开事件:
ESP_GATTS_DISCONNECT_EVT
,客户端断开连接时触发。
打印断开原因。
重新 启动广播。
属性表创建完成事件:
ESP_GATTS_CREAT_ATTR_TAB_EVT
,属性表创建完成时触发。
判断属性表创建状态:
如果创建失败,打印错误日志。
如果创建的句柄数量异常,打印警告。
否则打印创建成功日志,将返回的句柄数组复制到
heart_rate_handle_table
,最后 启动服务。服务成功启动后会触发
ESP_GATTS_START_EVT
事件。
其他事件:默认不处理。
gatts_event_handler()
gatts_event_handler()
是 GATT Server 总入口的回调处理函数,用于注册服务、处理读写请求、发送通知或指示、连接和断开等事件。详细说明见 GATT 事件回调函数。
主函数说明
该函数作为程序入口函数,用于完成蓝牙协议栈初始化、回调注册、应用注册等工作。
以下为函数的主要实现步骤,鉴于流程的大部分步骤已在通用步骤文档的 主函数说明 部分中详细介绍,对于重复内容此处不再赘述,仅重点说明本示例中的差异及细节补充部分。
在 释放蓝牙内存 时,因为本示例仅使用 BLE,所以需要将经典蓝牙部分的内存全部释放。
在 启动蓝牙控制器 时,需要将其指定为 BLE 模式。
在注册回调函数时:
传入
gatts_event_handler
注册 GATT Server 回调函数。传入
gap_event_handler
注册 GAP 回调函数。
将 本地 GATT MTU 设置为 500 字节。
程序执行逻辑
在程序每个事件中添加日志打印后,烧录运行可以看到如下执行结果:
I (369) Steps: 1 Initialize NVS
I (399) Steps: 2 Release mem
I (399) Steps: 3 Initialize controller
I (409) Steps: 4 Enable controller
I (459) Steps: 5 Initialize bluedroid
I (469) Steps: 6 Enable bluedroid
I (479) Steps: 7 Register gatts event handler
I (479) Steps: 8 Register gap event handler
I (479) Steps: 9 Register gatts app
I (479) Steps: 10 ESP_GATTS_REG_EVT in gatts_event_handler
I (489) Steps: 11 Event Dispatch
I (489) Steps: 12 ESP_GATTS_REG_EVT
I (499) Steps: 13 Event Dispatch
I (499) Steps: 14 ESP_GATTS_CREAT_ATTR_TAB_EVT
I (509) Steps: 15 ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT
I (509) Steps: 16 ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT
I (519) Steps: 17 Event Dispatch
I (519) Steps: 18 ESP_GATTS_START_EVT
I (529) Steps: 19 ESP_GAP_BLE_ADV_START_COMPLETE_EVT
I (539) Steps: 20 Set MTU
I (1389) Step: 21 Event Dispatch
I (1389) Step: 22 ESP_GATTS_CONNECT_EVT
I (1689) Step: 23 ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT
I (2449) Step: 24 Event Dispatch
I (2449) Step: 25 ESP_GATTS_MTU_EVT
I (2609) Step: 26 Event Dispatch
I (2609) Step: 27 ESP_GATTS_WRITE_EVT
I (2619) Step: 28 Event Dispatch
I (2619) Step: 29 ESP_GATTS_CONF_EVT
I (2689) Step: 30 Event Dispatch
I (2689) Step: 31 ESP_GATTS_WRITE_EVT
程序执行逻辑可以总结为以下几个阶段:
初始化阶段:程序依次初始化非易失性存储(NVS)、释放内存、初始化并启用蓝牙控制器,随后初始化并启用 Bluedroid 协议栈。
注册回调与应用:注册 GATT Server 和 GAP 的事件回调函数,随后注册 GATT 应用。
应用注册完成:应用注册成功后触发
ESP_GATTS_REG_EVT
,表示 GATT Server 应用已成功登记到协议栈中。此时事件进入统一事件回调gatts_event_handler()
,由事件分发器分发到具体的 Profile 回调中,在该回调中启动广播和扫描响应数据的写入操作。创建属性表:协议栈创建服务和特征属性表完成后触发
ESP_GATTS_CREAT_ATTR_TAB_EVT
,在该事件回调中启动服务。广播数据准备:广播数据和扫描响应数据写入完成分别触发
ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT
和ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT
启动广播。服务启动完成:服务启动后触发
ESP_GATTS_START_EVT
。广播启动完成:广播启动完成后触发
ESP_GAP_BLE_ADV_START_COMPLETE_EVT
,此时设备可被客户端扫描和连接。MTU 设置:进行 MTU 配置,以便优化传输效率。
建立连接:客户端发起连接后触发
ESP_GATTS_CONNECT_EVT
,表示 GATT Server 与 Client 建立了物理连接,后续可进行参数更新和数据交互。连接参数更新:在连接建立后,双方可能会更新连接参数(如连接间隔、超时时间),以优化通信稳定性和功耗。参数更新完成后触发
ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT
。MTU 协商完成:Server 收到 Client 的 MTU 请求后完成协商,确定双方传输数据包的最大长度。协商完成后触发
ESP_GATTS_MTU_EVT
。客户端写入数据:当 Client 向某个特征写入数据时触发
ESP_GATTS_WRITE_EVT
。Server 在该事件中接收并处理写入内容。写入确认:如果该写入需要响应,Server 在处理完成后会通过触发
ESP_GATTS_CONF_EVT
通知确认结果,表示写操作已完成。进一步交互:再次触发
ESP_GATTS_WRITE_EVT
,表示客户端持续写入数据,Server 持续接收和处理。
注意事项:
在执行 GATTS 事件前,需要先注册事件回调并启动事件分发器,以保证事件能够正确投递到对应的 Profile 回调函数。
服务启动、广播和扫描是异步操作,事件触发顺序体现了协议栈处理流程,但不同设备或配置可能略有差异。