SOC-ESP32S3部分:28-BLE低功耗蓝牙

飞书文档https://x509p6c8to.feishu.cn/wiki/CHcowZMLtiinuBkRhExcZN7Ynmc

蓝牙是一种短距的无线通讯技术,可实现固定设备、移动设备之间的数据交换,下图是一个蓝牙应用的分层架构,Application部分则是我们需要实现的内容,Protocol stack和Radio部分都由芯片原厂封装起来了,我们可以直接使用。那为什么我们还需要了解这个架构图呢?因为我们实现应用的时候,需要根据不同层传递给我们的消息做业务处理,例如广播事件、连接事件、通信数据等等。

如上图所述,要实现一个BLE应用,首先需要一个支持BLE射频的芯片,然后还需要提供一个与此芯片配套的BLE协议栈,最后在协议栈上开发自己的应用。可以看出BLE协议栈是连接芯片和应用的桥梁,是实现整个BLE应用的关键。那BLE协议栈具体包含哪些功能呢?简单来说,BLE协议栈主要用来对你的应用数据进行层层封包,以生成一个满足BLE协议的空中数据包,也就是说,把应用数据包裹在一系列的帧头(header)和帧尾(tail)中。具体来说,BLE协议栈主要由如下几部分组成:

复制代码
PHY层(Physical layer物理层)。
PHY层用来指定BLE所用的无线频段,调制解调方式和方法等。PHY层做得好不好,直接决定整个BLE芯片的功耗,灵敏度以及selectivity等射频指标。

LL层(Link Layer链路层)。
LL层是整个BLE协议栈的核心,也是BLE协议栈的难点和重点。像Nordic的BLE协议栈能同时支持20个link(连接),就是LL层的功劳。LL层要做的事情非常多,比如具体选择哪个射频通道进行通信,怎么识别空中数据包,具体在哪个时间点把数据包发送出去,怎么保证数据的完整性,ACK如何接收,如何进行重传,以及如何对链路进行管理和控制等等。LL层只负责把数据发出去或者收回来,对数据进行怎样的解析则交给上面的GAP或者ATT。

HCI(Host controller interface)。
HCI是可选的,HCI主要用于2颗芯片实现BLE协议栈的场合,用来规范两者之间的通信协议和通信命令等。

GAP层(Generic access profile)。
GAP是对LL层payload(有效数据包)如何进行解析的两种方式中的一种,而且是最简单的那一种。GAP简单的对LL payload进行一些规范和定义,因此GAP能实现的功能极其有限。GAP目前主要用来进行广播,扫描和发起连接等。

L2CAP层(Logic link control and adaptation protocol)。
L2CAP对LL进行了一次简单封装,LL只关心传输的数据本身,L2CAP就要区分是加密通道还是普通通道,同时还要对连接间隔进行管理。

SMP(Secure manager protocol)。
SMP用来管理BLE连接的加密和安全的,如何保证连接的安全性,同时不影响用户的体验,这些都是SMP要考虑的工作。

ATT(Attribute protocol)。
简单来说,ATT层用来定义用户命令及命令操作的数据,比如读取某个数据或者写某个数据。BLE协议栈中,开发者接触最多的就是ATT。BLE引入了attribute概念,用来描述一条一条的数据。Attribute除了定义数据,同时定义该数据可以使用的ATT命令,因此这一层被称为ATT层。

GATT(Generic attribute profile )。
GATT用来规范attribute中的数据内容,并运用group(分组)的概念对attribute进行分类管理。没有GATT,BLE协议栈也能跑,但互联互通就会出问题,也正是因为有了GATT和各种各样的应用profile,BLE摆脱了ZigBee等无线协议的兼容性困境,成了出货量最大的2.4G无线通信产品。

我们着重了解下GAP层和GATT层:

GAP****层 - 通用访问规范

GAP 层的全称为通用访问规范 (Generic Access Profile, GAP),定义了 Bluetooth LE 设备之间的连接行为以及设备在连接中所扮演的角色。

GAP 中共定义了三种设备的连接状态以及五种不同的设备角色,如下

  • 空闲 (Idle)
  • 此时设备无角色,处于就绪状态 (Standby)
  • 设备发现 (Device Discovery)
  • 广播者 (Advertiser)
  • 扫描者 (Scanner)
  • 连接发起者 (Initiator)
  • 连接 (Connection)
  • 外围设备 (Peripheral)
  • 中央设备 (Central)

GATT/ATT****层 - 数据表示与交换

GATT/ATT 层定义了进入连接状态后,设备之间的数据交换方式,包括数据的表示与交换过程。

ATT****层

ATT 的全称是属性协议 (Attribute Protocol, ATT),定义了一种称为**属性 (Attribute)** 的基本数据结构,以及基于服务器/客户端架构的数据访问方式。

简单来说,数据以属性的形式存储在服务器上,等待客户端的访问。以智能开关为例,开关量作为数据,以属性的形式存储在智能开关内的蓝牙芯片(服务器)中,此时用户可以通过手机(客户端)访问智能开关蓝牙芯片(服务器)上存放的开关量属性,获取当前的开关状态(读访问),或控制开关的闭合与断开(写访问)。

属性这一数据结构一般由以下三部分构成

  • 句柄 (Handle)
  • 类型 (Type)
  • 值 (Value)
  • 访问权限 (Permissions)

在协议栈实现中,属性一般被放在称为**属性表 (Attribute Table)** 的结构体数组中管理。一个属性在这张表中的索引,就是属性的句柄,常为一无符号整型。

属性的类型由 UUID 表示,可以分为 16 位、32 位与 128 位 UUID 三类。 16 位 UUID 由蓝牙技术联盟 (Bluetooth Special Interest Group, Bluetooth SIG) 统一定义,可以在其公开发布的 Assigned Numbers 文件中查询;其他两种长度的 UUID 用于表示厂商自定义的属性类型,其中 128 位 UUID 较为常用。

GATT****层

GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基础上,定义了以下三个概念

  • 特征数据 (Characteristic)
  • 服务 (Service)
  • 规范 (Profile)

这三个概念之间的层次关系如下图所示

GATT 中的层次关系

特征数据和服务都是以属性为基本数据结构的复合数据结构。一个特征数据往往由两个以上的属性描述,包括

  • 特征数据声明属性 (Characteristic Declaration Attribute)
  • 特征数据值属性 (Characteristic Value Attribute)

除此以外,特征数据中还可能包含若干可选的描述符属性 (Characteristic Descriptor Attribute)。

一个服务本身也由一个属性进行描述,称为服务声明属性 (Service Declaration Attribute)。一个服务中可以存在一个或多个特征数据,它们之间体现为从属关系。另外,一个服务可以通过 Include 机制引用另一个服务,复用其特性定义,避免如设备名称、制造商信息等相同特性的重复定义。

规范是一个预定义的服务集合,实现了某规范中所定义的所有服务的设备即满足该规范。例如 Heart Rate Profile 规范由 Heart Rate Service 和 Device Information Service 两个服务组成,那么可以称实现了 Heart Rate Service 和 Device Information Service 服务的设备符合 Heart Rate Profile 规范。

广义上,我们可以称所有存储并管理特征数据的设备为 GATT 服务器,称所有访问 GATT 服务器以访问特征数据的设备为 GATT 客户端。

IDF****蓝牙协议栈说明

我们了解了BLE分层架构后,前面我们说到,除了APP层,其它层都是芯片原厂已经实现了,但更准确来说是,芯片原厂仅仅是把第三方蓝牙协议栈,移植到芯片中,让它能够跑起来,所以无论是哪个芯片,用的蓝牙协议栈来来去去都是那么几个,因为一般芯片厂家也不会去自己写一个蓝牙协议栈。

ESP-IDF 目前支持两个主机堆栈,Bluedroid(默认) 和 Apache NimBLE 。

复制代码
Bluedroid
Bluedroid 是谷歌开发的开源蓝牙协议栈,最初是为 Android 系统设计的,后来被移植到了 ESP-IDF中,使得基于 ESP 芯片的设备也能使用该协议栈实现蓝牙功能。
该堆栈支持传统蓝牙(BR/EDR)和低功耗蓝牙(BLE)。如果是传统蓝牙(BR/EDR)有需求,则必须使用该堆栈

Apache NimBLE
Apache NimBLE 是由 Apache Software Foundation 管理的开源项目,它是从 Mynewt 操作系统中的蓝牙协议栈发展而来的,专门为资源受限的嵌入式设备设计。
仅支持低功耗蓝牙。如果仅仅是对BLE有使用需求,建议选择该协议栈,因为该协议栈代码占用和运行时对内存的需求都会低一些。

课程主要对bluedroid接口进行讲解,对于用户来说底层逻辑都是一样的,API有差异而已。

ESP32S3的蓝牙功能特别丰富,所有的例程都在esp-idf/examples/bluetooth/bluedroid下方

  • ble contains BLE examples
  • ble_50 contains BLE 5.0 examples
  • classic_bt contains Classic BT examples
  • coex contains Classic BT and BLE coex examples

下方的工程,首先需要启动蓝牙组件,Bluetooth在menuconfig默认是没选上的。

复制代码
(Top) → Component config → Bluetooth
Espressif IoT Development Framework Configuration
[*] Bluetooth
        Host (Bluedroid - Dual-mode)  --->
        Controller (Enabled)  --->
    Bluedroid Options  --->
    Controller Options  --->
    Common Options  --->
[ ] Enable Bluetooth HCI debug mode (NEW)

课程文档演示的是BLE4.2功能,所以需要开启BLE4.2协议栈

复制代码
(Top) → Component config → Bluetooth → Bluedroid Options
    ↑↑↑↑↑↑↑↑↑↑↑↑↑↑                             
[ ] Use dynamic memory allocation in BT/BLE stack
[ ] BLE queue congestion check
(15) BT/BLE maximum bond device count
[ ] Report adv data and scan response individually when BLE active scan
(30) Timeout of BLE connection establishment
(32) length of bluetooth device name
(900) Timeout of resolvable private address
[ ] Enable BLE 5.0 features(please disable BLE 4.2 if enable BLE 5.0)
[*] Enable BLE 4.2 features(please disable BLE 5.0 if enable BLE 4.2)

ibeacon****广播

复制代码
什么是ibeacon?
"iBeacon 是苹果公司2013年9月发布的移动设备用OS(iOS7)上配备的新功能。其工作方式是,配备有低功耗蓝牙(BLE)通信功能的设备使用BLE技术向周围发送自己特有的ID,接收到该ID的应用软件会根据该ID采取一些行动。比如,在店铺里设置iBeacon通信模块的话,便可让iPhone和iPad上运行一资讯告知服务器,或者由服务器向顾客发送折扣券及进店积分。此外,还可以在家电发生故障或停止工作时使用iBeacon向应用软件发送资讯。"以上来自百度百科。实际上ibeacon的本质就是一个蓝牙广播设备,不停的向外广播数据,因为是广播每个想接收这个数据的人都可以收到。

官方例程位于"esp-idf/examples/bluetooth/bluedroid/ble/ble_ibeacon"

我们可以通过"idf.py menuconfig"进行配置来选择代码工作在发送模式还是接收模式。

此文主要分析ibeacon发送流程,因此选择发送模式。

蓝牙控制器初始化

复制代码
/* 1. 定义一个默认配置*/
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
/* 2. 根据配置初始化蓝牙控制器*/
esp_bt_controller_init(&bt_cfg);
/* 3. 将蓝牙控制器设置为ble模式*/
esp_bt_controller_enable(ESP_BT_MODE_BLE);

