diff --git a/components/wifi-connect/CMakeLists.txt b/components/wifi-connect/CMakeLists.txt new file mode 100644 index 0000000..0437836 --- /dev/null +++ b/components/wifi-connect/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "wifi-connect.c" + INCLUDE_DIRS "include" + REQUIRES esp_wifi esp_timer esp_event esp_netif nvs_flash esp_http_server lwip driver +) diff --git a/components/wifi-connect/Kconfig.projbuild b/components/wifi-connect/Kconfig.projbuild new file mode 100644 index 0000000..7f69a3d --- /dev/null +++ b/components/wifi-connect/Kconfig.projbuild @@ -0,0 +1,18 @@ +menu "WiFi 连接" + +config WIFI_CONNECT_CONNECT_TIMEOUT_SEC + int "Wi-Fi 连接超时 (秒)" + range 5 180 + default 30 + +config WIFI_CONNECT_MAX_SCAN_RESULTS + int "最大扫描结果数" + range 5 50 + default 20 + +config WIFI_CONNECT_AP_MAX_CONNECTIONS + int "软AP最大连接数" + range 1 10 + default 4 + +endmenu diff --git a/components/wifi-connect/README.md b/components/wifi-connect/README.md new file mode 100644 index 0000000..865c19c --- /dev/null +++ b/components/wifi-connect/README.md @@ -0,0 +1,165 @@ +# wifi-connect 组件说明 + +`wifi-connect` 是一个基于 ESP-IDF 的 Wi-Fi 配网组件,支持: + +- 长按按键进入配网模式 +- 启动 SoftAP + Captive Portal(网页配网) +- 手机连接热点后,通过网页扫描并选择路由器 +- 保存 Wi-Fi 凭据到 NVS +- 下次开机自动重连 +- 支持两种配网模式:按键触发 / 常驻配网 + +面向最终用户的一页版操作说明见:`USER_GUIDE.md` +现场打印张贴版(四步卡)见:`QUICK_POSTER.md` + +--- + +## 目录结构 + +- `wifi-connect.c`:组件主实现(按键、APSTA、HTTP、DNS、状态机) +- `include/wifi-connect.h`:对外 API +- `Kconfig.projbuild`:组件配置项 +- `CMakeLists.txt`:组件构建依赖 + +--- + +## 对外 API + +头文件:`include/wifi-connect.h` + +- `esp_err_t wifi_connect_init(void);` + - 初始化组件(NVS、Wi-Fi、事件、按键任务等) + - 尝试自动连接已保存网络 + +- `esp_err_t wifi_connect_start(void);` + - 启动配网(APSTA + HTTP + DNS) + +- `esp_err_t wifi_connect_stop(void);` + - 停止配网(关闭热点与相关服务) + +- `wifi_connect_status_t wifi_connect_get_status(void);` + - 获取当前状态:`idle / provisioning / connecting / connected / failed / timeout` + +- `esp_err_t wifi_connect_get_config(wifi_connect_config_t *config);` + - 读取已保存的 Wi-Fi 凭据 + +- `esp_err_t wifi_connect_clear_config(void);` + - 清除已保存的 Wi-Fi 凭据(SSID/密码) + +--- + +## 快速使用 + +在 `main/main.c`: + +```c +#include "esp_check.h" +#include "wifi-connect.h" + +void app_main(void) +{ + ESP_ERROR_CHECK(wifi_connect_init()); +} +``` + +运行后: + +1. 选择配网模式: + - 按键触发模式:长按配置按键进入配网 + - 常驻配网模式:上电自动进入配网 +2. 手机连接 `ESP32-xxxxxx` 热点 +3. 打开 `http://192.168.4.1` +4. 选择 Wi-Fi 并输入密码提交 +5. 配网行为: + - 按键触发模式:连接成功后按配置自动关闭热点 + - 常驻配网模式:配网热点保持开启,不自动关闭 + +如需清空历史凭据,可在配网页面点击“清除已保存”。 + +--- + +## Kconfig 配置项 + +在 `idf.py menuconfig` 中:`WiFi Connect` 菜单 + +- `Provisioning mode`:配网模式(二选一) + - `Button triggered`:按键触发配网(默认) + - `Always-on provisioning`:常驻配网(上电自动进入且不自动关闭) + +- `WIFI_CONNECT_BUTTON_GPIO`:进入配网的按键 GPIO +- `WIFI_CONNECT_BUTTON_ACTIVE_LEVEL`:按键有效电平 +- `WIFI_CONNECT_DEBOUNCE_MS`:按键去抖时间 +- `WIFI_CONNECT_LONG_PRESS_MS`:长按触发时长 +- `WIFI_CONNECT_BUTTON_STARTUP_GUARD_MS`:上电保护窗口(该时间内忽略长按检测) +- `WIFI_CONNECT_BUTTON_RELEASE_ARM_MS`:松手解锁时间(先稳定松手再允许长按触发) +- `WIFI_CONNECT_CONNECT_TIMEOUT_SEC`:连接路由器超时 +- `WIFI_CONNECT_IDLE_TIMEOUT_SEC`:配网页面空闲超时 +- `WIFI_CONNECT_MAX_SCAN_RESULTS`:扫描网络最大数量 +- `WIFI_CONNECT_AP_MAX_CONNECTIONS`:SoftAP 最大连接数 +- `WIFI_CONNECT_AP_GRACEFUL_STOP_SEC`:联网成功后 AP 延迟关闭秒数 + +--- + +## 日志与状态说明(中文) + +组件会输出统一中文状态日志,例如: + +- `【状态】wifi-connect 初始化完成` +- `【状态】检测到按键长按:开始进入配网模式` +- `【状态】配网已启动:配网热点已开启,SSID=...` +- `【状态】开始连接路由器:收到配网请求,目标网络=...` +- `【状态】联网成功:已连接 ...,获取 IP=...` +- `【状态】配网已停止:热点已关闭,设备继续以 STA 模式运行` + +说明:ESP-IDF 驱动层(如 `wifi:`、`esp_netif_lwip:`)仍会输出英文日志,这是框架默认行为。 + +--- + +## 常见问题 + +### 1) 手机连上热点但不自动弹出页面 + +- 手动访问:`http://192.168.4.1` +- 确认手机没有强制使用 HTTPS +- 查看串口是否有 `配网已启动`、`DNS 劫持服务已启动` 日志 + +### 2) 提交后连接失败 + +- 检查密码是否正确 +- 查看日志中的失败原因码(`连接失败,原因=...`) +- 检查路由器是否禁用了新设备接入 +- 若曾保存过旧配置,可先在页面点击“清除已保存”后再重试 + +### 4) 按键未按下却误触发配网 + +- 常见原因是按键引脚与 LCD/外设复用,初始化期间电平抖动被误判为长按 +- 可增大 `WIFI_CONNECT_BUTTON_STARTUP_GUARD_MS`(如 8000~10000) +- 可增大 `WIFI_CONNECT_BUTTON_RELEASE_ARM_MS`(如 300~500) +- 若硬件允许,优先给配网按键使用独立 GPIO + +### 5) 成功后热点消失是否正常 + +- 在按键触发模式下:正常,可通过 `WIFI_CONNECT_AP_GRACEFUL_STOP_SEC` 调整关闭延时 +- 在常驻配网模式下:热点不会自动关闭 + +--- + +## 依赖 + +由 `CMakeLists.txt` 声明: + +- `esp_wifi` +- `esp_timer` +- `esp_event` +- `esp_netif` +- `nvs_flash` +- `esp_http_server` +- `lwip` +- `driver` + +--- + +## 版本建议 + +- 推荐 ESP-IDF `v5.5.x` +- 当前项目验证环境:`esp-idf v5.5.2`(ESP32-C3) diff --git a/components/wifi-connect/include/wifi-connect.h b/components/wifi-connect/include/wifi-connect.h new file mode 100644 index 0000000..37a325f --- /dev/null +++ b/components/wifi-connect/include/wifi-connect.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + WIFI_CONNECT_STATUS_IDLE = 0, + WIFI_CONNECT_STATUS_PROVISIONING, + WIFI_CONNECT_STATUS_CONNECTING, + WIFI_CONNECT_STATUS_CONNECTED, + WIFI_CONNECT_STATUS_FAILED, + WIFI_CONNECT_STATUS_TIMEOUT, +} wifi_connect_status_t; + +typedef struct { + bool has_config; + char ssid[33]; + char password[65]; +} wifi_connect_config_t; + +esp_err_t wifi_connect_init(void); +esp_err_t wifi_connect_start(void); +esp_err_t wifi_connect_stop(void); +wifi_connect_status_t wifi_connect_get_status(void); +esp_err_t wifi_connect_get_config(wifi_connect_config_t *config); +esp_err_t wifi_connect_clear_config(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/wifi-connect/wifi-connect.c b/components/wifi-connect/wifi-connect.c new file mode 100644 index 0000000..7b4b317 --- /dev/null +++ b/components/wifi-connect/wifi-connect.c @@ -0,0 +1,1028 @@ +#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_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 = + "" + "" + "智能体重秤控制台" + "
" + "

设备配网

" + "
" + "

请选择网络并输入密码连接路由器。

" + "
" + "" + "" + "
" + "
" + "" + "
" + "
"; + +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(); +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index d0067f9..bed4e72 100755 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,3 +1,3 @@ idf_component_register(SRCS "main.c" INCLUDE_DIRS "." - REQUIRES ws2812) + REQUIRES ws2812 wifi-connect) diff --git a/main/main.c b/main/main.c index a389533..b70c5e9 100755 --- a/main/main.c +++ b/main/main.c @@ -1,10 +1,14 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" - +#include "wifi-connect.h" #include "ws2812.h" void app_main(void) { - ws2812_start_task(); - vTaskDelete(NULL); + wifi_connect_init(); // 初始化 Wi-Fi 配网模块 + ws2812_start_task(); // 启动 WS2812 LED 灯控制任务 + for(;;) + { + vTaskDelay(100000); + } } diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..395e451 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,4 @@ +# Name,Type,SubType,Offset,Size,Flags +nvs,data,nvs,0x9000,0x6000, +phy_init,data,phy,0xf000,0x1000, +factory,app,factory,0x10000,0x390000, \ No newline at end of file