ESP32-IDF开发环境搭建与Wi-Fi配网实战:从环境踩坑到量产级BLE配网方案
文章目录

每日一句正能量
人生绝大多数困境并非能力不足,而是思维没跟上。
能力像工具,思维像使用说明书。很多时候我们拿着锤子找钉子,却没意识到门是推开的,不是砸开的。换个角度,困境可能只是伪装的台阶。
导读
谁说嵌入式只是调包和焊板子?一个Wi-Fi配网功能,就能区分"能跑Demo"和"能进量产"的工程师。本文从ESP-IDF环境搭建的12个踩坑点,到SmartConfig/BLE/SoftAP三种配网方案的深度对比,最终给出经过量产验证的BLE+NVS+自动回连完整方案。
一、背景:为什么配网是物联网产品的"最后一公里"
在物联网产品开发中,Wi-Fi配网(Provisioning)是用户接触产品的第一个环节,也是退货率最高的环节之一。据统计,消费级IoT设备的退货原因中,"无法连接Wi-Fi"占比高达34%。
ESP32-S3作为乐鑫最新的AIoT芯片,集成了Wi-Fi 4 + BLE 5.0双模,是配网方案的理想载体。但许多开发者在实际落地时面临以下痛点:
- 环境搭建:Python版本冲突、USB权限、Windows路径问题等"环境玄学"
- 方案选择:SmartConfig、BLE、SoftAP三种方案各有利弊,如何根据产品场景选型?
- 安全与可靠:明文传输密码?配网失败后如何优雅降级?
- 量产一致性:不同批次芯片的MAC地址管理、证书烧录、工厂测试流程
本文基于 ESP-IDF v5.2.1 + ESP32-S3-DevKitC-1 开发板,提供从环境搭建到量产配网的完整技术路径。
二、ESP-IDF开发环境搭建:踩坑实录与避坑指南
2.1 环境搭建全流程

步骤1:系统准备(Linux Ubuntu 22.04推荐)
# 检查Python版本(ESP-IDF v5.2要求Python 3.8-3.11,3.12+存在兼容性问题)
python3 --version # 输出应为 Python 3.10.12
# 安装依赖
sudo apt-get install -y git wget flex bison gperf python3-pip python3-venv \
cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0
# 解决USB权限问题(关键!否则idf.py flash会报Permission denied)
sudo usermod -aG dialout $USER
# 重新登录或执行:newgrp dialout
踩坑记录1:Python 3.12的distutils模块已被移除,而ESP-IDF的部分构建脚本仍依赖它。解决方案:使用pyenv或venv创建Python 3.10虚拟环境。
# 创建隔离的Python环境
python3.10 -m venv ~/esp-idf-venv
source ~/esp-idf-venv/bin/activate
步骤2:工具链安装
# 克隆ESP-IDF(建议使用国内镜像加速)
git clone -b v5.2.1 --recursive https://github.com/espressif/esp-idf.git ~/esp-idf
cd ~/esp-idf
git submodule update --init --recursive # 确保所有子模块完整
# 安装工具链(约下载2GB,耗时10-30分钟)
./install.sh esp32s3
# 激活环境(每个新终端都需要执行!)
. ./export.sh
# 建议添加到.bashrc:alias get_idf='. $HOME/esp-idf/export.sh'
踩坑记录2:export.sh必须在当前shell中执行(source),而非子shell中执行。直接运行./export.sh会导致环境变量未生效,idf.py命令找不到。
踩坑记录3:子模块不完整会导致编译时缺少esp32s3.rom.ld等链接脚本,报错ld: cannot find。务必执行git submodule update --init --recursive。
步骤3:VSCode + ESP-IDF插件配置
VSCode是ESP-IDF官方推荐的IDE,配置步骤如下:
- 安装插件:
Espressif IDF(由乐鑫官方维护) - 按
Ctrl+Shift+P→ESP-IDF: Configure ESP-IDF Extension - 选择
Use existing ESP-IDF setup,指定~/esp-idf路径 - 设置目标芯片:
ESP32-S3 - 配置OpenOCD调试器(可选,用于JTAG调试)
踩坑记录4:Windows用户需注意路径长度限制(260字符)。ESP-IDF的构建路径可能超出限制,建议在D:\esp等短路径下操作,或在注册表启用长路径支持。
步骤4:创建并验证工程
# 创建新项目
cd ~/esp
idf.py create-project wifi_prov_demo
cd wifi_prov_demo
# 设置目标芯片(只需执行一次)
idf.py set-target esp32s3
# 打开menuconfig进行关键配置
idf.py menuconfig
menuconfig关键配置项:
Component config → ESP32-S3-specific → CPU frequency → 240MHz
Component config → Wi-Fi → Max number of Wi-Fi TX buffers → 32
Component config → Bluetooth → Bluetooth controller mode → BLE Only
Component config → Bluetooth → NimBLE Options → Enable BLE 5.0 features
Partition Table → Partition Table → Custom partition table CSV
2.2 环境验证:Hello World与串口监控
# 编译
idf.py build
# 烧录(自动检测波特率,ESP32-S3支持USB-OTG直接下载,无需按Boot键)
idf.py -p /dev/ttyUSB0 flash
# 监控串口输出(115200 baud,自动解码ESP-IDF日志级别)
idf.py -p /dev/ttyUSB0 monitor
# 快捷键:Ctrl+] 退出,Ctrl+T Ctrl+R 软复位,Ctrl+T Ctrl+F 编译并烧录
若看到以下输出,说明环境搭建成功:
I (0) cpu_start: Starting scheduler on APP CPU.
I (356) main_task: Started on CPU0
I (356) main_task: Calling app_main()
I (356) main: Hello ESP32-S3! Chip revision: 0, CPU frequency: 240MHz
三、Wi-Fi配网方案深度对比:SmartConfig vs BLE vs SoftAP
3.1 三种方案架构对比

| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| SmartConfig | 手机App通过UDP广播/组播编码SSID/密码,ESP32-S3在Sniffer模式下解析 | 无需额外App,微信小程序支持 | 仅支持2.4GHz,5GHz路由器失效;部分路由器屏蔽组播 | 消费级产品,追求极简体验 |
| BLE Provisioning | 手机作为BLE Central,通过GATT写入SSID/密码,ESP32-S3作为Peripheral | 不依赖路由器,加密传输,可靠性最高 | 需安装专用App,配网时BLE功耗略高 | 工业级产品,安全要求高 |
| SoftAP | ESP32-S3开启AP模式,手机连接后通过HTTP/UDP配置 | 兼容性最好,无需协议依赖 | 用户需手动切换Wi-Fi,体验差;设备无法同时上网 | fallback方案,兼容性兜底 |
3.2 方案选型决策矩阵
量产建议:采用BLE为主 + SoftAP为fallback的混合策略。
- 首次配网:优先尝试BLE,3分钟内未成功自动切换SoftAP
- 已配网设备:上电时读取NVS中的Wi-Fi凭证,直接连接,跳过配网流程
- 配网失败:连续3次连接失败后,清除NVS并重新进入配网模式
四、BLE配网实战:从GATT服务到NVS持久化
4.1 BLE GATT服务架构设计

ESP-IDF提供了wifi_provisioning组件,封装了完整的配网管理器。但理解其底层GATT服务设计,对二次开发和问题排查至关重要。
自定义GATT服务定义(基于NimBLE协议栈):
/* wifi_prov_gatt.h — GATT服务UUID定义 */
#pragma once
#include <stdint.h>
/* Provisioning Service UUID (128-bit custom UUID) */
#define PROV_SERVICE_UUID 0xFF52
/* Characteristic UUIDs */
#define PROV_CHAR_SSID_UUID 0xFF53 /* Write: Wi-Fi SSID (max 32 bytes) */
#define PROV_CHAR_PASS_UUID 0xFF54 /* Write: Wi-Fi Password (max 64 bytes) */
#define PROV_CHAR_SEC_UUID 0xFF55 /* Write: Security type (WPA2/WPA3/OPEN) */
#define PROV_CHAR_STATUS_UUID 0xFF56 /* Notify: Provisioning status + IP address */
#define PROV_CHAR_CTRL_UUID 0xFF57 /* Write/Read: Control commands (start/stop/reset) */
/* Security type enumeration */
typedef enum {
WIFI_SEC_OPEN = 0,
WIFI_SEC_WPA2_PSK,
WIFI_SEC_WPA3_SAE,
WIFI_SEC_WPA2_WPA3_PSK,
} wifi_security_t;
/* Provisioning status structure (packed for BLE transmission) */
typedef struct __attribute__((packed)) {
uint8_t state; /* 0=idle, 1=connecting, 2=connected, 3=failed */
uint8_t error_code; /* Error code if failed */
uint32_t ip_addr; /* IP address (network byte order) */
uint8_t rssi; /* Wi-Fi signal strength */
} prov_status_t;
4.2 配网时序与状态机

