# ESP Config Manager

[中文]

## Introduction

Config Manager persists configuration for embedded devices with primary and backup slots, CRC checks, and default-value merging, so settings remain recoverable after power loss, interrupted writes, and structure changes. ESP Wi-Fi Service uses it to store Wi-Fi profiles. Other services that need persistent configuration can reuse it.

## Feature List

- Primary/backup dual-slot storage: each configuration group maintains two copies, `ESP_CONFIG_SLOT_PRIMARY` and `ESP_CONFIG_SLOT_BACKUP`

- Record header verification: `magic` / `schema_version` / `payload_len` / `crc32`; if any of these checks fails during loading, the slot is deemed invalid

- Automatic load fallback: if primary fails, backup is tried; if both fail, it falls back to default values

- Primary slot repair on sync: when backup takes effect, it is automatically copied back to primary, with no extra calls required

- Default value merging: a built-in “prefix overwrite” strategy is provided, and a custom `merge_fn` is also supported for version migration

- Pluggable storage backends: three built-in adapters for NVS, filesystem (SPIFFS/FATFS/LittleFS, etc.), and raw flash partitions are provided, and a custom `esp_config_storage_ops_t` can also be implemented

- Optional encryption hooks: `encrypt` / `decrypt` callbacks; data is stored in plaintext when not configured

## Technical Deep Dive

### Storage Structure and Verification

Each slot stores a binary blob consisting of a 16-byte private record header (`magic`, `schema_version`, `payload_len`, `crc32`) plus a variable-length payload, where `crc32` covers `schema_version + payload_len + payload`. During loading, if either the magic or the CRC check fails, the slot is considered invalid, triggering the next-level fallback.

### Load Fallback and Primary Slot Repair

`esp_config_manager_load()` tries the three sources, primary, backup, and default values, in sequence, and reports the actual source that was hit as well as whether a repair occurred through `esp_config_load_info_t`:

            flowchart TD
        start[load] --> primary{primary check}
        primary -- pass --> done[return primary data]
        primary -- fail --> backup{backup check}
        backup -- pass --> repair[copy backup to primary] --> done2[return backup data]
        backup -- fail --> defaults[merge defaults and write back to both slots] --> done3[return default values]

When both primary and backup are invalid, the component generates the runtime configuration from default values (optionally processed through `merge_fn`) and writes it back to both slots, so that the next load can hit primary directly.

### Default Value Merging

When `merge_fn` is not provided, the built-in strategy is “prefix overwrite”: the output buffer is first filled with `default_config`, and then overwritten with the first `min(loaded_len, default_size)` bytes of the loaded payload. This means that newly added trailing fields in the structure retain their default values when the old data is shorter than the new structure; however, if the old and new `sizeof` are the same, padding bytes in the old data will also overwrite the new fields as-is. It is recommended to use a `packed` layout for persisted structures, or to provide a custom `merge_fn` for explicit migration.

    typedef struct __attribute__((packed)) {
        uint32_t volume;
        uint8_t  mode;
        uint8_t  reserve[3];
    } app_cfg_t;

    static const app_cfg_t s_defaults = { .volume = 50, .mode = 1 };

    esp_config_storage_t storage = NULL;
    esp_config_storage_nvs_t nvs_ctx = {
        .nvs_namespace = "app_cfg",
        .key_primary   = "main",
        .key_backup    = "bak",
    };
    esp_config_storage_init_nvs(&nvs_ctx, &storage);

    esp_config_manager_cfg_t cfg = {
        .storage        = storage,
        .default_config = &s_defaults,
        .default_size   = sizeof(s_defaults),
        .schema_version = 1,
    };

    esp_config_manager_handle_t handle = NULL;
    esp_config_manager_init(&cfg, &handle);

    app_cfg_t runtime_cfg;
    esp_config_load_info_t info;
    esp_config_manager_load(handle, &runtime_cfg, &info);

    runtime_cfg.volume = (runtime_cfg.volume + 5) % 101;
    esp_config_manager_save(handle, &runtime_cfg, sizeof(runtime_cfg));

### Storage Backend Extension

