1479 lines
47 KiB
C
1479 lines
47 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
/* Native ESP-IDF Wi-Fi lifecycle and connection policy manager. */
|
|
|
|
#include "wifi_manager.h"
|
|
|
|
#include <limits.h>
|
|
#include <stddef.h>
|
|
#include <string.h>
|
|
|
|
#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";
|
|
}
|
|
}
|