BTHome 组件单元测试实战指南:用 Unity 框架验证 BTHome V2 协议编解码与加密链路
2026/9/18 22:46:33 网站建设 项目流程

BTHome 组件单元测试实战指南:用 Unity 框架验证 BTHome V2 协议编解码与加密链路

【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution

BTHome 是 ESP32 BLE 广播生态中广泛使用的传感器数据上报协议(V2 版本),esp-iot-solution 仓库在 components/bluetooth/ble_adv/bthome 提供了完整的组件实现,并配套了一套基于 ESP-IDF Unity 框架的单元测试工程。本文以 test_apps/README.md 为主线,逐条拆解 9 个测试用例的验证目标,并结合 bthome_v2.c 与 bthome_v2.h 的源码实现,说明如何构建、运行并理解这套测试,读者可据此掌握 BTHome 协议数据的构造、加密、广播打包与解析的完整验证方法。

测试工程结构与文件职责

测试应用位于components/bluetooth/ble_adv/bthome/test_apps/,其目录结构如下:

  • main/bthome_test.c—— 测试主体文件,包含 BTHome 功能的全部单元测试用例;
  • main/CMakeLists.txt—— 注册测试源文件与依赖(bthome组件与unity测试框架);
  • sdkconfig.defaults—— 测试应用的默认配置,重点开启 BLE 控制器并关闭 Bluedroid;
  • CMakeLists.txt—— 工程级 CMake 配置,通过EXTRA_COMPONENT_DIRS引入 IDF 自带的unit-test-app组件;
  • README.md—— 测试结构、用例与 CI 集成说明。

按 README 的描述,该目录还包含pytest_bthome.py(Pytest 自动化测试脚本),不过当前仓库快照中并未包含该文件,实际存在的核心测试逻辑集中在main/bthome_test.c中。从main/CMakeLists.txt可以看到测试的依赖关系非常简洁:

idf_component_register(SRCS "bthome_test.c" INCLUDE_DIRS "." REQUIRES bthome unity)

即测试直接链接bthome组件,并基于 IDF 官方unity测试框架编写。

九个测试用例逐一解析

README 列出了 9 个测试用例,全部定义在 bthome_test.c 中,每个用例都通过TEST_CASE("名称", "[bthome]")注册,并遵循"setup 记录内存基线 → 执行逻辑 → teardown 校验内存泄漏"的统一模式。整体概览如下表:

用例名称对应函数核心验证点
bthome_create_deletebthome_create/bthome_delete句柄创建成功且非空、删除成功
bthome_encryption_configbthome_set_encrypt_key加密密钥、本端/对端 MAC、回调注册
bthome_payload_creationbthome_payload_add_sensor_data13 类传感器数据的载荷构造
bthome_binary_sensor_databthome_payload_adv_add_bin_sensor_data二进制传感器数据构造
bthome_event_databthome_payload_adv_add_evt_data事件数据(按键、调光)构造
bthome_adv_data_creationbthome_make_adv_data完整广播数据打包及 flags 结构校验
bthome_adv_data_parsingbthome_parse_adv_data广播数据解析为 reports 结构
bthome_memory_managementbthome_create/bthome_delete10 轮创建/删除循环后的内存泄漏检测
bthome_error_handling全部公开 APINULL 句柄、NULL 参数等边界错误处理

1. 对象生命周期:创建与删除

TEST_CASE("bthome_create_delete", "[bthome]") { setup_test(); bthome_handle_t handle = NULL; esp_err_t ret = bthome_create(&handle); TEST_ASSERT_EQUAL(ESP_OK, ret); TEST_ASSERT_NOT_NULL(handle); ret = bthome_delete(handle); TEST_ASSERT_EQUAL(ESP_OK, ret); teardown_test(); }

在源码 bthome_v2.c 中,bthome_create通过calloc分配bthome_t结构体并初始化key_id = 0key_imported = falsebthome_delete则在句柄有效时先销毁已导入的 PSA 密钥再释放内存。测试用例通过断言返回值ESP_OK与句柄非空,验证了对象管理的基本契约。

2. 加密与地址配置