ble 蓝牙初始化一般分成三个部分:

  1. 定义一个蓝牙控制器的配置结构体bt_cfg让它等于默认配置,默认配置乐鑫已经帮我定义好了,基本上不用我们自己去修改。

  2. esp_bt_controller_init() 按照ble_cfg 创建任务.

  3. esp_err_t esp_bt_controller_enable(esp_bt_mode_t mode); 使能蓝牙的模式,

    typedef enum {
    ESP_BT_MODE_IDLE = 0x00, /*!< Bluetooth is not running /
    ESP_BT_MODE_BLE = 0x01, /
    !< 低功耗蓝牙模式 /
    ESP_BT_MODE_CLASSIC_BT = 0x02, /
    !< 传统蓝牙模式e /
    ESP_BT_MODE_BTDM = 0x03, /
    !< 双模,同时支持低功耗和传统蓝牙 */
    } esp_bt_mode_t;

ibeacon 初始化

复制代码
void ble_ibeacon_init(void){
    //初始化蓝牙协议栈
    esp_bluedroid_init();
    //使能蓝牙协议栈
    esp_bluedroid_enable();
    //注册gap 的回调函数
    ble_ibeacon_appRegister();
}

在ibeacon 初始化阶段我们只需关注ble_ibeacon_appRegister() 函数即可,因为在这个函数内部注册了ble_gap 回调函数.

