#include #include #include #include #include #include #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 inline bool wifi_connect_is_always_on_mode(void) { #if CONFIG_WIFI_CONNECT_PROVISION_MODE_ALWAYS_ON return true; #else return false; #endif } static inline bool wifi_connect_is_button_mode(void) { #if CONFIG_WIFI_CONNECT_PROVISION_MODE_BUTTON return true; #else return false; #endif } 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 = "" "" "ESP32 Wi-Fi Setup" "
" "

连接 Wi-Fi

请选择网络并输入密码。

" "
" "" "" "
" "
" "
"; 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 (wifi_connect_is_always_on_mode()) { return; } 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; if (wifi_connect_is_always_on_mode()) { return; } wifi_connect_stop(); } static void wifi_connect_idle_timeout_cb(void *arg) { (void)arg; if (wifi_connect_is_always_on_mode()) { return; } 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 (wifi_connect_is_always_on_mode()) { wifi_connect_log_state_i("常驻配网模式", "联网成功后保持配网热点开启"); } else { 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); if (wifi_connect_is_always_on_mode()) { wifi_connect_log_state_i("当前模式", "常驻配网模式(不会自动关闭)"); } 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; 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_always_on_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_always_on_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; }