本节目标:搞懂"手机 App 如何把 WiFi 密码告诉一台还没联网的设备",并会用按键长按触发 SmartConfig,把拿到的 SSID/密码存进 NVS
一、本节要解决的问题
上一节把 SSID 和密码写死在代码里 ,这只是学习阶段的权宜之计。真实产品出厂时不可能知道用户家的 WiFi,而且大多数 IOT 设备------插座、传感器、灯------没有屏幕、没有键盘,用户没法"输入"。
把 SSID 和密码送进设备、让它连上热点接入网络,这个过程叫 配网。常见的配网方式:
| 方式 | 做法 | 特点 |
|---|---|---|
| AP 配网 | 设备先开热点,手机连上设备的 AP,打开网页填写 WiFi 信息 | 稳定但步骤多 |
| 蓝牙配网 | 手机通过 BLE 把凭据发给设备 | 需要 App 支持,交互体验好 |
| SmartConfig | 手机 App 把密码"广播"出去,设备在混杂模式下嗅探 | 用户操作最简单,只需在 App 输密码 |
本节讲 SmartConfig:用户只要在手机上输入 WiFi 密码、点一下确定,设备就能自己"听"到密码并联网。
二、硬件基础
本节的主角是板载 WiFi,外部只用到一个按键 (长按 3 秒触发配网):按键未按下时 GPIO 被拉高读到 1,按下时 GPIO 短接到地读到 0,所以按键配置里 active_level = 0 表示"低电平为按下"。
注意 :仓库
main.c里按键用的是GPIO_NUM_39,在 ESP32 上 GPIO34~39 是输入专用引脚,芯片内部没有上拉/下拉电阻 。button.c里虽然写了pull_up_en = GPIO_PULLUP_ENABLE,对 GPIO39 实际不起作用,真正把它拉高的是板上的外部上拉电阻。具体接线以自己开发板原理图为准。
另外还需要:一台 2.4GHz 路由器(ESP32 连不上 5GHz)、一台装了 EspTouch App 的手机(乐鑫官方提供,安卓/iOS 都有,且安卓版开源),配网时手机必须先连上目标 WiFi。
三、知识点
3.1 核心矛盾:密码怎么"隔空"传进去?
配网的本质难点是:设备还没连上路由器,手机和设备之间没有任何现成的通道。
那能不能让手机直接发一个包含密码的 UDP 广播包,设备收到就解析?不行 。因为在 802.11 协议里,MAC 帧的数据域(MSDU)是加密的,没连上 WiFi 的设备只能拿到密文,读不出内容。
SmartConfig 的巧妙之处在于:内容读不到,但长度是明文可读的------于是就用"包的长度"来编码信息。
3.2 SmartConfig 原理:长度编码
设一个 UDP 广播包(802.11 里是 MAC 帧,数据域是 IP 报文)由三部分组成:
IP 报文长度 = 20 字节 IPv4 头 + 8 字节 UDP 头 + UDP 内容
定义 明文长度 = IP 报文长度 + 算法常量(算法常量由 App 和芯片厂商约定,固定值)。
教程的例子:假如算法常量是 10,App 想传一个数值 1234,只需发一个 IP 报文总长 = 1234 − 10 = 1224 字节 的 UDP 广播包(内容随便填)。设备收到后:1224 + 10 = 1234,数据就还原出来了。同理,算法常量是 16 时,要传字符 't',就发一个长度为 't' − 16 的包。
一次完整的字符传输流程(教程原例,发 "test",算法常量 16):
- App 连续发 3 个 UDP 广播包,内容都是前导码;
- 发 1 个包,长度对应字符
t; - 发 1 个包,长度对应字符
e; - 发 1 个包,长度对应字符
s; - 发 1 个包,长度对应字符
t; - 切换 WiFi 信道,重复以上步骤。
两个配角也很关键:
- 前导码:几个特殊字节组成的固定包,设备用它来区分"这是 SmartConfig 的包"而不是普通广播包;
- 混杂模式 + 信道轮询 :设备没连任何 AP 时,WiFi 芯片处于混杂(promiscuous)模式,快速在各条信道上嗅探;一旦遇到正在发前导码的信道,就锁定该信道继续接收,攒够数据后解码出密码,再用它去连路由器。
每家厂商的算法常量、前导码、数据格式都不一样,所以不同厂家的 SmartConfig 互不通用。ESP32 只能用支持 ESP-TOUCH 协议的 App(如乐鑫 EspTouch)来配网。
3.3 ESP32 里的 SmartConfig 事件流程
ESP32 上的 SmartConfig 实现看不到源码(乐鑫闭源),但用起来很简单,整个过程也是一串事件:
| 事件 | 含义 |
|---|---|
SC_EVENT_SCAN_DONE |
设备扫描环境中的 AP 完毕 |
SC_EVENT_FOUND_CHANNEL |
找到了手机正在发送数据的那条信道 |
SC_EVENT_GOT_SSID_PSWD |
成功解码出 SSID 和密码,可以拿去连接路由器了 |
SC_EVENT_SEND_ACK_DONE |
设备已向手机回复 ACK,配网可以结束 |
可以配的协议版本有 SC_TYPE_ESPTOUCH(V1)、SC_TYPE_AIRKISS(微信用的)、SC_TYPE_ESPTOUCH_AIRKISS、SC_TYPE_ESPTOUCH_V2。版本之间不兼容:App 选 V1、设备设成 V2 就永远配不上,两边必须一致。
整个流程串起来:esp_smartconfig_start() → 扫描完成 → 锁定信道 → 拿到 SSID/密码 → 用 wifi_config_t + esp_wifi_set_config() 去连路由器(和上一节完全一样)→ 回 ACK → 任务收到完成标志后 esp_smartconfig_stop() 收尾。
四、代码分析
4.1 工程结构
smartconfig/
├── CMakeLists.txt
├── sdkconfig
└── main/
├── CMakeLists.txt # SRCS "wifi_smartconfig.c" "main.c" "button.c"
├── main.c # app_main():NVS、WiFi 初始化,注册按键,等长按事件
├── wifi_smartconfig.c # WiFi 初始化 + SmartConfig 任务 + 事件回调 + NVS 读写
├── wifi_smartconfig.h
├── button.c # 通用按键驱动:状态机 + 5ms 定时器轮询
└── button.h # button_config_t 与 button_event_set()
4.2 按键驱动:状态机 + 定时器
button.c 用一个 5ms 周期的 esp_timer 轮询按键电平,状态机一共四态:
c
typedef enum
{
BUTTON_RELEASE, //按键没有按下
BUTTON_PRESS, //按键按下了,等待一点延时(消抖),然后触发短按回调事件,进入BUTTON_HOLD
BUTTON_HOLD, //按住状态,如果时间长度超过设定的超时计数,将触发长按回调函数,进入BUTTON_LONG_PRESS_HOLD
BUTTON_LONG_PRESS_HOLD, //此状态等待电平消失,回到BUTTON_RELEASE状态
}BUTTON_STATE;
工作方式(increase_cnt 每次为 5,代表 5ms):
- 电平等于
active_level开始计时,累计到FILITER_TIMER(20,即 20ms)才算真的按下 ,这就完成了消抖,并触发short_cb; - 继续按住,计时超过
long_press_time(结构体里给的 3000,单位 ms)就触发long_cb,之后必须松开 才回到BUTTON_RELEASE,避免一直重复触发; - 定时器创建时
dispatch_method = ESP_TIMER_TASK,回调运行在 esp_timer 任务里(不是中断),所以代码可以直接用xEventGroupSetBits()而不需要FromISR版本。
按键配置结构体一目了然:
c
typedef struct
{
int gpio_num; //gpio号
int active_level; //按下的电平
int long_press_time; //长按时间
button_press_cb short_cb; //短按回调函数
button_press_cb long_cb; //长按回调函数
}button_config_t;
4.3 app_main:长按 3 秒 → 启动配网
c
#define BTN_GPIO GPIO_NUM_39
void app_main(void)
{
nvs_flash_init(); //初始化NVS
initialise_wifi(); //初始化wifi
s_pressEvent = xEventGroupCreate();
button_config_t btn_cfg =
{
.gpio_num = BTN_GPIO, //gpio号
.active_level = 0, //按下的电平
.long_press_time = 3000, //长按时间
.short_cb = NULL, //短按回调函数
.long_cb = smartconfig_start //长按回调函数
};
button_event_set(&btn_cfg); //添加按键响应事件处理
长按回调直接绑定 smartconfig_start,所以用户长按 3 秒松手前就已经开始配网了。
4.4 initialise_wifi:STA 基础 + 从 NVS 取上次的凭据
initialise_wifi() 就是上一节 wifi_sta_init() 的翻版,只是多注册了一类事件、多了一段 NVS 读取:
c
void initialise_wifi(void)
{
ESP_ERROR_CHECK(esp_netif_init());
s_wifi_event_group = xEventGroupCreate();
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, &event_handler, NULL) );
ESP_ERROR_CHECK( esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL) );
ESP_ERROR_CHECK( esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL) );
ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) );
...
前几行和上一节的 STA 初始化一样(多了 xEventGroupCreate()),关键差别是第三个注册:SC_EVENT(SmartConfig 事件)也交给同一个 event_handler 统一处理,这样配网过程中的状态变化和三方事件就都在一个回调里分流了。
读 NVS 的两个函数用的是命名空间 "DEV_WIFI"、键名 "ssid" / "password",读成功后填进 s_ssid_value[33] 和 s_password_value[65] 两个缓存:
c
//从NVS中读出SSID
read_nvs_ssid(s_ssid_value,32);
//从NVS中读取PASSWORD
read_nvs_password(s_password_value,64);
4.5 smartconfig_start 与配网任务
c
void smartconfig_start(void)
{
if(!s_is_smartconfig)
{
s_is_smartconfig = true;
esp_wifi_disconnect();
xTaskCreate(smartconfig_example_task, "smartconfig_example_task", 4096, NULL, 3, NULL);
}
}
s_is_smartconfig是运行标志,防止重复启动 SmartConfig;- 先
esp_wifi_disconnect()断开现有连接,让 WiFi 芯片腾出手去"嗅探"; - 单独开一个 4096 字节栈的任务做配网(配网要保存密码、发 ACK,栈别给太小)。
c
static void smartconfig_example_task(void * parm)
{
EventBits_t uxBits;
ESP_ERROR_CHECK( esp_smartconfig_set_type(SC_TYPE_ESPTOUCH_V2) ); //设定SmartConfig版本
smartconfig_start_config_t cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
ESP_ERROR_CHECK( esp_smartconfig_start(&cfg) ); //启动SmartConfig
while (1) {
uxBits = xEventGroupWaitBits(s_wifi_event_group, CONNECTED_BIT | ESPTOUCH_DONE_BIT, true, false, portMAX_DELAY);
if(uxBits & CONNECTED_BIT) {
ESP_LOGI(TAG, "WiFi Connected to ap");
}
if(uxBits & ESPTOUCH_DONE_BIT) { //收到smartconfig配网完成通知
ESP_LOGI(TAG, "smartconfig over");
esp_smartconfig_stop(); //停止smartconfig配网
write_nvs_ssid(s_ssid_value); //将ssid写入NVS
write_nvs_password(s_password_value); //将password写入NVS
s_is_smartconfig = false;
vTaskDelete(NULL); //退出任务
}
}
}
三个要点:
esp_smartconfig_set_type()必须在esp_smartconfig_start()之前调用,这里选 V2,所以手机 App 上也要选 V2;SMARTCONFIG_START_CONFIG_DEFAULT()是默认启动配置(不打日志、不加密);- 任务同时等两个事件位:
CONNECTED_BIT(拿到 IP)和ESPTOUCH_DONE_BIT(配网完成)。xEventGroupWaitBits的pdTRUE, pdFALSE表示任一位满足即返回,并把置上的位清掉。
4.6 事件回调:四类事件汇于一处
回调里同时处理 WIFI、IP、SmartConfig 三类事件,最核心的两个分支:
c
} else if (event_base == SC_EVENT && event_id == SC_EVENT_GOT_SSID_PSWD) {
//smartconfig 获取到SSID和密码
ESP_LOGI(TAG, "Got SSID and password");
smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t *)event_data;
wifi_config_t wifi_config;
uint8_t ssid[33] = { 0 };
uint8_t password[65] = { 0 };
//从event_data中提取SSID和密码
bzero(&wifi_config, sizeof(wifi_config_t));
memcpy(wifi_config.sta.ssid, evt->ssid, sizeof(wifi_config.sta.ssid));
memcpy(wifi_config.sta.password, evt->password, sizeof(wifi_config.sta.password));
...
snprintf(s_ssid_value,33,"%s",(char*)ssid);
snprintf(s_password_value,65,"%s",(char*)password);
//重新连接WIFI
ESP_ERROR_CHECK( esp_wifi_disconnect() );
ESP_ERROR_CHECK( esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
esp_wifi_connect();
} else if (event_base == SC_EVENT && event_id == SC_EVENT_SEND_ACK_DONE) {
//smartconfig 已发起回应
xEventGroupSetBits(s_wifi_event_group, ESPTOUCH_DONE_BIT);
}
SC_EVENT_GOT_SSID_PSWD的event_data是smartconfig_event_got_ssid_pswd_t *,里面装着解码出来的ssid[32]、password[64](还有bssid、token等)。把密码拷进wifi_config_t,set_config后esp_wifi_connect()------后面的路和上一节 STA 完全一致,这就是配网与联网的衔接点;- 回调里同时把凭据缓存到
s_ssid_value/s_password_value,供后面的任务写 NVS; SC_EVENT_SEND_ACK_DONE表示设备已回 ACK 给手机,置位ESPTOUCH_DONE_BIT通知配网任务收尾。
另外两个分支也值得留意:
c
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
if(s_ssid_value[0] != 0)
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
//WIFI断开连接后,再次发起连接
if(!s_is_smartconfig)
esp_wifi_connect();
//清除连接标志位
xEventGroupClearBits(s_wifi_event_group, CONNECTED_BIT);
断开重连时加了 if(!s_is_smartconfig) 判断 :配网过程中 WiFi 本来就会被反复断开重连,如果这时还去 esp_wifi_connect(),会和嗅探打架,所以配网期间不重连。这个细节比上一节的无脑重连更贴近实用。
五、关键 API 速查
| API | 作用 |
|---|---|
esp_smartconfig_set_type(SC_TYPE_ESPTOUCH_V2) |
选择配网协议版本,必须在 start 之前调用 |
esp_smartconfig_start(&cfg) |
启动 SmartConfig,开始嗅探广播包 |
esp_smartconfig_stop() |
停止配网并释放资源(无论成功与否都应调用) |
SMARTCONFIG_START_CONFIG_DEFAULT() |
生成默认配网启动配置 |
esp_event_handler_register(SC_EVENT, ...) |
注册 SmartConfig 事件回调 |
esp_wifi_disconnect() |
配网前断开现有连接 |
esp_wifi_set_config(WIFI_IF_STA, &cfg) |
把解码出的 SSID/密码写入驱动 |
button_event_set(&cfg) |
注册按键 GPIO 及长/短按回调 |
xEventGroupCreate() / xEventGroupWaitBits() / xEventGroupSetBits() / xEventGroupClearBits() |
事件组的创建、等待、置位、清除 |
nvs_open() / nvs_set_str() / nvs_get_str() / nvs_commit() / nvs_close() |
NVS 的打开、写、读、提交、关闭 |
esp_timer_create() / esp_timer_start_periodic() |
创建并启动周期定时器(按键轮询用) |
六、编译与现象验证
bash
cd esp32-board/smartconfig
idf.py build # sdkconfig 已指定 CONFIG_IDF_TARGET="esp32"
idf.py -p COM<端口> flash monitor
操作步骤:
-
手机连上家里的 2.4GHz WiFi;
-
打开 EspTouch App,协议版本选 V2 (与代码里的
SC_TYPE_ESPTOUCH_V2对应),输入 WiFi 密码,点击确定(此时 App 开始反复广播); -
开发板长按按键 3 秒 ,串口开始打印
Scan done、Found channel即可松手; -
稍等片刻完成配网,串口大致打印:
I (xxx) smartconfig_example: Scan done
I (xxx) smartconfig_example: Found channel
I (xxx) smartconfig_example: Got SSID and password
I (xxx) smartconfig_example: SSID:xxxx
I (xxx) smartconfig_example: PASSWORD:xxxxxxxx
I (xxx) smartconfig_example: WiFi Connected to ap
I (xxx) smartconfig_example: smartconfig over
如果一直停在 Scan done/Found channel 不进 Got SSID and password:检查 App 与设备的协议版本是否一致、手机是否连着 2.4G WiFi、密码是否正确。