/* SPDX-License-Identifier: GPL-3.0-only */ /* Native ESP-IDF Wi-Fi lifecycle and connection policy manager. */ #include "wifi_manager.h" #include #include #include #include "esp_event.h" #include "esp_log.h" #include "esp_netif.h" #include "esp_timer.h" #include "esp_wifi.h" #include "esp_wifi_default.h" #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "freertos/semphr.h" #include "freertos/task.h" #define WIFI_MANAGER_QUEUE_LENGTH 16U #define WIFI_MANAGER_TASK_STACK_SIZE 6144U #define WIFI_MANAGER_TASK_PRIORITY 5U #define WIFI_MANAGER_ATTEMPT_US (12LL * 1000LL * 1000LL) #define WIFI_MANAGER_STABLE_US (30LL * 1000LL * 1000LL) #define WIFI_MANAGER_DISCONNECT_SETTLE_US (1LL * 1000LL * 1000LL) #define WIFI_MANAGER_INITIAL_BACKOFF_SECONDS 2U #define WIFI_MANAGER_MAX_BACKOFF_SECONDS 60U static const char *const TAG = "wifi_manager"; typedef enum { MESSAGE_COMMAND_START = 0, MESSAGE_COMMAND_STOP, MESSAGE_COMMAND_APPLY, MESSAGE_COMMAND_RECONNECT, MESSAGE_COMMAND_NEXT_PROFILE, MESSAGE_STA_CONNECTED, MESSAGE_STA_DISCONNECTED, MESSAGE_STA_GOT_IP, MESSAGE_STA_LOST_IP, MESSAGE_STA_STOPPED, MESSAGE_AP_STOPPED, MESSAGE_AP_CLIENT_JOINED, MESSAGE_AP_CLIENT_LEFT, } manager_message_type_t; typedef struct { manager_message_type_t type; union { struct { uint8_t channel; wifi_auth_mode_t auth; uint8_t ssid_len; uint8_t ssid[WIFI_CONFIG_SSID_MAX_LEN]; } connected; struct { uint16_t reason; } disconnected; struct { uint32_t ip; uint32_t netmask; uint32_t gateway; } got_ip; } data; } manager_message_t; typedef struct { wifi_app_config_t config; wifi_manager_snapshot_t snapshot; } manager_shared_t; typedef struct { bool radio_started; bool ap_enabled; bool associated; bool online; uint8_t profile_order[WIFI_CONFIG_STA_PROFILE_COUNT]; uint8_t profile_count; uint8_t next_profile; uint32_t next_backoff_seconds; uint32_t intentional_disconnects; bool advance_after_disconnect; bool stop_pending; bool restart_pending; bool restart_waits_for_ap_stop; int64_t attempt_deadline; int64_t disconnect_deadline; int64_t restart_deadline; int64_t backoff_deadline; int64_t stable_deadline; } manager_runtime_t; static SemaphoreHandle_t s_mutex; static QueueHandle_t s_queue; static TaskHandle_t s_task; static esp_netif_t *s_sta_netif; static esp_netif_t *s_ap_netif; static esp_event_handler_instance_t s_wifi_handler; static esp_event_handler_instance_t s_ip_handler; static bool s_event_loop_owned; static manager_shared_t s_shared; /* Event callbacks cannot take the manager mutex; this counter has its own lock. */ static portMUX_TYPE s_drop_mux = portMUX_INITIALIZER_UNLOCKED; static uint64_t s_queue_drops; static void manager_task(void *context); static void start_profile_cycle(manager_runtime_t *runtime); static void start_next_profile(manager_runtime_t *runtime); static void lock_shared(void) { (void)xSemaphoreTake(s_mutex, portMAX_DELAY); } static void unlock_shared(void) { (void)xSemaphoreGive(s_mutex); } static bool manager_is_started(void) { bool started; lock_shared(); started = s_shared.snapshot.started; unlock_shared(); return started; } static void count_queue_drop(void) { portENTER_CRITICAL(&s_drop_mux); ++s_queue_drops; portEXIT_CRITICAL(&s_drop_mux); } static bool enqueue_message(const manager_message_t *message) { if (xQueueSend(s_queue, message, 0) == pdTRUE) { return true; } count_queue_drop(); return false; } static void set_state(wifi_manager_state_t state) { lock_shared(); s_shared.snapshot.state = state; unlock_shared(); } static void set_last_error(esp_err_t error) { lock_shared(); s_shared.snapshot.last_error = error; unlock_shared(); } static void clear_station_network_snapshot(void) { lock_shared(); s_shared.snapshot.ip = 0U; s_shared.snapshot.netmask = 0U; s_shared.snapshot.gateway = 0U; s_shared.snapshot.sta_channel = 0U; s_shared.snapshot.sta_rssi = 0; s_shared.snapshot.sta_auth = WIFI_AUTH_OPEN; unlock_shared(); } static void set_active_profile(int8_t slot, const wifi_config_sta_profile_t *profile) { lock_shared(); s_shared.snapshot.active_profile = slot; memset(s_shared.snapshot.sta_ssid, 0, sizeof(s_shared.snapshot.sta_ssid)); s_shared.snapshot.sta_ssid_len = 0U; if (profile != NULL) { size_t length = profile->ssid_len; if (length > WIFI_CONFIG_SSID_MAX_LEN) { length = WIFI_CONFIG_SSID_MAX_LEN; } memcpy(s_shared.snapshot.sta_ssid, profile->ssid, length); s_shared.snapshot.sta_ssid[length] = '\0'; s_shared.snapshot.sta_ssid_len = (uint8_t)length; } unlock_shared(); } static void copy_working_config(wifi_app_config_t *config) { lock_shared(); *config = s_shared.config; unlock_shared(); } static bool connected_event_matches_active_profile(const manager_message_t *message) { bool matches = false; lock_shared(); int8_t slot = s_shared.snapshot.active_profile; if (slot >= 0 && slot < (int8_t)WIFI_CONFIG_STA_PROFILE_COUNT) { const wifi_config_sta_profile_t *profile = &s_shared.config.profiles[slot]; matches = profile->ssid_len == message->data.connected.ssid_len && memcmp(profile->ssid, message->data.connected.ssid, profile->ssid_len) == 0; } unlock_shared(); return matches; } static uint8_t build_profile_order(uint8_t *order, const wifi_app_config_t *config) { uint8_t count = 0U; /* Insertion sort preserves ascending slot order when priorities are equal. */ for (uint8_t slot = 0U; slot < WIFI_CONFIG_STA_PROFILE_COUNT; ++slot) { if (config->profiles[slot].enabled == 0U) { continue; } uint8_t position = count; while (position > 0U && config->profiles[order[position - 1U]].priority > config->profiles[slot].priority) { order[position] = order[position - 1U]; --position; } order[position] = slot; ++count; } return count; } static void note_ap_running(bool running, const wifi_app_config_t *config) { lock_shared(); bool changed = s_shared.snapshot.ap_running != running; s_shared.snapshot.ap_running = running; s_shared.snapshot.ap_channel = config->ap_channel; if (running) { s_shared.snapshot.ap_ssid_len = config->ap_ssid_len; memset(s_shared.snapshot.ap_ssid, 0, sizeof(s_shared.snapshot.ap_ssid)); memcpy(s_shared.snapshot.ap_ssid, config->ap_ssid, config->ap_ssid_len); } if (!running) { s_shared.snapshot.ap_client_count = 0U; } if (changed) { if (running) { ++s_shared.snapshot.counters.ap_starts; } else { ++s_shared.snapshot.counters.ap_stops; } } unlock_shared(); } static esp_err_t configure_ap(const wifi_app_config_t *config) { wifi_config_t wifi_config; memset(&wifi_config, 0, sizeof(wifi_config)); memcpy(wifi_config.ap.ssid, config->ap_ssid, config->ap_ssid_len); wifi_config.ap.ssid_len = config->ap_ssid_len; memcpy(wifi_config.ap.password, config->ap_psk, config->ap_psk_len); wifi_config.ap.channel = config->ap_channel; wifi_config.ap.authmode = WIFI_AUTH_WPA2_WPA3_PSK; wifi_config.ap.ssid_hidden = 0U; wifi_config.ap.max_connection = 4U; wifi_config.ap.pmf_cfg.capable = true; wifi_config.ap.pmf_cfg.required = false; wifi_config.ap.sae_pwe_h2e = WPA3_SAE_PWE_BOTH; esp_err_t error = esp_wifi_set_config(WIFI_IF_AP, &wifi_config); wifi_config_secure_wipe(&wifi_config, sizeof(wifi_config)); return error; } static esp_err_t configure_station(const wifi_config_sta_profile_t *profile) { wifi_config_t wifi_config; memset(&wifi_config, 0, sizeof(wifi_config)); memcpy(wifi_config.sta.ssid, profile->ssid, profile->ssid_len); memcpy(wifi_config.sta.password, profile->psk, profile->psk_len); wifi_config.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH; wifi_config.sta.pmf_cfg.capable = true; switch (profile->security) { case WIFI_CONFIG_SECURITY_MIXED: /* WPA2 is the minimum; SAE options also permit WPA3 and transition APs. */ wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; wifi_config.sta.pmf_cfg.required = false; break; case WIFI_CONFIG_SECURITY_WPA3: wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK; wifi_config.sta.pmf_cfg.required = true; break; default: wifi_config_secure_wipe(&wifi_config, sizeof(wifi_config)); return ESP_ERR_INVALID_ARG; } esp_err_t error = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); wifi_config_secure_wipe(&wifi_config, sizeof(wifi_config)); return error; } static esp_err_t set_runtime_ap_enabled(manager_runtime_t *runtime, bool enabled) { if (!runtime->radio_started) { runtime->ap_enabled = enabled; return ESP_OK; } if (runtime->ap_enabled == enabled) { return ESP_OK; } wifi_app_config_t config; copy_working_config(&config); esp_err_t error; if (enabled) { /* A live STA-to-APSTA transition starts the already-created AP netif. */ error = esp_wifi_set_mode(WIFI_MODE_APSTA); if (error == ESP_OK) { error = configure_ap(&config); } if (error == ESP_OK) { runtime->ap_enabled = true; note_ap_running(true, &config); } else { /* Do not leave an untracked default/stale AP active after failure. */ esp_err_t rollback_error = esp_wifi_set_mode(WIFI_MODE_STA); if (rollback_error != ESP_OK) { ESP_LOGW(TAG, "fallback AP rollback failed: %s", esp_err_to_name(rollback_error)); } } } else { error = esp_wifi_set_mode(WIFI_MODE_STA); if (error == ESP_OK) { runtime->ap_enabled = false; note_ap_running(false, &config); } } wifi_config_secure_wipe(&config, sizeof(config)); return error; } static void mark_intentional_disconnect(manager_runtime_t *runtime) { esp_err_t error = esp_wifi_disconnect(); if (error == ESP_OK) { /* The corresponding event is consumed without invoking retry policy. */ ++runtime->intentional_disconnects; } else if (error != ESP_ERR_WIFI_NOT_CONNECT) { set_last_error(error); } runtime->associated = false; runtime->online = false; clear_station_network_snapshot(); } static void stop_radio(manager_runtime_t *runtime) { if (!runtime->radio_started) { return; } if (runtime->associated || runtime->online) { mark_intentional_disconnect(runtime); } esp_err_t error = esp_wifi_stop(); if (error != ESP_OK && error != ESP_ERR_WIFI_NOT_STARTED) { set_last_error(error); ESP_LOGW(TAG, "esp_wifi_stop failed: %s", esp_err_to_name(error)); } wifi_app_config_t config; copy_working_config(&config); if (runtime->ap_enabled) { note_ap_running(false, &config); } wifi_config_secure_wipe(&config, sizeof(config)); runtime->radio_started = false; runtime->ap_enabled = false; runtime->associated = false; runtime->online = false; runtime->advance_after_disconnect = false; runtime->attempt_deadline = 0; runtime->disconnect_deadline = 0; runtime->backoff_deadline = 0; runtime->stable_deadline = 0; runtime->profile_count = 0U; runtime->next_profile = 0U; set_active_profile(-1, NULL); clear_station_network_snapshot(); } static void schedule_cycle_retry(manager_runtime_t *runtime) { wifi_app_config_t config; copy_working_config(&config); lock_shared(); ++s_shared.snapshot.counters.profile_cycles; unlock_shared(); if (config.ap_policy == WIFI_CONFIG_AP_POLICY_FALLBACK) { esp_err_t error = set_runtime_ap_enabled(runtime, true); if (error != ESP_OK) { set_last_error(error); ESP_LOGW(TAG, "failed to enable fallback AP: %s", esp_err_to_name(error)); } } uint32_t delay_seconds = runtime->next_backoff_seconds; if (delay_seconds < WIFI_MANAGER_INITIAL_BACKOFF_SECONDS) { delay_seconds = WIFI_MANAGER_INITIAL_BACKOFF_SECONDS; } runtime->backoff_deadline = esp_timer_get_time() + ((int64_t)delay_seconds * 1000LL * 1000LL); runtime->attempt_deadline = 0; runtime->disconnect_deadline = 0; runtime->stable_deadline = 0; runtime->associated = false; runtime->online = false; set_active_profile(-1, NULL); lock_shared(); ++s_shared.snapshot.counters.retries; s_shared.snapshot.retry_seconds = delay_seconds; s_shared.snapshot.state = WIFI_MANAGER_STATE_BACKOFF; unlock_shared(); if (delay_seconds >= WIFI_MANAGER_MAX_BACKOFF_SECONDS) { runtime->next_backoff_seconds = WIFI_MANAGER_MAX_BACKOFF_SECONDS; } else if (delay_seconds > WIFI_MANAGER_MAX_BACKOFF_SECONDS / 2U) { runtime->next_backoff_seconds = WIFI_MANAGER_MAX_BACKOFF_SECONDS; } else { runtime->next_backoff_seconds = delay_seconds * 2U; } wifi_config_secure_wipe(&config, sizeof(config)); } static void start_next_profile(manager_runtime_t *runtime) { wifi_app_config_t config; copy_working_config(&config); while (runtime->next_profile < runtime->profile_count) { uint8_t slot = runtime->profile_order[runtime->next_profile++]; const wifi_config_sta_profile_t *profile = &config.profiles[slot]; set_active_profile((int8_t)slot, profile); set_state(WIFI_MANAGER_STATE_CONNECTING); esp_err_t error = configure_station(profile); if (error == ESP_OK) { error = esp_wifi_connect(); } lock_shared(); ++s_shared.snapshot.counters.connect_attempts; unlock_shared(); if (error == ESP_OK) { runtime->attempt_deadline = esp_timer_get_time() + WIFI_MANAGER_ATTEMPT_US; runtime->backoff_deadline = 0; runtime->stable_deadline = 0; wifi_config_secure_wipe(&config, sizeof(config)); return; } set_last_error(error); ESP_LOGW(TAG, "profile %u connect attempt failed: %s", (unsigned)slot, esp_err_to_name(error)); } wifi_config_secure_wipe(&config, sizeof(config)); schedule_cycle_retry(runtime); } static void start_next_profile_after_current(manager_runtime_t *runtime) { wifi_app_config_t config; copy_working_config(&config); uint8_t order[WIFI_CONFIG_STA_PROFILE_COUNT]; uint8_t count = build_profile_order(order, &config); int8_t active_profile; lock_shared(); active_profile = s_shared.snapshot.active_profile; unlock_shared(); if (count == 0U) { wifi_config_secure_wipe(&config, sizeof(config)); set_last_error(ESP_ERR_NOT_FOUND); return; } uint8_t active_index = 0U; bool found_active = false; for (uint8_t index = 0U; index < count; ++index) { if (active_profile >= 0 && order[index] == (uint8_t)active_profile) { active_index = index; found_active = true; break; } } for (uint8_t index = 0U; index < count; ++index) { uint8_t source = found_active ? (uint8_t)((active_index + 1U + index) % count) : index; runtime->profile_order[index] = order[source]; } runtime->profile_count = count; runtime->next_profile = 0U; runtime->attempt_deadline = 0; runtime->disconnect_deadline = 0; runtime->backoff_deadline = 0; runtime->stable_deadline = 0; runtime->advance_after_disconnect = true; uint32_t intentional_disconnects = runtime->intentional_disconnects; mark_intentional_disconnect(runtime); if (runtime->intentional_disconnects == intentional_disconnects) { runtime->advance_after_disconnect = false; start_next_profile(runtime); } else { /* Recover if the bounded queue drops the matching disconnect event. */ runtime->disconnect_deadline = esp_timer_get_time() + WIFI_MANAGER_DISCONNECT_SETTLE_US; } wifi_config_secure_wipe(&config, sizeof(config)); } static void start_profile_cycle(manager_runtime_t *runtime) { wifi_app_config_t config; copy_working_config(&config); runtime->profile_count = build_profile_order(runtime->profile_order, &config); runtime->next_profile = 0U; runtime->advance_after_disconnect = false; runtime->attempt_deadline = 0; runtime->disconnect_deadline = 0; runtime->backoff_deadline = 0; runtime->stable_deadline = 0; runtime->associated = false; runtime->online = false; wifi_config_secure_wipe(&config, sizeof(config)); if (runtime->profile_count == 0U) { set_active_profile(-1, NULL); return; } start_next_profile(runtime); } static void start_radio_and_policy(manager_runtime_t *runtime) { wifi_app_config_t config; copy_working_config(&config); runtime->profile_count = build_profile_order(runtime->profile_order, &config); runtime->next_profile = 0U; runtime->next_backoff_seconds = WIFI_MANAGER_INITIAL_BACKOFF_SECONDS; runtime->advance_after_disconnect = false; runtime->stop_pending = false; runtime->restart_pending = false; runtime->restart_waits_for_ap_stop = false; runtime->restart_deadline = 0; runtime->attempt_deadline = 0; runtime->disconnect_deadline = 0; runtime->backoff_deadline = 0; runtime->stable_deadline = 0; runtime->associated = false; runtime->online = false; bool want_ap = config.ap_policy == WIFI_CONFIG_AP_POLICY_ALWAYS || (config.ap_policy == WIFI_CONFIG_AP_POLICY_FALLBACK && runtime->profile_count == 0U); wifi_mode_t mode = want_ap ? (runtime->profile_count == 0U ? WIFI_MODE_AP : WIFI_MODE_APSTA) : WIFI_MODE_STA; esp_err_t error = esp_wifi_set_mode(mode); if (error == ESP_OK && want_ap) { error = configure_ap(&config); } if (error == ESP_OK) { error = esp_wifi_start(); } if (error != ESP_OK) { set_last_error(error); set_state(WIFI_MANAGER_STATE_ERROR); ESP_LOGE(TAG, "Wi-Fi start failed: %s", esp_err_to_name(error)); wifi_config_secure_wipe(&config, sizeof(config)); return; } runtime->radio_started = true; runtime->ap_enabled = want_ap; set_last_error(ESP_OK); note_ap_running(want_ap, &config); if (runtime->profile_count == 0U) { set_active_profile(-1, NULL); lock_shared(); s_shared.snapshot.retry_seconds = 0U; if (want_ap) { s_shared.snapshot.state = WIFI_MANAGER_STATE_AP_ONLY; } else { s_shared.snapshot.last_error = ESP_ERR_NOT_FOUND; s_shared.snapshot.state = WIFI_MANAGER_STATE_ERROR; } unlock_shared(); wifi_config_secure_wipe(&config, sizeof(config)); return; } wifi_config_secure_wipe(&config, sizeof(config)); start_next_profile(runtime); } static void update_connected_snapshot(const manager_message_t *message, bool ap_enabled) { size_t length = message->data.connected.ssid_len; if (length > WIFI_CONFIG_SSID_MAX_LEN) { length = WIFI_CONFIG_SSID_MAX_LEN; } lock_shared(); memset(s_shared.snapshot.sta_ssid, 0, sizeof(s_shared.snapshot.sta_ssid)); memcpy(s_shared.snapshot.sta_ssid, message->data.connected.ssid, length); s_shared.snapshot.sta_ssid[length] = '\0'; s_shared.snapshot.sta_ssid_len = (uint8_t)length; s_shared.snapshot.sta_channel = message->data.connected.channel; s_shared.snapshot.sta_auth = message->data.connected.auth; if (ap_enabled) { /* In APSTA mode the SoftAP follows the station's radio channel. */ s_shared.snapshot.ap_channel = message->data.connected.channel; } ++s_shared.snapshot.counters.associations; s_shared.snapshot.state = WIFI_MANAGER_STATE_WAITING_IP; unlock_shared(); } static void handle_got_ip(manager_runtime_t *runtime, const manager_message_t *message) { esp_netif_ip_info_t current_ip; wifi_ap_record_t ap_record; memset(¤t_ip, 0, sizeof(current_ip)); memset(&ap_record, 0, sizeof(ap_record)); /* Driver/netif state is authoritative when old queued events arrive late. */ if (esp_netif_get_ip_info(s_sta_netif, ¤t_ip) != ESP_OK || current_ip.ip.addr == 0U || current_ip.ip.addr != message->data.got_ip.ip || esp_wifi_sta_get_ap_info(&ap_record) != ESP_OK) { return; } runtime->associated = true; runtime->online = true; runtime->intentional_disconnects = 0U; runtime->advance_after_disconnect = false; runtime->attempt_deadline = 0; runtime->disconnect_deadline = 0; runtime->backoff_deadline = 0; runtime->next_backoff_seconds = WIFI_MANAGER_INITIAL_BACKOFF_SECONDS; wifi_app_config_t config; copy_working_config(&config); runtime->stable_deadline = config.ap_policy == WIFI_CONFIG_AP_POLICY_FALLBACK ? esp_timer_get_time() + WIFI_MANAGER_STABLE_US : 0; lock_shared(); s_shared.snapshot.ip = message->data.got_ip.ip; s_shared.snapshot.netmask = message->data.got_ip.netmask; s_shared.snapshot.gateway = message->data.got_ip.gateway; s_shared.snapshot.sta_channel = ap_record.primary; s_shared.snapshot.sta_rssi = ap_record.rssi; s_shared.snapshot.sta_auth = ap_record.authmode; if (runtime->ap_enabled) { s_shared.snapshot.ap_channel = ap_record.primary; } s_shared.snapshot.retry_seconds = 0U; s_shared.snapshot.last_error = ESP_OK; s_shared.snapshot.state = WIFI_MANAGER_STATE_ONLINE; ++s_shared.snapshot.counters.got_ip; unlock_shared(); wifi_config_secure_wipe(&config, sizeof(config)); } static void handle_sta_disconnected(manager_runtime_t *runtime, const manager_message_t *message) { wifi_ap_record_t current_ap; memset(¤t_ap, 0, sizeof(current_ap)); if (esp_wifi_sta_get_ap_info(¤t_ap) == ESP_OK) { /* A newer live association supersedes this delayed disconnect event. */ if (runtime->intentional_disconnects > 0U) { --runtime->intentional_disconnects; } runtime->advance_after_disconnect = false; runtime->disconnect_deadline = 0; return; } bool had_attempt = runtime->attempt_deadline != 0; bool was_online = runtime->online; runtime->associated = false; runtime->online = false; runtime->attempt_deadline = 0; runtime->disconnect_deadline = 0; runtime->stable_deadline = 0; clear_station_network_snapshot(); lock_shared(); s_shared.snapshot.last_disconnect_reason = message->data.disconnected.reason; ++s_shared.snapshot.counters.disconnects; unlock_shared(); if (runtime->intentional_disconnects > 0U) { --runtime->intentional_disconnects; if (runtime->advance_after_disconnect && manager_is_started()) { runtime->advance_after_disconnect = false; start_next_profile(runtime); } return; } if (!manager_is_started()) { return; } if (had_attempt) { start_next_profile(runtime); } else if (was_online) { start_profile_cycle(runtime); } } static void handle_message(manager_runtime_t *runtime, const manager_message_t *message) { switch (message->type) { case MESSAGE_COMMAND_START: lock_shared(); if (s_shared.snapshot.started) { unlock_shared(); break; } s_shared.snapshot.started = true; s_shared.snapshot.state = WIFI_MANAGER_STATE_STARTING; ++s_shared.snapshot.counters.starts; unlock_shared(); if (runtime->stop_pending) { /* A rapid STOP→START waits for the old driver's stop boundary. */ runtime->restart_pending = true; } else { start_radio_and_policy(runtime); } break; case MESSAGE_COMMAND_STOP: /* Explicit stop cancels any previously requested automatic restart. */ runtime->restart_pending = false; lock_shared(); if (!s_shared.snapshot.started) { unlock_shared(); break; } s_shared.snapshot.started = false; ++s_shared.snapshot.counters.stops; unlock_shared(); if (runtime->radio_started) { bool wait_for_ap_stop = runtime->ap_enabled && runtime->profile_count == 0U; stop_radio(runtime); runtime->stop_pending = true; runtime->restart_waits_for_ap_stop = wait_for_ap_stop; runtime->restart_deadline = esp_timer_get_time() + WIFI_MANAGER_DISCONNECT_SETTLE_US; } set_state(WIFI_MANAGER_STATE_STOPPED); break; case MESSAGE_COMMAND_APPLY: case MESSAGE_COMMAND_RECONNECT: if (manager_is_started()) { set_state(WIFI_MANAGER_STATE_STARTING); if (runtime->radio_started) { bool wait_for_ap_stop = runtime->ap_enabled && runtime->profile_count == 0U; stop_radio(runtime); /* Old disconnect events are ordered before the matching stop event. */ runtime->stop_pending = true; runtime->restart_pending = true; runtime->restart_waits_for_ap_stop = wait_for_ap_stop; runtime->restart_deadline = esp_timer_get_time() + WIFI_MANAGER_DISCONNECT_SETTLE_US; } else if (runtime->stop_pending) { runtime->restart_pending = true; } else if (!runtime->restart_pending) { start_radio_and_policy(runtime); } } break; case MESSAGE_COMMAND_NEXT_PROFILE: if (manager_is_started()) { start_next_profile_after_current(runtime); } break; case MESSAGE_STA_CONNECTED: if (!manager_is_started() || !connected_event_matches_active_profile(message)) { break; } runtime->associated = true; runtime->online = false; if (runtime->attempt_deadline == 0) { runtime->attempt_deadline = esp_timer_get_time() + WIFI_MANAGER_ATTEMPT_US; } update_connected_snapshot(message, runtime->ap_enabled); break; case MESSAGE_STA_DISCONNECTED: handle_sta_disconnected(runtime, message); break; case MESSAGE_STA_GOT_IP: if (manager_is_started()) { handle_got_ip(runtime, message); } break; case MESSAGE_STA_LOST_IP: if (manager_is_started() && runtime->online) { esp_netif_ip_info_t current_ip; memset(¤t_ip, 0, sizeof(current_ip)); if (esp_netif_get_ip_info(s_sta_netif, ¤t_ip) == ESP_OK && current_ip.ip.addr != 0U) { /* Ignore a delayed loss event after a newer DHCP lease. */ break; } runtime->online = false; runtime->stable_deadline = 0; runtime->attempt_deadline = esp_timer_get_time() + WIFI_MANAGER_ATTEMPT_US; clear_station_network_snapshot(); set_state(WIFI_MANAGER_STATE_WAITING_IP); } break; case MESSAGE_STA_STOPPED: case MESSAGE_AP_STOPPED: { bool expected_stop = (message->type == MESSAGE_AP_STOPPED) == runtime->restart_waits_for_ap_stop; if (runtime->stop_pending && expected_stop) { runtime->stop_pending = false; runtime->restart_waits_for_ap_stop = false; runtime->restart_deadline = 0; runtime->intentional_disconnects = 0U; if (runtime->restart_pending && manager_is_started()) { runtime->restart_pending = false; start_radio_and_policy(runtime); } } break; } case MESSAGE_AP_CLIENT_JOINED: lock_shared(); ++s_shared.snapshot.counters.client_joins; if (s_shared.snapshot.ap_running && s_shared.snapshot.ap_client_count < UINT8_MAX) { ++s_shared.snapshot.ap_client_count; } unlock_shared(); break; case MESSAGE_AP_CLIENT_LEFT: lock_shared(); ++s_shared.snapshot.counters.client_leaves; if (s_shared.snapshot.ap_client_count > 0U) { --s_shared.snapshot.ap_client_count; } unlock_shared(); break; default: break; } } static int64_t next_runtime_deadline(const manager_runtime_t *runtime) { int64_t deadline = 0; const int64_t candidates[] = { runtime->attempt_deadline, runtime->disconnect_deadline, runtime->restart_deadline, runtime->backoff_deadline, runtime->stable_deadline, }; for (size_t i = 0U; i < sizeof(candidates) / sizeof(candidates[0]); ++i) { if (candidates[i] != 0 && (deadline == 0 || candidates[i] < deadline)) { deadline = candidates[i]; } } return deadline; } static TickType_t runtime_wait_ticks(const manager_runtime_t *runtime) { int64_t deadline = next_runtime_deadline(runtime); if (deadline == 0) { return portMAX_DELAY; } int64_t remaining_us = deadline - esp_timer_get_time(); if (remaining_us <= 0) { return 0; } uint32_t remaining_ms = (uint32_t)((remaining_us + 999LL) / 1000LL); TickType_t ticks = pdMS_TO_TICKS(remaining_ms); return ticks == 0 ? 1 : ticks; } static void handle_expired_deadlines(manager_runtime_t *runtime) { int64_t now = esp_timer_get_time(); if (runtime->attempt_deadline != 0 && now >= runtime->attempt_deadline) { esp_netif_ip_info_t current_ip; memset(¤t_ip, 0, sizeof(current_ip)); if (esp_netif_get_ip_info(s_sta_netif, ¤t_ip) == ESP_OK && current_ip.ip.addr != 0U) { /* Recover if the bounded manager queue dropped GOT_IP. */ manager_message_t synthetic = { .type = MESSAGE_STA_GOT_IP, .data.got_ip = { .ip = current_ip.ip.addr, .netmask = current_ip.netmask.addr, .gateway = current_ip.gw.addr, }, }; handle_got_ip(runtime, &synthetic); if (runtime->online) { return; } } runtime->attempt_deadline = 0; uint32_t intentional_before = runtime->intentional_disconnects; mark_intentional_disconnect(runtime); if (runtime->intentional_disconnects > intentional_before) { /* esp_wifi_set_config() must wait until the old attempt is gone. */ runtime->advance_after_disconnect = true; runtime->disconnect_deadline = now + WIFI_MANAGER_DISCONNECT_SETTLE_US; } else { start_next_profile(runtime); } return; } if (runtime->disconnect_deadline != 0 && now >= runtime->disconnect_deadline) { runtime->disconnect_deadline = 0; wifi_ap_record_t current_ap; memset(¤t_ap, 0, sizeof(current_ap)); if (esp_wifi_sta_get_ap_info(¤t_ap) == ESP_OK) { /* The driver is still associated; request disconnect and check again. */ (void)esp_wifi_disconnect(); runtime->disconnect_deadline = now + WIFI_MANAGER_DISCONNECT_SETTLE_US; } else { /* The event was dropped; authoritative driver state permits failover. */ runtime->intentional_disconnects = 0U; runtime->advance_after_disconnect = false; start_next_profile(runtime); } return; } if (runtime->restart_deadline != 0 && now >= runtime->restart_deadline) { runtime->restart_deadline = 0; if (runtime->stop_pending) { /* Recover if the bounded queue dropped the driver's stop event. */ runtime->stop_pending = false; runtime->restart_waits_for_ap_stop = false; runtime->intentional_disconnects = 0U; if (runtime->restart_pending && manager_is_started()) { runtime->restart_pending = false; start_radio_and_policy(runtime); } } return; } if (runtime->backoff_deadline != 0 && now >= runtime->backoff_deadline) { runtime->backoff_deadline = 0; lock_shared(); s_shared.snapshot.retry_seconds = 0U; unlock_shared(); start_profile_cycle(runtime); return; } if (runtime->stable_deadline != 0 && now >= runtime->stable_deadline) { runtime->stable_deadline = 0; if (runtime->online) { wifi_app_config_t config; copy_working_config(&config); if (config.ap_policy == WIFI_CONFIG_AP_POLICY_FALLBACK) { esp_err_t error = set_runtime_ap_enabled(runtime, false); if (error != ESP_OK) { set_last_error(error); ESP_LOGW(TAG, "failed to disable stable fallback AP: %s", esp_err_to_name(error)); } } wifi_config_secure_wipe(&config, sizeof(config)); } } } static void manager_task(void *context) { (void)context; manager_runtime_t runtime; memset(&runtime, 0, sizeof(runtime)); runtime.next_backoff_seconds = WIFI_MANAGER_INITIAL_BACKOFF_SECONDS; for (;;) { manager_message_t message; TickType_t wait = runtime_wait_ticks(&runtime); if (xQueueReceive(s_queue, &message, wait) == pdTRUE) { handle_message(&runtime, &message); } /* Checking after every message prevents a busy queue from starving timers. */ handle_expired_deadlines(&runtime); } } static void wifi_event_callback(void *argument, esp_event_base_t event_base, int32_t event_id, void *event_data) { (void)argument; manager_message_t message; memset(&message, 0, sizeof(message)); if (event_base != WIFI_EVENT) { return; } switch (event_id) { case WIFI_EVENT_STA_CONNECTED: { const wifi_event_sta_connected_t *event = event_data; if (event == NULL) { return; } message.type = MESSAGE_STA_CONNECTED; message.data.connected.channel = event->channel; message.data.connected.auth = event->authmode; message.data.connected.ssid_len = event->ssid_len; size_t length = event->ssid_len; if (length > WIFI_CONFIG_SSID_MAX_LEN) { length = WIFI_CONFIG_SSID_MAX_LEN; } memcpy(message.data.connected.ssid, event->ssid, length); break; } case WIFI_EVENT_STA_DISCONNECTED: { const wifi_event_sta_disconnected_t *event = event_data; if (event == NULL) { return; } message.type = MESSAGE_STA_DISCONNECTED; message.data.disconnected.reason = event->reason; break; } case WIFI_EVENT_STA_STOP: message.type = MESSAGE_STA_STOPPED; break; case WIFI_EVENT_AP_STOP: message.type = MESSAGE_AP_STOPPED; break; case WIFI_EVENT_AP_STACONNECTED: message.type = MESSAGE_AP_CLIENT_JOINED; break; case WIFI_EVENT_AP_STADISCONNECTED: message.type = MESSAGE_AP_CLIENT_LEFT; break; default: return; } /* No policy, Wi-Fi, NVS, logging, or delay is permitted in this callback. */ (void)enqueue_message(&message); } static void ip_event_callback(void *argument, esp_event_base_t event_base, int32_t event_id, void *event_data) { (void)argument; manager_message_t message; memset(&message, 0, sizeof(message)); if (event_base != IP_EVENT) { return; } if (event_id == IP_EVENT_STA_GOT_IP) { const ip_event_got_ip_t *event = event_data; if (event == NULL) { return; } message.type = MESSAGE_STA_GOT_IP; message.data.got_ip.ip = event->ip_info.ip.addr; message.data.got_ip.netmask = event->ip_info.netmask.addr; message.data.got_ip.gateway = event->ip_info.gw.addr; } else if (event_id == IP_EVENT_STA_LOST_IP) { message.type = MESSAGE_STA_LOST_IP; } else { return; } /* Keep IP events compact; address formatting belongs outside the callback. */ (void)enqueue_message(&message); } static esp_err_t create_default_wifi_netifs(void) { /* * The ESP-IDF convenience creators abort on internal failures. Build the * same default netifs explicitly so Wi-Fi failure cannot take down UART/USB. */ esp_netif_config_t sta_config = ESP_NETIF_DEFAULT_WIFI_STA(); s_sta_netif = esp_netif_new(&sta_config); if (s_sta_netif == NULL) { return ESP_ERR_NO_MEM; } esp_err_t error = esp_netif_attach_wifi_station(s_sta_netif); if (error == ESP_OK) { error = esp_wifi_set_default_wifi_sta_handlers(); } if (error != ESP_OK) { return error; } esp_netif_config_t ap_config = ESP_NETIF_DEFAULT_WIFI_AP(); s_ap_netif = esp_netif_new(&ap_config); if (s_ap_netif == NULL) { return ESP_ERR_NO_MEM; } error = esp_netif_attach_wifi_ap(s_ap_netif); if (error == ESP_OK) { error = esp_wifi_set_default_wifi_ap_handlers(); } return error; } static void cleanup_failed_init(bool wifi_initialized, bool wifi_handler_registered, bool ip_handler_registered, bool netif_initialized) { if (wifi_initialized) { (void)esp_wifi_deinit(); } if (ip_handler_registered) { (void)esp_event_handler_instance_unregister(IP_EVENT, ESP_EVENT_ANY_ID, s_ip_handler); } if (wifi_handler_registered) { (void)esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, s_wifi_handler); } if (s_ap_netif != NULL) { esp_netif_destroy_default_wifi(s_ap_netif); s_ap_netif = NULL; } if (s_sta_netif != NULL) { esp_netif_destroy_default_wifi(s_sta_netif); s_sta_netif = NULL; } if (s_event_loop_owned) { (void)esp_event_loop_delete_default(); s_event_loop_owned = false; } if (netif_initialized) { (void)esp_netif_deinit(); } if (s_queue != NULL) { vQueueDelete(s_queue); s_queue = NULL; } if (s_mutex != NULL) { vSemaphoreDelete(s_mutex); s_mutex = NULL; } s_task = NULL; } esp_err_t wifi_manager_init(const wifi_app_config_t *config) { esp_err_t error = wifi_config_validate(config); if (error != ESP_OK) { return error; } if (s_mutex != NULL) { return ESP_ERR_INVALID_STATE; } s_mutex = xSemaphoreCreateMutex(); if (s_mutex == NULL) { return ESP_ERR_NO_MEM; } s_queue = xQueueCreate(WIFI_MANAGER_QUEUE_LENGTH, sizeof(manager_message_t)); if (s_queue == NULL) { cleanup_failed_init(false, false, false, false); return ESP_ERR_NO_MEM; } memset(&s_shared, 0, sizeof(s_shared)); s_shared.config = *config; s_shared.snapshot.state = WIFI_MANAGER_STATE_STOPPED; s_shared.snapshot.active_profile = -1; s_shared.snapshot.ap_policy = config->ap_policy; s_shared.snapshot.ap_ssid_len = config->ap_ssid_len; memcpy(s_shared.snapshot.ap_ssid, config->ap_ssid, config->ap_ssid_len); s_shared.snapshot.ap_ssid[config->ap_ssid_len] = '\0'; s_shared.snapshot.ap_channel = config->ap_channel; s_shared.snapshot.sta_auth = WIFI_AUTH_OPEN; s_shared.snapshot.last_error = ESP_OK; s_shared.snapshot.config_generation = 1U; portENTER_CRITICAL(&s_drop_mux); s_queue_drops = 0U; portEXIT_CRITICAL(&s_drop_mux); bool netif_initialized = false; bool wifi_handler_registered = false; bool ip_handler_registered = false; bool wifi_initialized = false; error = esp_netif_init(); if (error != ESP_OK) { cleanup_failed_init(false, false, false, false); return error; } netif_initialized = true; error = esp_event_loop_create_default(); if (error == ESP_OK) { s_event_loop_owned = true; } else if (error != ESP_ERR_INVALID_STATE) { cleanup_failed_init(false, false, false, netif_initialized); return error; } error = create_default_wifi_netifs(); if (error != ESP_OK) { cleanup_failed_init(false, false, false, netif_initialized); return error; } error = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_callback, NULL, &s_wifi_handler); if (error != ESP_OK) { cleanup_failed_init(false, false, false, netif_initialized); return error; } wifi_handler_registered = true; error = esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, ip_event_callback, NULL, &s_ip_handler); if (error != ESP_OK) { cleanup_failed_init(false, wifi_handler_registered, false, netif_initialized); return error; } ip_handler_registered = true; wifi_init_config_t init_config = WIFI_INIT_CONFIG_DEFAULT(); error = esp_wifi_init(&init_config); if (error != ESP_OK) { cleanup_failed_init(false, wifi_handler_registered, ip_handler_registered, netif_initialized); return error; } wifi_initialized = true; /* Credentials must remain in volatile driver memory, never flash storage. */ error = esp_wifi_set_storage(WIFI_STORAGE_RAM); if (error != ESP_OK) { cleanup_failed_init(wifi_initialized, wifi_handler_registered, ip_handler_registered, netif_initialized); return error; } error = esp_wifi_set_ps(WIFI_PS_NONE); if (error != ESP_OK) { cleanup_failed_init(wifi_initialized, wifi_handler_registered, ip_handler_registered, netif_initialized); return error; } if (xTaskCreate(manager_task, "wifi_manager", WIFI_MANAGER_TASK_STACK_SIZE, NULL, WIFI_MANAGER_TASK_PRIORITY, &s_task) != pdPASS) { cleanup_failed_init(wifi_initialized, wifi_handler_registered, ip_handler_registered, netif_initialized); return ESP_ERR_NO_MEM; } lock_shared(); s_shared.snapshot.initialized = true; unlock_shared(); return ESP_OK; } esp_err_t wifi_manager_get_working_config(wifi_app_config_t *config) { if (config == NULL) { return ESP_ERR_INVALID_ARG; } if (s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } copy_working_config(config); return ESP_OK; } esp_err_t wifi_manager_apply_working_config(const wifi_app_config_t *config) { esp_err_t error = wifi_config_validate(config); if (error != ESP_OK) { return error; } if (s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } manager_message_t message = {.type = MESSAGE_COMMAND_APPLY}; lock_shared(); if (!enqueue_message(&message)) { unlock_shared(); return ESP_ERR_TIMEOUT; } s_shared.config = *config; ++s_shared.snapshot.config_generation; if (s_shared.snapshot.config_generation == 0U) { s_shared.snapshot.config_generation = 1U; } s_shared.snapshot.ap_policy = config->ap_policy; ++s_shared.snapshot.counters.applies; unlock_shared(); return ESP_OK; } static esp_err_t enqueue_lifecycle_command(manager_message_type_t type, int enabled_at_boot) { if (s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } manager_message_t message = {.type = type}; lock_shared(); if (!enqueue_message(&message)) { unlock_shared(); return ESP_ERR_TIMEOUT; } if (enabled_at_boot >= 0 && s_shared.config.enabled_at_boot != (uint8_t)enabled_at_boot) { s_shared.config.enabled_at_boot = (uint8_t)enabled_at_boot; ++s_shared.snapshot.config_generation; if (s_shared.snapshot.config_generation == 0U) { s_shared.snapshot.config_generation = 1U; } } unlock_shared(); return ESP_OK; } esp_err_t wifi_manager_start(void) { return enqueue_lifecycle_command(MESSAGE_COMMAND_START, 1); } esp_err_t wifi_manager_stop(void) { return enqueue_lifecycle_command(MESSAGE_COMMAND_STOP, 0); } esp_err_t wifi_manager_reconnect(void) { return enqueue_lifecycle_command(MESSAGE_COMMAND_RECONNECT, -1); } esp_err_t wifi_manager_next_profile(void) { return enqueue_lifecycle_command(MESSAGE_COMMAND_NEXT_PROFILE, -1); } esp_err_t wifi_manager_get_snapshot(wifi_manager_snapshot_t *snapshot) { if (snapshot == NULL) { return ESP_ERR_INVALID_ARG; } if (s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } lock_shared(); *snapshot = s_shared.snapshot; portENTER_CRITICAL(&s_drop_mux); snapshot->counters.queue_drops = s_queue_drops; portEXIT_CRITICAL(&s_drop_mux); unlock_shared(); return ESP_OK; } esp_err_t wifi_manager_clear_counters(void) { if (s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } lock_shared(); memset(&s_shared.snapshot.counters, 0, sizeof(s_shared.snapshot.counters)); portENTER_CRITICAL(&s_drop_mux); s_queue_drops = 0U; portEXIT_CRITICAL(&s_drop_mux); unlock_shared(); return ESP_OK; } const char *wifi_manager_state_to_string(wifi_manager_state_t state) { switch (state) { case WIFI_MANAGER_STATE_STOPPED: return "stopped"; case WIFI_MANAGER_STATE_STARTING: return "starting"; case WIFI_MANAGER_STATE_CONNECTING: return "connecting"; case WIFI_MANAGER_STATE_WAITING_IP: return "waiting-ip"; case WIFI_MANAGER_STATE_ONLINE: return "online"; case WIFI_MANAGER_STATE_BACKOFF: return "backoff"; case WIFI_MANAGER_STATE_AP_ONLY: return "ap-only"; case WIFI_MANAGER_STATE_ERROR: return "error"; default: return "unknown"; } }