bthome_encryption_config用例依次验证了四个配置接口:

ret = bthome_set_encrypt_key(handle, test_key); // 设置 16 字节 AES-128 密钥 ret = bthome_set_local_mac_addr(handle, test_local_mac); // 本端 MAC(加密时写入 nonce) ret = bthome_set_peer_mac_addr(handle, test_peer_mac); // 对端 MAC(解密时写入 nonce) bthome_callbacks_t callbacks = { .store = mock_store_func, .load = mock_load_func }; ret = bthome_register_callbacks(handle, &callbacks); // 注册持久化回调

测试数据中test_key为 16 字节密钥,test_local_mactest_peer_mac各 6 字节。从源码看,bthome_set_encrypt_key会调用psa_crypto_init(),以PSA_KEY_TYPE_AES+ 128 bit 的方式导入密钥,并绑定PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, 4)算法;重复设置密钥时会先销毁旧密钥再导入。回调结构bthome_callbacks_t包含storeload两个函数指针,用于加密计数器(counter)的持久化。

3. 传感器数据载荷构造

bthome_payload_creation用例通过bthome_payload_add_sensor_data(buffer, offset, obj_id, data, data_len)构造了 13 种传感器数据,是覆盖面最广的用例。测试数据与 BTHome 编码规则的对应关系如下:

传感器对象 ID测试原始值编码方式
温度(精确)BTHOME_SENSOR_ID_TEMPERATURE_PRECISE(0x02)23.5℃×100转 uint16
湿度(精确)BTHOME_SENSOR_ID_HUMIDITY_PRECISE(0x03)65.2%×100转 uint16
气压BTHOME_SENSOR_ID_PRESSURE(0x04)1013.25 hPa×100转 uint16
光照度BTHOME_SENSOR_ID_ILLUMINANCE(0x05)500 lxuint32
能量BTHOME_SENSOR_ID_ENERGY(0x0A)12345 Whuint32
功率BTHOME_SENSOR_ID_POWER(0x0B)15.5 W×100转 uint16
电压BTHOME_SENSOR_ID_VOLTAGE(0x0C)3.3 V×100转 uint16
PM2.5BTHOME_SENSOR_ID_PM25(0x0D)25 µg/m³uint16
PM10BTHOME_SENSOR_ID_PM10(0x0E)35 µg/m³uint16
CO2BTHOME_SENSOR_ID_CO2(0x12)400 ppmuint16
TVOCBTHOME_SENSOR_ID_TVOC(0x13)50 µg/m³uint16
电量BTHOME_SENSOR_ID_BATTERY(0x01)85%uint8

用例对每次调用断言TEST_ASSERT_GREATER_THAN(0, offset),即确认 payload 长度持续增长、数据被正确写入。值得注意的是,完整的对象 ID 枚举(含加速度、陀螺仪、气体、体积、水流、时间戳等)定义在 bthome_v2.h 中,共 40 余项,测试用例只是抽取了最常见的子集。

4. 二进制传感器数据

bthome_binary_sensor_data用例验证bthome_payload_adv_add_bin_sensor_data,二进制传感器数据为"1 字节对象 ID + 1 字节状态值":

offset = bthome_payload_adv_add_bin_sensor_data(buffer, offset, BTHOME_BIN_SENSOR_ID_MOTION, 1); // 移动 offset = bthome_payload_adv_add_bin_sensor_data(buffer, offset, BTHOME_BIN_SENSOR_ID_DOOR, 0); // 门 offset = bthome_payload_adv_add_bin_sensor_data(buffer, offset, BTHOME_BIN_SENSOR_ID_POWER, 1); // 电源 offset = bthome_payload_adv_add_bin_sensor_data(buffer, offset, BTHOME_BIN_SENSOR_ID_LIGHT, 0); // 灯光

二进制传感器 ID 在头文件中从BTHOME_BIN_SENSOR_ID_GENERIC(0x0F) 一直覆盖到BTHOME_BIN_SENSOR_ID_WINDOW(0x2D),包括门窗、人体感应、烟雾、燃气、震动、漏水等常用状态量。从实现看,该函数内部固定写入对象 ID 加 1 字节数据,返回offset + 2

