NVS and the Settings Schema
Every setting, favorite, card binding, schedule, and the resume record survives power loss through NVS (non-volatile storage), ESP-IDF's key-value store in flash. This chapter is the concept, the schema, and the policies.
NVS from zero
Flash memory erases in 4 KB sectors and each sector endures a finite number of erases (~100k). A naive "write the settings struct at a fixed address" design would erase the same sector on every change and kill it within months. NVS solves this with a log-structured design: writes append entries; old values are obsoleted, not erased in place; when pages fill, live entries are compacted onto fresh pages. Wear spreads across the whole 64 KB partition, and a power cut mid-write leaves the previous value intact because the new entry was never marked valid. Reads are indexed and fast; writes are cheap until a compaction amortizes.
The API is namespaced key-value with typed entries (integers, strings, blobs). All of Safi's access is wrapped in state_manager's nvs_manager.c, so the rest of the firmware never touches raw NVS handles; one file owns every key.
The schema
Storage spreads across several namespaces. Most belong to nvs_manager.c; the timezone and the API token live in their own namespaces beside the code that owns them.
Namespace safi_config holds the everyday settings:
| Key | Type | Meaning |
|---|---|---|
volume | u8 | Startup volume |
card_removal | u8 | NFC removal policy |
auto_resume | u8 | Boot resume switch |
time_fmt | u8 | Clock format: 0 for 12-hour (default), 1 for 24-hour |
display | blob | Brightness, auto-off minutes, invert colors |
sleep | blob | Sleep-timer minutes and audio fade |
pb_state | blob | The resume record: path or URL, position, flags |
The rest sits in dedicated namespaces:
| Namespace | Key(s) | Meaning |
|---|---|---|
safi_radio | radio_0, radio_1, ... | Saved station presets, one blob each |
safi_wifi | wifi_creds | SSID and password as one blob |
settings | timezone | POSIX TZ rule (written by time_sync.c) |
api_auth | token | The dashboard bearer token (written by api_auth.c) |
favorites | entries | Favorites with play counts |
scheduler | entries | Schedule entries |
Schema rules the wrapper enforces:
- Every key has a default: a missing key is a normal state (fresh device, post-reset), never an error path. Reads return the default; nothing writes defaults back, so an untouched device performs zero settings writes.
- Blobs carry a version byte so a future firmware can migrate or discard old shapes explicitly instead of misparsing them.
- Factory reset =
nvs_reset_all_settings()erases the namespaces, and first boot regenerates what must exist (notably a fresh API token).
Write policies
Flash wear is a budget, so when to write is a design decision per datum:
| Datum | Policy | Why |
|---|---|---|
| Volume, brightness, settings | On change | Human-rate, trivially rare |
| Resume record | Every 30 s while playing, plus on stop/pause, plus in the shutdown path | The 30 s cadence caps power-loss regression; per-second writing would multiply wear ~30x for imperceptible benefit |
| Favorites, cards, schedules, stations | On mutation via the API | Human-rate |
| Play counts | Batched with the favorite's record | Not worth independent writes |
The battery-critical path calls the resume save synchronously before deep sleep, one of the few justified uses of the event bus's synchronous mode (see power).
Encryption
The production profile enables NVS encryption: entries are encrypted with keys stored in the nvs_key partition, which is itself protected by flash encryption. Development builds skip it for debuggability (idf.py nvs-dump and friends). What each layer defends against is laid out in security; the schema and code are identical either way, which is the point of doing it at the storage layer.
Debugging NVS
nvs_manager_get_stats()surfaces used/free entries (also visible viaGET /api/system).- A full partition manifests as failed writes long before reads suffer; the stats exist because "resume stopped working" should be diagnosable from the dashboard, not by reflashing with logging.
- The wrapper logs every failed operation with the key name; NVS errors are rare enough that each one deserves a line.