The underlying persistence is described by `esp_config_storage_ops_t`, which requires implementing only the two mandatory callbacks `read` / `write` and one optional `erase`. The three built-in adapters for NVS, filesystem, and raw flash are created via `esp_config_storage_init_nvs()`, `esp_config_storage_init_fs()`, and `esp_config_storage_init_flash()` respectively; for a custom medium (external EEPROM, remote KV store, etc.), implement a set of `ops` and bind it via `esp_config_storage_init()`. The handle obtained from either approach is passed to the manager as `esp_config_manager_cfg_t.storage`.

Important

`esp_config_storage_init()` does not copy the passed-in `ctx`; the caller must ensure it remains valid until `esp_config_storage_deinit()` is called. The lifetime of the storage handle must also cover the Config Manager handle that uses it; the destruction order is to call `esp_config_manager_deinit()` first, then `esp_config_storage_deinit()`.

### Encryption Support

Inject `encrypt` / `decrypt` callbacks through `esp_config_crypto_ops_t`: when saving, the record header and payload are packed first and then encrypted before being written; when loading, decryption is performed first, followed by record header/CRC verification. Data is stored in plaintext when `crypto` is not configured; `crypto_extra_size` indicates the number of extra bytes the ciphertext occupies relative to the plaintext record, as determined by the encryption algorithm, and must be set to `0` when encryption is not used.

## Application Examples

- `examples/config_manager_example/main/config_manager_example.c` demonstrates multiple configuration groups, filesystem/flash backends, encryption hooks, and a self-test flow.

## FAQ

Q1: If both primary and backup are corrupted, will the configuration be lost?

No, it will not be silently lost. The component regenerates the configuration from default values and writes it back to both slots, so that subsequent loads will hit primary normally. The caller can check `esp_config_load_info_t::source` to determine whether this load took the default-value path, and decide accordingly whether additional prompts or re-guiding the user through configuration are needed.

## API Reference

### Header File

- components/esp_config_manager/include/esp_config_manager.h

### Functions

esp_err_t esp_config_manager_init(const esp_config_manager_cfg_t *cfg, esp_config_manager_handle_t *out_handle)

  Create manager and allocate internal buffers.

  Parameters:

    - cfg – [in] Configuration; storage and default_config must outlive handle

    - out_handle – [out] Receives new handle on success

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG If `cfg` or `out_handle` is NULL, or fields invalid

    - ESP_ERR_NO_MEM On allocation failure

void esp_config_manager_deinit(esp_config_manager_handle_t handle)

  Destroy manager and free buffers.

  Parameters:

    handle – [in] Handle from esp_config_manager_init(), or NULL (no-op)

esp_err_t esp_config_manager_load(esp_config_manager_handle_t handle, void *config_out, esp_config_load_info_t *info_out_opt)

  Load configuration: try primary, then backup, then defaults (with CRC)

          If both slots fail CRC, defaults are merged and written to primary and backup.
          If primary fails but backup succeeds, backup is used and copied back to primary before returning.

  Note

  Thread-safe with respect to other API on the same handle.

  Parameters:

    - handle – [in] Valid manager handle

    - config_out – [out] Output buffer, size must be default_size from init

    - info_out_opt – [out] Optional; if non-NULL, load source and repair flags

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG If `handle` or `config_out` is NULL

    - ESP_FAIL If persisting defaults after dual failure fails

    - Others From `merge_fn` or storage/crypto callbacks. If a slot holds a readable record but `merge_fn` fails, that error is returned (not masked by the defaults path). Only missing blobs (`ESP_ERR_NOT_FOUND`) and CRC/magic failures (`ESP_ERR_INVALID_CRC`) trigger backup / defaults fallback.

esp_err_t esp_config_manager_save(esp_config_manager_handle_t handle, const void *img, int len)

  Serialize an image, CRC, optional encrypt, write primary then backup.

  Parameters:

    - handle – [in] Valid manager handle

    - img – [in] Configuration image to persist

    - len – [in] Image length in bytes; must be > 0 and <= default_size from init

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG If `handle` or `img` is NULL, or `len` is invalid

    - ESP_FAIL If either slot write fails

    - Others From pack, crypto, or storage