复制代码
void ble_ibeacon_appRegister(void)
{
    esp_err_t status;
    ESP_LOGI(DEMO_TAG, "register callback");
    //register the scan callback function to the gap module
    if ((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK) {
        ESP_LOGE(DEMO_TAG, "gap register error: %s", esp_err_to_name(status));
        return;
    }

}

前面已经介绍了,如果两个设备的连接过程就是通过GAP实现的,那两个蓝牙设备是如何实现连接的那?根据前面GAP的介绍,两个设备连接必须是一个设备作为广播者不停的发送广播数据,而另外一个设备作为观察者不停的扫描,直到成功的扫描到了广播信号,两个设备开始建立连接。

之前介绍过了ibeacon就是通过广播发送数据,所以它自然需要使用到GAP。

beacon 配置

乐鑫根据ibeacon协议 定义了一个结构体 用于保存 Ibeacon 的数据

复制代码
typedef struct {
    uint8_t flags[3];
    uint8_t length;
    uint8_t type;
    uint16_t company_id;
    uint16_t beacon_type;
}__attribute__((packed)) esp_ble_ibeacon_head_t;

typedef struct {
    uint8_t proximity_uuid[16];
    uint16_t major;
    uint16_t minor;
    int8_t measured_power;
}__attribute__((packed)) esp_ble_ibeacon_vendor_t;

typedef struct {
    esp_ble_ibeacon_head_t ibeacon_head;
    esp_ble_ibeacon_vendor_t ibeacon_vendor;
}__attribute__((packed)) esp_ble_ibeacon_t;

这和iBeacon协议包是一致的

官方例程中初始化如下

复制代码
#define ESP_UUID    {0xFD, 0xA5, 0x06, 0x93, 0xA4, 0xE2, 0x4F, 0xB1, 0xAF, 0xCF, 0xC6, 0xEB, 0x07, 0x64, 0x78, 0x25}
#define ESP_MAJOR   10167
#define ESP_MINOR   61958

esp_ble_ibeacon_head_t ibeacon_common_head = {
    .flags = {0x02, 0x01, 0x06},
    .length = 0x1A,
    .type = 0xFF,
    .company_id = 0x004C,
    .beacon_type = 0x1502
};

/* Vendor part of iBeacon data*/
esp_ble_ibeacon_vendor_t vendor_config = {
    .proximity_uuid = ESP_UUID,
    .major = ENDIAN_CHANGE_U16(ESP_MAJOR), //Major=ESP_MAJOR
    .minor = ENDIAN_CHANGE_U16(ESP_MINOR), //Minor=ESP_MINOR
    .measured_power = 0xC5
};

flags = {0x02, 0x01, 0x06},length,type = 0xFF这三个是固定的,
因为ibeacon长度是固定,所以length位也是固定的=0x1A。

0x004C 这两位代表beacon的公司名称,4C就是苹果的ibeacon,其他公司的需要查询蓝牙联盟的数据库。

0x1502 这个代表了是ibeacon的服务类型,这个也是固定的,就是说我们设备如果需要扫描ibeacon设备,只要判断这里两位是就是可以判定这个是ibeacon设备。

ESP_UUID{0xFD, 0xA5, 0x06, 0x93, 0xA4, 0xE2, 0x4F, 0xB1, 0xAF, 0xCF, 0xC6, 0xEB, 0x07, 0x64, 0x78, 0x25}这16个字节是ibeacon的UUID,注意ibeacon里的UUID,不是唯一指这个设备是唯一的,一般指设备的服务类型,比如该beacon是用于干什么的,手机app开发的时候,就是通过一个固定的uuid扫描到一组beacon来处理。

ESP_MAJOR这两位是beacon的Major值,经常用于beacon的分组,比如1层楼的beacon是一组major的值,2层的beacon是一组major的值。
ESP_MINOR这两位是beacon的Minor值,跟上面的major值放在一起,指在同一major值(组)下,唯一的一个设备id号。
Major、Minor: 由 iBeacon 发布者自行设定,都是 16 位的标识符。比如,连锁店可以在 Major 写入区域资讯,可在 Minor 中写入个别店铺的 ID 等。另外,在家电中嵌入 iBeacon 功能时,可以用 Major 表示产品型号,用 Minor 表示错误代码,用来向外部通知故障

0xC5最后一位代表rssi的参考值,这个一般是指该beacon设备在一米处的rssi信号强度值,注意这个是有符号的int8类型,比如这里的C3就是代表了-61

初始化后,以上的数据,会通过esp_ble_config_ibeacon_data 组装起来,放到ibeacon_adv_data中,等待发送

复制代码
    esp_ble_ibeacon_t ibeacon_adv_data;
    esp_err_t status = esp_ble_config_ibeacon_data (&vendor_config, &ibeacon_adv_data);

广播配置

最后通过调用:esp_ble_gap_config_adv_data_raw((uint8_t*)&ibeacon_adv_data, sizeof(ibeacon_adv_data));将ibeancon 数据包填充到广播中开始发送。

复制代码
    if (status == ESP_OK){
        esp_ble_gap_config_adv_data_raw((uint8_t*)&ibeacon_adv_data, sizeof(ibeacon_adv_data));
    }
    else {
        ESP_LOGE(DEMO_TAG, "Config iBeacon data failed: %s", esp_err_to_name(status));
    }

设置广播参数成功后esp_gap_cb就会收到回调ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT,这时候就可以启动广播啦。

复制代码
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:{
        esp_ble_gap_start_advertising(&ble_adv_params);
        break;
    }

当然了,这里还有个广播参数需要设置,因为我们前面只设置了广播内容,至于广播具体参数,例如广播间隔,是否可连接,通道等等就需要通过ble_adv_params进行配置

复制代码
static esp_ble_adv_params_t ble_adv_params = {
    .adv_int_min        = 0x20,
    .adv_int_max        = 0x40,
    .adv_type           = ADV_TYPE_NONCONN_IND,
    .own_addr_type      = BLE_ADDR_TYPE_PUBLIC,
    .channel_map        = ADV_CHNL_ALL,
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};

1. adv_int_min 和 adv_int_max
单位:0.625 毫秒 (ms)
范围:
adv_int_min:最小广播间隔,表示设备广播数据包之间的最短时间间隔。
adv_int_max:最大广播间隔,表示设备广播数据包之间的最长时间间隔。
值:
0x20(十进制 32):最小广播间隔为 32 * 0.625 ms = 20 ms。
0x40(十进制 64):最大广播间隔为 64 * 0.625 ms = 40 ms。

2. adv_type:广播类型,决定了广播行为。
值:ADV_TYPE_NONCONN_IND
表示非连接指示广播(Non-connectable undirected advertising)。这种类型的广播不会响应连接请求,主要用于广播数据,如 iBeacon。

3. own_addr_type:广播时使用的本地地址类型。
值:BLE_ADDR_TYPE_PUBLIC
使用公共蓝牙地址(Public Device Address)。
如果设备没有公共地址,则可以使用随机静态地址(Random Static Address)或其他类型。

4. channel_map:指定广播使用的频道。
值:ADV_CHNL_ALL
使用所有可用的广播频道(通常是 37、38、39 频道)。
这可以确保广播数据在多个频道上传播,提高被扫描到的概率。

5. adv_filter_policy:过滤策略,决定哪些设备可以扫描或连接。
值:ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY
允许任何设备进行扫描和连接请求。对于非连接广播(如 iBeacon),这个设置通常允许任何设备接收到广播数据。

最终代码参考

复制代码
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "nvs_flash.h"

#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_gatt_defs.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"

// 定义日志标签,用于调试输出
static const char* DEMO_TAG = "IBEACON_DEMO";

// 定义一个全零的128位UUID,用于后续验证
const uint8_t uuid_zeros[ESP_UUID_LEN_128] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

// 宏定义用于将16位整数从当前字节序转换为大端字节序
#define ENDIAN_CHANGE_U16(x) ((((x)&0xFF00)>>8) + (((x)&0xFF)<<8))

// 定义ESP设备的UUID、Major和Minor值
#define ESP_UUID    {0xFD, 0xA5, 0x06, 0x93, 0xA4, 0xE2, 0x4F, 0xB1, 0xAF, 0xCF, 0xC6, 0xEB, 0x07, 0x64, 0x78, 0x25}
#define ESP_MAJOR   10167
#define ESP_MINOR   61958

// iBeacon头部结构体
typedef struct {
    uint8_t flags[3];          // 标志位
    uint8_t length;            // 数据长度
    uint8_t type;              // 数据类型
    uint16_t company_id;       // 公司ID (Apple: 0x004C)
    uint16_t beacon_type;      // Beacon类型 (iBeacon: 0x0215)
} __attribute__((packed)) esp_ble_ibeacon_head_t;

// iBeacon厂商数据结构体
typedef struct {
    uint8_t proximity_uuid[16]; // 唯一标识符UUID
    uint16_t major;             // Major值,标识较大区域
    uint16_t minor;             // Minor值,标识较小区域
    int8_t measured_power;      // 测量功率,用于距离估算
} __attribute__((packed)) esp_ble_ibeacon_vendor_t;

// 完整的iBeacon数据结构体
typedef struct {
    esp_ble_ibeacon_head_t ibeacon_head;    // iBeacon头部信息
    esp_ble_ibeacon_vendor_t ibeacon_vendor;// iBeacon厂商数据
} __attribute__((packed)) esp_ble_ibeacon_t;

// iBeacon数据包的公共头部信息
esp_ble_ibeacon_head_t ibeacon_common_head = {
    .flags = {0x02, 0x01, 0x06},            // 标志位:通用标志
    .length = 0x1A,                          // 数据长度
    .type = 0xFF,                            // 数据类型:厂商特定数据
    .company_id = 0x004C,                    // Apple公司ID
    .beacon_type = 0x1502                    // iBeacon类型
};

// iBeacon厂商数据配置
esp_ble_ibeacon_vendor_t vendor_config = {
    .proximity_uuid = ESP_UUID,              // 使用定义的ESP UUID
    .major = ENDIAN_CHANGE_U16(ESP_MAJOR),   // Major值,转换为大端模式
    .minor = ENDIAN_CHANGE_U16(ESP_MINOR),   // Minor值,转换为大端模式
    .measured_power = 0xC5                   // 测量功率
};

// 广播参数配置
static esp_ble_adv_params_t ble_adv_params = {
    .adv_int_min        = 0x20,              // 最小广播间隔
    .adv_int_max        = 0x40,              // 最大广播间隔
    .adv_type           = ADV_TYPE_NONCONN_IND, // 非连接广播
    .own_addr_type      = BLE_ADDR_TYPE_PUBLIC, // 使用公共地址
    .channel_map        = ADV_CHNL_ALL,      // 使用所有通道
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, // 不过滤扫描和连接请求
};

// GAP回调函数处理BLE事件
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    esp_err_t err;

    switch (event) {
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
        // 广播数据设置完成,开始广播
        esp_ble_gap_start_advertising(&ble_adv_params);
        break;
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
        // 广播启动完成事件,检查是否成功
        if ((err = param->adv_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(DEMO_TAG, "Adv start failed: %s", esp_err_to_name(err));
        }
        break;
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
        // 广播停止完成事件,检查是否成功
        if ((err = param->adv_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS){
            ESP_LOGE(DEMO_TAG, "Adv stop failed: %s", esp_err_to_name(err));
        } else {
            ESP_LOGI(DEMO_TAG, "Stop adv successfully");
        }
        break;
    default:
        break;
    }
}

// 配置iBeacon广播数据
esp_err_t esp_ble_config_ibeacon_data (esp_ble_ibeacon_vendor_t *vendor_config, esp_ble_ibeacon_t *ibeacon_adv_data){
    // 参数检查
    if ((vendor_config == NULL) || (ibeacon_adv_data == NULL) || (!memcmp(vendor_config->proximity_uuid, uuid_zeros, sizeof(uuid_zeros)))) {
        return ESP_ERR_INVALID_ARG;
    }

    // 复制公共头部信息到广播数据中
    memcpy(&ibeacon_adv_data->ibeacon_head, &ibeacon_common_head, sizeof(esp_ble_ibeacon_head_t));
    // 复制厂商数据到广播数据中
    memcpy(&ibeacon_adv_data->ibeacon_vendor, vendor_config, sizeof(esp_ble_ibeacon_vendor_t));

    return ESP_OK;
}

// 主应用程序入口
void app_main(void)
{
    // 初始化NVS存储
    ESP_ERROR_CHECK(nvs_flash_init());

    // 释放经典蓝牙模式占用的内存
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    // 配置蓝牙控制器
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    esp_bt_controller_init(&bt_cfg);

    // 启用蓝牙控制器(仅启用BLE模式)
    esp_bt_controller_enable(ESP_BT_MODE_BLE);

    // 初始化并启用BlueTooth协议栈
    esp_bluedroid_init();
    esp_bluedroid_enable();

    // 注册GAP回调函数
    esp_err_t status;
    if ((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK) {
        ESP_LOGE(DEMO_TAG, "gap register error: %s", esp_err_to_name(status));
        return;
    }

    // 创建iBeacon广播数据结构体
    esp_ble_ibeacon_t ibeacon_adv_data;
    status = esp_ble_config_ibeacon_data (&vendor_config, &ibeacon_adv_data);
    if (status == ESP_OK) {
        // 设置原始广播数据
        esp_ble_gap_config_adv_data_raw((uint8_t*)&ibeacon_adv_data, sizeof(ibeacon_adv_data));
    } else {
        ESP_LOGE(DEMO_TAG, "Config iBeacon data failed: %s", esp_err_to_name(status));
    }
}

如果不能确定是不是我们设备怎么办?

其实我们可以将uuid 和major minor 按自己的需求进行修改的,后续APP端按对应UUID进行检测即可。

复制代码
#define ESP_UUID    {0x1, 0x2, 0x03, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10}
#define ESP_MAJOR   0xAABB
#define ESP_MINOR   0x5566

自定义广播

当然,如果你不希望使用ibeacon的格式,你也是可以自定义的,例如我们定义下发的广播包adv_data

复制代码
// 定义一个16字节的服务UUID,用于标识设备可以提供哪方面的服务
//注意了,这是只是一个提示信息,方便客户端在未连接时就知道设备支持哪些服务
static uint8_t service_uuid[16] = {
    /* LSB <--------------------------------------------------------------------------------> MSB */
    // first uuid, 16bit, [12],[13] is the value
    0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00,
};

// 广播数据配置
/* The length of adv data must be less than 31 bytes */
static esp_ble_adv_data_t adv_data = {
    .set_scan_rsp        = false, // 这是广播数据,不是扫描响应数据
    .include_name        = true,  // 包含设备名称
    .include_txpower     = true,  // 包含发射功率信息
    .min_interval        = 0x0006, // 最小连接间隔,单位为1.25 ms,即 6 * 1.25 ms = 7.5 ms
    .max_interval        = 0x0010, // 最大连接间隔,单位为1.25 ms,即 16 * 1.25 ms = 20 ms
    .appearance          = 0x00,  // 外观属性,设置为默认值
    .manufacturer_len    = 0,     // 制造商数据长度,当前设置为0,表示不包含制造商数据
    .p_manufacturer_data = NULL,  // 制造商数据指针,当前设置为NULL
    .service_data_len    = 0,     // 服务数据长度,当前设置为0,表示不包含服务数据
    .p_service_data      = NULL,  // 服务数据指针,当前设置为NULL
    .service_uuid_len    = sizeof(service_uuid), // 服务UUID长度
    .p_service_uuid      = service_uuid,         // 服务UUID指针
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT), // 广播标志位
        // ESP_BLE_ADV_FLAG_GEN_DISC:通用发现模式
        // ESP_BLE_ADV_FLAG_BREDR_NOT_SPT:不支持BR/EDR(经典蓝牙),仅支持BLE
};

关于UUID的说明

复制代码
GATT层中定义的所有属性都有一个UUID值,UUID是全球唯一的128位的号码,它用来识别不同的特性。
蓝牙核心规范制定了两种不同的UUID
一种是基本的128位UUID
一种是代替基本UUID的16位UUID。

所有的蓝牙技术联盟定义UUID共用了一个基本的UUID:
0x0000xxxx-0000-1000-8000-00805F9B34FB
为了进一步简化基本UUID,每一个蓝牙技术联盟定义的属性有一个唯一的16位UUID,以代替上面的基本UUID的'x'部分。
例如,心率测量特性使用0X2A37作为它的16位UUID,因此它完整的128位UUID为:
0x00002A37-0000-1000-8000-00805F9B34FB
虽然蓝牙技术联盟使用相同的基本UUID,但是16位的UUID足够唯一地识别蓝牙技术联盟所定义的各种属性。
蓝牙技术联盟所用的基本UUID不能用于任何定制的属性、服务和特性。

对于定制的属性,必须使用另外完整的128位UUID。

同时,我们也可以

这里我们可以设置一个设备名称,最后设置好广播和广播应答数据即可

复制代码
#define SAMPLE_DEVICE_NAME    "XIAOZHI"

void app_main(void){
    .....
    ....
    ...
    esp_ble_gap_set_device_name(SAMPLE_DEVICE_NAME);
    esp_ble_gap_config_adv_data(&scan_rsp_data);
    esp_ble_gap_config_adv_data(&adv_data);
}

当然了,你还可以加一些自定义的数据到制造商数据或服务数据字段

复制代码
    .manufacturer_len    = 0,     // 制造商数据长度,当前设置为0,表示不包含制造商数据
    .p_manufacturer_data = NULL,  // 制造商数据指针,当前设置为NULL
    .service_data_len    = 0,     // 服务数据长度,当前设置为0,表示不包含服务数据
    .p_service_data      = NULL,  // 服务数据指针,当前设置为NULL

例如

复制代码
static uint8_t service_uuid[16] = {
    /* LSB <--------------------------------------------------------------------------------> MSB */
    //first uuid, 16bit, [12],[13] is the value
    0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00,
};

static uint8_t manufacturer_data[3] = {0x11, 0x22, 0x33};
static uint8_t manufacturer_data_rsp[3] = {0x66, 0x77, 0x88};

/* The length of adv data must be less than 31 bytes */
static esp_ble_adv_data_t adv_data = {
    .set_scan_rsp        = false,
    .include_name        = true,
    .include_txpower     = true,
    .min_interval        = 0x0006, //slave connection min interval, Time = min_interval * 1.25 msec
    .max_interval        = 0x0010, //slave connection max interval, Time = max_interval * 1.25 msec
    .appearance          = 0x00,
    .manufacturer_len = sizeof(manufacturer_data),
    .p_manufacturer_data = manufacturer_data,
    .service_data_len    = 0,
    .p_service_data      = NULL,
    .service_uuid_len    = sizeof(service_uuid),
    .p_service_uuid      = service_uuid,
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};

// scan response data
static esp_ble_adv_data_t scan_rsp_data = {
    .set_scan_rsp        = true,
    .include_name        = true,
    .include_txpower     = true,
    .min_interval        = 0x0006,
    .max_interval        = 0x0010,
    .appearance          = 0x00,
    .manufacturer_len = sizeof(manufacturer_data_rsp),
    .p_manufacturer_data = manufacturer_data_rsp,
    .service_data_len    = 0,
    .p_service_data      = NULL,
    .service_uuid_len    = sizeof(service_uuid),
    .p_service_uuid      = service_uuid,
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};

但是这里要注意广播数据和扫描响应数据的总长度不能超过 31 字节,总长度怎么算呢?

复制代码
设备名称:假设设备名称长度为 N 字节。
发射功率信息:1 字节。
制造商数据:5 字节(公司 ID 2 字节 + 自定义数据 3 字节)。
服务 UUID:16 字节。
广播数据 (adv_data):
include_name:N 字节。
include_txpower:1 字节。
manufacturer_len 和 p_manufacturer_data:5 字节。
service_uuid_len 和 p_service_uuid:16 字节。
总长度 = N + 1 + 5 + 16 = N + 22 字节。

如果超出了,怎么办呢?

减少设备名称长度

  • 设备名称长度 N 应该尽量短。假设设备名称长度为 5 字节,则总长度为 5 + 22 = 27 字节,仍然超过 31 字节。

移除不必要的字段

  • 移除 include_name 或 include_txpower。
  • 移除 manufacturer_len 和 p_manufacturer_data。
  • 移除 service_uuid_len 和 p_service_uuid。

优化数据结构

  • 如果必须包含所有字段,可以考虑只在广播数据中包含部分信息,而在扫描响应数据中包含其他信息。

最终程序如下:

复制代码
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "nvs_flash.h"

#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_gatt_defs.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"

#define SAMPLE_DEVICE_NAME    "XIAOZHI"
// 定义日志标签,用于调试输出
static const char* DEMO_TAG = "IBEACON_DEMO";

static uint8_t service_uuid[16] = {
    /* LSB <--------------------------------------------------------------------------------> MSB */
    //first uuid, 16bit, [12],[13] is the value
    0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00,
};

static uint8_t manufacturer_data[3] = {0x11, 0x22, 0x33};
static uint8_t manufacturer_data_rsp[3] = {0x66, 0x77, 0x88};

/* The length of adv data must be less than 31 bytes */
static esp_ble_adv_data_t adv_data = {
    .set_scan_rsp        = false,
    .include_name        = true,
    .include_txpower     = true,
    .min_interval        = 0x0006, //slave connection min interval, Time = min_interval * 1.25 msec
    .max_interval        = 0x0010, //slave connection max interval, Time = max_interval * 1.25 msec
    .appearance          = 0x00,
    .manufacturer_len = sizeof(manufacturer_data),
    .p_manufacturer_data = manufacturer_data,
    .service_data_len    = 0,
    .p_service_data      = NULL,
    .service_uuid_len    = sizeof(service_uuid),
    .p_service_uuid      = service_uuid,
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};

// scan response data
static esp_ble_adv_data_t scan_rsp_data = {
    .set_scan_rsp        = true,
    .include_name        = true,
    .include_txpower     = true,
    .min_interval        = 0x0006,
    .max_interval        = 0x0010,
    .appearance          = 0x00,
    .manufacturer_len = sizeof(manufacturer_data_rsp),
    .p_manufacturer_data = manufacturer_data_rsp,
    .service_data_len    = 0,
    .p_service_data      = NULL,
    .service_uuid_len    = sizeof(service_uuid),
    .p_service_uuid      = service_uuid,
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};

// 广播参数配置
static esp_ble_adv_params_t ble_adv_params = {
    .adv_int_min        = 0x20,              // 最小广播间隔
    .adv_int_max        = 0x40,              // 最大广播间隔
    .adv_type           = ADV_TYPE_NONCONN_IND, // 非连接广播
    .own_addr_type      = BLE_ADDR_TYPE_PUBLIC, // 使用公共地址
    .channel_map        = ADV_CHNL_ALL,      // 使用所有通道
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, // 不过滤扫描和连接请求
};

// GAP回调函数处理BLE事件
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    esp_err_t err;

    switch (event) {
    case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
        // 广播数据设置完成,开始广播
        esp_ble_gap_start_advertising(&ble_adv_params);
        break;
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
        // 广播启动完成事件,检查是否成功
        if ((err = param->adv_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(DEMO_TAG, "Adv start failed: %s", esp_err_to_name(err));
        }
        break;
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
        // 广播停止完成事件,检查是否成功
        if ((err = param->adv_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS){
            ESP_LOGE(DEMO_TAG, "Adv stop failed: %s", esp_err_to_name(err));
        } else {
            ESP_LOGI(DEMO_TAG, "Stop adv successfully");
        }
        break;
    default:
        break;
    }
}

// 主应用程序入口
void app_main(void)
{
    // 初始化NVS存储
    ESP_ERROR_CHECK(nvs_flash_init());

    // 释放经典蓝牙模式占用的内存
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    // 配置蓝牙控制器
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    esp_bt_controller_init(&bt_cfg);

    // 启用蓝牙控制器(仅启用BLE模式)
    esp_bt_controller_enable(ESP_BT_MODE_BLE);

    // 初始化并启用BlueTooth协议栈
    esp_bluedroid_init();
    esp_bluedroid_enable();

    // 注册GAP回调函数
    esp_err_t status;
    if ((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK) {
        ESP_LOGE(DEMO_TAG, "gap register error: %s", esp_err_to_name(status));
        return;
    }

    esp_ble_gap_set_device_name(SAMPLE_DEVICE_NAME);
    esp_ble_gap_config_adv_data(&scan_rsp_data);
    esp_ble_gap_config_adv_data(&adv_data);

}

上面代码中,我们发送广播时都是设置为不可连接的,如果我们希望这个设备可以连接,我们可以设置

复制代码
// 广播参数配置
static esp_ble_adv_params_t ble_adv_params = {
    .adv_int_min        = 0x20,              // 最小广播间隔
    .adv_int_max        = 0x40,              // 最大广播间隔
    .adv_type           = ADV_TYPE_IND,      // 可连接广播
    .own_addr_type      = BLE_ADDR_TYPE_PUBLIC, // 使用公共地址
    .channel_map        = ADV_CHNL_ALL,      // 使用所有通道
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, // 不过滤扫描和连接请求
};

adv_type参数说明:
ADV_TYPE_IND:可连接可扫描广播(Connectable Undirected Advertising)。
用途:允许其他设备扫描并连接到广播设备。
特点:
设备可以被其他设备发现并连接。
适用于需要建立连接的场景,如配对设备。

ADV_TYPE_DIRECT_IND_HIGH:高占空比直接广播(Directed Advertising - High Duty Cycle)。
用途:直接向特定设备发送广播,适用于快速连接。
特点:
广播数据仅发送给指定的设备地址。
占空比高,广播频率高,适用于快速连接。
适用于需要快速响应的场景。

ADV_TYPE_SCAN_IND:可扫描不可连接广播(Scannable Undirected Advertising)。
用途:允许其他设备扫描广播数据,但不允许连接。
特点:
设备可以被其他设备扫描,但不能直接连接。
适用于需要广播数据但不需要连接的场景,如发送传感器数据。

ADV_TYPE_NONCONN_IND:非连接广播(Non-connectable Undirected Advertising)。
用途:仅广播数据,不允许连接。
特点:
设备仅广播数据,不响应连接请求。
适用于仅需要广播数据的场景,如 iBeacon。
占用带宽少,功耗低。

ADV_TYPE_DIRECT_IND_LOW :低占空比直接广播(Directed Advertising - Low Duty Cycle)。
用途:直接向特定设备发送广播,适用于节省电量。
特点:
广播数据仅发送给指定的设备地址。
占空比低,广播频率低,适用于节省电量。
适用于不需要快速响应的场景。

own_addr_type参数说明
BLE_ADDR_TYPE_PUBLIC (0x00):设备使用其固定的公共地址进行广播。
特点:
地址是固定的,每次广播时使用相同的地址。适用于需要固定标识的场景。

BLE_ADDR_TYPE_RANDOM (0x01):设备使用随机生成的地址进行广播。
特点:
地址是随机生成的,每次广播时可以使用不同的地址。适用于需要隐私保护的场景。需要设备支持随机地址。

BLE_ADDR_TYPE_RPA_PUBLIC (0x02):设备使用基于公共地址生成的可解析随机地址进行广播。
特点:
地址是随机生成的,但可以通过解析密钥解析回公共地址。
适用于需要隐私保护但仍然希望被特定设备识别的场景。
需要设备支持可解析随机地址和解析密钥。

BLE_ADDR_TYPE_RPA_RANDOM (0x03):设备使用基于随机地址生成的可解析随机地址进行广播。
特点:
地址是随机生成的,但可以通过解析密钥解析回随机地址。 0 - 适用于需要隐私保护但仍然希望被特定设备识别的场景。
需要设备支持可解析随机地址和解析密钥。

adv_filter_policy参数说明:
• ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY 可被任何设备扫描和连接(不使用白名单)
• ADV_FILTER_ALLOW_SCAN_WLST_CON_ANY 处理所有连接请求和只处理在白名单设备中的扫描请求
• ADV_FILTER_ALLOW_SCAN_ANY_CON_WLST 处理所有扫描请求和只处理在白名单中的连接请求
• ADV_FILTER_ALLOW_SCAN_WLST_CON_WLST 只处理在白名单中设备的连接请求和扫描请求

当然,除了上述的方式,我们还可以用16进制的方式生成广播数据

复制代码
// 定义原始广播数据数组
static uint8_t raw_adv_data[] = {
    /* 广播数据中的标志字段 0x02 表示数据长度为 2 字节   */
    0x02, ESP_BLE_AD_TYPE_FLAG, 0x06,
    /* 发射功率级别字段 */
    0x02, ESP_BLE_AD_TYPE_TX_PWR, 0xEB,
    /* 完整的 16 位服务 UUID 字段 0x03 表示该字段数据长度为 3 字节 */
    0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0xFF, 0x00,
    /* 制造商特定的数据 长度(7字节),类型(制造商特定的数据),公司ID(0x0102),数据(0x03, 0x04, 0x05) */
    0x06, ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, 0x01, 0x02, 0x03, 0x04, 0x05
};

// 定义原始扫描响应数据数组
static uint8_t raw_scan_rsp_data[] = {
    /* 扫描响应数据中的标志字段 */
    0x02, ESP_BLE_AD_TYPE_FLAG, 0x06,
    /* 发射功率级别字段 */
    0x02, ESP_BLE_AD_TYPE_TX_PWR, 0xEB,
    /* 完整的 16 位服务 UUID 字段 */
    0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0xFE, 0x00
};

每种类型的字段说明可参考官方文档:

https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=302735

最终代码

复制代码
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "nvs_flash.h"

#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_gatt_defs.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"

#define SAMPLE_DEVICE_NAME    "XIAOZHI"
// 定义日志标签,用于调试输出
static const char* DEMO_TAG = "IBEACON_DEMO";

// 定义原始广播数据数组
static uint8_t raw_adv_data[] = {
    /* 广播数据中的标志字段 0x02 表示数据长度为 2 字节   */
    0x02, ESP_BLE_AD_TYPE_FLAG, 0x06,
    /* 发射功率级别字段 */
    0x02, ESP_BLE_AD_TYPE_TX_PWR, 0xEB,
    /* 完整的 16 位服务 UUID 字段 0x03 表示该字段数据长度为 3 字节 */
    0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0xFF, 0x00,
    /* 制造商特定的数据 长度(7字节),类型(制造商特定的数据),公司ID(0x0102),数据(0x03, 0x04, 0x05) */
    0x06, ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, 0x01, 0x02, 0x03, 0x04, 0x05
};

// 定义原始扫描响应数据数组
static uint8_t raw_scan_rsp_data[] = {
    /* 扫描响应数据中的标志字段 */
    0x02, ESP_BLE_AD_TYPE_FLAG, 0x06,
    /* 发射功率级别字段 */
    0x02, ESP_BLE_AD_TYPE_TX_PWR, 0xEB,
    /* 完整的 16 位服务 UUID 字段 */
    0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0xFE, 0x00
};

// 广播参数配置
static esp_ble_adv_params_t ble_adv_params = {
    .adv_int_min        = 0x20,              // 最小广播间隔
    .adv_int_max        = 0x40,              // 最大广播间隔
    .adv_type           = ADV_TYPE_IND,      // 可连接广播
    .own_addr_type      = BLE_ADDR_TYPE_PUBLIC, // 使用公共地址
    .channel_map        = ADV_CHNL_ALL,      // 使用所有通道
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, // 不过滤扫描和连接请求
};

// GAP回调函数处理BLE事件
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    esp_err_t err;

    switch (event) {
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
        esp_ble_gap_start_advertising(&ble_adv_params);
        break;
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
        // 广播启动完成事件,检查是否成功
        if ((err = param->adv_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(DEMO_TAG, "Adv start failed: %s", esp_err_to_name(err));
        }
        break;
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
        // 广播停止完成事件,检查是否成功
        if ((err = param->adv_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS){
            ESP_LOGE(DEMO_TAG, "Adv stop failed: %s", esp_err_to_name(err));
        } else {
            ESP_LOGI(DEMO_TAG, "Stop adv successfully");
        }
        break;
    default:
        break;
    }
}

// 主应用程序入口
void app_main(void)
{
    // 初始化NVS存储
    ESP_ERROR_CHECK(nvs_flash_init());

    // 释放经典蓝牙模式占用的内存
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    // 配置蓝牙控制器
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    esp_bt_controller_init(&bt_cfg);

    // 启用蓝牙控制器(仅启用BLE模式)
    esp_bt_controller_enable(ESP_BT_MODE_BLE);

    // 初始化并启用BlueTooth协议栈
    esp_bluedroid_init();
    esp_bluedroid_enable();

    // 注册GAP回调函数
    esp_err_t status;
    if ((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK) {
        ESP_LOGE(DEMO_TAG, "gap register error: %s", esp_err_to_name(status));
        return;
    }

    esp_ble_gap_set_device_name(SAMPLE_DEVICE_NAME);
    esp_ble_gap_config_adv_data_raw(raw_adv_data, sizeof(raw_adv_data));
    esp_ble_gap_config_scan_rsp_data_raw(raw_scan_rsp_data, sizeof(raw_scan_rsp_data));

}

了解完蓝牙广播后,我们来看看蓝牙服务,蓝牙广播是蓝牙设备告诉其它设备自己存在的一种办法,别人知道你存在,那肯定还需要了解你有什么作用,能提供什么服务?

我们以一个蓝牙灯产品为例,蓝牙灯产品可以提供灯服务,具体的服务内容为可以支持设置灯开关、读取灯颜色、读取灯位置。那我们先来自定义一个蓝牙灯服务,服务的UUID为0x00FF。

自定义服务

这里我们参考乐鑫官方的handle_table表格创建方式,可以更方便我们维护蓝牙服务

复制代码
enum
{
    IDX_SVC,          // 服务id
    HRS_IDX_NB,       // 总数
};
uint16_t handle_table[HRS_IDX_NB];  //服务表句柄,用于启动服务
 
// GATT协议主服务标识
static const uint16_t primary_service_uuid         = ESP_GATT_UUID_PRI_SERVICE; 
// 自定义服务的UUID
static const uint16_t GATTS_SERVICE_UUID_TEST      = 0x00FF; 
// 服务据库描述
static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] =
{
    // 服务声明
    [IDX_SVC]        =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)&GATTS_SERVICE_UUID_TEST}},
}

代码说明:

上述代码声明一个服务,这就像在设备上挂一个牌子,告诉别人"我这里有一个服务,UUID是0x00FF"。gatt_db[HRS_IDX_NB]中每个参数的含义如下

复制代码
IDX_SVC:属性索引
ESP_GATT_AUTO_RSP:BLE堆栈在读取或写入事件到达时自动进行响应;如果是ESP_GATT_RSP_BY_APP:则应用程序需要调用 esp_ble_gatts_send_response()手动响应消息。
ESP_UUID_LEN_16:UUID 长度设置为 16-bit
primary_service_uuid:表示这是一个主服务
ESP_GATT_PERM_READ:允许客户端读取这个服务的信息
sizeof(uint16_t):最大可传输长度
sizeof(GATTS_SERVICE_UUID_TEST):当前消息长度
GATTS_SERVICE_UUID_TEST:服务的UUID是0x00FF

esp_gatts_attr_db_t结构体解析如下:
/**
 * @brief 添加到 GATT 服务器数据库的属性类型
 */
typedef struct
{
    esp_attr_control_t      attr_control;                   /*!< 属性控制类型 */
    esp_attr_desc_t         att_desc;                       /*!< 属性类型描述 */
} esp_gatts_attr_db_t;
/**
 * @brief 属性自动响应标志
 */
typedef struct
{
#define ESP_GATT_RSP_BY_APP             0
#define ESP_GATT_AUTO_RSP               1
    /**
     * @brief 如果 auto_rsp 设置为 ESP_GATT_RSP_BY_APP,表示写/读操作的响应由应用程序回复。
              如果 auto_rsp 设置为 ESP_GATT_AUTO_RSP,表示写/读操作的响应由 GATT 栈自动回复。
     */
    uint8_t auto_rsp;
} esp_attr_control_t;
/**
 * @brief 属性描述(用于创建数据库)
 */
typedef struct
{
    uint16_t uuid_length;              /*!< UUID 长度 */
    uint8_t  *uuid_p;                  /*!< UUID 值 */
    uint16_t perm;                     /*!< 属性权限 */
    uint16_t max_length;               /*!< 元素的最大长度 */
    uint16_t length;                   /*!< 元素的当前长度 */
    uint8_t  *value;                   /*!< 元素值数组 */
} esp_attr_desc_t;

参考代码如下

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#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_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gatt_common_api.h"

#define GATTS_TABLE_TAG "GATTS_TABLE_DEMO"

#define SAMPLE_DEVICE_NAME          "XIAOZHI"  // 设备名称
#define SVC_INST_ID                 0       // 服务实例 ID

#define ADV_CONFIG_FLAG             (1 << 0)  // 广播配置标志
#define SCAN_RSP_CONFIG_FLAG        (1 << 1)  // 扫描响应配置标志

static uint8_t adv_config_done       = 0;  // 广播配置完成标志

// 定义原始广播数据数组
static uint8_t raw_adv_data[] = {
    /* 广播数据中的标志字段 0x02 表示数据长度为 2 字节   */
    0x02, ESP_BLE_AD_TYPE_FLAG, 0x06,
    /* 发射功率级别字段 */
    0x02, ESP_BLE_AD_TYPE_TX_PWR, 0xEB,
    /* 完整的 16 位服务 UUID 字段 0x03 表示该字段数据长度为 3 字节 */
    0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0xFF, 0x00,
    /* 制造商特定的数据 长度(7字节),类型(制造商特定的数据),公司ID(0x0102),数据(0x03, 0x04, 0x05) */
    0x06, ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, 0x01, 0x02, 0x03, 0x04, 0x05
};

// 定义原始扫描响应数据数组
static uint8_t raw_scan_rsp_data[] = {
    /* 扫描响应数据中的标志字段 */
    0x02, ESP_BLE_AD_TYPE_FLAG, 0x06,
    /* 发射功率级别字段 */
    0x02, ESP_BLE_AD_TYPE_TX_PWR, 0xEB,
    /* 完整的 16 位服务 UUID 字段 */
    0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0xFE, 0x00
};

static esp_ble_adv_params_t adv_params = {
    .adv_int_min         = 0x20,   // 最小广播间隔
    .adv_int_max         = 0x40,   // 最大广播间隔
    .adv_type            = ADV_TYPE_IND,  // 广播类型
    .own_addr_type       = BLE_ADDR_TYPE_PUBLIC,  // 地址类型
    .channel_map         = ADV_CHNL_ALL,  // 广播通道
    .adv_filter_policy   = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,  // 广播过滤策略
};

enum
{
    IDX_SVC,          // 服务声明
    HRS_IDX_NB,       // 总数
};
uint16_t handle_table[HRS_IDX_NB]; 
// GATT协议主服务标识
static const uint16_t primary_service_uuid         = ESP_GATT_UUID_PRI_SERVICE; 
// 自定义服务的UUID
static const uint16_t GATTS_SERVICE_UUID_TEST      = 0x00FF; 
/* 完整的数据库描述 - 用于将属性添加到数据库 */
static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] =
{
    // 服务声明
    [IDX_SVC]        =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)&GATTS_SERVICE_UUID_TEST}},
};

