Files
agri_env/components/wifi-connect/wifi-connect.c

1029 lines
38 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <stdio.h>
#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_system.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
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 dns_task;
SemaphoreHandle_t lock;
esp_timer_handle_t connect_timer;
int dns_sock;
bool dns_running;
char ap_ssid[32];
char pending_ssid[33];
char pending_password[65];
char last_error[96];
char sta_ip[16];
esp_timer_handle_t ap_stop_timer;
} wifi_connect_ctx_t;
static wifi_connect_ctx_t s_ctx = {
.status = WIFI_CONNECT_STATUS_IDLE,
.dns_sock = -1,
};
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>设备联网控制台</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:20px;box-shadow:0 8px 20px rgba(0,0,0,.08);}"
"h1{font-size:22px;margin:0 0 16px;color:#111827;}"
"p{margin:0 0 12px;color:#6b7280;}"
"button{border:none;border-radius:10px;padding:10px 14px;font-size:14px;cursor:pointer;transition:background .2s;}"
"button:active{opacity:0.8;}"
".btn{background:#2563eb;color:#fff;}"
".btn2{background:#e5e7eb;color:#111827;margin-left:8px;}"
".btn3{background:#ef4444;color:#fff;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;}"
".hidden{display:none !important;}"
".stat-box{background:#f9fafb;padding:14px;border-radius:10px;margin-top:10px;display:flex;justify-content:space-between;border:1px solid #e5e7eb;font-size:15px;}"
".stat-box span{color:#4b5563;}"
".stat-box strong{color:#111827;}"
"#status{margin-top:12px;font-size:13px;color:#1f2937;min-height:18px;text-align:center;}"
"#error{margin-top:6px;font-size:13px;color:#dc2626;min-height:18px;text-align:center;}"
"</style></head><body><div class='card'>"
"<h1 id='main-title'>设备配网</h1>"
"<div id='wifi-view'>"
"<p>请选择网络并输入密码连接路由器。</p>"
"<div style='display:flex;'><button class='btn' style='flex:1' onclick='loadScan()'>扫描网络</button><button class='btn2' style='flex:1' onclick='pollStatus()'>刷新状态</button></div>"
"<label>网络名称</label><select id='ssid'></select>"
"<label>Wi-Fi 密码</label><input id='pwd' type='password' placeholder='请输入密码'/>"
"<div style='margin-top:16px'><button class='btn' style='width:100%' onclick='connectWifi()'>开始连接</button></div>"
"</div>"
"<div id='dash-view' class='hidden'>"
"<div style='text-align:center;margin-bottom:20px;'><svg style='width:48px;height:48px;color:#10b981;margin:0 auto;' fill='none' stroke='currentColor' viewBox='0 0 24 24'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z'></path></svg><h2 style='font-size:18px;margin:8px 0 0;color:#10b981;'>已成功连接网络</h2></div>"
"<div class='stat-box'><span>局域网 IP</span><strong id='dash-ip'>-</strong></div>"
"<div class='stat-box'><span>连续运行</span><strong id='dash-up'>-</strong></div>"
"<div class='stat-box'><span>可用内存</span><strong id='dash-mem'>-</strong></div>"
"<div style='margin-top:20px;display:flex;'><button class='btn' style='flex:1' onclick='fetchInfo()'>获取状态</button><button class='btn3' style='flex:1' onclick='clearSaved()'>清除网络</button></div>"
"</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');"
"let isConnected=false;"
"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(){if(isConnected)return;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(){if(!confirm('确定要清除记录吗?这会断开当前网络。')){return;}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('配置已清除,设备即将进入重新配网...');"
"setTimeout(()=>{location.reload();},2000);}catch(e){setError('清除超时,可能网络已断开');setStatus('');}finally{setBusy(false);}}"
"async function fetchInfo(){"
"try{const r=await fetch('/api/sysinfo');const d=await r.json();"
"let sec=d.uptime;let m=Math.floor(sec/60);let h=Math.floor(m/60);sec=sec%60;m=m%60;"
"let timeStr='';if(h>0)timeStr+=h+'小时';if(m>0)timeStr+=m+'分';timeStr+=sec+'秒';"
"document.getElementById('dash-up').textContent=timeStr;document.getElementById('dash-mem').textContent=d.free_heap+' KB';}catch(e){}}"
"async function pollStatus(){"
"try{const r=await fetch('/api/status');const d=await r.json();"
"if(d.status==='connected'){"
" if(!isConnected){isConnected=true;"
" if(location.hostname==='192.168.4.1' && d.ip && !window.redirecting){"
" setStatus('配置成功设备IP: '+d.ip+'。3秒后自动跳转...');window.redirecting=true;setTimeout(()=>{window.location.href='http://'+d.ip;}, 3000);"
" }else{"
" document.getElementById('wifi-view').classList.add('hidden');"
" document.getElementById('dash-view').classList.remove('hidden');"
" document.getElementById('main-title').textContent='智能设备配网 - 控制台';"
" document.getElementById('dash-ip').textContent=d.ip||location.hostname;"
" setStatus('');setError('');fetchInfo();"
" }"
" }"
"}else{"
" isConnected=false;document.getElementById('dash-view').classList.add('hidden');document.getElementById('wifi-view').classList.remove('hidden');"
" document.getElementById('main-title').textContent='设备配网';"
" setStatus('当前状态: '+statusText(d.status));setError(d.error||'');"
"} }catch(e){if(!isConnected)setError('无法获取设备状态,检查连接');}}"
"pollStatus();"
"setInterval(()=>{if(!isConnected)pollStatus();},2500);"
"setInterval(()=>{if(isConnected)fetchInfo();},3000);"
"</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 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_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)
ESP_LOGW(TAG, "Disconnect STA failed: %s", esp_err_to_name(dis_err));
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_http_connect_handler(httpd_req_t *req)
{
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);
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_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)
{
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_sysinfo_handler(httpd_req_t *req)
{
char payload[128];
snprintf(payload, sizeof(payload), "{\"uptime\":%lld,\"free_heap\":%" PRIu32 "}", esp_timer_get_time() / 1000000ULL, esp_get_free_heap_size() / 1024);
return wifi_connect_send_json(req, payload);
}
static esp_err_t wifi_connect_http_index_handler(httpd_req_t *req)
{
httpd_resp_set_hdr(req, "Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
httpd_resp_set_hdr(req, "Pragma", "no-cache");
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)
{
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);
static void wifi_connect_http_stop(void);
static esp_err_t wifi_connect_http_start(void)
{
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 handlers[] = {
{"/", HTTP_GET, wifi_connect_http_index_handler, NULL},
{"/api/scan", HTTP_GET, wifi_connect_http_scan_handler, NULL},
{"/api/connect", HTTP_POST, wifi_connect_http_connect_handler, NULL},
{"/api/status", HTTP_GET, wifi_connect_http_status_handler, NULL},
{"/api/clear", HTTP_POST, wifi_connect_http_clear_handler, NULL},
{"/api/sysinfo", HTTP_GET, wifi_connect_http_sysinfo_handler, NULL},
{"/generate_204", HTTP_GET, wifi_connect_http_probe_handler, NULL},
{"/hotspot-detect.html", HTTP_GET, wifi_connect_http_probe_handler, NULL},
{"/ncsi.txt", HTTP_GET, wifi_connect_http_probe_handler, NULL},
{"/connecttest.txt", HTTP_GET, wifi_connect_http_probe_handler, NULL},
{"/redirect", HTTP_GET, wifi_connect_http_probe_handler, NULL},
{"/canonical.html", HTTP_GET, wifi_connect_http_probe_handler, NULL},
{"/mobile/status.php", HTTP_GET, wifi_connect_http_probe_handler, NULL},
{"/success.txt", HTTP_GET, wifi_connect_http_probe_handler, NULL},
{"/library/test/success.html", HTTP_GET, wifi_connect_http_probe_handler, NULL},
{"/*", HTTP_GET, wifi_connect_http_probe_handler, NULL},
};
for (size_t i = 0; i < sizeof(handlers) / sizeof(handlers[0]); ++i)
{
if (httpd_register_uri_handler(s_ctx.http_server, &handlers[i]) != ESP_OK)
{
httpd_stop(s_ctx.http_server);
s_ctx.http_server = NULL;
return ESP_FAIL;
}
}
return ESP_OK;
}
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)
{
uint8_t rx_buf[256], 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)
{
s_ctx.dns_running = false;
vTaskDelete(NULL);
return;
}
if (bind(s_ctx.dns_sock, (struct sockaddr *)&addr, sizeof(addr)) != 0)
{
close(s_ctx.dns_sock);
s_ctx.dns_sock = -1;
s_ctx.dns_running = false;
vTaskDelete(NULL);
return;
}
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)
{
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;
if (xTaskCreate(wifi_connect_dns_task, "wifi_dns", 3072, NULL, 4, &s_ctx.dns_task) != 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)
{
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)
{
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
if (s_ctx.status == WIFI_CONNECT_STATUS_CONNECTED && s_ctx.provisioning_active)
{
wifi_connect_dns_stop();
esp_wifi_set_mode(WIFI_MODE_STA);
s_ctx.provisioning_active = false;
wifi_connect_log_state_i("自动关闭热点", "已切换至纯 STA 模式HTTP 服务继续运行在 STA IP 上");
}
xSemaphoreGive(s_ctx.lock);
}
static void wifi_connect_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
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);
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], password[65];
snprintf(s_ctx.sta_ip, sizeof(s_ctx.sta_ip), IPSTR, IP2STR(&got_ip->ip_info.ip));
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=%s", ssid, s_ctx.sta_ip);
wifi_connect_log_state_i("联网成功", success_msg);
esp_timer_stop(s_ctx.connect_timer);
if (should_save)
wifi_connect_save_credentials(ssid, password);
// 启动定时器5秒后关闭AP热点和DNS仅保留STA和HTTP服务
esp_timer_stop(s_ctx.ap_stop_timer);
esp_timer_start_once(s_ctx.ap_stop_timer, 5000000ULL);
wifi_connect_log_state_i("网络状态", "5秒后将自动关闭配网热点");
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;
wifi_connect_log_state_w("自动重连中断", "连接丢失");
}
else
{
wifi_connect_set_status_locked(WIFI_CONNECT_STATUS_FAILED);
snprintf(s_ctx.last_error, sizeof(s_ctx.last_error), "连接失败,原因=%d", dis->reason);
wifi_connect_log_state_w("连接路由器失败", s_ctx.last_error);
}
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%02X%02X%02X-192.168.4.1",
mac[0], mac[1], mac[2], 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;
}
esp_err_t wifi_connect_start(void)
{
ESP_RETURN_ON_FALSE(s_ctx.initialized, ESP_ERR_INVALID_STATE, TAG, "not initialized");
xSemaphoreTake(s_ctx.lock, portMAX_DELAY);
if (s_ctx.provisioning_active)
{
xSemaphoreGive(s_ctx.lock);
return ESP_OK;
}
if (wifi_connect_start_apsta_locked() != ESP_OK || wifi_connect_http_start() != ESP_OK || wifi_connect_dns_start() != ESP_OK)
{
wifi_connect_http_stop();
xSemaphoreGive(s_ctx.lock);
return ESP_FAIL;
}
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);
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;
}
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);
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;
}
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);
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;
}
esp_err_t wifi_connect_init(void)
{
if (s_ctx.initialized)
return ESP_OK;
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;
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_RETURN_ON_ERROR(esp_timer_create(&connect_timer_args, &s_ctx.connect_timer), TAG, "connect timer create failed");
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(&ap_stop_timer_args, &s_ctx.ap_stop_timer), TAG, "ap stop timer create failed");
s_ctx.initialized = true;
wifi_connect_log_state_i("配网模式", "常驻配网(上电自动开启且不会自动关闭)");
wifi_connect_try_auto_connect();
return wifi_connect_start();
}