From 421fd9da6270d9d6026e9dd5b6e733951011cffc Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 28 Feb 2026 20:47:24 +0800 Subject: [PATCH] feat(platform): migrate to softap web provisioning portal --- README.md | 15 +- components/platform/CMakeLists.txt | 1 + .../platform/assets/wifi_configuration.html | 580 ++++++++++++++ components/platform/src/wifi_manager.c | 731 +++++++++++++++++- main/Kconfig.projbuild | 20 +- sdkconfig.defaults | 8 +- 6 files changed, 1303 insertions(+), 52 deletions(-) create mode 100644 components/platform/assets/wifi_configuration.html diff --git a/README.md b/README.md index 1a4b0b7..f4c9cc5 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,10 @@ idf.py build ## Config In `menuconfig -> TQ Controller Config`: -- `TQ_WIFI_SSID` -- `TQ_WIFI_PASSWORD` +- `TQ_WIFI_PROV_SOFTAP_SSID` +- `TQ_WIFI_PROV_SOFTAP_PASSWORD` +- `TQ_WIFI_PROV_SOFTAP_CHANNEL` +- `TQ_WIFI_PROV_SOFTAP_MAX_CONN` - `TQ_HTTP_PORT` - `TQ_API_KEY` (optional) - Z-Image HTTP auth and model fields: @@ -123,9 +125,12 @@ When `TQ_API_KEY` is not empty, each request must include: X-API-Key: ``` -STA-only behavior: -- If SSID is empty, startup fails. -- If STA connect fails, startup fails (no AP fallback). +Wi-Fi provisioning behavior: +- Device first tries saved STA credentials from NVS (`wifi_cfg` namespace). +- If no valid credentials exist or STA connect fails, device starts a SoftAP portal. +- Connect to the SoftAP and open `http://192.168.4.1`; the static page auto-scans nearby SSIDs for selection. +- Provisioning page supports Chinese/English switching and remembers the selected language in browser local storage. +- Credentials are written to NVS only after STA connect succeeds. ## Flash diff --git a/components/platform/CMakeLists.txt b/components/platform/CMakeLists.txt index 41a4f93..7831655 100644 --- a/components/platform/CMakeLists.txt +++ b/components/platform/CMakeLists.txt @@ -33,5 +33,6 @@ idf_component_register( esp_audio_codec driver EMBED_FILES + "assets/wifi_configuration.html" "assets/talkingq_logo.png" ) diff --git a/components/platform/assets/wifi_configuration.html b/components/platform/assets/wifi_configuration.html new file mode 100644 index 0000000..00ae253 --- /dev/null +++ b/components/platform/assets/wifi_configuration.html @@ -0,0 +1,580 @@ + + + + + + + TalkingQ Network Configuration + + + +
+
+
+ +
+

TalkingQ

+

Draw smarter. Connect faster.

+
+
+ +
+
+ +
+
+

Wi-Fi Configuration

+

Select a scanned network or enter SSID manually. Credentials are saved only after connection succeeds.

