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

article/2025/6/8 5:30:21

飞书文档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

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

(Top) → Component config → Bluetooth
Espressif IoT Development Framework Configuration
[*] BluetoothHost (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 moduleif ((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   61958esp_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]; // 唯一标识符UUIDuint16_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 value0xfb, 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 value0xfb, 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_nameinclude_txpower
  • 移除 manufacturer_lenp_manufacturer_data
  • 移除 service_uuid_lenp_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 value0xfb, 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,          // 服务idHRS_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是0x00FFesp_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 = 40msconn_params.min_int = 0x10;    // min_int = 0x10*1.25ms = 20msconn_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事件中设置为trueif(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,           // 连接IDattr_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 = 40msconn_params.min_int = 0x10;    // min_int = 0x10*1.25ms = 20msconn_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的特征描述符,来开启或关闭通知功能。


http://www.hkcw.cn/article/StokhSAvNq.shtml

相关文章

动态规划-1143.最长公共子序列-力扣(LeetCode)

一、题目解析 对于给定了两个字符串中&#xff0c;需要找到最长的公共子序列&#xff0c;也就是两个字符串所共同拥有的子序列。 二、算法原理 1、状态表示 dp[i][j]&#xff1a;表示s1的[0,i]和s2的[0,j]区间内所有子序列&#xff0c;最长子序列的长度 2、状态转移方程 根…

EMQX 社区版单机和集群部署

EMQ 支持 Docker&#xff0c;宿主机&#xff0c;k8s部署&#xff1b;支持单机或集群部署。以下给出EMQX社区版单机和集群部署方法 1. Docker单机部署 官方推荐最小配置&#xff1a;2核 4G 下载容器镜像 docker pull emqx/emqx:5.3.2启动容器 docker run -d --name emqx \-…

小牛电动2025新品矩阵,引领技术普惠新风潮

自2014年成立以来&#xff0c;全球高端智能电动车领导品牌小牛电动已走过十个年头&#xff0c;在全球智能城市出行领域留下了深刻印记。秉持“科技、潮流、自由”的品牌理念&#xff0c;小牛电动致力于改变出行&#xff0c;让城市生活更美好。十年来&#xff0c;小牛电动推出多…

SU-03T1烧录使用教程

一、简介 SU-03T1模块是一款由深圳机芯智能开发的低成本、低功耗、小体积的离线语音识别模组&#xff0c;适用于智能家居、各类智能小家电、86盒、玩具、灯具等需要语音操控的场景。它是SU-03T的一个版本或后续产品&#xff0c;可能在功能或性能上有所改进或特定的应用优化。 该…

SOC-ESP32S3部分:27-设备OTA

飞书文档https://x509p6c8to.feishu.cn/wiki/Hd9TwkuZ3iEQiUkjaoic5p7Knuh ESO32S3应用程序可以在运行时通过网络从服务器下载新的固件&#xff0c;然后将其存储到某个分区中&#xff0c;从而实现固件的升级功能。 在ESP-IDF中有两种方式可以进行空中(OTA)升级: 使用 app_up…

Windows清理之后,资源管理器卡顿-解决方法

一、点击本地磁盘选择属性 二、选择工具 三、选择驱动器进行优化

VBA模拟进度条

在上一章中我跟大家介绍了ProgressBar控件的使用方法&#xff0c;但由于该控件无法在64位版本的Office中运行&#xff0c;为此我们可以采用Lable控件来模拟进度条的变化&#xff0c;以解决在64位版本的Office中无进度条控件的问题。 一、设计思路 添加两个重叠的Lable标签控件…

Linux(线程概念)

目录 一 虚拟地址到物理地址的转换 1. 操作系统如何管理物理内存&#xff1a; 2. 下面来谈谈虚拟地址如何转换到物理地址&#xff1a; 3. 补充字段&#xff1a; 二 Linux中的线程 1. 先来说说进程&#xff1a; 2. 线程&#xff1a; 3. 线程相比较于进程的优缺点&#x…

手把手教你用Appsmith打造企业级低代码平台:从部署到性能调优实战

文章目录 前言1.什么是Appsmith2.Docker部署3.Appsmith简单使用4.安装cpolar内网穿透5. 配置公网地址6. 配置固定公网地址总结 前言 在当今快速变化的商业环境中&#xff0c;企业正面临内部系统建设的双重挑战。传统开发模式不仅需要漫长的开发周期&#xff08;通常需要数月&a…

PyTorch 入门学习笔记(数字识别实战)

目录 一、关于 PyTorch 的一个重要概念——神经网络 二、PyTorch 是如何解决问题的&#xff08;解决案例&#xff09; 1 案例&#xff1a;手写一个数字&#xff0c;让计算机识别出是哪个数字。 2 PyThorch 解决问题大约需要以下几个步骤&#xff1a; 3 代码示例&#xff1…

OSCP备战-BSides-Vancouver-2018-Workshop靶机详细步骤

一、靶机介绍 靶机地址&#xff1a;https://www.vulnhub.com/entry/bsides-vancouver-2018-workshop%2C231/ 靶机难度&#xff1a;中级&#xff08;CTF&#xff09; 靶机发布日期&#xff1a;2018年3月21日 靶机描述&#xff1a; Boot2root挑战旨在创建一个安全的环境&…

CANopen转Profinet 全攻略:打通施耐德变频器与西门子 300PLC通讯链路

Profinet转CAN open西门子300PLC与施耐德变频器通讯 项目 福建某公司在国外的一个工业自动化项目中&#xff0c;控制中心系统通过监控变频器的不同状态发送不同的命令启动/停止变频器&#xff0c;设定变频器的运行速度进而控制变频器所连接的伺服电机。监控中心系统使用的是西…

Shell脚本编程

shell概述 什么是shell&#xff1f; 在Linux内核与用户之间的解释器程序 Linux默认解释器为/bin/bash负责向内核翻译及传达用户/程序指令相当于操作系统的“外壳” shell的使用方式 交互式-命令行 人工干预&#xff0c;智能化程度高逐条解释执行&#xff0c;效率低、 非交…

win11中使用grep

一、下载 https://nchc.dl.sourceforge.net/project/gnuwin32/grep/2.5.4/grep-2.5.4-setup.exe?viasf1 二、控制面板的环境变量 Path中增加 E:\software\GnuWin32\bin 三、测试使用

负载均衡相关基本概念

负载均衡在系统架构设计中至关重要&#xff0c;其核心目标是合理分配负载&#xff0c;提升系统整体性能和可靠性。本文简要介绍了负载均衡的基本概念&#xff0c;包括四层和七层负载均衡、负载均衡的使用场景和实现方式、负载均衡的常用算法以及一些配置相关知识。 1、负载均衡…

Houdini POP入门学习03

跟着教程学习降雪效果制作&#xff0c;这部分包含blast裁剪、外部引脚获取等。 阶段1 1.Geometry中创建grid&#xff0c;连接popnet。 2.双击进入popnet&#xff0c;在wire_pops_into_here前添加popforce&#xff0c;这一步并不是为了添加重力&#xff0c;而是增加一些乱流。 …

ULVAC DC-10-4P 400V input 10kW DC Pulse power supply 爱发科直流电源

ULVAC DC-10-4P 400V input 10kW DC Pulse power supply 爱发科直流电源

星野录(博客系统)测试报告

目录 一. 项目背景 二、项目功能 三、测试计划 1. 功能测试 1.1 测试用例 1.2 执行测试部分操作截图 2. 使用selenium进行自动化测试 2.1 添加相关依赖 2.2 登录页面测试 3.3 注册页面测试 3.4 博客列表页面测试 3.5 博客详情页测试 3.6 博客编辑页面测试 3.7 个人…

WPF技术体系与现代化样式

目录 ​​1 WPF技术架构解析​​ ​​1.1 技术演进与定位​​ ​​1.2 核心机制对比​​ ​​2 样式与资源系统​​ ​​2.1 资源(Resource)定义与作用域​​ ​​2.2 样式(Style)与触发器​​ ​​3 开发环境配置(.NET 8)​​ ​​3.1 安装流程​​ ​​3.2 项目结…

智能快递地址解析接口如何用PHP调用?

一、什么是智能快递地址解析接口 随着互联网技术的普及和电子商务的迅猛发展&#xff0c;网购已成为现代人日常生活的重要组成部分。然而&#xff0c;在这个便捷的背后&#xff0c;一个看似不起眼却影响深远的问题正悄然浮现——用户填写的快递地址格式混乱、信息不全甚至错漏…