esp-iot-solution BTHome 组件指南:基于 BLE 广播实现传感器上报与 Home Assistant 集成
【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution
导读
本文以 esp-iot-solution 仓库中的 bt_home.rst 文档为主体,全面讲解 BTHome 组件(components/bluetooth/ble_adv/bthome)如何实现 BTHome V2 协议:支持传感器数据、二进制传感器数据和事件数据通过低功耗蓝牙(BLE)广播进行上报,支持加密与非加密两种模式,并能与 Home Assistant 等智能家居平台无缝集成。读完本文,你将掌握 BTHome 实例的创建与配置、加密密钥与对端 MAC 的设置、广播数据的构造与解析、回调存储机制的接入方式,以及如何借助仓库中的 bulb/dimmer 示例快速搭建一套可实际运行的 BTHome 设备。
BTHome 协议与组件定位
BTHome 是一种基于 BLE 广播的轻量级物联网数据格式协议。其核心思路是:设备将传感器读数、二进制状态或按钮事件编码进蓝牙广播数据包的服务数据(Service Data)字段中,接收方(如 Home Assistant、手机网关或另一块 ESP32 开发板)扫描到广播后即可直接解码,无需建立连接,因此功耗低、部署简单,非常适合电池供电的温湿度计、门窗传感器、遥控器等设备。
esp-iot-solution 的 BTHome 组件位于 components/bluetooth/ble_adv/bthome,对外仅暴露一个头文件 include/bthome_v2.h,核心实现集中在 bthome_v2.c。从组件清单 idf_component.yml 可以看到,它依赖 IDF>=5.0,支持 esp32、esp32c3、esp32c6、esp32h2、esp32h4、esp32s3、esp32c2 等多种芯片目标。根据 CHANGELOG.md,组件自 v0.1.0 起支持 BTHome V2 协议、加密与非加密模式、传感器/二进制传感器/事件上报(含按钮与调光器事件),并在 v0.1.1 中将加解密实现从 mbedTLS 迁移到 PSA Crypto API,以修复编译兼容性问题。
组件实现的功能可概括为三类:
- 广播数据的构造(发送侧):把传感器、二进制传感器、事件数据组装成符合 BTHome V2 格式的广播报文,可附加设备名称并选择是否加密;
- 广播数据的解析(接收侧):扫描到 BTHome 广播后,识别服务 UUID
0xFCD2,解密(如需)并逐条拆解出报告(report); - 加密密钥与会话计数器的持久化:通过用户自定义的回调函数把加密所需的会话计数器写入 NVS 等存储介质,保证重启后仍能续用。
BTHome 组件的初始化流程
使用组件的第一步是创建实例并完成基础配置。官方文档 bt_home.rst 给出如下初始化步骤:
- 使用
bthome_create创建 BTHome 实例; - 使用
bthome_register_callbacks注册存储回调函数; - 使用
bthome_set_encrypt_key设置加密密钥(可选); - 使用
bthome_set_peer_mac_addr设置对端 MAC 地址; - 使用
settings_store/settings_load配置存储; - 使用
bthome_parse_adv_data解析广播数据、bthome_free_reports释放报告数据。
从源码看,bthome_create(bthome_v2.c)会校验入参、初始化 PSA Crypto 子系统,并以calloc分配一个bthome_t结构体作为句柄返回。bthome_t内部持有 16 字节加密密钥、本机/对端 MAC 地址、32 位会话计数器、回调函数指针以及 PSA 密钥 ID 等字段,具体定义见 bthome_v2.c。与之配套的bthome_delete(bthome_v2.c)在销毁句柄前会调用psa_destroy_key清理已导入的密钥,避免资源泄漏。
基础初始化代码(来自文档示例,可直接编译运行):
#include "bthome_v2.h" // 创建 BTHome 实例 bthome_handle_t bthome_recv; ESP_ERROR_CHECK(bthome_create(&bthome_recv)); // 注册回调函数 bthome_callbacks_t callbacks = { .store = settings_store, .load = settings_load, }; ESP_ERROR_CHECK(bthome_register_callbacks(bthome_recv, &callbacks)); // 设置加密密钥(16 字节,与发送端保持一致) static const uint8_t encrypt_key[] = {0x23, 0x1d, 0x39, 0xc1, 0xd7, 0xcc, 0x1a, 0xb1, 0xae, 0xe2, 0x24, 0xcd, 0x09, 0x6d, 0xb9, 0x32}; ESP_ERROR_CHECK(bthome_set_encrypt_key(bthome_recv, encrypt_key)); // 设置对端 MAC 地址(用于解密时构造 nonce) static const uint8_t peer_mac[] = {0x54, 0x48, 0xE6, 0x8F, 0x80, 0xA5}; ESP_ERROR_CHECK(bthome_set_peer_mac_addr(bthome_recv, peer_mac));回调函数的作用与实现
bthome_callbacks_t结构体(include/bthome_v2.h)包含store与load两个函数指针,签名分别如下:
typedef void (*bthome_store_func_t)(bthome_handle_t handle, const char *key, const uint8_t *data, uint8_t len); typedef void (*bthome_load_func_t)(bthome_handle_t handle, const char *key, uint8_t *data, uint8_t len);组件的调用约定是:每当使用加密模式构造出一条广播后,bthome_make_adv_data会自增内部会话计数器,并调用callbacks.store(handle, "counter", &counter, sizeof(counter))将新值保存;启动时可调用bthome_load_params(bthome_v2.c)通过callbacks.load把计数器读回内存。因此这两个回调的典型实现就是把数据写入/读出 NVS。
仓库中的 bulb 示例 examples/bluetooth/ble_adv/bthome/bulb/main/app_main.c 给出了完整可用的 NVS 实现,其中settings_store使用nvs_set_blob写入并nvs_commit提交,settings_load使用nvs_get_blob读取,读取失败时清零填充。测试应用中则使用更简单的 mock 实现(test_apps/main/bthome_test.c)。
需要注意的是,bthome_register_callbacks要求store与load都非空,否则返回ESP_ERR_INVALID_ARG;bthome_create、bthome_set_encrypt_key、bthome_set_peer_mac_addr、bthome_set_local_mac_addr对空指针入参同样有严格校验,这些边界行为都被测试用例bthome_error_handling覆盖(test_apps/main/bthome_test.c)。
构造 BTHome 广播报文(发送侧)
发送侧的核心能力由三组 API 提供,它们负责把结构化数据写入负载缓冲区,并返回新的写入偏移量(即"已占用字节数"),调用方只需把每次的返回值作为下一次的偏移继续追加即可。
1. 传感器数据
uint8_t bthome_payload_add_sensor_data(uint8_t *buffer, uint8_t offset, bthome_sensor_id_t obj_id, uint8_t *data, uint8_t data_len);该函数将obj_id(1 字节对象 ID)与data_len字节的数据依次写入缓冲区,返回offset + data_len + 1。传感器 ID 全部定义在bthome_sensor_id_t枚举中(include/bthome_v2.h),常用取值包括:BTHOME_SENSOR_ID_BATTERY(电池电量,1 字节)、BTHOME_SENSOR_ID_TEMPERATURE_PRECISE(高精度温度,2 字节)、BTHOME_SENSOR_ID_HUMIDITY_PRECISE(高精度湿度,2 字节)、BTHOME_SENSOR_ID_PRESSURE(气压,3 字节)、BTHOME_SENSOR_ID_ILLUMINANCE(光照度,3 字节)、BTHOME_SENSOR_ID_ENERGY(能量,3 字节)、BTHOME_SENSOR_ID_POWER(功率,3 字节)、BTHOME_SENSOR_ID_VOLTAGE(电压,2 字节)、BTHOME_SENSOR_ID_PM25/BTHOME_SENSOR_ID_PM10(颗粒物浓度)、BTHOME_SENSOR_ID_CO2、BTHOME_SENSOR_ID_TVOC、BTHOME_SENSOR_ID_CURRENT(电流)、BTHOME_SENSOR_ID_UV(紫外线)等。
每个传感器对象的数据长度必须与协议一致,组件内部维护了一张对象 ID 到数据长度的映射表object_length(bthome_v2.c),解析侧正是依据它来切分数据段的。示例中常见的编码方式是定点缩放:温度 23.5°C 编码为(uint16_t)(23.5 * 100) = 2350,湿度 65.2% 编码为6520,详见测试用例bthome_payload_creation(test_apps/main/bthome_test.c)。
2. 二进制传感器数据
uint8_t bthome_payload_adv_add_bin_sensor_data(uint8_t *buffer, uint8_t offset, bthome_bin_sensor_id_t obj_id, uint8_t data);二进制传感器(Binary Sensor)只有 1 字节数据,固定占用offset + 2。ID 枚举bthome_bin_sensor_id_t(include/bthome_v2.h)覆盖了BTHOME_BIN_SENSOR_ID_MOTION(人体运动)、BTHOME_BIN_SENSOR_ID_DOOR/BTHOME_BIN_SENSOR_ID_WINDOW(门窗开合)、BTHOME_BIN_SENSOR_ID_OPENING、BTHOME_BIN_SENSOR_ID_POWER(电源状态)、BTHOME_BIN_SENSOR_ID_LIGHT(光照)、BTHOME_BIN_SENSOR_ID_BATTERY_CHARGING、BTHOME_BIN_SENSOR_ID_MOISTURE(漏水)、BTHOME_BIN_SENSOR_ID_SMOKE、BTHOME_BIN_SENSOR_ID_OCCUPANCY、BTHOME_BIN_SENSOR_ID_PRESENCE、BTHOME_BIN_SENSOR_ID_TAMPER等常见家庭安防语义。
3. 事件数据
uint8_t bthome_payload_adv_add_evt_data(uint8_t *buffer, uint8_t offset, bthome_event_id_t obj_id, uint8_t *evt, uint8_t evt_size);事件类型只有两种:BTHOME_EVENT_ID_BUTTON(按钮事件,1 字节事件值,如 1 表示单击)和BTHOME_EVENT_ID_DIMMER(调光器事件,2 字节,第一字节为方向/类型、第二字节为亮度增量)。解析侧bthome_parse_payload会依据 ID 区分:按钮事件按 2 字节(ID + 1 字节值)解析,调光器事件按 3 字节(ID + 2 字节值)解析(bthome_v2.c)。
4. 组装完整广播报文
负载拼装完成后,调用bthome_make_adv_data生成可直接交给 BLE 广播的完整报文:
uint8_t bthome_make_adv_data(bthome_handle_t handle, uint8_t *buffer, uint8_t *name, uint8_t name_len, bthome_device_info_t info, uint8_t *payload, uint8_t payload_len);bthome_device_info_t是一个位域联合体(include/bthome_v2.h),各 bit 含义为:bit0 加密标志(encryption_flag)、bit2 触发型设备标志(trigger_based_flag)、bit5-7 BTHome 协议版本(bthome_version,V2 填 2)。构造报文时组件会自动追加:Flags 广播段(0x02 0x01 0x06)、可选的 Complete Local Name 段(类型0x09)、Service Data 段(UUID0xFCD2,小端序写入)+ 设备信息字节 + 负载。
测试用例bthome_adv_data_creation(test_apps/main/bthome_test.c)展示了完整的发送侧调用链:先bthome_create,再设置加密密钥与本机 MAC(加密必须,用于构造 nonce),注册回调,追加温湿度与运动传感器数据后生成设备信息:
bthome_device_info_t device_info = { .bit = { .encryption_flag = 1, // 1 表示加密 .trigger_based_flag = 0, // 0 表示周期性上报设备 .bthome_version = 2 // BTHome V2 } }; uint8_t adv_len = bthome_make_adv_data(handle, adv_data, (uint8_t *)device_name, name_len, device_info, payload, payload_len);并验证了报文前三个字节确实为广播标志段0x02, 0x01, 0x06。如果设置了加密标志但尚未导入密钥,bthome_make_adv_data会返回 0(构造失败),对应测试用例bthome_encrypted_adv_without_key(test_apps/main/bthome_test.c)。
解析 BTHome 广播报文(接收侧)
接收侧入口是bthome_parse_adv_data:
bthome_reports_t *bthome_parse_adv_data(bthome_handle_t handle, uint8_t *adv, uint8_t len);它逐条遍历广播 AD 结构:取长度字段adv[index],若为 0 则结束;再取类型字段,当类型为0x16(Service Data)且解析出的 16 位 UUID 等于0xFCD2时,进入服务数据解析(bthome_v2.c)。
bthome_parse_service_data会读取设备信息字节,检查加密标志:若未加密,直接对负载调用bthome_parse_payload;若加密,则先解密再解析(bthome_v2.c)。解析结果以bthome_reports_t返回:
typedef struct { uint8_t id; /* 对象 ID:传感器 / 二进制传感器 / 事件 ID 之一 */ uint8_t len; /* 数据长度 */ uint8_t *data; /* 数据指针 */ } bthome_report_t; typedef struct { uint8_t num_reports; /* 报告数量 */ bthome_report_t report[BTHOME_REPORTS_MAX]; /* 报告数组,最多 10 条 */ } bthome_reports_t;bthome_parse_payload(bthome_v2.c)按对象 ID 分流:二进制传感器与按钮事件按 2 字节解析、调光器事件按 3 字节解析、Raw/Text 类型先从下一字节读取长度再拷贝数据、普通传感器则查object_length表确定长度后memcpy。每一条报告的数据都是动态分配的,因此解析完成后必须调用bthome_free_reports释放,防止内存泄漏——该函数会先释放每条 report 的 data 指针再释放 reports 结构体本身(bthome_v2.c)。同时bthome_reports_t有 10 条上限(BTHOME_REPORTS_MAX),超出会报 "bthome_reports_t overflow" 并返回 NULL。
官方文档给出的解析侧代码骨架如下:
// 解析广播数据 bthome_reports_t *reports = bthome_parse_adv_data(bthome_recv, result.ble_adv, result.adv_data_len); if (reports != NULL) { // 处理报告数据 for (int i = 0; i < reports->num_reports; i++) { // 处理每个报告,例如: // reports->report[i].id 判断类型 // reports->report[i].data 读取数据 } bthome_free_reports(reports); }注意:bthome_parse_adv_data本身不分配持久内存,只在解析成功时通过calloc分配 reports,失败路径(未知对象 ID、报告数溢出、解密失败)均返回 NULL,测试用例bthome_adv_data_parsing(test_apps/main/bthome_test.c)验证了非加密报文的完整解析链路。
加密与解密原理
BTHome V2 加密模式使用AES-128-CCM算法,具体通过 PSA Crypto API 的psa_aead_encrypt/psa_aead_decrypt实现,认证标签缩短为 4 字节(PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_TAG_LEN))。
加密 nonce(13 字节)的构造顺序为(bthome_v2.c):
- 本机 BLE MAC 地址(6 字节);
- BTHome 服务 UUID
0xFCD2(2 字节,小端); - 设备信息字节(1 字节);
- 4 字节会话计数器(
bthome->counter)。
加密输出的密文与 4 字节 tag 一并写入广播的服务数据字段,随后计数器自增并通过store回调持久化,确保同一 nonce 不会被重复使用。解密时对称地使用对端 MAC、UUID、设备信息字节与报文末尾的计数器重建 nonce(bthome_v2.c)——这正是文档要求设置peer_mac_addr(解密侧)与local_mac_addr(加密侧)的原因。若未导入密钥就调用加解密,函数会打印 "encryption key not set" 并返回ESP_ERR_INVALID_STATE。
密钥通过bthome_set_encrypt_key导入:内部调用psa_crypto_init后,以PSA_KEY_TYPE_AES、128 位、允许加密/解密、算法为缩短 tag 的 CCM 的密钥属性执行psa_import_key(bthome_v2.c)。重复设置新密钥时,旧密钥会先被psa_destroy_key销毁。必须强调:加密密钥(16 字节)是发送端与接收端/Home Assistant 之间的共享秘密,务必保持一致,且应与对端 MAC 一样通过白名单与密钥绑定来防止伪造设备混入。
在真实工程中的集成(bulb 示例剖析)
仓库提供了两个可直接运行的端到端示例:发送侧的 dimmer 示例(旋钮调光器,主动广播事件)与接收侧的 bulb 示例(灯泡,被动扫描并按事件控制 WS2812 LED 灯带)。两者使用同一把测试密钥0x23, 0x1d, 0x39, ...与对端 MAC0x54, 0x48, 0xE6, 0x8F, 0x80, 0xA5。
bulb 示例的整体数据流如下(app_main.c):
- 初始化 NVS:
nvs_flash_init,失败时擦除重试; - 配置 BLE 扫描:调用
ble_hci_init/ble_hci_reset/ble_hci_enable_meta_event,设置被动扫描参数(scan_interval、scan_window均为0x50),注册扫描回调ble_hci_scan_cb,并把对端 MAC 加入接受列表(accept list)实现地址过滤,最后ble_hci_set_scan_enable(true, true)开启扫描; - 初始化 BTHome 接收实例:
bthome_create→bthome_register_callbacks(settings_store, settings_load)→bthome_set_encrypt_key→bthome_set_peer_mac_addr; - 主循环消费扫描结果:从队列取出扫描结果,
bthome_parse_adv_data解析;若 report 的 id 为BTHOME_EVENT_ID_BUTTON且 data[0] == 0x01,翻转 LED 开关状态;若为BTHOME_EVENT_ID_DIMMER,按 data[0] 的方向(0x01 增亮 / 0x02 减暗)与 data[1] 的幅度更新亮度;随后led_strip_set_pixel+led_strip_refresh刷新灯带,最后bthome_free_reports释放内存。
这个例子完整示范了本文前面所有 API 的组合用法,也是"解析 BTHome 事件并驱动外设"的最佳参考模板。
编译与烧录
bulb 示例的编译流程(examples/bluetooth/ble_adv/bthome/bulb/README.md):
cd examples/bluetooth/ble_adv/bthome/bulb # 设置目标芯片(默认 ESP32-H2,亦支持 ESP32-H4) idf.py set-target esp32h2 # 编译并烧录,PORT 替换为实际串口 idf.py -p PORT build flash支持的目标芯片为 ESP32-H2(默认,与 dimmer 示例配对)与 ESP32-H4(LED 灯带 GPIO 默认 37)。硬件上需要一块支持 BLE 的 ESP32 开发板与一根 WS2812 LED 灯带(默认数据引脚 GPIO 8,可通过 menuconfig 中BTHome Bulb Configuration调整引脚、LED 数量与 RMT 分辨率)。验证方式:在一台板子上烧录 dimmer 示例,另一台烧录 bulb 示例,按下 dimmer 按钮即可看到 LED 开关切换,旋转旋钮即可调节亮度。
测试与验证
组件自带一套基于 Unity 框架的完整单元测试,位于 components/bluetooth/ble_adv/bthome/test_apps,构建与运行方式见 test_apps/README.md:
cd components/bluetooth/ble_adv/bthome/test_apps idf.py build idf.py monitor # 进入 Unity 测试菜单运行用例测试用例覆盖:
| 用例名 | 验证内容 |
|---|---|
bthome_create_delete | 实例创建与销毁 |
bthome_encryption_config | 加密密钥、本机/对端 MAC、回调注册 |
bthome_payload_creation | 温湿度、气压、光照、能量、功率、电压、PM2.5/PM10、CO2、TVOC、电池等传感器数据编码 |
bthome_binary_sensor_data | 运动、门磁、电源、光照等二进制传感器编码 |
bthome_event_data | 按钮与调光器事件编码 |
bthome_adv_data_creation | 完整广播报文构造与结构校验 |
bthome_adv_data_parsing | 广播报文解析与报告数量边界 |
bthome_memory_management | 10 次创建/销毁循环 + 内存泄漏检测(8bit/32bit 堆) |
bthome_error_handling | NULL 入参、空回调、未设密钥等异常路径 |
根据 test_apps/README.md 的说明,该测试应用已接入 CI 流水线,在 ESP32、ESP32-S3、ESP32-C3 及 IDF 4.4、5.0、5.1、5.2 等多版本组合上自动运行。
常见问题与注意事项
- 内存管理:
bthome_parse_adv_data返回的 reports 及其内部 data 指针均为堆内存,必须配对调用bthome_free_reports,否则会产生内存泄漏;组件 v0.1.0 曾修复过内存泄漏问题(见 CHANGELOG.md)。 - 报告上限:单个广播最多承载
BTHOME_REPORTS_MAX(10)条报告,负载较大时需拆分或精简传感器数量。 - 密钥一致性:加密模式要求发送端
bthome_set_encrypt_key+bthome_set_local_mac_addr,接收端(或 Home Assistant)使用同一把密钥 +bthome_set_peer_mac_addr,任何一端缺失或不同都会导致解析失败(解密打印psa_aead_decrypt failed)。 - nonce 唯一性:会话计数器必须通过 store/load 回调持久化,否则重启后计数器回绕可能造成 nonce 复用,削弱加密安全性。
- IDF 版本:组件依赖 PSA Crypto API,需要 IDF
>=5.0(见 idf_component.yml)。 - 回调非空:
bthome_register_callbacks强制要求 store 与 load 都非空,纯发送侧也需要提供存储实现。
总结
esp-iot-solution 的 BTHome 组件以"广播即数据"的方式,用极少的工程成本把 ESP32 变成 BTHome V2 协议的发送端或接收端:发送侧用bthome_payload_add_sensor_data/bthome_payload_adv_add_bin_sensor_data/bthome_payload_adv_add_evt_data拼装负载,用bthome_make_adv_data生成报文;接收侧用bthome_parse_adv_data拆解报告并用bthome_free_reports释放内存;加密模式依赖 AES-128-CCM 与持久化的会话计数器保证数据机密性与完整性。配合 Home Assistant 的 BTHome 集成,开发者可以快速构建温湿度计、门磁、人体传感器、智能开关、调光灯泡等低功耗智能家居设备,bulb/dimmer 示例则提供了从 BLE 扫描到外设控制的完整参考实现。
【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考