From 324193103b2ccaf1975ea83ec224c2ddb28c1aab Mon Sep 17 00:00:00 2001 From: shodan Date: Sun, 11 Oct 2026 14:12:05 +0200 Subject: [PATCH 01/12] feat(settings): store up to five wifi networks The robot keeps a list of up to five WiFi networks instead of one. At boot it scans and tries the visible saved networks, strongest signal first, then the networks the scan missed (hidden SSIDs) in saved order. - POST /settings with wifi_ssid + wifi_password adds a network, or replaces the password of a saved one, and puts it first. Outside setup AP mode the network is saved without a test. - POST /settings/wifi/remove?wifi_ssid= removes a network. The last one stays, so factory reset is the only way to clear WiFi. - GET /settings returns wifi_networks (SSIDs only) and drops wifi_ssid and wifi_password_set. - The list is one NVS blob (wifi_nets). The older wifi_ssid / wifi_pass keys are read as a one-network list until the next save. - The Config page lists the saved networks with an add form. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/api.md | 40 ++- docs/hardware/testing.md | 7 +- docs/settings.md | 10 +- platformio.ini | 1 + src/http/json.cpp | 22 ++ src/http/json.h | 4 + src/http/routes.cpp | 3 + src/http/settings_handlers.cpp | 300 +++++++++--------- src/http/settings_handlers.h | 1 + src/network/wifi_connect.cpp | 65 +++- src/settings/cache.cpp | 17 +- src/settings/getters.cpp | 14 +- src/settings/internal.h | 6 +- src/settings/load.cpp | 37 ++- src/settings/nvs.cpp | 12 - src/settings/nvs.h | 6 +- src/settings/reset.cpp | 7 +- src/settings/save.cpp | 66 ++-- src/settings/settings.h | 12 +- src/settings/wifi_network_list.cpp | 113 +++++++ src/settings/wifi_network_list.h | 33 ++ .../test_wifi_network_list.cpp | 113 +++++++ ui/index.html | 58 +++- ui/src/settings.js | 2 + ui/src/style.css | 26 ++ ui/src/wifi-networks.js | 61 ++++ ui/test/robot.js | 13 +- ui/test/wifi-networks.test.js | 49 +++ 28 files changed, 814 insertions(+), 284 deletions(-) create mode 100644 src/settings/wifi_network_list.cpp create mode 100644 src/settings/wifi_network_list.h create mode 100644 test/test_wifi_network_list/test_wifi_network_list.cpp create mode 100644 ui/src/wifi-networks.js create mode 100644 ui/test/wifi-networks.test.js diff --git a/docs/api.md b/docs/api.md index 3a5f0716..a7e919f6 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 the order they were added, newest first. 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) | | `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 credentials (`wifi_ssid` + `wifi_password`) add a network to the saved list, or replace the password of a saved SSID, and put it first in the list. Up to 5 networks; a sixth → **400** `wifi network list full`. In setup AP mode they are **tested before save**; on success the device connects to the 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 mode the network is saved **without a test** and is tried at the next boot. 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" @@ -159,6 +157,7 @@ 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://tiny-engineer.local/settings?wifi_ssid=Office&wifi_password=secret" 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,12 +177,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_networks": ["MyNetwork"], "wifi_connect_success": true, "wifi_ip": "192.168.1.10", "wifi_hostname": "desk-bot.local", @@ -202,18 +200,32 @@ 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 | +| `wifi_ssid` | string | 1–32 chars; must be sent with `wifi_password`; up to 5 saved networks | +| `wifi_password` | string | 0–63 chars; empty allowed for open networks; tested before save in setup AP mode only | | `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`. 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. + +### `POST /settings/wifi/remove` + +Remove one saved WiFi network. Query param `wifi_ssid` (exact saved name). Applies to the next boot; the current connection stays up. The last saved network cannot be removed, so the robot always has a network to try; use factory reset to clear WiFi. Requires Bearer when auth is enabled. Same JSON shape as `GET /settings`. + +```bash +curl -X POST "http://tiny-engineer.local/settings/wifi/remove?wifi_ssid=Office" +``` + +| Param | Type | Range | +| --- | --- | --- | +| `wifi_ssid` | string | Name of a saved network | + +Missing `wifi_ssid` → **400** `missing wifi_ssid`. Not saved → **404** `unknown wifi_ssid`. Only one network saved → **400** `cannot remove the last wifi network`. ### `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. @@ -579,7 +591,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/remove`, `/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..1410cad0 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?...&wifi_ssid=&wifi_password=` | Update NVS settings; WiFi params add a saved network (tested before save in setup AP only); `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` | +| `POST` | `/settings/wifi/remove?wifi_ssid=` | Remove a saved WiFi network; the last one stays | | `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..215f5431 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`, `wifi_ssid`, `wifi_password` (adds to `wifi_networks`), `servo_mins` / `servo_maxs`, `rgb_order`, `oled_rotate_180`. -**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 @@ -34,7 +34,7 @@ Public API: [`settings.h`](../src/settings/settings.h). RAM cache: [`internal.h` 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 @@ -89,7 +89,7 @@ Flash only when you want to try it on hardware (`pio run -t upload`). - **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..80627516 100644 --- a/platformio.ini +++ b/platformio.ini @@ -63,6 +63,7 @@ test_build_src = true build_src_filter = -<**/*.*> + + + + +