### Structures

struct esp_config_crypto_ops

  Optional encryption hooks (identity pass-through is valid)

  Public Members

  esp_config_crypto_encrypt_fn encrypt

    Encrypt record before storage; NULL if unused

  esp_config_crypto_decrypt_fn decrypt

    Decrypt record after load; NULL if unused

  void *ctx

    User context for `encrypt` / `decrypt`

struct esp_config_manager_config

  Parameters to create a configuration manager.

  Note

  When `merge_fn` is NULL: output starts as `default_config`, then the first min(loaded_len, default_size) bytes are overwritten from storage. New trailing fields keep defaults only if the stored payload is shorter than the current struct. If old and new `sizeof` match, trailing padding in the old blob can overwrite new fields — use a packed layout or a custom `merge_fn`. Internal plaintext capacity is the record header plus `default_size`; the store buffer adds `crypto_extra_size` for ciphertext expansion. When `crypto` is NULL, `crypto_extra_size` must be `0`. Maximum `esp_config_manager_save()` length is `default_size`.

  Public Members

  esp_config_storage_t storage

    Storage backend; must outlive handle

  const void *default_config

    Default image; must outlive handle

  size_t default_size

    Bytes: `default_config` and runtime config

  uint32_t schema_version

    Stored in record; for `merge_fn` migration

  esp_config_merge_fn merge_fn

    NULL: built-in POD merge (prefix overlay)

  void *merge_ctx

    User context for `merge_fn`

  const esp_config_crypto_ops_t *crypto

    NULL: plaintext record except private metadata

  size_t crypto_extra_size

    Extra bytes crypto may add to the profile store

struct esp_config_load_info

  Extra information from esp_config_manager_load()

  Public Members

  esp_config_load_source_t source

    Source of the active configuration

  bool primary_repair_scheduled

    Backup used; primary repair completed synchronously

### Type Definitions

typedef struct esp_config_manager *esp_config_manager_handle_t

  Opaque handle to a configuration manager instance.

typedef esp_err_t (*esp_config_merge_fn)(void *user_ctx, const void *loaded, size_t loaded_len, const void *defaults, size_t default_size, void *out)

  Merge persisted payload with current defaults (e.g. new fields)

  Param user_ctx:

    [in] User context from esp_config_manager_cfg_t::merge_ctx

  Param loaded:

    [in] Payload from storage (may be shorter than `default_size`)

  Param loaded_len:

    [in] Length of `loaded` in bytes

  Param defaults:

    [in] Default struct/image to merge against

  Param default_size:

    [in] Size of `defaults` and `out` in bytes

  Param out:

    [out] Merged output buffer, capacity `default_size`

  Return:

    - ESP_OK On success

    - Others Application-defined merge failure

typedef esp_err_t (*esp_config_crypto_encrypt_fn)(void *ctx, const uint8_t *in, size_t in_len, uint8_t *out, size_t out_size, size_t *out_len)

  Encrypt plaintext before storing.

  Param ctx:

    [in] User context from esp_config_crypto_ops_t::ctx

  Param in:

    [in] Plaintext input

  Param in_len:

    [in] Length of `in`

  Param out:

    [out] Output buffer for ciphertext

  Param out_size:

    [in] Capacity of `out`

  Param out_len:

    [out] Actual ciphertext length written

  Return:

    - ESP_OK On success

    - ESP_ERR_NO_MEM If `out_size` is too small

    - Others Application-defined error

typedef esp_err_t (*esp_config_crypto_decrypt_fn)(void *ctx, const uint8_t *in, size_t in_len, uint8_t *out, size_t out_size, size_t *out_len)

  Decrypt ciphertext after loading.

  Param ctx:

    [in] User context from esp_config_crypto_ops_t::ctx

  Param in:

    [in] Ciphertext input

  Param in_len:

    [in] Length of `in`

  Param out:

    [out] Output buffer for plaintext

  Param out_size:

    [in] Capacity of `out`

  Param out_len:

    [out] Actual plaintext length written

  Return:

    - ESP_OK On success

    - ESP_ERR_NO_MEM If `out_size` is too small

    - Others Application-defined error

