arduino-esp32 WPS 配网实战:Push Button 与 PIN 模式的串口日志全解读
【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32
本篇围绕 arduino-esp32 中libraries/WiFi的 WPS 示例展开,讲清 ESP32 如何通过 WPS(Wi-Fi Protected Setup,Wi-Fi 保护配置)免密/免手输口令接入路由器:Push Button(PBC)与 PIN 两种模式的正确操作步骤、四种典型串口日志(成功/失败 × 两种模式)逐行解读,以及示例代码背后的事件驱动流程。读完之后,你可以直接烧录 WPS 示例完成配网,并能准确判断"卡在哪一步、为什么没连上"。
WPS 是什么,为什么适合设备配网
WPS 是路由器与设备之间的一套免手输口令的配网协议,主流路由器普遍支持两种方式:
- Push Button(PBC,按钮模式):ESP32 启动 WPS 后,按下路由器上的 WPS 按钮(通常为机身背面或侧面的小按钮,标有 WPS 字样或环形箭头图标),路由器自动下发 SSID 和密码,设备完成配网;
- PIN 模式:ESP32 生成一个 8 位数字 PIN 并通过串口打印,你在路由器管理页面(常见地址 192.168.0.1)的 WPS 设置中手动输入该 PIN 完成配网。PIN 在成功连接前会每 2 分钟刷新一次,需要在新 PIN 生成的窗口期内及时输入。
相比手动输入密码,WPS 适合"设备上没有屏幕、不方便交互"的量产场景;相比 SmartConfig 等 App 配网方案,WPS 无需额外 App。示例自带的 README.md 正是以串口日志的形式,给出四种场景下 ESP32 的完整输出,作为本文解读的基准。
示例工程与运行前提
示例代码位于 WPS.ino,硬件需求仅两块:ESP32 开发板 + 一台支持 WPS 的路由器。工程顶部有一处硬约束,直接编译即可验证:
#include "sdkconfig.h" #if CONFIG_ESP_HOSTED_ENABLED #error "WPS is only supported in SoCs with native Wi-Fi support" #endif即:启用了 esp_hosted(无线功能由协处理器承担)的 SoC 不支持 WPS,只有自带原生 Wi-Fi 射频的芯片(经典 ESP32、ESP32-S2/S3/C3/C5/C6 等)才能运行此示例。
两种模式如何切换
模式由一个宏决定:
/* Change the definition of the WPS mode from WPS_TYPE_PBC to WPS_TYPE_PIN in the case that you are using pin type WPS (pin is 00000000) */ #define ESP_WPS_MODE WPS_TYPE_PBC- 按钮模式保持
WPS_TYPE_PBC; - PIN 模式改为
WPS_TYPE_PIN,届时串口会打印WPS_PIN = <8位数字>。
注意:此处注释中的00000000是初始化配置时填入config.pin的占位值(见下文wpsStart()中snprintf(config.pin, ...)的写法);实际配网使用的 PIN 由 Wi-Fi 协议栈动态生成,并通过ARDUINO_EVENT_WPS_ER_PIN事件回调上报到串口,两者不是同一个东西。
示例代码逐段剖析
启动 WPS:wpsStart()
核心配置与启动逻辑在 WPS.ino:
void wpsStart() { esp_wps_config_t config; memset(&config, 0, sizeof(esp_wps_config_t)); //Same as config = WPS_CONFIG_INIT_DEFAULT(ESP_WPS_MODE); config.wps_type = ESP_WPS_MODE; snprintf(config.factory_info.manufacturer, sizeof(config.factory_info.manufacturer), "ESPRESSIF"); snprintf(config.factory_info.model_number, sizeof(config.factory_info.model_number), "%s", CONFIG_IDF_TARGET); snprintf(config.factory_info.model_name, sizeof(config.factory_info.model_name), "ESPRESSIF IOT"); snprintf(config.factory_info.device_name, sizeof(config.factory_info.device_name), "ESP DEVICE"); snprintf(config.pin, sizeof(config.pin), "00000000"); esp_err_t err = esp_wifi_wps_enable(&config); if (err != ESP_OK) { Serial.printf("WPS Enable Failed: 0x%x: %s\n", err, esp_err_to_name(err)); return; } #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) err = esp_wifi_wps_start(); #else err = esp_wifi_wps_start(0); #endif if (err != ESP_OK) { Serial.printf("WPS Start Failed: 0x%x: %s\n", err, esp_err_to_name(err)); } }几个关键点:
esp_wps_config_t全零初始化后只填必要字段。factory_info(制造商、型号、设备名等)是 WPS 协议要求携带的设备信息,路由器在认证时会用到;model_number直接取编译目标CONFIG_IDF_TARGET(如esp32、esp32s3),无需手写。- 流程分两步:
esp_wifi_wps_enable(&config)使能并写入配置,随后esp_wifi_wps_start()开始监听/发起 WPS 会话。任一步失败都会通过esp_err_to_name打印可读错误码,便于定位是配置阶段还是启动阶段出错。 - 代码对 ESP-IDF 版本做了兼容:IDF 6.0 起
esp_wifi_wps_start不再需要参数,旧版本需传0。示例同时支持新旧两条 IDF 基线。 - 停止 WPS 的
wpsStop()仅调用esp_wifi_wps_disable(),失败时打印WPS Disable Failed。
事件回调:WPS 的成败全靠它
WPS 是异步协议,所有结果通过 FreeRTOS 事件任务回调。WPS.ino 中的WiFiEvent()处理了全部关键事件:
// WARNING: WiFiEvent is called from a separate FreeRTOS task (thread)! void WiFiEvent(WiFiEvent_t event, arduino_event_info_t info) { switch (event) { case ARDUINO_EVENT_WIFI_STA_START: Serial.println("Station Mode Started"); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: Serial.println("Connected to :" + String(WiFi.SSID())); Serial.print("Got IP: "); Serial.println(WiFi.localIP()); break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: Serial.println("Disconnected from station, attempting reconnection"); WiFi.reconnect(); break; case ARDUINO_EVENT_WPS_ER_SUCCESS: Serial.println("WPS Successful, stopping WPS and connecting to: " + String(WiFi.SSID())); wpsStop(); delay(10); WiFi.begin(); break; case ARDUINO_EVENT_WPS_ER_FAILED: Serial.println("WPS Failed, retrying"); wpsStop(); wpsStart(); break; case ARDUINO_EVENT_WPS_ER_TIMEOUT: Serial.println("WPS Timedout, retrying"); wpsStop(); wpsStart(); break; case ARDUINO_EVENT_WPS_ER_PIN: Serial.println("WPS_PIN = " + wpspin2string(info.wps_er_pin.pin_code)); break; default: break; } }事件语义与对应的串口打印一一映射:
| 事件 | 含义 | 示例行为 |
|---|---|---|
ARDUINO_EVENT_WIFI_STA_START | STA 模式已启动,WPS 开始监听 | 打印Station Mode Started |
ARDUINO_EVENT_WPS_ER_PIN | PIN 模式下新 PIN 已生成 | 打印WPS_PIN = xxxxxxxx(8 位) |
ARDUINO_EVENT_WPS_ER_SUCCESS | WPS 交换完成,凭证已写入 | 停止 WPS,稍等 10ms 后WiFi.begin()用新凭证连接 |
ARDUINO_EVENT_WPS_ER_FAILED | 本次 WPS 失败 | 停止后立即重启 WPS,无限重试 |
ARDUINO_EVENT_WPS_ER_TIMEOUT | 等待路由器响应超时 | 停止后立即重启 WPS,无限重试 |
ARDUINO_EVENT_WIFI_STA_DISCONNECTED | 已连接但掉线 | 打印提示并WiFi.reconnect() |
ARDUINO_EVENT_WIFI_STA_GOT_IP | 拿到 IP,配网彻底完成 | 打印 SSID 与Got IP: x.x.x.x |
注意回调开头的注释:该函数运行在独立的 FreeRTOS 任务里,回调内部不要做长时间阻塞操作。另外成功路径里有一个细节:WPS 交换成功后设备已经拿到 SSID/密码,示例仍调用一次WiFi.begin()触发正式连接——这正是日志中"成功后又出现一次连接"的原因。
PIN 数组转字符串的工具函数wpspin2string()把 8 字节pin_code拼成以'\0'结尾的 C 字符串再转String输出(WPS.ino)。
setup()则完成初始化:115200 波特率串口 → 注册事件回调WiFi.onEvent(WiFiEvent)→WiFi.mode(WIFI_MODE_STA)→wpsStart(),之后loop()留空,全程由事件驱动。
读懂串口日志:四个典型场景
README.md 给出的四段日志覆盖了"两种模式 × 成功/失败"的全部组合。下面逐段继承原文日志并解读。四段日志共享同一段启动横幅:
ets Jun 8 2016 00:22:57 rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:1 load:0x3fff0010,len:4 load:0x3fff0014,len:732 load:0x40078000,len:0 load:0x40078000,len:11572 entry 0x40078a14这是 ESP32 早期 BootROM 的标准输出:rst:0x10 (RTCWDT_RTC_RESET)表示由 RTC 看门狗复位,boot:0x13 (SPI_FAST_FLASH_BOOT)表示以 SPI 快读模式从 Flash 启动,随后的load:0x...是各段镜像的加载过程,entry 0x40078a14为程序入口。这部分与 WPS 本身无关,属于板卡正常启动的基线。
场景一:Push Button 失败
操作步骤:ESP32 上电并打印 WPS 已启动后,在短时间内按下路由器的 WPS 按钮;若按钮 2 分钟窗口内未按下或路由器不响应,即出现下述日志。若不确定自己的路由器是否支持 WPS 按钮,需查看路由器规格说明。
ets Jun 8 2016 00:22:57 rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:1 load:0x3fff0010,len:4 load:0x3fff0014,len:732 load:0x40078000,len:0 load:0x40078000,len:11572 entry 0x40078a14 Starting WPS Station Mode Started WPS Timedout, retrying WPS Timedout, retrying逐行解读:
Starting WPS:setup()中wpsStart()已执行;Station Mode Started:ARDUINO_EVENT_WIFI_STA_START,Wi-Fi STA 就绪,WPS 监听已开启,此时可以按路由器 WPS 键;WPS Timedout, retrying(反复出现):ARDUINO_EVENT_WPS_ER_TIMEOUT,每次超时后示例都会wpsStop()+wpsStart()重新监听。也就是说按钮模式下设备会自动无限重试——只要你随后按下了 WPS 键,下一轮监听窗口内就可能成功,无需重新上电。
场景二:Push Button 成功
ets Jun 8 2016 00:22:57 rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) ets Jun 8 2016 00:22:57 rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:1 load:0x3fff0010,len:4 load:0x3fff0014,len:732 load:0x40078000,len:0 load:0x40078000,len:11572 entry 0x40078a14 Starting WPS Station Mode Started WPS Successful, stopping WPS and connecting to: < Your Router SSID > Disconnected from station, attempting reconnection Connected to : < Your Router SSID > Got IP: 192.168.1.100关键路径:
WPS Successful, stopping WPS and connecting to: < Your Router SSID >:ARDUINO_EVENT_WPS_ER_SUCCESS,凭证交换完成,此时WiFi.SSID()已能读出路由器 SSID;Disconnected from station, attempting reconnection:这是 WPS 收尾阶段的瞬态事件(WIFI_STA_DISCONNECTED),属正常现象,不必当作故障;Connected to : < Your Router SSID >:ARDUINO_EVENT_WIFI_STA_GOT_IP前置的连接建立;Got IP: 192.168.1.100:DHCP 拿到地址,配网完成。
出现Got IP之后,WPS 的使命已经结束,之后的网络请求可走WiFiClient、HTTPClient等任意库。
场景三:PIN 模式失败
操作方式与按钮模式不同:串口会周期性打印 8 位 PIN,你需要登录路由器管理页面(一般为 192.168.0.1)在 WPS 设置里输入。PIN 每 2 分钟刷新一次,输入必须赶在当前 PIN 有效期内;错过或输错就会出现连续超时:
ets Jun 8 2016 00:22:57 rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) ets Jun 8 2016 00:22:57 rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:1 load:0x3fff0010,len:4 load:0x3fff0014,len:732 load:0x40078000,len:0 load:0x40078000,len:11572 entry 0x40078a14 Starting WPS Station Mode Started WPS_PIN = 94842104 WPS Timedout, retrying WPS_PIN = 55814171 WPS Timedout, retrying WPS_PIN = 71321622日志节奏与代码行为完全吻合:每轮 WPS 会话超时会打印WPS Timedout, retrying并重启 WPS;PIN 模式下每个新会话都会通过ARDUINO_EVENT_WPS_ER_PIN事件生成并打印一个新的WPS_PIN。可以看到94842104 → 55814171 → 71321622三个 PIN 各不相同,印证了"PIN 会随会话刷新"。排错要点:路由器端输入的必须是当前这一轮打印出的 PIN,且要在该轮 2 分钟窗口内完成提交。
场景四:PIN 模式成功
ets Jun 8 2016 00:22:57 rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:1 load:0x3fff0010,len:4 load:0x3fff0014,len:732 load:0x40078000,len:0 load:0x40078000,len:11572 entry 0x40078a14 Starting WPS Station Mode Started WPS_PIN = 36807581 WPS Successful, stopping WPS and connecting to: <Your Router SSID> Disconnected from station, attempting reconnection Connected to :<Your Router SSID> Got IP: 192.168.1.100与按钮模式成功日志的差异只有一处:成功前多了一行WPS_PIN = 36807581——即你在路由器端输入的那个 PIN。之后的WPS Successful → Disconnected(瞬态)→ Connected → Got IP四步与按钮模式完全一致。
快速诊断速查
| 串口现象 | 含义 | 处理建议 |
|---|---|---|
只有Starting WPS,无Station Mode Started | STA 未就绪或使能失败 | 检查是否打印了WPS Enable/Start Failed: 0x...错误码 |
反复WPS Timedout, retrying(PBC) | 监听窗口内未按 WPS 键 | 在下一轮窗口内及时按键;确认路由器支持 WPS 按钮 |
反复WPS Timedout+ 新WPS_PIN(PIN) | PIN 未在有效期内输入或输错 | 用当前打印的 PIN,在 2 分钟内提交到路由器 WPS 设置页 |
WPS Successful后无Got IP | 凭证已交换但连接失败 | 查看是否持续Disconnected... attempting reconnection,检查路由器侧是否允许新设备 |
事件桥接机制:从 IDF 事件到 Arduino 事件
示例代码并不直接注册 Wi-Fi 事件,而是通过WiFi.onEvent()注册 Arduino 层回调。底层桥接在 WiFiGeneric.cpp 的_arduino_event_cb()中完成:ESP-IDF 的WIFI_EVENT_STA_WPS_ER_*事件被逐一映射为 Arduino 事件——
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_WPS_ER_SUCCESS) { arduino_event.event_id = ARDUINO_EVENT_WPS_ER_SUCCESS; } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_WPS_ER_FAILED) { arduino_event.event_id = ARDUINO_EVENT_WPS_ER_FAILED; memcpy(&arduino_event.event_info.wps_fail_reason, event_data, sizeof(wifi_event_sta_wps_fail_reason_t)); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_WPS_ER_TIMEOUT) { arduino_event.event_id = ARDUINO_EVENT_WPS_ER_TIMEOUT; } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_WPS_ER_PIN) { arduino_event.event_id = ARDUINO_EVENT_WPS_ER_PIN; memcpy(&arduino_event.event_info.wps_er_pin, event_data, sizeof(wifi_event_sta_wps_er_pin_t)); }两个值得注意的实现细节:
- 失败原因可取:
WIFI_EVENT_STA_WPS_ER_FAILED的负载(wifi_event_sta_wps_fail_reason_t,含reason与status_code)会被复制到arduino_event_info_t.wps_fail_reason字段,示例没有打印它,但在自定义配网程序中可以借此区分"路由器拒绝""协议错误"等具体失败原因。 - PIN 载荷可取:
wifi_event_sta_wps_er_pin_t被复制到wps_er_pin字段,示例正是从info.wps_er_pin.pin_code读出 8 字节 PIN。
事件枚举与 info 字段定义分别位于 NetworkEvents.h 的ARDUINO_EVENT_WPS_ER_PIN等枚举项,以及 NetworkEvents.h 中arduino_event_info_t联合体里的wps_er_pin/wps_fail_reason成员。从源码结构看,这套映射是 Arduino 事件总线(ARDUINO_EVENTS)对 Wi-Fi 事件层统一封装的一部分,因此 WPS 事件与扫描、SmartConfig、Provisioning 等事件共享同一回调通道,写自定义配网固件时可以一次onEvent同时处理多种事件。
使用建议与安全提示
- PBC 优先:按钮模式无需登录路由器后台,交互最简单;PIN 模式涉及管理页面,适合没有实体 WPS 键的无线路由器。
- 窗口期意识:无论哪种模式,WPS 会话都是限时窗口(超时即触发示例中的自动重启循环),操作要紧跟串口打印的
Station Mode Started/ 当前WPS_PIN。 - 自动重试是双刃剑:示例在失败/超时后无限重启 WPS,方便"晚按的键"被下一轮捕获,但也意味着配网失败时 WPS 端口长期开放。WPS 协议本身存在被离线攻击破解 PIN 的已知安全隐患(PIN 空间小),量产设备建议仅在配网阶段开启 WPS、取得 IP 后关闭(示例已在
WPS Successful后调用wpsStop(),可作为参考)。 - 适用边界:WPS 仅适用于原生 Wi-Fi SoC;基于 esp_hosted 架构的芯片编译期即被
#error拦截(见前文),请改用 SmartConfig 或 Network Provisioning 等其他配网方案(可参见 WiFiSmartConfig 等示例)。
小结
WPS 示例的完整链路是:setup()以 STA 模式启动 →esp_wifi_wps_enable/start开启 WPS 监听 → 用户在路由器侧按键(PBC)或输入 PIN →ARDUINO_EVENT_WPS_ER_PIN/SUCCESS/FAILED/TIMEOUT事件经 WiFiGeneric.cpp 桥接回到WiFiEvent()回调 → 成功后停止 WPS 并WiFi.begin()正式联网、拿到Got IP。掌握四段串口日志的判读方式(本文"场景一~四"),再配合事件表与诊断速查表,即可独立完成配网开发与现场排障;需要更多实现细节时,可直接对照 WPS.ino 与 README.md 原文。
【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考