mirror of
https://git.beihong.wang/wangbeihong/iot-bedroom-environment-controller.git
synced 2026-04-23 11:33:05 +08:00
- Created new Zone.Identifier file in components/wifi-connect with ZoneId=3. - Created new Zone.Identifier file in partitions.csv with ZoneId=3.
1619 lines
54 KiB
C
1619 lines
54 KiB
C
#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/JS(s_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 查询并返回固定 IP(AP 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;
|
||
}
|
||
|
||
/*
|
||
* 对外 API:start/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;
|
||
}
|