typedef struct esp_config_crypto_ops esp_config_crypto_ops_t

  Optional encryption hooks (identity pass-through is valid)

typedef struct esp_config_manager_config esp_config_manager_cfg_t

  Parameters to create a configuration manager.

  Note

  When `merge_fn` is NULL: output starts as `default_config`, then the first min(loaded_len, default_size) bytes are overwritten from storage. New trailing fields keep defaults only if the stored payload is shorter than the current struct. If old and new `sizeof` match, trailing padding in the old blob can overwrite new fields — use a packed layout or a custom `merge_fn`. Internal plaintext capacity is the record header plus `default_size`; the store buffer adds `crypto_extra_size` for ciphertext expansion. When `crypto` is NULL, `crypto_extra_size` must be `0`. Maximum `esp_config_manager_save()` length is `default_size`.

typedef struct esp_config_load_info esp_config_load_info_t

  Extra information from esp_config_manager_load()

### Enumerations

enum esp_config_load_source_t

  Which storage path supplied the configuration last loaded.

  Values:

  enumerator ESP_CONFIG_LOAD_SOURCE_NONE

    No successful load yet

  enumerator ESP_CONFIG_LOAD_SOURCE_PRIMARY

    Valid record from primary slot

  enumerator ESP_CONFIG_LOAD_SOURCE_BACKUP

    Primary failed; valid record from backup

  enumerator ESP_CONFIG_LOAD_SOURCE_DEFAULTS

    Both slots invalid; merged defaults (may be persisted)

### Header File

- components/esp_config_manager/include/esp_config_storage.h

### Functions

esp_err_t esp_config_storage_init(const esp_config_storage_ops_t *storage_ops, void *storage_ctx, esp_config_storage_t *out_handle)

  Initialize a storage backend.

  Parameters:

    - storage_ops – [in] Storage operations

    - storage_ctx – [in] User context for `storage_ops` callbacks; valid until esp_config_storage_deinit()

    - out_handle – [out] Receives new storage handle on success

esp_err_t esp_config_storage_init_nvs(const esp_config_storage_nvs_t *nvs_ctx, esp_config_storage_t *out_handle)

  Initialize a storage backend backed by NVS.

  Parameters:

    - nvs_ctx – [in] Namespace and blob keys; must remain valid until esp_config_storage_deinit()

    - out_handle – [out] Receives new storage handle on success

esp_err_t esp_config_storage_init_fs(const esp_config_storage_fs_t *fs_ctx, esp_config_storage_t *out_handle)

  Initialize a storage backend backed by VFS.

  Parameters:

    - fs_ctx – [in] Primary and backup paths; must remain valid until esp_config_storage_deinit()

    - out_handle – [out] Receives new storage handle on success

esp_err_t esp_config_storage_init_flash(const esp_config_storage_flash_t *flash_ctx, esp_config_storage_t *out_handle)

  Initialize a storage backend backed by raw flash partitions.

  Parameters:

    - flash_ctx – [in] Partition labels; must remain valid until esp_config_storage_deinit()

    - out_handle – [out] Receives new storage handle on success

void esp_config_storage_deinit(esp_config_storage_t handle)

  Destroy a storage backend.

  Parameters:

    handle – [in] Storage handle

esp_err_t esp_config_storage_read(esp_config_storage_t handle, esp_config_slot_t slot, uint8_t *buf, size_t *inout_len)

  Read a blob from the storage backend.

  Parameters:

    - handle – [in] Storage handle

    - slot – [in] Primary or backup slot

    - buf – [out] Destination buffer

    - inout_len – [inout] In: capacity of `buf`; out: bytes read

esp_err_t esp_config_storage_write(esp_config_storage_t handle, esp_config_slot_t slot, const uint8_t *buf, size_t len)

  Write a blob to the storage backend.

  Parameters:

    - handle – [in] Storage handle

    - slot – [in] Primary or backup slot

    - buf – [in] Data to store

    - len – [in] Length of `buf`