static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    switch (event) {
        case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT");
            adv_config_done &= (~ADV_CONFIG_FLAG);  // 清除广播配置标志
            if (adv_config_done == 0){
                esp_ble_gap_start_advertising(&adv_params);  // 开始广播
            }
            break;
        case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT");
            adv_config_done &= (~SCAN_RSP_CONFIG_FLAG);  // 清除扫描响应配置标志
            if (adv_config_done == 0){
                esp_ble_gap_start_advertising(&adv_params);  // 开始广播
            }
            break;
        case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
            /* 广播开始完成事件,表示广播开始成功或失败 */
            if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
                ESP_LOGE(GATTS_TABLE_TAG, "advertising start failed");
            }else{
                ESP_LOGI(GATTS_TABLE_TAG, "advertising start successfully");
            }
            break;
        case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
            if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {
                ESP_LOGE(GATTS_TABLE_TAG, "Advertising stop failed");
            }
            else {
                ESP_LOGI(GATTS_TABLE_TAG, "Stop adv successfully");
            }
            break;
        case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "update connection params status = %d, conn_int = %d, latency = %d, timeout = %d",
                  param->update_conn_params.status,
                  param->update_conn_params.conn_int,
                  param->update_conn_params.latency,
                  param->update_conn_params.timeout);
            break;
        default:
            break;
    }
}

