在这里插入图片描述

每日一句正能量

人生绝大多数困境并非能力不足,而是思维没跟上。
能力像工具,思维像使用说明书。很多时候我们拿着锤子找钉子,却没意识到门是推开的,不是砸开的。换个角度,困境可能只是伪装的台阶。

导读

谁说嵌入式只是调包和焊板子?一个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的部分构建脚本仍依赖它。解决方案:使用pyenvvenv创建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'

踩坑记录2export.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,配置步骤如下:

  1. 安装插件:Espressif IDF(由乐鑫官方维护)
  2. Ctrl+Shift+PESP-IDF: Configure ESP-IDF Extension
  3. 选择Use existing ESP-IDF setup,指定~/esp-idf路径
  4. 设置目标芯片:ESP32-S3
  5. 配置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版本)的交互流程:

  1. 扫描:App扫描BLE广播,过滤Service UUID为0xFF52的设备
  2. 连接:点击设备名称,建立BLE连接并配对(可选)
  3. 发送凭证:通过GATT Write依次写入SSID、Password、Security Type
  4. 等待状态:订阅Status Characteristic的Notify,实时接收连接状态
  5. 验证成功:收到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
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