Add native Wi-Fi management and persistence
This commit is contained in:
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
Reference in New Issue
Block a user