diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt index 7811d17..416919e 100644 --- a/firmware/main/CMakeLists.txt +++ b/firmware/main/CMakeLists.txt @@ -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 ) diff --git a/firmware/main/main.c b/firmware/main/main.c index 860489c..ab422db 100644 --- a/firmware/main/main.c +++ b/firmware/main/main.c @@ -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]; diff --git a/firmware/main/nvs_migration.c b/firmware/main/nvs_migration.c new file mode 100644 index 0000000..ef49f9a --- /dev/null +++ b/firmware/main/nvs_migration.c @@ -0,0 +1,216 @@ +#include "nvs_migration.h" +#include "spaxel.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "nvs.h" +#include + +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; +} diff --git a/firmware/main/nvs_migration.h b/firmware/main/nvs_migration.h new file mode 100644 index 0000000..bcb6bfb --- /dev/null +++ b/firmware/main/nvs_migration.h @@ -0,0 +1,15 @@ +#pragma once + +#include "esp_err.h" +#include + +// 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);