Manage your ESP32 settings with ease!
- Description
- Usage
- License
Manage your ESP32 device settings effortlessly with the SettingsManagerESP32 library. Built on top of the ESP-IDF NVS API, it provides a clean and type-safe interface to store and retrieve your device settings in non-volatile storage.
Core features:
- Single place to define a group of settings using X-Macros.
- Each setting has a Key, a Hint (description text), and a Default Value. All metadata lives in flash - no heap usage.
- Access settings via a type-safe
enum classinstead of raw key strings. - Each
Settingsobject owns its own NVS namespace handle, allowing multiple independent groups or shared namespaces. - Per-setting and global change callbacks.
- Full autocompletion support in IDEs like VS Code.
Search for SettingsManagerESP32 in the Library Manager.
; Most recent changes
lib_deps =
https://github.com/alkonosst/SettingsManagerESP32.git
; Specific release (recommended for production)
lib_deps =
https://github.com/alkonosst/SettingsManagerESP32.git#v4.0.0#include "SettingsManagerESP32.h"All classes and types live in the NVS namespace. The main pieces are:
Classes:
NVS::Settings<T, ENUM, N>- typed container for a group of NVS settings under a single namespace.T- value type (bool,uint32_t,int32_t,float,double,NVS::Str,NVS::ByteStream).ENUM- enum class used to index settings.N- number of settings (useSETTINGS_COUNT(your_macro)).
NVS::ISettings- type-erased interface. Useful for storing heterogeneousSettingsobjects in an array.
Types:
| Type | Description |
|---|---|
NVS::Str |
Mutable string buffer for getValue(). Caller allocates the buffer. |
NVS::StrView |
Read-only string view. Used for default values and setValue(). Implicitly constructed from const char*. |
NVS::ByteStream |
Mutable byte buffer for getValue(). Caller allocates the buffer. |
NVS::ByteStreamView |
Read-only byte view. Used for default values and setValue(). |
NVS::ByteStream::Format |
Metadata enum: Hex, Base64, JSONObject, JSONArray. Not persisted in NVS. |
NVS::Type |
Identifies the value type of a Settings object: Bool, UInt32, Int32, Float, Double, String, ByteStream. |
NVS partition lifecycle functions:
NVS::init(); // Initialize the default NVS flash partition. Call once in setup() before any begin().
NVS::deinit(); // Deinitialize the partition.
NVS::erase(); // Erase all data in the partition (requires init() again afterwards).All three accept an optional const char* partition_name to target a custom partition.
Important
Call end() on every open Settings object before calling NVS::erase(). Erasing the
partition implicitly deinitializes it; any handle that is still open will be left in an invalid
state and subsequent reads/writes will fail silently.
The library uses X-Macros to keep all settings in one place.
#define FLOATS(X) \
X(SenThr, "Sensor Threshold", 3.14, false) \
X(AdcSlope, "ADC Slope", 1.2345, true) \
X(Offset, "ADC Offset", 0.0, true)Each row defines one setting:
| Field | Description |
|---|---|
| Name | Becomes the enum enumerator and the NVS key string. Max 15 characters, no whitespace. |
| Hint | Human-readable description. |
| Default value | Must match the type of the Settings object. |
| Formattable | true if formatAll() should reset this setting to its default. |
All settings in the same macro must be of the same type.
enum class MyFloats : uint8_t { FLOATS(SETTINGS_EXPAND_ENUM_CLASS) };
NVS::Settings<float, MyFloats, SETTINGS_COUNT(FLOATS)> my_floats("esp32", {FLOATS(SETTINGS_EXPAND_SETTINGS)});This expands to:
enum class MyFloats : uint8_t { SenThr, AdcSlope, Offset };
NVS::Settings<float, MyFloats, 3> my_floats("esp32", {
{"SenThr", "Sensor Threshold", 3.14, false},
{"AdcSlope", "ADC Slope", 1.2345, true},
{"Offset", "ADC Offset", 0.0, true},
});SETTINGS_CREATE_FLOATS(Floats, "esp32", FLOATS)| Parameter | Description |
|---|---|
Floats |
Name for the enum class and the st_Floats object. |
"esp32" |
NVS namespace name (max 15 characters). |
FLOATS |
X-macro with the settings list. |
This creates enum class Floats and NVS::Settings<float, Floats, N> st_Floats(...).
Before any read or write, initialize the NVS partition and open each Settings handle:
void setup() {
if (!NVS::init()) {
// Handle error
}
if (!st_Floats.begin()) {
// Handle error
}
}#include "SettingsManagerESP32.h"
#define UINT32S(X) \
X(UInt1, "uint32 1", 1, true) \
X(UInt2, "uint32 2", 2, true) \
X(UInt3, "uint32 3", 3, true)
#define FLOATS(X) \
X(Float1, "float 1", 1.1, true) \
X(Float2, "float 2", 2.2, true) \
X(Float3, "float 3", 3.3, true)
// Manual creation
enum class Floats : uint8_t { FLOATS(SETTINGS_EXPAND_ENUM_CLASS) };
NVS::Settings<float, Floats, SETTINGS_COUNT(FLOATS)> float_settings("esp32", {FLOATS(SETTINGS_EXPAND_SETTINGS)});
// Automatic creation
SETTINGS_CREATE_UINT32S(UInt32s, "esp32", UINT32S)
void setup() {
if (!NVS::init()) { /* handle error */ }
if (!float_settings.begin()) { /* handle error */ }
if (!st_UInt32s.begin()) { /* handle error */ }
// Get the key string: "UInt1"
const char* key = st_UInt32s.getKey(UInt32s::UInt1);
// Write a value
float_settings.setValue(Floats::Float1, 9.99f);
// Read a value
float val;
if (float_settings.getValue(Floats::Float1, val)) {
// val == 9.99f
}
}// Write
settings.setValue(MyEnum::Key, value);
// Read - returns true if the key exists in NVS, false if not yet saved
MyType val;
bool found = settings.getValue(MyEnum::Key, val);
// Read with automatic fallback to the default value
// Returns the NVS value if the key exists, or the default value if not saved or on error.
// In both cases `val` is updated to match the returned value.
MyType result = settings.getValueOrDefault(MyEnum::Key, val);
// Get default value
auto def = settings.getDefaultValue(MyEnum::Key);
// Get key/hint strings
const char* key = settings.getKey(MyEnum::Key);
const char* hint = settings.getHint(MyEnum::Key);Note
getValue() returns false when the key has not been saved to NVS yet. In that case, val is
left unchanged - no default value is written to it. Check the return value and fall back to
getDefaultValue() if needed, or use getValueOrDefault() to get the fallback automatically.
"Formatting" means resetting a setting's NVS value back to its default.
settings.format(MyEnum::Key); // Reset one setting (respects the formattable flag)
settings.format(MyEnum::Key, true); // Reset one setting (ignores the formattable flag)
settings.formatAll(); // Reset all formattable settings
settings.formatAll(true); // Reset all settings regardless of the formattable flagCallbacks fire when a value is written via setValue() or format().
// Per-setting callback
// callable_on_format: whether to fire when a format operation writes this setting
settings.setOnChangeCallback(MyEnum::Key,
[](const char* key, MyEnumType setting, MyWriteType value) {
// handle change
},
/*callable_on_format=*/false);
// Global callback (fires for every setting change in this object)
// The value is passed as const void* - cast to WriteType* before use
settings.setGlobalOnChangeCallback(
[](const char* key, NVS::Type type, size_t index, const void* value) {
// handle change
},
/*callable_on_format=*/true);For NVS::Str, the per-setting callback receives NVS::StrView. For NVS::ByteStream, it receives NVS::ByteStreamView.
NVS::ISettings* lets you store heterogeneous Settings objects in a plain array and operate on them without knowing the value type:
NVS::ISettings* all[] = {&st_Floats, &st_UInt32s, &st_Strings};
for (auto* s : all) {
s->begin();
}
// Access by index
const char* key = all[0]->getKey(0);
NVS::Type type = all[0]->getType();
// Read/write via void*
float f;
all[0]->getValuePtr(0, &f, sizeof(f));
// Read with fallback via void* - returns true if successful (NVS value or default written to buffer)
// Returns false on index out of bounds or buffer too small. The value is always in f on true.
bool ok = all[0]->getValuePtrOrDefault(0, &f, sizeof(f));
if (ok) {
// f holds the NVS value, or the default if the key was not found
}
float new_val = 1.23f;
all[0]->setValuePtr(0, &new_val);// Boolean
#define BOOLS(X) \
X(Bool1, "boolean 1", false, true) \
X(Bool2, "boolean 2", true, true)
SETTINGS_CREATE_BOOLS(Bools, "esp32", BOOLS)
// Unsigned 32-bit integer
#define UINT32S(X) \
X(UInt1, "uint32 1", 1, true) \
X(UInt2, "uint32 2", 2, true)
SETTINGS_CREATE_UINT32S(UInt32s, "esp32", UINT32S)
// Signed 32-bit integer
#define INT32S(X) \
X(Int1, "int32 1", -1, true) \
X(Int2, "int32 2", -2, true)
SETTINGS_CREATE_INT32S(Int32s, "esp32", INT32S)
// Float
#define FLOATS(X) \
X(Float1, "float 1", 1.1, true) \
X(Float2, "float 2", 2.2, true)
SETTINGS_CREATE_FLOATS(Floats, "esp32", FLOATS)
// Double
#define DOUBLES(X) \
X(Double1, "double 1", 1.123456, true) \
X(Double2, "double 2", 2.123456, true)
SETTINGS_CREATE_DOUBLES(Doubles, "esp32", DOUBLES)
// String - default values are StrView, constructed from a string literal
#define STRINGS(X) \
X(Str1, "string 1", "default 1", true) \
X(Str2, "string 2", "default 2", true)
SETTINGS_CREATE_STRINGS(Strings, "esp32", STRINGS)
// ByteStream - default values are ByteStreamView; data must remain valid for the object's lifetime
const uint8_t bs1_data[] = {0xDE, 0xAD, 0xBE, 0xEF};
const NVS::ByteStreamView bs1_def = {bs1_data, sizeof(bs1_data), NVS::ByteStream::Format::Hex};
const uint8_t bs2_data[] = {0x01, 0x02, 0x03, 0x04};
const NVS::ByteStreamView bs2_def = {bs2_data, sizeof(bs2_data), NVS::ByteStream::Format::Hex};
#define BYTESTREAMS(X) \
X(BS1, "byte stream 1", bs1_def, true) \
X(BS2, "byte stream 2", bs2_def, true)
SETTINGS_CREATE_BYTE_STREAMS(ByteStreams, "esp32", BYTESTREAMS)All utility functions are in the NVS namespace.
// Get NVS storage statistics for the default (or a custom) partition
nvs_stats_t stats;
if (NVS::getStats(stats)) {
Serial.printf("Used entries : %u\n", stats.used_entries);
Serial.printf("Free entries : %u\n", stats.free_entries);
Serial.printf("Available entries: %u\n", stats.available_entries);
Serial.printf("Total entries : %u\n", stats.total_entries);
Serial.printf("Namespaces : %u\n", stats.namespace_count);
}
// Convert a Type enum to a string ("Bool", "Float", "String", etc.)
const char* NVS::typeToStr(NVS::Type t);
// Convert a ByteStream::Format enum to a string ("Hex", "Base64", etc.)
const char* NVS::formatToStr(NVS::ByteStream::Format f);
// Calculate the buffer size needed to hold the hex string for byte_count bytes
// Without spaces: "DEADBEEF\0" -> hexStrSize(4) = 9
// With spaces: "DE AD BE EF\0" -> hexStrSize(4, true) = 12
constexpr size_t NVS::hexStrSize(size_t byte_count, bool with_spaces = false);
// Encode a ByteStreamView to a hex string
// buf must hold at least hexStrSize(bs.size, with_spaces) bytes
bool NVS::fromHexToStr(NVS::ByteStreamView bs, char* buf, size_t buf_size, bool with_spaces = false);
// Decode a hex string into a ByteStream buffer; out.size is updated on success
bool NVS::fromStrToHex(const char* hex, NVS::ByteStream& out, bool with_spaces = false);Example:
uint8_t buf[32];
NVS::ByteStream value{buf, sizeof(buf)};
bytestreams.getValue(ByteStreams::BS1, value);
char hex_compact[NVS::hexStrSize(32)];
char hex_spaced[NVS::hexStrSize(32, true)];
NVS::fromHexToStr(value, hex_compact, sizeof(hex_compact)); // "DEADBEEF"
NVS::fromHexToStr(value, hex_spaced, sizeof(hex_spaced), true); // "DE AD BE EF"
// Decode back
NVS::ByteStream decoded{buf, sizeof(buf)};
NVS::fromStrToHex("CAFEBABE", decoded);
NVS::fromStrToHex("CA FE BA BE", decoded, true);NVS::erase() erases the entire partition and implicitly deinitializes it. AnySettings handle that is still open at that point is left dangling: subsequent reads and writes on it will fail or produce undefined behaviour.
The correct sequence is:
// 1. Close all open handles
st_Floats.end();
st_UInt32s.end();
// 2. Erase
NVS::erase();
// 3. Re-initialize and re-open
NVS::init();
st_Floats.begin();
st_UInt32s.begin();NVS::Str and NVS::ByteStream require the caller to provide a buffer before calling getValue(). The library writes directly into that buffer - no heap allocation, no shared internal buffer, and no mutex needed.
Strings:
// Default values use StrView - implicitly constructed from const char*
// No extra declaration needed in the macro
// Reading a string value
char buf[64];
NVS::Str out{buf, sizeof(buf)};
if (strings.getValue(Strings::Str1, out)) {
Serial.println(out.data);
}
// Writing a string value (StrView is constructed implicitly)
strings.setValue(Strings::Str1, "new value");
// Getting the default value
NVS::StrView def = strings.getDefaultValue(Strings::Str1);
Serial.println(def.data);ByteStreams:
// Default values - must declare ByteStreamView (data pointer must outlive the Settings object)
const uint8_t my_data[] = {0xDE, 0xAD, 0xBE, 0xEF};
const NVS::ByteStreamView my_def = {my_data, sizeof(my_data), NVS::ByteStream::Format::Hex};
// Reading a ByteStream value
uint8_t buf[32];
NVS::ByteStream out{buf, sizeof(buf)};
if (bytestreams.getValue(ByteStreams::BS1, out)) {
// out.data contains the bytes, out.size is the number of bytes read
}
// Writing a ByteStream value (ByteStreamView)
const uint8_t new_data[] = {0xCA, 0xFE, 0xBA, 0xBE};
const NVS::ByteStreamView new_value = {new_data, sizeof(new_data), NVS::ByteStream::Format::Hex};
bytestreams.setValue(ByteStreams::BS1, new_value);
// Or use the implicit conversion from ByteStream to ByteStreamView
NVS::ByteStream writable{buf, sizeof(buf)};
// ... fill buf ...
bytestreams.setValue(ByteStreams::BS1, writable); // implicit conversionNote
The ByteStream::Format field is metadata only - it is not persisted in NVS. Use it as a
hint to know how to interpret the raw bytes when displaying or transmitting them.
The previous v3 release can be found at v3.1.0.
v4 is a full rewrite with several breaking changes:
| Area | v3 | v4 |
|---|---|---|
| NVS backend | Arduino Preferences |
ESP-IDF nvs.h |
| Global NVS object | Preferences nvs (global) |
NVS::init() / NVS::deinit() |
| Handle lifecycle | nvs.begin("ns") (global) |
settings.begin() per object |
| Namespace | Shared single namespace | Each Settings object owns its namespace |
SETTINGS_CREATE_XXX params |
(name, macro) |
(name, ns, macro) |
| String type | const char* |
NVS::Str (read) / NVS::StrView (write/default) |
| ByteStream default | const NVS::ByteStream |
const NVS::ByteStreamView |
getValue() on miss |
Wrote default to out-param | Leaves out-param unchanged, returns false |
| Mutex for strings | giveMutex() required |
Not needed - caller provides the buffer |
| Shared internal buffer | Yes (strings/bytestreams) | No - caller-owned buffers throughout |
Minimal migration steps:
- Replace
nvs.begin("ns")withNVS::init()+settings.begin(). - Add the namespace string as the second parameter to all
SETTINGS_CREATE_XXXcalls. - Change string default values from
"str"toNVS::StrView{"str"}(or keep the string literal - it converts implicitly). - Change ByteStream default values from
const NVS::ByteStream{...}toconst NVS::ByteStreamView{...}. - Change string read variables from
const char*toNVS::Str{buf, sizeof(buf)}with a caller-owned buffer. - Change bytestream read variables to
NVS::ByteStream{buf, sizeof(buf)}with a caller-owned buffer. - Remove all
giveMutex()calls. - If you relied on
getValue()fillingoutwith the default on a miss, add an explicit fallback.
This project is licensed under the MIT License - see the LICENSE file for details.


