feat: implement NVS schema migration on boot
Implement versioned NVS key migration on ESP32-S3 firmware so OTA-updated firmware gracefully handles NVS written by older versions. - Add nvs_migration.c/h with migration framework - On boot, read schema_ver from NVS; initialize to 1 if missing - Run migrations sequentially if schema_ver < COMPILED_NVS_VERSION - Each migration commits after each write for durability - Log all migration steps to UART for debugging - Example migration v1→v2: rename 'ms_ip' to 'mothership_ip', add 'ntp_server' with default 'pool.ntp.org' - Migration failure leaves NVS in consistent state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
80bca356cd
commit
391ed884e4
4 changed files with 240 additions and 8 deletions
|
|
@ -5,6 +5,7 @@ idf_component_register(
|
|||
"csi.c"
|
||||
"ble.c"
|
||||
"provision.c"
|
||||
"nvs_migration.c"
|
||||
INCLUDE_DIRS "." "${CMAKE_BINARY_DIR}/spaxel-firmware/main"
|
||||
REQUIRES esp_wifi esp_netif nvs_flash mdns esp_http_client esp_timer bt driver log esp_http_server mbedtls esp_ota
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "csi.h"
|
||||
#include "ble.h"
|
||||
#include "provision.h"
|
||||
#include "nvs_migration.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
|
|
@ -70,14 +71,6 @@ static esp_err_t load_nvs_config(void) {
|
|||
return err;
|
||||
}
|
||||
|
||||
// Check schema version
|
||||
uint8_t schema_ver = 0;
|
||||
nvs_get_u8(nvs, NVS_KEY_SCHEMA_VER, &schema_ver);
|
||||
if (schema_ver < NVS_SCHEMA_VERSION) {
|
||||
ESP_LOGW(TAG, "NVS schema migration needed: %d -> %d", schema_ver, NVS_SCHEMA_VERSION);
|
||||
// Migration code would go here
|
||||
}
|
||||
|
||||
// Check provisioned flag
|
||||
g_state.provisioned = false;
|
||||
uint8_t provisioned = 0;
|
||||
|
|
@ -383,6 +376,13 @@ void app_main(void) {
|
|||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
// Run NVS schema migration if needed
|
||||
esp_err_t migration_err = nvs_migration_run();
|
||||
if (migration_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "NVS migration failed: %s", esp_err_to_name(migration_err));
|
||||
// Continue anyway - NVS should be in a consistent state
|
||||
}
|
||||
|
||||
// Get MAC address
|
||||
esp_read_mac(g_state.mac, ESP_MAC_WIFI_STA);
|
||||
char mac_str[18];
|
||||
|
|
|
|||
216
firmware/main/nvs_migration.c
Normal file
216
firmware/main/nvs_migration.c
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
#include "nvs_migration.h"
|
||||
#include "spaxel.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "nvs_migration";
|
||||
|
||||
// Forward declarations for migration functions
|
||||
static esp_err_t migrate_v1_to_v2(nvs_handle_t nvs);
|
||||
static esp_err_t migrate_v2_to_v3(nvs_handle_t nvs);
|
||||
|
||||
// Helper: Read a string key, return ESP_OK if exists
|
||||
static esp_err_t nvs_str_exists(nvs_handle_t nvs, const char *key) {
|
||||
size_t len = 0;
|
||||
esp_err_t err = nvs_get_str(nvs, key, NULL, &len);
|
||||
return (err == ESP_OK) ? ESP_OK : ESP_ERR_NVS_NOT_FOUND;
|
||||
}
|
||||
|
||||
// Helper: Rename a string key
|
||||
static esp_err_t nvs_rename_str(nvs_handle_t nvs, const char *old_key, const char *new_key) {
|
||||
char buf[128];
|
||||
size_t len = sizeof(buf);
|
||||
|
||||
esp_err_t err = nvs_get_str(nvs, old_key, buf, &len);
|
||||
if (err != ESP_OK) {
|
||||
return err; // Old key doesn't exist or other error
|
||||
}
|
||||
|
||||
err = nvs_set_str(nvs, new_key, buf);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set new key '%s': %s", new_key, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = nvs_commit(nvs);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to commit after rename to '%s': %s", new_key, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = nvs_erase_key(nvs, old_key);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) {
|
||||
ESP_LOGW(TAG, "Failed to erase old key '%s': %s", old_key, esp_err_to_name(err));
|
||||
// Non-fatal: new key exists, continue
|
||||
}
|
||||
|
||||
err = nvs_commit(nvs);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to commit after erasing '%s': %s", old_key, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Migration v1 → v2
|
||||
// Changes:
|
||||
// - Rename 'ms_ip' to 'mothership_ip'
|
||||
// - Add 'ntp_server' with default 'pool.ntp.org'
|
||||
static esp_err_t migrate_v1_to_v2(nvs_handle_t nvs) {
|
||||
ESP_LOGI(TAG, "Running migration v1→v2...");
|
||||
|
||||
// Rename ms_ip → mothership_ip
|
||||
esp_err_t err = nvs_rename_str(nvs, NVS_KEY_MS_IP, "mothership_ip");
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, " [v1→v2] Renamed 'ms_ip' → 'mothership_ip'");
|
||||
} else if (err == ESP_ERR_NVS_NOT_FOUND) {
|
||||
ESP_LOGD(TAG, " [v1→v2] 'ms_ip' not found, skipping rename");
|
||||
} else {
|
||||
ESP_LOGE(TAG, " [v1→v2] Failed to rename 'ms_ip': %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
// Add ntp_server with default if not exists
|
||||
if (nvs_str_exists(nvs, "ntp_server") != ESP_OK) {
|
||||
err = nvs_set_str(nvs, "ntp_server", "pool.ntp.org");
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, " [v1→v2] Failed to set 'ntp_server': %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
err = nvs_commit(nvs);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, " [v1→v2] Failed to commit 'ntp_server': %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
ESP_LOGI(TAG, " [v1→v2] Added 'ntp_server' = 'pool.ntp.org'");
|
||||
} else {
|
||||
ESP_LOGD(TAG, " [v1→v2] 'ntp_server' already exists, skipping");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Migration v1→v2 complete");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Migration v2 → v3 (placeholder for future migrations)
|
||||
// Add future migrations here
|
||||
static esp_err_t migrate_v2_to_v3(nvs_handle_t nvs) {
|
||||
ESP_LOGI(TAG, "Running migration v2→v3...");
|
||||
// No changes yet
|
||||
ESP_LOGI(TAG, "Migration v2→v3 complete");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Array of migration functions
|
||||
// Index i contains the migration from version i to i+1
|
||||
typedef esp_err_t (*migration_fn_t)(nvs_handle_t);
|
||||
|
||||
static const migration_fn_t migrations[] = {
|
||||
migrate_v1_to_v2, // Index 0: v1 → v2
|
||||
migrate_v2_to_v3, // Index 1: v2 → v3
|
||||
// Add new migrations here
|
||||
};
|
||||
|
||||
esp_err_t nvs_migration_run(void) {
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Starting NVS schema migration check...");
|
||||
|
||||
// Open NVS namespace
|
||||
err = nvs_open(SPAXEL_NAMESPACE, NVS_READWRITE, &nvs);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to open NVS namespace '%s': %s", SPAXEL_NAMESPACE, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
// Read current schema version
|
||||
uint8_t schema_ver = 0;
|
||||
err = nvs_get_u8(nvs, NVS_KEY_SCHEMA_VER, &schema_ver);
|
||||
if (err == ESP_ERR_NVS_NOT_FOUND) {
|
||||
// No schema version found - initialize to 1
|
||||
ESP_LOGI(TAG, "No schema_ver found, initializing to v1");
|
||||
schema_ver = 1;
|
||||
err = nvs_set_u8(nvs, NVS_KEY_SCHEMA_VER, schema_ver);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set initial schema_ver: %s", esp_err_to_name(err));
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
err = nvs_commit(nvs);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to commit initial schema_ver: %s", esp_err_to_name(err));
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
} else if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to read schema_ver: %s", esp_err_to_name(err));
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Current NVS schema version: %d, compiled version: %d", schema_ver, COMPILED_NVS_VERSION);
|
||||
|
||||
// Check if migration is needed
|
||||
if (schema_ver > COMPILED_NVS_VERSION) {
|
||||
ESP_LOGW(TAG, "NVS schema version %d is newer than compiled version %d. "
|
||||
"Firmware may be downgraded. Proceeding with caution.",
|
||||
schema_ver, COMPILED_NVS_VERSION);
|
||||
nvs_close(nvs);
|
||||
return ESP_OK; // Don't downgrade, just continue
|
||||
}
|
||||
|
||||
if (schema_ver == COMPILED_NVS_VERSION) {
|
||||
ESP_LOGI(TAG, "NVS schema is up to date");
|
||||
nvs_close(nvs);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Run migrations in order
|
||||
ESP_LOGI(TAG, "Migrating NVS schema from v%d to v%d...", schema_ver, COMPILED_NVS_VERSION);
|
||||
|
||||
for (uint8_t v = schema_ver; v < COMPILED_NVS_VERSION; v++) {
|
||||
// Index in migrations array is (v - 1) since array is 0-indexed
|
||||
// Example: v1→v2 is at index 0, v2→v3 is at index 1
|
||||
size_t migration_idx = v - 1;
|
||||
|
||||
if (migration_idx >= (sizeof(migrations) / sizeof(migrations[0]))) {
|
||||
ESP_LOGE(TAG, "Migration function for v%d→v%d not found!", v, v + 1);
|
||||
nvs_close(nvs);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Running migration v%d→v%d...", v, v + 1);
|
||||
err = migrations[migration_idx](nvs);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Migration v%d→v%d failed: %s", v, v + 1, esp_err_to_name(err));
|
||||
ESP_LOGE(TAG, "NVS left in consistent state at v%d. Please investigate.", v);
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
|
||||
// Update schema_ver after successful migration
|
||||
uint8_t new_ver = v + 1;
|
||||
err = nvs_set_u8(nvs, NVS_KEY_SCHEMA_VER, new_ver);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to update schema_ver to %d: %s", new_ver, esp_err_to_name(err));
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = nvs_commit(nvs);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to commit schema_ver update: %s", esp_err_to_name(err));
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Schema version updated to v%d", new_ver);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "NVS migration complete: v%d → v%d", schema_ver, COMPILED_NVS_VERSION);
|
||||
nvs_close(nvs);
|
||||
return ESP_OK;
|
||||
}
|
||||
15
firmware/main/nvs_migration.h
Normal file
15
firmware/main/nvs_migration.h
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
|
||||
// Current compiled NVS schema version
|
||||
// Increment this when adding new migrations
|
||||
#define COMPILED_NVS_VERSION 1
|
||||
|
||||
// Run NVS schema migration on boot
|
||||
// Opens 'spaxel' NVS namespace and reads schema_ver.
|
||||
// If missing, initializes schema_ver to 1.
|
||||
// If schema_ver < COMPILED_NVS_VERSION, runs migrations in order.
|
||||
// Returns ESP_OK on success, or error code on failure.
|
||||
esp_err_t nvs_migration_run(void);
|
||||
Loading…
Add table
Reference in a new issue