Persistent settings live in NVS (namespace te) via src/settings.cpp and src/settings_storage.cpp. They are exposed on GET/POST /settings, the Config web UI, and must stay in sync with API docs.
Existing keys: sleep_timeout, hostname, volume, welcome, serial_log, continuous_timeout, loading, access_token, wifi_ssid, wifi_password. Follow the same pattern for a new one.
| 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 credentials are setup-AP-only and tested before save |
| 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.h / src/settings.cpp / src/settings_storage.cpp
- Add
SETTINGS_DEFAULT_*, min/max (or length) constants. - Add NVS key string in
settings_storage.cpp(e.g.kKeyFoo = "foo"). - Add RAM cache variable in
settings_internal.h/settings_storage.cpp; load + validate ininitSettings(); fall back to default on bad/missing data. - Add getter
settingsFoo()andsettingsValidateFoo(...)insettings.cpp. - Extend
saveSettings(...)with a nullableconst T* foo:- Reject the whole save if validation fails.
- Require at least one non-null arg among all settings.
- Write all persisted fields together (current pattern rewrites sleep/host/volume/welcome/continuous_timeout/loading/access_token each save).
- Log the new value on load and save (for secrets like
access_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). - Grow the JSON buffer if the payload no longer fits.
- 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 — src/http/index_page.cpp
- 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.
- JSON buffer:
sendSettingsJsonuses a fixedcharbuffer — bump size when adding fields. - Factory reset:
factoryResetSettings()insettings_storage.cppclears NVS namespaceteand writes all defaults, including WiFi credentials; exposed asPOST /settings/reset. After reset, power-cycle into setup AP mode to configure WiFi again (WiFi is not editable on the normal Config page). - Hostname-style settings: freeze the boot value separately if live change cannot apply (see
settingsBootHostname()/reboot_required). - HTML string size: the panel is a big string literal in
index_page.cpp; 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