5. 事件数据

bthome_event_data用例验证 BTHome V2 的事件上报能力,目前组件支持两类事件(见 bthome_v2.h 的bthome_event_id_t):

uint8_t button_event = 1; // 按键按下 offset = bthome_payload_adv_add_evt_data(buffer, offset, BTHOME_EVENT_ID_BUTTON, &button_event, sizeof(button_event)); uint8_t dimmer_event = 50; // 调光值 offset = bthome_payload_adv_add_evt_data(buffer, offset, BTHOME_EVENT_ID_DIMMER, &dimmer_event, sizeof(dimmer_event));

从 bthome_v2.c 的解析逻辑可以印证:BTHOME_EVENT_ID_BUTTON(0x3A) 被解析为 1 字节数据,而BTHOME_EVENT_ID_DIMMER(0x3C) 被解析为 2 字节数据(因此载荷中消耗 3 字节),事件编码在解析端与构造端保持严格对称。

6. 广播数据打包与结构校验

bthome_adv_data_creation是端到端综合用例:先创建句柄、配置密钥与本端 MAC、注册回调,再构造温度+湿度+移动传感器的组合 payload,最后调用bthome_make_adv_data生成完整广播数据,并断言广播开头是标准的 flags 三段:

bthome_device_info_t device_info = { .bit = { .encryption_flag = 1, // bit 0:开启加密 .trigger_based_flag = 0, // bit 2:非触发式 .bthome_version = 2 // bit 5-7:协议版本 V2 } }; uint8_t adv_len = bthome_make_adv_data(handle, adv_data, (uint8_t *)device_name, name_len, device_info, payload, payload_len); // 验证广播数据结构 TEST_ASSERT_EQUAL(0x02, adv_data[0]); // flags 长度 TEST_ASSERT_EQUAL(0x01, adv_data[1]); // flags 类型 TEST_ASSERT_EQUAL(0x06, adv_data[2]); // flags 值:LE 通用可发现 + BR/EDR 不支持

结合 bthome_v2.c 的实现,bthome_make_adv_data打包的完整结构为:0x02 0x01 0x06(flags)→ 可选设备名 AD(类型 0x09)→ Service Data 段(长度、类型 0x16、UUID 0xFCD2、设备信息字节),其中加密模式下长度字段为payload_len + 12(含 UUID 2 字节 + 设备信息 1 字节 + 计数器 4 字节 + 标签 4 字节 + 长度字节自身),并追加 4 字节计数器与 4 字节认证标签。bthome_device_info_t是一个位域联合体,8 个 bit 分别表示加密标志、预留位、触发式标志、预留位与协议版本,最终以单字节写入广播。

7. 广播数据解析

bthome_adv_data_parsing用例验证从原始广播字节流反解出结构化报告的能力:

uint8_t test_adv_data[] = { 0x02, 0x01, 0x06, 0x04, 0x09, 0x44, 0x49, 0x59, 0x11, 0x16, 0xd2, 0xfc, 0x41, 0xe6, 0x8b, 0x80, 0x0d, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x7a, 0xcd, 0xcd, 0xfb }; bthome_reports_t *reports = bthome_parse_adv_data(handle, test_adv_data, sizeof(test_adv_data)); TEST_ASSERT_GREATER_THAN(0, reports->num_reports); TEST_ASSERT_LESS_OR_EQUAL(BTHOME_REPORTS_MAX, reports->num_reports); bthome_free_reports(reports);

解析结果bthome_reports_t内含最多BTHOME_REPORTS_MAX(定义为 10)个bthome_report_t,每个报告包含idlen与指向数据的指针。解析实现会先遍历广播的各 AD 结构,当遇到类型 0x16(Service Data)且 UUID 等于0xFCD2时,进入 Service Data 解析:读取设备信息字节,若加密标志为 0 则直接解析明文 payload;否则用对端 MAC + UUID + 设备信息 + 计数器组成 13 字节 nonce,通过 PSA 的psa_aead_decrypt(CCM 算法、4 字节缩短标签)解密后再解析。用例同时用bthome_free_reports验证了报告结构的内存释放路径。

8. 内存管理:泄漏检测

bthome_memory_management用例连续执行 10 轮bthome_create/bthome_delete循环。配合统一的setup_test/teardown_test机制,每轮测试前后都会记录MALLOC_CAP_8BITMALLOC_CAP_32BIT两种堆区域的空闲内存:

#define TEST_MEMORY_LEAK_THRESHOLD (-460) static void check_leak(size_t before_free, size_t after_free, const char *type) { ssize_t delta = after_free - before_free; TEST_ASSERT_MESSAGE(delta >= TEST_MEMORY_LEAK_THRESHOLD, "memory leak"); }

只要释放后的空闲内存差小于阈值(即净泄漏超过 460 字节)即判定泄漏。这一机制与 CHANGELOG 中"Fixed memory leak issues"的修复记录相互印证——组件在反复创建/删除句柄及解析报告的场景下必须保持内存无泄漏,这是嵌入式长期运行场景的关键质量门槛。

9. 错误处理与边界情况

bthome_error_handling用例系统性地覆盖了异常入参,包括:

  • bthome_create(NULL)bthome_delete(NULL)应返回非ESP_OK
  • 所有配置接口传入 NULL 句柄应被拒绝;
  • 有效句柄下传入 NULL 密钥、NULL MAC、NULL 回调结构应被拒绝;
  • bthome_load_params(NULL)应被拒绝。

此外还有一个独立的bthome_encrypted_adv_without_key用例:在未设置加密密钥的情况下,若设备信息声明加密标志,bthome_make_adv_data应返回长度为 0。这对应 bthome_v2.c 中bthome_encrypt_payloadkey_imported == false的提前拦截(返回ESP_ERR_INVALID_STATE)。这类防御性测试确保组件在未配置完整时不会产生非法广播。

构建与运行测试

编译

在工程目录下执行 IDF 构建命令:

cd components/bluetooth/ble_adv/bthome/test_apps idf.py build

工程级 CMakeLists.txt 通过以下方式接入 IDF 的单元测试基础设施:

cmake_minimum_required(VERSION 3.5) set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components" "../../") include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(bthome_test)

其中EXTRA_COMPONENT_DIRS将 IDF 自带的unit-test-app组件目录以及上级目录(即bthome组件所在路径)加入组件搜索路径。

运行

idf.py monitor

测试程序入口app_main仅打印标识并调用unity_run_menu(),启动后会在串口终端呈现 Unity 菜单,可运行全部用例或选择单个用例执行。每个用例执行前后会打印 8BIT/32BIT 堆内存的 before/after 数值与差值,便于直接观察内存行为。

关键配置项

sdkconfig.defaults 中的配置决定了测试运行环境:

CONFIG_FREERTOS_HZ=1000 CONFIG_ESP_TASK_WDT_EN=n CONFIG_BT_ENABLED=y CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n CONFIG_BTDM_CTRL_MODE_BTDM=n CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_CONTROLLER_ONLY=y

要点如下:

  • CONFIG_BT_ENABLED=yCONFIG_BT_CONTROLLER_ONLY=y:仅启用 BLE 控制器(Controller-only 模式),不启用 Bluedroid 协议栈,这与 BTHome 组件"仅使用广播/扫描,不依赖 GATT 连接"的定位一致;
  • CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y:控制器仅运行 BLE 模式,关闭 BR/EDR;
  • CONFIG_FREERTOS_HZ=1000:系统节拍 1000 Hz,提供更精细的调度粒度;
  • CONFIG_ESP_TASK_WDT_EN=n:关闭任务看门狗,避免长时间运行测试用例时被看门狗复位。

测试背后的协议实现原理

理解测试用例后,再结合源码可以看清 BTHome V2 在组件内的完整数据通路,这有助于扩展测试或排查问题。

对象 ID 与数据长度映射:组件在 bthome_v2.c 中维护了一张静态的object_length[]映射表,覆盖 40 余种对象 ID 与其固定数据长度(如 Battery=1 字节、Temperature Precise=2 字节、Energy=3 字节、Illuminance=3 字节等),解析时据此定位每个对象在 payload 中的边界;RawText两类对象则以"1 字节长度前缀 + 变长数据"的方式编码。

加密体系:组件使用 PSA Crypto API 而非直接调用 mbedTLS(CHANGELOG v0.1.1 记录了这一迁移)。加密流程为:

  1. 组成 13 字节 nonce:本端 MAC(6 字节)+ BTHome UUID0xFCD2(2 字节)+ 设备信息字节(1 字节)+ 计数器(4 字节);
  2. 使用PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, 4)对明文 payload 做 CCM 加密,输出密文与 4 字节认证标签;
  3. 广播中依次携带密文、计数器与标签;计数器在每次加密广播后自增,并通过store回调持久化。

解密路径完全对称:用对端 MAC 替代本端 MAC 组成 nonce,从广播尾部取出计数器与标签后调用psa_aead_decrypt。这就是测试中bthome_set_local_mac_addr(加密用)与bthome_set_peer_mac_addr(解密用)需要分别配置的原因。

计数器持久化回调bthome_callbacks_tstore/load由应用层实现(测试中用 mock 函数模拟),用于保存和恢复加密计数器,防止设备重启后计数器回退导致重放攻击。bthome_load_params会从存储中加载计数器,bthome_make_adv_data每发一包加密广播就会调用store更新。测试中的 mock 实现仅打印日志,真实应用中通常应接入 NVS。

CI 集成与多目标覆盖

按 README 的说明,该测试应用已集成到仓库 CI 流水线,触发条件包括:

  • BTHome 组件代码(components/bluetooth/ble_adv/bthome)被修改;
  • 测试应用本身被修改;
  • 手动触发 CI。

测试在多种 ESP32 芯片变体(ESP32、ESP32-S3、ESP32-C3)以及多个 IDF 版本(4.4、5.0、5.1、5.2)上运行。需要注意的是,idf_component.yml 中声明组件依赖idf: ">=5.0",因此在较新 IDF(5.x)环境下使用是明确支持的;同时该组件已声明支持 esp32c2、esp32c6、esp32h2、esp32h4 等目标。这种跨芯片、跨版本的矩阵测试确保了协议编码与 PSA 加密链路在不同硬件/软件组合下的一致性。

从测试走向实战:配套示例

测试工程之外,仓库还提供了两个可直接参考的完整示例(见 examples/bluetooth/ble_adv/bthome):

  • bulb/—— 灯泡示例,展示传感器数据上报与按键事件处理;
  • dimmer/—— 调光器示例,展示调光事件上报。

结合 CHANGELOG v0.1.0 的说明,组件完整支持 BTHome V2 协议、加密与非加密两种模式、传感器/二进制传感器/事件三类数据上报、NVS 存储配置与自定义回调。测试用例(特别是bthome_payload_creationbthome_adv_data_creationbthome_adv_data_parsing)本质上就是这些实战功能的"最小可验证样本"——理解了用例中的对象 ID 选择、精度缩放(如×100)、设备信息位域设置与广播结构断言,就能直接将其迁移到自己的传感器广播固件中。

小结

本测试工程通过 9 个结构化用例 + 内存泄漏检测 + 错误处理覆盖,为 BTHome V2 组件提供了从"对象生命周期"到"加密广播端到端打包/解析"的全链路保障。对开发者而言,这套测试的价值在于:

  1. 协议验证bthome_payload_creationbthome_binary_sensor_data等用例可作为对象 ID 与数据长度的"活文档",验证任何传感器数据的编码是否正确;
  2. 加密安全bthome_adv_data_creation/bthome_adv_data_parsing用例覆盖了 nonce 组成、计数器持久化与 CCM 认证标签的完整闭环;
  3. 工程可移植性bthome_error_handlingbthome_memory_management保证了组件在长期运行、异常输入下的健壮性,适合直接复用到产品固件。

如需深入协议细节,BTHome 各结构体定义与对象 ID 注释位于 bthome_v2.h,其头部注释亦指向 BTHome 官方格式规范(https://bthome.io/format/)作为设计依据。

【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询