/* SPDX-License-Identifier: GPL-3.0-only */ /* Physical-admin UART0 console for Wi-Fi configuration and diagnostics. */ #include "wifi_console.h" #include #include #include #include #include #include "console_input.h" #include "esp_console.h" #include "esp_err.h" #include "esp_netif_ip_addr.h" #include "esp_wifi_types.h" #include "network_console.h" #include "wifi_config.h" #include "wifi_manager.h" #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|next-profile\n"); printf(" wifi profile set \n"); printf(" wifi profile secret \n"); printf(" wifi profile enable|disable|delete \n"); printf(" wifi ap policy \n"); printf(" wifi ap ssid \n"); printf(" wifi ap channel <1..11>\n"); printf(" wifi ap secret|show-secret\n"); printf(" wifi save|load|defaults|reset\n"); printf(" wifi ping [count]\n"); printf(" wifi nslookup \n"); printf(" wifi traceroute [max-hops]\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(""); } 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 + 1U] = {0}; size_t length = 0U; esp_err_t error = console_input_read_hidden( "Enter 8..63 printable ASCII characters (input hidden, Ctrl-C cancels): ", buffer, sizeof(buffer), WIFI_CONFIG_PSK_MIN_LEN, WIFI_CONFIG_PSK_MAX_LEN, &length); if (error == ESP_OK) { 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 error; } 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 if (strcmp(operation, "reconnect") == 0) { error = wifi_manager_reconnect(); } else { error = wifi_manager_next_profile(); } 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 >= 2 && network_console_is_command(argv[1])) { /* Shift `wifi` away so aliases and subcommands share one implementation. */ return network_console_execute(argc - 1, argv + 1); } 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 || strcmp(argv[1], "next-profile") == 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); }