// GATT 事件处理程序
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{
    switch (event) {
        case ESP_GATTS_REG_EVT:{
            ESP_LOGI(GATTS_TABLE_TAG, "gatts_if %d, app_id %d", gatts_if, param->reg.app_id);
            // 设置设备名称
            esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(SAMPLE_DEVICE_NAME);
            if (set_dev_name_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "set device name failed, error code = %x", set_dev_name_ret);
            }
            // 配置广播数据
            esp_err_t raw_adv_ret = esp_ble_gap_config_adv_data_raw(raw_adv_data, sizeof(raw_adv_data));
            if (raw_adv_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "config raw adv data failed, error code = %x ", raw_adv_ret);
            }
            adv_config_done |= ADV_CONFIG_FLAG;
            // 配置扫描响应数据
            esp_err_t raw_scan_ret = esp_ble_gap_config_scan_rsp_data_raw(raw_scan_rsp_data, sizeof(raw_scan_rsp_data));
            if (raw_scan_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "config raw scan rsp data failed, error code = %x", raw_scan_ret);
            }
            adv_config_done |= SCAN_RSP_CONFIG_FLAG;
            // 创建属性表
            esp_err_t create_attr_ret = esp_ble_gatts_create_attr_tab(gatt_db, gatts_if, HRS_IDX_NB, SVC_INST_ID);
            if (create_attr_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "create attr table failed, error code = %x", create_attr_ret);
            }
        }
        break;
        case ESP_GATTS_READ_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_READ_EVT");
        break;
        case ESP_GATTS_WRITE_EVT:{
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_WRITE_EVT");
            if (!param->write.is_prep){
                // 处理非准备写入事件
                ESP_LOGI(GATTS_TABLE_TAG, "prep write, handle = %d, value len = %d, value :", param->write.handle, param->write.len);
                ESP_LOG_BUFFER_HEX(GATTS_TABLE_TAG, param->write.value, param->write.len);
            }else{
                // 处理准备写入事件,适合大数据分段写入
                ESP_LOGI(GATTS_TABLE_TAG, "prepare write, handle = %d, value len = %d", param->write.handle, param->write.len);
                ESP_LOG_BUFFER_HEX(GATTS_TABLE_TAG, param->write.value, param->write.len);
            }
            // 如果需要响应,则发送响应
            if (param->write.need_rsp){
                esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
            }
        }
        break;
        case ESP_GATTS_EXEC_WRITE_EVT:
            // 如果客户端执行了ESP_GATTS_WRITE_EVT分段写入,再触发ESP_GATTS_EXEC_WRITE_EVT则是提交所有数据
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_EXEC_WRITE_EVT");
            if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC){
                //此处可以执行数据提交操作
            }
            break;
        case ESP_GATTS_MTU_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_MTU_EVT, MTU %d", param->mtu.mtu);
            break;
        case ESP_GATTS_CONF_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_CONF_EVT, status = %d, attr_handle %d", param->conf.status, param->conf.handle);
            break;
        case ESP_GATTS_START_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "SERVICE_START_EVT, status %d, service_handle %d", param->start.status, param->start.service_handle);
            break;
        case ESP_GATTS_CONNECT_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_CONNECT_EVT, conn_id = %d", param->connect.conn_id);
            ESP_LOG_BUFFER_HEX(GATTS_TABLE_TAG, param->connect.remote_bda, 6);
            // 更新连接参数
            esp_ble_conn_update_params_t conn_params = {0};
            memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
            /* 对于 iOS 系统,请参考 Apple 官方文档关于 BLE 连接参数限制。 */
            conn_params.latency = 0;
            conn_params.max_int = 0x20;    // max_int = 0x20*1.25ms = 40ms
            conn_params.min_int = 0x10;    // min_int = 0x10*1.25ms = 20ms
            conn_params.timeout = 400;    // timeout = 400*10ms = 4000ms
            // 开始向对等设备发送更新连接参数请求
            esp_ble_gap_update_conn_params(&conn_params);
            break;
        case ESP_GATTS_DISCONNECT_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_DISCONNECT_EVT, reason = 0x%x", param->disconnect.reason);
            esp_ble_gap_start_advertising(&adv_params);
            break;
        case ESP_GATTS_CREAT_ATTR_TAB_EVT:{
            if (param->add_attr_tab.status != ESP_GATT_OK){
                ESP_LOGE(GATTS_TABLE_TAG, "create attribute table failed, error code=0x%x", param->add_attr_tab.status);
            }
            else if (param->add_attr_tab.num_handle != HRS_IDX_NB){
                ESP_LOGE(GATTS_TABLE_TAG, "create attribute table abnormally, num_handle (%d) \
                        doesn't equal to HRS_IDX_NB(%d)", param->add_attr_tab.num_handle, HRS_IDX_NB);
            }
            else {
                ESP_LOGI(GATTS_TABLE_TAG, "create attribute table successfully, the number handle = %d",param->add_attr_tab.num_handle);
                memcpy(handle_table, param->add_attr_tab.handles, sizeof(handle_table));
                esp_ble_gatts_start_service(handle_table[IDX_SVC]);
            }
            break;
        }
        case ESP_GATTS_STOP_EVT:
        case ESP_GATTS_OPEN_EVT:
        case ESP_GATTS_CANCEL_OPEN_EVT:
        case ESP_GATTS_CLOSE_EVT:
        case ESP_GATTS_LISTEN_EVT:
        case ESP_GATTS_CONGEST_EVT:
        case ESP_GATTS_UNREG_EVT:
        case ESP_GATTS_DELETE_EVT:
        default:
            break;
    }
}

