Files
Wang Beihong 8c33fe7411 Add Zone.Identifier files for wifi-connect and partitions
- Created new Zone.Identifier file in components/wifi-connect with ZoneId=3.
- Created new Zone.Identifier file in partitions.csv with ZoneId=3.
2026-03-28 16:12:37 +08:00

1619 lines
54 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <stdio.h>
/*
* 轻量级 ESP32 Wi-Fi 配网组件
*
* 【配网流程总览】
* 1. 启动阶段:
* - 初始化 NVS、网络栈、Wi-Fi 驱动、事件循环。
* - 根据 Kconfig 选择“自动配网”或“按键触发配网”模式。
* - 自动配网模式下,开机即启动 AP 热点和 HTTP 配网页面。
* - 按键模式下,长按指定 GPIO 进入配网。
*
* 2. 手机连接 ESP32 热点:
* - 手机 Wi-Fi 连接 ESP32-xxxxxx 热点,自动跳转到 http://192.168.4.1 配网页面。
* - 支持 DNS 劫持,任意域名都跳转到配网页面。
*
* 3. 配网页面操作:
* - 用户扫描附近 Wi-Fi选择目标路由器并输入密码点击“连接”。
* - ESP32 断开当前 STA配置新 Wi-Fi 并尝试连接。
* - 连接成功后保存凭据到 NVS。
* - 自动配网模式下,连接成功后热点/AP会延迟关闭仅保留 STA 联网。
*
* 4. 失败与重试:
* - 若连接超时或失败,前端页面显示错误,用户可重试。
* - 支持清除已保存 Wi-Fi 配置,恢复配网模式。
*
* 5. 事件与状态管理:
* - 通过事件回调处理 Wi-Fi 连接、断开、获取 IP 等状态。
* - 通过 HTTP API 实时反馈配网状态给前端页面。
*
* 适用场景:
* - 适合物联网设备首次部署、Wi-Fi 变更、远程维护等场景。
* - 支持多种配网入口,兼容绝大多数手机和路由器。
*
* 主要文件结构:
* - wifi_connect_init初始化配网组件
* - wifi_connect_start/stop启动/停止配网热点与服务
* - HTTP handler处理网页扫描、连接、状态、清除等接口
* - 事件回调:处理 Wi-Fi/IP 事件,管理状态机
* - NVS 操作:保存/读取/清除 Wi-Fi 凭据
*/
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include <unistd.h>
#include <errno.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "esp_check.h"
#include "esp_event.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_netif.h"
#include "esp_netif_ip_addr.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "lwip/sockets.h"
#include "lwip/inet.h"
#include "wifi-connect.h"
/*
* wifi-connect.c
*
* 简要说明:
* 该文件实现了一个轻量的 Wi-Fi 配网模块,包含:
* - 在设备上开启一个 AP热点和内嵌的 HTTP 配网页面,供手机扫码/连接后配置目标路由器SSID/密码;
* - DNS 劫持(将所有域名解析到设备),便于在手机打开任意地址时跳转到配网页面;
* - 本地保存/清除 Wi-Fi 凭据NVS
* - 支持按键长按进入配网或常驻配网模式(由 Kconfig 控制);
* - 自动尝试连接已保存的网络并在成功后可选择关闭热点。
*
* 代码中大量使用了 ESP-IDF 的事件、定时器、FreeRTOS 任务与 NVS。
*/
#define WIFI_CONNECT_NVS_NAMESPACE "wifi_connect"
#define WIFI_CONNECT_NVS_KEY_SSID "ssid"
#define WIFI_CONNECT_NVS_KEY_PASS "pass"
#define WIFI_CONNECT_HTTP_BUF_SIZE 256
#ifndef CONFIG_WIFI_CONNECT_BUTTON_STARTUP_GUARD_MS
#define CONFIG_WIFI_CONNECT_BUTTON_STARTUP_GUARD_MS 5000
#endif
#ifndef CONFIG_WIFI_CONNECT_BUTTON_RELEASE_ARM_MS
#define CONFIG_WIFI_CONNECT_BUTTON_RELEASE_ARM_MS 200
#endif
static const char *TAG = "wifi_connect";
// 判断当前是否为“按键触发配网”模式
// 若 Kconfig 选择了 BUTTON则返回 true否则 false
static inline bool wifi_connect_is_button_mode(void)
{
#if CONFIG_WIFI_CONNECT_PROVISION_MODE_BUTTON
return true;
#else
return false;
#endif
}
/*
* Kconfig 帮助函数:判断当前是否配置为自动配网模式(上电自动开启,连接后自动关闭)
*/
// 判断当前是否为“自动配网(连接后关闭)”模式
// 若 Kconfig 选择了 AUTO则返回 true否则 false
static inline bool wifi_connect_is_auto_mode(void)
{
#if CONFIG_WIFI_CONNECT_PROVISION_MODE_AUTO
return true;
#else
return false;
#endif
}
// 日志辅助函数info
// 统一输出带“状态”标签的 info 日志,便于串口快速定位配网流程
static void wifi_connect_log_state_i(const char *state, const char *detail)
{
if (detail != NULL && detail[0] != '\0')
{
ESP_LOGI(TAG, "【状态】%s%s", state, detail);
}
else
{
ESP_LOGI(TAG, "【状态】%s", state);
}
}
// 日志辅助函数warn
static void wifi_connect_log_state_w(const char *state, const char *detail)
{
if (detail != NULL && detail[0] != '\0')
{
ESP_LOGW(TAG, "【状态】%s%s", state, detail);
}
else
{
ESP_LOGW(TAG, "【状态】%s", state);
}
}
// 日志辅助函数error
static void wifi_connect_log_state_e(const char *state, const char *detail)
{
if (detail != NULL && detail[0] != '\0')
{
ESP_LOGE(TAG, "【状态】%s%s", state, detail);
}
else
{
ESP_LOGE(TAG, "【状态】%s", state);
}
}
/*
* 日志辅助函数info/warn/error
* 统一输出带中文“状态”标签的日志,便于在串口日志中快速定位配网流程状态。
*/
// 配网全局上下文结构体
// 保存配网状态、资源句柄、互斥锁、定时器等
typedef struct
{
// 当前配网组件运行状态
wifi_connect_status_t status;
bool initialized;
bool wifi_started;
bool provisioning_active;
bool sta_connected;
bool sta_connect_requested;
bool auto_connecting;
esp_netif_t *sta_netif;
esp_netif_t *ap_netif;
httpd_handle_t http_server;
esp_event_handler_instance_t wifi_event_instance;
esp_event_handler_instance_t ip_event_instance;
TaskHandle_t button_task;
TaskHandle_t dns_task;
SemaphoreHandle_t lock;
esp_timer_handle_t connect_timer;
esp_timer_handle_t idle_timer;
esp_timer_handle_t ap_stop_timer;
int dns_sock;
bool dns_running;
char ap_ssid[32];
char pending_ssid[33];
char pending_password[65];
char last_error[96];
} wifi_connect_ctx_t;
/*
* 全局上下文结构体 `wifi_connect_ctx_t`
* - 保存配网组件的运行状态(是否初始化、是否已启动 Wi-Fi、热点 SSID、待连接凭据等
* - 存放各类资源句柄netif、http server、event 句柄、定时器、任务句柄、互斥锁等);
* - 该结构仅在内部使用,并通过互斥锁保护对关键字段的并发访问。
*/
// 全局唯一配网上下文实例
static wifi_connect_ctx_t s_ctx = {
.status = WIFI_CONNECT_STATUS_IDLE,
.dns_sock = -1,
};
// 配网页面(内嵌 HTML + JS
static const char *s_html_page =
"<!doctype html><html><head><meta charset='utf-8'/>"
"<meta name='viewport' content='width=device-width, initial-scale=1'/>"
"<title>ESP32 Wi-Fi Setup</title>"
"<style>"
"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f3f4f6;margin:0;padding:16px;color:#111827;}"
".card{max-width:420px;margin:0 auto;background:#fff;border-radius:14px;padding:18px;box-shadow:0 8px 20px rgba(0,0,0,.08);}"
"h1{font-size:20px;margin:0 0 10px;}"
"p{margin:0 0 12px;color:#6b7280;}"
"button{border:none;border-radius:10px;padding:10px 14px;font-size:14px;cursor:pointer;}"
".btn{background:#2563eb;color:#fff;}"
".btn2{background:#e5e7eb;color:#111827;margin-left:8px;}"
"input,select{width:100%;padding:10px;border:1px solid #d1d5db;border-radius:10px;margin-top:8px;font-size:14px;box-sizing:border-box;}"
"label{font-size:13px;color:#374151;margin-top:10px;display:block;}"
"#status{margin-top:12px;font-size:13px;color:#1f2937;min-height:18px;}"
"#error{margin-top:6px;font-size:13px;color:#dc2626;min-height:18px;}"
"</style></head><body><div class='card'>"
"<h1>连接 Wi-Fi</h1><p>请选择网络并输入密码。</p>"
"<div><button class='btn' onclick='loadScan()'>扫描网络</button><button class='btn2' onclick='pollStatus()'>刷新状态</button><button class='btn2' onclick='clearSaved()'>清除已保存</button></div>"
"<label>网络</label><select id='ssid'></select>"
"<label>密码</label><input id='pwd' type='password' placeholder='请输入密码'/>"
"<div style='margin-top:12px'><button class='btn' onclick='connectWifi()'>连接</button></div>"
"<div id='status'></div><div id='error'></div>"
"</div><script>"
"const ssidSel=document.getElementById('ssid');"
"const pwdEl=document.getElementById('pwd');"
"const statusEl=document.getElementById('status');"
"const errorEl=document.getElementById('error');"
"function setStatus(t){statusEl.textContent=t||'';}"
"function setError(t){errorEl.textContent=t||'';}"
"function setBusy(v){document.querySelectorAll('button').forEach(b=>b.disabled=!!v);}"
"function statusText(s){const map={idle:'待机',provisioning:'配网中',connecting:'连接中',connected:'已连接',failed:'连接失败',timeout:'配网超时'};return map[s]||s||'未知';}"
"async function loadScan(){setError('');setStatus('正在扫描...');"
"try{const r=await fetch('/api/scan');const d=await r.json();ssidSel.innerHTML='';"
"(d.networks||[]).forEach(n=>{const o=document.createElement('option');o.value=n.ssid;o.textContent=`${n.ssid} (${n.rssi} dBm)`;ssidSel.appendChild(o);});"
"if(!ssidSel.options.length){setError('未找到网络');}setStatus('扫描完成');}catch(e){setError('扫描失败');setStatus('');}}"
"async function connectWifi(){setError('');const ssid=ssidSel.value;const password=pwdEl.value;"
"if(!ssid){setError('请先选择网络');return;}setStatus('正在连接...');"
"setBusy(true);"
"try{const r=await fetch('/api/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ssid,password})});"
"const d=await r.json();if(!r.ok||!d.ok){setError('提交失败,请重试');setStatus('');return;}"
"setStatus('连接请求已发送,请等待结果...');await pollStatus();}"
"catch(e){setError('提交失败');setStatus('');}finally{setBusy(false);}}"
"async function clearSaved(){setError('');setStatus('正在清除...');setBusy(true);"
"try{const r=await fetch('/api/clear',{method:'POST'});const d=await r.json();"
"if(!r.ok||!d.ok){setError('清除失败');setStatus('');return;}"
"pwdEl.value='';setStatus('已清除保存的 Wi-Fi 配置');await pollStatus();await loadScan();}"
"catch(e){setError('清除失败');setStatus('');}finally{setBusy(false);}}"
"async function pollStatus(){"
"try{const r=await fetch('/api/status');const d=await r.json();setStatus('状态: '+statusText(d.status));setError(d.error||'');"
"if(d.status==='connected'){setStatus('连接成功');setError('');}}catch(e){setError('状态获取失败');}}"
"loadScan();setInterval(pollStatus,2500);"
"</script></body></html>";
/*
* 内嵌的配网页面 HTML/JSs_html_page说明
* - 页面提供:扫描可用网络、输入密码、提交连接请求、清除保存项、轮询状态等功能;
* - 前端与设备通过 `/api/` HTTP 接口交互(详见下方的 http handler 实现);
* - 为了简洁将页面作为 C 字符串内嵌,便于无需额外文件即可启动配网服务。
*/
// 线程安全设置配网状态(需持有互斥锁)
static void wifi_connect_set_status_locked(wifi_connect_status_t status)
{
s_ctx.status = status;
}
/*
* 线程安全的状态/错误设置函数:
* - 这些函数假定调用者已持有 s_ctx.lock互斥锁用于在多任务环境下安全更新状态与错误消息。
*/
// 线程安全设置错误信息(需持有互斥锁)
static void wifi_connect_set_error_locked(const char *message)
{
if (message == NULL)
{
s_ctx.last_error[0] = '\0';
return;
}
snprintf(s_ctx.last_error, sizeof(s_ctx.last_error), "%s", message);
}
// 若为自动配网模式,重置/启动空闲超时定时器,长时间无操作自动关闭热点
static void wifi_connect_refresh_idle_timeout(void)
{
// 始终开启配网模式已移除
if (s_ctx.idle_timer == NULL)
{
return;
}
esp_timer_stop(s_ctx.idle_timer);
esp_timer_start_once(s_ctx.idle_timer, (uint64_t)CONFIG_WIFI_CONNECT_IDLE_TIMEOUT_SEC * 1000000ULL);
}
/*
* 若组件处于非常驻配网模式,则重置/启动空闲超时定时器:
* - 当长时间无操作时会触发 `wifi_connect_idle_timeout_cb` 以自动关闭配网热点,节省电量。
*/
// 保存 Wi-Fi 凭据到 NVS
static esp_err_t wifi_connect_save_credentials(const char *ssid, const char *password)
{
nvs_handle_t handle;
ESP_RETURN_ON_ERROR(nvs_open(WIFI_CONNECT_NVS_NAMESPACE, NVS_READWRITE, &handle), TAG, "open nvs failed");
esp_err_t err = nvs_set_str(handle, WIFI_CONNECT_NVS_KEY_SSID, ssid);
if (err == ESP_OK)
{
err = nvs_set_str(handle, WIFI_CONNECT_NVS_KEY_PASS, password);
}
if (err == ESP_OK)
{
err = nvs_commit(handle);
}
nvs_close(handle);
return err;
}
/*
* NVS 操作:保存/清除/读取 Wi-Fi 凭据
* - `wifi_connect_save_credentials` 将 ssid/password 写入 NVS 命名空间;
* - `wifi_connect_clear_config` 删除保存的项并在内部同步清理运行时状态;
* - `wifi_connect_get_config` 从 NVS 读取并填充结构返回是否存在配置。
*/
// 清除已保存的 Wi-Fi 配置NVS并同步清理运行时状态
esp_err_t wifi_connect_clear_config(void)
{
nvs_handle_t handle;
ESP_RETURN_ON_ERROR(nvs_open(WIFI_CONNECT_NVS_NAMESPACE, NVS_READWRITE, &handle), TAG, "open nvs failed");
esp_err_t err_ssid = nvs_erase_key(handle, WIFI_CONNECT_NVS_KEY_SSID);
esp_err_t err_pass = nvs_erase_key(handle, WIFI_CONNECT_NVS_KEY_PASS);
if (err_ssid != ESP_OK && err_ssid != ESP_ERR_NVS_NOT_FOUND)
{
nvs_close(handle);
return err_ssid;
}
if (err_pass != ESP_OK && err_pass != ESP_ERR_NVS_NOT_FOUND)
{
nvs_close(handle);
return err_pass;
}
esp_err_t err = nvs_commit(handle);
nvs_close(handle);
if (err != ESP_OK)
{
return err;
}
if (s_ctx.initialized && s_ctx.lock != NULL)
{
bool should_disconnect = false;
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
s_ctx.pending_ssid[0] = '\0';
s_ctx.pending_password[0] = '\0';
wifi_connect_set_error_locked(NULL);
if (s_ctx.provisioning_active)
{
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_PROVISIONING);
}
if (s_ctx.status == WIFI_CONNECT_STATUS_CONNECTING)
{
s_ctx.sta_connect_requested = false;
s_ctx.auto_connecting = false;
wifi_connect_set_status_locked(s_ctx.provisioning_active ? WIFI_CONNECT_STATUS_PROVISIONING : WIFI_CONNECT_STATUS_IDLE);
should_disconnect = true;
}
xSemaphoreGive(s_ctx.lock);
if (should_disconnect)
{
esp_wifi_disconnect();
}
}
wifi_connect_log_state_i("已清除保存的 Wi-Fi 配置", "下次上电将不会自动重连");
return ESP_OK;
}
// 读取已保存的 Wi-Fi 配置NVS填充到 config 结构体
esp_err_t wifi_connect_get_config(wifi_connect_config_t *config)
{
ESP_RETURN_ON_FALSE(config != NULL, ESP_ERR_INVALID_ARG, TAG, "config is null");
memset(config, 0, sizeof(*config));
nvs_handle_t handle;
esp_err_t err = nvs_open(WIFI_CONNECT_NVS_NAMESPACE, NVS_READONLY, &handle);
if (err != ESP_OK)
{
return err;
}
size_t ssid_len = sizeof(config->ssid);
size_t pass_len = sizeof(config->password);
err = nvs_get_str(handle, WIFI_CONNECT_NVS_KEY_SSID, config->ssid, &ssid_len);
if (err == ESP_OK)
{
err = nvs_get_str(handle, WIFI_CONNECT_NVS_KEY_PASS, config->password, &pass_len);
}
nvs_close(handle);
config->has_config = (err == ESP_OK && config->ssid[0] != '\0');
return err;
}
// 配网状态枚举转字符串,供前端/日志使用
static const char *wifi_connect_status_to_string(wifi_connect_status_t status)
{
switch (status)
{
case WIFI_CONNECT_STATUS_IDLE:
return "idle";
case WIFI_CONNECT_STATUS_PROVISIONING:
return "provisioning";
case WIFI_CONNECT_STATUS_CONNECTING:
return "connecting";
case WIFI_CONNECT_STATUS_CONNECTED:
return "connected";
case WIFI_CONNECT_STATUS_FAILED:
return "failed";
case WIFI_CONNECT_STATUS_TIMEOUT:
return "timeout";
default:
return "unknown";
}
}
/*
* 状态转换:将 enum 状态转换为供前端/日志使用的字符串。
*/
// 获取当前配网状态(线程安全)
wifi_connect_status_t wifi_connect_get_status(void)
{
if (!s_ctx.initialized || s_ctx.lock == NULL)
{
return WIFI_CONNECT_STATUS_IDLE;
}
wifi_connect_status_t status;
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
status = s_ctx.status;
xSemaphoreGive(s_ctx.lock);
return status;
}
// HTTP JSON 响应辅助函数
static esp_err_t wifi_connect_send_json(httpd_req_t *req, const char *json)
{
httpd_resp_set_type(req, "application/json");
return httpd_resp_sendstr(req, json);
}
/*
* 简单的 HTTP JSON 响应帮助函数:设置 Content-Type 并发送字符串。
*/
// JSON 字符串转义,防止注入和格式破坏
static void wifi_connect_json_escape(const char *src, char *dst, size_t dst_size)
{
size_t j = 0;
for (size_t i = 0; src != NULL && src[i] != '\0' && j + 2 < dst_size; ++i)
{
char c = src[i];
if (c == '"' || c == '\\')
{
dst[j++] = '\\';
dst[j++] = c;
}
else if ((unsigned char)c >= 32 && (unsigned char)c <= 126)
{
dst[j++] = c;
}
}
dst[j] = '\0';
}
/*
* 将字符串转义以安全嵌入 JSON 值中:
* - 转义双引号与反斜杠仅保留可打印字符32-126避免注入或破坏 JSON 格式。
*/
// Wi-Fi 加密类型枚举转字符串,供前端显示
static const char *wifi_connect_auth_to_string(wifi_auth_mode_t auth)
{
switch (auth)
{
case WIFI_AUTH_OPEN:
return "OPEN";
case WIFI_AUTH_WEP:
return "WEP";
case WIFI_AUTH_WPA_PSK:
return "WPA";
case WIFI_AUTH_WPA2_PSK:
return "WPA2";
case WIFI_AUTH_WPA_WPA2_PSK:
return "WPA/WPA2";
case WIFI_AUTH_WPA2_ENTERPRISE:
return "WPA2-ENT";
case WIFI_AUTH_WPA3_PSK:
return "WPA3";
case WIFI_AUTH_WPA2_WPA3_PSK:
return "WPA2/WPA3";
default:
return "UNKNOWN";
}
}
/*
* 将 wifi_auth_mode_t 转为可读字符串,供扫描结果返回前端显示。
*/
// HTTP handler扫描附近 Wi-Fi 并返回 JSON 列表
// 前端页面点击“扫描网络”时触发
static esp_err_t wifi_connect_http_scan_handler(httpd_req_t *req)
{
wifi_connect_refresh_idle_timeout();
wifi_scan_config_t scan_cfg = {
.show_hidden = false,
};
esp_err_t err = esp_wifi_scan_start(&scan_cfg, true);
if (err != ESP_OK)
{
char msg[64] = {0};
snprintf(msg, sizeof(msg), "scan_start failed: %s", esp_err_to_name(err));
wifi_connect_log_state_w("扫描启动失败", msg);
return wifi_connect_send_json(req, "{\"networks\":[],\"error\":\"scan_failed\"}");
}
uint16_t count = CONFIG_WIFI_CONNECT_MAX_SCAN_RESULTS;
wifi_ap_record_t *records = calloc(count, sizeof(wifi_ap_record_t));
if (records == NULL)
{
return ESP_ERR_NO_MEM;
}
err = esp_wifi_scan_get_ap_records(&count, records);
if (err != ESP_OK)
{
char msg[64] = {0};
snprintf(msg, sizeof(msg), "scan_get failed: %s", esp_err_to_name(err));
wifi_connect_log_state_w("扫描读取失败", msg);
free(records);
return wifi_connect_send_json(req, "{\"networks\":[],\"error\":\"scan_read_failed\"}");
}
size_t out_size = 512 + count * 96;
char *out = calloc(1, out_size);
if (out == NULL)
{
free(records);
return ESP_ERR_NO_MEM;
}
size_t used = snprintf(out, out_size, "{\"networks\":[");
for (uint16_t i = 0; i < count && used + 96 < out_size; ++i)
{
char ssid[65] = {0};
char escaped[130] = {0};
snprintf(ssid, sizeof(ssid), "%s", (char *)records[i].ssid);
wifi_connect_json_escape(ssid, escaped, sizeof(escaped));
used += snprintf(out + used, out_size - used,
"%s{\"ssid\":\"%s\",\"rssi\":%d,\"auth\":\"%s\"}",
(i == 0 ? "" : ","), escaped, records[i].rssi,
wifi_connect_auth_to_string(records[i].authmode));
}
snprintf(out + used, out_size - used, "]}");
err = wifi_connect_send_json(req, out);
free(out);
free(records);
return err;
}
/*
* HTTP 接口说明(主要处理函数):
* - /api/scan : 扫描附近 Wi-Fi 并返回 JSON 列表;
* - /api/connect : 接收前端提交的 ssid/password启动连接流程
* - /api/status : 返回当前配网状态idle/provisioning/connecting/connected/etc
* - /api/clear : 清除已保存的 Wi-Fi 配置。
*
* 具体的处理函数实现见下方各 handler扫描、连接、状态、清除、index 页面等)。
*/
// 从 JSON 字符串中提取指定 key 的字符串值
static bool wifi_connect_extract_json_string(const char *json, const char *key, char *out, size_t out_len)
{
char pattern[32];
snprintf(pattern, sizeof(pattern), "\"%s\":\"", key);
const char *start = strstr(json, pattern);
if (start == NULL)
{
return false;
}
start += strlen(pattern);
size_t idx = 0;
while (*start != '\0' && *start != '"' && idx + 1 < out_len)
{
if (*start == '\\' && *(start + 1) != '\0')
{
start++;
}
out[idx++] = *start++;
}
out[idx] = '\0';
return idx > 0 || strcmp(key, "password") == 0;
}
// 应用 STA 配置,断开当前连接,设置新 SSID/密码并连接
static esp_err_t wifi_connect_apply_sta_credentials(const char *ssid, const char *password)
{
wifi_config_t sta_cfg = {0};
snprintf((char *)sta_cfg.sta.ssid, sizeof(sta_cfg.sta.ssid), "%s", ssid);
snprintf((char *)sta_cfg.sta.password, sizeof(sta_cfg.sta.password), "%s", password);
sta_cfg.sta.scan_method = WIFI_FAST_SCAN;
sta_cfg.sta.threshold.authmode = WIFI_AUTH_OPEN;
sta_cfg.sta.pmf_cfg.capable = true;
sta_cfg.sta.pmf_cfg.required = false;
esp_err_t dis_err = esp_wifi_disconnect();
if (dis_err != ESP_OK && dis_err != ESP_ERR_WIFI_NOT_CONNECT)
{
char dis_msg[96] = {0};
snprintf(dis_msg, sizeof(dis_msg), "切换网络前断开 STA 失败,错误=%s", esp_err_to_name(dis_err));
wifi_connect_log_state_w("预断开当前连接失败", dis_msg);
}
ESP_RETURN_ON_ERROR(esp_wifi_set_config(WIFI_IF_STA, &sta_cfg), TAG, "set sta config failed");
ESP_RETURN_ON_ERROR(esp_wifi_connect(), TAG, "wifi connect failed");
return ESP_OK;
}
// 自动重连:尝试连接已保存的 Wi-Fi 配置
static esp_err_t wifi_connect_try_auto_connect(void)
{
wifi_connect_config_t config = {0};
esp_err_t err = wifi_connect_get_config(&config);
if (err != ESP_OK || !config.has_config)
{
wifi_connect_log_state_i("未发现已保存的 Wi-Fi 配置", "设备保持待机");
return ESP_OK;
}
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
snprintf(s_ctx.pending_ssid, sizeof(s_ctx.pending_ssid), "%s", config.ssid);
snprintf(s_ctx.pending_password, sizeof(s_ctx.pending_password), "%s", config.password);
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_CONNECTING);
wifi_connect_set_error_locked(NULL);
s_ctx.sta_connect_requested = true;
s_ctx.auto_connecting = true;
xSemaphoreGive(s_ctx.lock);
err = wifi_connect_apply_sta_credentials(config.ssid, config.password);
if (err != ESP_OK)
{
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
s_ctx.sta_connect_requested = false;
s_ctx.auto_connecting = false;
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_IDLE);
wifi_connect_set_error_locked(NULL);
xSemaphoreGive(s_ctx.lock);
char msg[96] = {0};
snprintf(msg, sizeof(msg), "自动重连启动失败,错误=%s", esp_err_to_name(err));
wifi_connect_log_state_w("自动重连失败", msg);
return err;
}
esp_timer_stop(s_ctx.connect_timer);
esp_timer_start_once(s_ctx.connect_timer, (uint64_t)CONFIG_WIFI_CONNECT_CONNECT_TIMEOUT_SEC * 1000000ULL);
char msg[96] = {0};
snprintf(msg, sizeof(msg), "尝试连接已保存网络:%s", config.ssid);
wifi_connect_log_state_i("自动重连中", msg);
return ESP_OK;
}
// HTTP handler处理前端提交的连接请求启动连接流程
static esp_err_t wifi_connect_http_connect_handler(httpd_req_t *req)
{
wifi_connect_refresh_idle_timeout();
if (req->content_len <= 0 || req->content_len >= WIFI_CONNECT_HTTP_BUF_SIZE)
{
return wifi_connect_send_json(req, "{\"ok\":false,\"error\":\"invalid_payload\"}");
}
char body[WIFI_CONNECT_HTTP_BUF_SIZE] = {0};
int received = httpd_req_recv(req, body, sizeof(body) - 1);
if (received <= 0)
{
return wifi_connect_send_json(req, "{\"ok\":false,\"error\":\"read_failed\"}");
}
body[received] = '\0';
char ssid[33] = {0};
char password[65] = {0};
if (!wifi_connect_extract_json_string(body, "ssid", ssid, sizeof(ssid)))
{
return wifi_connect_send_json(req, "{\"ok\":false,\"error\":\"ssid_missing\"}");
}
wifi_connect_extract_json_string(body, "password", password, sizeof(password));
char req_msg[96] = {0};
snprintf(req_msg, sizeof(req_msg), "收到配网请求,目标网络:%s", ssid);
wifi_connect_log_state_i("开始连接路由器", req_msg);
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
snprintf(s_ctx.pending_ssid, sizeof(s_ctx.pending_ssid), "%s", ssid);
snprintf(s_ctx.pending_password, sizeof(s_ctx.pending_password), "%s", password);
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_CONNECTING);
wifi_connect_set_error_locked(NULL);
s_ctx.sta_connect_requested = true;
s_ctx.auto_connecting = false;
xSemaphoreGive(s_ctx.lock);
esp_err_t err = wifi_connect_apply_sta_credentials(ssid, password);
if (err != ESP_OK)
{
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_FAILED);
wifi_connect_set_error_locked("启动连接失败");
xSemaphoreGive(s_ctx.lock);
char err_msg[96] = {0};
snprintf(err_msg, sizeof(err_msg), "提交连接失败,错误=%s", esp_err_to_name(err));
wifi_connect_log_state_w("连接启动失败", err_msg);
return wifi_connect_send_json(req, "{\"ok\":false,\"error\":\"connect_start_failed\"}");
}
esp_timer_stop(s_ctx.connect_timer);
esp_timer_start_once(s_ctx.connect_timer, (uint64_t)CONFIG_WIFI_CONNECT_CONNECT_TIMEOUT_SEC * 1000000ULL);
return wifi_connect_send_json(req, "{\"ok\":true}");
}
// HTTP handler返回当前配网状态和错误信息供前端轮询
static esp_err_t wifi_connect_http_status_handler(httpd_req_t *req)
{
wifi_connect_refresh_idle_timeout();
wifi_connect_status_t status;
char error[96] = {0};
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
status = s_ctx.status;
snprintf(error, sizeof(error), "%s", s_ctx.last_error);
xSemaphoreGive(s_ctx.lock);
char escaped[192] = {0};
wifi_connect_json_escape(error, escaped, sizeof(escaped));
char payload[260];
snprintf(payload, sizeof(payload), "{\"status\":\"%s\",\"error\":\"%s\"}",
wifi_connect_status_to_string(status), escaped);
return wifi_connect_send_json(req, payload);
}
// HTTP handler清除已保存的 Wi-Fi 配置
static esp_err_t wifi_connect_http_clear_handler(httpd_req_t *req)
{
wifi_connect_refresh_idle_timeout();
esp_err_t err = wifi_connect_clear_config();
if (err != ESP_OK)
{
return wifi_connect_send_json(req, "{\"ok\":false,\"error\":\"clear_failed\"}");
}
return wifi_connect_send_json(req, "{\"ok\":true}");
}
// HTTP handler返回配网页面 HTML
static esp_err_t wifi_connect_http_index_handler(httpd_req_t *req)
{
wifi_connect_refresh_idle_timeout();
httpd_resp_set_type(req, "text/html");
return httpd_resp_send(req, s_html_page, HTTPD_RESP_USE_STRLEN);
}
// 获取当前 AP 的 HTTP 访问地址(用于重定向)
static void wifi_connect_get_ap_http_url(char *out, size_t out_len)
{
esp_netif_ip_info_t ip_info = {0};
if (s_ctx.ap_netif != NULL && esp_netif_get_ip_info(s_ctx.ap_netif, &ip_info) == ESP_OK)
{
uint32_t ip = ntohl(ip_info.ip.addr);
snprintf(out, out_len, "http://%" PRIu32 ".%" PRIu32 ".%" PRIu32 ".%" PRIu32 "/",
(ip >> 24) & 0xFF,
(ip >> 16) & 0xFF,
(ip >> 8) & 0xFF,
ip & 0xFF);
return;
}
snprintf(out, out_len, "http://192.168.4.1/");
}
// HTTP handler处理探测/重定向请求,兼容各类手机/系统
static esp_err_t wifi_connect_http_probe_handler(httpd_req_t *req)
{
wifi_connect_refresh_idle_timeout();
char location[48] = {0};
wifi_connect_get_ap_http_url(location, sizeof(location));
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", location);
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
return httpd_resp_send(req, NULL, 0);
}
// 启动 HTTP 服务器,注册所有配网相关接口
static esp_err_t wifi_connect_http_start(void)
{
esp_err_t ret = ESP_OK;
if (s_ctx.http_server != NULL)
{
return ESP_OK;
}
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = 20;
config.uri_match_fn = httpd_uri_match_wildcard;
config.max_open_sockets = 2; // 配网页面只需处理单个客户端
ESP_RETURN_ON_ERROR(httpd_start(&s_ctx.http_server, &config), TAG, "start http server failed");
const httpd_uri_t index_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = wifi_connect_http_index_handler,
};
const httpd_uri_t scan_uri = {
.uri = "/api/scan",
.method = HTTP_GET,
.handler = wifi_connect_http_scan_handler,
};
const httpd_uri_t connect_uri = {
.uri = "/api/connect",
.method = HTTP_POST,
.handler = wifi_connect_http_connect_handler,
};
const httpd_uri_t status_uri = {
.uri = "/api/status",
.method = HTTP_GET,
.handler = wifi_connect_http_status_handler,
};
const httpd_uri_t clear_uri = {
.uri = "/api/clear",
.method = HTTP_POST,
.handler = wifi_connect_http_clear_handler,
};
const httpd_uri_t probe_1 = {
.uri = "/generate_204",
.method = HTTP_GET,
.handler = wifi_connect_http_probe_handler,
};
const httpd_uri_t probe_2 = {
.uri = "/hotspot-detect.html",
.method = HTTP_GET,
.handler = wifi_connect_http_probe_handler,
};
const httpd_uri_t probe_3 = {
.uri = "/ncsi.txt",
.method = HTTP_GET,
.handler = wifi_connect_http_probe_handler,
};
const httpd_uri_t probe_4 = {
.uri = "/connecttest.txt",
.method = HTTP_GET,
.handler = wifi_connect_http_probe_handler,
};
const httpd_uri_t probe_5 = {
.uri = "/redirect",
.method = HTTP_GET,
.handler = wifi_connect_http_probe_handler,
};
const httpd_uri_t probe_6 = {
.uri = "/canonical.html",
.method = HTTP_GET,
.handler = wifi_connect_http_probe_handler,
};
const httpd_uri_t probe_7 = {
.uri = "/mobile/status.php",
.method = HTTP_GET,
.handler = wifi_connect_http_probe_handler,
};
const httpd_uri_t probe_8 = {
.uri = "/success.txt",
.method = HTTP_GET,
.handler = wifi_connect_http_probe_handler,
};
const httpd_uri_t probe_9 = {
.uri = "/library/test/success.html",
.method = HTTP_GET,
.handler = wifi_connect_http_probe_handler,
};
const httpd_uri_t wildcard = {
.uri = "/*",
.method = HTTP_GET,
.handler = wifi_connect_http_probe_handler,
};
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &index_uri), fail, TAG, "register / failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &scan_uri), fail, TAG, "register /api/scan failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &connect_uri), fail, TAG, "register /api/connect failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &status_uri), fail, TAG, "register /api/status failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &clear_uri), fail, TAG, "register /api/clear failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &probe_1), fail, TAG, "register /generate_204 failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &probe_2), fail, TAG, "register /hotspot-detect.html failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &probe_3), fail, TAG, "register /ncsi.txt failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &probe_4), fail, TAG, "register /connecttest.txt failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &probe_5), fail, TAG, "register /redirect failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &probe_6), fail, TAG, "register /canonical.html failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &probe_7), fail, TAG, "register /mobile/status.php failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &probe_8), fail, TAG, "register /success.txt failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &probe_9), fail, TAG, "register /library/test/success.html failed");
ESP_GOTO_ON_ERROR(httpd_register_uri_handler(s_ctx.http_server, &wildcard), fail, TAG, "register wildcard failed");
return ESP_OK;
fail:
httpd_stop(s_ctx.http_server);
s_ctx.http_server = NULL;
return ret;
}
// 停止 HTTP 服务器
static void wifi_connect_http_stop(void)
{
if (s_ctx.http_server != NULL)
{
httpd_stop(s_ctx.http_server);
s_ctx.http_server = NULL;
}
}
// 构建 DNS 劫持响应,将所有域名解析到本机 AP IP
static size_t wifi_connect_build_dns_response(const uint8_t *req, size_t req_len, uint8_t *resp, size_t resp_max, uint32_t ip_addr)
{
if (req_len < 12 || resp_max < 64)
{
return 0;
}
const size_t q_offset = 12;
size_t q_name_end = q_offset;
while (q_name_end < req_len && req[q_name_end] != 0)
{
q_name_end += req[q_name_end] + 1;
}
if (q_name_end + 5 >= req_len)
{
return 0;
}
size_t question_len = (q_name_end + 5) - q_offset;
resp[0] = req[0];
resp[1] = req[1];
resp[2] = 0x81;
resp[3] = 0x80;
resp[4] = 0x00;
resp[5] = 0x01;
resp[6] = 0x00;
resp[7] = 0x01;
resp[8] = 0x00;
resp[9] = 0x00;
resp[10] = 0x00;
resp[11] = 0x00;
memcpy(&resp[12], &req[q_offset], question_len);
size_t pos = 12 + question_len;
if (pos + 16 > resp_max)
{
return 0;
}
resp[pos++] = 0xC0;
resp[pos++] = 0x0C;
resp[pos++] = 0x00;
resp[pos++] = 0x01;
resp[pos++] = 0x00;
resp[pos++] = 0x01;
resp[pos++] = 0x00;
resp[pos++] = 0x00;
resp[pos++] = 0x00;
resp[pos++] = 0x3C;
resp[pos++] = 0x00;
resp[pos++] = 0x04;
resp[pos++] = (ip_addr >> 24) & 0xFF;
resp[pos++] = (ip_addr >> 16) & 0xFF;
resp[pos++] = (ip_addr >> 8) & 0xFF;
resp[pos++] = (ip_addr) & 0xFF;
return pos;
}
/*
* DNS 劫持:构建 DNS 响应并监听 53 端口
* - `wifi_connect_build_dns_response` 负责根据请求组装一个标准的 A 记录响应,返回长度;
* - `wifi_connect_dns_task` 创建 UDP socket绑定 53 端口,循环接收 DNS 查询并返回固定 IPAP IP实现劫持效果
* - 该机制用于在手机打开任意 URL 时将请求导向设备的配网页面。
*/
// DNS 劫持任务,监听 53 端口,将所有请求指向配网页面
static void wifi_connect_dns_task(void *arg)
{
(void)arg;
uint8_t rx_buf[256];
uint8_t tx_buf[512];
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(53),
.sin_addr.s_addr = htonl(INADDR_ANY),
};
s_ctx.dns_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (s_ctx.dns_sock < 0)
{
wifi_connect_log_state_e("DNS 服务启动失败", "创建 socket 失败");
s_ctx.dns_running = false;
vTaskDelete(NULL);
return;
}
if (bind(s_ctx.dns_sock, (struct sockaddr *)&addr, sizeof(addr)) != 0)
{
char err_msg[96] = {0};
snprintf(err_msg, sizeof(err_msg), "绑定 53 端口失败errno=%d", errno);
wifi_connect_log_state_e("DNS 服务启动失败", err_msg);
close(s_ctx.dns_sock);
s_ctx.dns_sock = -1;
s_ctx.dns_running = false;
vTaskDelete(NULL);
return;
}
wifi_connect_log_state_i("DNS 劫持服务已启动", "手机访问任意域名将跳转配网页面");
esp_netif_ip_info_t ip_info;
esp_netif_get_ip_info(s_ctx.ap_netif, &ip_info);
uint32_t ip = ntohl(ip_info.ip.addr);
while (s_ctx.dns_running)
{
struct sockaddr_in from_addr;
socklen_t from_len = sizeof(from_addr);
struct timeval tv = {.tv_sec = 1, .tv_usec = 0};
setsockopt(s_ctx.dns_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
int len = recvfrom(s_ctx.dns_sock, rx_buf, sizeof(rx_buf), 0,
(struct sockaddr *)&from_addr, &from_len);
if (len <= 0)
{
continue;
}
size_t resp_len = wifi_connect_build_dns_response(rx_buf, (size_t)len, tx_buf, sizeof(tx_buf), ip);
if (resp_len > 0)
{
sendto(s_ctx.dns_sock, tx_buf, resp_len, 0, (struct sockaddr *)&from_addr, from_len);
}
}
close(s_ctx.dns_sock);
s_ctx.dns_sock = -1;
vTaskDelete(NULL);
}
// 启动 DNS 劫持服务
static esp_err_t wifi_connect_dns_start(void)
{
if (s_ctx.dns_running)
{
return ESP_OK;
}
s_ctx.dns_running = true;
BaseType_t ok = xTaskCreate(wifi_connect_dns_task, "wifi_dns", 3072, NULL, 4, &s_ctx.dns_task);
if (ok != pdPASS)
{
s_ctx.dns_running = false;
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
/*
* 启动/停止 DNS 劫持服务的包装函数:
* - `wifi_connect_dns_start` 创建接收任务并设置运行标志;
* - `wifi_connect_dns_stop` 停止任务并关闭 socket通过 shutdown 触发 recvfrom 退出循环)。
*/
// 停止 DNS 劫持服务
static void wifi_connect_dns_stop(void)
{
if (!s_ctx.dns_running)
{
return;
}
s_ctx.dns_running = false;
if (s_ctx.dns_sock >= 0)
{
shutdown(s_ctx.dns_sock, 0);
}
s_ctx.dns_task = NULL;
}
// 连接超时定时器回调,处理连接失败/重试逻辑
static void wifi_connect_connect_timeout_cb(void *arg)
{
(void)arg;
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
if (s_ctx.status == WIFI_CONNECT_STATUS_CONNECTING)
{
if (s_ctx.auto_connecting)
{
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_IDLE);
wifi_connect_set_error_locked(NULL);
s_ctx.auto_connecting = false;
wifi_connect_log_state_w("自动重连超时", "回到待机状态");
}
else
{
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_FAILED);
wifi_connect_set_error_locked("连接超时");
wifi_connect_log_state_w("连接路由器超时", "请确认密码和路由器信号");
}
s_ctx.sta_connect_requested = false;
esp_wifi_disconnect();
}
xSemaphoreGive(s_ctx.lock);
}
/*
* 定时器回调:连接超时 / AP 优雅关闭 / 空闲超时
* - `wifi_connect_connect_timeout_cb` 在连接阶段超时被调用,根据是否为自动重连调整状态;
* - `wifi_connect_ap_stop_timer_cb` 在配置为短时间优雅关闭时触发停止 AP
* - `wifi_connect_idle_timeout_cb` 在长时间无用户交互时触发并关闭配网(非常驻模式)。
*/
// AP 优雅关闭定时器回调,连接成功后延迟关闭热点
static void wifi_connect_ap_stop_timer_cb(void *arg)
{
(void)arg;
// 始终开启配网模式已移除
wifi_connect_stop();
}
// 配网空闲超时定时器回调,长时间无操作自动关闭热点
static void wifi_connect_idle_timeout_cb(void *arg)
{
(void)arg;
// 始终开启配网模式已移除
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
bool should_stop = s_ctx.provisioning_active;
if (should_stop)
{
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_TIMEOUT);
wifi_connect_set_error_locked("配网空闲超时");
wifi_connect_log_state_w("配网超时", "长时间无操作,正在关闭配网热点");
}
xSemaphoreGive(s_ctx.lock);
if (should_stop)
{
wifi_connect_stop();
}
}
// Wi-Fi/IP 事件回调,处理连接、断开、获取 IP 等状态
// 关键:连接成功后保存凭据,失败时反馈错误,自动关闭热点
static void wifi_connect_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
(void)arg;
// STA 获取到 IP判定联网成功并根据配置决定是否关闭配网热点
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
ip_event_got_ip_t *got_ip = (ip_event_got_ip_t *)event_data;
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
bool should_save = (s_ctx.status == WIFI_CONNECT_STATUS_CONNECTING || s_ctx.status == WIFI_CONNECT_STATUS_PROVISIONING);
bool provisioning_active = s_ctx.provisioning_active;
s_ctx.sta_connected = true;
s_ctx.sta_connect_requested = false;
s_ctx.auto_connecting = false;
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_CONNECTED);
wifi_connect_set_error_locked(NULL);
char ssid[33];
char password[65];
snprintf(ssid, sizeof(ssid), "%s", s_ctx.pending_ssid);
snprintf(password, sizeof(password), "%s", s_ctx.pending_password);
xSemaphoreGive(s_ctx.lock);
char success_msg[128] = {0};
snprintf(success_msg, sizeof(success_msg), "已连接 %s获取 IP=" IPSTR, ssid, IP2STR(&got_ip->ip_info.ip));
wifi_connect_log_state_i("联网成功", success_msg);
esp_timer_stop(s_ctx.connect_timer);
if (should_save)
{
esp_err_t err = wifi_connect_save_credentials(ssid, password);
if (err != ESP_OK)
{
char save_msg[96] = {0};
snprintf(save_msg, sizeof(save_msg), "保存凭据失败,错误=%s", esp_err_to_name(err));
wifi_connect_log_state_w("保存 Wi-Fi 信息失败", save_msg);
}
}
if (provisioning_active)
{
if (CONFIG_WIFI_CONNECT_AP_GRACEFUL_STOP_SEC == 0)
{
wifi_connect_stop();
}
else
{
esp_timer_stop(s_ctx.ap_stop_timer);
esp_timer_start_once(s_ctx.ap_stop_timer, (uint64_t)CONFIG_WIFI_CONNECT_AP_GRACEFUL_STOP_SEC * 1000000ULL);
}
}
return;
}
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
// 仅在“正在连接”阶段把断开视为失败;避免影响普通联网后的波动处理
wifi_event_sta_disconnected_t *dis = (wifi_event_sta_disconnected_t *)event_data;
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
bool connecting = (s_ctx.status == WIFI_CONNECT_STATUS_CONNECTING);
bool auto_connecting = s_ctx.auto_connecting;
s_ctx.sta_connected = false;
if (connecting)
{
if (auto_connecting)
{
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_IDLE);
wifi_connect_set_error_locked(NULL);
s_ctx.auto_connecting = false;
char dis_msg[96] = {0};
snprintf(dis_msg, sizeof(dis_msg), "自动重连断开,原因=%d", dis->reason);
wifi_connect_log_state_w("自动重连中断", dis_msg);
}
else
{
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_FAILED);
snprintf(s_ctx.last_error, sizeof(s_ctx.last_error), "连接失败,原因=%d", dis->reason);
char dis_msg[96] = {0};
snprintf(dis_msg, sizeof(dis_msg), "连接失败,原因=%d", dis->reason);
wifi_connect_log_state_w("连接路由器失败", dis_msg);
}
s_ctx.sta_connect_requested = false;
esp_timer_stop(s_ctx.connect_timer);
}
xSemaphoreGive(s_ctx.lock);
}
}
/*
* 事件处理器:处理 Wi-Fi / IP 事件
* - 监听 `IP_EVENT_STA_GOT_IP`:表示 STA 成功获取到 IP判定为联网成功
* 在此处会保存凭据(如是由配网/连接流程触发)、停止连接超时定时器,并根据配置决定是否关闭 AP
* - 监听 `WIFI_EVENT_STA_DISCONNECTED`:在连接阶段视为失败并记录原因;
* - 通过事件回调可以把底层 wifi 状态与本模块的运行态s_ctx.status进行映射与同步。
*/
// 生成唯一 AP SSID便于多设备区分
static void wifi_connect_generate_ap_ssid(char *out, size_t out_len)
{
uint8_t mac[6] = {0};
esp_wifi_get_mac(WIFI_IF_STA, mac);
snprintf(out, out_len, "ESP32-%02X%02X%02X%02X%02X%02X-192.168.4.1",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
/*
* 生成 AP SSID使用设备 MAC 地址的全部 6 个字节拼接,便于区分多设备
* 示例结果ESP32-112233AABBCC-192.168.4.1
*/
// 启动 AP+STA 模式,配置热点参数,若未启动则启动 Wi-Fi
static esp_err_t wifi_connect_start_apsta_locked(void)
{
wifi_config_t ap_cfg = {0};
wifi_connect_generate_ap_ssid(s_ctx.ap_ssid, sizeof(s_ctx.ap_ssid));
snprintf((char *)ap_cfg.ap.ssid, sizeof(ap_cfg.ap.ssid), "%s", s_ctx.ap_ssid);
ap_cfg.ap.ssid_len = strlen(s_ctx.ap_ssid);
ap_cfg.ap.channel = 1;
ap_cfg.ap.authmode = WIFI_AUTH_OPEN;
ap_cfg.ap.max_connection = CONFIG_WIFI_CONNECT_AP_MAX_CONNECTIONS;
ap_cfg.ap.pmf_cfg.required = false;
ESP_RETURN_ON_ERROR(esp_wifi_set_mode(WIFI_MODE_APSTA), TAG, "set mode apsta failed");
ESP_RETURN_ON_ERROR(esp_wifi_set_config(WIFI_IF_AP, &ap_cfg), TAG, "set ap config failed");
if (!s_ctx.wifi_started)
{
ESP_RETURN_ON_ERROR(esp_wifi_start(), TAG, "wifi start failed");
s_ctx.wifi_started = true;
}
return ESP_OK;
}
/*
* 启动 AP+STA 模式并配置 AP
* - 生成 AP SSID、设置频道、认证模式以及最大连接数
* - 切换 Wi-Fi 模式为 APSTA 并应用 AP 配置;
* - 若 Wi-Fi 尚未 start 则调用 `esp_wifi_start()`。
*
* 注意:该函数假定调用者已持有 s_ctx.lock因此加了 `_locked` 后缀)。
*/
// 按键检测任务,支持消抖、上电保护、长按触发配网
static void wifi_connect_button_task(void *arg)
{
(void)arg;
const TickType_t interval = pdMS_TO_TICKS(CONFIG_WIFI_CONNECT_DEBOUNCE_MS);
const TickType_t startup_guard_ticks = pdMS_TO_TICKS(CONFIG_WIFI_CONNECT_BUTTON_STARTUP_GUARD_MS);
const TickType_t release_arm_ticks = pdMS_TO_TICKS(CONFIG_WIFI_CONNECT_BUTTON_RELEASE_ARM_MS);
int stable_level = gpio_get_level(CONFIG_WIFI_CONNECT_BUTTON_GPIO);
int last_level = stable_level;
TickType_t changed_at = xTaskGetTickCount();
TickType_t low_since = 0;
TickType_t released_since = 0;
TickType_t startup_at = changed_at;
bool triggered = false;
bool armed = false;
wifi_connect_log_state_i("按键防误触保护", "已启用上电保护窗口与松手解锁机制");
while (true)
{
vTaskDelay(interval);
int level = gpio_get_level(CONFIG_WIFI_CONNECT_BUTTON_GPIO);
TickType_t now = xTaskGetTickCount();
if (level != last_level)
{
last_level = level;
changed_at = now;
}
if ((now - changed_at) >= interval && stable_level != level)
{
stable_level = level;
if (stable_level == CONFIG_WIFI_CONNECT_BUTTON_ACTIVE_LEVEL)
{
low_since = now;
released_since = 0;
triggered = false;
}
else
{
low_since = 0;
released_since = now;
triggered = false;
armed = false;
}
}
if (!armed && stable_level != CONFIG_WIFI_CONNECT_BUTTON_ACTIVE_LEVEL && released_since != 0)
{
if ((now - released_since) >= release_arm_ticks)
{
armed = true;
}
}
if ((now - startup_at) < startup_guard_ticks)
{
continue;
}
if (armed && stable_level == CONFIG_WIFI_CONNECT_BUTTON_ACTIVE_LEVEL && low_since != 0 && !triggered)
{
TickType_t held = now - low_since;
if (held >= pdMS_TO_TICKS(CONFIG_WIFI_CONNECT_LONG_PRESS_MS))
{
triggered = true;
armed = false;
wifi_connect_log_state_i("检测到按键长按", "开始进入配网模式");
wifi_connect_start();
}
}
}
}
/*
* 按键任务说明:
* - 负责按键防抖、上电保护窗口(避免误触)以及“松手解锁”机制;
* - 通过检测长按CONFIG_WIFI_CONNECT_LONG_PRESS_MS触发配网启动
* - 使用了多项时间窗(上电保护、释放装配窗口、消抖)来减少误触发概率。
*/
// 启动配网流程:开启 AP、HTTP、DNS进入可配网状态
esp_err_t wifi_connect_start(void)
{
ESP_RETURN_ON_FALSE(s_ctx.initialized, ESP_ERR_INVALID_STATE, TAG, "not initialized");
// 启动 AP+STA、HTTP 配网页面和 DNS 劫持,进入可配网状态
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
if (s_ctx.provisioning_active)
{
xSemaphoreGive(s_ctx.lock);
return ESP_OK;
}
esp_err_t err = wifi_connect_start_apsta_locked();
if (err != ESP_OK)
{
xSemaphoreGive(s_ctx.lock);
return err;
}
err = wifi_connect_http_start();
if (err != ESP_OK)
{
xSemaphoreGive(s_ctx.lock);
return err;
}
err = wifi_connect_dns_start();
if (err != ESP_OK)
{
wifi_connect_http_stop();
xSemaphoreGive(s_ctx.lock);
return err;
}
s_ctx.provisioning_active = true;
s_ctx.sta_connect_requested = false;
s_ctx.auto_connecting = false;
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_PROVISIONING);
wifi_connect_set_error_locked(NULL);
xSemaphoreGive(s_ctx.lock);
wifi_connect_refresh_idle_timeout();
char ap_msg[96] = {0};
snprintf(ap_msg, sizeof(ap_msg), "配网热点已开启SSID=%s访问 http://192.168.4.1", s_ctx.ap_ssid);
wifi_connect_log_state_i("配网已启动", ap_msg);
// 始终开启配网模式已移除
return ESP_OK;
}
/*
* 对外 APIstart/stop/init
* - `wifi_connect_start`启动配网AP+HTTP+DNS并更改模块状态
* - `wifi_connect_stop`:停止所有配网服务,若已成功联网则保持 STA 模式;
* - `wifi_connect_init`:一次性初始化 NVS、netif、事件回调、定时器、按键任务并尝试自动连接已保存网络。
*
* 这些函数构成模块对外的主要入口,外部模块只需调用 `wifi_connect_init()`,并在需要时调用 `wifi_connect_start()`/`wifi_connect_stop()`。
*/
// 停止配网流程:关闭 AP、HTTP、DNS恢复为 STA 联网
esp_err_t wifi_connect_stop(void)
{
if (!s_ctx.initialized)
{
return ESP_ERR_INVALID_STATE;
}
// 停止配网相关服务,若已联网则回到 STA 模式
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
s_ctx.provisioning_active = false;
s_ctx.sta_connect_requested = false;
s_ctx.auto_connecting = false;
esp_timer_stop(s_ctx.connect_timer);
esp_timer_stop(s_ctx.idle_timer);
esp_timer_stop(s_ctx.ap_stop_timer);
wifi_connect_http_stop();
wifi_connect_dns_stop();
if (s_ctx.sta_connected)
{
esp_wifi_set_mode(WIFI_MODE_STA);
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_CONNECTED);
}
else if (s_ctx.status != WIFI_CONNECT_STATUS_TIMEOUT)
{
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_IDLE);
}
xSemaphoreGive(s_ctx.lock);
wifi_connect_log_state_i("配网已停止", "热点已关闭,设备继续以 STA 模式运行");
return ESP_OK;
}
// 初始化配网组件注册事件、定时器、HTTP、DNS等自动重连
esp_err_t wifi_connect_init(void)
{
if (s_ctx.initialized)
{
return ESP_OK;
}
// 一次性初始化 NVS/Wi-Fi/事件/按键任务,并尝试自动连接已保存网络
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_RETURN_ON_ERROR(err, TAG, "nvs init failed");
ESP_RETURN_ON_ERROR(esp_netif_init(), TAG, "netif init failed");
err = esp_event_loop_create_default();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE)
{
ESP_RETURN_ON_ERROR(err, TAG, "event loop create failed");
}
wifi_init_config_t wifi_init_cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_RETURN_ON_ERROR(esp_wifi_init(&wifi_init_cfg), TAG, "wifi init failed");
ESP_RETURN_ON_ERROR(esp_wifi_set_storage(WIFI_STORAGE_RAM), TAG, "wifi storage set failed");
s_ctx.sta_netif = esp_netif_create_default_wifi_sta();
s_ctx.ap_netif = esp_netif_create_default_wifi_ap();
ESP_RETURN_ON_ERROR(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_connect_event_handler, NULL, &s_ctx.wifi_event_instance),
TAG, "register wifi handler failed");
ESP_RETURN_ON_ERROR(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_connect_event_handler, NULL, &s_ctx.ip_event_instance),
TAG, "register ip handler failed");
ESP_RETURN_ON_ERROR(esp_wifi_set_mode(WIFI_MODE_STA), TAG, "set mode sta failed");
ESP_RETURN_ON_ERROR(esp_wifi_start(), TAG, "wifi start failed");
s_ctx.wifi_started = true;
if (wifi_connect_is_button_mode())
{
gpio_config_t io = {
.pin_bit_mask = (1ULL << CONFIG_WIFI_CONNECT_BUTTON_GPIO),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_RETURN_ON_ERROR(gpio_config(&io), TAG, "button gpio config failed");
}
s_ctx.lock = xSemaphoreCreateMutex();
ESP_RETURN_ON_FALSE(s_ctx.lock != NULL, ESP_ERR_NO_MEM, TAG, "create lock failed");
esp_timer_create_args_t connect_timer_args = {
.callback = wifi_connect_connect_timeout_cb,
.name = "wifi_conn_to",
};
esp_timer_create_args_t idle_timer_args = {
.callback = wifi_connect_idle_timeout_cb,
.name = "wifi_idle_to",
};
esp_timer_create_args_t ap_stop_timer_args = {
.callback = wifi_connect_ap_stop_timer_cb,
.name = "wifi_ap_stop",
};
ESP_RETURN_ON_ERROR(esp_timer_create(&connect_timer_args, &s_ctx.connect_timer), TAG, "connect timer create failed");
ESP_RETURN_ON_ERROR(esp_timer_create(&idle_timer_args, &s_ctx.idle_timer), TAG, "idle timer create failed");
ESP_RETURN_ON_ERROR(esp_timer_create(&ap_stop_timer_args, &s_ctx.ap_stop_timer), TAG, "ap stop timer create failed");
if (wifi_connect_is_button_mode())
{
BaseType_t ok = xTaskCreate(wifi_connect_button_task, "wifi_btn", 3072, NULL, 4, &s_ctx.button_task);
ESP_RETURN_ON_FALSE(ok == pdPASS, ESP_ERR_NO_MEM, TAG, "button task create failed");
}
s_ctx.initialized = true;
if (wifi_connect_is_auto_mode())
{
wifi_connect_log_state_i("配网模式", "自动配网(上电自动开启,连接后关闭)");
}
else
{
wifi_connect_log_state_i("配网模式", "按键触发配网(长按进入)");
}
err = wifi_connect_try_auto_connect();
if (err != ESP_OK)
{
char skip_msg[96] = {0};
snprintf(skip_msg, sizeof(skip_msg), "自动重连已跳过,错误=%s", esp_err_to_name(err));
wifi_connect_log_state_w("初始化后自动重连未执行", skip_msg);
}
if (wifi_connect_is_auto_mode())
{
wifi_connect_log_state_i("wifi-connect 初始化完成", "自动配网模式已启用");
err = wifi_connect_start();
if (err != ESP_OK)
{
char mode_msg[96] = {0};
snprintf(mode_msg, sizeof(mode_msg), "自动开启配网失败,错误=%s", esp_err_to_name(err));
wifi_connect_log_state_w("配网启动失败", mode_msg);
return err;
}
}
else
{
wifi_connect_log_state_i("wifi-connect 初始化完成", "长按按键可进入配网");
}
return ESP_OK;
}