✔零知开源(零知IDE)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 "配置环境" 转移到 "创意实现",极大降低了技术门槛。零知IDE编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知开源(零知IDE)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!
✔访问零知实验室,获取更多实战项目和教程资源吧!
目录
[1.1 硬件清单](#1.1 硬件清单)
[1.2 接线方案表](#1.2 接线方案表)
[1.3 具体接线图](#1.3 具体接线图)
[1.4 接线实物图](#1.4 接线实物图)
[2.1 节点模型定义](#2.1 节点模型定义)
[2.2 多消息处理回调](#2.2 多消息处理回调)
[2.3 系统初始化与NVS持久化](#2.3 系统初始化与NVS持久化)
[2.4 PWM平滑渐变](#2.4 PWM平滑渐变)
[3.1 操作流程](#3.1 操作流程)
[3.2 视频演示](#3.2 视频演示)
[4.1 协议栈架构](#4.1 协议栈架构)
[4.2 组播订阅与分组控制](#4.2 组播订阅与分组控制)
[4.3 Relay多跳转发](#4.3 Relay多跳转发)
项目概述
本项目以零知ESP32 (ESP32-WROOM-32)为核心主控**,构建了一套五节点的BLE Mesh智能照明控制系统。区别于传统的主从蓝牙(BLE Central/Peripheral)方案,本项目采用**Bluetooth SIG Mesh标准协议,实现了真正意义上的无中心多跳自组网:任意一个节点可以作为消息的中继节点,将控制指令转发给距离更远的设备,网络具备自修复能力。
项目难点及解决方案
问题描述:NVS配网数据在固件更新后被意外清除
解决方案: 仅在ESP_ERR_NVS_NO_FREE_PAGES(分区物理写满)时才执行擦除,版本不匹配时保留现有数据
一、系统接线部分
1.1 硬件清单
| 元器件 | 型号 | 数量 | 说明 |
|---|---|---|---|
| 主控板 | 零知ESP32(ESP32-WROOM-32) | 5 | 240MHz双核,内置BLE 5.0 |
| OLED显示屏 | SSD1306 0.96寸 128×64 | 5 | I2C接口,3.3V供电 |
| LED灯珠模块 | LED+限流电阻模块 | 5 | 内置限流电阻 |
| 数据线 | USB Type-A to Micro-USB | 1 | 烧录用 |
| 杜邦线 | 公对母/母对母 | 若干 | 连接用 |
| 手机 | iOS/Android | 1 | 安装nRF Mesh App |
1.2 接线方案表
以下引脚定义严格依据
project_config.h中的宏定义,五台设备接线完全相同。
| 模块 | 模块引脚 | ESP32引脚 | 说明 |
|---|---|---|---|
| OLED SSD1306 | VCC | 3.3V | 注意:只能接3.3V |
| OLED SSD1306 | GND | GND | 接地 |
| OLED SSD1306 | SDA | GPIO 21 | I2C数据,对应OLED_SDA_GPIO |
| OLED SSD1306 | SCL | GPIO 22 | I2C时钟,对应OLED_SCL_GPIO |
| LED模块 | IN/SIG | GPIO 5 | PWM控制,对应LED_GPIO |
| LED模块 | VCC | 3.3V | LED模块用3.3V供电 |
| LED模块 | GND | GND | 接地 |
1.3 具体接线图

OLED VCC务必接3.3V引脚,ESP32的5V引脚会损坏OLED、GPIO 21和GPIO 22之间无需外接上拉电阻
1.4 接线实物图

二、核心代码讲解
本项目代码聚焦四个核心部分:BLE Mesh节点模型定义、Generic Server多消息处理回调、系统初始化流程、LED PWM平滑渐变
2.1 节点模型定义
这是整个BLE Mesh系统的基础,定义了节点向网络"注册"的功能清单
cpp
/* ============================================================
* Config Server:BLE Mesh规范要求每个节点必须包含此模型
* 它管理节点的网络配置,使能GATT Proxy和Relay功能
* ============================================================ */
static esp_ble_mesh_cfg_srv_t config_server = {
.relay = ESP_BLE_MESH_RELAY_ENABLED, // 开启中继:本节点转发他人消息
.beacon = ESP_BLE_MESH_BEACON_ENABLED, // 开启网络信标广播
.friend_state = ESP_BLE_MESH_FRIEND_ENABLED, // 开启Friend功能
.gatt_proxy = ESP_BLE_MESH_GATT_PROXY_ENABLED, // 开启GATT代理:手机可连接
.default_ttl = 7, // 消息最多转发7跳,覆盖多跳拓扑
.net_transmit = ESP_BLE_MESH_TRANSMIT(2, 20), // 原始消息重传2次,间隔20ms
.relay_retransmit = ESP_BLE_MESH_TRANSMIT(2, 20), // 中继消息重传2次
};
/* ============================================================
* Generic OnOff Server:处理开关指令
* AUTO_RSP表示协议栈自动回复Get消息,无需应用层干预
* ============================================================ */
static esp_ble_mesh_gen_onoff_srv_t onoff_server = {
.rsp_ctrl = {
.get_auto_rsp = ESP_BLE_MESH_SERVER_AUTO_RSP, // 自动回复状态查询
.set_auto_rsp = ESP_BLE_MESH_SERVER_AUTO_RSP, // 自动回复Set指令
},
};
/* ============================================================
* Generic Level Server:处理亮度调节指令
* Level范围:-32768~32767,映射到PWM亮度0~255
* ============================================================ */
static esp_ble_mesh_gen_level_srv_t level_server = {
.rsp_ctrl = {
.get_auto_rsp = ESP_BLE_MESH_SERVER_AUTO_RSP,
.set_auto_rsp = ESP_BLE_MESH_SERVER_AUTO_RSP,
},
};
/* ============================================================
* 将三个模型打包进一个Element(元素)
* 每个节点至少有一个Element,本项目每个节点只有Primary Element
* ============================================================ */
static esp_ble_mesh_model_t root_models[] = {
ESP_BLE_MESH_MODEL_CFG_SRV(&config_server), // Config Server
ESP_BLE_MESH_MODEL_GEN_ONOFF_SRV(&onoff_pub, &onoff_server), // OnOff Server
ESP_BLE_MESH_MODEL_GEN_LEVEL_SRV(&level_pub, &level_server), // Level Server
};
static esp_ble_mesh_elem_t elements[] = {
ESP_BLE_MESH_ELEMENT(0, root_models, ESP_BLE_MESH_MODEL_NONE),
};
/* 节点组合数据:告诉Provisioner本节点有哪些Element和Model */
static esp_ble_mesh_comp_t composition = {
.cid = MESH_COMPANY_ID, // 0x02E5 = Espressif公司ID
.elements = elements,
.element_count = ARRAY_SIZE(elements),
};
default_ttl消息存活跳数设置为7,每经过一个中继节点减1,为0时丢弃
2.2 多消息处理回调
nRF Mesh App在不同操作场景下发送的消息类型不同,必须全部处理才能实现完整的控制体验
cpp
/* ============================================================
* 三个内联辅助函数:做好Level与亮度的双向映射
*
* BLE Mesh Level范围:-32768 ~ 32767(int16_t)
* LED亮度范围:0 ~ 255(uint8_t)
* 映射公式:bri = (level + 32768) * 255 / 65535
* ============================================================ */
static inline int16_t clamp16(int32_t v) {
if (v > 32767) return 32767;
if (v < -32768) return -32768;
return (int16_t)v;
}
static inline uint8_t level_to_bri(int16_t lv) {
return (uint8_t)(((int32_t)lv + 32768) * 255 / 65535);
}
static inline int16_t bri_to_level(uint8_t bri) {
return (int16_t)((int32_t)bri * 65535 / 255 - 32768);
}
static void mesh_generic_server_cb(
esp_ble_mesh_generic_server_cb_event_t event,
esp_ble_mesh_generic_server_cb_param_t *param)
{
/* 只处理状态变化事件,忽略其他类型 */
if (event != ESP_BLE_MESH_GENERIC_SERVER_STATE_CHANGE_EVT) return;
uint32_t op = param->ctx.recv_op; // 获取具体操作码
/* ── ① Generic OnOff Set ────────────────────────────────
* 触发场景:App中的ON/OFF开关按钮(单节点或分组均可触发)
* 关键处理:关灯时保留当前亮度值,下次开灯时恢复
* ─────────────────────────────────────────────────────── */
if (op == ESP_BLE_MESH_MODEL_OP_GEN_ONOFF_SET ||
op == ESP_BLE_MESH_MODEL_OP_GEN_ONOFF_SET_UNACK) {
bool on = (param->value.state_change.onoff_set.onoff != 0);
g_local_led_on = on;
if (on) {
// 开灯:恢复上次亮度,若无历史亮度则默认78%(200/255)
uint8_t bri = g_local_brightness ? g_local_brightness : 200;
g_local_brightness = bri;
led_pwm_set(true, bri);
} else {
led_pwm_set(false, 0); // 关灯:PWM占空比置0
}
}
/* ── ② Generic Level Set(绝对值)──────────────────────
* 触发场景:App单节点控制界面的Level滑块
* 注意:Level是int16_t绝对值,需要映射到uint8_t亮度
* ─────────────────────────────────────────────────────── */
if (op == ESP_BLE_MESH_MODEL_OP_GEN_LEVEL_SET ||
op == ESP_BLE_MESH_MODEL_OP_GEN_LEVEL_SET_UNACK) {
int16_t lv = param->value.state_change.level_set.level;
uint8_t bri = level_to_bri(lv);
g_local_brightness = bri;
g_local_led_on = (bri > 0);
led_pwm_set(g_local_led_on, bri);
}
/* ── ③ Generic Level Delta Set(相对增量)──────────────
* 触发场景:App分组控制界面的"+""-"按钮
*
* state_change结构体里存的是协议栈计算后的【结果Level值】
* 字段名是 .level(int16_t),不是 .delta_level
* 直接读取结果值即可,无需自己做加减运算
* ─────────────────────────────────────────────────────── */
if (op == ESP_BLE_MESH_MODEL_OP_GEN_DELTA_SET ||
op == ESP_BLE_MESH_MODEL_OP_GEN_DELTA_SET_UNACK) {
int16_t new_lv = param->value.state_change.delta_set.level; // 直接取结果
uint8_t bri = level_to_bri(new_lv);
g_local_brightness = bri;
g_local_led_on = (bri > 0);
led_pwm_set(g_local_led_on, bri);
}
/* ── ④ Generic Level Move Set(连续移动)───────────────
* 触发场景:长按分组控制界面的"+""-"按钮
* 同样读取 .level 字段,逻辑与Delta Set完全一致
* ─────────────────────────────────────────────────────── */
if (op == ESP_BLE_MESH_MODEL_OP_GEN_MOVE_SET ||
op == ESP_BLE_MESH_MODEL_OP_GEN_MOVE_SET_UNACK) {
int16_t new_lv = param->value.state_change.move_set.level;
uint8_t bri = level_to_bri(new_lv);
g_local_brightness = bri;
g_local_led_on = (bri > 0);
led_pwm_set(g_local_led_on, bri);
}
}
操作码对应关系速查表:
| App操作 | 发送消息类型 | 操作码 | 回调字段 |
|---|---|---|---|
| 单节点ON/OFF开关 | Generic OnOff Set | MODEL_OP_GEN_ONOFF_SET |
onoff_set.onoff |
| 单节点Level滑块 | Generic Level Set | MODEL_OP_GEN_LEVEL_SET |
level_set.level |
| 分组+/-按钮 | Generic Level Delta Set | MODEL_OP_GEN_DELTA_SET |
delta_set.level(结果值!) |
2.3 系统初始化与NVS持久化
cpp
void app_main(void)
{
/* ── 步骤1:NVS初始化(含关键保护逻辑)─────────────────
* BLE Mesh协议栈将以下数据自动存入NVS:
* - NetKey(网络密钥)
* - AppKey(应用密钥)
* - 单播地址(Unicast Address)
* - 序列号(防重放攻击)
* - 分组订阅地址
*
* 关键修复:区分两种错误码
* ESP_ERR_NVS_NO_FREE_PAGES → 分区物理写满,必须擦除
* ESP_ERR_NVS_NEW_VERSION_FOUND → 固件更新版本不匹配,【不擦除】保留数据
* ─────────────────────────────────────────────────────── */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
ESP_LOGW(TAG, "NVS partition full - erasing (provisioning lost)");
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
} else if (ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// 固件更新后版本号不匹配,但数据仍然有效,保留不擦
ESP_LOGW(TAG, "NVS version mismatch - keeping existing data");
ret = ESP_OK;
}
ESP_ERROR_CHECK(ret);
/* ── 步骤2:LED PWM初始化 ─────────────────────────────
* 参数:GPIO 25,LEDC_CHANNEL_0,TIMER_0,5kHz,8bit分辨率
* 8bit = 256级调光,对应占空比0~255
* ─────────────────────────────────────────────────────── */
led_pwm_init(LED_GPIO, LED_PWM_CHANNEL, LED_PWM_TIMER,
LED_PWM_FREQ_HZ, LED_PWM_RESOLUTION);
/* ── 步骤3:OLED初始化 ───────────────────────────────── */
oled_init(OLED_I2C_PORT, OLED_SDA_GPIO, OLED_SCL_GPIO, OLED_I2C_ADDR);
/* ── 步骤4:BLE Mesh初始化(含NVS状态检测)──────────── */
ESP_ERROR_CHECK(ble_mesh_init());
/* ── 步骤5:启动两个后台任务 ─────────────────────────── */
xTaskCreate(oled_refresh_task, "oled_refresh", 2560, NULL, 3, NULL);
xTaskCreate(led_fade_task, "led_fade", 1024, NULL, 5, NULL);
}
在ble_mesh_init()函数末尾,通过esp_ble_mesh_node_is_provisioned()检测NVS中是否存有配网数据,已配网节点跳过node_prov_enable()直接恢复运行
cpp
if (esp_ble_mesh_node_is_provisioned()) {
// 已配网:直接以原单播地址恢复,不广播未配网信标
g_provisioned = true;
ESP_LOGI(TAG, "Node %d ALREADY PROVISIONED - resuming", NODE_ID);
} else {
// 未配网(首次或出厂重置):启动ADV广播,等待App配网
esp_ble_mesh_node_prov_enable(ESP_BLE_MESH_PROV_ADV | ESP_BLE_MESH_PROV_GATT);
}
2.4 PWM平滑渐变
cpp
/* 渐变参数 */
#define FADE_STEP 4 // 每次渐变步长(占空比单位)
#define FADE_INTERVAL 15 // 渐变更新间隔(毫秒)
/* led_fade_task每15ms调用一次此函数 */
void led_pwm_fade_tick(void)
{
uint32_t now = xTaskGetTickCount() * portTICK_PERIOD_MS;
if (now - s_last_tick < FADE_INTERVAL) return; // 未到更新时刻
s_last_tick = now;
if (s_current == s_target) return; // 已到达目标值
/* 非线性逼近:剩余差值大于步长时固定步进,接近目标时精确到达 */
if (s_current < s_target) {
uint8_t d = s_target - s_current;
s_current += (d > FADE_STEP) ? FADE_STEP : d; // 最后一步精确对齐
} else {
uint8_t d = s_current - s_target;
s_current -= (d > FADE_STEP) ? FADE_STEP : d;
}
/* 将当前亮度值写入LEDC外设 */
ledc_set_duty(LEDC_LOW_SPEED_MODE, s_channel, s_current);
ledc_update_duty(LEDC_LOW_SPEED_MODE, s_channel);
}
总渐变时间从0~100%约960ms完成全程
系统流程图

调光控制依赖库
esp_ble_mesh_generic_model_api 定义了BLE Mesh Generic Model的全部服务端(Server)和客户端(Client)API
cpp
// 服务端回调注册函数
esp_err_t esp_ble_mesh_register_generic_server_callback(
esp_ble_mesh_generic_server_cb_t callback);
// 服务端回调参数中的状态变化联合体
typedef union {
struct { uint8_t onoff; } onoff_set; // OnOff结果
struct { int16_t level; } level_set; // Level绝对值结果
struct { int16_t level; } delta_set; // Delta应用后的结果
struct { int16_t level; } move_set; // Move应用后的结果
} esp_ble_mesh_server_state_change_t;
BLE Mesh Generic Level模型的三种Set消息
Generic Level Set:设置绝对Level值,适用于已知目标亮度的精确控制场景
Generic Level Delta Set:相对当前Level值进行偏移,适用于增减调节场景(旋钮、+/-按钮);同一手势的多次消息会叠加(累积Delta),协议栈自动维护基准值
三、项目结果演示
3.1 操作流程
编译与烧录
①打开 main/project_config.h,将 #define NODE_ID 0修改为对应设备编号(0~4)

②设置目标芯片,点击底部任务栏"Set Espressif Device Target"设置IDF_TARGET芯片为ESP32

③烧录并打开串口监视器,通过日志和操作数据进行调试

手机配网
①打开nRF Mesh App → Network → 右上角"+"扫描
通过UUID第三字节(00,01,02,03,04)识别节点,依次配网(选择No OOB)
②配网成功后,节点LED闪烁三次,OLED显示变为已配网状态,为每个节点的OnOff Server和Level Server绑定App Key 1

创建分组与订阅
在Groups界面创建
Node 0,1、Node 2,3、Node 4、Node 0~4四个分组,为每个节点的OnOff Server和Level Server订阅相应分组
功能控制
单灯控制:在Network中选择节点,使用ON/OFF和Level滑块
分组控制:在Groups中选择分组,使用ON/OFF和+/-按钮
3.2 视频演示
零知ESP32五节点蓝牙组网多灯控制------nRF Mesh A
本视频演示了基于零知ESP32的五节点BLE Mesh智能照明系统的完整操作流程,包括:五台ESP32设备的固件烧录、nRF Mesh iOS App的配网全过程(从扫描发现到AppKey绑定)、创建三个自定义分组并为各节点配置订阅、分组控制下的LED同步开关与PWM渐变调光效果,以及OLED显示屏实时状态反馈
四、ESP-BLE-MESH技术讲解
BLE Mesh构建了一个无中心多跳自组网,一条消息可以通过多跳跨越很远的距离,即使个别节点离线,消息也能找到替代路径到达目标
4.1 协议栈架构
Mesh Networking功能的实现是基于层级结构,每一层功能框架如图,

4.2 组播订阅与分组控制
组播地址由 nRF Mesh App 分配并写入节点模型的 Subscription List,节点收到发往已订阅组播地址的消息后触发
mesh_generic_server_cb

4.3 Relay多跳转发
对应 Networking 层的 Relay Feature:Flooding 泛洪机制、TTL 逐跳递减、消息缓存去重

本项目配置 default_ttl=7、relay_retransmit=TRANSMIT(2,20)
五、常见问题解答(FAQ)
Q1:多台设备如何区分?UUID有什么规律?
A:本项目Device UUID格式为 DD:DD:XX:00:00:...,第三字节XX即为 NODE_ID(0~4的十六进制值)。节点0的UUID为 DD:DD:00:...,节点1为 DD:DD:01:...,以此类推,在nRF Mesh App扫描界面可以直接看到UUID从而区分设备
Q2:手机App无法扫描到设备?
A:①确认ESP32已上电且串口日志显示 Open nRF Mesh App -> Scanner -> find ESP-BLE-MESH;②iOS需要在系统设置中开启nRF Mesh的蓝牙权限;③若该设备已被配网,需要先在App中将其重置(Reset Node)才能重新扫描到。
项目资源整合
ESP-BLE-MESH 架构: ble-mesh-architecture
BLE Mesh API: bluetooth/esp-ble-mesh