esp_err_t esp_config_storage_erase(esp_config_storage_t handle, esp_config_slot_t slot)

  Erase a slot from the storage backend.

  Parameters:

    - handle – [in] Storage handle

    - slot – [in] Primary or backup slot

### Structures

struct esp_config_storage_ops

  Low-level persistence: one primary slot and one backup slot.

          Implementations may map slots to NVS keys, files, flash partitions, etc.

  Public Members

  esp_err_t (*read)(void *ctx, esp_config_slot_t slot, uint8_t *buf, size_t *inout_len)

    Read stored blob for `slot` into `buf`.

    Param ctx:

      [in] User context (e.g. NVS or FS descriptor)

    Param slot:

      [in] Primary or backup slot

    Param buf:

      [out] Destination buffer

    Param inout_len:

      [inout] In: capacity of `buf`; out: bytes read

    Return:

      - ESP_OK On success

      - ESP_ERR_NOT_FOUND No blob stored (set *inout_len to 0)

      - ESP_ERR_NO_MEM `buf` too small

      - Others Implementation-defined

  esp_err_t (*write)(void *ctx, esp_config_slot_t slot, const uint8_t *buf, size_t len)

    Write blob for `slot` (overwrite)

    Param ctx:

      [in] User context

    Param slot:

      [in] Primary or backup slot

    Param buf:

      [in] Data to store

    Param len:

      [in] Length of `buf`

    Return:

      - ESP_OK On success

      - Others Implementation-defined

  esp_err_t (*erase)(void *ctx, esp_config_slot_t slot)

    Erase slot contents (optional)

    Note

    May be NULL if erase is not supported

    Param ctx:

      [in] User context

    Param slot:

      [in] Primary or backup slot

    Return:

      - ESP_OK On success

      - ESP_ERR_NOT_FOUND If already empty (implementation may map to ESP_OK)

      - Others Implementation-defined

struct esp_config_storage_nvs

  NVS adapter context (one namespace, two blob keys)

  Public Members

  const char *nvs_namespace

    NVS namespace name

  const char *key_primary

    Blob key for primary slot

  const char *key_backup

    Blob key for backup slot

struct esp_config_storage_fs

  File adapter: absolute paths after VFS is mounted (FATFS, SPIFFS, LittleFS, etc.)

          Writes use a sibling ".tmp" file then rename for basic atomicity on the same volume.

  Public Members

  const char *path_primary

    Full path to primary file

  const char *path_backup

    Full path to backup file

struct esp_config_storage_flash

  Raw flash adapter: two dedicated `data` partitions.

          Per slot on-disk layout: little-endian uint32 length, then blob bytes. Partition is erased on write.

  Public Members

  const char *label_primary

    Partition label for primary slot

  const char *label_backup

    Partition label for backup slot

### Type Definitions

typedef struct esp_config_storage *esp_config_storage_t

  Storage handle.

typedef struct esp_config_storage_ops esp_config_storage_ops_t

  Low-level persistence: one primary slot and one backup slot.

          Implementations may map slots to NVS keys, files, flash partitions, etc.

typedef struct esp_config_storage_nvs esp_config_storage_nvs_t

  NVS adapter context (one namespace, two blob keys)

typedef struct esp_config_storage_fs esp_config_storage_fs_t

  File adapter: absolute paths after VFS is mounted (FATFS, SPIFFS, LittleFS, etc.)

          Writes use a sibling ".tmp" file then rename for basic atomicity on the same volume.

typedef struct esp_config_storage_flash esp_config_storage_flash_t

  Raw flash adapter: two dedicated `data` partitions.

          Per slot on-disk layout: little-endian uint32 length, then blob bytes. Partition is erased on write.

### Enumerations

enum esp_config_slot_t

  Logical slot index for dual-copy persistence.

  Values:

  enumerator ESP_CONFIG_SLOT_PRIMARY

    Main configuration copy

  enumerator ESP_CONFIG_SLOT_BACKUP

    Redundant copy for CRC fallback

Provide feedback about this document