1209 lines
40 KiB
C
1209 lines
40 KiB
C
#include <stdio.h>
|
||
#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"
|
||
|
||
#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";
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
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>";
|
||
|
||
static void wifi_connect_set_status_locked(wifi_connect_status_t status)
|
||
{
|
||
s_ctx.status = status;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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";
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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';
|
||
}
|
||
|
||
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";
|
||
}
|
||
}
|
||
|
||
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) {
|
||
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) {
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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}");
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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}");
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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/");
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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;
|
||
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;
|
||
}
|
||
|
||
static void wifi_connect_http_stop(void)
|
||
{
|
||
if (s_ctx.http_server != NULL) {
|
||
httpd_stop(s_ctx.http_server);
|
||
s_ctx.http_server = NULL;
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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", 4096, NULL, 4, &s_ctx.dns_task);
|
||
if (ok != pdPASS) {
|
||
s_ctx.dns_running = false;
|
||
return ESP_ERR_NO_MEM;
|
||
}
|
||
return ESP_OK;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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", mac[3], mac[4], mac[5]);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
|
||
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");
|
||
|
||
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;
|
||
|
||
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);
|
||
}
|
||
|
||
wifi_connect_log_state_i("wifi-connect 初始化完成", "长按按键可进入配网");
|
||
return ESP_OK;
|
||
}
|