配网:让用户自己给设备设 WiFi
- 理解为什么真实产品必须做配网,不能把 WiFi 写死在
- 用 ESP-IDF 的 esp_smartconfig 实现"手机广播凭据→设备嗅探解码→自动联网"的完整流程
- 在 SC_EVENT 回调里把收到的 ssid/password 填进 wifi_config_t、发起连接
- 知道 SmartConfig / SoftAP / BLE 三种配网怎么选,以及连不上时怎么排查
上一节你让 ESP32-S3 连上了 WiFi,名字和密码是写死在 #define WIFI_SSID "你的WiFi名" 里的。这在你自己桌面上跑得很爽,但只要你把这块板子送给别人,或者你自己换了路由器、改了密码,麻烦就来了——你得插上数据线、改两行字符串、重新 idf.py build flash。一台设备这么干还行,十台呢?卖给客户的那台,难道让客户也装一套 ESP-IDF?
这就是配网要解决的问题。配网,就是让用户在不碰代码的情况下,把自己家的 WiFi 账号密码告诉设备。市面上每一个智能插座、智能灯泡、扫地机器人,开箱第一件事都是配网——你用 App 把家里 WiFi 填进去,设备就记住了。这是产品从"我自己玩的东西"变成"能交付的东西"的一道必修关。
读这篇前,请先跑通让 ESP32-S3 连上 WiFi。你需要已经理解那套 wifi_init_sta() 骨架——esp_netif_init / esp_event_loop_create_default / esp_wifi_init、事件回调、FreeRTOS 事件组怎么等。配网本质上就是把"密码从哪来"这件事,从 #define 改成用户输入,WiFi 连接那套骨架完全复用上一节的,本篇只在它外面包一层"先从空中拿到密码"。
配网是什么,三条主流路线
设备出厂时不知道用户家的 WiFi 叫什么、密码是多少。配网就是设备第一次上电(或者连不上旧网络)时,开放一个临时通道让用户输入这些信息,输入完存进 NVS,以后自动连。
ESP-IDF 圈子里主流是三条路,各有取舍:
路线一:SmartConfig(乐鑫叫 ESP-TOUCH)。 设备进入"嗅探"模式(射频混杂模式,把空中所有 WiFi 包都收下来),用户在手机 App 里输入自家 WiFi 密码,App 把密码按特定规则编码进一串 UDP 广播/组播包,设备从空中"听"到这些包、解出 SSID 和密码。好处是用户手机全程不用切换 WiFi,体验顺。坏处也实在:必须配一个专门的 App(乐鑫官方 App 或微信小程序),而且成功率受路由器影响大——有些路由器对广播包做了限制、信号弱时容易失败。它适合已经有 App 生态的成熟产品。本篇正文以 SmartConfig 给完整代码。
路线二:SoftAP 配网。 设备自己开一个 WiFi 热点(AP 模式,就是上一节末尾提的 WIFI_MODE_AP),用户手机连上这个热点,在网页或小程序里选自家 WiFi、填密码、点保存,设备就连上了。好处是不依赖广播包、成功率高、调试直观;代价是用户得手动切一次 WiFi。ESP-IDF 官方的 wifi_provisioning 组件就支持这种方式。
路线三:BLE 配网。 走蓝牙把凭据传给设备,不占 WiFi 信道、抗干扰好,但要搭蓝牙协议栈、App 也更重。wifi_provisioning 同样支持它。
我的建议: 如果你想要最省心、成功率最稳的方案,用官方 wifi_provisioning(SoftAP/BLE),它把存凭据、加密、超时这些都封装好了——这块够单独一篇,上一节让 ESP32-S3 连上 WiFi末尾已经指过路。本篇专讲 SmartConfig,因为它最能体现"无切网"的丝滑体验,而且代码量小、正好复用你刚学的事件驱动骨架,把"配网怎么挂到 WiFi 事件上"这件事讲透。
第一步:先把整段 SmartConfig 代码跑通
SmartConfig 的逻辑是这样的:照常初始化 WiFi(STA 模式)→ 在 WIFI_EVENT_STA_START 事件里不直接连,而是起一个 smartconfig 任务进入嗅探 → 手机 App 一发凭据,系统抛 SC_EVENT_GOT_SSID_PSWD 事件,你在回调里把收到的 ssid/password 填进 wifi_config_t、esp_wifi_connect() → 连上拿到 IP,再 esp_smartconfig_stop() 收尾。
下面这段是完整可烧录的程序,直接放进工程的 main/main.c:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_smartconfig.h" // SmartConfig 的全部 API 都在这
static const char *TAG = "smartconfig";
static EventGroupHandle_t s_wifi_event_group;
#define CONNECTED_BIT BIT0 // 拿到 IP
#define ESPTOUCH_DONE_BIT BIT1 // 配网整套流程结束
// 嗅探任务:进入 SmartConfig,然后等"配网完成"的信号灯再退出
static void smartconfig_task(void *parm) {
EventBits_t bits;
ESP_ERROR_CHECK(esp_smartconfig_set_type(SC_TYPE_ESPTOUCH)); // 用 ESP-TOUCH 协议
smartconfig_start_config_t cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_smartconfig_start(&cfg)); // 开始嗅探
while (1) {
// 阻塞等:连上了 且 收到了 SEND_ACK_DONE,才算彻底完成
bits = xEventGroupWaitBits(s_wifi_event_group,
CONNECTED_BIT | ESPTOUCH_DONE_BIT,
pdTRUE, pdFALSE, portMAX_DELAY);
if (bits & CONNECTED_BIT) ESP_LOGI(TAG, "WiFi 已连上");
if (bits & ESPTOUCH_DONE_BIT) {
ESP_LOGI(TAG, "SmartConfig 完成,停止嗅探");
esp_smartconfig_stop(); // 收尾:关掉混杂模式、释放资源
vTaskDelete(NULL); // 任务自己退出
}
}
}
static void event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) {
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
// 注意:和上一节不同,这里不直接 esp_wifi_connect(),而是起嗅探任务等用户发凭据
xTaskCreate(smartconfig_task, "sc", 4096, NULL, 3, NULL);
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
esp_wifi_connect(); // 断了重连(凭据已在 wifi_config 里)
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *e = (ip_event_got_ip_t *) data;
ESP_LOGI(TAG, "拿到 IP: " IPSTR, IP2STR(&e->ip_info.ip)); // 成功标志
xEventGroupSetBits(s_wifi_event_group, CONNECTED_BIT);
} else if (base == SC_EVENT && id == SC_EVENT_SCAN_DONE) {
ESP_LOGI(TAG, "嗅探中,等手机 App 发凭据…");
} else if (base == SC_EVENT && id == SC_EVENT_GOT_SSID_PSWD) {
// 核心:从事件数据里取出手机广播过来的 SSID / 密码
smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t *) data;
wifi_config_t wc = { 0 };
memcpy(wc.sta.ssid, evt->ssid, sizeof(wc.sta.ssid));
memcpy(wc.sta.password, evt->password, sizeof(wc.sta.password));
ESP_LOGI(TAG, "收到凭据,SSID=%s,去连接", wc.sta.ssid);
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wc)); // 凭据落进 wifi_config
esp_wifi_connect(); // 发起连接
} else if (base == SC_EVENT && id == SC_EVENT_SEND_ACK_DONE) {
// 设备已把"我连上了"回执发给手机 App,配网到此结束
xEventGroupSetBits(s_wifi_event_group, ESPTOUCH_DONE_BIT);
}
}
static void wifi_init_sta_smartconfig(void) {
s_wifi_event_group = xEventGroupCreate();
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));
// 比上一节多注册一类事件:SC_EVENT(SmartConfig 自己的事件源)
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(SC_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start()); // 注意:这里没 set_config,凭据等用户发
}
void app_main(void) {
ESP_ERROR_CHECK(nvs_flash_init()); // WiFi 射频校准数据存 NVS,连网前必做
wifi_init_sta_smartconfig();
}
在工程目录下编译、烧录、看日志:
idf.py build flash monitor
(第一次用要先 idf.py set-target esp32s3。按 Ctrl + ] 退出监视。)
你应该看到什么
光烧固件还不够,SmartConfig 必须手机端配合——你得装乐鑫官方的 ESPTOUCH App(iOS / 安卓应用商店搜 "EspTouch",微信里也有对应小程序)。把手机连上你家 2.4GHz WiFi,打开 App,它会自动带出当前 WiFi 名,你填上密码,点"确认/开始配网"。
正常流程下,idf.py monitor 的日志会按顺序滚出:
I (1500) smartconfig: 嗅探中,等手机 App 发凭据…
I (8200) smartconfig: 收到凭据,SSID=MyWiFi,去连接
I (9100) smartconfig: 拿到 IP: 192.168.1.123
I (9300) smartconfig: WiFi 已连上
I (9400) smartconfig: SmartConfig 完成,停止嗅探
- 一上电,
WIFI_EVENT_STA_START触发,起了嗅探任务,esp_smartconfig_start让设备进入混杂模式听空中的包——这时它在"等手机说话"。 - 你在 ESPTOUCH App 点配网后,设备从广播包里解出 SSID 和密码,抛
SC_EVENT_GOT_SSID_PSWD,回调里把凭据填进wifi_config_t并发起连接。 - 路由器分了 IP,
IP_EVENT_STA_GOT_IP打出拿到 IP: 192.168.x.x——这串数字就是成功的标志。 - 设备把回执发回手机(
SC_EVENT_SEND_ACK_DONE),点亮ESPTOUCH_DONE_BIT,嗅探任务esp_smartconfig_stop()收尾、自删。手机 App 这时也会显示"配网成功"。
看到串口打出你家网段的 IP、手机 App 显示成功,就成了。
这版代码为了聚焦主线,没把凭据存进 NVS——一断电就忘了,下次开机又得重配。真做产品时,esp_wifi_set_config 设的凭据其实已经被 WiFi 驱动写进 NVS 了(前提是 esp_wifi_set_storage 用默认的 WIFI_STORAGE_FLASH),下次开机可以先尝试用存的凭据直连、连不上再进 SmartConfig。这一层"先试旧的、不行再配网"的判断,留作下面的动手挑战。
第二步:把和上一节的差异讲透
这段代码看着比上一节长,但骨架是同一套——esp_netif_init / esp_event_loop_create_default / esp_wifi_init / WIFI_MODE_STA / esp_wifi_start 一字没变。真正的差异只有三处,吃透这三处就懂了 SmartConfig 是怎么"挂"到你已有的事件驱动框架上的。
差异一:STA_START 里不直接连,而是起嗅探
上一节 WIFI_EVENT_STA_START 来了直接 esp_wifi_connect(),因为密码写死在 #define 里、esp_wifi_set_config 早就设好了。但 SmartConfig 一开始根本没有密码——密码要等用户从手机发过来。所以这里把"连接"延后:先起一个 smartconfig_task 进入嗅探,等真拿到凭据了再连。wifi_init_sta_smartconfig() 里也没有 esp_wifi_set_config,就是这个道理。
差异二:多了一个事件源 SC_EVENT
上一节你只注册了 WIFI_EVENT 和 IP_EVENT 两个事件源。SmartConfig 自己是第三个事件源 SC_EVENT,所以代码里多了一行注册:
esp_event_handler_instance_register(SC_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL);
注册完,SmartConfig 过程中的几个关键节点就会像 WiFi 事件一样投递到你同一个 event_handler 里。重点接住两个:SC_EVENT_GOT_SSID_PSWD(收到凭据)和 SC_EVENT_SEND_ACK_DONE(配网收尾)。这正是上一节讲的"事件循环 + 回调"范式的复用——SmartConfig 没有发明新机制,它只是往你已有的事件循环里多塞了一类事件。
差异三:核心动作——把广播来的凭据填进 wifi_config_t
整篇最关键的就是 SC_EVENT_GOT_SSID_PSWD 这个分支:
smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t *) data;
wifi_config_t wc = { 0 };
memcpy(wc.sta.ssid, evt->ssid, sizeof(wc.sta.ssid));
memcpy(wc.sta.password, evt->password, sizeof(wc.sta.password));
esp_wifi_set_config(WIFI_IF_STA, &wc);
esp_wifi_connect();
手机广播过来的 SSID 和密码,被系统解码后放进了事件数据 smartconfig_event_got_ssid_pswd_t 这个结构体的 ssid / password 字段里。你要做的就是把它们 memcpy 进一个 wifi_config_t,然后走和上一节完全一样的两步——esp_wifi_set_config 把凭据交给 WiFi 驱动、esp_wifi_connect 发起连接。从这一行往后,连接、拿 IP、断线重连的逻辑就和上一节一模一样了。SmartConfig 的全部价值,就是替你把这两个字符串从空中"捡"回来,省掉了 #define。
嗅探为什么能"听"到密码——原理一层
你可能会问:手机和 ESP32-S3 还没连上同一个网,密码是怎么传过去的?答案藏在 ESP-TOUCH 的设计里:手机 App 把密码编码进一连串 UDP 广播/组播包的"长度字段"里(包的内容是加密的看不懂,但每个包有多长是公开信息),按特定规律连续发出。ESP32-S3 这边开了混杂模式(promiscuous mode,把空中所有 802.11 包不管是不是发给自己的都收下来),它不关心包里装了啥,只数每个包的长度,按 App 约定的规律把这串长度"翻译"回原始的 SSID 和密码。
这就是为什么 SmartConfig 不需要先连网就能传密码——它走的是所有人都能收到的广播信道,靠包长度这个"边信道"来夹带信息。也正因为如此,它对路由器的广播策略很敏感:路由器要是限制了组播/广播转发,或者信号弱导致丢包,这串"长度密码"就拼不全,配网就失败。这是 SmartConfig 成功率不如 SoftAP 稳的根因。
三种配网怎么选
| 方式 | 用户操作 | 依赖 | 成功率 | 适合 |
|---|---|---|---|---|
| SmartConfig(本篇) | 装 App、填密码、点配网,不用切 WiFi | 专门 App / 微信小程序;混杂模式 | 中(受路由器广播策略影响) | 已有 App 生态、追求"无切网"丝滑体验 |
SoftAP(wifi_provisioning) |
切到设备热点、网页/小程序填密码 | AP 模式;网页或配网 SDK | 高(不靠广播包) | 入门、中小项目、要稳的场合 |
BLE(wifi_provisioning) |
App 走蓝牙配 | 蓝牙协议栈;更重的 App | 高 | 抗 WiFi 干扰、已有蓝牙的产品 |
一句话决策:想稳、想省事,用官方 wifi_provisioning 的 SoftAP(上一节让 ESP32-S3 连上 WiFi末尾指过路);想要手机不切网的丝滑体验、且能接受装个 App,用本篇的 SmartConfig;做了蓝牙的产品顺手用 BLE 配网。三者底层都还是把凭据填进 wifi_config_t 那一套,区别只在"凭据从哪个通道进来"。
故障排查表
SmartConfig 的坑大半在"环境"而非代码,按这张表从上往下查:
| 现象 | 大概率原因 | 怎么办 |
|---|---|---|
| App 一直转圈、设备日志停在"嗅探中" | 手机连的是 5GHz 网 | SmartConfig 必须手机和设备都在 2.4GHz,把手机切到 2.4G 那个 SSID 再配 |
| 配网偶尔成功、偶尔超时 | 路由器限制了组播/广播转发,或信号弱丢包 | 离路由器近点;进路由器后台关掉"组播过滤""IGMP Snooping"等限制;多试几次 |
日志收到凭据但连不上、刷 STA_DISCONNECTED |
密码解错(信号差时长度被丢包打乱)或确实密码错 | 凑近路由器重配;在 DISCONNECTED 分支把 reason code 打出来(见上一节)核实 |
esp_wifi_init 直接崩 |
没先 nvs_flash_init() |
确认 app_main 第一句是 NVS 初始化 |
| App 连不上设备 / 找不到设备 | 手机和设备不在同一个 2.4G 网,或 App 权限没给 | 确认手机连的就是要配的那个 2.4G WiFi;安卓给 App 定位权限(扫 WiFi 需要) |
| 配过一次,重启后又要重配 | 本篇代码没做"先试旧凭据"逻辑 | 见下方动手挑战,开机先尝试用 NVS 里的凭据直连 |
SmartConfig 最大的隐形门槛是 2.4GHz:ESP32-S3 射频只支持 2.4G,而且嗅探也只在 2.4G 信道上进行。只要你手机连的是 5GHz(现在很多手机在双频合一的网里默认挑 5G),设备就永远"听"不到那串广播包,表现就是 App 一直转圈、设备一直嗅探不动。配网前务必确认手机当前连的就是 2.4G 那个网。
变体:超时与回退
本篇主线一直在 portMAX_DELAY 无限等用户配网,生产里得加超时,免得设备永远卡在嗅探:
// 在 smartconfig_task 里把 portMAX_DELAY 换成有限超时,例如 60 秒
bits = xEventGroupWaitBits(s_wifi_event_group,
CONNECTED_BIT | ESPTOUCH_DONE_BIT,
pdTRUE, pdFALSE, pdMS_TO_TICKS(60 * 1000));
if (bits == 0) { // 超时,一个 bit 都没亮
ESP_LOGW(TAG, "配网超时,停止嗅探");
esp_smartconfig_stop();
// 这里可选择:重启重来、或回退到 SoftAP 配网、或点个红灯提示用户
esp_restart();
}
成熟产品常做双通道兜底:先开 SmartConfig 等 60 秒,用户没配上就自动切到 SoftAP 配网(开个热点让用户连),覆盖"手机没装 App"或"路由器广播被限"的情况。这正好把上面那张三选一的表用起来——两条路并联,哪条通走哪条。
动手挑战
把本篇代码改成真正能交付的样子:
- 先试旧凭据再配网:在
app_main里esp_wifi_start()之后,先用esp_wifi_get_config(WIFI_IF_STA, &wc)读 NVS 里有没有存过 SSID,有就直接esp_wifi_connect()、跳过 SmartConfig;只有读到空 SSID(或连了几次失败)才起嗅探任务。验证:配网成功后断电重启,确认它直接连上、不再要你掏手机配。 - 加配网超时:用上面变体里的 60 秒超时替换
portMAX_DELAY,故意不去 App 点配网,看它 60 秒后是不是按你写的打日志、然后重启或回退。 - 打 reason code:在
WIFI_EVENT_STA_DISCONNECTED分支里把断开原因码打出来(写法见上一节),故意在 App 里填错一位密码,看设备收到凭据后连不上时报的是不是15(握手超时)。 - 进阶:把
SC_TYPE_ESPTOUCH换成SC_TYPE_ESPTOUCH_V2,对照乐鑫文档看 V2 多了什么(更快、支持自定义数据),用 App 的 V2 模式重配一次。
跑通第 1 条,你的设备就真正"配一次、以后自动连"了——这是它具备产品雏形的标志。
本篇代码为参考实现,需结合你所用的最新 ESP-IDF 文档自校,尤其是
esp_smartconfig/esp_wifi的 API 细节、smartconfig_event_got_ssid_pswd_t字段、SC_TYPE 取值随版本可能微调,以官方 esp_smartconfig 文档为准。
小结 · 你现在掌握了什么
- 你能用 ESP-IDF 的
esp_smartconfig做一键配网:esp_smartconfig_set_type选协议、esp_smartconfig_start进嗅探、在SC_EVENT_GOT_SSID_PSWD回调里把手机广播来的凭据填进wifi_config_t并esp_wifi_connect、SC_EVENT_SEND_ACK_DONE后esp_smartconfig_stop收尾。 - 你看清了它和上一节的差异只有三处:
STA_START里不直接连而是起嗅探、多注册一个SC_EVENT事件源、核心是把广播来的 ssid/password 落进wifi_config_t——WiFi 连接骨架完全复用wifi_init_sta()。 - 你理解了 SmartConfig 的原理:手机把密码编码进广播包长度、设备开混杂模式嗅探解码,所以它不用先连网就能传密码,但也因此对路由器广播策略和 2.4GHz 很敏感。
- 你知道了三种配网(SmartConfig / SoftAP / BLE)怎么选,以及连不上时从 2.4GHz、广播限制、密码这几个方向排查。
配网搞定,设备就跨过了从"自己玩"到"能交付"的那道坎。设备稳定联网之后,下一个常见需求是让它知道现在几点——上报数据要带时间戳、定时任务要按点触发。继续看用 NTP 给设备同步时间。
本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。