Persistent settings live in NVS (namespace te) under src/settings/. They are exposed on GET/POST /settings, the Config web UI, and must stay in sync with API docs.
HTTP / JSON (query params and GET /settings fields — see api.md): sleep_timeout, hostname, volume, welcome, serial_log, continuous_timeout, loading, eyes_style, access_token, servo_mins / servo_maxs, rgb_order, oled_rotate_180. WiFi networks have their own endpoint, PUT /settings/wifi, and show as wifi_networks.
NVS keys (≤15 chars; defined in nvs.h): sleep_m, host, vol, welcome, serial_log, cont_to, loading, eyes_sty, access_tok, wifi_nets (blob of up to 5 networks; replaces the older single-network wifi_ssid / wifi_pass, which are migrated on load), sranges, rgb_ord, oled_rot. Add new short keys there — do not reuse HTTP names as NVS keys unless they already fit the length limit.
Follow the same pattern for a new setting.
| Choice | Convention |
|---|---|
| Storage | NVS under namespace te; short key name (≤15 chars for Preferences) |
| RAM | Cached in a module-static after initSettings(); callers use getters |
| Update API | Nullable pointer args to saveSettings(...) — only non-null fields change |
| HTTP | Query params on POST /settings; at least one param required |
| Apply timing | Prefer immediate apply. If boot-only (like hostname), set reboot_required and document it. WiFi networks live in a WifiNetworkList saved with saveWifiNetworks(), outside saveSettings(...); they are tested before save only in setup AP mode. Servo min/max (servo_mins / servo_maxs), RGB LED mapping (rgb_order), and OLED rotation (oled_rotate_180) are setup-AP-only and apply immediately |
| Types | Prefer small integers / short strings; validate before any NVS write |
Work through these layers in order. Mirror an existing setting (volume is the simplest integer example; hostname shows reboot + string validation).
1. Core — src/settings/
Public API: settings.h. RAM cache: internal.h / cache.cpp. NVS keys: nvs.h / nvs.cpp. Load: load.cpp. Save: save.cpp. Reset: reset.cpp. Getters: getters.cpp. Validation: validate.cpp.
- Add
SETTINGS_DEFAULT_*, min/max (or length) constants insettings.h. - Add NVS key string in
nvs.h(e.g.kKeyFoo = "foo") and persist it inwriteAllToNvs/settingsNvsPutAllinnvs.cpp. - Add RAM cache variable in
internal.h/cache.cpp; load + validate ininitSettings()(load.cpp); fall back to default on bad/missing data. - Add getter
settingsFoo()ingetters.cppandsettingsValidateFoo(...)invalidate.cpp. - Extend
saveSettings(...)insave.cppwith a nullableconst T* foo:- Reject the whole save if validation fails.
- Write all persisted fields together (current pattern rewrites sleep/host/volume/welcome/serial_log/continuous_timeout/loading/access_token each save; servo/rgb/oled too).
- Log the new value on load and save via
logSettingsSnapshotincache.cpp(for secrets likeaccess_token, log set/unset only — never the raw value).
Call the getter wherever the value affects behavior (e.g. settingsVolume() in audio). Prefer reading the getter at use time so POST /settings applies without reboot.
3. HTTP — src/http/settings_handlers.cpp
- Include the field in both
snprintfbranches ofsendSettingsJson(with and withoutreboot_required). - In
handleSettingsPost:server.hasArg("foo")- Parse (same style as
sleep_timeout/volume:strtoul+ end-pointer check for integers) - Validate → 400 with a clear
errorstring - Pass pointer into
saveSettings
- Update the “missing …” 400 message to list the new param.
4. Web UI — ui/
- Config form control (
#config-foo). Use.range-row+ range/number for 0–N scales (see volume). loadSettings()readsj.foo; save POST includes&foo=….- API reference table under POST
/settings: param, type, range. - Home Config card blurb if the setting is user-facing.
Per .cursor/rules/sync-api-endpoints.mdc:
docs/api.md— GET example JSON, field table, POST curl + param table, apply timing.README.md— short mention if the Config/settings summary lists settings.docs/hardware/testing.md— settings rows in the route table if present.
pio runFlash only when you want to try it on hardware (pio run -t upload).
| Layer | What landed |
|---|---|
| Settings | SETTINGS_DEFAULT_VOLUME = 70, key vol, settingsVolume(), range 0–100 |
| Audio | Tone amplitude and WAV PCM scaled by settingsVolume() / 100 |
| API | "volume":70 on GET; POST /settings?volume=40 |
| UI | Slider + number on Config; live N% label |
| Docs | api.md + README |
- Partial writes: validate first; never write NVS then fail validation mid-way.
- Factory reset:
factoryResetSettings()inreset.cppclears NVS namespaceteand writes defaults, which clears every saved WiFi network. Servo min/max (sranges), RGB LED mapping (rgb_ord), and OLED rotation (oled_rot) are written back unchanged. Exposed asPOST /settings/reset. After reset, power-cycle into setup AP mode to configure WiFi again. Servo ranges, LED mapping, and screen rotation can be retuned in that wizard. - Hostname-style settings: freeze the boot value separately if live change cannot apply (see
settingsBootHostname()/reboot_required). - UI size: the panel is embedded gzipped in the firmware; keep controls compact.
- Doc drift: HTML param tables must match
api.mdexactly.
- Runtime API:
api.md(GET/POST /settings,POST /settings/reset) - Endpoint sync rule:
.cursor/rules/sync-api-endpoints.mdc