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
+63 -1
View File
@@ -12,7 +12,7 @@ Universal wireless serial adaptor firmware for the ESP32-S3.
- 8 MB octal PSRAM - 8 MB octal PSRAM
- Adafruit MAX3243 full-pinout RS-232 breakout, product 5988 - Adafruit MAX3243 full-pinout RS-232 breakout, product 5988
The firmware has completed **Phase 0 hardware characterization**, the **Phase 1 serial-core foundation**, and the **Phase 2 transport-neutral session broker**. The current phase adds the first real broker transport: native USB CDC-ACM on the ESP32-S3 USB port. The MAX3243 diagnostics and persistent serial configuration remain available. No electrical test starts automatically; UART1 starts when requested explicitly or when a host opens native USB CDC. The firmware has completed **Phase 0 hardware characterization**, the **Phase 1 serial-core foundation**, the **Phase 2 transport-neutral session broker**, and the first real broker transport through native USB CDC-ACM. The current phase adds persistent multi-profile Wi-Fi station configuration and managed AP fallback/AP+STA operation. The MAX3243 diagnostics and recovery consoles remain available. No electrical test starts automatically; UART1 starts when requested explicitly or when a host opens native USB CDC.
## Hardware wiring ## Hardware wiring
@@ -188,6 +188,68 @@ Close the terminal and check `usb status` and `broker clients`; DTR-aware applic
Power down and remove the DE-9 pin 3-to-2 jumper before connecting an external serial peer. Power down and remove the DE-9 pin 3-to-2 jumper before connecting an external serial peer.
### Wi-Fi foundation
Wi-Fi is managed independently of the serial-session broker in this phase. It provides network connectivity and recovery access-point policy for the later HTTPS/WebSocket transport, but it does not yet run HTTP, HTTPS, DNS interception, a captive portal, NAPT, or any TCP serial listener.
Configuration uses four fixed station-profile slots. Lower numeric priority values are tried first, with slot number breaking ties. Profiles support WPA2/WPA3 mixed operation or require WPA3-SAE. ESP-IDF's station threshold can express “WPA2 or stronger” but not a strict WPA2-only maximum, so the configuration does not pretend to offer a distinct WPA2-only mode. Each profile attempt has a 12-second association/DHCP deadline. After all enabled profiles fail, the manager uses exponential retry delays from 2 to 60 seconds.
AP policy is independent of the station profiles:
| Policy | Behavior |
|---|---|
| `off` | Station only; never start the fallback AP |
| `fallback` | Start the AP immediately when no profiles exist, or after one failed profile cycle; disable it after station connectivity has remained stable for 30 seconds |
| `always` | Keep AP and station active concurrently |
Fresh defaults enable Wi-Fi with `fallback` policy, AP channel 6, a MAC-suffixed SSID such as `ESP32-SAK-A1B2C3`, and a randomly generated 16-character password. The initial random credential is saved to NVS automatically when possible so it remains stable across reboot. Retrieve it deliberately from the physical UART0 administration console with `wifi ap show-secret`.
The `wifi` command provides:
```text
wifi status
wifi profiles
wifi start|stop|reconnect
wifi profile set <slot> <priority> <mixed|wpa3> <ssid>
wifi profile secret <slot>
wifi profile enable|disable|delete <slot>
wifi ap policy <off|fallback|always>
wifi ap ssid <ssid>
wifi ap channel <1..11>
wifi ap secret|show-secret
wifi save|load|defaults|reset
wifi counters|clear-counters
```
Ordinary status and profile output never displays passwords. `wifi profile secret` and `wifi ap secret` read through a dedicated no-echo UART0 prompt, keeping credentials out of the command line and its history. SSIDs containing spaces can be quoted. Profile and AP edits apply to the working RAM configuration and restart Wi-Fi asynchronously if it is running; use `wifi save` explicitly to persist them. `wifi start` and `wifi stop` also change the working `enabled-at-boot` setting, which becomes persistent only after `wifi save`.
A typical station setup is:
```text
wifi profile set 0 10 mixed "your SSID"
wifi profile secret 0
wifi profile enable 0
wifi save
wifi reconnect
wifi status
```
The fallback AP uses Espressif's default `192.168.4.1/24` network for now. AP clients receive addresses through its DHCP server but are not routed to the station network. ESP32-S3 has one 2.4 GHz radio, so in AP+STA mode the AP follows the connected station's channel. Station connection attempts and scans can temporarily increase AP latency, and clients can briefly reconnect when the channel moves.
Wi-Fi credentials currently reside as plaintext in the application-owned `wifi_app/config` NVS blob. Selecting `WIFI_STORAGE_RAM` prevents the ESP-IDF driver from creating a second persistent credential copy, but it does not encrypt the application's blob. The reserved `nvs_key` partition alone does not enable encryption. NVS encryption, secure boot, flash encryption, and core-dump credential exposure require a deliberate later security phase.
#### Wi-Fi validation
1. Boot with no station profiles. `wifi status` should report `ap-only`, and the generated SSID should be visible from another device.
2. Use `wifi ap show-secret`, join the AP, confirm a `192.168.4.x` lease, and ping `192.168.4.1`. No web page is expected yet.
3. Configure and enable a WPA2/WPA3 station profile using the example above. `wifi status` should progress through `connecting`, `waiting-ip`, and `online` and display the acquired address, channel, RSSI, and negotiated authentication.
4. Reboot and verify profile and AP credential persistence.
5. Configure two profiles with different priorities, make the first unavailable, and verify failover to the second after its timeout.
6. Make all profiles unavailable and verify fallback AP startup plus increasing retry delays in `wifi status`/`wifi counters`.
7. Test `wifi ap policy always` while online and confirm both interfaces remain available; expect the AP channel to follow the station.
8. Test `wifi stop`, `wifi start`, and `wifi reconnect` while confirming UART0 and native USB serial operation remain unaffected.
9. If available, test a WPA3-only profile and a wrong password, then inspect the disconnect reason and counters.
### Phase 0 diagnostics ### Phase 0 diagnostics
The retained hardware-characterization commands are: The retained hardware-characterization commands are:
+5
View File
@@ -13,5 +13,10 @@ CONFIG_TINYUSB_CDC_RX_BUFSIZE=1024
CONFIG_TINYUSB_CDC_TX_BUFSIZE=1024 CONFIG_TINYUSB_CDC_TX_BUFSIZE=1024
CONFIG_TINYUSB_CDC_EP_BUFSIZE=512 CONFIG_TINYUSB_CDC_EP_BUFSIZE=512
# Support WPA3-SAE for station profiles and the WPA2/WPA3 fallback AP.
CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y
CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y
CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=y
# Keep diagnostic and interactive-console logging concise but useful. # Keep diagnostic and interactive-console logging concise but useful.
CONFIG_LOG_DEFAULT_LEVEL_INFO=y CONFIG_LOG_DEFAULT_LEVEL_INFO=y
+8
View File
@@ -11,15 +11,23 @@ idf_component_register(
"session_console.c" "session_console.c"
"usb_cdc_transport.c" "usb_cdc_transport.c"
"usb_console.c" "usb_console.c"
"wifi_config.c"
"wifi_manager.c"
"wifi_console.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES REQUIRES
bootloader_support
console console
esp_driver_gpio esp_driver_gpio
esp_driver_uart esp_driver_uart
esp_event
esp_netif
esp_psram esp_psram
esp_timer esp_timer
esp_tinyusb esp_tinyusb
esp_wifi
freertos freertos
led_strip led_strip
mbedtls
nvs_flash nvs_flash
) )
+53 -1
View File
@@ -13,6 +13,9 @@
#include "status_led.h" #include "status_led.h"
#include "usb_cdc_transport.h" #include "usb_cdc_transport.h"
#include "usb_console.h" #include "usb_console.h"
#include "wifi_config.h"
#include "wifi_console.h"
#include "wifi_manager.h"
#define CONSOLE_BAUD_RATE 115200 #define CONSOLE_BAUD_RATE 115200
#define CONSOLE_TX_GPIO 43 #define CONSOLE_TX_GPIO 43
@@ -22,7 +25,7 @@ static const char *TAG = "firmware";
void app_main(void) 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()) { if (esp_psram_is_initialized()) {
ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size()); 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"); 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. */ /* Blue means the firmware is initialized and waiting for a console command. */
ESP_ERROR_CHECK(status_led_init()); ESP_ERROR_CHECK(status_led_init());
ESP_ERROR_CHECK(rs232_port_owner_init()); ESP_ERROR_CHECK(rs232_port_owner_init());
@@ -49,6 +55,51 @@ void app_main(void)
ESP_ERROR_CHECK(session_broker_init()); ESP_ERROR_CHECK(session_broker_init());
/* Native USB owns GPIO19/20; UART0 logging stays on the USB-to-UART bridge. */ /* Native USB owns GPIO19/20; UART0 logging stays on the USB-to-UART bridge. */
ESP_ERROR_CHECK(usb_cdc_transport_init()); 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( ESP_LOGI(
TAG, TAG,
"Using %s serial configuration; UART service starts on 'serial start' or native USB open", "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(serial_console_register_commands());
ESP_ERROR_CHECK(session_console_register_commands()); ESP_ERROR_CHECK(session_console_register_commands());
ESP_ERROR_CHECK(usb_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_ERROR_CHECK(esp_console_start_repl(repl));
ESP_LOGI(TAG, "Interactive test console ready at %d baud", CONSOLE_BAUD_RATE); 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
+1
View File
@@ -85,6 +85,7 @@ The breakout's `OFF` pin is connected to the MAX3243 active-low `!FORCEOFF` inpu
- The breakout is not isolated: ESP32 ground, breakout ground, USB ground, and DE-9 pin 5 are electrically connected. - The breakout is not isolated: ESP32 ground, breakout ground, USB ground, and DE-9 pin 5 are electrically connected.
- The breakout is electrically DTE-style: `TX`, `RTS`, and `DTR` drive the DE-9, while `RX`, `CTS`, `DSR`, `DCD`, and `RI` receive from it. - The breakout is electrically DTE-style: `TX`, `RTS`, and `DTR` drive the DE-9, while `RX`, `CTS`, `DSR`, `DCD`, and `RI` receive from it.
- A straight-through male-to-male gender changer changes connector gender only. Use a straight-through cable for a DCE peer and an appropriate null-modem cable for another DTE peer. - A straight-through male-to-male gender changer changes connector gender only. Use a straight-through cable for a DCE peer and an appropriate null-modem cable for another DTE peer.
- The current female Adafruit breakout was validated in a temporary DTE-to-DTE lab chain using a straight-through male-to-male gender changer, a null-modem cable, and a USB RS-232 adapter. The gender changer corrected only the physical connector while the null-modem cable crossed the signal directions. A male version of the Adafruit breakout is planned as the cleaner permanent connector arrangement.
GPIO8 and GPIO9 are not adjacent on the official J1 header. GPIO3 and GPIO46 lie between them and are boot-strapping pins, so follow the printed GPIO labels instead of counting header positions. GPIO8 and GPIO9 are not adjacent on the official J1 header. GPIO3 and GPIO46 lie between them and are boot-strapping pins, so follow the printed GPIO labels instead of counting header positions.