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
+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);
}