// 主应用程序入口
void app_main(void)
{
    esp_err_t ret;

    // 初始化 NVS。
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK( ret );

    // 释放经典蓝牙模式的控制器内存
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    // 初始化蓝牙控制器配置
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    // 启用蓝牙控制器
    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    // 初始化蓝牙协议栈
    ret = esp_bluedroid_init();
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    // 启用蓝牙协议栈
    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    // 注册 GATT 服务回调函数
    ret = esp_ble_gatts_register_callback(gatts_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gatts register error, error code = %x", ret);
        return;
    }

    // 注册 GAP 回调函数
    ret = esp_ble_gap_register_callback(gap_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gap register error, error code = %x", ret);
        return;
    }

    // 注册 GATT 应用程序
    //app_id = 0,ESP32中可以同时运行多个GATT应用程序,每个应用程序都需要一个唯一的 app_id,取值范围为 0x0000 到 0xFFFF。
    ret = esp_ble_gatts_app_register(0);
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gatts app register error, error code = %x", ret);
        return;
    }

    //设置本地设备的GATT最大传输单元(MTU)。MTU 是 GATT 通信中一次传输的最大数据包大小
    esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500);
    if (local_mtu_ret){
        ESP_LOGE(GATTS_TABLE_TAG, "set local  MTU failed, error code = %x", local_mtu_ret);
    }
}

这里使用的APP是LightBlue,可以自行下载,其它蓝牙软件都是可以的。

运行后,通过手机连接可以看到

上方栏目是广播时该设备支持的服务,我们定义了两个00FF 00FE两个。

下方栏目表示从设备支持3个服务,其中前面0x1800、0x1801是蓝牙联盟定义的,后面的0x00FF是自定义的,显示Unknown Service,因为是我们自定义的,第三方的APP不知道具体的服务名称,如果是我们自己开发的APP,我们可以查询到0x00FF后,在界面上显示,这是一个灯服务。

让别人知道你能提供服务后,如果别人希望使用你的服务,那应该如何使用呢?我们就可以给服务添加多个特征,还是原来的例子,前面我们创建了灯的服务,如果我们希望别人可以知道现在灯的颜色和位置,那我们就可以添加

这些参数作为服务的特征,例如灯的颜色。

添加自定义可读特征****A

灯的颜色值是可以被别人读取的,所以我们用特征A表示灯的颜色值,一个特征必须包含特征声明、特征值,每个特征都需要有自己的UUID,这里我们把特征A的UUID设置为0xFF01,它的值用4个byte表示,这个特征是可以被读取的,所以设置为ESP_GATT_CHAR_PROP_BIT_READ特征可读。

上面可以在自定义服务的工程中,添加下方代码:

复制代码
enum
{
    IDX_SVC,          // 服务声明
    IDX_CHAR_A,       // 特征 A 声明
    IDX_CHAR_VAL_A,   // 特征 A 值

    HRS_IDX_NB,       // 总数
};
uint16_t handle_table[HRS_IDX_NB]; 
// GATT协议主服务标识
static const uint16_t primary_service_uuid         = ESP_GATT_UUID_PRI_SERVICE; 
// GATT特征标识
static const uint16_t character_declaration_uuid   = ESP_GATT_UUID_CHAR_DECLARE; 

// 自定义服务的UUID
static const uint16_t GATTS_SERVICE_UUID_TEST      = 0x00FF;
// 自定义特征A的UUID
static const uint16_t GATTS_CHAR_UUID_TEST_A       = 0xFF01; 

static const uint8_t char_prop_read                =  ESP_GATT_CHAR_PROP_BIT_READ;  // 读属性
static const uint8_t char_value[4]                 = {0x11, 0x22, 0x33, 0x44};  // 特征值初始值

/* 完整的数据库描述 - 用于将属性添加到数据库 */
static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] =
{
    // 服务声明
    [IDX_SVC]        =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)&GATTS_SERVICE_UUID_TEST}},

    /* 特征 A 声明 */
    [IDX_CHAR_A]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read), (uint8_t *)&char_prop_read}},

    /* 特征 A 值 */
    [IDX_CHAR_VAL_A] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_A, ESP_GATT_PERM_READ,
      500, sizeof(char_value), (uint8_t *)char_value}},
};

代码解释

复制代码
    /* 特征 A 声明 */
    [IDX_CHAR_A]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read), (uint8_t *)&char_prop_read}},
作用:声明一个特征(Characteristic),特征可被客户端读取,并声明它支持可读属性。
通俗解释:这就像在服务下挂一个子牌子,告诉别人"我这里有一个特征,支持读操作"。
关键点:
character_declaration_uuid:表示这是一个特征声明。
char_prop_read:这个特征支持读操作。

    /* 特征 A 值 */
    [IDX_CHAR_VAL_A] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_A, ESP_GATT_PERM_READ,
      500, sizeof(char_value), (uint8_t *)char_value}},
作用:定义特征 A 的实际值和权限。
通俗解释:这是特征 A 的具体数据存储位置,客户端可以读取或写入这个值。
关键点:
500对应设置的MTU最大值,表示一次可传输的最大字节长度
GATTS_CHAR_UUID_TEST_A:特征 A 的UUID是0xFF01。
ESP_GATT_PERM_READ :允许客户端读取这个值。
char_value:特征的初始值是{0x11, 0x22, 0x33, 0x44}。

然后通过定时器更新特征A的值,也可以理解为更新灯的颜色值,把这部分代码也添加到自定义服务的工程中,如果你不知道如何添加,在文章最后一个模块,有最终的整体工程。

复制代码
#include "esp_timer.h"

static bool is_connect = false;

// 定时器回调函数
void timer_callback(void *arg) {
    // 修改特征值
    char_value[0]++;
    char_value[1]++;
    char_value[2]++;
    char_value[3]++;
    //需在ESP_GATTS_CONNECT_EVT事件中设置为true
    if(is_connect){
        // 更新 GATT 服务器中的特征值
        esp_ble_gatts_set_attr_value(handle_table[IDX_CHAR_VAL_A], sizeof(char_value), (uint8_t *)char_value);
    }
}

// 定时器初始化函数
// 需把函数添加到app_main最后
static void timer_init(void)
{
    const esp_timer_create_args_t periodic_timer_args = {
        .callback = &timer_callback,
        .name = "periodic_timer"
    };

    esp_timer_handle_t periodic_timer;
    ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
    ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, 1000000)); // 1秒
}

烧录后,重新连接设备,我们就可以发现服务0x00FF下有一个可读的特征FF01,我们可以点击右侧的↓箭头,就可以看到每次读取到的数据都在变化。

添加自定义可写特征****B

前面我们说到,灯除了一些可以被读取的特征,例如颜色、位置,还有可以被别人控制的功能,那就是灯的开关,我们也可以为灯的开关定义一个特征,用户可以通过修改该特征来控制灯的开关。

需要同时声明特征和设置特征的值,特征B的UUID为0xFF02,属性为可写ESP_GATT_CHAR_PROP_BIT_WRITE

复制代码
enum
{
    IDX_SVC,          // 服务声明
    IDX_CHAR_A,       // 特征 A 声明
    IDX_CHAR_VAL_A,   // 特征 A 值

    IDX_CHAR_B,       // 特征 B 声明
    IDX_CHAR_VAL_B,   // 特征 B 值

    HRS_IDX_NB,       // 总数
};
uint16_t handle_table[HRS_IDX_NB]; 
// GATT协议主服务标识
static const uint16_t primary_service_uuid         = ESP_GATT_UUID_PRI_SERVICE; 
// GATT特征标识
static const uint16_t character_declaration_uuid   = ESP_GATT_UUID_CHAR_DECLARE; 

// 自定义服务的UUID
static const uint16_t GATTS_SERVICE_UUID_TEST      = 0x00FF;
// 自定义特征的UUID
static const uint16_t GATTS_CHAR_UUID_TEST_A       = 0xFF01;
static const uint16_t GATTS_CHAR_UUID_TEST_B       = 0xFF02;   

static const uint8_t char_prop_read          =  ESP_GATT_CHAR_PROP_BIT_READ; // 读属性
static const uint8_t char_prop_write         =  ESP_GATT_CHAR_PROP_BIT_WRITE; // 写属性

static const uint8_t char_value[4]                 = {0x11, 0x22, 0x33, 0x44};  // 特征值初始值

/* 完整的数据库描述 - 用于将属性添加到数据库 */
static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] =
{
    // 服务声明
    [IDX_SVC]        =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)&GATTS_SERVICE_UUID_TEST}},

    /* 特征 A 声明 */
    [IDX_CHAR_A]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read), (uint8_t *)&char_prop_read}},

    /* 特征 A 值 */
    [IDX_CHAR_VAL_A] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_A, ESP_GATT_PERM_READ,
      500, sizeof(char_value), (uint8_t *)char_value}},

    /* 特征 B 声明 */
    [IDX_CHAR_B]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_write), (uint8_t *)&char_prop_write}},

    /* 特征 B 值 */
    [IDX_CHAR_VAL_B] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_B, ESP_GATT_PERM_WRITE,
      500, 0, NULL}},
};

解释:
    /* 特征 B 声明 */
    [IDX_CHAR_B]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_write), (uint8_t *)&char_prop_write}},
    /* 特征 B 值 */
    [IDX_CHAR_VAL_B] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_B, ESP_GATT_PERM_WRITE,
      500, 0, NULL}},
     
作用:声明特征 B 并定义它的值和权限。
通俗解释:特征 B 只支持写入操作,客户端可以写入它的值。
关键点:
char_prop_write:特征 B 只支持写入操作。
GATTS_CHAR_UUID_TEST_C:特征 B 的UUID是0xFF02。

重新连接设备后,我们就可以发现有一个可写的特征,我们可以点击右侧的↑箭头,填写数据发送给设备。

添加自定义可读写特征****C

同理,如果设备中有一些特征是可读可写的,我们也可以自定义一个可读写特征,例如前面灯的开关,我们可以通过写属性控制灯的亮灭,也可以通过读属性读取当前灯的亮灭状态,所以灯的开关更准确来说应该是一个可读写的特征。

复制代码
enum
{
    IDX_SVC,          // 服务声明
    IDX_CHAR_A,       // 特征 A 声明
    IDX_CHAR_VAL_A,   // 特征 A 值

    IDX_CHAR_B,       // 特征 B 声明
    IDX_CHAR_VAL_B,   // 特征 B 值

    IDX_CHAR_C,       // 特征 C 声明
    IDX_CHAR_VAL_C,   // 特征 C 值

    HRS_IDX_NB,       // 总数
};
uint16_t handle_table[HRS_IDX_NB]; 
// GATT协议主服务标识
static const uint16_t primary_service_uuid         = ESP_GATT_UUID_PRI_SERVICE; 
// GATT特征标识
static const uint16_t character_declaration_uuid   = ESP_GATT_UUID_CHAR_DECLARE; 

// 自定义服务的UUID
static const uint16_t GATTS_SERVICE_UUID_TEST      = 0x00FF;
// 自定义特征的UUID
static const uint16_t GATTS_CHAR_UUID_TEST_A       = 0xFF01;
static const uint16_t GATTS_CHAR_UUID_TEST_B       = 0xFF02;   
static const uint16_t GATTS_CHAR_UUID_TEST_C       = 0xFF03;   

static const uint8_t char_prop_read          =  ESP_GATT_CHAR_PROP_BIT_READ; // 读属性
static const uint8_t char_prop_write         =  ESP_GATT_CHAR_PROP_BIT_WRITE; // 写属性
static const uint8_t char_prop_read_write    =  ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE; // 读写属性

static const uint8_t char_value[4]                 = {0x11, 0x22, 0x33, 0x44};  // 特征值初始值

