spaxel/firmware/main/nvs_migration.c
jedarden 391ed884e4 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>
2026-04-07 10:27:38 -04:00

216 lines
7.3 KiB
C

#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;
}