下面的方案和示例代码基于 ESP-IDF (建议使用最新版本,如ESP-IDF v5.0或更高) 中的 BLE Mesh 核心组件。本文将重点讲解如何使用 ESP32-C3 作为 BLE Mesh Provisioner,扫描并添加 Mesh 节点,并与已加入网络的节点进行数据通信。此过程主要分为以下步骤:
-
准备工作
- 确保您的开发环境已安装 ESP-IDF,并已配置好编译和烧录环境。
- 一块 ESP32-C3 开发板作为 Provisioner。
- 一块或多块其他支持 BLE Mesh 的 ESP32/ESP32-C3 开发板作为待加入的节点(Device)。
- 蓝牙 Mesh 特性在menuconfig中开启(默认已启用于 BLE Mesh 示例)。
-
BLE Mesh 核心概念回顾
- Provisioner:负责建立或管理一个 Mesh 网络。它可以扫描未配网(unprovisioned)的节点,使用OOB数据(如果有)完成配网(Provisioning),为节点分配地址和网络信息。
- Node:加入网络的设备,它有唯一地址,并实现一个或多个 Mesh 模型,用于发送/接收特定类型的数据。
- 模型(Model):定义节点可支持的功能,如 Generic OnOff 模型,可实现简单的灯开关功能。
- 通信:通常使用模型的消息来进行控制和数据交换。
-
功能实现流程
- Provisioner 启动 BLE Mesh 栈:初始化 BLE 栈、BLE Mesh 栈,加载 Provisioner 相关配置信息(如 UUID、OOB、发号地址范围)。
- 扫描并发现未配网设备:Provisioner 广播请求,待配网设备广播不带加密的信标,Provisioner 接收后触发回调。
- 执行配网(Provisioning)过程:Provisioner 与目标设备建立会话,交换公钥和身份信息,分配网络密钥(NetKey)、应用密钥(AppKey)、单播地址等信息,最终使节点加入 Mesh 网络。
- 为节点配置模型和订阅/发布地址:常见地,Provisioner作为配置客户端(Config Client Model),向新加入的节点(运行 Config Server Model)发送配置消息,为其设置模型发布/订阅地址、绑定AppKey等。
- 数据通信:使用通用模型(如Generic OnOff Client/Server)或厂商自定义模型发送控制消息。Provisioner通过Client模型向Node的Server模型发送指令或查询状态。
-
示例代码结构
以下代码参考自 ESP-IDF 提供的 BLE Mesh 示例进行适当简化和注释(如examples/bluetooth/esp_ble_mesh
目录下的prov
和client
示例)。代码重点展示初始化、Provisioner流程和发送/接收消息的基本逻辑。实际项目中请根据需要拆分文件和添加错误检查。请在
menuconfig
中启用 BLE Mesh 功能:Component config → Bluetooth → Bluetooth mesh support
假设使用 Generic OnOff Client 模型作为示例模型,用于演示控制已配网节点的 LED。
示例代码(main.c)
#include <string.h>
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_ble_mesh_defs.h"
#include "esp_ble_mesh_generic_model_api.h"
#include "esp_ble_mesh_networking_api.h"
#include "esp_ble_mesh_provisioning_api.h"
#include "esp_ble_mesh_config_model_api.h"
#include "nvs_flash.h"
#define TAG "MESH_PROV"
// 使用Generic OnOff Client模型ID
static esp_ble_mesh_client_t onoff_client = ESP_BLE_MESH_CLIENT_INIT;
// 定义OnOff客户端模型
static esp_ble_mesh_model_t root_models[] = {
ESP_BLE_MESH_MODEL_GEN_ONOFF_CLI(&onoff_client),
};
// 元素定义
static esp_ble_mesh_elem_t elements[] = {
ESP_BLE_MESH_ELEMENT(0, root_models, ESP_BLE_MESH_MODEL_NONE),
};
// Provisioner节点组件定义
static esp_ble_mesh_comp_t comp = {
.cid = ESP_BLE_MESH_CID_NVAL, // 使用本地CID或者0xFFFF
.elements = elements,
.element_count = ARRAY_SIZE(elements),
};
// Provisioning信息(Provisioner自身信息)
static esp_ble_mesh_prov_t prov = {
.uuid = {0x32, 0xC3, 0x00, 0x00}, // 用于区分本Provisioner的UUID
.prov_name = "ESP32-C3_Prov",
.prov_uuid = {0x32, 0xC3, 0x00, 0x00},
.prov_unicast_addr = 0x0001, // Provisioner自有的单播地址
.prov_start_address = 0x0005, // 为分配给新节点的地址起始位置
};
// 全局变量存储配网完成的节点信息
typedef struct {
uint16_t unicast_addr;
uint8_t dev_uuid[16];
} node_info_t;
static node_info_t nodes[10];
static int node_count = 0;
// 回调函数处理Prov和配置事件
static void ble_mesh_prov_cb(esp_ble_mesh_prov_cb_event_t event,
esp_ble_mesh_prov_cb_param_t *param)
{
switch (event) {
case ESP_BLE_MESH_PROV_REGISTER_COMP_EVT:
ESP_LOGI(TAG, "Provisioner register complete, err_code %d", param->prov_register_comp.err_code);
break;
case ESP_BLE_MESH_PROV_LINK_OPEN_EVT:
ESP_LOGI(TAG, "Provisioning link opened with device");
break;
case ESP_BLE_MESH_PROV_LINK_CLOSE_EVT:
ESP_LOGI(TAG, "Provisioning link closed");
break;
case ESP_BLE_MESH_PROV_COMPLETE_EVT:
ESP_LOGI(TAG, "Provisioning completed: addr 0x%04x", param->prov_complete.addr);
if (node_count < 10) {
nodes[node_count].unicast_addr = param->prov_complete.addr;
memcpy(nodes[node_count].dev_uuid, param->prov_complete.dev_uuid, 16);
node_count++;
}
break;
default:
break;
}
}
// 配置模型回调,用于配置节点(如绑定AppKey,设置订阅地址等)
static void ble_mesh_config_client_cb(esp_ble_mesh_cfg_client_cb_event_t event,
esp_ble_mesh_cfg_client_cb_param_t *param)
{
if (event == ESP_BLE_MESH_CFG_CLIENT_GET_STATE_EVT ||
event == ESP_BLE_MESH_CFG_CLIENT_SET_STATE_EVT) {
ESP_LOGI(TAG, "Config Client event, opcode:0x%04x, status:0x%02x",
param->params->opcode, param->status_cb.data->status);
}
}
// 通用模型消息回调
static void ble_mesh_generic_client_cb(esp_ble_mesh_generic_client_cb_event_t event,
esp_ble_mesh_generic_client_cb_param_t *param)
{
if (event == ESP_BLE_MESH_GENERIC_CLIENT_STATE_CHANGE_EVT) {
// 收到状态响应
if (param->params->opcode == ESP_BLE_MESH_MODEL_OP_GEN_ONOFF_STATUS) {
ESP_LOGI(TAG, "OnOff Status from 0x%04X: %d",
param->params->ctx.addr, param->status_cb.onoff_status.present_onoff);
}
}
}
// 初始化BLE,Mesh
static void ble_mesh_init(void)
{
esp_ble_mesh_register_prov_callback(ble_mesh_prov_cb);
esp_ble_mesh_register_config_client_callback(ble_mesh_config_client_cb);
esp_ble_mesh_register_generic_client_callback(ble_mesh_generic_client_cb);
esp_ble_mesh_init(&prov, &comp);
esp_ble_mesh_provisioner_prov_enable(ESP_BLE_MESH_PROV_ADV); // 启用Provisioner模式
esp_ble_mesh_provisioner_set_dev_uuid_match((uint8_t *)"\x32\xC3", 2, 0, false);
// 匹配UUID前2字节,扫描时将识别并尝试配网此类设备
esp_ble_mesh_enable();
ESP_LOGI(TAG, "BLE Mesh provisioner initialized");
}
// 向节点发送OnOff命令(示例函数)
static void send_onoff_set(uint16_t addr, bool on)
{
esp_ble_mesh_generic_client_set_state_t set = {0};
esp_ble_mesh_msg_ctx_t ctx = {
.addr = addr, // 目标节点地址
.app_idx = 0,
.net_idx = 0,
.send_ttl = 3,
.send_rel = false,
};
set.onoff_set.op_en = false;
set.onoff_set.onoff = on ? 1 : 0;
esp_err_t err = esp_ble_mesh_generic_client_set_state(&onoff_client.model, &ctx, &set);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send OnOff set message (err 0x%x)", err);
} else {
ESP_LOGI(TAG, "OnOff set to %d sent to 0x%04X", on, addr);
}
}
void app_main(void)
{
esp_err_t err = nvs_flash_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "NVS init failed");
return;
}
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
err = esp_bt_controller_init(&bt_cfg);
if (err) {
ESP_LOGE(TAG, "BT controller init failed (0x%x)", err);
return;
}
err = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (err) {
ESP_LOGE(TAG, "BT controller enable failed (0x%x)", err);
return;
}
err = esp_bluedroid_init();
if (err) {
ESP_LOGE(TAG, "Bluedroid init failed (0x%x)", err);
return;
}
err = esp_bluedroid_enable();
if (err) {
ESP_LOGE(TAG, "Bluedroid enable failed (0x%x)", err);
return;
}
ble_mesh_init();
// 稍后通过一些机制,比如等待一段时间,让Provisioner扫描并自动配网节点
// 实际中可以加入按键触发扫描/配网流程
// 示例:延迟30秒后,如果成功配网了某节点,为该节点发送OnOff指令
vTaskDelay(30000 / portTICK_PERIOD_MS);
if (node_count > 0) {
// 对第一个配网的节点发送OnOff=ON
send_onoff_set(nodes[0].unicast_addr, true);
}
}
代码说明
prov
结构和comp
结构定义了Provisioner自身的基本信息和其模型组成。这里包含了一个Generic OnOff Client模型用来发送指令。ble_mesh_prov_cb
回调处理配网事件。当ESP_BLE_MESH_PROV_COMPLETE_EVT
触发时,表示有新节点已成功加入网络。我们在这里记录下节点的单播地址。ble_mesh_config_client_cb
可用于后续配置节点的AppKey绑定、Publication地址等高级操作。ble_mesh_generic_client_cb
在收到节点状态回复时会回调,这里简化为打印状态。send_onoff_set()
函数示范了如何向已配网的节点发送Generic OnOff指令,以实现数据通信。
-
实际部署与测试
- 烧录代码到ESP32-C3。
- 另准备一个ESP32/ESP32-C3作为节点,运行一个未配网的 BLE Mesh Node 示例(如
ble_mesh_node
示例),使用相同的AppKey。 - 当Provisioner运行后,会扫描周边未配网设备并自动为其配网。
- 配网成功后30秒后会自动给第一个节点发送OnOff=ON指令(仅作为演示)。
- 可观察串口日志,确认配网和消息发送接收情况。
-
扩展和补充
- 可在
menuconfig
中配置更多特性,如更改发号地址范围,启用OOB验证等。 - 实际项目中需要实现更多事件回调中的逻辑,如对多个节点进行管理、在
PROV_COMPLETE_EVT
后对节点进行AppKey绑定、Publish地址设置,确保节点有正确的通信路径。 - 增加自定义的Vendor Model可以实现更复杂的数据交互。
- 可在
以上示例演示了一个基本的BLE Mesh Provisioner角色的实现步骤和参考代码,帮助您在ESP32-C3上构建一个可发现、配网并与其他BLE Mesh节点通信的系统。