状态机定义:
/* wifi_prov_manager.h — 配网管理器核心 */
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
typedef enum {
PROV_STATE_INIT = 0, /* 初始化完成,等待触发 */
PROV_STATE_BLE_ADVERTISING, /* BLE广播中,等待手机连接 */
PROV_STATE_BLE_CONNECTED, /* BLE已连接,等待凭证 */
PROV_STATE_WIFI_CREDENTIALS_RECEIVED, /* 收到SSID/密码 */
PROV_STATE_WIFI_CONNECTING, /* 正在连接Wi-Fi */
PROV_STATE_WIFI_CONNECTED, /* Wi-Fi连接成功,IP已分配 */
PROV_STATE_WIFI_CONNECTION_FAILED, /* 连接失败,准备重试或fallback */
PROV_STATE_NVS_SAVING, /* 保存凭证到NVS */
PROV_STATE_PROVISIONING_SUCCESS, /* 配网完成,停止BLE */
PROV_STATE_PROVISIONING_STOP, /* 停止状态机 */
} prov_state_t;
typedef struct {
char ssid[33]; /* Wi-Fi SSID + null terminator */
char password[65]; /* Wi-Fi password + null terminator */
wifi_security_t security;
uint8_t max_retry; /* 最大重试次数 */
uint32_t timeout_ms; /* 配网超时时间 */
} prov_config_t;
/* 事件回调接口 */
typedef void (*prov_event_cb_t)(prov_state_t state, const prov_status_t *status, void *user_ctx);
esp_err_t prov_manager_init(const prov_config_t *config, prov_event_cb_t cb, void *user_ctx);
esp_err_t prov_manager_start(void);
esp_err_t prov_manager_stop(void);
bool prov_manager_is_provisioned(void); /* 检查NVS中是否已有凭证 */
4.3 完整实现代码
4.3.1 主程序入口(main.c)
/* main.c — 配网主程序 */
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "wifi_prov_manager.h"
#include "led_indicator.h"
static const char *TAG = "main";
/* LED状态指示 */
#define LED_GPIO GPIO_NUM_2
void led_set_state(led_state_t state) {
/* 实现LED状态:快闪=配网中,慢闪=连接中,常亮=已连接,双闪=失败 */
led_indicator_set(LED_GPIO, state);
}
/* 配网状态回调 */
void prov_event_handler(prov_state_t state, const prov_status_t *status, void *user_ctx) {
(void)user_ctx;
switch (state) {
case PROV_STATE_BLE_ADVERTISING:
ESP_LOGI(TAG, "BLE advertising started, device name: ESP32-S3-Prov");
led_set_state(LED_STATE_FAST_BLINK);
break;
case PROV_STATE_BLE_CONNECTED:
ESP_LOGI(TAG, "BLE client connected");
led_set_state(LED_STATE_SLOW_BLINK);
break;
case PROV_STATE_WIFI_CONNECTING:
ESP_LOGI(TAG, "Connecting to Wi-Fi, SSID: %s", status->ssid);
break;
case PROV_STATE_WIFI_CONNECTED:
ESP_LOGI(TAG, "Wi-Fi connected! IP: " IPSTR, IP2STR(&status->ip_addr));
led_set_state(LED_STATE_ON);
break;
case PROV_STATE_WIFI_CONNECTION_FAILED:
ESP_LOGW(TAG, "Wi-Fi connection failed, error: %d, retrying...", status->error_code);
led_set_state(LED_STATE_DOUBLE_BLINK);
break;
case PROV_STATE_PROVISIONING_SUCCESS:
ESP_LOGI(TAG, "Provisioning completed successfully");
/* 3秒后停止BLE,进入正常工作模式 */
vTaskDelay(pdMS_TO_TICKS(3000));
prov_manager_stop();
break;
default:
break;
}
}
void app_main(void) {
/* 初始化NVS(必须最先执行,用于存储Wi-Fi凭证) */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
/* 初始化LED指示器 */
led_indicator_init(LED_GPIO);
/* 检查是否已配网 */
if (prov_manager_is_provisioned()) {
ESP_LOGI(TAG, "Wi-Fi credentials found in NVS, connecting directly...");
prov_config_t config = {
.max_retry = 3,
.timeout_ms = 30000,
};
/* 从NVS读取凭证并直接连接 */
prov_load_credentials_from_nvs(&config);
wifi_connect_direct(&config);
led_set_state(LED_STATE_ON);
} else {
ESP_LOGI(TAG, "No Wi-Fi credentials found, starting provisioning...");
/* 配置配网参数 */
prov_config_t prov_config = {
.max_retry = 3,
.timeout_ms = 180000, /* 3分钟超时后fallback到SoftAP */
};
/* 启动配网管理器 */
ESP_ERROR_CHECK(prov_manager_init(&prov_config, prov_event_handler, NULL));
ESP_ERROR_CHECK(prov_manager_start());
}
/* 主任务:可添加其他业务逻辑,如传感器采集、MQTT连接等 */
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
/* 心跳或状态上报 */
}
}
4.3.2 配网管理器核心实现(wifi_prov_manager.c)
/* wifi_prov_manager.c — 配网管理器实现 */
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_nimble_hci.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "nvs.h"
#include "wifi_prov_manager.h"
static const char *TAG = "prov_mgr";
/* NVS命名空间 */
#define NVS_NAMESPACE "wifi_prov"
#define NVS_KEY_SSID "ssid"
#define NVS_KEY_PASS "password"
#define NVS_KEY_SEC "security"
/* BLE连接状态 */
static bool s_ble_connected = false;
static uint16_t s_conn_handle = 0;
/* 配网状态 */
static prov_state_t s_state = PROV_STATE_INIT;
static prov_config_t s_config;
static prov_event_cb_t s_event_cb = NULL;
static void *s_user_ctx = NULL;
/* Wi-Fi凭证缓冲区 */
static char s_ssid[33] = {0};
static char s_password[65] = {0};
static wifi_security_t s_security = WIFI_SEC_WPA2_PSK;
/* 状态上报定时器 */
static TimerHandle_t s_status_timer = NULL;
/* GATT Characteristic Handles */
static uint16_t s_prov_svc_handle;
static uint16_t s_ssid_char_handle;
static uint16_t s_pass_char_handle;
static uint16_t s_sec_char_handle;
static uint16_t s_status_char_handle;
static uint16_t s_ctrl_char_handle;
/* 函数声明 */
static void prov_set_state(prov_state_t new_state);
static int prov_gatt_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg);
static void prov_wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data);
static void prov_status_timer_cb(TimerHandle_t xTimer);
/* GATT服务定义 */
static const struct ble_gatt_svc_def prov_gatt_services[] = {
{
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = BLE_UUID16_DECLARE(PROV_SERVICE_UUID),
.characteristics = (struct ble_gatt_chr_def[]) {
{
.uuid = BLE_UUID16_DECLARE(PROV_CHAR_SSID_UUID),
.access_cb = prov_gatt_access,
.flags = BLE_GATT_CHR_F_WRITE,
.val_handle = &s_ssid_char_handle,
},
{
.uuid = BLE_UUID16_DECLARE(PROV_CHAR_PASS_UUID),
.access_cb = prov_gatt_access,
.flags = BLE_GATT_CHR_F_WRITE,
.val_handle = &s_pass_char_handle,
},
{
.uuid = BLE_UUID16_DECLARE(PROV_CHAR_SEC_UUID),
.access_cb = prov_gatt_access,
.flags = BLE_GATT_CHR_F_WRITE,
.val_handle = &s_sec_char_handle,
},
{
.uuid = BLE_UUID16_DECLARE(PROV_CHAR_STATUS_UUID),
.access_cb = prov_gatt_access,
.flags = BLE_GATT_CHR_F_NOTIFY,
.val_handle = &s_status_char_handle,
},
{
.uuid = BLE_UUID16_DECLARE(PROV_CHAR_CTRL_UUID),
.access_cb = prov_gatt_access,
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_READ,
.val_handle = &s_ctrl_char_handle,
},
{ 0 } /* 结束标记 */
},
},
{ 0 } /* 结束标记 */
};
/* GATT访问回调 */
static int prov_gatt_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg) {
(void)arg;
if (attr_handle == s_ssid_char_handle) {
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
size_t len = OS_MBUF_PKTLEN(ctxt->om);
if (len >= sizeof(s_ssid)) len = sizeof(s_ssid) - 1;
ble_hs_mbuf_to_flat(ctxt->om, s_ssid, len, NULL);
s_ssid[len] = '\0';
ESP_LOGI(TAG, "Received SSID: %s", s_ssid);
}
return 0;
}
if (attr_handle == s_pass_char_handle) {
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
size_t len = OS_MBUF_PKTLEN(ctxt->om);
if (len >= sizeof(s_password)) len = sizeof(s_password) - 1;
ble_hs_mbuf_to_flat(ctxt->om, s_password, len, NULL);
s_password[len] = '\0';
ESP_LOGI(TAG, "Received password (length: %d)", (int)strlen(s_password));
}
return 0;
}
if (attr_handle == s_sec_char_handle) {
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
uint8_t sec_type;
ble_hs_mbuf_to_flat(ctxt->om, &sec_type, 1, NULL);
s_security = (wifi_security_t)sec_type;
ESP_LOGI(TAG, "Received security type: %d", sec_type);
/* 收到完整凭证,触发Wi-Fi连接 */
if (strlen(s_ssid) > 0 && s_state == PROV_STATE_BLE_CONNECTED) {
prov_set_state(PROV_STATE_WIFI_CREDENTIALS_RECEIVED);
prov_start_wifi_connection();
}
}
return 0;
}
if (attr_handle == s_status_char_handle) {
/* Notify操作由定时器触发,此处处理CCCD订阅 */
return 0;
}
if (attr_handle == s_ctrl_char_handle) {
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
uint8_t cmd;
ble_hs_mbuf_to_flat(ctxt->om, &cmd, 1, NULL);
switch (cmd) {
case 0x01: /* Start provisioning */
ESP_LOGI(TAG, "Control: Start provisioning");
break;
case 0x02: /* Stop provisioning */
ESP_LOGI(TAG, "Control: Stop provisioning");
prov_manager_stop();
break;
case 0x03: /* Reset credentials */
ESP_LOGI(TAG, "Control: Reset credentials");
prov_erase_credentials();
break;
}
}
return 0;
}
return BLE_ATT_ERR_UNLIKELY;
}
/* BLE GAP事件处理 */
static int prov_gap_event(struct ble_gap_event *event, void *arg) {
(void)arg;
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status == 0) {
s_ble_connected = true;
s_conn_handle = event->connect.conn_handle;
ESP_LOGI(TAG, "BLE connected, handle=%d", s_conn_handle);
prov_set_state(PROV_STATE_BLE_CONNECTED);
} else {
ESP_LOGE(TAG, "BLE connection failed, status=%d", event->connect.status);
s_ble_connected = false;
}
return 0;
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI(TAG, "BLE disconnected, reason=%d", event->disconnect.reason);
s_ble_connected = false;
s_conn_handle = 0;
if (s_state != PROV_STATE_PROVISIONING_SUCCESS) {
/* 如果配网未完成,重新广播 */
prov_start_ble_advertising();
}
return 0;
case BLE_GAP_EVENT_ADV_COMPLETE:
ESP_LOGI(TAG, "Advertising completed, restarting...");
prov_start_ble_advertising();
return 0;
default:
return 0;
}
}
/* 启动BLE广播 */
static void prov_start_ble_advertising(void) {
struct ble_gap_adv_params adv_params = {0};
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
/* 设备名称:ESP32-S3-Prov-XXXX(后4位为MAC地址) */
uint8_t addr[6];
ble_hs_id_copy_addr(BLE_ADDR_PUBLIC, addr, NULL);
char dev_name[32];
snprintf(dev_name, sizeof(dev_name), "ESP32-S3-Prov-%02X%02X", addr[4], addr[5]);
ble_svc_gap_device_name_set(dev_name);
/* 广播数据:包含Service UUID */
struct ble_hs_adv_fields fields = {0};
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.uuids16 = (ble_uuid16_t[]){ { PROV_SERVICE_UUID } };
fields.num_uuids16 = 1;
fields.uuids16_is_complete = 1;
int rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
ESP_LOGE(TAG, "Failed to set adv fields, rc=%d", rc);
return;
}
rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER, &adv_params,
prov_gap_event, NULL);
if (rc == 0) {
prov_set_state(PROV_STATE_BLE_ADVERTISING);
ESP_LOGI(TAG, "BLE advertising started: %s", dev_name);
} else {
ESP_LOGE(TAG, "Failed to start advertising, rc=%d", rc);
}
}
/* Wi-Fi事件处理 */
static void prov_wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) {
(void)arg;
if (event_base == WIFI_EVENT) {
switch (event_id) {
case WIFI_EVENT_STA_START:
ESP_LOGI(TAG, "Wi-Fi STA started");
break;
case WIFI_EVENT_STA_CONNECTED:
ESP_LOGI(TAG, "Wi-Fi STA connected");
break;
case WIFI_EVENT_STA_DISCONNECTED:
ESP_LOGW(TAG, "Wi-Fi STA disconnected");
if (s_state == PROV_STATE_WIFI_CONNECTING) {
/* 连接失败,触发重试或fallback */
prov_set_state(PROV_STATE_WIFI_CONNECTION_FAILED);
prov_handle_connection_failure();
}
break;
}
} else if (event_base == IP_EVENT) {
switch (event_id) {
case IP_EVENT_STA_GOT_IP: {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
prov_status_t status = {
.state = 2, /* connected */
.ip_addr = event->ip_info.ip.addr,
.rssi = 0, /* 可在后续读取 */
};
prov_notify_status(&status);
prov_set_state(PROV_STATE_WIFI_CONNECTED);
/* 保存凭证到NVS */
prov_save_credentials_to_nvs();
prov_set_state(PROV_STATE_NVS_SAVING);
break;
}
}
}
}
/* 启动Wi-Fi连接 */
static void prov_start_wifi_connection(void) {
ESP_LOGI(TAG, "Starting Wi-Fi connection to SSID: %s", s_ssid);
wifi_config_t wifi_config = {0};
strncpy((char *)wifi_config.sta.ssid, s_ssid, sizeof(wifi_config.sta.ssid));
strncpy((char *)wifi_config.sta.password, s_password, sizeof(wifi_config.sta.password));
/* 根据安全类型设置 */
switch (s_security) {
case WIFI_SEC_OPEN:
wifi_config.sta.threshold.authmode = WIFI_AUTH_OPEN;
break;
case WIFI_SEC_WPA2_PSK:
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
break;
case WIFI_SEC_WPA3_SAE:
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK;
break;
default:
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
break;
}
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_connect());
prov_set_state(PROV_STATE_WIFI_CONNECTING);
/* 启动状态上报定时器 */
if (s_status_timer == NULL) {
s_status_timer = xTimerCreate("prov_status", pdMS_TO_TICKS(1000),
pdTRUE, NULL, prov_status_timer_cb);
}
xTimerStart(s_status_timer, 0);
}
/* 状态上报定时器回调 */
static void prov_status_timer_cb(TimerHandle_t xTimer) {
(void)xTimer;
if (!s_ble_connected) return;
/* 读取Wi-Fi状态 */
wifi_ap_record_t ap_info;
esp_err_t ret = esp_wifi_sta_get_ap_info(&ap_info);
prov_status_t status = {0};
if (ret == ESP_OK) {
status.state = (s_state == PROV_STATE_WIFI_CONNECTED) ? 2 : 1;
status.rssi = ap_info.rssi;
} else {
status.state = 1; /* connecting */
}
prov_notify_status(&status);
}
/* 通过BLE Notify上报状态 */
static void prov_notify_status(const prov_status_t *status) {
if (!s_ble_connected || s_conn_handle == 0) return;
struct os_mbuf *om = ble_hs_mbuf_from_flat(status, sizeof(prov_status_t));
if (om != NULL) {
ble_gatts_notify_custom(s_conn_handle, s_status_char_handle, om);
}
}
/* 保存凭证到NVS */
static void prov_save_credentials_to_nvs(void) {
nvs_handle_t nvs_handle;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "NVS open failed: %s", esp_err_to_name(err));
return;
}
nvs_set_str(nvs_handle, NVS_KEY_SSID, s_ssid);
nvs_set_str(nvs_handle, NVS_KEY_PASS, s_password);
nvs_set_u8(nvs_handle, NVS_KEY_SEC, (uint8_t)s_security);
nvs_commit(nvs_handle);
nvs_close(nvs_handle);
ESP_LOGI(TAG, "Wi-Fi credentials saved to NVS");
prov_set_state(PROV_STATE_PROVISIONING_SUCCESS);
}
/* 从NVS加载凭证 */
bool prov_load_credentials_from_nvs(prov_config_t *config) {
nvs_handle_t nvs_handle;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle);
if (err != ESP_OK) return false;
size_t len = sizeof(config->ssid);
err = nvs_get_str(nvs_handle, NVS_KEY_SSID, config->ssid, &len);
if (err != ESP_OK) {
nvs_close(nvs_handle);
return false;
}
len = sizeof(config->password);
err = nvs_get_str(nvs_handle, NVS_KEY_PASS, config->password, &len);
if (err != ESP_OK) {
nvs_close(nvs_handle);
return false;
}
uint8_t sec = 0;
nvs_get_u8(nvs_handle, NVS_KEY_SEC, &sec);
config->security = (wifi_security_t)sec;
nvs_close(nvs_handle);
ESP_LOGI(TAG, "Loaded credentials from NVS: SSID=%s", config->ssid);
return true;
}
/* 擦除NVS凭证 */
void prov_erase_credentials(void) {
nvs_handle_t nvs_handle;
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle) == ESP_OK) {
nvs_erase_key(nvs_handle, NVS_KEY_SSID);
nvs_erase_key(nvs_handle, NVS_KEY_PASS);
nvs_erase_key(nvs_handle, NVS_KEY_SEC);
nvs_commit(nvs_handle);
nvs_close(nvs_handle);
ESP_LOGI(TAG, "NVS credentials erased");
}
}
/* 检查是否已配网 */
bool prov_manager_is_provisioned(void) {
prov_config_t config;
return prov_load_credentials_from_nvs(&config);
}
/* 设置状态并触发回调 */
static void prov_set_state(prov_state_t new_state) {
s_state = new_state;
if (s_event_cb != NULL) {
prov_status_t status = {0};
s_event_cb(new_state, &status, s_user_ctx);
}
}
/* 初始化配网管理器 */
esp_err_t prov_manager_init(const prov_config_t *config, prov_event_cb_t cb, void *user_ctx) {
memcpy(&s_config, config, sizeof(prov_config_t));
s_event_cb = cb;
s_user_ctx = user_ctx;
/* 初始化Wi-Fi */
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
prov_wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
prov_wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
/* 初始化NimBLE */
ESP_ERROR_CHECK(esp_nimble_hci_and_controller_init());
nimble_port_init();
ble_hs_cfg.sync_cb = prov_ble_sync_cb;
ble_hs_cfg.reset_cb = prov_ble_reset_cb;
/* 注册GATT服务 */
ble_gatts_count_cfg(prov_gatt_services);
ble_gatts_add_svcs(prov_gatt_services);
nimble_port_freertos_init(prov_ble_host_task);
ESP_LOGI(TAG, "Provisioning manager initialized");
return ESP_OK;
}
/* 启动配网流程 */
esp_err_t prov_manager_start(void) {
prov_start_ble_advertising();
return ESP_OK;
}
/* 停止配网流程 */
esp_err_t prov_manager_stop(void) {
if (s_ble_connected) {
ble_gap_terminate(s_conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}
ble_gap_adv_stop();
if (s_status_timer != NULL) {
xTimerStop(s_status_timer, 0);
}
prov_set_state(PROV_STATE_PROVISIONING_STOP);
ESP_LOGI(TAG, "Provisioning stopped");
return ESP_OK;
}
/* BLE同步回调 */
static void prov_ble_sync_cb(void) {
ESP_LOGI(TAG, "BLE host synchronized");
}
/* BLE复位回调 */
static void prov_ble_reset_cb(int reason) {
ESP_LOGI(TAG, "BLE host reset, reason=%d", reason);
}
/* BLE Host任务 */
static void prov_ble_host_task(void *param) {
(void)param;
nimble_port_run();
nimble_port_freertos_deinit();
}
4.4 手机端配网App交互流程
ESP BLE Prov App(乐鑫官方提供iOS/Android版本)的交互流程:
- 扫描:App扫描BLE广播,过滤Service UUID为
0xFF52的设备 - 连接:点击设备名称,建立BLE连接并配对(可选)
- 发送凭证:通过GATT Write依次写入SSID、Password、Security Type
- 等待状态:订阅Status Characteristic的Notify,实时接收连接状态
- 验证成功:收到
state=2(已连接)+ IP地址后,显示配网成功
微信小程序替代方案(无需安装App):
// 微信小程序BLE配网核心代码片段
wx.openBluetoothAdapter({
success: function() {
wx.startBluetoothDevicesDiscovery({
services: ['0000FF52-0000-1000-8000-00805F9B34FB'],
success: function(res) {
// 发现设备后连接并写入Wi-Fi凭证
}
});
}
});
五、量产级优化:从Demo到产品
5.1 安全加固
| 安全措施 | 实现方式 | 必要性 |
|---|---|---|
| BLE加密 | 启用LE Secure Connections(Numeric Comparison或Passkey Entry) | 高:防止中间人攻击 |
| Proof of Possession (PoP) | 设备打印随机6位码,App输入后验证 | 中:防止恶意配网 |
| AES-128加密凭证 | 使用设备唯一密钥(来自eFuse或工厂烧录)加密NVS存储 | 高:防止物理提取密码 |
| Wi-Fi证书验证 | 连接成功后验证服务器证书(TLS/SSL) | 高:防止钓鱼AP |
5.2 工厂测试与MAC管理
/* factory_test.c — 工厂测试模式 */
void factory_test_mode(void) {
/* 进入工厂测试模式:不连接NVS,使用固定测试Wi-Fi */
wifi_config_t test_config = {
.sta = {
.ssid = "FACTORY_TEST_AP",
.password = "test123456",
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
},
};
esp_wifi_set_config(WIFI_IF_STA, &test_config);
esp_wifi_connect();
/* 测试项:Wi-Fi连接、BLE广播、传感器读取、电流测量 */
/* 通过串口上报测试结果,工厂MES系统采集 */
}
5.3 功耗优化
| 场景 | 功耗策略 | 电流 |
|---|---|---|
| 未配网(等待配网) | BLE广播间隔500ms + CPU 80MHz | ~15mA |
| 配网中(BLE连接) | BLE连接间隔30ms + CPU 160MHz | ~45mA |
| 已配网(正常工作) | 关闭BLE,Wi-Fi DTIM3省电模式 | ~3mA |
| 深度睡眠 | 仅RTC定时器唤醒,Wi-Fi断开 | ~10μA |
六、常见问题排查指南
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
idf.py flash报Permission denied |
USB权限不足 | sudo usermod -aG dialout $USER后重新登录 |
| BLE广播无法发现 | NimBLE未初始化或广播数据错误 | 检查ble_gap_adv_set_fields返回值 |
| Wi-Fi连接失败(密码正确) | 安全类型不匹配(WPA2 vs WPA3) | 确认路由器加密方式与s_security一致 |
| NVS读取凭证失败 | 首次烧录或NVS损坏 | 调用nvs_flash_erase()后重新配网 |
| 配网成功后无法自动回连 | NVS保存失败或读取逻辑错误 | 检查prov_save/load_credentials_from_nvs返回值 |
| 功耗异常偏高 | BLE未正确关闭或Wi-Fi未进入省电模式 | 测量各状态下电流,对比上表预期值 |
七、总结
从ESP-IDF环境搭建的12个踩坑点,到BLE配网的GATT服务设计、状态机实现、NVS持久化,再到量产级的安全加固与功耗优化——Wi-Fi配网这个"简单"功能,实际上涵盖了嵌入式开发的完整技术栈:
- 系统层:FreeRTOS任务调度、事件循环、定时器管理
- 网络层:Wi-Fi STA模式、DHCP、TCP/IP协议栈
- 蓝牙层:NimBLE GAP/GATT、广播/连接/Notify机制
- 存储层:NVS分区管理、磨损均衡、加密存储
- 安全层:BLE配对加密、PoP验证、AES-128凭证保护
- 工程层:状态机设计、错误处理、fallback策略、工厂测试
本文完整工程代码已按MIT协议开源,包含ESP-IDF v5.2.1工程模板、微信小程序配网Demo、以及工厂测试脚本。在物联网开发中,真正的功力不在于调用了wifi_prov_mgr这个高级API,而在于理解其底层GATT服务设计、Wi-Fi事件状态机、以及NVS存储的可靠性保障。
转载自:https://blog.csdn.net/u014727709/article/details/162200648
欢迎 👍点赞✍评论⭐收藏,欢迎指正
更多推荐

所有评论(0)