
✔零知开源零知IDE是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录让开发重心从 “配置环境” 转移到 “创意实现”极大降低了技术门槛。零知IDE编程软件内置上千个覆盖多场景的示例代码支持项目源码一键下载项目文章在线浏览。零知开源(零知IDE)平台通过软硬件协同创新让你的创意快速转化为实物来动手试试吧✔访问零知实验室获取更多实战项目和教程资源吧www.lingzhilab.com目录一、系统接线部分1.1 硬件清单1.2 接线方案表1.3 具体接线图1.4 接线实物图二、核心代码讲解2.1 节点模型定义2.2 多消息处理回调2.3 系统初始化与NVS持久化2.4 PWM平滑渐变三、项目结果演示3.1 操作流程3.2 视频演示四、ESP-BLE-MESH技术讲解4.1 协议栈架构4.2 组播订阅与分组控制4.3 Relay多跳转发五、常见问题解答FAQQ1多台设备如何区分UUID有什么规律Q2手机App无法扫描到设备项目概述本项目以零知ESP32ESP32-WROOM-32为核心主控构建了一套五节点的BLE Mesh智能照明控制系统。区别于传统的主从蓝牙BLE Central/Peripheral方案本项目采用Bluetooth SIG Mesh标准协议实现了真正意义上的无中心多跳自组网任意一个节点可以作为消息的中继节点将控制指令转发给距离更远的设备网络具备自修复能力。项目难点及解决方案问题描述NVS配网数据在固件更新后被意外清除解决方案仅在ESP_ERR_NVS_NO_FREE_PAGES分区物理写满时才执行擦除版本不匹配时保留现有数据一、系统接线部分1.1 硬件清单元器件型号数量说明主控板零知ESP32ESP32-WROOM-325240MHz双核内置BLE 5.0OLED显示屏SSD1306 0.96寸 128×645I2C接口3.3V供电LED灯珠模块LED限流电阻模块5内置限流电阻数据线USB Type-A to Micro-USB1烧录用杜邦线公对母/母对母若干连接用手机iOS/Android1安装nRF Mesh App1.2 接线方案表以下引脚定义严格依据project_config.h中的宏定义五台设备接线完全相同。模块模块引脚ESP32引脚说明OLED SSD1306VCC3.3V注意只能接3.3VOLED SSD1306GNDGND接地OLED SSD1306SDAGPIO 21I2C数据对应OLED_SDA_GPIOOLED SSD1306SCLGPIO 22I2C时钟对应OLED_SCL_GPIOLED模块IN/SIGGPIO 5PWM控制对应LED_GPIOLED模块VCC3.3VLED模块用3.3V供电LED模块GNDGND接地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系统的基础定义了节点向网络注册的功能清单/* * Config ServerBLE 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在不同操作场景下发送的消息类型不同必须全部处理才能实现完整的控制体验/* * 三个内联辅助函数做好Level与亮度的双向映射 * * BLE Mesh Level范围-32768 ~ 32767int16_t * LED亮度范围0 ~ 255uint8_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值】 * 字段名是 .levelint16_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 SetMODEL_OP_GEN_ONOFF_SETonoff_set.onoff单节点Level滑块Generic Level SetMODEL_OP_GEN_LEVEL_SETlevel_set.level分组/-按钮Generic Level Delta SetMODEL_OP_GEN_DELTA_SETdelta_set.level结果值2.3 系统初始化与NVS持久化void app_main(void) { /* ── 步骤1NVS初始化含关键保护逻辑───────────────── * 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); /* ── 步骤2LED PWM初始化 ───────────────────────────── * 参数GPIO 25LEDC_CHANNEL_0TIMER_05kHz8bit分辨率 * 8bit 256级调光对应占空比0~255 * ─────────────────────────────────────────────────────── */ led_pwm_init(LED_GPIO, LED_PWM_CHANNEL, LED_PWM_TIMER, LED_PWM_FREQ_HZ, LED_PWM_RESOLUTION); /* ── 步骤3OLED初始化 ───────────────────────────────── */ oled_init(OLED_I2C_PORT, OLED_SDA_GPIO, OLED_SCL_GPIO, OLED_I2C_ADDR); /* ── 步骤4BLE 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()直接恢复运行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平滑渐变/* 渐变参数 */ #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和客户端ClientAPI// 服务端回调注册函数 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_cb4.3 Relay多跳转发对应 Networking 层的 Relay FeatureFlooding 泛洪机制、TTL 逐跳递减、消息缓存去重本项目配置default_ttl7、relay_retransmitTRANSMIT(2,20)五、常见问题解答FAQQ1多台设备如何区分UUID有什么规律A本项目Device UUID格式为 DD:DD:XX:00:00:...第三字节XX即为 NODE_ID0~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-architectureBLE Mesh API bluetooth/esp-ble-mesh