/* 完整的数据库描述 - 用于将属性添加到数据库 */
static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] =
{
    // 服务声明
    [IDX_SVC]        =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)&GATTS_SERVICE_UUID_TEST}},

    /* 特征 A 声明 */
    [IDX_CHAR_A]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read), (uint8_t *)&char_prop_read}},

    /* 特征 A 值 */
    [IDX_CHAR_VAL_A] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_A, ESP_GATT_PERM_READ,
      500, sizeof(char_value), (uint8_t *)char_value}},

    /* 特征 B 声明 */
    [IDX_CHAR_B]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_write), (uint8_t *)&char_prop_write}},

    /* 特征 B 值 */
    [IDX_CHAR_VAL_B] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_B, ESP_GATT_PERM_WRITE,
      500, 0, NULL}},

    /* 特征 C 声明 */
    [IDX_CHAR_C]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read_write), (uint8_t *)&char_prop_read_write}},

    /* 特征 C 值 */
    [IDX_CHAR_VAL_C] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_C, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
      500, sizeof(char_value), (uint8_t *)char_value}},
};

重新烧录连接设备后,我们可以看到特征FF03右侧有上下两个箭头,代表这个特征是可读写的。

添加自定义可读写**+** 通知特征 D

如果我们的灯比较特别,还有故障提醒的功能,在灯故障时,我们希望灯可主动给我们发一些信息,这时候我们就可以自定义一个通知特征。

通知需要和特征描述符搭配使用

复制代码
enum
{
    IDX_SVC,          // 服务声明
    IDX_CHAR_A,       // 特征 A 声明
    IDX_CHAR_VAL_A,   // 特征 A 值

    IDX_CHAR_B,       // 特征 B 声明
    IDX_CHAR_VAL_B,   // 特征 B 值

    IDX_CHAR_C,       // 特征 C 声明
    IDX_CHAR_VAL_C,   // 特征 C 值

    IDX_CHAR_D,       // 特征 D 声明
    IDX_CHAR_VAL_D,   // 特征 D 值
    IDX_CHAR_CFG_D,   // 特征 D 配置描述符

    HRS_IDX_NB,       // 总数
};
uint16_t handle_table[HRS_IDX_NB]; 
// GATT协议主服务标识
static const uint16_t primary_service_uuid         = ESP_GATT_UUID_PRI_SERVICE; 
// GATT特征标识
static const uint16_t character_declaration_uuid   = ESP_GATT_UUID_CHAR_DECLARE; 
// GATT特征描述符标识
static const uint16_t character_client_config_uuid = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;

// 自定义服务的UUID
static const uint16_t GATTS_SERVICE_UUID_TEST      = 0x00FF;
// 自定义特征的UUID
static const uint16_t GATTS_CHAR_UUID_TEST_A       = 0xFF01;
static const uint16_t GATTS_CHAR_UUID_TEST_B       = 0xFF02;   
static const uint16_t GATTS_CHAR_UUID_TEST_C       = 0xFF03;   
static const uint16_t GATTS_CHAR_UUID_TEST_D       = 0xFF04;   

static const uint8_t char_prop_read          =  ESP_GATT_CHAR_PROP_BIT_READ; // 读属性
static const uint8_t char_prop_write         =  ESP_GATT_CHAR_PROP_BIT_WRITE; // 写属性
static const uint8_t char_prop_read_write    =  ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE; // 读写属性
static const uint8_t char_prop_read_write_notify    =  ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY; // 读写通知属性

static uint8_t char_value[4]            = {0x11, 0x22, 0x33, 0x44};  // 特征值初始值
static const uint8_t char_d_ccc[2]      = {0x00, 0x00};  // 客户端配置描述符初始值 0x0000关闭通知和指示 0x0001开启通知,0x0002开启指示。

/* 完整的数据库描述 - 用于将属性添加到数据库 */
static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] =
{
    // 服务声明
    [IDX_SVC]        =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)&GATTS_SERVICE_UUID_TEST}},

    /* 特征 A 声明 */
    [IDX_CHAR_A]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read), (uint8_t *)&char_prop_read}},

    /* 特征 A 值 */
    [IDX_CHAR_VAL_A] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_A, ESP_GATT_PERM_READ,
      500, sizeof(char_value), (uint8_t *)char_value}},

    /* 特征 B 声明 */
    [IDX_CHAR_B]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_write), (uint8_t *)&char_prop_write}},

    /* 特征 B 值 */
    [IDX_CHAR_VAL_B] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_B, ESP_GATT_PERM_WRITE,
      500, 0, NULL}},

    /* 特征 C 声明 */
    [IDX_CHAR_C]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read_write), (uint8_t *)&char_prop_read_write}},

    /* 特征 C 值 */
    [IDX_CHAR_VAL_C] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_C, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
      500, sizeof(char_value), (uint8_t *)char_value}},

    /* 特征 D 声明 */
    [IDX_CHAR_D]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read_write), (uint8_t *)&char_prop_read_write_notify}},

    /* 特征 D 值 */
    [IDX_CHAR_VAL_D] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_D, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
      500, sizeof(char_value), (uint8_t *)char_value}},

    /* 特征 D 描述符 */
    [IDX_CHAR_CFG_D]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
      sizeof(uint16_t), sizeof(char_d_ccc), (uint8_t *)char_d_ccc}},
};

解释:
    /* 特征 D 描述符 */
    [IDX_CHAR_CFG_D]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
      sizeof(uint16_t), sizeof(char_d_ccc), (uint8_t *)char_d_ccc}},
作用:配置特征 D 的通知或指示功能。
通俗解释:这就像一个开关,客户端可以通过它来开启或关闭特征 D 的通知功能。
关键点:
character_client_config_uuid:表示这是一个客户端配置描述符。
char_d_ccc:描述符的初始值是{0x00, 0x00},表示通知功能默认关闭。

上方设置了通知后,我们就可以在手机端开启通知,等待设备更新消息了,设备更新消息使用下方函数:

esp_ble_gatts_send_indicate 用于发送指示(Indicate)或通知(Notify)。通知和指示的区别在于,指示需要客户端确认,而通知不需要。

复制代码
esp_err_t ret = esp_ble_gatts_send_indicate(
    gatts_if,          // GATT接口句柄
    conn_id,           // 连接ID
    attr_handle,       // 特征值的属性句柄
    value_len,         // 数据长度
    value,             // 数据内容
    false              // need_confirm:false表示通知,true表示指示);

最终参考,我们开启了一个定时器,在客户端连接成功后,每隔1s上报一条通知

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#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_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gatt_common_api.h"
#include "esp_timer.h"

#define GATTS_TABLE_TAG "GATTS_TABLE_DEMO"

#define SAMPLE_DEVICE_NAME          "XIAOZHI"  // 设备名称
#define SVC_INST_ID                 0       // 服务实例 ID

#define ADV_CONFIG_FLAG             (1 << 0)  // 广播配置标志
#define SCAN_RSP_CONFIG_FLAG        (1 << 1)  // 扫描响应配置标志

static uint8_t adv_config_done       = 0;  // 广播配置完成标志
static bool is_connect = false;
static esp_gatt_if_t g_gatts_if;
static uint16_t gatt_conn_id;

// 定义原始广播数据数组
static uint8_t raw_adv_data[] = {
    /* 广播数据中的标志字段 0x02 表示数据长度为 2 字节   */
    0x02, ESP_BLE_AD_TYPE_FLAG, 0x06,
    /* 发射功率级别字段 */
    0x02, ESP_BLE_AD_TYPE_TX_PWR, 0xEB,
    /* 完整的 16 位服务 UUID 字段 0x03 表示该字段数据长度为 3 字节 */
    0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0xFF, 0x00,
    /* 制造商特定的数据 长度(7字节),类型(制造商特定的数据),公司ID(0x0102),数据(0x03, 0x04, 0x05) */
    0x06, ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, 0x01, 0x02, 0x03, 0x04, 0x05
};

// 定义原始扫描响应数据数组
static uint8_t raw_scan_rsp_data[] = {
    /* 扫描响应数据中的标志字段 */
    0x02, ESP_BLE_AD_TYPE_FLAG, 0x06,
    /* 发射功率级别字段 */
    0x02, ESP_BLE_AD_TYPE_TX_PWR, 0xEB,
    /* 完整的 16 位服务 UUID 字段 */
    0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0xFE, 0x00
};

static esp_ble_adv_params_t adv_params = {
    .adv_int_min         = 0x20,   // 最小广播间隔
    .adv_int_max         = 0x40,   // 最大广播间隔
    .adv_type            = ADV_TYPE_IND,  // 广播类型
    .own_addr_type       = BLE_ADDR_TYPE_PUBLIC,  // 地址类型
    .channel_map         = ADV_CHNL_ALL,  // 广播通道
    .adv_filter_policy   = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,  // 广播过滤策略
};

enum
{
    IDX_SVC,          // 服务声明
    IDX_CHAR_A,       // 特征 A 声明
    IDX_CHAR_VAL_A,   // 特征 A 值

    IDX_CHAR_B,       // 特征 B 声明
    IDX_CHAR_VAL_B,   // 特征 B 值

    IDX_CHAR_C,       // 特征 C 声明
    IDX_CHAR_VAL_C,   // 特征 C 值

    IDX_CHAR_D,       // 特征 D 声明
    IDX_CHAR_VAL_D,   // 特征 D 值
    IDX_CHAR_CFG_D,   // 特征 D 配置描述符

    HRS_IDX_NB,       // 总数
};
uint16_t handle_table[HRS_IDX_NB]; 
// GATT协议主服务标识
static const uint16_t primary_service_uuid         = ESP_GATT_UUID_PRI_SERVICE; 
// GATT特征标识
static const uint16_t character_declaration_uuid   = ESP_GATT_UUID_CHAR_DECLARE; 
// GATT特征描述符标识
static const uint16_t character_client_config_uuid = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;

// 自定义服务的UUID
static const uint16_t GATTS_SERVICE_UUID_TEST      = 0x00FF;
// 自定义特征的UUID
static const uint16_t GATTS_CHAR_UUID_TEST_A       = 0xFF01;
static const uint16_t GATTS_CHAR_UUID_TEST_B       = 0xFF02;   
static const uint16_t GATTS_CHAR_UUID_TEST_C       = 0xFF03;   
static const uint16_t GATTS_CHAR_UUID_TEST_D       = 0xFF04;   

static const uint8_t char_prop_read          =  ESP_GATT_CHAR_PROP_BIT_READ; // 读属性
static const uint8_t char_prop_write         =  ESP_GATT_CHAR_PROP_BIT_WRITE; // 写属性
static const uint8_t char_prop_read_write    =  ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE; // 读写属性
static const uint8_t char_prop_read_write_notify    =  ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY; // 读写通知属性

static uint8_t char_value[4]            = {0x11, 0x22, 0x33, 0x44};  // 特征值初始值
static const uint8_t char_d_ccc[2]      = {0x00, 0x00};  // 客户端配置描述符初始值 0x0000关闭通知和指示 0x0001开启通知,0x0002开启指示。

/* 完整的数据库描述 - 用于将属性添加到数据库 */
static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] =
{
    // 服务声明
    [IDX_SVC]        =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)&GATTS_SERVICE_UUID_TEST}},

    /* 特征 A 声明 */
    [IDX_CHAR_A]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read), (uint8_t *)&char_prop_read}},

    /* 特征 A 值 */
    [IDX_CHAR_VAL_A] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_A, ESP_GATT_PERM_READ,
      500, sizeof(char_value), (uint8_t *)char_value}},

    /* 特征 B 声明 */
    [IDX_CHAR_B]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_write), (uint8_t *)&char_prop_write}},

    /* 特征 B 值 */
    [IDX_CHAR_VAL_B] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_B, ESP_GATT_PERM_WRITE,
      500, 0, NULL}},

    /* 特征 C 声明 */
    [IDX_CHAR_C]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read_write), (uint8_t *)&char_prop_read_write}},

    /* 特征 C 值 */
    [IDX_CHAR_VAL_C] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_C, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
      500, sizeof(char_value), (uint8_t *)char_value}},

    /* 特征 D 声明 */
    [IDX_CHAR_D]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read_write), (uint8_t *)&char_prop_read_write_notify}},

    /* 特征 D 值 */
    [IDX_CHAR_VAL_D] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_D, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
      500, sizeof(char_value), (uint8_t *)char_value}},

    /* 特征 D 描述符 */
    [IDX_CHAR_CFG_D]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
      sizeof(uint16_t), sizeof(char_d_ccc), (uint8_t *)char_d_ccc}},
};

