Skip to main content

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:

KeyTypeMeaning
volumeu8Startup volume
card_removalu8NFC removal policy
auto_resumeu8Boot resume switch
time_fmtu8Clock format: 0 for 12-hour (default), 1 for 24-hour
displayblobBrightness, auto-off minutes, invert colors
sleepblobSleep-timer minutes and audio fade
pb_stateblobThe resume record: path or URL, position, flags

The rest sits in dedicated namespaces:

NamespaceKey(s)Meaning
safi_radioradio_0, radio_1, ...Saved station presets, one blob each
safi_wifiwifi_credsSSID and password as one blob
settingstimezonePOSIX TZ rule (written by time_sync.c)
api_authtokenThe dashboard bearer token (written by api_auth.c)
favoritesentriesFavorites with play counts
schedulerentriesSchedule 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:

DatumPolicyWhy
Volume, brightness, settingsOn changeHuman-rate, trivially rare
Resume recordEvery 30 s while playing, plus on stop/pause, plus in the shutdown pathThe 30 s cadence caps power-loss regression; per-second writing would multiply wear ~30x for imperceptible benefit
Favorites, cards, schedules, stationsOn mutation via the APIHuman-rate
Play countsBatched with the favorite's recordNot 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 via GET /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.