#include "wifi_portal.h" #include #include #include "esp_check.h" #include "esp_err.h" #include "esp_log.h" #include "esp_timer.h" #include "esp_wifi.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" #include "platform.h" #include "sdkconfig.h" #include "ssid_manager.h" #include "wifi_manager.h" #if __has_include("wifi_storage_hooks.h") #include "wifi_storage_hooks.h" #define WIFI_PORTAL_HAS_STORAGE_HOOKS 1 #else #define WIFI_PORTAL_HAS_STORAGE_HOOKS 0 #endif static const char *TAG = "wifi_portal"; #define WIFI_PORTAL_LOCK_TIMEOUT_MS 2000 #define WIFI_PORTAL_DEFAULT_CONNECT_TIMEOUT_MS 10000 #define WIFI_PORTAL_CONNECT_POLL_MS 100 #ifndef CONFIG_TQ_WIFI_STA_POWER_SAVE #define CONFIG_TQ_WIFI_STA_POWER_SAVE 0 #endif #ifndef CONFIG_TQ_WIFI_STA_INACTIVE_TIME_SEC #define CONFIG_TQ_WIFI_STA_INACTIVE_TIME_SEC 15 #endif static SemaphoreHandle_t s_portal_mutex; #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) static StaticSemaphore_t s_portal_mutex_buf; #endif static bool s_wifi_manager_initialized; static std::string s_config_ap_ssid_prefix = "TQ-CAT"; static wifi_portal_behavior_hooks_t s_behavior_hooks = {}; static esp_err_t apply_sta_stability_policy(void); static void install_wifi_storage_hooks(void) { static bool s_installed; if (s_installed) { return; } #if WIFI_PORTAL_HAS_STORAGE_HOOKS const wifi_storage_hooks_t hooks = { .set_string = platform_storage_set_string, .get_string = platform_storage_get_string, .set_u8 = platform_storage_set_u8, .get_u8 = platform_storage_get_u8, .set_i8 = platform_storage_set_i8, .get_i8 = platform_storage_get_i8, .erase = platform_storage_erase, }; wifi_storage_set_hooks(&hooks); #else ESP_LOGW(TAG, "Wi-Fi storage hook API unavailable; using component default storage"); #endif s_installed = true; } static void log_saved_wifi_candidates(const char *context) { install_wifi_storage_hooks(); const auto &ssid_list = SsidManager::GetInstance().GetSsidList(); ESP_LOGI(TAG, "%s: %u saved Wi-Fi credential candidate(s)", context != NULL ? context : "Wi-Fi credentials", (unsigned)ssid_list.size()); for (size_t i = 0; i < ssid_list.size(); ++i) { ESP_LOGI(TAG, "%s: candidate[%u] SSID=\"%s\"", context != NULL ? context : "Wi-Fi credentials", (unsigned)i, ssid_list[i].ssid.c_str()); } } struct PortalUnlockGuard { ~PortalUnlockGuard() { if (active) { (void)xSemaphoreGiveRecursive(s_portal_mutex); } } bool active = true; }; static esp_err_t wifi_portal_lock(void) { if (!s_portal_mutex) { #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) s_portal_mutex = xSemaphoreCreateRecursiveMutexStatic(&s_portal_mutex_buf); #else s_portal_mutex = xSemaphoreCreateRecursiveMutex(); #endif if (!s_portal_mutex) { return ESP_ERR_NO_MEM; } } TickType_t timeout_ticks = pdMS_TO_TICKS(WIFI_PORTAL_LOCK_TIMEOUT_MS); if (timeout_ticks == 0) { timeout_ticks = 1; } if (xSemaphoreTakeRecursive(s_portal_mutex, timeout_ticks) != pdTRUE) { return ESP_ERR_TIMEOUT; } return ESP_OK; } static wifi_portal_behavior_hooks_t snapshot_behavior_hooks(void) { wifi_portal_behavior_hooks_t hooks = {}; if (wifi_portal_lock() != ESP_OK) { return hooks; } hooks = s_behavior_hooks; (void)xSemaphoreGiveRecursive(s_portal_mutex); return hooks; } static void notify_keep_awake(bool keep_awake) { wifi_portal_behavior_hooks_t hooks = snapshot_behavior_hooks(); if (hooks.set_keep_awake != NULL) { hooks.set_keep_awake(keep_awake); } } static void wifi_manager_event_callback(WifiEvent event, const std::string &data) { switch (event) { case WifiEvent::Connecting: ESP_LOGI(TAG, "Connecting to \"%s\"", data.c_str()); break; case WifiEvent::Connected: ESP_LOGI(TAG, "Connected to \"%s\"", data.c_str()); (void)apply_sta_stability_policy(); break; case WifiEvent::Disconnected: if (!data.empty()) { ESP_LOGW(TAG, "Wi-Fi disconnected, reason %s", data.c_str()); } break; case WifiEvent::ConfigModeEnter: ESP_LOGI(TAG, "Wi-Fi configuration portal started"); break; case WifiEvent::ConfigModeExit: ESP_LOGI(TAG, "Wi-Fi configuration portal stopped"); notify_keep_awake(false); break; case WifiEvent::Scanning: default: break; } } static esp_err_t ensure_wifi_manager_initialized(void) { install_wifi_storage_hooks(); if (s_wifi_manager_initialized) { return ESP_OK; } WifiManagerConfig config; config.ssid_prefix = s_config_ap_ssid_prefix; config.language = "zh-CN"; auto &manager = WifiManager::GetInstance(); if (!manager.Initialize(config)) { ESP_LOGE(TAG, "Wi-Fi manager initialization failed"); return ESP_FAIL; } manager.SetEventCallback(wifi_manager_event_callback); s_wifi_manager_initialized = true; return ESP_OK; } static void seed_default_credentials(const char *ssid, const char *password) { install_wifi_storage_hooks(); auto &manager = SsidManager::GetInstance(); const auto saved_count = manager.GetSsidList().size(); if (ssid == NULL || ssid[0] == '\0') { ESP_LOGI(TAG, "No configured Wi-Fi SSID; using %u saved credential candidate(s)", (unsigned)saved_count); log_saved_wifi_candidates("Configured Wi-Fi empty; NVS fallback"); return; } bool configured_ssid_saved = false; for (const auto &item : manager.GetSsidList()) { if (item.ssid == ssid) { configured_ssid_saved = true; break; } } if (saved_count > 0U && !configured_ssid_saved) { ESP_LOGI(TAG, "Configured seed SSID changed to \"%s\"; replacing %u stale credential candidate(s)", ssid, (unsigned)saved_count); manager.Clear(); } manager.AddSsid(ssid, password != NULL ? password : ""); ESP_LOGI(TAG, "Ensured configured Wi-Fi credentials for SSID \"%s\" with %u previous candidate(s)", ssid, (unsigned)saved_count); } static int normalize_connect_timeout_ms(int timeout_ms) { return timeout_ms > 0 ? timeout_ms : WIFI_PORTAL_DEFAULT_CONNECT_TIMEOUT_MS; } static esp_err_t apply_sta_stability_policy(void) { wifi_ps_type_t ps_type = CONFIG_TQ_WIFI_STA_POWER_SAVE ? WIFI_PS_MIN_MODEM : WIFI_PS_NONE; esp_err_t err = esp_wifi_set_ps(ps_type); if (err != ESP_OK) { ESP_LOGW(TAG, "Set Wi-Fi power save failed: %s", esp_err_to_name(err)); return err; } uint16_t inactive_sec = CONFIG_TQ_WIFI_STA_INACTIVE_TIME_SEC; if (inactive_sec < 3U) { inactive_sec = 3U; } err = esp_wifi_set_inactive_time(WIFI_IF_STA, inactive_sec); if (err != ESP_OK) { ESP_LOGW(TAG, "Set Wi-Fi STA inactive time to %u s failed: %s", (unsigned)inactive_sec, esp_err_to_name(err)); return err; } ESP_LOGI(TAG, "Wi-Fi STA stability policy: ps=%s inactive=%us", ps_type == WIFI_PS_NONE ? "none" : "min_modem", (unsigned)inactive_sec); return ESP_OK; } esp_err_t wifi_portal_init_defaults(const char *seed_ssid, const char *seed_password) { ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); PortalUnlockGuard guard; seed_default_credentials(seed_ssid, seed_password); return ESP_OK; } bool wifi_portal_has_saved_credentials(void) { install_wifi_storage_hooks(); return !SsidManager::GetInstance().GetSsidList().empty(); } bool wifi_portal_sta_is_connected(void) { if (!s_wifi_manager_initialized) { return false; } return WifiManager::GetInstance().IsConnected(); } esp_err_t wifi_portal_start_config_ap(const char *ssid_prefix) { ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); PortalUnlockGuard guard; if (ssid_prefix != NULL && ssid_prefix[0] != '\0') { s_config_ap_ssid_prefix = ssid_prefix; } ESP_RETURN_ON_ERROR(ensure_wifi_manager_initialized(), TAG, "Failed to initialize Wi-Fi manager"); WifiManager::GetInstance().StartConfigAp(); return ESP_OK; } esp_err_t wifi_portal_connect_sta(int timeout_ms) { { ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); PortalUnlockGuard guard; if (!wifi_portal_has_saved_credentials()) { return ESP_ERR_NOT_FOUND; } ESP_LOGI(TAG, "Starting STA connection with %u saved credential candidate(s)", (unsigned)SsidManager::GetInstance().GetSsidList().size()); log_saved_wifi_candidates("STA connect"); ESP_RETURN_ON_ERROR(ensure_wifi_manager_initialized(), TAG, "Failed to initialize Wi-Fi manager"); auto &manager = WifiManager::GetInstance(); if (manager.IsConnected()) { (void)apply_sta_stability_policy(); return ESP_OK; } manager.StartStation(); (void)apply_sta_stability_policy(); } int effective_timeout_ms = normalize_connect_timeout_ms(timeout_ms); int64_t deadline_us = esp_timer_get_time() + (int64_t)effective_timeout_ms * 1000; while (esp_timer_get_time() < deadline_us) { if (WifiManager::GetInstance().IsConnected()) { return ESP_OK; } vTaskDelay(pdMS_TO_TICKS(WIFI_PORTAL_CONNECT_POLL_MS)); } WifiManager::GetInstance().StopStation(); ESP_LOGW(TAG, "Wi-Fi connect timed out after %d ms", effective_timeout_ms); return ESP_ERR_TIMEOUT; } esp_err_t wifi_portal_reconnect_sta(int timeout_ms) { int effective_timeout_ms = normalize_connect_timeout_ms(timeout_ms); int64_t deadline_us = esp_timer_get_time() + (int64_t)effective_timeout_ms * 1000; { ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); PortalUnlockGuard guard; if (!wifi_portal_has_saved_credentials()) { return ESP_ERR_NOT_FOUND; } ESP_RETURN_ON_ERROR(ensure_wifi_manager_initialized(), TAG, "Failed to initialize Wi-Fi manager"); ESP_LOGW(TAG, "Controlled STA reassociation started timeout_ms=%d connected=%d", effective_timeout_ms, WifiManager::GetInstance().IsConnected() ? 1 : 0); esp_err_t disconnect_err = esp_wifi_disconnect(); if (disconnect_err != ESP_OK && disconnect_err != ESP_ERR_WIFI_NOT_CONNECT) { ESP_LOGW(TAG, "Controlled STA disconnect returned %s", esp_err_to_name(disconnect_err)); } } int64_t disconnect_deadline_us = esp_timer_get_time() + 2000LL * 1000LL; if (disconnect_deadline_us > deadline_us) { disconnect_deadline_us = deadline_us; } while (esp_timer_get_time() < disconnect_deadline_us && WifiManager::GetInstance().IsConnected()) { vTaskDelay(pdMS_TO_TICKS(WIFI_PORTAL_CONNECT_POLL_MS)); } esp_err_t connect_err = esp_wifi_connect(); if (connect_err != ESP_OK) { ESP_LOGI(TAG, "Controlled STA reconnect command returned %s; wait for manager auto-reconnect", esp_err_to_name(connect_err)); } while (esp_timer_get_time() < deadline_us) { if (WifiManager::GetInstance().IsConnected()) { (void)apply_sta_stability_policy(); ESP_LOGI(TAG, "Controlled STA reassociation completed"); return ESP_OK; } vTaskDelay(pdMS_TO_TICKS(WIFI_PORTAL_CONNECT_POLL_MS)); } ESP_LOGW(TAG, "Controlled STA reassociation timed out after %d ms", effective_timeout_ms); return ESP_ERR_TIMEOUT; } esp_err_t wifi_portal_save_credentials(const char *ssid, const char *password) { install_wifi_storage_hooks(); ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); PortalUnlockGuard guard; ESP_RETURN_ON_FALSE(ssid != NULL && ssid[0] != '\0', ESP_ERR_INVALID_ARG, TAG, "SSID missing"); ESP_RETURN_ON_FALSE(strlen(ssid) <= 32, ESP_ERR_INVALID_ARG, TAG, "SSID too long"); ESP_RETURN_ON_FALSE(password == NULL || strlen(password) <= 64, ESP_ERR_INVALID_ARG, TAG, "Password too long"); SsidManager::GetInstance().AddSsid(ssid, password != NULL ? password : ""); ESP_LOGI(TAG, "Saved Wi-Fi credentials for SSID \"%s\"", ssid); return ESP_OK; } esp_err_t wifi_portal_clear_saved_credentials(void) { install_wifi_storage_hooks(); ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); PortalUnlockGuard guard; SsidManager::GetInstance().Clear(); ESP_LOGI(TAG, "Cleared stored Wi-Fi credentials"); return ESP_OK; } void wifi_portal_set_behavior_hooks(const wifi_portal_behavior_hooks_t *hooks) { if (wifi_portal_lock() != ESP_OK) { return; } if (hooks == NULL) { memset(&s_behavior_hooks, 0, sizeof(s_behavior_hooks)); } else { s_behavior_hooks = *hooks; } (void)xSemaphoreGiveRecursive(s_portal_mutex); }