diff --git a/docs/api.md b/docs/api.md index 3a5f0716..1ede96c5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -115,12 +115,11 @@ curl http://tiny-engineer.local/settings "eyes_style": "classic", "access_token_set": false, "wifi_configured": true, - "wifi_ssid": "MyNetwork", - "wifi_password_set": true, "servo_mins": [60, 40, 45, 35, 40], "servo_maxs": [130, 130, 135, 125, 130], "rgb_order": "GRB", - "oled_rotate_180": false + "oled_rotate_180": false, + "wifi_networks": ["MyNetwork", "Office"] } ``` @@ -135,9 +134,8 @@ curl http://tiny-engineer.local/settings | `loading` | Boot loading screen: `progress` (bar + large IP for 3 s) or `sleep_inertia` (eyes wake; no bar/IP). Default **`progress`**. Applies on **next reboot** | | `eyes_style` | OLED eyes look: `classic` (procedural rounded eyes), `kaomoji` (animated faces), `cover` (full-half bars for an eye-hole mask), or `dots` (12×12 circles). Default **`classic`**. Applies **immediately**. Classic, cover, and dots sequences are described under [`POST /anim`](#post-anim); kaomoji face mapping is under [Kaomoji eyes](#kaomoji-eyes); cover drawing is under [Cover eyes](#cover-eyes); dots drawing is under [Dots eyes](#dots-eyes) | | `access_token_set` | `true` when a non-empty access token is stored (secret itself is never returned) | -| `wifi_configured` | `true` when a WiFi SSID is saved | -| `wifi_ssid` | Saved network name (password is never returned) | -| `wifi_password_set` | `true` when a non-empty WiFi password is saved | +| `wifi_configured` | `true` when at least one WiFi network is saved | +| `wifi_networks` | Saved network names (up to 5), in saved order. Passwords are never returned. At boot the robot scans and tries the visible saved networks strongest first, then the rest in this order (hidden SSIDs do not show in a scan). Change the list with [`PUT /settings/wifi`](#put-settingswifi) | | `servo_mins` | Per-joint safe minimum degrees `[head, neck, left, right, body]` (stock defaults **60, 40, 45, 35, 40**) | | `servo_maxs` | Per-joint safe maximum degrees (stock defaults **130, 130, 135, 125, 130**). Each max must be greater than the matching min | | `rgb_order` | Onboard WS2812 wire-byte order (`RGB`, `RBG`, `GRB`, `GBR`, `BRG`, `BGR`). Default **`GRB`**. Survives factory reset | @@ -145,7 +143,7 @@ curl http://tiny-engineer.local/settings ### `POST /settings` -Update one or more settings. Query params. Values are written to NVS. `sleep_timeout`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, `eyes_style`, and `access_token` apply immediately; a changed `hostname` or `loading` takes effect on the **next reboot**. WiFi credentials (`wifi_ssid` + `wifi_password`) are accepted **only in setup AP mode**; they are **tested before save**; on success the device connects to the home network and returns `wifi_connect_success`, `wifi_ip`, and `wifi_hostname`. `hostname` may be sent with those WiFi params (same validation as Config); it is saved before the STA connect so mDNS uses the chosen name immediately. Outside setup AP → **400** `wifi setup only in AP mode`. Servo ranges (`servo_mins` + `servo_maxs`) are also **setup AP only**; they apply immediately to motion clamps. Outside setup AP → **400** `servo setup only in AP mode`. RGB LED mapping (`rgb_order`) is **setup AP only** and applies immediately to the onboard LED. Outside setup AP → **400** `rgb setup only in AP mode`. OLED rotation (`oled_rotate_180`) is **setup AP only** and applies immediately to the display. Outside setup AP → **400** `oled setup only in AP mode`. Requires Bearer when auth is enabled. +Update one or more settings. Query params. Values are written to NVS. `sleep_timeout`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, `eyes_style`, and `access_token` apply immediately; a changed `hostname` or `loading` takes effect on the **next reboot**. WiFi networks are set with [`PUT /settings/wifi`](#put-settingswifi). Servo ranges (`servo_mins` + `servo_maxs`) are also **setup AP only**; they apply immediately to motion clamps. Outside setup AP → **400** `servo setup only in AP mode`. RGB LED mapping (`rgb_order`) is **setup AP only** and applies immediately to the onboard LED. Outside setup AP → **400** `rgb setup only in AP mode`. OLED rotation (`oled_rotate_180`) is **setup AP only** and applies immediately to the display. Outside setup AP → **400** `oled setup only in AP mode`. Requires Bearer when auth is enabled. ```bash curl -X POST "http://tiny-engineer.local/settings?sleep_timeout=2" @@ -158,7 +156,6 @@ curl -X POST "http://tiny-engineer.local/settings?loading=sleep_inertia" curl -X POST "http://tiny-engineer.local/settings?eyes_style=kaomoji" curl -X POST "http://tiny-engineer.local/settings?access_token=secret" curl -X POST "http://tiny-engineer.local/settings?access_token=" -curl -X POST "http://192.168.4.1/settings?wifi_ssid=MyNetwork&wifi_password=secret&hostname=desk-bot" curl -X POST "http://192.168.4.1/settings?servo_mins=60,40,45,35,40&servo_maxs=130,130,135,125,130" curl -X POST "http://192.168.4.1/settings?rgb_order=GRB" curl -X POST "http://192.168.4.1/settings?oled_rotate_180=1" @@ -178,15 +175,11 @@ curl -X POST "http://tiny-engineer.local/settings?sleep_timeout=10&hostname=tiny "eyes_style": "classic", "access_token_set": true, "wifi_configured": true, - "wifi_ssid": "MyNetwork", - "wifi_password_set": true, "servo_mins": [60, 40, 45, 35, 40], "servo_maxs": [130, 130, 135, 125, 130], "rgb_order": "GRB", "oled_rotate_180": false, - "wifi_connect_success": true, - "wifi_ip": "192.168.1.10", - "wifi_hostname": "desk-bot.local", + "wifi_networks": ["MyNetwork"], "reboot_required": true } ``` @@ -202,18 +195,35 @@ curl -X POST "http://tiny-engineer.local/settings?sleep_timeout=10&hostname=tiny | `loading` | string | `progress` or `sleep_inertia` | | `eyes_style` | string | `classic`, `kaomoji`, `cover`, or `dots` | | `access_token` | string | 0–64 printable ASCII; empty string clears the token and disables auth | -| `wifi_ssid` | string | 1–32 chars; setup AP only; must be sent with `wifi_password` | -| `wifi_password` | string | 0–63 chars; setup AP only; empty allowed for open networks; tested before save | | `servo_mins` | string | Five comma-separated integers `0`–`180` (`head,neck,left,right,body`); setup AP only; must be sent with `servo_maxs` | | `servo_maxs` | string | Five comma-separated integers `0`–`180`; setup AP only; each value must be greater than the matching min | | `rgb_order` | string | `RGB`, `RBG`, `GRB`, `GBR`, `BRG`, or `BGR`; setup AP only; default `GRB` | | `oled_rotate_180` | integer | `0` or `1`; setup AP only; default `0` | -At least one param required. Invalid or missing-all → **400**. WiFi params outside setup AP → **400** `wifi setup only in AP mode`. Servo range params outside setup AP → **400** `servo setup only in AP mode`. RGB order outside setup AP → **400** `rgb setup only in AP mode`. OLED rotation outside setup AP → **400** `oled setup only in AP mode`. WiFi test failure → **400** with error such as `SSID not found`, `Auth failed`, or `Timeout`. `reboot_required` is present and `true` only when the saved hostname differs from the one used at this boot. After successful WiFi save, `wifi_connect_success`, `wifi_ip`, and `wifi_hostname` are included. +At least one param required. Invalid or missing-all → **400**. Servo range params outside setup AP → **400** `servo setup only in AP mode`. RGB order outside setup AP → **400** `rgb setup only in AP mode`. OLED rotation outside setup AP → **400** `oled setup only in AP mode`. `reboot_required` is present and `true` only when the saved hostname differs from the one used at this boot. + +### `PUT /settings/wifi` + +Replace the saved WiFi networks with the list in the JSON body. The robot checks the whole list, then saves it in one write. A network without `password` keeps its saved password; a new network needs one (`""` for an open network). Order is boot priority for networks a scan misses. Requires Bearer when auth is enabled. Same JSON shape as `GET /settings`. + +Outside setup AP mode the list is saved **without a test** and applies at the next boot; the current connection stays up. In setup AP mode the robot **tests the first network** before saving; on success it joins that network and the reply adds `wifi_connect_success`, `wifi_ip`, and `wifi_hostname`. The test registers mDNS under the saved hostname, so set `hostname` with `POST /settings` first. + +```bash +curl -X PUT "http://tiny-engineer.local/settings/wifi" \ + -H "Content-Type: application/json" \ + -d '[{"ssid":"Office","password":"secret"},{"ssid":"Home"},{"ssid":"Cafe","password":""}]' +``` + +| Field | Type | Range | +| --- | --- | --- | +| `ssid` | string | 1–32 chars; unique in the list | +| `password` | string | 0–63 chars; omit to keep the saved password of a saved `ssid` | + +The list must hold 1 to 5 networks, so the robot always has one to try; factory reset clears WiFi. Body not a JSON array → **400** `body must be a JSON array of networks`. An entry that is not an object, or has a non-string `ssid` or `password` (including `null`) → **400** `each network needs a string ssid and an optional string password`. Empty → **400** `send at least one wifi network`. New SSID without `password` → **400** `password required for a new network`. Bad length, the same SSID twice, or more than 5 networks → **400** `invalid ssid or password, duplicate ssid, or more than 5 networks`. WiFi test failure in setup AP mode → **400** with an error such as `SSID not found`, `Auth failed`, or `Timeout`. ### `POST /settings/reset` -Factory reset: clears the NVS `te` namespace and restores settings to compile-time defaults, including WiFi credentials. **Servo min/max, RGB LED mapping (`rgb_order`), and OLED rotation (`oled_rotate_180`) are kept.** After reset, power-cycle (or press reset) so the device reopens setup AP mode and WiFi can be configured again; the servo, screen, and LED steps are pre-filled with the saved values and can be retuned. Until reboot, credentials are cleared in NVS but the radio may still be on the previous STA network. No query params. Requires Bearer when auth is enabled. Same JSON shape as `GET /settings`; includes `reboot_required` when the boot hostname, loading screen, or saved WiFi differed from defaults before reset. +Factory reset: clears the NVS `te` namespace and restores settings to compile-time defaults and clears every saved WiFi network. **Servo min/max, RGB LED mapping (`rgb_order`), and OLED rotation (`oled_rotate_180`) are kept.** After reset, power-cycle (or press reset) so the device reopens setup AP mode and WiFi can be configured again; the servo, screen, and LED steps are pre-filled with the saved values and can be retuned. Until reboot, credentials are cleared in NVS but the radio may still be on the previous STA network. No query params. Requires Bearer when auth is enabled. Same JSON shape as `GET /settings`; includes `reboot_required` when the boot hostname, loading screen, or saved WiFi differed from defaults before reset. The Config web UI clears the browser-stored Bearer token and shows a one-shot gate: power-cycle the device, then join the robot WiFi shown on the OLED and open the setup wizard. It does not poll for reconnect. @@ -233,6 +243,12 @@ curl -X POST "http://tiny-engineer.local/settings/reset" "loading": "progress", "eyes_style": "classic", "access_token_set": false, + "wifi_configured": false, + "servo_mins": [60, 40, 45, 35, 40], + "servo_maxs": [130, 130, 135, 125, 130], + "rgb_order": "GRB", + "oled_rotate_180": false, + "wifi_networks": [], "reboot_required": true } ``` @@ -579,7 +595,7 @@ Errors arrive before any audio plays: | `401` | `{"ok":false,"error":"unauthorized"}` | Access token configured and `Authorization: Bearer` missing or wrong | | `404` | `{"ok":false,"error":"not found"}` | Unknown path | | `415` | `{"ok":false,"error":"..."}` | `/play` body is not a supported WAV | -| `405` | `{"ok":false,"error":"method not allowed"}` | Wrong method on a `/test/*`, `/setup/servo`, `/setup/led`, `/setup/audio`, `/setup/oled`, `/settings`, `/settings/reset`, `/anim`, `/play`, or `/auth` path | +| `405` | `{"ok":false,"error":"method not allowed"}` | Wrong method on a `/test/*`, `/setup/servo`, `/setup/led`, `/setup/audio`, `/setup/oled`, `/settings`, `/settings/reset`, `/settings/wifi`, `/anim`, `/play`, or `/auth` path | Test routes are **POST**. GET/prefetch would move hardware. `/anim` allows **GET** (read) and **POST** (set). `/auth` is **GET** only and always public. diff --git a/docs/hardware/testing.md b/docs/hardware/testing.md index f529dc73..256a5bc0 100644 --- a/docs/hardware/testing.md +++ b/docs/hardware/testing.md @@ -110,11 +110,12 @@ curl -X POST "http://tiny-engineer.local/test/servo?index=0&angle=90" | `GET` | `/health` | Health JSON — full field list in [api.md](../api.md#get-health) | | `GET` / `POST` | `/anim` | Current animation / start one (`name`, optional `interrupt`) — full params in [api.md](../api.md#post-anim) | | `POST` | `/play` | Stream a WAV while `talking` (or `/play/{name}`) runs; answers when the clip ends — full params in [api.md](../api.md#post-play) | -| `GET` | `/settings` | Persistent settings (`sleep_timeout`, `hostname`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, `loading`, `eyes_style`, `access_token_set`, `wifi_configured`, `wifi_ssid`, `wifi_password_set`, `servo_mins`, `servo_maxs`, `rgb_order`, `oled_rotate_180`) | -| `POST` | `/settings?...&wifi_ssid=&wifi_password=` | Update NVS settings; WiFi params setup-AP-only and tested before save; `reboot_required` if hostname changed | +| `GET` | `/settings` | Persistent settings (`sleep_timeout`, `hostname`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, `loading`, `eyes_style`, `access_token_set`, `wifi_configured`, `servo_mins`, `servo_maxs`, `rgb_order`, `oled_rotate_180`, `wifi_networks`) | +| `POST` | `/settings?...` | Update NVS settings; `reboot_required` if hostname changed | | `POST` | `/settings?...&servo_mins=&servo_maxs=` | Servo min/max comma lists; setup-AP-only | | `POST` | `/settings?...&rgb_order=` | WS2812 byte order (`RGB`/`RBG`/`GRB`/`GBR`/`BRG`/`BGR`); setup-AP-only; default `GRB` | | `POST` | `/settings?...&oled_rotate_180=` | OLED 180° rotation (`0`/`1`); setup-AP-only; default `0` | +| `PUT` | `/settings/wifi` | Replace the saved WiFi networks (JSON array of 1 to 5); first one tested in setup AP mode | | `POST` | `/settings/reset` | Factory reset settings to defaults (clears WiFi, keeps servo ranges, RGB mapping, and screen rotation); power-cycle to reopen setup AP | | `POST` | `/test/audio` | `{"ok":true,"test":"audio"}` after `runSoundTest()` | | `POST` | `/test/audio/bell` | `{"ok":true,"test":"bell"}` after `playBell()` | @@ -138,7 +139,7 @@ HTTP runs on STA when connected, or on setup AP at `192.168.4.1` during provisio | OLED `ERROR: OLED not found` then rest of boot runs | Nothing ACK’d at `0x3C` | OLED **VCC=3V3**, GND, SDA=GP0, **SCL**=GP1, common ground, address jumper still 0x3C | | OLED found but `ERROR: OLED initialization failed` | ACK then `display.begin` failed | Wiring/power glitch, wrong size module, I2C noise | | OLED shows `Join this WiFi` / AP name, then `Then open` / `192.168.4.1` | Setup AP mode active | Connect to the shown AP, open `http://192.168.4.1/config`, finish the servo + screen + LED + speaker + WiFi wizard | -| OLED `WiFi failed` then setup AP | Saved STA credentials failed | Join setup AP, open `http://192.168.4.1/config`, enter home WiFi again | +| OLED `WiFi failed` then setup AP | No saved network connected | Join setup AP, open `http://192.168.4.1/config`, enter home WiFi again | | `ERROR: PCA9685 not found` + red RGB + **hang** | Nothing ACK’d at `0x40` | PCA9685 **VCC=3V3** (not V+), GND, SDA/SCL, I2C address pads, +5V not required for the ACK but needed later for motion | | `ERROR: I2S initialization failed` + red RGB + **hang** | `I2S.begin` failed | GPIO2/3/4 not shorted to 5V/GND; pin constants; USB CDC still alive so you can read the line | | I2S OK but `POST /test/audio` is silent | Amp or speaker | MAX98357A **Vin**=USB 5V, GND, GP2/3/4 → BCLK/LRC/DIN, speaker on **SPK+ / SPK-** (not on the PNG, not GND) | diff --git a/docs/settings.md b/docs/settings.md index 90a40b6c..5583b23c 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -2,9 +2,9 @@ Persistent settings live in NVS (namespace `te`) under [`src/settings/`](../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](api.md)): `sleep_timeout`, `hostname`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, `loading`, `eyes_style`, `access_token`, `wifi_ssid`, `wifi_password`, `servo_mins` / `servo_maxs`, `rgb_order`, `oled_rotate_180`. +**HTTP / JSON** (query params and `GET /settings` fields — see [api.md](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`](../src/settings/nvs.h)): `sleep_m`, `host`, `vol`, `welcome`, `serial_log`, `cont_to`, `loading`, `eyes_sty`, `access_tok`, `wifi_ssid`, `wifi_pass`, `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. +**NVS keys** (≤15 chars; defined in [`nvs.h`](../src/settings/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. @@ -16,7 +16,7 @@ Follow the same pattern for a new setting. | 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. 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 | +| 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 | ## Checklist @@ -33,8 +33,7 @@ Public API: [`settings.h`](../src/settings/settings.h). RAM cache: [`internal.h` 4. Add getter `settingsFoo()` in [`getters.cpp`](../src/settings/getters.cpp) and `settingsValidateFoo(...)` in [`validate.cpp`](../src/settings/validate.cpp). 5. Extend `saveSettings(...)` in [`save.cpp`](../src/settings/save.cpp) with a nullable `const 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/serial_log/continuous_timeout/loading/access_token each save; wifi/servo/rgb/oled too). + - 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). 6. Log the new value on load and save via `logSettingsSnapshot` in [`cache.cpp`](../src/settings/cache.cpp) (for secrets like `access_token`, log set/unset only — never the raw value). ### 2. Consumers @@ -44,7 +43,6 @@ Call the getter wherever the value affects behavior (e.g. `settingsVolume()` in ### 3. HTTP — [`src/http/settings_handlers.cpp`](../src/http/settings_handlers.cpp) 1. Include the field in both `snprintf` branches of `sendSettingsJson` (with and without `reboot_required`). -2. Grow the JSON buffer if the payload no longer fits. 3. In `handleSettingsPost`: - `server.hasArg("foo")` - Parse (same style as `sleep_timeout` / `volume`: `strtoul` + end-pointer check for integers) @@ -88,8 +86,7 @@ Flash only when you want to try it on hardware (`pio run -t upload`). ## Pitfalls - **Partial writes:** validate first; never write NVS then fail validation mid-way. -- **JSON buffer:** `sendSettingsJson` uses a fixed `char` buffer — bump size when adding fields. -- **Factory reset:** `factoryResetSettings()` in [`reset.cpp`](../src/settings/reset.cpp) clears NVS namespace `te` and writes defaults, including WiFi credentials. Servo min/max (`sranges`), RGB LED mapping (`rgb_ord`), and OLED rotation (`oled_rot`) are written back unchanged. Exposed as `POST /settings/reset`. After reset, power-cycle into setup AP mode to configure WiFi again (WiFi is not editable on the normal Config page). Servo ranges, LED mapping, and screen rotation can be retuned in that wizard. +- **Factory reset:** `factoryResetSettings()` in [`reset.cpp`](../src/settings/reset.cpp) clears NVS namespace `te` and 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 as `POST /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.md` exactly. diff --git a/platformio.ini b/platformio.ini index 1cd572db..41ae60d0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -23,6 +23,7 @@ lib_deps = adafruit/Adafruit PWM Servo Driver Library adafruit/Adafruit GFX Library adafruit/Adafruit SSD1306 + bblanchon/ArduinoJson@^7.4.3 build_flags = -D ARDUINO_USB_MODE=1 @@ -63,6 +64,7 @@ test_build_src = true build_src_filter = -<**/*.*> + + + + +