feat: validate credentials, persist them, and switch without rebooting

This commit is contained in:
2026-09-01 22:17:08 +03:00
parent 50ed341474
commit 64f1cb35cc
7 changed files with 107 additions and 28 deletions
+10 -2
View File
@@ -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 implementations 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
+8
View File
@@ -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
+2 -1
View File
@@ -2,7 +2,8 @@
#define NETWORK_CREDENTIAL_STORE_H
#include <stdbool.h>
#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
+81 -24
View File
@@ -2,6 +2,7 @@
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#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));
+1 -1
View File
@@ -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; }
+1
View File
@@ -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) {
+4
View File
@@ -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));