static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    switch (event) {
        case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT");
            adv_config_done &= (~ADV_CONFIG_FLAG);  // 清除广播配置标志
            if (adv_config_done == 0){
                esp_ble_gap_start_advertising(&adv_params);  // 开始广播
            }
            break;
        case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT");
            adv_config_done &= (~SCAN_RSP_CONFIG_FLAG);  // 清除扫描响应配置标志
            if (adv_config_done == 0){
                esp_ble_gap_start_advertising(&adv_params);  // 开始广播
            }
            break;
        case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
            /* 广播开始完成事件,表示广播开始成功或失败 */
            if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
                ESP_LOGE(GATTS_TABLE_TAG, "advertising start failed");
            }else{
                ESP_LOGI(GATTS_TABLE_TAG, "advertising start successfully");
            }
            break;
        case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
            if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {
                ESP_LOGE(GATTS_TABLE_TAG, "Advertising stop failed");
            }
            else {
                ESP_LOGI(GATTS_TABLE_TAG, "Stop adv successfully");
            }
            break;
        case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "update connection params status = %d, conn_int = %d, latency = %d, timeout = %d",
                  param->update_conn_params.status,
                  param->update_conn_params.conn_int,
                  param->update_conn_params.latency,
                  param->update_conn_params.timeout);
            break;
        default:
            break;
    }
}

// GATT 事件处理程序
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{
    switch (event) {
        case ESP_GATTS_REG_EVT:{
            ESP_LOGI(GATTS_TABLE_TAG, "gatts_if %d, app_id %d", gatts_if, param->reg.app_id);
            // 设置设备名称
            esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(SAMPLE_DEVICE_NAME);
            if (set_dev_name_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "set device name failed, error code = %x", set_dev_name_ret);
            }
            // 配置广播数据
            esp_err_t raw_adv_ret = esp_ble_gap_config_adv_data_raw(raw_adv_data, sizeof(raw_adv_data));
            if (raw_adv_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "config raw adv data failed, error code = %x ", raw_adv_ret);
            }
            adv_config_done |= ADV_CONFIG_FLAG;
            // 配置扫描响应数据
            esp_err_t raw_scan_ret = esp_ble_gap_config_scan_rsp_data_raw(raw_scan_rsp_data, sizeof(raw_scan_rsp_data));
            if (raw_scan_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "config raw scan rsp data failed, error code = %x", raw_scan_ret);
            }
            adv_config_done |= SCAN_RSP_CONFIG_FLAG;
            // 创建属性表
            esp_err_t create_attr_ret = esp_ble_gatts_create_attr_tab(gatt_db, gatts_if, HRS_IDX_NB, SVC_INST_ID);
            if (create_attr_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "create attr table failed, error code = %x", create_attr_ret);
            }
        }
        break;
        case ESP_GATTS_READ_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_READ_EVT");
        break;
        case ESP_GATTS_WRITE_EVT:{
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_WRITE_EVT");
            if (!param->write.is_prep){
                // 处理非准备写入事件
                ESP_LOGI(GATTS_TABLE_TAG, "prep write, handle = %d, value len = %d, value :", param->write.handle, param->write.len);
                ESP_LOG_BUFFER_HEX(GATTS_TABLE_TAG, param->write.value, param->write.len);
                if (handle_table[IDX_CHAR_CFG_D] == param->write.handle && param->write.len == 2){
                    uint16_t descr_value = param->write.value[1]<<8 | param->write.value[0];
                    if (descr_value == 0x0001){
                        ESP_LOGI(GATTS_TABLE_TAG, "notify enable");
                    }else if (descr_value == 0x0002){
                        ESP_LOGI(GATTS_TABLE_TAG, "indicate enable");
                    }else if (descr_value == 0x0000){
                        ESP_LOGI(GATTS_TABLE_TAG, "notify/indicate disable ");
                    }
                }
            }else{
                // 处理准备写入事件,适合大数据分段写入
                ESP_LOGI(GATTS_TABLE_TAG, "prepare write, handle = %d, value len = %d", param->write.handle, param->write.len);
                ESP_LOG_BUFFER_HEX(GATTS_TABLE_TAG, param->write.value, param->write.len);
            }
            // 如果需要响应,则发送响应
            if (param->write.need_rsp){
                esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
            }
        }
        break;
        case ESP_GATTS_EXEC_WRITE_EVT:
            // 如果客户端执行了ESP_GATTS_WRITE_EVT分段写入,再触发ESP_GATTS_EXEC_WRITE_EVT则是提交所有数据
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_EXEC_WRITE_EVT");
            if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC){
                //此处可以执行数据提交操作
            }
            break;
        case ESP_GATTS_MTU_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_MTU_EVT, MTU %d", param->mtu.mtu);
            break;
        case ESP_GATTS_CONF_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_CONF_EVT, status = %d, attr_handle %d", param->conf.status, param->conf.handle);
            break;
        case ESP_GATTS_START_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "SERVICE_START_EVT, status %d, service_handle %d", param->start.status, param->start.service_handle);
            break;
        case ESP_GATTS_CONNECT_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_CONNECT_EVT, conn_id = %d", param->connect.conn_id);
            g_gatts_if = gatts_if;
            gatt_conn_id = param->connect.conn_id;
            is_connect = true;
            ESP_LOG_BUFFER_HEX(GATTS_TABLE_TAG, param->connect.remote_bda, 6);
            // 更新连接参数
            esp_ble_conn_update_params_t conn_params = {0};
            memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
            /* 对于 iOS 系统,请参考 Apple 官方文档关于 BLE 连接参数限制。 */
            conn_params.latency = 0;
            conn_params.max_int = 0x20;    // max_int = 0x20*1.25ms = 40ms
            conn_params.min_int = 0x10;    // min_int = 0x10*1.25ms = 20ms
            conn_params.timeout = 400;    // timeout = 400*10ms = 4000ms
            // 开始向对等设备发送更新连接参数请求
            esp_ble_gap_update_conn_params(&conn_params);
            break;
        case ESP_GATTS_DISCONNECT_EVT:
            is_connect = false;
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_DISCONNECT_EVT, reason = 0x%x", param->disconnect.reason);
            esp_ble_gap_start_advertising(&adv_params);
            break;
        case ESP_GATTS_CREAT_ATTR_TAB_EVT:{
            if (param->add_attr_tab.status != ESP_GATT_OK){
                ESP_LOGE(GATTS_TABLE_TAG, "create attribute table failed, error code=0x%x", param->add_attr_tab.status);
            }
            else if (param->add_attr_tab.num_handle != HRS_IDX_NB){
                ESP_LOGE(GATTS_TABLE_TAG, "create attribute table abnormally, num_handle (%d) \
                        doesn't equal to HRS_IDX_NB(%d)", param->add_attr_tab.num_handle, HRS_IDX_NB);
            }
            else {
                ESP_LOGI(GATTS_TABLE_TAG, "create attribute table successfully, the number handle = %d",param->add_attr_tab.num_handle);
                memcpy(handle_table, param->add_attr_tab.handles, sizeof(handle_table));
                esp_ble_gatts_start_service(handle_table[IDX_SVC]);
            }
            break;
        }
        case ESP_GATTS_STOP_EVT:
        case ESP_GATTS_OPEN_EVT:
        case ESP_GATTS_CANCEL_OPEN_EVT:
        case ESP_GATTS_CLOSE_EVT:
        case ESP_GATTS_LISTEN_EVT:
        case ESP_GATTS_CONGEST_EVT:
        case ESP_GATTS_UNREG_EVT:
        case ESP_GATTS_DELETE_EVT:
        default:
            break;
    }
}

// 定时器回调函数
void timer_callback(void *arg) {
    // 修改特征值
    char_value[0]++;
    char_value[1]++;
    char_value[2]++;
    char_value[3]++;

    if(is_connect){
        uint8_t notify_data[15];
        for (int i = 0; i < sizeof(notify_data); ++i)
        {
            notify_data[i] = i;
        }
        ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_CONNECT_EVT, g_gatts_if = %d conn_id = %d", g_gatts_if,gatt_conn_id);

        //用于发送指示(Indicate)或通知(Notify)。通知和指示的区别在于,指示需要客户端确认,而通知不需要,也就是最后一个参数true/false。
        esp_ble_gatts_send_indicate(g_gatts_if, gatt_conn_id, handle_table[IDX_CHAR_VAL_D],
            sizeof(notify_data), notify_data, false);
    }
}

// 定时器初始化函数
static void timer_init(void)
{
    const esp_timer_create_args_t periodic_timer_args = {
        .callback = &timer_callback,
        .name = "periodic_timer"
    };

    esp_timer_handle_t periodic_timer;
    ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
    ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, 1000000)); // 1秒
}

// 主应用程序入口
void app_main(void)
{
    esp_err_t ret;

    // 初始化 NVS。
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK( ret );

    // 释放经典蓝牙模式的控制器内存
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    // 初始化蓝牙控制器配置
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    // 启用蓝牙控制器
    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    // 初始化蓝牙协议栈
    ret = esp_bluedroid_init();
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    // 启用蓝牙协议栈
    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    // 注册 GATT 服务回调函数
    ret = esp_ble_gatts_register_callback(gatts_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gatts register error, error code = %x", ret);
        return;
    }

    // 注册 GAP 回调函数
    ret = esp_ble_gap_register_callback(gap_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gap register error, error code = %x", ret);
        return;
    }

    // 注册 GATT 应用程序
    //app_id = 0,ESP32中可以同时运行多个GATT应用程序,每个应用程序都需要一个唯一的 app_id,取值范围为 0x0000 到 0xFFFF。
    ret = esp_ble_gatts_app_register(0);
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gatts app register error, error code = %x", ret);
        return;
    }

    //设置本地设备的GATT最大传输单元(MTU)。MTU 是 GATT 通信中一次传输的最大数据包大小
    esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500);
    if (local_mtu_ret){
        ESP_LOGE(GATTS_TABLE_TAG, "set local  MTU failed, error code = %x", local_mtu_ret);
    }

    timer_init();
}

|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
| | |

重新烧录后,我们可以看到特征FF04中,多了一个↓箭头,底部有一个横线的,我们就可以点击它来开启通知监听,一旦设备信息有变化,我们就可以收到数据,另外,我们也可以通过设置2902的特征描述符,来开启或关闭通知功能。

相关推荐
jz_ddk1 小时前
[zynq] Zynq Linux 环境下 AXI BRAM 控制器驱动方法详解(代码示例)
linux·运维·c语言·网络·嵌入式硬件
深思慎考1 小时前
Linux网络——socket网络通信udp
linux·网络·udp
孤寂大仙v1 小时前
【计算机网络】NAT、代理服务器、内网穿透、内网打洞、局域网中交换机
网络·计算机网络·智能路由器
LuckyRich11 小时前
【websocket】安装与使用
网络·websocket·网络协议
KIDAKN1 小时前
理解网络协议
网络·网络协议
s_little_monster2 小时前
【Linux】网络--数据链路层--以太网
linux·运维·网络·经验分享·笔记·学习·计算机网络
most diligent9 小时前
蓝桥杯_DS18B20温度传感器---新手入门级别超级详细解析
单片机·嵌入式硬件
君鼎10 小时前
stm32——SPI协议
stm32·单片机·嵌入式硬件
明金同学11 小时前
电脑wifi显示已禁用怎么点都无法启用
运维·服务器·网络
秋水丶秋水11 小时前
GlobalSign、DigiCert、Sectigo三种SSL安全证书有什么区别?
运维·服务器·网络