零知IDE——基于ESP32的BLE Mesh蓝牙组网多灯智能控制系统

✔零知开源(零知IDE)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 "配置环境" 转移到 "创意实现",极大降低了技术门槛。零知IDE编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知开源(零知IDE)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!

✔访问零知实验室,获取更多实战项目和教程资源吧!

www.lingzhilab.com

目录

一、系统接线部分

[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 视频演示)

四、ESP-BLE-MESH技术讲解

[4.1 协议栈架构](#4.1 协议栈架构)

[4.2 组播订阅与分组控制](#4.2 组播订阅与分组控制)

[4.3 Relay多跳转发](#4.3 Relay多跳转发)

五、常见问题解答(FAQ)

Q1:多台设备如何区分?UUID有什么规律?

Q2:手机App无法扫描到设备?


项目概述

本项目以零知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,1Node 2,3Node 4Node 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=7relay_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

相关推荐
沐欣工作室_lvyiyi2 小时前
基于单片机的智能音箱系统(论文+源码)
单片机·嵌入式硬件·毕业设计·智能音箱
zmj3203242 小时前
PLC与单片机、继电器控制系统 的价格比较
单片机·嵌入式硬件·plc
【 STM32开发 】2 小时前
【STM32 + CubeMX】低功耗 -- Stop 停止模式
stm32·单片机·嵌入式硬件
zhouping@2 小时前
[极客大挑战 2020]Greatphp
android·ide·web安全·android studio
jghhh012 小时前
51单片机控制42步进电机程序
单片机·嵌入式硬件·51单片机
没有余地 EliasJie2 小时前
FFmpeg介绍与ESP32资源受限下的视频流传输优化策略
单片机·物联网·ffmpeg
liangdabiao3 小时前
XHS_Business_Idea_Validator-小红书解析市场机会智能体
java·ide·intellij-idea
zmj3203243 小时前
PLC与单片机(微控制器MCU)、传统继电器控制系统
单片机·嵌入式硬件·plc
AnalogElectronic3 小时前
ESP-01S 和树莓派pico,普中51单片机开发板,综合比较
单片机·嵌入式硬件·51单片机