ESP Config Manager

[English]

简介

Config Manager 为嵌入式设备提供配置持久化:主备双槽存储、CRC 校验与默认值合并,使配置在掉电、写入中断与结构扩展后仍可恢复。ESP Wi-Fi Service 用它保存无线网络配置,其他需要持久化配置的服务也可以直接复用。

功能清单

  • 主/备双槽存储:每组配置维护 ESP_CONFIG_SLOT_PRIMARYESP_CONFIG_SLOT_BACKUP 两个副本

  • 记录头校验:magic / schema_version / payload_len / crc32,加载时任一项不通过即判定该槽位失效

  • 自动加载回退:primary 失败尝试 backup,两者都失败则回退到默认值

  • 主槽同步修复:backup 生效时自动回拷到 primary,无需额外调用

  • 默认值合并:内建“前缀覆盖”策略,也支持自定义 merge_fn 做版本迁移

  • 可插拔存储后端:内置 NVS、文件系统(SPIFFS/FATFS/LittleFS 等)、原始 flash 分区三种适配器,也可实现自定义 esp_config_storage_ops_t

  • 可选加密钩子:encrypt / decrypt 回调,未配置时按明文存储

技术拆解

存储结构与校验

每个槽位保存一段二进制 blob,由 16 字节私有记录头(magicschema_versionpayload_lencrc32)加上变长 payload 组成,crc32 覆盖 schema_version + payload_len + payload。加载时 magic 或 CRC 任一项不通过即认为该槽位无效,从而触发下一级回退。

加载回退与主槽修复

esp_config_manager_load() 依次尝试 primary、backup、默认值三级来源,并通过 esp_config_load_info_t 报告实际命中的来源与是否发生了修复:

        flowchart TD
    start[load] --> primary{primary 校验}
    primary -- 通过 --> done[返回 primary 数据]
    primary -- 失败 --> backup{backup 校验}
    backup -- 通过 --> repair[回拷 backup 到 primary] --> done2[返回 backup 数据]
    backup -- 失败 --> defaults[合并默认值并写回两槽] --> done3[返回默认值]
    

primary 和 backup 都失效时,组件用默认值生成运行时配置(可经过 merge_fn),并重新写回两个槽位,使下一次加载能直接从 primary 命中。

默认值合并

未提供 merge_fn 时,内建策略是“前缀覆盖”:输出缓冲区先填充为 default_config,再用已加载 payload 的前 min(loaded_len, default_size) 字节覆盖。这意味着结构体新增的尾部字段在旧数据比新结构短时能保持默认值;但如果新旧 sizeof 相同,旧数据里的 padding 也会原样覆盖新字段。建议持久化结构使用 packed 布局,或者提供自定义 merge_fn 做显式迁移。

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));

存储后端扩展

底层持久化由 esp_config_storage_ops_t 描述,只需实现 read / write 两个必选回调和一个可选的 erase。内置的 NVS、文件系统、原始 flash 三种适配器分别通过 esp_config_storage_init_nvs()esp_config_storage_init_fs()esp_config_storage_init_flash() 创建;自定义介质(外部 EEPROM、远端 KV 等)实现一套 ops 后通过 esp_config_storage_init() 绑定即可,两种方式得到的句柄都以 esp_config_manager_cfg_t.storage 传给管理器。

重要

esp_config_storage_init() 不会拷贝传入的 ctx,调用方须保证其在 esp_config_storage_deinit() 之前保持有效;存储句柄的生命周期也必须覆盖使用它的 Config Manager 句柄,销毁顺序为先 esp_config_manager_deinit()esp_config_storage_deinit()

加密支持

通过 esp_config_crypto_ops_t 注入 encrypt / decrypt 回调:保存时先打包记录头和 payload 再加密写入;加载时先解密再做记录头/CRC 校验。未配置 crypto 时按明文存储;crypto_extra_size 表示密文相对明文记录额外占用的字节数,由加密算法决定,未使用加密时须设为 0

应用示例

  • examples/config_manager_example/main/config_manager_example.c 演示多配置组、文件系统/flash 后端、加密钩子和自测流程。

FAQ

Q1:primary 和 backup 都损坏时,配置会丢失吗?

不会静默丢失。组件会用默认值重新生成配置并写回两个槽位,之后的加载会正常命中 primary;调用方可通过 esp_config_load_info_t::source 判断本次是否走了默认值路径,据此决定是否需要额外提示或重新引导用户配置。

API 参考

Header File

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.

参数:
  • cfg[in] Configuration; storage and default_config must outlive handle

  • out_handle[out] Receives new handle on success

返回:

  • 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.

参数:

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.

备注

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

参数:
  • 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

返回:

  • 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.

参数:
  • 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

返回:

  • 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.

备注

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.

备注

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

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.

参数:
  • 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.

参数:
  • 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.

参数:
  • 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.

参数:
  • 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.

参数:

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.

参数:
  • 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.

参数:
  • 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.

参数:
  • 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)

备注

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