Add native Wi-Fi management and persistence

This commit is contained in:
2026-08-23 14:57:53 +02:00
parent aa5fa207b6
commit c5f3595173
11 changed files with 2869 additions and 2 deletions
+8
View File
@@ -11,15 +11,23 @@ idf_component_register(
"session_console.c"
"usb_cdc_transport.c"
"usb_console.c"
"wifi_config.c"
"wifi_manager.c"
"wifi_console.c"
INCLUDE_DIRS "."
REQUIRES
bootloader_support
console
esp_driver_gpio
esp_driver_uart
esp_event
esp_netif
esp_psram
esp_timer
esp_tinyusb
esp_wifi
freertos
led_strip
mbedtls
nvs_flash
)
+53 -1
View File
@@ -13,6 +13,9 @@
#include "status_led.h"
#include "usb_cdc_transport.h"
#include "usb_console.h"
#include "wifi_config.h"
#include "wifi_console.h"
#include "wifi_manager.h"
#define CONSOLE_BAUD_RATE 115200
#define CONSOLE_TX_GPIO 43
@@ -22,7 +25,7 @@ static const char *TAG = "firmware";
void app_main(void)
{
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife native USB CDC phase started");
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Wi-Fi foundation phase started");
if (esp_psram_is_initialized()) {
ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size());
@@ -30,6 +33,9 @@ void app_main(void)
ESP_LOGW(TAG, "PSRAM is not initialized");
}
/* Seed credential generation before any future RF or ADC initialization. */
esp_err_t wifi_entropy_error = wifi_config_entropy_init();
/* Blue means the firmware is initialized and waiting for a console command. */
ESP_ERROR_CHECK(status_led_init());
ESP_ERROR_CHECK(rs232_port_owner_init());
@@ -49,6 +55,51 @@ void app_main(void)
ESP_ERROR_CHECK(session_broker_init());
/* Native USB owns GPIO19/20; UART0 logging stays on the USB-to-UART bridge. */
ESP_ERROR_CHECK(usb_cdc_transport_init());
wifi_app_config_t wifi_config;
wifi_config_load_source_t wifi_config_source;
esp_err_t wifi_config_error = wifi_entropy_error;
if (wifi_config_error == ESP_OK) {
wifi_config_error = wifi_config_load(&wifi_config, &wifi_config_source);
}
if (wifi_config_error != ESP_OK) {
ESP_LOGW(TAG,
"NVS Wi-Fi configuration unavailable (%s); trying generated RAM defaults",
esp_err_to_name(wifi_config_error));
wifi_config_error = wifi_config_defaults(&wifi_config);
} else if (wifi_config_source == WIFI_CONFIG_LOAD_GENERATED_MISSING) {
/* Persist the random per-device fallback-AP credential on true first boot. */
esp_err_t save_error = wifi_config_save(&wifi_config);
if (save_error != ESP_OK) {
ESP_LOGW(TAG, "Could not persist initial Wi-Fi defaults: %s",
esp_err_to_name(save_error));
}
} else if (wifi_config_source == WIFI_CONFIG_LOAD_GENERATED_INVALID) {
ESP_LOGW(TAG,
"Stored Wi-Fi configuration is incompatible; using RAM defaults without overwriting it");
}
if (wifi_config_error == ESP_OK) {
esp_err_t wifi_error = wifi_manager_init(&wifi_config);
if (wifi_error == ESP_OK && wifi_config.enabled_at_boot != 0U) {
wifi_error = wifi_manager_start();
}
if (wifi_error != ESP_OK) {
/* UART0 and native USB remain recovery paths if networking is unavailable. */
ESP_LOGE(TAG, "Wi-Fi manager unavailable: %s", esp_err_to_name(wifi_error));
} else {
ESP_LOGI(TAG, "Using %s Wi-Fi configuration; AP policy=%s",
wifi_config_source == WIFI_CONFIG_LOAD_STORED
? "stored"
: "generated default",
wifi_config_ap_policy_to_string(wifi_config.ap_policy));
}
} else {
ESP_LOGE(TAG, "Could not create a valid Wi-Fi configuration: %s",
esp_err_to_name(wifi_config_error));
}
wifi_config_secure_wipe(&wifi_config, sizeof(wifi_config));
ESP_LOGI(
TAG,
"Using %s serial configuration; UART service starts on 'serial start' or native USB open",
@@ -77,6 +128,7 @@ void app_main(void)
ESP_ERROR_CHECK(serial_console_register_commands());
ESP_ERROR_CHECK(session_console_register_commands());
ESP_ERROR_CHECK(usb_console_register_commands());
ESP_ERROR_CHECK(wifi_console_register_commands());
ESP_ERROR_CHECK(esp_console_start_repl(repl));
ESP_LOGI(TAG, "Interactive test console ready at %d baud", CONSOLE_BAUD_RATE);
+439
View File
@@ -0,0 +1,439 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Native ESP-IDF Wi-Fi configuration and non-destructive NVS persistence. */
#include "wifi_config.h"
#include <limits.h>
#include <stddef.h>
#include <string.h>
#include "bootloader_random.h"
#include "esp_mac.h"
#include "esp_random.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "mbedtls/ctr_drbg.h"
#include "nvs.h"
#include "nvs_flash.h"
_Static_assert(sizeof(wifi_config_sta_profile_t) == WIFI_CONFIG_STA_PROFILE_BLOB_SIZE,
"Wi-Fi STA profile schema size changed");
_Static_assert(offsetof(wifi_app_config_t, profiles) == 112U,
"Wi-Fi config schema offsets changed");
_Static_assert(sizeof(wifi_app_config_t) == WIFI_CONFIG_BLOB_SIZE,
"Wi-Fi config schema size changed");
static SemaphoreHandle_t s_drbg_mutex;
static mbedtls_ctr_drbg_context s_drbg;
static bool s_drbg_ready;
static int early_entropy_source(void *context, unsigned char *output, size_t length)
{
(void)context;
/* This callback is used only during the explicit pre-radio initialization. */
bootloader_random_enable();
esp_fill_random(output, length);
bootloader_random_disable();
return 0;
}
esp_err_t wifi_config_entropy_init(void)
{
static const unsigned char personalization[] = "esp32-serial-sak-wifi";
if (s_drbg_ready) {
return ESP_OK;
}
if (s_drbg_mutex == NULL) {
s_drbg_mutex = xSemaphoreCreateMutex();
if (s_drbg_mutex == NULL) {
return ESP_ERR_NO_MEM;
}
}
xSemaphoreTake(s_drbg_mutex, portMAX_DELAY);
esp_err_t error = ESP_OK;
if (!s_drbg_ready) {
mbedtls_ctr_drbg_init(&s_drbg);
int result = mbedtls_ctr_drbg_seed(&s_drbg,
early_entropy_source,
NULL,
personalization,
sizeof(personalization) - 1U);
if (result == 0) {
/* Credential generation is rare; never invoke early entropy after RF starts. */
mbedtls_ctr_drbg_set_reseed_interval(&s_drbg, INT_MAX);
s_drbg_ready = true;
} else {
mbedtls_ctr_drbg_free(&s_drbg);
error = ESP_FAIL;
}
}
xSemaphoreGive(s_drbg_mutex);
return error;
}
static esp_err_t fill_credential_random(uint8_t *output, size_t length)
{
if (!s_drbg_ready || output == NULL) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_drbg_mutex, portMAX_DELAY);
int result = mbedtls_ctr_drbg_random(&s_drbg, output, length);
xSemaphoreGive(s_drbg_mutex);
return result == 0 ? ESP_OK : ESP_FAIL;
}
static bool bytes_are_zero(const uint8_t *data, size_t size)
{
for (size_t i = 0; i < size; ++i) {
if (data[i] != 0U) {
return false;
}
}
return true;
}
static bool unused_bytes_are_zero(const uint8_t *data, size_t used, size_t capacity)
{
return used <= capacity && bytes_are_zero(data + used, capacity - used);
}
static bool psk_is_valid(const uint8_t *psk, uint8_t length)
{
if (length < WIFI_CONFIG_PSK_MIN_LEN || length > WIFI_CONFIG_PSK_MAX_LEN) {
return false;
}
/* ESP-IDF accepts WPA passphrases as 8..63 printable ASCII characters. */
for (uint8_t i = 0; i < length; ++i) {
if (psk[i] < 0x20U || psk[i] > 0x7eU) {
return false;
}
}
return unused_bytes_are_zero(psk, length, WIFI_CONFIG_PSK_MAX_LEN);
}
static bool ap_policy_is_valid(wifi_config_ap_policy_t policy)
{
return policy == WIFI_CONFIG_AP_POLICY_OFF ||
policy == WIFI_CONFIG_AP_POLICY_FALLBACK ||
policy == WIFI_CONFIG_AP_POLICY_ALWAYS;
}
static bool security_is_valid(wifi_config_security_t security)
{
return security == WIFI_CONFIG_SECURITY_MIXED ||
security == WIFI_CONFIG_SECURITY_WPA3;
}
void wifi_config_secure_wipe(void *data, size_t size)
{
volatile uint8_t *byte = (volatile uint8_t *)data;
if (byte == NULL) {
return;
}
while (size-- > 0U) {
*byte++ = 0U;
}
}
esp_err_t wifi_config_defaults(wifi_app_config_t *config)
{
static const uint8_t ap_prefix[] = "ESP32-SAK-";
static const char password_alphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
static const char hex[] = "0123456789ABCDEF";
uint8_t mac[6] = {0};
uint8_t random_bytes[WIFI_CONFIG_DEFAULT_AP_PSK_LEN] = {0};
if (config == NULL) {
return ESP_ERR_INVALID_ARG;
}
/* A zero baseline makes all reserved and unused bytes canonical. */
memset(config, 0, sizeof(*config));
config->schema_version = WIFI_CONFIG_SCHEMA_VERSION;
config->blob_size = WIFI_CONFIG_BLOB_SIZE;
config->enabled_at_boot = 1U;
config->ap_policy = WIFI_CONFIG_AP_POLICY_FALLBACK;
/* Channel 6 is a conventional world-safe default until country settings exist. */
config->ap_channel = 6U;
esp_err_t err = esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP);
if (err != ESP_OK) {
return err;
}
memcpy(config->ap_ssid, ap_prefix, sizeof(ap_prefix) - 1U);
size_t offset = sizeof(ap_prefix) - 1U;
for (size_t i = 3U; i < sizeof(mac); ++i) {
config->ap_ssid[offset++] = (uint8_t)hex[mac[i] >> 4U];
config->ap_ssid[offset++] = (uint8_t)hex[mac[i] & 0x0fU];
}
config->ap_ssid_len = (uint8_t)offset;
/*
* A 64-character alphabet maps six random bits without modulo bias. The
* password is intentionally independent of the public MAC-derived suffix.
*/
err = fill_credential_random(random_bytes, sizeof(random_bytes));
if (err != ESP_OK) {
wifi_config_secure_wipe(random_bytes, sizeof(random_bytes));
return err;
}
for (size_t i = 0; i < sizeof(random_bytes); ++i) {
config->ap_psk[i] = (uint8_t)password_alphabet[random_bytes[i] & 0x3fU];
}
config->ap_psk_len = WIFI_CONFIG_DEFAULT_AP_PSK_LEN;
wifi_config_secure_wipe(random_bytes, sizeof(random_bytes));
return wifi_config_validate(config);
}
esp_err_t wifi_config_validate(const wifi_app_config_t *config)
{
if (config == NULL ||
config->schema_version != WIFI_CONFIG_SCHEMA_VERSION ||
config->blob_size != WIFI_CONFIG_BLOB_SIZE ||
config->enabled_at_boot > 1U ||
!ap_policy_is_valid(config->ap_policy) ||
config->ap_ssid_len == 0U ||
config->ap_ssid_len > WIFI_CONFIG_SSID_MAX_LEN ||
!unused_bytes_are_zero(config->ap_ssid, config->ap_ssid_len,
WIFI_CONFIG_SSID_MAX_LEN) ||
!psk_is_valid(config->ap_psk, config->ap_psk_len) ||
config->ap_channel < WIFI_CONFIG_AP_CHANNEL_MIN ||
config->ap_channel > WIFI_CONFIG_AP_CHANNEL_MAX ||
!bytes_are_zero(config->reserved, sizeof(config->reserved)) ||
!bytes_are_zero(config->reserved_tail, sizeof(config->reserved_tail))) {
return ESP_ERR_INVALID_ARG;
}
for (size_t i = 0; i < WIFI_CONFIG_STA_PROFILE_COUNT; ++i) {
const wifi_config_sta_profile_t *profile = &config->profiles[i];
if (profile->enabled > 1U ||
!security_is_valid(profile->security) ||
!bytes_are_zero(profile->reserved, sizeof(profile->reserved)) ||
!bytes_are_zero(profile->reserved_tail, sizeof(profile->reserved_tail))) {
return ESP_ERR_INVALID_ARG;
}
bool empty = profile->ssid_len == 0U && profile->psk_len == 0U;
if (empty) {
if (profile->enabled != 0U ||
!bytes_are_zero(profile->ssid, sizeof(profile->ssid)) ||
!bytes_are_zero(profile->psk, sizeof(profile->psk))) {
return ESP_ERR_INVALID_ARG;
}
continue;
}
/* A disabled SSID may be staged before its no-echo secret is entered. */
if (profile->ssid_len == 0U ||
profile->ssid_len > WIFI_CONFIG_SSID_MAX_LEN ||
!unused_bytes_are_zero(profile->ssid, profile->ssid_len,
WIFI_CONFIG_SSID_MAX_LEN)) {
return ESP_ERR_INVALID_ARG;
}
if (profile->psk_len == 0U) {
if (profile->enabled != 0U ||
!bytes_are_zero(profile->psk, sizeof(profile->psk))) {
return ESP_ERR_INVALID_ARG;
}
} else if (!psk_is_valid(profile->psk, profile->psk_len)) {
return ESP_ERR_INVALID_ARG;
}
}
return ESP_OK;
}
bool wifi_config_parse_ap_policy(const char *text, wifi_config_ap_policy_t *value)
{
if (text == NULL || value == NULL) {
return false;
}
if (strcmp(text, "off") == 0) {
*value = WIFI_CONFIG_AP_POLICY_OFF;
return true;
}
if (strcmp(text, "fallback") == 0) {
*value = WIFI_CONFIG_AP_POLICY_FALLBACK;
return true;
}
if (strcmp(text, "always") == 0) {
*value = WIFI_CONFIG_AP_POLICY_ALWAYS;
return true;
}
return false;
}
const char *wifi_config_ap_policy_to_string(wifi_config_ap_policy_t value)
{
switch (value) {
case WIFI_CONFIG_AP_POLICY_OFF:
return "off";
case WIFI_CONFIG_AP_POLICY_FALLBACK:
return "fallback";
case WIFI_CONFIG_AP_POLICY_ALWAYS:
return "always";
default:
return NULL;
}
}
bool wifi_config_parse_security(const char *text, wifi_config_security_t *value)
{
if (text == NULL || value == NULL) {
return false;
}
if (strcmp(text, "mixed") == 0) {
*value = WIFI_CONFIG_SECURITY_MIXED;
return true;
}
if (strcmp(text, "wpa3") == 0) {
*value = WIFI_CONFIG_SECURITY_WPA3;
return true;
}
return false;
}
const char *wifi_config_security_to_string(wifi_config_security_t value)
{
switch (value) {
case WIFI_CONFIG_SECURITY_MIXED:
return "mixed";
case WIFI_CONFIG_SECURITY_WPA3:
return "wpa3";
default:
return NULL;
}
}
esp_err_t wifi_config_storage_init(void)
{
/* Never erase shared NVS to recover an error; report it to the caller. */
return nvs_flash_init();
}
esp_err_t wifi_config_load(wifi_app_config_t *config,
wifi_config_load_source_t *source)
{
if (config == NULL || source == NULL) {
return ESP_ERR_INVALID_ARG;
}
*source = WIFI_CONFIG_LOAD_GENERATED_MISSING;
/* Defaults are established before any NVS access, including failure paths. */
esp_err_t err = wifi_config_defaults(config);
if (err != ESP_OK) {
return err;
}
err = wifi_config_storage_init();
if (err != ESP_OK) {
return err;
}
nvs_handle_t handle;
err = nvs_open(WIFI_CONFIG_NVS_NAMESPACE, NVS_READONLY, &handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
return ESP_OK;
}
if (err != ESP_OK) {
return err;
}
size_t stored_size = 0U;
err = nvs_get_blob(handle, WIFI_CONFIG_NVS_BLOB_KEY, NULL, &stored_size);
if (err == ESP_ERR_NVS_NOT_FOUND) {
nvs_close(handle);
return ESP_OK;
}
if (err == ESP_ERR_NVS_TYPE_MISMATCH) {
*source = WIFI_CONFIG_LOAD_GENERATED_INVALID;
nvs_close(handle);
return ESP_OK;
}
if (err != ESP_OK) {
nvs_close(handle);
return err;
}
/* A different size is a different schema, even if its prefix looks valid. */
if (stored_size != WIFI_CONFIG_BLOB_SIZE) {
*source = WIFI_CONFIG_LOAD_GENERATED_INVALID;
nvs_close(handle);
return ESP_OK;
}
wifi_app_config_t stored_config;
memset(&stored_config, 0, sizeof(stored_config));
err = nvs_get_blob(handle, WIFI_CONFIG_NVS_BLOB_KEY, &stored_config, &stored_size);
nvs_close(handle);
if (err == ESP_ERR_NVS_INVALID_LENGTH) {
*source = WIFI_CONFIG_LOAD_GENERATED_INVALID;
wifi_config_secure_wipe(&stored_config, sizeof(stored_config));
return ESP_OK;
}
if (err != ESP_OK) {
wifi_config_secure_wipe(&stored_config, sizeof(stored_config));
return err;
}
if (stored_size == WIFI_CONFIG_BLOB_SIZE &&
wifi_config_validate(&stored_config) == ESP_OK) {
*config = stored_config;
*source = WIFI_CONFIG_LOAD_STORED;
} else {
*source = WIFI_CONFIG_LOAD_GENERATED_INVALID;
}
wifi_config_secure_wipe(&stored_config, sizeof(stored_config));
return ESP_OK;
}
esp_err_t wifi_config_save(const wifi_app_config_t *config)
{
esp_err_t err = wifi_config_validate(config);
if (err != ESP_OK) {
return err;
}
err = wifi_config_storage_init();
if (err != ESP_OK) {
return err;
}
nvs_handle_t handle;
err = nvs_open(WIFI_CONFIG_NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
return err;
}
err = nvs_set_blob(handle, WIFI_CONFIG_NVS_BLOB_KEY, config,
WIFI_CONFIG_BLOB_SIZE);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
return err;
}
esp_err_t wifi_config_reset_storage(const wifi_app_config_t *defaults)
{
if (defaults != NULL) {
return wifi_config_save(defaults);
}
wifi_app_config_t generated_defaults;
esp_err_t err = wifi_config_defaults(&generated_defaults);
if (err == ESP_OK) {
err = wifi_config_save(&generated_defaults);
}
wifi_config_secure_wipe(&generated_defaults, sizeof(generated_defaults));
return err;
}
+113
View File
@@ -0,0 +1,113 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/*
* Native ESP-IDF Wi-Fi configuration and persistence.
*
* The public structures below are also the version-1 NVS wire format. Keep
* every field fixed-width and introduce a new schema version for layout
* changes; do not silently reinterpret an existing blob.
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#define WIFI_CONFIG_SCHEMA_VERSION 1U
#define WIFI_CONFIG_BLOB_SIZE 528U
#define WIFI_CONFIG_STA_PROFILE_BLOB_SIZE 104U
#define WIFI_CONFIG_NVS_NAMESPACE "wifi_app"
#define WIFI_CONFIG_NVS_BLOB_KEY "config"
#define WIFI_CONFIG_STA_PROFILE_COUNT 4U
#define WIFI_CONFIG_SSID_MAX_LEN 32U
#define WIFI_CONFIG_PSK_MIN_LEN 8U
#define WIFI_CONFIG_PSK_MAX_LEN 63U
#define WIFI_CONFIG_DEFAULT_AP_PSK_LEN 16U
#define WIFI_CONFIG_AP_CHANNEL_MIN 1U
#define WIFI_CONFIG_AP_CHANNEL_MAX 11U
/* uint8_t aliases keep the persisted representation independent of enum size. */
typedef uint8_t wifi_config_ap_policy_t;
enum {
WIFI_CONFIG_AP_POLICY_OFF = 0U,
WIFI_CONFIG_AP_POLICY_FALLBACK = 1U,
WIFI_CONFIG_AP_POLICY_ALWAYS = 2U,
};
typedef uint8_t wifi_config_security_t;
enum {
/* ESP-IDF thresholds can express WPA2-or-stronger, not WPA2-only. */
WIFI_CONFIG_SECURITY_MIXED = 0U,
WIFI_CONFIG_SECURITY_WPA3 = 1U,
};
typedef uint8_t wifi_config_load_source_t;
enum {
WIFI_CONFIG_LOAD_GENERATED_MISSING = 0U,
WIFI_CONFIG_LOAD_STORED = 1U,
WIFI_CONFIG_LOAD_GENERATED_INVALID = 2U,
};
typedef struct {
/* Persisted booleans use 0/1 bytes so the blob layout remains fixed. */
uint8_t enabled;
uint8_t priority;
wifi_config_security_t security;
uint8_t ssid_len;
uint8_t psk_len;
uint8_t reserved[3];
/* Lengths are authoritative; these fixed-width byte arrays need no NUL. */
uint8_t ssid[WIFI_CONFIG_SSID_MAX_LEN];
uint8_t psk[WIFI_CONFIG_PSK_MAX_LEN];
uint8_t reserved_tail[1];
} wifi_config_sta_profile_t;
typedef struct {
uint32_t schema_version;
uint16_t blob_size;
uint8_t enabled_at_boot;
wifi_config_ap_policy_t ap_policy;
uint8_t ap_ssid_len;
uint8_t ap_psk_len;
uint8_t ap_channel;
uint8_t reserved[5];
uint8_t ap_ssid[WIFI_CONFIG_SSID_MAX_LEN];
uint8_t ap_psk[WIFI_CONFIG_PSK_MAX_LEN];
uint8_t reserved_tail[1];
wifi_config_sta_profile_t profiles[WIFI_CONFIG_STA_PROFILE_COUNT];
} wifi_app_config_t;
/* Seed the credential DRBG before Wi-Fi, Bluetooth, or ADC use. */
esp_err_t wifi_config_entropy_init(void);
/* Generates a unique AP SSID and a fresh random AP password. */
esp_err_t wifi_config_defaults(wifi_app_config_t *config);
esp_err_t wifi_config_validate(const wifi_app_config_t *config);
bool wifi_config_parse_ap_policy(const char *text, wifi_config_ap_policy_t *value);
const char *wifi_config_ap_policy_to_string(wifi_config_ap_policy_t value);
bool wifi_config_parse_security(const char *text, wifi_config_security_t *value);
const char *wifi_config_security_to_string(wifi_config_security_t value);
/*
* Storage uses only wifi_app/config. Invalid, stale, or absent blobs are left
* untouched. source distinguishes first boot from incompatible stored data so
* callers never overwrite a potentially migratable schema automatically.
*/
esp_err_t wifi_config_storage_init(void);
esp_err_t wifi_config_load(wifi_app_config_t *config,
wifi_config_load_source_t *source);
esp_err_t wifi_config_save(const wifi_app_config_t *config);
/* Pass NULL to generate fresh defaults, or supply validated defaults to save. */
esp_err_t wifi_config_reset_storage(const wifi_app_config_t *defaults);
/* Volatile writes prevent ordinary optimization from eliding secret cleanup. */
void wifi_config_secure_wipe(void *data, size_t size);
+670
View File
@@ -0,0 +1,670 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Physical-admin UART0 console for Wi-Fi configuration and diagnostics. */
#include "wifi_console.h"
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "driver/uart.h"
#include "esp_console.h"
#include "esp_err.h"
#include "esp_netif_ip_addr.h"
#include "esp_wifi_types.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "wifi_config.h"
#include "wifi_manager.h"
#define WIFI_CONSOLE_UART UART_NUM_0
#define WIFI_CONSOLE_SECRET_CAPACITY WIFI_CONFIG_PSK_MAX_LEN
static void print_usage(void)
{
printf("Usage:\n");
printf(" wifi status|profiles|counters|clear-counters\n");
printf(" wifi start|stop|reconnect\n");
printf(" wifi profile set <slot> <priority> <mixed|wpa3> <ssid>\n");
printf(" wifi profile secret <slot>\n");
printf(" wifi profile enable|disable|delete <slot>\n");
printf(" wifi ap policy <off|fallback|always>\n");
printf(" wifi ap ssid <ssid>\n");
printf(" wifi ap channel <1..11>\n");
printf(" wifi ap secret|show-secret\n");
printf(" wifi save|load|defaults|reset\n");
}
static bool parse_u32(const char *text, uint32_t maximum, uint32_t *value)
{
if (text == NULL || *text == '\0') {
return false;
}
for (const char *character = text; *character != '\0'; ++character) {
if (*character < '0' || *character > '9') {
return false;
}
}
char *end = NULL;
errno = 0;
unsigned long parsed = strtoul(text, &end, 10);
if (errno != 0 || end == text || *end != '\0' || parsed > maximum) {
return false;
}
*value = (uint32_t)parsed;
return true;
}
static bool parse_slot(const char *text, uint8_t *slot)
{
uint32_t parsed;
if (!parse_u32(text, WIFI_CONFIG_STA_PROFILE_COUNT - 1U, &parsed)) {
return false;
}
*slot = (uint8_t)parsed;
return true;
}
static void print_bytes_escaped(const uint8_t *data, size_t length)
{
putchar('"');
for (size_t index = 0; index < length; ++index) {
uint8_t byte = data[index];
if (byte == '\\' || byte == '"') {
printf("\\%c", (char)byte);
} else if (byte >= 0x20U && byte <= 0x7eU) {
putchar((char)byte);
} else {
printf("\\x%02x", (unsigned int)byte);
}
}
putchar('"');
}
static const char *auth_mode_name(wifi_auth_mode_t auth)
{
switch (auth) {
case WIFI_AUTH_OPEN:
return "open";
case WIFI_AUTH_WEP:
return "wep";
case WIFI_AUTH_WPA_PSK:
return "wpa";
case WIFI_AUTH_WPA2_PSK:
return "wpa2";
case WIFI_AUTH_WPA_WPA2_PSK:
return "wpa/wpa2";
case WIFI_AUTH_WPA2_ENTERPRISE:
return "wpa2-enterprise";
case WIFI_AUTH_WPA3_PSK:
return "wpa3";
case WIFI_AUTH_WPA2_WPA3_PSK:
return "wpa2/wpa3";
case WIFI_AUTH_WPA3_ENT_192:
return "wpa3-enterprise-192";
default:
return "other";
}
}
static void print_ipv4(uint32_t address)
{
esp_ip4_addr_t ip = {.addr = address};
printf(IPSTR, IP2STR(&ip));
}
static int show_status(void)
{
wifi_manager_snapshot_t snapshot;
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_snapshot(&snapshot);
if (error == ESP_OK) {
error = wifi_manager_get_working_config(&config);
}
if (error != ESP_OK) {
printf("Wi-Fi manager unavailable: %s\n", esp_err_to_name(error));
return 1;
}
printf("Wi-Fi: initialized=%s enabled-at-boot=%s started=%s state=%s generation=%" PRIu32 "\n",
snapshot.initialized ? "yes" : "no",
config.enabled_at_boot ? "yes" : "no",
snapshot.started ? "yes" : "no",
wifi_manager_state_to_string(snapshot.state),
snapshot.config_generation);
if (snapshot.active_profile >= 0) {
printf("STA: profile=%d SSID=", snapshot.active_profile);
print_bytes_escaped((const uint8_t *)snapshot.sta_ssid, snapshot.sta_ssid_len);
printf(" channel=%u RSSI=%d auth=%s\n",
(unsigned int)snapshot.sta_channel,
(int)snapshot.sta_rssi,
auth_mode_name(snapshot.sta_auth));
} else {
printf("STA: no active profile\n");
}
if (snapshot.ip != 0U) {
printf("IPv4: address=");
print_ipv4(snapshot.ip);
printf(" netmask=");
print_ipv4(snapshot.netmask);
printf(" gateway=");
print_ipv4(snapshot.gateway);
putchar('\n');
}
printf("AP: policy=%s running=%s clients=%u channel=%u SSID=",
wifi_config_ap_policy_to_string(snapshot.ap_policy),
snapshot.ap_running ? "yes" : "no",
(unsigned int)snapshot.ap_client_count,
(unsigned int)snapshot.ap_channel);
print_bytes_escaped(config.ap_ssid, config.ap_ssid_len);
printf(" secret=set\n");
printf("Last: disconnect-reason=%u error=%s retry=%" PRIu32 "s\n",
(unsigned int)snapshot.last_disconnect_reason,
esp_err_to_name(snapshot.last_error),
snapshot.retry_seconds);
wifi_config_secure_wipe(&config, sizeof(config));
return 0;
}
static int show_profiles(void)
{
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
if (error != ESP_OK) {
printf("Could not read Wi-Fi profiles: %s\n", esp_err_to_name(error));
return 1;
}
printf("slot enabled priority security secret SSID\n");
for (size_t slot = 0U; slot < WIFI_CONFIG_STA_PROFILE_COUNT; ++slot) {
const wifi_config_sta_profile_t *profile = &config.profiles[slot];
printf("%u %-7s %-8u %-8s %-6s ",
(unsigned int)slot,
profile->enabled ? "yes" : "no",
(unsigned int)profile->priority,
wifi_config_security_to_string(profile->security),
profile->psk_len > 0U ? "set" : "unset");
if (profile->ssid_len > 0U) {
print_bytes_escaped(profile->ssid, profile->ssid_len);
} else {
printf("<empty>");
}
putchar('\n');
}
wifi_config_secure_wipe(&config, sizeof(config));
return 0;
}
static int show_counters(void)
{
wifi_manager_snapshot_t snapshot;
esp_err_t error = wifi_manager_get_snapshot(&snapshot);
if (error != ESP_OK) {
printf("Could not read Wi-Fi counters: %s\n", esp_err_to_name(error));
return 1;
}
const wifi_manager_counters_t *counter = &snapshot.counters;
printf("Lifecycle: starts=%" PRIu64 " stops=%" PRIu64 " applies=%" PRIu64 "\n",
counter->starts, counter->stops, counter->applies);
printf("STA: attempts=%" PRIu64 " associations=%" PRIu64
" got-IP=%" PRIu64 " disconnects=%" PRIu64 "\n",
counter->connect_attempts, counter->associations,
counter->got_ip, counter->disconnects);
printf("Policy: exhausted-cycles=%" PRIu64 " retries=%" PRIu64 "\n",
counter->profile_cycles, counter->retries);
printf("AP: starts=%" PRIu64 " stops=%" PRIu64
" joins=%" PRIu64 " leaves=%" PRIu64 "\n",
counter->ap_starts, counter->ap_stops,
counter->client_joins, counter->client_leaves);
printf("Manager queue drops=%" PRIu64 "\n", counter->queue_drops);
return 0;
}
static esp_err_t apply_candidate(wifi_app_config_t *candidate)
{
esp_err_t error = wifi_manager_apply_working_config(candidate);
wifi_config_secure_wipe(candidate, sizeof(*candidate));
return error;
}
static esp_err_t read_secret_no_echo(uint8_t *secret, uint8_t *secret_len)
{
uint8_t buffer[WIFI_CONSOLE_SECRET_CAPACITY];
size_t length = 0U;
memset(buffer, 0, sizeof(buffer));
/*
* esp_console may execute on CR while the terminal's trailing LF is still
* arriving. Let that line ending settle, then discard only pre-prompt RX so
* it cannot be mistaken for an immediately submitted empty secret.
*/
vTaskDelay(1U);
esp_err_t flush_error = uart_flush_input(WIFI_CONSOLE_UART);
if (flush_error != ESP_OK) {
wifi_config_secure_wipe(buffer, sizeof(buffer));
printf("Could not prepare secret input: %s\n", esp_err_to_name(flush_error));
return flush_error;
}
printf("Enter 8..63 printable ASCII characters (input hidden, Ctrl-C cancels): ");
fflush(stdout);
for (;;) {
uint8_t byte = 0U;
int received = uart_read_bytes(WIFI_CONSOLE_UART, &byte, 1U, portMAX_DELAY);
if (received != 1) {
wifi_config_secure_wipe(buffer, sizeof(buffer));
printf("\nSecret input failed.\n");
return ESP_FAIL;
}
if (byte == 0x03U) {
wifi_config_secure_wipe(buffer, sizeof(buffer));
printf("\nCancelled.\n");
return ESP_ERR_INVALID_STATE;
}
if (byte == '\r' || byte == '\n') {
break;
}
if (byte == 0x08U || byte == 0x7fU) {
if (length > 0U) {
buffer[--length] = 0U;
}
continue;
}
if (byte < 0x20U || byte > 0x7eU || length >= sizeof(buffer)) {
putchar('\a');
fflush(stdout);
continue;
}
buffer[length++] = byte;
}
putchar('\n');
if (length < WIFI_CONFIG_PSK_MIN_LEN || length > WIFI_CONFIG_PSK_MAX_LEN) {
wifi_config_secure_wipe(buffer, sizeof(buffer));
printf("Secret length must be 8..63 characters.\n");
return ESP_ERR_INVALID_ARG;
}
memset(secret, 0, WIFI_CONFIG_PSK_MAX_LEN);
memcpy(secret, buffer, length);
*secret_len = (uint8_t)length;
wifi_config_secure_wipe(buffer, sizeof(buffer));
return ESP_OK;
}
static int set_profile(char **argv)
{
uint8_t slot;
uint32_t priority;
wifi_config_security_t security;
size_t ssid_len = strlen(argv[6]);
if (!parse_slot(argv[3], &slot) ||
!parse_u32(argv[4], UINT8_MAX, &priority) ||
!wifi_config_parse_security(argv[5], &security) ||
ssid_len == 0U || ssid_len > WIFI_CONFIG_SSID_MAX_LEN) {
printf("Profile requires slot 0..%u, priority 0..255, valid security, and a 1..32-byte SSID.\n",
WIFI_CONFIG_STA_PROFILE_COUNT - 1U);
return 1;
}
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
if (error != ESP_OK) {
printf("Could not read working configuration: %s\n", esp_err_to_name(error));
return 1;
}
wifi_config_sta_profile_t *profile = &config.profiles[slot];
memset(profile->ssid, 0, sizeof(profile->ssid));
memcpy(profile->ssid, argv[6], ssid_len);
profile->ssid_len = (uint8_t)ssid_len;
profile->priority = (uint8_t)priority;
profile->security = security;
error = apply_candidate(&config);
if (error != ESP_OK) {
printf("Could not apply profile: %s\n", esp_err_to_name(error));
return 1;
}
printf("Profile %u updated in RAM; its existing secret and enabled state were preserved.\n",
(unsigned int)slot);
return 0;
}
static int set_profile_secret(const char *slot_text)
{
uint8_t slot;
if (!parse_slot(slot_text, &slot)) {
printf("Profile slot must be 0..%u.\n", WIFI_CONFIG_STA_PROFILE_COUNT - 1U);
return 1;
}
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
if (error != ESP_OK) {
printf("Could not read working configuration: %s\n", esp_err_to_name(error));
return 1;
}
if (config.profiles[slot].ssid_len == 0U) {
wifi_config_secure_wipe(&config, sizeof(config));
printf("Set the profile SSID before its secret.\n");
return 1;
}
error = read_secret_no_echo(config.profiles[slot].psk,
&config.profiles[slot].psk_len);
if (error == ESP_OK) {
error = apply_candidate(&config);
} else {
wifi_config_secure_wipe(&config, sizeof(config));
}
if (error != ESP_OK) {
if (error != ESP_ERR_INVALID_STATE) {
printf("Could not set profile secret: %s\n", esp_err_to_name(error));
}
return 1;
}
printf("Profile %u secret updated in RAM; use 'wifi save' to persist it.\n",
(unsigned int)slot);
return 0;
}
static int change_profile_state(const char *operation, const char *slot_text)
{
uint8_t slot;
if (!parse_slot(slot_text, &slot)) {
printf("Profile slot must be 0..%u.\n", WIFI_CONFIG_STA_PROFILE_COUNT - 1U);
return 1;
}
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
if (error != ESP_OK) {
printf("Could not read working configuration: %s\n", esp_err_to_name(error));
return 1;
}
wifi_config_sta_profile_t *profile = &config.profiles[slot];
if (strcmp(operation, "delete") == 0) {
wifi_config_secure_wipe(profile, sizeof(*profile));
} else {
profile->enabled = strcmp(operation, "enable") == 0 ? 1U : 0U;
}
error = apply_candidate(&config);
if (error != ESP_OK) {
printf("Could not %s profile %u: %s\n", operation,
(unsigned int)slot, esp_err_to_name(error));
return 1;
}
printf("Profile %u %sd in RAM.\n", (unsigned int)slot,
strcmp(operation, "delete") == 0 ? "delete" : operation);
return 0;
}
static int set_ap_parameter(const char *parameter, const char *value)
{
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
if (error != ESP_OK) {
printf("Could not read working configuration: %s\n", esp_err_to_name(error));
return 1;
}
if (strcmp(parameter, "policy") == 0) {
if (!wifi_config_parse_ap_policy(value, &config.ap_policy)) {
wifi_config_secure_wipe(&config, sizeof(config));
printf("AP policy must be off, fallback, or always.\n");
return 1;
}
} else if (strcmp(parameter, "ssid") == 0) {
size_t length = strlen(value);
if (length == 0U || length > WIFI_CONFIG_SSID_MAX_LEN) {
wifi_config_secure_wipe(&config, sizeof(config));
printf("AP SSID must be 1..32 bytes.\n");
return 1;
}
memset(config.ap_ssid, 0, sizeof(config.ap_ssid));
memcpy(config.ap_ssid, value, length);
config.ap_ssid_len = (uint8_t)length;
} else {
uint32_t channel;
if (!parse_u32(value, WIFI_CONFIG_AP_CHANNEL_MAX, &channel) ||
channel < WIFI_CONFIG_AP_CHANNEL_MIN) {
wifi_config_secure_wipe(&config, sizeof(config));
printf("AP channel must be %u..%u.\n",
WIFI_CONFIG_AP_CHANNEL_MIN, WIFI_CONFIG_AP_CHANNEL_MAX);
return 1;
}
config.ap_channel = (uint8_t)channel;
}
error = apply_candidate(&config);
if (error != ESP_OK) {
printf("Could not apply AP configuration: %s\n", esp_err_to_name(error));
return 1;
}
printf("AP %s updated in RAM; use 'wifi save' to persist it.\n", parameter);
return 0;
}
static int set_ap_secret(void)
{
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
if (error != ESP_OK) {
printf("Could not read working configuration: %s\n", esp_err_to_name(error));
return 1;
}
error = read_secret_no_echo(config.ap_psk, &config.ap_psk_len);
if (error == ESP_OK) {
error = apply_candidate(&config);
} else {
wifi_config_secure_wipe(&config, sizeof(config));
}
if (error != ESP_OK) {
if (error != ESP_ERR_INVALID_STATE) {
printf("Could not set AP secret: %s\n", esp_err_to_name(error));
}
return 1;
}
printf("AP secret updated in RAM; use 'wifi save' to persist it.\n");
return 0;
}
static int show_ap_secret(void)
{
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
if (error != ESP_OK) {
printf("Could not read AP secret: %s\n", esp_err_to_name(error));
return 1;
}
printf("AP secret: ");
fwrite(config.ap_psk, 1U, config.ap_psk_len, stdout);
putchar('\n');
printf("This credential is stored as plaintext in NVS until NVS encryption is enabled.\n");
wifi_config_secure_wipe(&config, sizeof(config));
return 0;
}
static int save_config(void)
{
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
if (error == ESP_OK) {
error = wifi_config_save(&config);
}
wifi_config_secure_wipe(&config, sizeof(config));
if (error != ESP_OK) {
printf("Could not save Wi-Fi configuration: %s\n", esp_err_to_name(error));
return 1;
}
printf("Wi-Fi configuration saved to NVS. Credentials are not yet encrypted at rest.\n");
return 0;
}
static int load_config(void)
{
wifi_app_config_t config;
wifi_config_load_source_t source;
esp_err_t error = wifi_config_load(&config, &source);
if (error == ESP_OK) {
error = wifi_manager_apply_working_config(&config);
}
wifi_config_secure_wipe(&config, sizeof(config));
if (error != ESP_OK) {
printf("Could not load Wi-Fi configuration: %s\n", esp_err_to_name(error));
return 1;
}
const char *description = source == WIFI_CONFIG_LOAD_STORED
? "stored"
: (source == WIFI_CONFIG_LOAD_GENERATED_MISSING
? "new default (no stored blob)"
: "new default (stored blob is incompatible)");
printf("Loaded %s Wi-Fi configuration into RAM.\n", description);
return 0;
}
static int apply_defaults(bool persist)
{
wifi_app_config_t previous;
wifi_app_config_t defaults;
esp_err_t error = wifi_manager_get_working_config(&previous);
if (error == ESP_OK) {
error = wifi_config_defaults(&defaults);
}
if (error == ESP_OK) {
error = wifi_manager_apply_working_config(&defaults);
}
if (error == ESP_OK && persist) {
error = wifi_config_reset_storage(&defaults);
if (error != ESP_OK) {
/* Restore RAM behavior if persistence failed. */
(void)wifi_manager_apply_working_config(&previous);
}
}
wifi_config_secure_wipe(&previous, sizeof(previous));
wifi_config_secure_wipe(&defaults, sizeof(defaults));
if (error != ESP_OK) {
printf("Could not apply Wi-Fi defaults: %s\n", esp_err_to_name(error));
return 1;
}
printf("Fresh defaults applied%s; use 'wifi ap show-secret' for the new AP credential.\n",
persist ? " and saved" : " in RAM");
return 0;
}
static int queue_lifecycle(const char *operation)
{
esp_err_t error;
if (strcmp(operation, "start") == 0) {
error = wifi_manager_start();
} else if (strcmp(operation, "stop") == 0) {
error = wifi_manager_stop();
} else {
error = wifi_manager_reconnect();
}
if (error != ESP_OK) {
printf("Could not queue Wi-Fi %s: %s\n", operation, esp_err_to_name(error));
return 1;
}
printf("Wi-Fi %s queued.\n", operation);
return 0;
}
static int command_wifi(int argc, char **argv)
{
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0)) {
return show_status();
}
if (argc == 2 && strcmp(argv[1], "profiles") == 0) {
return show_profiles();
}
if (argc == 2 && strcmp(argv[1], "counters") == 0) {
return show_counters();
}
if (argc == 2 && strcmp(argv[1], "clear-counters") == 0) {
esp_err_t error = wifi_manager_clear_counters();
if (error != ESP_OK) {
printf("Could not clear Wi-Fi counters: %s\n", esp_err_to_name(error));
return 1;
}
printf("Wi-Fi counters cleared.\n");
return 0;
}
if (argc == 2 && (strcmp(argv[1], "start") == 0 ||
strcmp(argv[1], "stop") == 0 ||
strcmp(argv[1], "reconnect") == 0)) {
return queue_lifecycle(argv[1]);
}
if (argc == 7 && strcmp(argv[1], "profile") == 0 &&
strcmp(argv[2], "set") == 0) {
return set_profile(argv);
}
if (argc == 4 && strcmp(argv[1], "profile") == 0 &&
strcmp(argv[2], "secret") == 0) {
return set_profile_secret(argv[3]);
}
if (argc == 4 && strcmp(argv[1], "profile") == 0 &&
(strcmp(argv[2], "enable") == 0 ||
strcmp(argv[2], "disable") == 0 ||
strcmp(argv[2], "delete") == 0)) {
return change_profile_state(argv[2], argv[3]);
}
if (argc == 4 && strcmp(argv[1], "ap") == 0 &&
(strcmp(argv[2], "policy") == 0 ||
strcmp(argv[2], "ssid") == 0 ||
strcmp(argv[2], "channel") == 0)) {
return set_ap_parameter(argv[2], argv[3]);
}
if (argc == 3 && strcmp(argv[1], "ap") == 0 &&
strcmp(argv[2], "secret") == 0) {
return set_ap_secret();
}
if (argc == 3 && strcmp(argv[1], "ap") == 0 &&
strcmp(argv[2], "show-secret") == 0) {
return show_ap_secret();
}
if (argc == 2 && strcmp(argv[1], "save") == 0) {
return save_config();
}
if (argc == 2 && strcmp(argv[1], "load") == 0) {
return load_config();
}
if (argc == 2 && strcmp(argv[1], "defaults") == 0) {
return apply_defaults(false);
}
if (argc == 2 && strcmp(argv[1], "reset") == 0) {
return apply_defaults(true);
}
print_usage();
return 1;
}
esp_err_t wifi_console_register_commands(void)
{
const esp_console_cmd_t command = {
.command = "wifi",
.help = "Configure multi-profile Wi-Fi and AP fallback; use 'wifi' for usage/status",
.hint = NULL,
.func = &command_wifi,
.argtable = NULL,
};
return esp_console_cmd_register(&command);
}
+8
View File
@@ -0,0 +1,8 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#pragma once
#include "esp_err.h"
/* Register Wi-Fi configuration, lifecycle, and diagnostic commands on UART0. */
esp_err_t wifi_console_register_commands(void);
+1407
View File
File diff suppressed because it is too large Load Diff
+102
View File
@@ -0,0 +1,102 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Native ESP-IDF Wi-Fi lifecycle and connection policy manager. */
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "esp_wifi_types.h"
#include "wifi_config.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
WIFI_MANAGER_STATE_STOPPED = 0,
WIFI_MANAGER_STATE_STARTING,
WIFI_MANAGER_STATE_CONNECTING,
WIFI_MANAGER_STATE_WAITING_IP,
WIFI_MANAGER_STATE_ONLINE,
WIFI_MANAGER_STATE_BACKOFF,
WIFI_MANAGER_STATE_AP_ONLY,
WIFI_MANAGER_STATE_ERROR,
} wifi_manager_state_t;
typedef struct {
uint64_t starts;
uint64_t stops;
uint64_t applies;
uint64_t connect_attempts;
uint64_t associations;
uint64_t got_ip;
uint64_t disconnects;
uint64_t profile_cycles;
uint64_t retries;
uint64_t ap_starts;
uint64_t ap_stops;
uint64_t client_joins;
uint64_t client_leaves;
uint64_t queue_drops;
} wifi_manager_counters_t;
typedef struct {
bool initialized;
bool started;
wifi_manager_state_t state;
/* -1 means that no station profile is currently being attempted or used. */
int8_t active_profile;
uint8_t sta_ssid_len;
char sta_ssid[WIFI_CONFIG_SSID_MAX_LEN + 1U];
uint32_t ip;
uint32_t netmask;
uint32_t gateway;
uint8_t sta_channel;
int8_t sta_rssi;
wifi_auth_mode_t sta_auth;
wifi_config_ap_policy_t ap_policy;
bool ap_running;
uint8_t ap_client_count;
uint8_t ap_channel;
uint16_t last_disconnect_reason;
esp_err_t last_error;
uint32_t retry_seconds;
uint32_t config_generation;
wifi_manager_counters_t counters;
} wifi_manager_snapshot_t;
/*
* Initializes ESP-NETIF, the default event loop, both default Wi-Fi netifs,
* Wi-Fi itself, and the permanent policy task. The manager never aborts the
* application on failure; callers may keep UART/USB services operational.
*/
esp_err_t wifi_manager_init(const wifi_app_config_t *config);
/* Returns a copy of the RAM working configuration, including credentials. */
esp_err_t wifi_manager_get_working_config(wifi_app_config_t *config);
/*
* Replaces the RAM working configuration. Application is asynchronous; when
* Wi-Fi is running, the manager task restarts it using the newest generation.
*/
esp_err_t wifi_manager_apply_working_config(const wifi_app_config_t *config);
/* Lifecycle requests are asynchronous and serialized by the manager task. */
esp_err_t wifi_manager_start(void);
esp_err_t wifi_manager_stop(void);
esp_err_t wifi_manager_reconnect(void);
/* Snapshot data never contains station or AP passwords. */
esp_err_t wifi_manager_get_snapshot(wifi_manager_snapshot_t *snapshot);
esp_err_t wifi_manager_clear_counters(void);
const char *wifi_manager_state_to_string(wifi_manager_state_t state);
#ifdef __cplusplus
}
#endif