feat: implement Wi-Fi provisioning and persistent configuration

This commit is contained in:
2026-09-03 22:58:57 +03:00
parent 8ffd6dabcf
commit b10c7f5972
29 changed files with 1713 additions and 16 deletions
+100
View File
@@ -0,0 +1,100 @@
#include "nvs.h"
#include "nvs_flash.h"
#include <string.h>
static int initialized;
static int present;
static uint32_t schema;
static char ssid[33];
static char password[64];
esp_err_t nvs_flash_init(void)
{
initialized = 1;
return ESP_OK;
}
esp_err_t nvs_flash_deinit(void)
{
initialized = 0;
return ESP_OK;
}
esp_err_t nvs_flash_erase(void)
{
present = 0;
return ESP_OK;
}
esp_err_t nvs_open(const char *namespace_name, int mode, nvs_handle_t *handle)
{
if (!initialized || strcmp(namespace_name, "maze32") != 0) return ESP_FAIL;
if (mode == NVS_READONLY && !present) return ESP_ERR_NVS_NOT_FOUND;
*handle = 1U;
return ESP_OK;
}
void nvs_close(nvs_handle_t handle)
{
(void)handle;
}
esp_err_t nvs_get_u32(nvs_handle_t handle, const char *key, uint32_t *value)
{
(void)handle;
if (!present || strcmp(key, "cfg_ver") != 0) return ESP_ERR_NVS_NOT_FOUND;
*value = schema;
return ESP_OK;
}
esp_err_t nvs_set_u32(nvs_handle_t handle, const char *key, uint32_t value)
{
(void)handle;
if (strcmp(key, "cfg_ver") != 0) return ESP_FAIL;
schema = value;
present = 1;
return ESP_OK;
}
esp_err_t nvs_get_str(nvs_handle_t handle, const char *key, char *output, size_t *length)
{
(void)handle;
const char *source = strcmp(key, "wifi_ssid") == 0 ? ssid :
strcmp(key, "wifi_pass") == 0 ? password : NULL;
if (!present || source == NULL) return ESP_ERR_NVS_NOT_FOUND;
size_t required = strlen(source) + 1U;
if (*length < required) return ESP_FAIL;
memcpy(output, source, required);
*length = required;
return ESP_OK;
}
esp_err_t nvs_set_str(nvs_handle_t handle, const char *key, const char *value)
{
(void)handle;
char *destination = strcmp(key, "wifi_ssid") == 0 ? ssid :
strcmp(key, "wifi_pass") == 0 ? password : NULL;
size_t capacity = strcmp(key, "wifi_ssid") == 0 ? sizeof(ssid) : sizeof(password);
if (destination == NULL || strlen(value) >= capacity) return ESP_FAIL;
memcpy(destination, value, strlen(value) + 1U);
return ESP_OK;
}
esp_err_t nvs_erase_all(nvs_handle_t handle)
{
(void)handle;
present = 0;
return ESP_OK;
}
esp_err_t nvs_commit(nvs_handle_t handle)
{
(void)handle;
return ESP_OK;
}
void fake_nvs_set_schema(uint32_t version)
{
schema = version;
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef TEST_ESP_ERR_H
#define TEST_ESP_ERR_H
typedef int esp_err_t;
#define ESP_OK 0
#define ESP_FAIL 0x101
#define ESP_ERR_NO_MEM 0x102
#define ESP_ERR_INVALID_ARG 0x103
#define ESP_ERR_INVALID_STATE 0x104
#define ESP_ERR_INVALID_VERSION 0x105
#define ESP_ERR_NOT_FOUND 0x106
#define ESP_ERR_NVS_NOT_FOUND 0x1102
#define ESP_ERR_NVS_NO_FREE_PAGES 0x110d
#define ESP_ERR_NVS_NEW_VERSION_FOUND 0x1110
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef TEST_NVS_H
#define TEST_NVS_H
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
typedef unsigned int nvs_handle_t;
#define NVS_READONLY 0
#define NVS_READWRITE 1
esp_err_t nvs_open(const char *namespace_name, int mode, nvs_handle_t *handle);
void nvs_close(nvs_handle_t handle);
esp_err_t nvs_get_u32(nvs_handle_t handle, const char *key, uint32_t *value);
esp_err_t nvs_set_u32(nvs_handle_t handle, const char *key, uint32_t value);
esp_err_t nvs_get_str(nvs_handle_t handle, const char *key, char *output, size_t *length);
esp_err_t nvs_set_str(nvs_handle_t handle, const char *key, const char *value);
esp_err_t nvs_erase_all(nvs_handle_t handle);
esp_err_t nvs_commit(nvs_handle_t handle);
void fake_nvs_set_schema(uint32_t version);
#endif
+10
View File
@@ -0,0 +1,10 @@
#ifndef TEST_NVS_FLASH_H
#define TEST_NVS_FLASH_H
#include "esp_err.h"
esp_err_t nvs_flash_init(void);
esp_err_t nvs_flash_erase(void);
esp_err_t nvs_flash_deinit(void);
#endif
+45
View File
@@ -0,0 +1,45 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "maze32_config_store.h"
#include "nvs.h"
#include "nvs_flash.h"
static maze32_wifi_credentials_t credentials(const char *ssid, const char *password)
{
maze32_wifi_credentials_t value = {0};
memcpy(value.ssid, ssid, strlen(ssid) + 1U);
memcpy(value.password, password, strlen(password) + 1U);
return value;
}
int main(void)
{
assert(maze32_config_store_init() == ESP_OK);
maze32_wifi_credentials_t loaded = {0};
assert(maze32_config_store_load(&loaded) == ESP_ERR_NOT_FOUND);
maze32_wifi_credentials_t saved = credentials("PersistentNetwork", "safe-passphrase");
assert(maze32_config_store_save(&saved) == ESP_OK);
assert(nvs_flash_deinit() == ESP_OK);
assert(maze32_config_store_init() == ESP_OK);
assert(maze32_config_store_load(&loaded) == ESP_OK);
assert(strcmp(loaded.ssid, saved.ssid) == 0);
assert(strcmp(loaded.password, saved.password) == 0);
maze32_wifi_credentials_t invalid = credentials("PersistentNetwork", "short");
assert(maze32_config_store_save(&invalid) == ESP_ERR_INVALID_ARG);
memset(&loaded, 0, sizeof(loaded));
assert(maze32_config_store_load(&loaded) == ESP_OK);
assert(strcmp(loaded.password, saved.password) == 0);
fake_nvs_set_schema(MAZE32_WIFI_CONFIG_SCHEMA_VERSION + 1U);
assert(maze32_config_store_load(&loaded) == ESP_ERR_INVALID_VERSION);
fake_nvs_set_schema(MAZE32_WIFI_CONFIG_SCHEMA_VERSION);
assert(maze32_config_store_clear() == ESP_OK);
assert(maze32_config_store_load(&loaded) == ESP_ERR_NOT_FOUND);
puts("configuration NVS tests: PASS");
return 0;
}
+40
View File
@@ -0,0 +1,40 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "maze32_provisioning_protocol.h"
static void accept(const char *json, const char *ssid, const char *password)
{
maze32_wifi_credentials_t credentials;
assert(maze32_provisioning_parse_credentials(json, strlen(json), &credentials));
assert(strcmp(credentials.ssid, ssid) == 0);
assert(strcmp(credentials.password, password) == 0);
}
static void reject(const char *json)
{
maze32_wifi_credentials_t credentials;
assert(!maze32_provisioning_parse_credentials(json, strlen(json), &credentials));
}
int main(void)
{
accept("{\"ssid\":\"Maze WiFi\",\"password\":\"password8\"}",
"Maze WiFi", "password8");
accept(" { \"password\" : \"quote\\\"pass\" , \"ssid\" : \"Maze\" } ",
"Maze", "quote\"pass");
accept("{\"ssid\":\"Open\",\"password\":\"\"}", "Open", "");
reject("{}");
reject("{\"ssid\":\"Maze\"}");
reject("{\"ssid\":\"Maze\",\"password\":\"short\"}");
reject("{\"ssid\":\"Maze\",\"password\":\"password8\",\"extra\":1}");
reject("{\"ssid\":12,\"password\":\"password8\"}");
reject("{\"ssid\":\"Maze\\nBad\",\"password\":\"password8\"}");
reject("{\"ssid\":\"Maze\",\"ssid\":\"Again\"}");
reject("not-json");
puts("provisioning protocol tests: PASS");
return 0;
}
+115
View File
@@ -0,0 +1,115 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "maze32_wifi_config.h"
typedef struct {
int guard_result;
int guard_calls;
int save_calls;
int clear_calls;
maze32_wifi_credentials_t saved;
} fixture_t;
static int guard(void *context)
{
fixture_t *fixture = context;
++fixture->guard_calls;
return fixture->guard_result;
}
static int save(const maze32_wifi_credentials_t *credentials, void *context)
{
fixture_t *fixture = context;
++fixture->save_calls;
fixture->saved = *credentials;
return 0;
}
static int clear(void *context)
{
fixture_t *fixture = context;
++fixture->clear_calls;
return 0;
}
static maze32_wifi_credentials_t valid_credentials(void)
{
maze32_wifi_credentials_t credentials = {0};
memcpy(credentials.ssid, "Labyrinth", sizeof("Labyrinth"));
memcpy(credentials.password, "eight888", sizeof("eight888"));
return credentials;
}
static void test_validation_boundaries(void)
{
maze32_wifi_credentials_t credentials = valid_credentials();
assert(maze32_wifi_credentials_validate(&credentials) == MAZE32_WIFI_CONFIG_VALID);
credentials.password[0] = '\0';
assert(maze32_wifi_credentials_validate(&credentials) == MAZE32_WIFI_CONFIG_VALID);
memcpy(credentials.password, "short", sizeof("short"));
assert(maze32_wifi_credentials_validate(&credentials) ==
MAZE32_WIFI_CONFIG_INVALID_PASSWORD);
credentials = valid_credentials();
credentials.ssid[0] = '\0';
assert(maze32_wifi_credentials_validate(&credentials) == MAZE32_WIFI_CONFIG_INVALID_SSID);
memset(credentials.ssid, 's', sizeof(credentials.ssid));
assert(maze32_wifi_credentials_validate(&credentials) == MAZE32_WIFI_CONFIG_INVALID_SSID);
credentials = valid_credentials();
credentials.ssid[2] = '\n';
assert(maze32_wifi_credentials_validate(&credentials) == MAZE32_WIFI_CONFIG_INVALID_SSID);
}
static void test_invalid_input_never_mutates_storage(void)
{
fixture_t fixture = {0};
maze32_wifi_credentials_t credentials = valid_credentials();
credentials.password[0] = 'x';
credentials.password[1] = '\0';
assert(maze32_wifi_config_apply(&credentials, guard, save, &fixture) == -1);
assert(fixture.guard_calls == 0);
assert(fixture.save_calls == 0);
}
static void test_guard_precedes_every_mutation(void)
{
fixture_t fixture = {.guard_result = -1};
maze32_wifi_credentials_t credentials = valid_credentials();
assert(maze32_wifi_config_apply(&credentials, guard, save, &fixture) == -2);
assert(fixture.guard_calls == 1);
assert(fixture.save_calls == 0);
assert(maze32_wifi_config_reset(guard, clear, &fixture) == -2);
assert(fixture.guard_calls == 2);
assert(fixture.clear_calls == 0);
}
static void test_valid_apply_and_reset(void)
{
fixture_t fixture = {0};
maze32_wifi_credentials_t credentials = valid_credentials();
assert(maze32_wifi_config_apply(&credentials, guard, save, &fixture) == 0);
assert(fixture.guard_calls == 1);
assert(fixture.save_calls == 1);
assert(strcmp(fixture.saved.ssid, credentials.ssid) == 0);
assert(strcmp(fixture.saved.password, credentials.password) == 0);
assert(maze32_wifi_config_reset(guard, clear, &fixture) == 0);
assert(fixture.guard_calls == 2);
assert(fixture.clear_calls == 1);
}
int main(void)
{
test_validation_boundaries();
test_invalid_input_never_mutates_storage();
test_guard_precedes_every_mutation();
test_valid_apply_and_reset();
puts("wifi configuration tests: PASS");
return 0;
}