From 64f1cb35cc2b75a970508bf237ac648964a8f67e Mon Sep 17 00:00:00 2001 From: sasa Date: Tue, 1 Sep 2026 22:17:08 +0300 Subject: [PATCH] feat: validate credentials, persist them, and switch without rebooting --- PLANS.md | 12 +++- docs/RELEASE_GUIDE.md | 8 +++ include/network_credential_store.h | 3 +- src/main.c | 105 +++++++++++++++++++++------- src/network_credential_store.c | 2 +- src/network_state.c | 1 + test/host/test_network_foundation.c | 4 ++ 7 files changed, 107 insertions(+), 28 deletions(-) diff --git a/PLANS.md b/PLANS.md index e668d96..69a9720 100644 --- a/PLANS.md +++ b/PLANS.md @@ -114,7 +114,7 @@ If all criteria pass, set this milestone to `DONE`, append its execution record, ## Milestone 001 — Prove build, flashing, and stable basic operation -**Status:** `DONE` +**Status:** `DONE` **Depends on:** Milestone 000 ### Objective @@ -2306,7 +2306,7 @@ When all criteria pass, set this milestone to `DONE` and change Milestone 007 to ## Milestone 029 — Validate credentials, persist them, and switch without rebooting -**Status:** `BLOCKED` +**Status:** `DONE` **Depends on:** Milestone 028 ### Objective @@ -2342,6 +2342,14 @@ Any client with network access to the device can change or delete its Wi-Fi conf When all criteria pass, set this milestone to `DONE` and change Milestone 008 to `READY`. +### Execution record + +- Date: 2026-09-01 +- Result: PASS. +- Evidence: The user completed the required manual device validation and confirmed that saved-network validation, persistence, no-reboot switching, fallback access, failure recovery, and profile deletion work as expected. +- Verification: The implementation’s host tests, browser tests, JavaScript syntax check, firmware build, LittleFS build, and `git diff --check` passed. The follow-up recovery fix preserves the 30-second fallback deadline across repeated STA disconnects, retries fallback AP startup, and does not erase NVS automatically. +- Next action: Milestone 030 remains blocked until explicitly requested and completed with its required recovery and stability evidence. + --- ## Milestone 030 — Verify recovery, game continuity, and resource stability diff --git a/docs/RELEASE_GUIDE.md b/docs/RELEASE_GUIDE.md index 81cf8c8..553b2ab 100644 --- a/docs/RELEASE_GUIDE.md +++ b/docs/RELEASE_GUIDE.md @@ -92,6 +92,14 @@ state/delivery latency with the hard limits in `RESOURCE_BUDGET.md`. state polling and retries WebSocket at 1, 2, 5, then 10 seconds. - If the board has a new DHCP address, use the router/serial information and open the new local URL. No fixed IP or external service is required. +- For a saved network that does not become operational, the serial log records + the profile outcome, Wi-Fi disconnect reason, the 30-second timeout, AP + startup result, and STA/AP addresses. `fallback AP active` means that + `Battleship-open` should be available; connect to it and open + `http://192.168.4.1/setup`. These diagnostics never include the password. +- A corrupt or unsupported saved record is left intact for diagnosis but is + ignored for that boot; the board starts `Battleship-open`. Do not erase NVS + or flash as an initial recovery step. - If LittleFS assets fail to load, repeat `uploadfs` for the confirmed target port before reflashing firmware. Do not format LittleFS automatically. - Capture `/api/health`, reset reason, and the exact source revision before diff --git a/include/network_credential_store.h b/include/network_credential_store.h index 8012430..62c4c29 100644 --- a/include/network_credential_store.h +++ b/include/network_credential_store.h @@ -2,7 +2,8 @@ #define NETWORK_CREDENTIAL_STORE_H #include #include "network_state.h" -bool network_credential_store_load(network_profile_t *profile); +typedef enum { NETWORK_CREDENTIAL_LOAD_VALID, NETWORK_CREDENTIAL_LOAD_ABSENT, NETWORK_CREDENTIAL_LOAD_INVALID } network_credential_load_result_t; +network_credential_load_result_t network_credential_store_load(network_profile_t *profile); bool network_credential_store_replace(const network_profile_t *profile); bool network_credential_store_delete(void); #endif diff --git a/src/main.c b/src/main.c index 5d6f66d..5127a77 100644 --- a/src/main.c +++ b/src/main.c @@ -2,6 +2,7 @@ #include #include #include +#include #include "app_config.h" #include "application.h" @@ -40,6 +41,8 @@ static portMUX_TYPE s_diagnostics_lock = portMUX_INITIALIZER_UNLOCKED; static wifi_state_t s_wifi_state = {0}; static bool s_littlefs_mounted; static httpd_handle_t s_server; +static esp_netif_t *s_station_netif; +static esp_netif_t *s_access_point_netif; static application_t s_application; static http_api_t s_api; static sync_service_t s_sync; @@ -52,6 +55,7 @@ static network_configuration_t s_configuration; static bool s_scan_pending; static bool s_scan_ready; static bool s_dns_running; +static bool s_validation_waiting_for_disconnect; enum { kNetworkRequestBytes = 160U, kNetworkResponseBytes = 768U }; @@ -266,14 +270,20 @@ static bool fallback_active(void) { return state.fallback_active; } +static void log_netif_ip(const char *name, esp_netif_t *netif) { + esp_netif_ip_info_t info = {0}; + if (netif == NULL || esp_netif_get_ip_info(netif, &info) != ESP_OK) { ESP_LOGW(kLogTag, "%s IP unavailable", name); return; } + ESP_LOGI(kLogTag, "%s IP " IPSTR, name, IP2STR(&info.ip)); +} + static void captive_dns_task(void *unused) { (void)unused; const int socket_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (socket_fd < 0) { ESP_LOGW(kLogTag, "captive DNS unavailable"); s_dns_running = false; vTaskDelete(NULL); return; } + if (socket_fd < 0) { ESP_LOGE(kLogTag, "captive DNS socket failed: errno=%d", errno); s_dns_running = false; vTaskDelete(NULL); return; } struct sockaddr_in local = {.sin_family = AF_INET, .sin_port = htons(kDnsPort), .sin_addr.s_addr = htonl(INADDR_ANY)}; - if (bind(socket_fd, (struct sockaddr *)&local, sizeof(local)) != 0) { close(socket_fd); s_dns_running = false; vTaskDelete(NULL); return; } + if (bind(socket_fd, (struct sockaddr *)&local, sizeof(local)) != 0) { ESP_LOGE(kLogTag, "captive DNS bind failed: errno=%d", errno); close(socket_fd); s_dns_running = false; vTaskDelete(NULL); return; } const struct timeval timeout = {.tv_sec = 1, .tv_usec = 0}; - setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + if (setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) != 0) ESP_LOGW(kLogTag, "captive DNS timeout setup failed: errno=%d", errno); const uint8_t address[] = {192U, 168U, 4U, 1U}; while (fallback_active()) { uint8_t query[256]; uint8_t response[272]; struct sockaddr_in client = {0}; socklen_t client_length = sizeof(client); @@ -290,14 +300,22 @@ static void start_captive_dns(void) { if (xTaskCreate(captive_dns_task, "captive_dns", 3072U, NULL, 3U, NULL) != pdPASS) { s_dns_running = false; ESP_LOGW(kLogTag, "captive DNS task unavailable"); } } -static void enable_fallback_ap(void) { +static bool enable_fallback_ap(void) { wifi_config_t access_point = {0}; snprintf((char *)access_point.ap.ssid, sizeof(access_point.ap.ssid), "%s", "Battleship-open"); access_point.ap.ssid_len = strlen((const char *)access_point.ap.ssid); access_point.ap.channel = 1U; access_point.ap.max_connection = kFallbackMaxConnections; access_point.ap.authmode = WIFI_AUTH_OPEN; - if (esp_wifi_set_config(WIFI_IF_AP, &access_point) != ESP_OK || esp_wifi_set_mode(WIFI_MODE_APSTA) != ESP_OK) { ESP_LOGW(kLogTag, "fallback access point unavailable"); return; } + const esp_err_t config_result = esp_wifi_set_config(WIFI_IF_AP, &access_point); + const esp_err_t mode_result = esp_wifi_set_mode(WIFI_MODE_APSTA); + if (config_result != ESP_OK || mode_result != ESP_OK) { + ESP_LOGE(kLogTag, "fallback AP attempt failed: config=%s mode=%s", esp_err_to_name(config_result), esp_err_to_name(mode_result)); + return false; + } portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.fallback_active = true; portEXIT_CRITICAL(&s_wifi_lock); start_captive_dns(); + log_netif_ip("AP", s_access_point_netif); + ESP_LOGI(kLogTag, "fallback AP active"); + return true; } static void disable_fallback_ap(void) { @@ -340,8 +358,12 @@ static const char *configuration_message(void) { } static void apply_network_action(network_action_t action) { - if (action == NETWORK_ACTION_FALLBACK) enable_fallback_ap(); - else if (action == NETWORK_ACTION_CONNECT && esp_wifi_connect() != ESP_OK) ESP_LOGW(kLogTag, "saved network connection request failed"); + if (action == NETWORK_ACTION_FALLBACK) { ESP_LOGW(kLogTag, "network transition: fallback"); enable_fallback_ap(); } + else if (action == NETWORK_ACTION_CONNECT) { + const esp_err_t result = esp_wifi_connect(); + if (result != ESP_OK) ESP_LOGW(kLogTag, "saved network connection request failed: %s", esp_err_to_name(result)); + else ESP_LOGI(kLogTag, "network transition: connection requested"); + } } static network_action_t network_event_action(bool connected) { @@ -357,7 +379,11 @@ static void network_timer_callback(void *unused) { const uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000U); network_action_t action; portENTER_CRITICAL(&s_network_lock); action = network_manager_tick(&s_network_manager, now_ms); portEXIT_CRITICAL(&s_network_lock); + if (action == NETWORK_ACTION_FALLBACK) ESP_LOGW(kLogTag, "saved network connection timed out after 30 seconds"); apply_network_action(action); + bool must_retry_fallback = false; + portENTER_CRITICAL(&s_network_lock); must_retry_fallback = s_network_manager.state == NETWORK_STATE_FALLBACK; portEXIT_CRITICAL(&s_network_lock); + if (must_retry_fallback && !fallback_active()) { ESP_LOGW(kLogTag, "retrying fallback AP startup"); enable_fallback_ap(); } bool validation_timed_out = false; bool success_notice_expired = false; network_profile_t previous = {0}; bool had_previous = false; portENTER_CRITICAL(&s_configuration_lock); validation_timed_out = network_configuration_validation_timed_out(&s_configuration, now_ms); @@ -396,31 +422,43 @@ static void validation_connected_work(void *unused) { } static void wifi_event_handler(void *argument, esp_event_base_t event_base, int32_t event_id, void *event_data) { - (void)argument; (void)event_data; - if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) apply_network_action(network_event_action(false)); + (void)argument; + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { ESP_LOGI(kLogTag, "Wi-Fi event: STA started"); apply_network_action(network_event_action(false)); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + const wifi_event_sta_disconnected_t *disconnected = event_data; + ESP_LOGW(kLogTag, "Wi-Fi event: STA disconnected, reason=%u", disconnected == NULL ? 0U : (unsigned int)disconnected->reason); portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.connected = false; portEXIT_CRITICAL(&s_wifi_lock); - if (!configuration_is_validating()) apply_network_action(network_event_action(false)); - } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { s_scan_pending = false; s_scan_ready = true; } + if (configuration_is_validating()) { + portENTER_CRITICAL(&s_configuration_lock); s_validation_waiting_for_disconnect = false; portEXIT_CRITICAL(&s_configuration_lock); + } else apply_network_action(network_event_action(false)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { ESP_LOGI(kLogTag, "Wi-Fi event: scan complete"); s_scan_pending = false; s_scan_ready = true; } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { bool validating = configuration_is_validating(); if (validating) { - if (s_server != NULL) httpd_queue_work(s_server, validation_connected_work, NULL); + bool accepts_ip; + portENTER_CRITICAL(&s_configuration_lock); accepts_ip = !s_validation_waiting_for_disconnect; portEXIT_CRITICAL(&s_configuration_lock); + if (accepts_ip && s_server != NULL) httpd_queue_work(s_server, validation_connected_work, NULL); } else network_event_action(true); portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.connected = true; portEXIT_CRITICAL(&s_wifi_lock); + log_netif_ip("STA", s_station_netif); if (!validating && fallback_active()) disable_fallback_ap(); - } + } else if (event_base == WIFI_EVENT) ESP_LOGW(kLogTag, "Wi-Fi event ignored: id=%ld", (long)event_id); } static esp_err_t start_wifi(void) { network_profile_t profile = {0}; - const bool has_profile = network_credential_store_load(&profile); + const network_credential_load_result_t load_result = network_credential_store_load(&profile); + const bool has_profile = load_result == NETWORK_CREDENTIAL_LOAD_VALID; wifi_init_config_t init_config = WIFI_INIT_CONFIG_DEFAULT(); wifi_config_t station_config = {0}; const uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000U); - portENTER_CRITICAL(&s_network_lock); const network_action_t initial_action = network_manager_init(&s_network_manager, has_profile ? &profile : NULL, now_ms); portEXIT_CRITICAL(&s_network_lock); + portENTER_CRITICAL(&s_network_lock); network_action_t initial_action = network_manager_init(&s_network_manager, has_profile ? &profile : NULL, now_ms); portEXIT_CRITICAL(&s_network_lock); portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.configured = has_profile; portEXIT_CRITICAL(&s_wifi_lock); - esp_netif_create_default_wifi_sta(); esp_netif_create_default_wifi_ap(); + if (load_result == NETWORK_CREDENTIAL_LOAD_VALID) ESP_LOGI(kLogTag, "saved network profile accepted"); + else if (load_result == NETWORK_CREDENTIAL_LOAD_ABSENT) ESP_LOGW(kLogTag, "saved network profile absent; starting fallback"); + else ESP_LOGE(kLogTag, "saved network profile invalid or unsupported; starting fallback"); + s_station_netif = esp_netif_create_default_wifi_sta(); s_access_point_netif = esp_netif_create_default_wifi_ap(); + if (s_station_netif == NULL || s_access_point_netif == NULL) return ESP_ERR_NO_MEM; ESP_RETURN_ON_ERROR(esp_wifi_init(&init_config), kLogTag, "wifi initialization failed"); ESP_RETURN_ON_ERROR(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, NULL), kLogTag, "wifi event registration failed"); ESP_RETURN_ON_ERROR(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL, NULL), kLogTag, "ip event registration failed"); @@ -429,9 +467,20 @@ static esp_err_t start_wifi(void) { if (has_profile) { memcpy(station_config.sta.ssid, profile.ssid, kNetworkSsidBytes); memcpy(station_config.sta.password, profile.password, kNetworkPasswordBytes); - ESP_RETURN_ON_ERROR(esp_wifi_set_config(WIFI_IF_STA, &station_config), kLogTag, "saved network configuration failed"); + const esp_err_t station_result = esp_wifi_set_config(WIFI_IF_STA, &station_config); + if (station_result != ESP_OK) { + ESP_LOGE(kLogTag, "saved network configuration rejected: %s; falling back", esp_err_to_name(station_result)); + portENTER_CRITICAL(&s_network_lock); initial_action = network_manager_init(&s_network_manager, NULL, now_ms); portEXIT_CRITICAL(&s_network_lock); + portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.configured = false; portEXIT_CRITICAL(&s_wifi_lock); + } } - ESP_RETURN_ON_ERROR(esp_wifi_set_mode(initial_action == NETWORK_ACTION_FALLBACK ? WIFI_MODE_AP : WIFI_MODE_STA), kLogTag, "wifi mode setup failed"); + esp_err_t mode_result = esp_wifi_set_mode(initial_action == NETWORK_ACTION_FALLBACK ? WIFI_MODE_AP : WIFI_MODE_STA); + if (mode_result != ESP_OK && initial_action != NETWORK_ACTION_FALLBACK) { + ESP_LOGE(kLogTag, "STA mode setup failed: %s; attempting fallback", esp_err_to_name(mode_result)); + portENTER_CRITICAL(&s_network_lock); initial_action = network_manager_init(&s_network_manager, NULL, now_ms); portEXIT_CRITICAL(&s_network_lock); + mode_result = esp_wifi_set_mode(WIFI_MODE_AP); + } + ESP_RETURN_ON_ERROR(mode_result, kLogTag, "wifi mode setup failed"); ESP_RETURN_ON_ERROR(esp_wifi_start(), kLogTag, "wifi start failed"); ESP_RETURN_ON_ERROR(esp_timer_start_periodic(s_network_timer, 1000000U), kLogTag, "network timer start failed"); if (initial_action == NETWORK_ACTION_FALLBACK) enable_fallback_ap(); @@ -573,7 +622,10 @@ static esp_err_t network_validate_handler(httpd_req_t *request) { started = network_configuration_begin(&s_configuration, &s_network_manager, &candidate, now_ms); portEXIT_CRITICAL(&s_configuration_lock); portEXIT_CRITICAL(&s_network_lock); if (!started) { network_response(request, 409U, "{\"ok\":false,\"code\":\"NETWORK_BUSY\",\"message\":\"Другая операция уже выполняется.\"}"); return ESP_OK; } - enable_fallback_ap(); esp_wifi_disconnect(); + enable_fallback_ap(); + portENTER_CRITICAL(&s_configuration_lock); s_validation_waiting_for_disconnect = true; portEXIT_CRITICAL(&s_configuration_lock); + const esp_err_t disconnect_result = esp_wifi_disconnect(); + if (disconnect_result == ESP_ERR_WIFI_NOT_CONNECT) { portENTER_CRITICAL(&s_configuration_lock); s_validation_waiting_for_disconnect = false; portEXIT_CRITICAL(&s_configuration_lock); } if (configure_station_profile(&candidate) != ESP_OK || esp_wifi_connect() != ESP_OK) { portENTER_CRITICAL(&s_network_lock); portENTER_CRITICAL(&s_configuration_lock); network_configuration_finish(&s_configuration, &s_network_manager, false, now_ms); @@ -588,7 +640,7 @@ static esp_err_t network_delete_handler(httpd_req_t *request) { if (!network_credential_store_delete()) { network_response(request, 503U, "{\"ok\":false,\"code\":\"STORAGE_UNAVAILABLE\",\"message\":\"Не удалось удалить сохранённую сеть.\"}"); return ESP_OK; } const uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000U); portENTER_CRITICAL(&s_network_lock); network_manager_init(&s_network_manager, NULL, now_ms); portEXIT_CRITICAL(&s_network_lock); - portENTER_CRITICAL(&s_configuration_lock); network_configuration_init(&s_configuration); portEXIT_CRITICAL(&s_configuration_lock); + portENTER_CRITICAL(&s_configuration_lock); network_configuration_init(&s_configuration); s_validation_waiting_for_disconnect = false; portEXIT_CRITICAL(&s_configuration_lock); portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.configured = false; s_wifi_state.connected = false; portEXIT_CRITICAL(&s_wifi_lock); esp_wifi_disconnect(); configure_station_profile(NULL); enable_fallback_ap(); network_response(request, 200U, "{\"ok\":true,\"state\":\"fallback\",\"message\":\"Сохранённая сеть удалена. Battleship-open остаётся доступной.\"}"); return ESP_OK; @@ -617,7 +669,8 @@ static esp_err_t static_file_handler(httpd_req_t *request) { static esp_err_t start_http_server(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.max_uri_handlers = 25U; config.max_open_sockets = kHttpMaxOpenSockets; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true; - ESP_RETURN_ON_ERROR(httpd_start(&s_server, &config), kLogTag, "http server start failed"); + const esp_err_t start_result = httpd_start(&s_server, &config); + if (start_result != ESP_OK) { ESP_LOGE(kLogTag, "HTTP server startup failed: %s", esp_err_to_name(start_result)); return start_result; } const httpd_uri_t routes[] = { {.uri = "/", .method = HTTP_GET, .handler = root_handler}, {.uri = "/setup", .method = HTTP_GET, .handler = setup_handler}, @@ -643,7 +696,11 @@ static esp_err_t start_http_server(void) { {.uri = "/ws", .method = HTTP_GET, .handler = websocket_handler, .is_websocket = true}, {.uri = "/*", .method = HTTP_GET, .handler = static_file_handler}, }; - for (size_t index = 0; index < sizeof(routes) / sizeof(routes[0]); ++index) ESP_RETURN_ON_ERROR(httpd_register_uri_handler(s_server, &routes[index]), kLogTag, "route registration failed"); + for (size_t index = 0; index < sizeof(routes) / sizeof(routes[0]); ++index) { + const esp_err_t register_result = httpd_register_uri_handler(s_server, &routes[index]); + if (register_result != ESP_OK) { ESP_LOGE(kLogTag, "HTTP route registration failed: %s", esp_err_to_name(register_result)); return register_result; } + } + ESP_LOGI(kLogTag, "HTTP server active"); return ESP_OK; } @@ -655,8 +712,8 @@ static void mount_littlefs(void) { void app_main(void) { esp_err_t result = nvs_flash_init(); - if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); result = nvs_flash_init(); } - ESP_ERROR_CHECK(result); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); + if (result != ESP_OK) ESP_LOGE(kLogTag, "NVS unavailable (%s); preserving storage and starting fallback", esp_err_to_name(result)); + ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); application_init(&s_application, (random_source_t){.next_u32 = platform_random, .context = NULL}); const esp_timer_create_args_t bot_timer_args = {.callback = bot_timer_callback, .name = "bot_turn"}; ESP_ERROR_CHECK(esp_timer_create(&bot_timer_args, &s_bot_timer)); diff --git a/src/network_credential_store.c b/src/network_credential_store.c index 5e472cf..b9611fd 100644 --- a/src/network_credential_store.c +++ b/src/network_credential_store.c @@ -4,6 +4,6 @@ static const char *const kNamespace = "network"; static const char *const kProfileKey = "profile"; -bool network_credential_store_load(network_profile_t *profile) { nvs_handle_t handle; network_credential_record_t record; size_t size = sizeof(record); if (nvs_open(kNamespace, NVS_READONLY, &handle) != ESP_OK) return false; const esp_err_t result = nvs_get_blob(handle, kProfileKey, &record, &size); nvs_close(handle); return result == ESP_OK && size == sizeof(record) && network_credential_decode(&record, profile); } +network_credential_load_result_t network_credential_store_load(network_profile_t *profile) { nvs_handle_t handle; network_credential_record_t record; size_t size = sizeof(record); const esp_err_t open_result = nvs_open(kNamespace, NVS_READONLY, &handle); if (open_result == ESP_ERR_NVS_NOT_FOUND) return NETWORK_CREDENTIAL_LOAD_ABSENT; if (open_result != ESP_OK) return NETWORK_CREDENTIAL_LOAD_INVALID; const esp_err_t result = nvs_get_blob(handle, kProfileKey, &record, &size); nvs_close(handle); if (result == ESP_ERR_NVS_NOT_FOUND) return NETWORK_CREDENTIAL_LOAD_ABSENT; return result == ESP_OK && size == sizeof(record) && network_credential_decode(&record, profile) ? NETWORK_CREDENTIAL_LOAD_VALID : NETWORK_CREDENTIAL_LOAD_INVALID; } bool network_credential_store_replace(const network_profile_t *profile) { network_credential_record_t record; nvs_handle_t handle; if (!network_credential_encode(profile, &record) || nvs_open(kNamespace, NVS_READWRITE, &handle) != ESP_OK) return false; const esp_err_t result = nvs_set_blob(handle, kProfileKey, &record, sizeof(record)); const bool ok = result == ESP_OK && nvs_commit(handle) == ESP_OK; nvs_close(handle); return ok; } bool network_credential_store_delete(void) { nvs_handle_t handle; if (nvs_open(kNamespace, NVS_READWRITE, &handle) != ESP_OK) return false; const esp_err_t result = nvs_erase_key(handle, kProfileKey); const bool ok = (result == ESP_OK || result == ESP_ERR_NVS_NOT_FOUND) && nvs_commit(handle) == ESP_OK; nvs_close(handle); return ok; } diff --git a/src/network_state.c b/src/network_state.c index 6792968..da0ac3c 100644 --- a/src/network_state.c +++ b/src/network_state.c @@ -20,6 +20,7 @@ network_action_t network_manager_tick(network_manager_t *manager, uint32_t now_m network_action_t network_manager_connected(network_manager_t *manager) { manager->state = NETWORK_STATE_EXTERNAL; return NETWORK_ACTION_NONE; } network_action_t network_manager_disconnected(network_manager_t *manager, uint32_t now_ms) { if (!manager->has_profile) { manager->state = NETWORK_STATE_FALLBACK; return NETWORK_ACTION_FALLBACK; } + if (manager->state == NETWORK_STATE_CONNECTING) return NETWORK_ACTION_CONNECT; manager->state = NETWORK_STATE_CONNECTING; manager->started_ms = now_ms; return NETWORK_ACTION_CONNECT; } bool network_manager_begin_validation(network_manager_t *manager, const network_profile_t *candidate) { diff --git a/test/host/test_network_foundation.c b/test/host/test_network_foundation.c index 1ce459b..038e367 100644 --- a/test/host/test_network_foundation.c +++ b/test/host/test_network_foundation.c @@ -16,6 +16,8 @@ int main(void) { assert(network_manager_tick(&manager, 30009U) == NETWORK_ACTION_NONE); assert(network_manager_tick(&manager, 30010U) == NETWORK_ACTION_FALLBACK); assert(network_manager_disconnected(&manager, 400U) == NETWORK_ACTION_CONNECT); + assert(network_manager_disconnected(&manager, 1000U) == NETWORK_ACTION_CONNECT); + assert(network_manager_tick(&manager, 30400U) == NETWORK_ACTION_FALLBACK); assert(network_manager_connected(&manager) == NETWORK_ACTION_NONE && manager.state == NETWORK_STATE_EXTERNAL); assert(network_manager_begin_validation(&manager, &next)); assert(network_manager_finish_validation(&manager, false, 500U) == NETWORK_ACTION_NONE && strcmp(manager.profile.ssid, "Home") == 0); @@ -24,6 +26,8 @@ int main(void) { network_credential_record_t record; network_profile_t restored = {0}; assert(network_credential_encode(&next, &record)); assert(network_credential_decode(&record, &restored)); assert(strcmp(restored.password, "newsecret") == 0); record.profile.ssid[0] = 'X'; assert(!network_credential_decode(&record, &restored)); + assert(network_credential_encode(&next, &record)); ++record.version; assert(!network_credential_decode(&record, &restored)); + assert(network_credential_encode(&next, &record)); record.profile.password[0] = '\0'; assert(!network_credential_decode(&record, &restored)); memory_store_t store = {0}; network_credential_backend_t backend = {&store, read_record, write_record, delete_record}; assert(network_credential_replace(&backend, &home)); assert(network_credential_load(&backend, &restored)); assert(strcmp(restored.ssid, "Home") == 0); store.record.profile.ssid[0] = 'X'; assert(!network_credential_load(&backend, &restored));