mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-22 06:32:35 -05:00
fix(firmware): harden config rollback and prepare 0.8.15 release
This commit is contained in:
1 parent
a9b46055db
commit
b3d5885c26
16 files changed
+371
-75
No files matched your search
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 @@
|
||||
0.8.14
|
||||
0.8.15
|
||||
Reference in new issue
Block a user