MycilaConfig
A simple, efficient configuration library for ESP32 (Arduino framework) with pluggable storage backends (NVS included). Supports native types (bool, integers, floats, strings) with type safety via std::variant. Provides defaults, caching, generic typed API, validators, change/restore callbacks, backup/restore, and optional JSON export with password masking.
Table of Contents
- Features
- Installation
- Quick Start
- Migrating from v10 to v11
- Storage Backends
- Configuration Migration
- API Reference
- JSON Export and Password Masking
- Backup and Restore Example
- Configuration Defines
- Key Naming Conventions
- Memory Optimization
- Examples
- Custom Storage Backend
- License
Features
- πΎ Multiple storage backends: NVS (default) and FileSystem (LittleFS/SPIFFS) with pluggable architecture
- π― Default values with efficient memory management (flash strings stored as pointers)
- π’ Native type support: bool, int8/16/32/64, uint8/16/32/64, int, unsigned int, float, double (optional), and strings via
std::variant - β‘ Generic typed API:
get<T>()andset<T>()with compile-time type safety - π Change listener callback fired when values change with variant values
- π Restore callback fired after bulk configuration restore
- β Validators can be set globally, per-key, or during key configuration (receive variant values)
- πΏ Backup and restore from key=value text format or
std::map<const char*, Value> - π Migration utilities for moving configuration between storage backends
- π Optional JSON export with ArduinoJson integration (
toJson()) - native types exported directly - π Password masking for keys ending with
_pwdin JSON output - ποΈ Smart restore order: non-
_enablekeys applied first, then_enablekeys last (useful for feature toggles) - π Key helpers:
isPasswordKey(),isEnableKey()detect special suffixes - π NVS constraint enforcement: asserts key names β€ 15 characters
- π§ Memory tracking:
heapUsage()method reports memory consumption
Installation
PlatformIO
Add to your platformio.ini:
lib_deps =
mathieucarbou/MycilaConfig
Optional: JSON Support
To enable toJson() method with ArduinoJson:
build_flags =
-D MYCILA_JSON_SUPPORT
lib_deps =
mathieucarbou/MycilaConfig
bblanchon/ArduinoJson
Quick Start
#include <MycilaConfig.h>
#include <MycilaConfigStorageNVS.h>
Mycila::config::NVS storage;
Mycila::config::Config config(storage);
void setup() {
Serial.begin(115200);
// Declare configuration keys with optional default values
// Key names must be β€ 15 characters
config.configure("debug_enable", false);
config.configure("wifi_ssid", "MyNetwork");
config.configure("wifi_pwd", "");
config.configure("port", 80);
config.configure("timeout", 30.0f);
config.begin("MYAPP", true); // Preload all values
// Use typed getters/setters
bool debug = config.get<bool>("debug_enable");
int port = config.get<int>("port");
float timeout = config.get<float>("timeout");
const char* ssid = config.getString("wifi_ssid");
}
Migrating from v10 to v11
Version 11 introduces a major refactoring with:
- New namespace structure:
Mycila::config:: - Native type support with
std::variant - Generic typed API:
get<T>()andset<T>() - Callbacks now receive
std::optional<Value>instead of string values
To ease migration from v10, a deprecated compatibility wrapper is provided that maintains the v10 string-based API:
#include <MycilaConfig.h>
#include <MycilaConfigV10.h>
#include <MycilaConfigStorageNVS.h>
Mycila::config::NVS storage;
Mycila::config::Config configNew(storage);
Mycila::config::ConfigV10 config(configNew);
void setup() {
// Use the old v10 API - all methods still work
config.configure("debug_enable", "false");
config.configure("port", "80");
config.begin("MYAPP");
// Old string-based API
const char* port = config.getString("port");
bool debug = config.getBool("debug_enable");
int portNum = config.getInt("port");
// Old string-based callbacks
config.listen([](const char* key, const char* newValue) {
Serial.printf("Changed: %s = %s\n", key, newValue);
});
// Old validators
config.setValidator("port", [](const char* key, const char* value) {
int p = atoi(value);
return p > 0 && p < 65536;
});
config.setString("port", "8080");
}
Migration path:
- Immediate compatibility: Include
MycilaConfigV10.hand useConfigV10wrapper - no code changes needed - Gradual migration: Start using new typed API alongside deprecated API
- Full migration: Remove deprecated wrapper and update to new API
Note: The deprecated wrapper will be removed in a future major version. Plan to migrate to the new typed API.
Storage Backends
MycilaConfig supports multiple storage backends through a pluggable architecture.
NVS Storage (Recommended)
The default storage backend uses ESP32βs Non-Volatile Storage (NVS):
#include <MycilaConfig.h>
#include <MycilaConfigStorageNVS.h>
Mycila::config::NVS storage;
Mycila::config::Config config(storage);
void setup() {
config.begin("MYAPP"); // NVS namespace
}
Advantages:
- β Compact storage (no per-key overhead)
- β Designed for frequent writes with built-in wear leveling
- β Efficient for many keys (100+)
- β Type-aware storage (native int32, uint32, etc.)
- β Fast access with minimal overhead
Limitations:
- β οΈ Key names limited to 15 characters (NVS constraint)
FileSystem Storage
Store configuration in LittleFS or SPIFFS:
#include <LittleFS.h>
#include <MycilaConfig.h>
#include <MycilaConfigStorageFS.h>
Mycila::config::FileSystem storage;
Mycila::config::Config config(storage);
void setup() {
LittleFS.begin(true);
config.begin("MYAPP", LittleFS, "/config"); // namespace, filesystem, root path
}
Advantages:
- β Human-readable files (one file per key)
- β No key length restrictions
- β
Easy to inspect/debug (files in
/config/MYAPP/) - β Compatible with LittleFS, SPIFFS, SD, etc.
Limitations:
- β οΈ Each key creates a separate file = 4KB block with LittleFS
- β οΈ Not suitable for many keys (flash wear, storage waste)
- β οΈ All values stored as strings (parsed on load)
- β οΈ Slower than NVS for frequent updates
Recommended use case: Small configurations (< 20 keys), debugging, or when human-readable files are required.
File structure:
/config/MYAPP/
βββ wifi_ssid.txt (contains: "MyNetwork")
βββ port.txt (contains: "8080")
βββ debug_enable.txt (contains: "true")
Storage overhead example:
config.configure("key1", "value1"); // Creates /config/MYAPP/key1.txt (4KB with LittleFS)
config.configure("key2", 42); // Creates /config/MYAPP/key2.txt (4KB with LittleFS)
// Total: 8KB for 2 small values!
See also: examples/FS/FS.ino for a complete FileSystem storage example.
Configuration Migration
The Migration class helps you move configuration between storage backends (e.g., from NVS to FileSystem or vice versa).
Basic Migration
#include <LittleFS.h>
#include <MycilaConfig.h>
#include <MycilaConfigMigration.h>
#include <MycilaConfigStorageFS.h>
#include <MycilaConfigStorageNVS.h>
Mycila::config::NVS nvsStorage;
Mycila::config::FileSystem fsStorage;
Mycila::config::Config nvsConfig(nvsStorage);
Mycila::config::Config fsConfig(fsStorage);
void setup() {
LittleFS.begin(true);
// Initialize both configs
nvsConfig.begin("MYAPP");
fsConfig.begin("MYAPP", LittleFS, "/config");
// Configure keys on both (must match!)
nvsConfig.configure("wifi_ssid", "");
nvsConfig.configure("port", 80);
fsConfig.configure("wifi_ssid", "");
fsConfig.configure("port", 80);
// Migrate from NVS to FileSystem
Mycila::config::Migration migration;
if (migration.migrate(nvsConfig, fsConfig)) {
Serial.println("Migration successful!");
// Optional: Clear source storage after successful migration
nvsConfig.clear();
} else {
Serial.println("Migration failed!");
}
}
Migration with Callback
Monitor migration progress:
Mycila::config::Migration migration;
migration.listen([](const char* key) {
Serial.printf("Migrating key: %s\n", key);
});
if (migration.migrate(nvsConfig, fsConfig)) {
Serial.println("All keys migrated successfully");
}
See also: examples/Migration/Migration.ino for a complete migration example.
API Reference
Class: Mycila::config::Config
Constructor
-
Config(Storage& storage)
Create a Config instance with the specified storage backend. The storage reference must remain valid for the lifetime of the Config object.Example:
Mycila::config::NVS storage; Mycila::config::Config config(storage);
Setup and Storage
-
bool begin(const char* name = "CONFIG", bool preload = false)(NVS storage)
bool begin(const char* name, FS& fs, const char* root = "/config", bool preload = false)(FileSystem storage)
Initialize the configuration system. Returns true on success.Parameters:
name: Namespace (NVS) or subdirectory name (FileSystem)fs: Filesystem object (LittleFS, SPIFFS, SD, etc.) - FileSystem storage onlyroot: Root directory path - FileSystem storage onlypreload: Load all values into cache immediately
Preloading:
preload = false(default): Values loaded on-demand (lazy loading)preload = true: All stored values loaded into cache at initialization
Examples:
// NVS storage config.begin("CONFIG"); config.begin("CONFIG", true); // with preload // FileSystem storage config.begin("MYAPP", LittleFS, "/config"); config.begin("MYAPP", LittleFS, "/config", true); // with preload -
template <typename T> bool configure(const char* key, T defaultValue, ValidatorCallback validator = nullptr)
Register a configuration key with a typed default value and optional validator. Returns true on success.Supported types for
T:bool- Boolean valuesint8_t,uint8_t,int16_t,uint16_t,int32_t,uint32_t- Fixed-width integersint,unsigned int- Standard integersint64_t,uint64_t- 64-bit integers (ifMYCILA_CONFIG_USE_LONG_LONGenabled)float- Single precision floating pointdouble- Double precision (ifMYCILA_CONFIG_USE_DOUBLEenabled)const char*- C-strings (stored as pointer if in flash/ROM, zero copy)Mycila::config::Str- String wrapper with heap/flash detectionMycila::config::Value- Variant type directly
Examples:
config.configure("enabled", false); config.configure("port", 8080); config.configure("threshold", 25.5f); config.configure("name", "Device"); config.configure("count", static_cast<uint32_t>(1000)); // With validator config.configure("port", 80, [](const char* key, const Mycila::config::Value& value) { int port = value.as<int>(); return port > 0 && port < 65536; }); -
bool configured(const char* key) const
Check if a configuration key has been registered. -
bool stored(const char* key) const
Check if a configuration key is currently stored in NVS (not just using default). -
void clear()
Clear all persisted settings from NVS and cache. -
size_t heapUsage() const
Returns the total heap memory consumed by the config system, including:- Vector storage for Key objects (capacity Γ sizeof(Key))
- Heap usage from default values embedded in Key objects
- Map structure overhead (red-black tree nodes) for cache and validators
- Str object structures in maps
- Heap-allocated string content (flash strings contribute 0 bytes)
Reading Values
-
template <typename T = Value> const T& get(const char* key) const
Get the typed value from configuration. Returns a reference to the value with the specified type. Throwsstd::runtime_errorif the key doesnβt exist or type doesnβt match.Supported types:
bool,int8_t,uint8_t,int16_t,uint16_t,int32_t,uint32_t,int,unsigned intint64_t,uint64_t(ifMYCILA_CONFIG_USE_LONG_LONGenabled)float,double(ifMYCILA_CONFIG_USE_DOUBLEenabled)Mycila::config::Str- Returns string wrapper
Example:
bool enabled = config.get<bool>("debug_enable"); int port = config.get<int>("port"); float temp = config.get<float>("temperature"); uint32_t count = config.get<uint32_t>("counter"); -
const char* getString(const char* key) const
Get the value as a C-string. Convenience wrapper aroundget<Str>(key).c_str().Example:
const char* ssid = config.getString("wifi_ssid"); -
bool isEmpty(const char* key) const
Check if the string value is empty. bool isEqual(const char* key, const std::string& value) constbool isEqual(const char* key, const char* value) const
Compare the stored string value with the provided value.
Writing Values
-
template <typename T = Value> const Result set(const char* key, T value, bool fireChangeCallback = true)
Set a typed configuration value. Returns aResultobject that converts tobool(true = operation successful). The type must match the type used duringconfigure().Supported types: Same as
get<T>()Example:
config.set<bool>("debug_enable", true); config.set<int>("port", 8080); config.set<float>("threshold", 25.5f); config.set<uint32_t>("counter", 1000); Result setString(const char* key, const std::string& value, bool fireChangeCallback = true)-
Result setString(const char* key, const char* value, bool fireChangeCallback = true)
Set a string configuration value. Convenience wrapper aroundset<Str>().Example:
config.setString("wifi_ssid", "MyNetwork"); -
bool set(std::map<const char*, Value> settings, bool fireChangeCallback = true)
Set multiple values at once from a map of variant values. Returns true if any value was changed. Pass map by value or usestd::move()to avoid copying.Example:
std::map<const char*, Mycila::config::Value> batch; batch.emplace("enabled", true); batch.emplace("port", 8080); batch.emplace("name", Mycila::config::Str("Device")); config.set(std::move(batch)); Result unset(const char* key, bool fireChangeCallback = true)
Remove the persisted value (revert to default). Returns aResultindicating success or error.
Result and Status Enum
set() and unset() return a Result object that:
- Converts to
bool(true if operation was successful) - Can be cast to
Mycila::config::Statusenum for detailed status - Has
isStorageUpdated()method (true only if NVS storage was actually modified)
Status codes:
Success codes (converts to true):
PERSISTEDβ Value changed and written to NVSDEFAULTEDβ Value equals default, not storedREMOVEDβ Key successfully removed from NVS
Error codes (converts to false):
ERR_UNKNOWN_KEYβ Key not registered viaconfigure()ERR_INVALID_VALUEβ Rejected by validatorERR_FAIL_ON_WRITEβ NVS write operation failedERR_FAIL_ON_REMOVEβ NVS remove operation failed
Example:
auto res = config.setString("key", "value");
// Simple success check
if (res) {
Serial.println("Success!");
}
// Check if storage was updated
if (res.isStorageUpdated()) {
Serial.println("Value written to NVS");
}
// Detailed error handling
if (!res) {
switch ((Mycila::config::Status)res) {
case Mycila::config::Status::ERR_INVALID_VALUE:
Serial.println("Validator rejected the value");
break;
case Mycila::config::Status::ERR_UNKNOWN_KEY:
Serial.println("Key not configured");
break;
case Mycila::config::Status::ERR_FAIL_ON_WRITE:
Serial.println("NVS write failed");
break;
default:
break;
}
}
// You can also return Status directly from functions returning Result:
Mycila::config::Result myFunction() {
if (error) {
return Mycila::config::Status::ERR_UNKNOWN_KEY;
}
return config.setString("key", "value");
}
Callbacks and Validators
-
void listen(ChangeCallback callback)
Register a callback invoked when any value changes.
Signature:void callback(const char* key, const std::optional<Mycila::config::Value>& newValue)The callback receives a
std::optional<std::variant>value:newValue.has_value() == true- Key was set or updated with a typed valuenewValue.has_value() == false(std::nullopt) - Key was removed/unset
config.listen([](const char* key, const Mycila::config::Value& newValue) { Serial.printf("Key '%s' changed to: %s\n", key, newValue.as<const char*>()); // Type-specific handling if (std::holds_alternative<bool>(newValue)) { bool val = newValue.as<bool>(); Serial.printf("Boolean value: %s\n", val ? "true" : "false"); } else if (std::holds_alternative<int>(newValue)) { int val = newValue.as<int>(); Serial.printf("Integer value: %d\n", val); } } else { Serial.printf("Key '%s' was removed/unset\n", key); } }); -
void listen(RestoredCallback callback)
Register a callback invoked after a bulk restore:void callback() -
bool setValidator(ValidatorCallback callback)
Set a global validator called for all keys. Passnullptrto remove.
Signature:bool validator(const char* key, const Mycila::config::Value& newValue) -
bool setValidator(const char* key, ValidatorCallback callback)
Set a per-key validator. Passnullptrto remove. Returns false if key doesnβt exist.
Note: Validators can also be set during configure() and receive typed variant values:
config.configure("port", 80, [](const char* key, const Mycila::config::Value& value) {
// Value is guaranteed to be int type (matches configure type)
int port = value.as<int>();
return port > 0 && port < 65536;
});
config.configure("temperature", 25.0f, [](const char* key, const Mycila::config::Value& value) {
float temp = value.as<float>();
return temp >= -40.0f && temp <= 85.0f;
});
Backup and Restore
-
void backup(Print& out, bool includeDefaults = true)
Write configuration askey=value\nlines to anyPrintdestination (Serial, File, etc.).includeDefaults = true: Exports all keys (even those using defaults)includeDefaults = false: Only exports keys with stored values
-
bool restore(const char* data)
Parse and restore configuration fromkey=value\nformatted text. Returns true if successful. -
bool restore(const std::map<const char*, std::string>& settings)
Restore configuration from a map. Returns true if successful.
Restore order: Non-_enable keys are applied first, then _enable keys, ensuring feature toggles are activated after their dependencies.
Utilities
-
const std::vector<Key>& keys() const
Get a sorted vector of all registered configuration keys. EachKeycontainsname(const char*) anddefaultValue(Mycila::config::Value variant).Example:
for (const auto& key : config.keys()) { Serial.printf("%s = %s\n", key.name, key.defaultValue.toString().c_str()); } -
const char* keyRef(const char* buffer) const
Given a string buffer, return the canonical key pointer if it matches a registered key (useful for pointer comparisons). Returnsnullptrif not found. -
const Key* key(const char* buffer) const
Given a string buffer, return a pointer to the Key object if it matches a registered key. Returnsnullptrif not found. -
bool isPasswordKey() const
Returns true if key ends withMYCILA_CONFIG_KEY_PASSWORD_SUFFIX(default:"_pwd"). -
bool isEnableKey() const
Returns true if key ends withMYCILA_CONFIG_KEY_ENABLE_SUFFIX(default:"_enable"). -
void toJson(const JsonObject& root) const(requiresMYCILA_JSON_SUPPORT)
Export all configuration to an ArduinoJson object. Password keys are masked.
JSON Export and Password Masking
When MYCILA_JSON_SUPPORT is defined, you can export configuration to JSON with native type preservation:
#include <ArduinoJson.h>
JsonDocument doc;
config.toJson(doc.to<JsonObject>());
serializeJson(doc, Serial);
// Output: {"enabled":true,"port":8080,"threshold":25.5,"name":"Device"}
Type handling in JSON:
- Boolean values exported as JSON
true/false(not strings) - Integer values exported as JSON numbers
- Float/double values exported as JSON numbers with decimal points
- String values exported as JSON strings
- Password keys (ending with
_pwd) are automatically masked as strings (unless you defineMYCILA_CONFIG_SHOW_PASSWORD)
Customize the mask:
// In platformio.ini
build_flags =
-D MYCILA_CONFIG_PASSWORD_MASK=\"****\"
Backup and Restore Example
// Backup to Serial
config.backup(Serial);
// Backup to String (all keys including defaults)
String backup;
StringPrint sp(backup);
config.backup(sp, true);
Serial.println(backup);
// Backup to String (only stored values)
String backupStored;
StringPrint sp2(backupStored);
config.backup(sp2, false);
// Restore from text
const char* savedConfig =
"wifi_ssid=MyNetwork\n"
"wifi_pwd=secret123\n"
"debug_enable=true\n";
config.restore(savedConfig);
// Restore from map with typed values
```cpp
std::map<const char*, Mycila::config::Value> settings;
settings.emplace("wifi_ssid", Mycila::config::Str("NewNetwork"));
settings.emplace("port", 8080);
settings.emplace("enabled", true);
config.restore(std::move(settings));
Configuration Defines
Customize behavior with build flags:
// Key suffix for password keys (default: "_pwd")
-D MYCILA_CONFIG_KEY_PASSWORD_SUFFIX=\"_password\"
// Key suffix for enable keys (default: "_enable")
-D MYCILA_CONFIG_KEY_ENABLE_SUFFIX=\"_on\"
// Show passwords in JSON export (default: masked)
-D MYCILA_CONFIG_SHOW_PASSWORD
// Password mask string (default: "********")
-D MYCILA_CONFIG_PASSWORD_MASK=\"[REDACTED]\"
// Boolean value strings (defaults: "true" / "false")
-D MYCILA_CONFIG_VALUE_TRUE=\"1\"
-D MYCILA_CONFIG_VALUE_FALSE=\"0\"
// Enable extended bool parsing: yes/no, on/off, etc. (default: 1)
-D MYCILA_CONFIG_EXTENDED_BOOL_VALUE_PARSING=0
// Enable 64-bit integer support (int64_t, uint64_t) (default: 1)
-D MYCILA_CONFIG_USE_LONG_LONG=1
// Enable double precision floating point support (default: 1)
-D MYCILA_CONFIG_USE_DOUBLE=1
Key Naming Conventions
- Maximum length: 15 characters (NVS limitation enforced by assert)
- Password keys: End with
_pwd(or custom suffix) β masked in JSON export - Enable keys: End with
_enable(or custom suffix) β applied last during bulk restore
Memory Optimization
The library optimizes memory usage through:
-
Zero-copy flash strings: Default values that are string literals stored in ROM/DROM are referenced by pointer, consuming zero heap.
-
Smart caching: Values are cached on first access, avoiding repeated NVS reads.
-
RAII memory management: The
Strclass automatically manages heap-allocated strings with move semantics. -
Heap tracking: Use
heapUsage()to monitor exact memory consumption:
Serial.printf("Config heap usage: %d bytes\n", config.heapUsage());
Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap());
Examples
Test Example
See examples/Test/Test.ino for a complete example demonstrating:
- Setting up keys and defaults with string values
- Using global and per-key validators with variant values
- Setting and getting string values
- Checking Result codes and Status enum
- Backup and restore functionality
- Change and restore callbacks (with optional values for unset operations)
JSON Export
See examples/Json/Json.ino for:
- JSON export with
toJson() - Backup to string with
backup() - Integration with ArduinoJson
- Toggling configuration values
Native Type Support
examples/Native/Native.ino - All supported types, validators, batch operations
Demonstrates:
- Using all native types: bool, int8/16/32/64, uint8/16/32/64, int, unsigned int, float, double
- Type-safe getters and setters with
get<T>()andset<T>() - Validators with typed variant values
- Batch operations with native types
- Value class methods:
as<T>(),toString(),fromString() - Heap usage tracking
FileSystem Storage Example
examples/FS/FS.ino - Using LittleFS storage backend
Demonstrates:
- Setting up FileSystem storage with LittleFS
- All native type operations with file-based storage
- Storage overhead and file structure inspection
- Type conversion and string parsing
- Performance characteristics vs NVS
Configuration Migration Example
examples/Migration/Migration.ino - Migrating between NVS and FileSystem storage
Demonstrates:
- Setting up both NVS and FileSystem storage
- Configuring identical keys on both configs
- Using Migration class to copy data between backends
- Monitoring migration progress with callbacks
- Verifying migration success
V10 Compatibility
examples/CompatV10/CompatV10.ino - Using the deprecated v10 API wrapper
Large Configuration
See examples/Big/Big.ino for:
- Managing 150+ configuration keys
- Heap usage monitoring
- Random operations stress test
- Performance benchmarking
Custom Storage Backend
You can implement custom storage backends by inheriting from Mycila::config::Storage:
class MyCustomStorage : public Mycila::config::Storage {
public:
bool begin(const char* name) override {
// Initialize your storage
return true;
}
bool hasKey(const char* key) const override {
// Check if key exists
return false;
}
std::optional<Mycila::config::Str> loadString(const char* key) const override {
// Load string value from your storage
// Return std::nullopt if key doesn't exist
return std::nullopt;
}
bool storeString(const char* key, const char* value) override {
// Save string value to your storage
return true;
}
bool remove(const char* key) override {
// Remove key from your storage
return true;
}
bool removeAll() override {
// Clear all keys
return true;
}
// Optional: Implement typed load/store methods for better performance
std::optional<bool> loadBool(const char* key) const override { return std::nullopt; }
bool storeBool(const char* key, bool value) override { return false; }
std::optional<int32_t> loadI32(const char* key) const override { return std::nullopt; }
bool storeI32(const char* key, int32_t value) override { return false; }
std::optional<float> loadFloat(const char* key) const override { return std::nullopt; }
bool storeFloat(const char* key, float value) override { return false; }
// ... other typed methods (loadI8/U8/I16/U16/I64/U64, loadDouble, etc.)
};
// Usage
MyCustomStorage storage;
Mycila::config::Config config(storage);
config.begin("MYAPP");
Storage interface highlights:
- All typed
load*()methods returnstd::optional<T>(nullopt = key not found) - All typed
store*()methods returnbool(true = success) - The library automatically uses typed methods when available for better performance
- Falls back to
loadString()/storeString()with conversion if typed methods not implemented
For a complete reference implementation, see the included NVS storage backend:
- Header:
src/MycilaConfigStorageNVS.h - Implementation: Inline in header file
License
MIT License - see LICENSE file for details.