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
[*] 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 蓝牙初始化一般分成三个部分:
- 定义一个蓝牙控制器的配置结构体bt_cfg让它等于默认配置,默认配置乐鑫已经帮我定义好了,基本上不用我们自己去修改。
- esp_bt_controller_init() 按照ble_cfg 创建任务.
- 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_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 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的特征描述符,来开启或关闭通知功能。