fix(firmware): harden config rollback and prepare 0.8.15 release

This commit is contained in:
ruv committed 2026-10-21 14:51:34 -04:00
1 parent a9b46055db
commit b3d5885c26
16 files changed
+371 -75

No files matched your search

+2
View File
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.8.15-esp32] - 2026-10-21
### ESP32 CSI input and onboarding corrections
- Process the bounded prefix of S3 STBC CSI frames instead of discarding the
+31
View File
@@ -0,0 +1,31 @@
# RuView ESP32 firmware 0.8.15 prerelease
Firmware 0.8.15 improves CSI processing and remote configuration recovery for
ESP32-S3 and ESP32-C6 nodes.
## Changes
- S3 STBC frames now contribute their bounded CSI prefix to edge processing
instead of being discarded, preserving input for vitals and feature output.
- Gateway self-ping waits are capped to one probe interval. AP-specific ICMP
airtime remains a deployment factor; `scripts/csi-udp-poke.py` provides a
bounded private-LAN traffic source when CSI traffic is otherwise sparse.
- USB serial/JTAG onboarding prepares or reuses the IDF 6 driver and reports
setup failures.
- The config API validates complete requests before writing and retains a
durable rollback bank across failed writes and recovery attempts.
- Rust dependencies were updated for current Wasmtime and WebPKI advisories;
the audit policy retains narrowly documented ignores for ZIP extraction
advisories whose vulnerable extraction APIs are not used by the workspace.
## Validation
- Firmware host suite passed, including production-code STBC, self-ping,
configuration rules, and write-through NVS rollback fault injection.
- ESP32-C6 firmware built with ESP-IDF 6.0.2 and fits the 4 MB partition map.
- The attached ESP32-C6 was observed on the LAN running firmware 0.8.14.
Thirty-second paired captures recorded about 29 CSI packets/s in baseline
and with bounded host traffic. The board was not flashed with 0.8.15 because
a complete flash backup could not be obtained.
- This prerelease has no new physical-board firmware qualification or sensing
accuracy claim. See the [candidate validation record](../validation/2026-10-20-esp32-csi-input-release-candidate.md).
+15 -1
View File
@@ -19,7 +19,21 @@ This firmware captures WiFi Channel State Information (CSI) from an ESP32-S3 (pr
> | **Fall detection** | Phase acceleration threshold | Configurable sensitivity |
> | **Programmable sensing** | WASM modules loaded over HTTP | Hot-swap, no reflash |
## Firmware 0.8.14 prerelease: device identity and OTA provisioning
## Firmware 0.8.15 prerelease: CSI reliability and recoverable configuration
Version 0.8.15 preserves bounded CSI input for ESP32-S3 STBC frames, caps
gateway ping waits to one probe interval, and prepares IDF 6 USB serial/JTAG
before onboarding. The authenticated remote configuration endpoint validates
whole requests before writing and retains a durable rollback bank if a write
or recovery step fails.
The attached ESP32-C6 supplied live CSI transport evidence while running
0.8.14. It was not flashed with 0.8.15 because a complete flash backup could
not be obtained. CI builds and host fault-injection tests verify software
behavior; they do not establish 0.8.15 physical-board qualification or sensing
accuracy.
### Firmware 0.8.14 prerelease: device identity and OTA provisioning
Version 0.8.14 fixes the six-byte base-MAC input used for USB onboarding on
ESP32-C6, where the previous API wrote an eight-byte EUI-64 into that buffer.
+93 -68
View File
@@ -70,12 +70,16 @@ static volatile bool s_pending_reboot = false;
* literal string "(set)" and lock the node off the network permanently -- the
* exact failure the trial exists to prevent.
*/
static cJSON *read_key(nvs_handle_t h, const cfg_key_t *k, bool redact)
static cJSON *read_key(nvs_handle_t h, const cfg_key_t *k, bool redact,
esp_err_t *read_status)
{
if (read_status) *read_status = ESP_OK;
if (k->type == T_STR) {
char buf[96];
size_t len = sizeof(buf);
if (nvs_get_str(h, k->nvs, buf, &len) != ESP_OK) return cJSON_CreateNull();
esp_err_t e = nvs_get_str(h, k->nvs, buf, &len);
if (read_status) *read_status = e;
if (e != ESP_OK) return cJSON_CreateNull();
/* Never hand back key material, even to an authenticated caller: it
* would end up in logs and terminal scrollback. Presence only. */
if (redact && strcmp(k->json, "wifi_password") == 0) {
@@ -86,12 +90,13 @@ static cJSON *read_key(nvs_handle_t h, const cfg_key_t *k, bool redact)
uint32_t v = 0;
esp_err_t e;
if (k->type == T_U8) {
uint8_t x; e = nvs_get_u8(h, k->nvs, &x); v = x;
uint8_t x = 0; e = nvs_get_u8(h, k->nvs, &x); v = x;
} else if (k->type == T_U16) {
uint16_t x; e = nvs_get_u16(h, k->nvs, &x); v = x;
uint16_t x = 0; e = nvs_get_u16(h, k->nvs, &x); v = x;
} else {
e = nvs_get_u32(h, k->nvs, &v);
}
if (read_status) *read_status = e;
if (e != ESP_OK) return cJSON_CreateNull();
if (k->scale > 0) return cJSON_CreateNumber((double)v / k->scale);
return cJSON_CreateNumber((double)v);
@@ -192,8 +197,8 @@ static uint32_t banked_number(const cfg_key_t *k, const cJSON *v)
static bool restore_key(nvs_handle_t h, const cfg_key_t *k, const cJSON *v)
{
if (cJSON_IsNull(v)) {
nvs_erase_key(h, k->nvs); /* absence is a state worth restoring */
return true;
esp_err_t e = nvs_erase_key(h, k->nvs);
return e == ESP_OK || e == ESP_ERR_NVS_NOT_FOUND;
}
char err[64];
return store_key(h, k, v, banked_number(k, v), err, sizeof(err));
@@ -212,63 +217,66 @@ static bool restore_bank(nvs_handle_t h, const cJSON *bank)
cJSON *it = NULL;
cJSON_ArrayForEach(it, bank) {
const cfg_key_t *k = config_find_key(it->string);
if (k && !restore_key(h, k, it)) {
ESP_LOGE(TAG, "could not restore %s", k->json);
if (!k || !restore_key(h, k, it)) {
ESP_LOGE(TAG, "could not restore banked key");
ok = false;
}
}
return ok;
}
/* Recovery metadata is discarded only after the previous state is durable. */
static bool clear_recovery_bank(void)
{
nvs_handle_t h;
if (nvs_open(TRIAL_NS, NVS_READWRITE, &h) != ESP_OK) return false;
bool ok = nvs_erase_all(h) == ESP_OK && nvs_commit(h) == ESP_OK;
nvs_close(h);
return ok;
}
static bool finish_recovery_bank(bool needs_trial)
{
if (!needs_trial) return clear_recovery_bank();
nvs_handle_t h;
if (nvs_open(TRIAL_NS, NVS_READWRITE, &h) != ESP_OK) return false;
bool ok = nvs_set_u8(h, "rollback", 0) == ESP_OK && nvs_commit(h) == ESP_OK;
nvs_close(h);
return ok;
}
/* ---------------------------------------------------------------- trial --- */
/** Apply the banked values and reboot. Runs from the esp_timer task. */
static void trial_revert(void *arg)
{
(void)arg;
ESP_LOGE(TAG, "config trial FAILED to associate -- reverting");
nvs_handle_t t, c;
if (nvs_open(TRIAL_NS, NVS_READWRITE, &t) != ESP_OK) {
/* Cannot read the bank, so cannot revert. Rebooting would spin
* forever; stay up so the node is at least diagnosable if it is
* somehow still reachable. */
ESP_LOGE(TAG, "trial namespace unreadable; cannot revert");
s_pending_reboot = true;
nvs_handle_t t;
if (nvs_open(TRIAL_NS, NVS_READONLY, &t) != ESP_OK) {
ESP_LOGE(TAG, "recovery bank unreadable; retaining write lock");
return;
}
size_t len = 0;
char *json = NULL;
if (nvs_get_str(t, "bank", NULL, &len) == ESP_OK && len > 1 &&
(json = malloc(len)) != NULL &&
nvs_get_str(t, "bank", json, &len) == ESP_OK &&
nvs_open(CFG_NS, NVS_READWRITE, &c) == ESP_OK) {
cJSON *root = cJSON_Parse(json);
if (root) {
cJSON *it = NULL;
cJSON_ArrayForEach(it, root) {
const cfg_key_t *k = config_find_key(it->string);
if (k) {
restore_key(c, k, it);
ESP_LOGW(TAG, "reverted %s", k->json);
}
}
cJSON_Delete(root);
}
nvs_commit(c);
bool loaded = nvs_get_str(t, "bank", NULL, &len) == ESP_OK && len > 1 &&
len <= BODY_MAX * 2 && (json = malloc(len)) != NULL &&
nvs_get_str(t, "bank", json, &len) == ESP_OK;
nvs_close(t);
cJSON *bank = loaded ? cJSON_Parse(json) : NULL;
free(json);
bool restored = false;
nvs_handle_t c;
if (cJSON_IsObject(bank) && nvs_open(CFG_NS, NVS_READWRITE, &c) == ESP_OK) {
restored = restore_bank(c, bank) && nvs_commit(c) == ESP_OK;
nvs_close(c);
}
free(json);
/* Clear the trial BEFORE rebooting: if the restored config is itself
* broken we must not loop reverting forever. One attempt, then the node
* comes up on the old values and stays there. */
nvs_erase_all(t);
nvs_commit(t);
nvs_close(t);
ESP_LOGE(TAG, "rebooting onto the previous configuration");
cJSON_Delete(bank);
if (!restored || !clear_recovery_bank()) {
ESP_LOGE(TAG, "recovery failed; keeping bank and write lock for retry");
return;
}
ESP_LOGW(TAG, "previous configuration restored; rebooting");
esp_restart();
}
@@ -278,10 +286,16 @@ void config_trial_boot_check(void)
if (nvs_open(TRIAL_NS, NVS_READONLY, &t) != ESP_OK) return;
uint8_t armed = 0;
uint8_t rollback = 0;
uint32_t deadline = TRIAL_DEFAULT;
nvs_get_u8(t, "armed", &armed);
nvs_get_u8(t, "rollback", &rollback);
nvs_get_u32(t, "deadline", &deadline);
nvs_close(t);
if (rollback) {
trial_revert(NULL);
return;
}
if (!armed) return;
s_trial_armed = true;
@@ -302,7 +316,11 @@ void config_trial_boot_check(void)
void config_trial_notify_connected(void)
{
if (!s_trial_armed) return;
if (!s_trial_armed || s_pending_reboot) return;
if (!clear_recovery_bank()) {
ESP_LOGE(TAG, "could not commit trial; preserving recovery state");
return;
}
s_trial_armed = false;
if (s_revert_timer) {
@@ -311,12 +329,6 @@ void config_trial_notify_connected(void)
s_revert_timer = NULL;
}
nvs_handle_t t;
if (nvs_open(TRIAL_NS, NVS_READWRITE, &t) == ESP_OK) {
nvs_erase_all(t);
nvs_commit(t);
nvs_close(t);
}
ESP_LOGI(TAG, "config trial COMMITTED -- association proved on new settings");
}
@@ -351,7 +363,7 @@ static esp_err_t config_get_handler(httpd_req_t *req)
cJSON *vals = cJSON_AddObjectToObject(root, "config");
if (nvs_open(CFG_NS, NVS_READONLY, &h) == ESP_OK) {
for (size_t i = 0; i < CONFIG_KEY_COUNT; i++) {
cJSON_AddItemToObject(vals, CONFIG_KEYS[i].json, read_key(h, &CONFIG_KEYS[i], true));
cJSON_AddItemToObject(vals, CONFIG_KEYS[i].json, read_key(h, &CONFIG_KEYS[i], true, NULL));
}
nvs_close(h);
}
@@ -503,7 +515,18 @@ static esp_err_t config_post_handler(httpd_req_t *req)
cJSON_ArrayForEach(it, root) {
if (config_is_meta(it->string)) continue;
const cfg_key_t *k = config_find_key(it->string);
cJSON_AddItemToObject(bank, k->json, read_key(ro, k, false));
esp_err_t read_status;
cJSON *value = read_key(ro, k, false, &read_status);
if (!value || (read_status != ESP_OK && read_status != ESP_ERR_NVS_NOT_FOUND) ||
!cJSON_AddItemToObject(bank, k->json, value)) {
cJSON_Delete(value);
nvs_close(ro);
cJSON_Delete(root);
cJSON_Delete(bank);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Could not snapshot the complete current config");
return ESP_FAIL;
}
}
nvs_close(ro);
}
@@ -514,14 +537,15 @@ static esp_err_t config_post_handler(httpd_req_t *req)
* leave the node on unproven settings with nothing to fall back to. Once
* the bank IS on flash the node can get back to the previous
* configuration even if everything after this point fails. */
if (needs_trial) {
{
char *bj = cJSON_PrintUnformatted(bank);
nvs_handle_t t;
bool ok = bj && nvs_open(TRIAL_NS, NVS_READWRITE, &t) == ESP_OK;
if (ok) {
ok = nvs_set_str(t, "bank", bj) == ESP_OK &&
nvs_set_u32(t, "deadline", deadline) == ESP_OK &&
nvs_set_u8(t, "armed", 1) == ESP_OK &&
nvs_set_u8(t, "rollback", 1) == ESP_OK &&
nvs_set_u8(t, "armed", needs_trial ? 1 : 0) == ESP_OK &&
nvs_commit(t) == ESP_OK;
nvs_close(t);
}
@@ -545,7 +569,7 @@ static esp_err_t config_post_handler(httpd_req_t *req)
* 2, and the recovery bank is already on flash. */
nvs_handle_t rw;
if (nvs_open(CFG_NS, NVS_READWRITE, &rw) != ESP_OK) {
s_pending_reboot = false;
s_pending_reboot = !clear_recovery_bank();
cJSON_Delete(root);
cJSON_Delete(bank);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "NVS unavailable");
@@ -568,6 +592,7 @@ static esp_err_t config_post_handler(httpd_req_t *req)
}
esp_err_t ce = err[0] ? ESP_FAIL : nvs_commit(rw);
nvs_close(rw);
if (ce == ESP_OK && !finish_recovery_bank(needs_trial)) ce = ESP_FAIL;
if (err[0] || ce != ESP_OK) {
/* A write failed part way through: the node is holding a mix of old
@@ -577,17 +602,14 @@ static esp_err_t config_post_handler(httpd_req_t *req)
nvs_handle_t r;
bool restored = false;
if (nvs_open(CFG_NS, NVS_READWRITE, &r) == ESP_OK) {
restored = restore_bank(r, bank);
restored = restore_bank(r, bank) && nvs_commit(r) == ESP_OK;
nvs_close(r);
}
if (needs_trial) {
nvs_handle_t t;
if (nvs_open(TRIAL_NS, NVS_READWRITE, &t) == ESP_OK) {
nvs_erase_all(t);
nvs_close(t);
}
}
s_pending_reboot = false;
if (restored) restored = clear_recovery_bank();
/* A failed restore must retain the persisted bank and write lock.
* The rollback marker makes the next boot retry recovery BEFORE WiFi,
* instead of promoting a partially written config on association. */
s_pending_reboot = !restored;
cJSON_Delete(root);
cJSON_Delete(bank);
if (!restored) {
@@ -655,12 +677,15 @@ static esp_err_t config_post_handler(httpd_req_t *req)
const esp_timer_create_args_t a = { .callback = reboot_soon, .name = "cfg_boot" };
esp_timer_handle_t th;
if (esp_timer_create(&a, &th) == ESP_OK) {
esp_timer_start_once(th, 3000000ULL);
if (esp_timer_start_once(th, 3000000ULL) != ESP_OK) {
esp_timer_delete(th);
ESP_LOGE(TAG, "reboot timer failed; retaining write lock until reset");
}
} else {
/* No reboot means no pending window. Leaving the lock set would
* refuse every later write for the life of the process. */
ESP_LOGE(TAG, "could not schedule the reboot; clearing the pending lock");
s_pending_reboot = false;
/* Keep the bank protected from a second request until reset. */
}
} else {
/* Applied immediately, nothing in flight. */
+15 -5
View File
@@ -38,6 +38,7 @@ CFLAGS = -fsanitize=fuzzer,address,undefined -g -O1 \
STUBS_SRC = stubs/esp_stubs.c
MAIN_DIR = ../main
CJSON_DIR ?= $(if $(wildcard $(IDF_PATH)/components/json/cJSON/cJSON.h),$(IDF_PATH)/components/json/cJSON,$(MAIN_DIR)/../managed_components/espressif__cjson/cJSON)
# GNU ld and Apple's ld spell dead section elimination differently.
# Keep the host sanitation test portable across the Linux CI runner and macOS.
@@ -165,6 +166,17 @@ test_config_rules: test_config_rules.c $(MAIN_DIR)/config_rules.c $(MAIN_DIR)/co
run_config_rules: test_config_rules
./test_config_rules
# --- Configuration API write-through rollback and boot recovery ---
# Compiles the production handler against deterministic, write-through NVS
# stubs and injects each failure between partial writes and restoration.
test_config_recovery: test_config_recovery.c $(MAIN_DIR)/config_api.c $(MAIN_DIR)/config_rules.c $(MAIN_DIR)/config_limits.h $(CJSON_DIR)/cJSON.h $(CJSON_DIR)/cJSON.c $(wildcard config_stubs/*)
cc -std=c99 -Wall -Wextra -Wno-unused-variable -Wno-unused-parameter -Werror \
-Iconfig_stubs -Istubs -I$(MAIN_DIR) -I$(CJSON_DIR) \
test_config_recovery.c $(MAIN_DIR)/config_rules.c $(CJSON_DIR)/cJSON.c -lm -o $@
run_config_recovery: test_config_recovery
./test_config_recovery
run_delivery_contract:
python3 test_delivery_contract.py
@@ -183,8 +195,8 @@ run_self_ping: test_self_ping
host_tests: run_self_ping run_edge_stbc run_adr110 run_vitals run_mmwave_detect run_thermal run_csi_sanitize \
run_c6_antenna run_serial_onboarding run_onboarding_identity run_onboarding_console run_delivery_contract run_ota_health \
run_config_rules
@echo "Host tests passed (firmware contracts, DSP, configuration, onboarding and OTA health)"
run_config_rules run_config_recovery
@echo "Host tests passed (firmware contracts, DSP, configuration recovery, onboarding and OTA health)"
# --- Serialize fuzzer ---
# Tests csi_serialize_frame() with random wifi_csi_info_t inputs.
@@ -222,7 +234,5 @@ run_all: run_serialize run_edge run_nvs
clean:
rm -f fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detect \
test_thermal test_csi_sanitize test_c6_antenna test_serial_onboarding \
test_ota_health test_onboarding_identity
rm -f test_edge_stbc test_self_ping
test_ota_health test_onboarding_identity test_config_rules
test_ota_health test_onboarding_identity test_config_rules test_edge_stbc test_self_ping test_config_recovery
rm -rf corpus_serialize/ corpus_edge/ corpus_nvs/
@@ -0,0 +1,58 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef int esp_err_t;
#define ESP_OK 0
#define ESP_FAIL -1
#define ESP_ERR_NO_MEM 0x101
#define ESP_ERR_NVS_NOT_FOUND 0x1102
#define ESP_LOGE(...) ((void)0)
#define ESP_LOGW(...) ((void)0)
#define ESP_LOGI(...) ((void)0)
typedef int nvs_handle_t;
#define NVS_READONLY 0
#define NVS_READWRITE 1
esp_err_t nvs_open(const char *, int, nvs_handle_t *);
void nvs_close(nvs_handle_t);
esp_err_t nvs_commit(nvs_handle_t);
esp_err_t nvs_get_str(nvs_handle_t, const char *, char *, size_t *);
esp_err_t nvs_get_u8(nvs_handle_t, const char *, uint8_t *);
esp_err_t nvs_get_u16(nvs_handle_t, const char *, uint16_t *);
esp_err_t nvs_get_u32(nvs_handle_t, const char *, uint32_t *);
esp_err_t nvs_set_str(nvs_handle_t, const char *, const char *);
esp_err_t nvs_set_u8(nvs_handle_t, const char *, uint8_t);
esp_err_t nvs_set_u16(nvs_handle_t, const char *, uint16_t);
esp_err_t nvs_set_u32(nvs_handle_t, const char *, uint32_t);
esp_err_t nvs_erase_key(nvs_handle_t, const char *);
esp_err_t nvs_erase_all(nvs_handle_t);
typedef struct { const char *body; int content_len, offset, status; } httpd_req_t;
typedef void *httpd_handle_t;
typedef struct { const char *uri; int method; esp_err_t (*handler)(httpd_req_t *); } httpd_uri_t;
#define HTTP_GET 0
#define HTTP_POST 1
#define HTTPD_400_BAD_REQUEST 400
#define HTTPD_403_FORBIDDEN 403
#define HTTPD_500_INTERNAL_SERVER_ERROR 500
static inline int httpd_req_recv(httpd_req_t *r, char *b, size_t n) {
if (n > (size_t)(r->content_len - r->offset)) n = r->content_len - r->offset;
memcpy(b, r->body + r->offset, n); r->offset += n; return (int)n;
}
static inline esp_err_t httpd_resp_send_err(httpd_req_t *r, int s, const char *m) { (void)m; r->status = s; return ESP_OK; }
static inline esp_err_t httpd_resp_sendstr(httpd_req_t *r, const char *s) { (void)s; if (!r->status) r->status = 200; return ESP_OK; }
static inline esp_err_t httpd_resp_set_type(httpd_req_t *r, const char *s) { (void)r; (void)s; return ESP_OK; }
static inline esp_err_t httpd_resp_set_status(httpd_req_t *r, const char *s) { r->status = atoi(s); return ESP_OK; }
static inline esp_err_t httpd_register_uri_handler(httpd_handle_t h, const httpd_uri_t *u) { (void)h; (void)u; return ESP_OK; }
typedef void *esp_timer_handle_t;
typedef struct { void (*callback)(void *); const char *name; } esp_timer_create_args_t;
esp_err_t esp_timer_create(const esp_timer_create_args_t *, esp_timer_handle_t *);
esp_err_t esp_timer_start_once(esp_timer_handle_t, uint64_t);
static inline esp_err_t esp_timer_stop(esp_timer_handle_t h) { (void)h; return ESP_OK; }
static inline esp_err_t esp_timer_delete(esp_timer_handle_t h) { (void)h; return ESP_OK; }
static inline int64_t esp_timer_get_time(void) { return 100000000; }
void esp_restart(void);
static inline esp_err_t esp_wifi_disconnect(void) { return ESP_OK; }
static inline const char *esp_err_to_name(esp_err_t e) { (void)e; return "synthetic"; }
@@ -0,0 +1,2 @@
#pragma once
#include "config_test_sdk.h"
@@ -0,0 +1,2 @@
#pragma once
#include "config_test_sdk.h"
@@ -0,0 +1,2 @@
#pragma once
#include "config_test_sdk.h"
@@ -0,0 +1,2 @@
#pragma once
#include "config_test_sdk.h"
@@ -0,0 +1,2 @@
#pragma once
#include "config_test_sdk.h"
@@ -0,0 +1,2 @@
#pragma once
#include "config_test_sdk.h"
@@ -0,0 +1,2 @@
#pragma once
#include "config_test_sdk.h"
@@ -0,0 +1,2 @@
#pragma once
#include "config_test_sdk.h"
@@ -0,0 +1,140 @@
/* SYNTHETIC: run the real HTTP handler and boot recovery against write-through
* NVS fault injection. No device, credentials or RF data are used. */
#include "config_test_sdk.h"
#include "cJSON.h"
#include <assert.h>
#include "../main/config_api.c"
nvs_config_t g_nvs_config;
static cJSON *db[3];
static int live_sets, fail_set_at, restarts;
static bool fail_restore, fail_erase, fail_bank_clear, fail_read, fail_restore_open;
static bool fail_timer, fail_commit;
static void put(nvs_handle_t h, const char *key, cJSON *value)
{
cJSON_DeleteItemFromObjectCaseSensitive(db[h], key);
assert(cJSON_AddItemToObject(db[h], key, value));
}
esp_err_t nvs_open(const char *name, int mode, nvs_handle_t *h)
{
*h = strcmp(name, "csi_cfg") == 0 ? 1 : 2;
if (*h == 1 && mode == NVS_READWRITE && fail_restore_open && live_sets >= 2) return ESP_FAIL;
return ESP_OK;
}
void nvs_close(nvs_handle_t h) { (void)h; }
esp_err_t nvs_commit(nvs_handle_t h) { return h == 1 && fail_commit ? ESP_FAIL : ESP_OK; }
esp_err_t nvs_get_str(nvs_handle_t h, const char *key, char *value, size_t *size)
{
if (h == 1 && fail_read) return ESP_FAIL;
cJSON *v = cJSON_GetObjectItemCaseSensitive(db[h], key);
if (!cJSON_IsString(v)) return ESP_ERR_NVS_NOT_FOUND;
size_t n = strlen(v->valuestring) + 1;
if (value) { if (*size < n) return ESP_FAIL; memcpy(value, v->valuestring, n); }
*size = n; return ESP_OK;
}
static esp_err_t get_num(nvs_handle_t h, const char *key, uint32_t *value)
{
cJSON *v = cJSON_GetObjectItemCaseSensitive(db[h], key);
if (!cJSON_IsNumber(v)) return ESP_ERR_NVS_NOT_FOUND;
*value = (uint32_t)v->valuedouble; return ESP_OK;
}
esp_err_t nvs_get_u8(nvs_handle_t h, const char *k, uint8_t *v) { uint32_t n=0; int e=get_num(h,k,&n); *v=(uint8_t)n; return e; }
esp_err_t nvs_get_u16(nvs_handle_t h, const char *k, uint16_t *v) { uint32_t n=0; int e=get_num(h,k,&n); *v=(uint16_t)n; return e; }
esp_err_t nvs_get_u32(nvs_handle_t h, const char *k, uint32_t *v) { return get_num(h,k,v); }
static esp_err_t set_value(nvs_handle_t h, const char *k, cJSON *v)
{
if (h == 1) {
live_sets++;
if ((fail_set_at && live_sets == fail_set_at) ||
(fail_restore && live_sets > fail_set_at)) { cJSON_Delete(v); return ESP_FAIL; }
}
put(h,k,v); return ESP_OK; /* setters immediately change persistent state */
}
esp_err_t nvs_set_str(nvs_handle_t h, const char *k, const char *v) { return set_value(h,k,cJSON_CreateString(v)); }
esp_err_t nvs_set_u8(nvs_handle_t h, const char *k, uint8_t v) { return set_value(h,k,cJSON_CreateNumber(v)); }
esp_err_t nvs_set_u16(nvs_handle_t h, const char *k, uint16_t v) { return set_value(h,k,cJSON_CreateNumber(v)); }
esp_err_t nvs_set_u32(nvs_handle_t h, const char *k, uint32_t v) { return set_value(h,k,cJSON_CreateNumber(v)); }
esp_err_t nvs_erase_key(nvs_handle_t h, const char *k)
{
if (fail_erase) return ESP_FAIL;
if (!cJSON_GetObjectItemCaseSensitive(db[h],k)) return ESP_ERR_NVS_NOT_FOUND;
cJSON_DeleteItemFromObjectCaseSensitive(db[h],k); return ESP_OK;
}
esp_err_t nvs_erase_all(nvs_handle_t h)
{
if (h == 2 && fail_bank_clear) return ESP_FAIL;
cJSON_Delete(db[h]); db[h]=cJSON_CreateObject(); return ESP_OK;
}
esp_err_t esp_timer_create(const esp_timer_create_args_t *a, esp_timer_handle_t *h) { (void)a; *h=(void *)1; return fail_timer ? ESP_FAIL : ESP_OK; }
esp_err_t esp_timer_start_once(esp_timer_handle_t h, uint64_t us) { (void)h; (void)us; return fail_timer ? ESP_FAIL : ESP_OK; }
void esp_restart(void) { restarts++; }
bool ota_auth_check(httpd_req_t *r) { (void)r; return true; }
void ota_update_log_httpd_stack(const char *s) { (void)s; }
static void reset(void)
{
for (int i=1;i<=2;i++) { cJSON_Delete(db[i]); db[i]=cJSON_CreateObject(); }
put(1,"ssid",cJSON_CreateString("old-network"));
put(1,"edge_tier",cJSON_CreateNumber(0));
live_sets=fail_set_at=restarts=0;
fail_restore=fail_erase=fail_bank_clear=fail_read=fail_restore_open=fail_timer=fail_commit=false;
s_pending_reboot=s_trial_armed=false; s_revert_timer=NULL;
}
static int request(const char *body)
{
httpd_req_t req={.body=body,.content_len=(int)strlen(body)};
config_post_handler(&req); return req.status;
}
static bool bank_present(void) { return cJSON_IsString(cJSON_GetObjectItem(db[2],"bank")); }
static void assert_old(void) { assert(strcmp(cJSON_GetObjectItem(db[1],"ssid")->valuestring,"old-network")==0); }
int main(void)
{
const char *change="{\"wifi_ssid\":\"new-network\",\"edge_tier\":1}";
reset();
assert(request("{\"wifi_ssid\":\"new-network\",\"edge_tier\":\"invalid\"}")==400);
assert_old(); assert(live_sets==0 && !bank_present());
reset(); fail_read=true;
assert(request(change)==500); assert(live_sets==0); assert_old();
reset();
assert(request("{\"led_mode\":1,\"reboot\":false}")==200);
assert(!s_pending_reboot && !bank_present() && g_nvs_config.led_mode==1);
reset(); fail_set_at=2;
assert(request(change)==500); assert_old();
assert(!s_pending_reboot && !bank_present());
for (int mode=0;mode<3;mode++) {
reset(); fail_set_at=2;
fail_restore=mode==0; fail_restore_open=mode==1; fail_bank_clear=mode==2;
assert(request(change)==500);
assert(bank_present() && s_pending_reboot);
int previous=live_sets;
assert(request("{\"led_mode\":1}")==409 && live_sets==previous);
config_trial_notify_connected(); assert(bank_present());
/* A reset must restore before association can promote mixed values. */
s_pending_reboot=s_trial_armed=false;
fail_restore=fail_restore_open=fail_bank_clear=false; fail_set_at=0;
config_trial_boot_check();
assert_old(); assert(restarts==1 && !bank_present());
}
reset(); fail_set_at=2; fail_restore=true;
assert(request(change)==500);
trial_revert(NULL); assert(bank_present() && restarts==0 && s_pending_reboot);
reset(); fail_erase=true;
cJSON *absent=cJSON_CreateNull();
assert(!restore_key(1,config_find_key("led_mode"),absent)); cJSON_Delete(absent);
reset(); fail_timer=true;
assert(request(change)==200); assert(bank_present() && s_pending_reboot);
assert(request("{\"led_mode\":1}")==409);
reset();
assert(request(change)==200); assert(bank_present() && s_pending_reboot);
config_trial_notify_connected(); assert(bank_present()); /* old connection */
s_pending_reboot=s_trial_armed=false;
config_trial_boot_check(); assert(s_trial_armed && bank_present());
config_trial_notify_connected(); assert(!s_trial_armed && !bank_present());
reset(); fail_commit=true;
assert(request(change)==500); assert(bank_present() && s_pending_reboot);
for (int i=1;i<=2;i++) cJSON_Delete(db[i]);
puts("PASS: production HTTP/NVS validation, rollback faults, write lock and boot recovery");
return 0;
}
+1 -1
View File
@@ -1 +1 @@
0.8.14
0.8.15