+
+ +
+
+ +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+
+
+
+
+ + + + diff --git a/components/platform/src/wifi_manager.c b/components/platform/src/wifi_manager.c index 24f986a..bcab015 100644 --- a/components/platform/src/wifi_manager.c +++ b/components/platform/src/wifi_manager.c @@ -1,26 +1,67 @@ #include "platform.h" +#include #include +#include #include #include "esp_event.h" +#include "esp_http_server.h" #include "esp_log.h" #include "esp_netif.h" #include "esp_wifi.h" #include "esp_wifi_default.h" #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" +#include "nvs.h" #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 +#define WIFI_PROV_DONE_BIT BIT2 + +#define WIFI_STA_SSID_MAX_LEN 32 +#define WIFI_STA_PASS_MAX_LEN 64 +#define WIFI_PROV_HTTP_BODY_MAX_LEN 512 +#define WIFI_SCAN_RESULT_LIMIT 20 + +#define WIFI_PROV_NVS_NAMESPACE "wifi_cfg" +#define WIFI_PROV_NVS_KEY_SSID "ssid" +#define WIFI_PROV_NVS_KEY_PASS "password" + +#ifndef CONFIG_TQ_WIFI_PROV_SOFTAP_SSID +#define CONFIG_TQ_WIFI_PROV_SOFTAP_SSID "TalkingQ-Setup" +#endif + +#ifndef CONFIG_TQ_WIFI_PROV_SOFTAP_PASSWORD +#define CONFIG_TQ_WIFI_PROV_SOFTAP_PASSWORD "" +#endif + +#ifndef CONFIG_TQ_WIFI_PROV_SOFTAP_CHANNEL +#define CONFIG_TQ_WIFI_PROV_SOFTAP_CHANNEL 1 +#endif + +#ifndef CONFIG_TQ_WIFI_PROV_SOFTAP_MAX_CONN +#define CONFIG_TQ_WIFI_PROV_SOFTAP_MAX_CONN 4 +#endif + +typedef struct { + char ssid[WIFI_STA_SSID_MAX_LEN + 1]; + char password[WIFI_STA_PASS_MAX_LEN + 1]; +} wifi_sta_credentials_t; static const char *TAG = "wifi_manager"; +extern const uint8_t s_provision_page_start[] asm("_binary_wifi_configuration_html_start"); +extern const uint8_t s_provision_page_end[] asm("_binary_wifi_configuration_html_end"); static EventGroupHandle_t s_wifi_event_group; static int s_retry_num; static bool s_ready; static bool s_started; +static bool s_sta_reconnect_enabled; +static bool s_provisioning_active; static esp_netif_t *s_sta_netif; +static esp_netif_t *s_ap_netif; +static httpd_handle_t s_prov_httpd; static esp_event_handler_instance_t s_wifi_event_inst; static esp_event_handler_instance_t s_ip_event_inst; @@ -48,31 +89,113 @@ static void wifi_manager_apply_sta_throughput_profile(void) { } } +static esp_err_t wifi_manager_save_credentials(const wifi_sta_credentials_t *creds) { + if (creds == NULL || creds->ssid[0] == '\0') { + return ESP_ERR_INVALID_ARG; + } + + nvs_handle_t nvs = 0; + esp_err_t err = nvs_open(WIFI_PROV_NVS_NAMESPACE, NVS_READWRITE, &nvs); + if (err != ESP_OK) { + return err; + } + + err = nvs_set_str(nvs, WIFI_PROV_NVS_KEY_SSID, creds->ssid); + if (err == ESP_OK) { + err = nvs_set_str(nvs, WIFI_PROV_NVS_KEY_PASS, creds->password); + } + if (err == ESP_OK) { + err = nvs_commit(nvs); + } + + nvs_close(nvs); + return err; +} + +static esp_err_t wifi_manager_load_credentials(wifi_sta_credentials_t *out_creds) { + if (out_creds == NULL) { + return ESP_ERR_INVALID_ARG; + } + + memset(out_creds, 0, sizeof(*out_creds)); + + nvs_handle_t nvs = 0; + esp_err_t err = nvs_open(WIFI_PROV_NVS_NAMESPACE, NVS_READONLY, &nvs); + if (err != ESP_OK) { + return err; + } + + size_t ssid_len = sizeof(out_creds->ssid); + err = nvs_get_str(nvs, WIFI_PROV_NVS_KEY_SSID, out_creds->ssid, &ssid_len); + if (err != ESP_OK) { + nvs_close(nvs); + return err; + } + + size_t pass_len = sizeof(out_creds->password); + err = nvs_get_str(nvs, WIFI_PROV_NVS_KEY_PASS, out_creds->password, &pass_len); + if (err == ESP_ERR_NVS_NOT_FOUND) { + out_creds->password[0] = '\0'; + err = ESP_OK; + } + + nvs_close(nvs); + if (err != ESP_OK) { + return err; + } + if (out_creds->ssid[0] == '\0') { + return ESP_ERR_NOT_FOUND; + } + + return ESP_OK; +} + +static void wifi_manager_record_connect_result(esp_err_t err, const char *stage) { + if (err == ESP_OK) { + runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_SUCCESS, 1); + return; + } + + if (err == ESP_ERR_TIMEOUT) { + runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_TIMEOUT, 1); + } else { + runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_FAILED, 1); + } + + runtime_diag_record_error("wifi_connect", err, stage); +} + static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { (void)arg; - (void)event_data; if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { - if (!s_started) { - return; - } - (void)esp_wifi_connect(); + // Connection is triggered explicitly by wifi_manager_connect_sta(). + // Keep this event hook only for lifecycle visibility. return; } if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { s_ready = false; - if (!s_started) { + if (!s_started || !s_sta_reconnect_enabled) { return; } + + wifi_event_sta_disconnected_t *disc = (wifi_event_sta_disconnected_t *)event_data; + uint8_t reason = disc != NULL ? disc->reason : 0; + if (s_retry_num < CONFIG_TQ_WIFI_MAXIMUM_RETRY) { (void)esp_wifi_connect(); s_retry_num++; - ESP_LOGW(TAG, "retry to connect to the AP (%d/%d)", s_retry_num, CONFIG_TQ_WIFI_MAXIMUM_RETRY); + ESP_LOGW(TAG, + "retry to connect AP (%d/%d), reason=%u", + s_retry_num, + CONFIG_TQ_WIFI_MAXIMUM_RETRY, + (unsigned)reason); } else if (s_wifi_event_group != NULL) { + ESP_LOGW(TAG, "connect failed after retries, reason=%u", (unsigned)reason); xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); } return; @@ -82,6 +205,7 @@ static void wifi_event_handler(void *arg, if (!s_started) { return; } + ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip)); s_retry_num = 0; @@ -89,37 +213,309 @@ static void wifi_event_handler(void *arg, if (s_wifi_event_group != NULL) { xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); } - return; } } -static esp_err_t start_sta_mode(void) { - wifi_config_t wifi_config = {0}; - if (strlen(CONFIG_TQ_WIFI_SSID) == 0) { - ESP_LOGE(TAG, "CONFIG_TQ_WIFI_SSID is empty; STA-only mode requires valid SSID"); - return ESP_ERR_INVALID_STATE; +static int wifi_manager_hex_digit_to_int(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + c = (char)tolower((unsigned char)c); + if (c >= 'a' && c <= 'f') { + return 10 + (c - 'a'); + } + return -1; +} + +static void wifi_manager_url_decode(char *dst, size_t dst_len, const char *src) { + if (dst == NULL || dst_len == 0) { + return; } - strlcpy((char *)wifi_config.sta.ssid, CONFIG_TQ_WIFI_SSID, sizeof(wifi_config.sta.ssid)); - strlcpy((char *)wifi_config.sta.password, CONFIG_TQ_WIFI_PASSWORD, sizeof(wifi_config.sta.password)); - wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; + size_t wi = 0; + for (size_t ri = 0; src != NULL && src[ri] != '\0' && wi + 1 < dst_len; ++ri) { + if (src[ri] == '+') { + dst[wi++] = ' '; + continue; + } + + if (src[ri] == '%' && src[ri + 1] != '\0' && src[ri + 2] != '\0') { + int hi = wifi_manager_hex_digit_to_int(src[ri + 1]); + int lo = wifi_manager_hex_digit_to_int(src[ri + 2]); + if (hi >= 0 && lo >= 0) { + dst[wi++] = (char)((hi << 4) | lo); + ri += 2; + continue; + } + } + + dst[wi++] = src[ri]; + } + + dst[wi] = '\0'; +} + +static bool wifi_manager_form_get_value(const char *form, + const char *key, + char *out, + size_t out_len) { + if (form == NULL || key == NULL || out == NULL || out_len == 0) { + return false; + } + + size_t key_len = strlen(key); + const char *cursor = form; + + while (*cursor != '\0') { + const char *segment_end = strchr(cursor, '&'); + size_t segment_len = segment_end != NULL ? (size_t)(segment_end - cursor) : strlen(cursor); + const char *eq = memchr(cursor, '=', segment_len); + + if (eq != NULL && (size_t)(eq - cursor) == key_len && strncmp(cursor, key, key_len) == 0) { + size_t encoded_len = segment_len - key_len - 1; + char encoded[WIFI_PROV_HTTP_BODY_MAX_LEN]; + if (encoded_len >= sizeof(encoded)) { + return false; + } + memcpy(encoded, eq + 1, encoded_len); + encoded[encoded_len] = '\0'; + wifi_manager_url_decode(out, out_len, encoded); + return true; + } + + if (segment_end == NULL) { + break; + } + cursor = segment_end + 1; + } + + out[0] = '\0'; + return false; +} + +static bool wifi_manager_is_url_unreserved(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || + c == '-' || c == '_' || c == '.' || c == '~'; +} + +static void wifi_manager_url_encode(const char *src, char *dst, size_t dst_len) { + if (dst == NULL || dst_len == 0) { + return; + } + + size_t wi = 0; + for (size_t ri = 0; src != NULL && src[ri] != '\0' && wi + 1 < dst_len; ++ri) { + unsigned char ch = (unsigned char)src[ri]; + if (wifi_manager_is_url_unreserved((char)ch)) { + dst[wi++] = (char)ch; + continue; + } + + if (wi + 3 >= dst_len) { + break; + } + + static const char hex[] = "0123456789ABCDEF"; + dst[wi++] = '%'; + dst[wi++] = hex[(ch >> 4) & 0x0F]; + dst[wi++] = hex[ch & 0x0F]; + } + + dst[wi] = '\0'; +} + +static const char *wifi_manager_authmode_to_str(wifi_auth_mode_t authmode) { + switch (authmode) { + 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_enterprise"; + case WIFI_AUTH_WPA3_PSK: + return "wpa3"; + case WIFI_AUTH_WPA2_WPA3_PSK: + return "wpa2_wpa3"; + case WIFI_AUTH_WAPI_PSK: + return "wapi"; + default: + return "unknown"; + } +} + +static esp_err_t wifi_manager_send_prov_json(httpd_req_t *req, bool ok, const char *error_msg) { + char payload[192] = {0}; + if (ok) { + strlcpy(payload, "{\"ok\":true}", sizeof(payload)); + } else { + snprintf(payload, + sizeof(payload), + "{\"ok\":false,\"error\":\"%s\"}", + error_msg != NULL ? error_msg : "unknown"); + } + + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Cache-Control", "no-store"); + return httpd_resp_sendstr(req, payload); +} + +static esp_err_t wifi_manager_scan_get_handler(httpd_req_t *req) { + wifi_scan_config_t scan_cfg = { + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + }; + + esp_err_t err = esp_wifi_scan_start(&scan_cfg, true); + if (err != ESP_OK) { + runtime_diag_record_error("wifi_scan", err, "esp_wifi_scan_start failed"); + return wifi_manager_send_prov_json(req, false, "scan_failed"); + } + + uint16_t ap_count = 0; + err = esp_wifi_scan_get_ap_num(&ap_count); + if (err != ESP_OK) { + runtime_diag_record_error("wifi_scan", err, "esp_wifi_scan_get_ap_num failed"); + return wifi_manager_send_prov_json(req, false, "scan_failed"); + } + + if (ap_count > WIFI_SCAN_RESULT_LIMIT) { + ap_count = WIFI_SCAN_RESULT_LIMIT; + } + + wifi_ap_record_t *records = NULL; + if (ap_count > 0) { + records = calloc(ap_count, sizeof(*records)); + if (records == NULL) { + return wifi_manager_send_prov_json(req, false, "oom"); + } + + uint16_t fetch_num = ap_count; + err = esp_wifi_scan_get_ap_records(&fetch_num, records); + if (err != ESP_OK) { + free(records); + runtime_diag_record_error("wifi_scan", err, "esp_wifi_scan_get_ap_records failed"); + return wifi_manager_send_prov_json(req, false, "scan_failed"); + } + ap_count = fetch_num; + } + + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Cache-Control", "no-store"); + + err = httpd_resp_sendstr_chunk(req, "{\"ok\":true,\"networks\":["); + if (err != ESP_OK) { + free(records); + return err; + } + + bool first = true; + char seen[WIFI_SCAN_RESULT_LIMIT][WIFI_STA_SSID_MAX_LEN + 1] = {{0}}; + size_t seen_count = 0; + + for (uint16_t i = 0; i < ap_count; ++i) { + char ssid_buf[WIFI_STA_SSID_MAX_LEN + 1] = {0}; + memcpy(ssid_buf, records[i].ssid, WIFI_STA_SSID_MAX_LEN); + if (ssid_buf[0] == '\0') { + continue; + } + + bool duplicate = false; + for (size_t j = 0; j < seen_count; ++j) { + if (strncmp(seen[j], ssid_buf, WIFI_STA_SSID_MAX_LEN) == 0) { + duplicate = true; + break; + } + } + if (duplicate) { + continue; + } + + if (seen_count < WIFI_SCAN_RESULT_LIMIT) { + strlcpy(seen[seen_count], ssid_buf, sizeof(seen[seen_count])); + seen_count++; + } + + char ssid_encoded[(WIFI_STA_SSID_MAX_LEN * 3) + 1]; + wifi_manager_url_encode(ssid_buf, ssid_encoded, sizeof(ssid_encoded)); + + char item[196]; + snprintf(item, + sizeof(item), + "%s{\"ssid\":\"%s\",\"rssi\":%d,\"auth\":\"%s\"}", + first ? "" : ",", + ssid_encoded, + records[i].rssi, + wifi_manager_authmode_to_str(records[i].authmode)); + + err = httpd_resp_sendstr_chunk(req, item); + if (err != ESP_OK) { + free(records); + return err; + } + first = false; + } + + free(records); + err = httpd_resp_sendstr_chunk(req, "]}"); + if (err == ESP_OK) { + err = httpd_resp_sendstr_chunk(req, NULL); + } + return err; +} + +static esp_err_t wifi_manager_connect_sta(const wifi_sta_credentials_t *creds, bool keep_softap) { + if (creds == NULL || creds->ssid[0] == '\0') { + return ESP_ERR_INVALID_ARG; + } + + wifi_config_t wifi_config = {0}; + strlcpy((char *)wifi_config.sta.ssid, creds->ssid, sizeof(wifi_config.sta.ssid)); + strlcpy((char *)wifi_config.sta.password, creds->password, sizeof(wifi_config.sta.password)); + wifi_config.sta.threshold.authmode = WIFI_AUTH_OPEN; wifi_config.sta.pmf_cfg.capable = true; wifi_config.sta.pmf_cfg.required = false; - esp_err_t rc = esp_wifi_set_mode(WIFI_MODE_STA); - if (rc != ESP_OK) { - return rc; + xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT); + s_retry_num = 0; + s_ready = false; + + s_sta_reconnect_enabled = false; + esp_err_t err = esp_wifi_disconnect(); + if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_STARTED && err != ESP_ERR_WIFI_CONN) { + ESP_LOGW(TAG, "esp_wifi_disconnect before connect failed, err=0x%x", (unsigned)err); } - rc = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); - if (rc != ESP_OK) { - return rc; - } - rc = esp_wifi_start(); - if (rc != ESP_OK && rc != ESP_ERR_INVALID_STATE) { - return rc; + s_sta_reconnect_enabled = true; + + err = esp_wifi_set_mode(keep_softap ? WIFI_MODE_APSTA : WIFI_MODE_STA); + if (err != ESP_OK) { + wifi_manager_record_connect_result(err, "set mode failed"); + return err; } - ESP_LOGI(TAG, "wifi_init_sta finished"); + err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); + if (err != ESP_OK) { + wifi_manager_record_connect_result(err, "set sta config failed"); + return err; + } + + err = esp_wifi_start(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + wifi_manager_record_connect_result(err, "wifi start failed"); + return err; + } + + err = esp_wifi_connect(); + if (err != ESP_OK && err != ESP_ERR_WIFI_CONN) { + wifi_manager_record_connect_result(err, "wifi connect failed"); + return err; + } + ESP_LOGI(TAG, "trying to connect to SSID:%s", creds->ssid); EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, @@ -128,7 +524,7 @@ static esp_err_t start_sta_mode(void) { pdMS_TO_TICKS(runtime_policy_wifi_connect_timeout_ms())); if (bits & WIFI_CONNECTED_BIT) { - ESP_LOGI(TAG, "connected to AP SSID:%s", CONFIG_TQ_WIFI_SSID); + ESP_LOGI(TAG, "connected to AP SSID:%s", creds->ssid); wifi_manager_apply_sta_throughput_profile(); esp_err_t ps_rc = esp_wifi_set_ps(WIFI_PS_NONE); if (ps_rc == ESP_OK) { @@ -137,18 +533,243 @@ static esp_err_t start_sta_mode(void) { ESP_LOGW(TAG, "failed to set WIFI_PS_NONE, rc=0x%x", (unsigned)ps_rc); } s_ready = true; + wifi_manager_record_connect_result(ESP_OK, "connect success"); return ESP_OK; } if (bits & WIFI_FAIL_BIT) { - ESP_LOGW(TAG, "Failed to connect to SSID:%s", CONFIG_TQ_WIFI_SSID); + ESP_LOGW(TAG, "failed to connect to SSID:%s", creds->ssid); + wifi_manager_record_connect_result(ESP_FAIL, "connect failed after retries"); return ESP_FAIL; } - ESP_LOGW(TAG, "Wi-Fi connect timeout"); + ESP_LOGW(TAG, "Wi-Fi connect timeout, SSID:%s", creds->ssid); + wifi_manager_record_connect_result(ESP_ERR_TIMEOUT, "connect timeout"); return ESP_ERR_TIMEOUT; } +static esp_err_t wifi_manager_prov_index_get_handler(httpd_req_t *req) { + httpd_resp_set_type(req, "text/html"); + httpd_resp_set_hdr(req, "Cache-Control", "no-store"); + size_t page_len = (size_t)(s_provision_page_end - s_provision_page_start); + return httpd_resp_send(req, (const char *)s_provision_page_start, page_len); +} + +static esp_err_t wifi_manager_prov_submit_post_handler(httpd_req_t *req) { + if (req->content_len <= 0 || req->content_len >= WIFI_PROV_HTTP_BODY_MAX_LEN) { + return wifi_manager_send_prov_json(req, false, "invalid_payload"); + } + + char body[WIFI_PROV_HTTP_BODY_MAX_LEN] = {0}; + int received_total = 0; + + while (received_total < req->content_len) { + int ret = httpd_req_recv(req, body + received_total, req->content_len - received_total); + if (ret > 0) { + received_total += ret; + continue; + } + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + continue; + } + return wifi_manager_send_prov_json(req, false, "read_failed"); + } + + body[received_total] = '\0'; + + wifi_sta_credentials_t creds = {0}; + bool has_ssid = wifi_manager_form_get_value(body, "ssid", creds.ssid, sizeof(creds.ssid)); + (void)wifi_manager_form_get_value(body, "password", creds.password, sizeof(creds.password)); + + if (!has_ssid || creds.ssid[0] == '\0') { + return wifi_manager_send_prov_json(req, false, "ssid_required"); + } + + size_t password_len = strlen(creds.password); + if (password_len > 0 && password_len < 8) { + return wifi_manager_send_prov_json(req, false, "password_too_short"); + } + + esp_err_t err = wifi_manager_connect_sta(&creds, true); + if (err != ESP_OK) { + return wifi_manager_send_prov_json(req, false, "connect_failed"); + } + + err = wifi_manager_save_credentials(&creds); + if (err != ESP_OK) { + runtime_diag_record_error("wifi_provision", err, "save credentials failed"); + s_ready = false; + (void)esp_wifi_disconnect(); + return wifi_manager_send_prov_json(req, false, "save_failed"); + } + + ESP_LOGI(TAG, "Wi-Fi provisioning succeeded, credentials stored in NVS"); + if (s_wifi_event_group != NULL) { + xEventGroupSetBits(s_wifi_event_group, WIFI_PROV_DONE_BIT); + } + + return wifi_manager_send_prov_json(req, true, NULL); +} + +static esp_err_t wifi_manager_start_prov_http_server(void) { + if (s_prov_httpd != NULL) { + return ESP_OK; + } + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = 80; + config.max_uri_handlers = 8; + + esp_err_t err = httpd_start(&s_prov_httpd, &config); + if (err != ESP_OK) { + return err; + } + + const httpd_uri_t index_uri = { + .uri = "/", + .method = HTTP_GET, + .handler = wifi_manager_prov_index_get_handler, + .user_ctx = NULL, + }; + err = httpd_register_uri_handler(s_prov_httpd, &index_uri); + if (err != ESP_OK) { + httpd_stop(s_prov_httpd); + s_prov_httpd = NULL; + return err; + } + + const httpd_uri_t scan_uri = { + .uri = "/scan", + .method = HTTP_GET, + .handler = wifi_manager_scan_get_handler, + .user_ctx = NULL, + }; + err = httpd_register_uri_handler(s_prov_httpd, &scan_uri); + if (err != ESP_OK) { + httpd_stop(s_prov_httpd); + s_prov_httpd = NULL; + return err; + } + + const httpd_uri_t submit_uri = { + .uri = "/provision", + .method = HTTP_POST, + .handler = wifi_manager_prov_submit_post_handler, + .user_ctx = NULL, + }; + err = httpd_register_uri_handler(s_prov_httpd, &submit_uri); + if (err != ESP_OK) { + httpd_stop(s_prov_httpd); + s_prov_httpd = NULL; + return err; + } + + return ESP_OK; +} + +static void wifi_manager_stop_prov_http_server(void) { + if (s_prov_httpd == NULL) { + return; + } + + httpd_stop(s_prov_httpd); + s_prov_httpd = NULL; +} + +static esp_err_t wifi_manager_start_softap_provisioning(void) { + wifi_config_t ap_config = {0}; + + const char *ap_ssid = CONFIG_TQ_WIFI_PROV_SOFTAP_SSID; + if (ap_ssid == NULL || ap_ssid[0] == '\0') { + ap_ssid = "TalkingQ-Setup"; + } + + strlcpy((char *)ap_config.ap.ssid, ap_ssid, sizeof(ap_config.ap.ssid)); + ap_config.ap.ssid_len = strlen((const char *)ap_config.ap.ssid); + ap_config.ap.channel = CONFIG_TQ_WIFI_PROV_SOFTAP_CHANNEL; + ap_config.ap.max_connection = CONFIG_TQ_WIFI_PROV_SOFTAP_MAX_CONN; + ap_config.ap.pmf_cfg.capable = true; + ap_config.ap.pmf_cfg.required = false; + + const char *ap_password = CONFIG_TQ_WIFI_PROV_SOFTAP_PASSWORD; + if (ap_password != NULL && strlen(ap_password) >= 8) { + strlcpy((char *)ap_config.ap.password, ap_password, sizeof(ap_config.ap.password)); + ap_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK; + } else { + ap_config.ap.password[0] = '\0'; + ap_config.ap.authmode = WIFI_AUTH_OPEN; + } + + xEventGroupClearBits(s_wifi_event_group, WIFI_PROV_DONE_BIT | WIFI_CONNECTED_BIT | WIFI_FAIL_BIT); + + esp_err_t err = esp_wifi_set_mode(WIFI_MODE_APSTA); + if (err != ESP_OK) { + return err; + } + + err = esp_wifi_set_config(WIFI_IF_AP, &ap_config); + if (err != ESP_OK) { + return err; + } + + err = esp_wifi_start(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + return err; + } + + err = wifi_manager_start_prov_http_server(); + if (err != ESP_OK) { + return err; + } + + s_provisioning_active = true; + + ESP_LOGI(TAG, + "SoftAP provisioning started: ssid=%s password=%s ip=192.168.4.1", + ap_ssid, + (ap_password != NULL && strlen(ap_password) >= 8) ? "***" : ""); + ESP_LOGI(TAG, "open http://192.168.4.1 from a phone/computer connected to this AP"); + + return ESP_OK; +} + +static void wifi_manager_stop_softap_provisioning(void) { + if (!s_provisioning_active) { + return; + } + + wifi_manager_stop_prov_http_server(); + + esp_err_t err = esp_wifi_set_mode(WIFI_MODE_STA); + if (err != ESP_OK) { + ESP_LOGW(TAG, "failed to switch to STA mode after provisioning, err=0x%x", (unsigned)err); + } + + s_provisioning_active = false; +} + +static esp_err_t wifi_manager_run_softap_provisioning(void) { + esp_err_t err = wifi_manager_start_softap_provisioning(); + if (err != ESP_OK) { + return err; + } + + while (s_started) { + EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, + WIFI_PROV_DONE_BIT, + pdTRUE, + pdFALSE, + pdMS_TO_TICKS(500)); + if (bits & WIFI_PROV_DONE_BIT) { + break; + } + } + + wifi_manager_stop_softap_provisioning(); + + return s_ready ? ESP_OK : ESP_ERR_INVALID_STATE; +} + esp_err_t wifi_manager_start(void) { if (s_started) { return s_ready ? ESP_OK : ESP_ERR_INVALID_STATE; @@ -156,6 +777,8 @@ esp_err_t wifi_manager_start(void) { s_retry_num = 0; s_ready = false; + s_sta_reconnect_enabled = true; + s_provisioning_active = false; if (s_wifi_event_group == NULL) { s_wifi_event_group = xEventGroupCreate(); @@ -163,7 +786,7 @@ esp_err_t wifi_manager_start(void) { return ESP_ERR_NO_MEM; } } - xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT); + xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT | WIFI_PROV_DONE_BIT); esp_err_t err = esp_netif_init(); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { @@ -185,6 +808,14 @@ esp_err_t wifi_manager_start(void) { } } + if (s_ap_netif == NULL) { + s_ap_netif = esp_netif_create_default_wifi_ap(); + if (s_ap_netif == NULL) { + runtime_diag_record_error("wifi_start", ESP_ERR_NO_MEM, "create ap netif failed"); + return ESP_ERR_NO_MEM; + } + } + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); err = esp_wifi_init(&cfg); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { @@ -192,6 +823,12 @@ esp_err_t wifi_manager_start(void) { return err; } + err = esp_wifi_set_storage(WIFI_STORAGE_RAM); + if (err != ESP_OK) { + runtime_diag_record_error("wifi_start", err, "esp_wifi_set_storage RAM failed"); + return err; + } + if (s_wifi_event_inst == NULL) { err = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, @@ -217,18 +854,26 @@ esp_err_t wifi_manager_start(void) { } s_started = true; - err = start_sta_mode(); + + wifi_sta_credentials_t saved = {0}; + err = wifi_manager_load_credentials(&saved); if (err == ESP_OK) { - runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_SUCCESS, 1); - } else { - if (err == ESP_ERR_TIMEOUT) { - runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_TIMEOUT, 1); - } else { - runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_FAILED, 1); + ESP_LOGI(TAG, "found saved Wi-Fi credentials for SSID:%s", saved.ssid); + err = wifi_manager_connect_sta(&saved, false); + if (err == ESP_OK) { + return ESP_OK; } - runtime_diag_record_error("wifi_connect", err, "STA connect failed"); + ESP_LOGW(TAG, "saved Wi-Fi credentials failed, starting SoftAP provisioning"); + } else { + ESP_LOGI(TAG, "no valid saved Wi-Fi credentials, starting SoftAP provisioning"); + } + + err = wifi_manager_run_softap_provisioning(); + if (err != ESP_OK) { + runtime_diag_record_error("wifi_start", err, "SoftAP provisioning failed"); (void)wifi_manager_stop(); } + return err; } @@ -241,6 +886,9 @@ esp_err_t wifi_manager_stop(void) { s_ready = false; s_retry_num = 0; + s_sta_reconnect_enabled = false; + + wifi_manager_stop_softap_provisioning(); if (s_wifi_event_inst != NULL) { esp_err_t err = esp_event_handler_instance_unregister(WIFI_EVENT, @@ -288,6 +936,11 @@ esp_err_t wifi_manager_stop(void) { s_sta_netif = NULL; } + if (s_ap_netif != NULL) { + esp_netif_destroy_default_wifi(s_ap_netif); + s_ap_netif = NULL; + } + if (s_wifi_event_group != NULL) { vEventGroupDelete(s_wifi_event_group); s_wifi_event_group = NULL; diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 0434daf..a89225b 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -1,12 +1,22 @@ menu "TQ Controller Config" -config TQ_WIFI_SSID - string "Wi-Fi SSID" +config TQ_WIFI_PROV_SOFTAP_SSID + string "Wi-Fi provisioning SoftAP SSID" + default "TalkingQ-Setup" + +config TQ_WIFI_PROV_SOFTAP_PASSWORD + string "Wi-Fi provisioning SoftAP Password (empty means open AP)" default "" -config TQ_WIFI_PASSWORD - string "Wi-Fi Password" - default "" +config TQ_WIFI_PROV_SOFTAP_CHANNEL + int "Wi-Fi provisioning SoftAP channel" + range 1 13 + default 1 + +config TQ_WIFI_PROV_SOFTAP_MAX_CONN + int "Wi-Fi provisioning SoftAP max clients" + range 1 8 + default 4 config TQ_WIFI_MAXIMUM_RETRY int "Wi-Fi Maximum Retry" diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 7071698..9f82439 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -4,7 +4,7 @@ CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ROLE_CENTRAL=y CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=247 -# CONFIG_ESP_WIFI_SOFTAP_SUPPORT is not set +CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=n CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=24 CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=256 @@ -18,8 +18,10 @@ CONFIG_LWIP_TCP_SND_BUF_DEFAULT=57344 CONFIG_LWIP_TCP_WND_DEFAULT=262144 CONFIG_LWIP_TCP_RECVMBOX_SIZE=192 CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=256 -CONFIG_TQ_WIFI_SSID="TalkingQ" -CONFIG_TQ_WIFI_PASSWORD="TalkingQ123" +CONFIG_TQ_WIFI_PROV_SOFTAP_SSID="TalkingQ-Setup" +CONFIG_TQ_WIFI_PROV_SOFTAP_PASSWORD="" +CONFIG_TQ_WIFI_PROV_SOFTAP_CHANNEL=1 +CONFIG_TQ_WIFI_PROV_SOFTAP_MAX_CONN=4 CONFIG_TQ_Z_IMAGE_DEFAULT_SIZE="512*768" CONFIG_TQ_Z_IMAGE_HTTP_BUFFER_BYTES=16384 CONFIG_TQ_Z_IMAGE_HTTP_READ_CHUNK_BYTES=16384