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 (POST /settings body 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 | One SettingsValues struct (g_settings) filled by initSettings(); callers use getters |
| Update API | saveSettings(const SettingsPatch&): only the fields the patch sets change, and only changed keys are written to NVS |
| HTTP | JSON object body on POST /settings; at least one field required |
| Apply timing | Prefer immediate apply. If boot-only (like hostname), add it to settingsRebootRequired() so replies report 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. Values struct: values.h / values.cpp. NVS keys, load, save and reset: nvs.h / nvs.cpp. Boot: load.cpp. Getters: getters.cpp. Validation: validate.cpp.
- Add
SETTINGS_DEFAULT_*, min/max (or length) constants insettings.h. - Add the field to
SettingsValuesinvalues.h, set its default inSettingsValues::defaults()and log it inSettingsValues::log()(values.cpp). For secrets likeaccess_token, log set/unset only, never the raw value. - Add the NVS key in
nvs.h(e.g.kKeyFoo = "foo"). Read it inSettingsValues::load()(keep the default on bad or missing data) and write it inwriteValues()(nvs.cpp). - Add
settingsValidateFoo(...)invalidate.cppand the gettersettingsFoo()ingetters.cpp. - Add the field to
SettingsPatchinsettings.h(std::optional<T>, orconst char*for text), check it inSettingsPatch::invalidField()and copy it inSettingsValues::apply().saveSettings()rejects the whole patch when a field is invalid and writes only the keys whose value changed.
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
- Add the field to
settingsJson(). - Read it into the patch with one
read("foo", patch.foo)line inreadPatch(). A wrong JSON type answers 400invalid <field>, and so does a valueSettingsPatch::invalidField()rejects. FieldsreadPatch()does not read answer 400unknown field <field>.
4. Web UI — ui/
- Add the field to
Settingsinui/src/api/types.ts. - Add a control to
ConfigForminui/src/config/ConfigForm.tsx: read it indraftFrom()and send it fromsave(). Use.range-row+ range/number for 0–N scales (see volume). - API reference table under POST
/settingsinui/src/views/ApiReference.tsx: 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()innvs.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
g_bootSettingsandsettingsRebootRequired()). - 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