Backup ingecheckt, werkt niet lekker met wifi settings

This commit is contained in:
koenieeee
2026-02-08 00:02:22 +01:00
parent ae50459f35
commit 1cc0949d98
77 changed files with 1149 additions and 83 deletions
+108
View File
@@ -0,0 +1,108 @@
# Gesture Filename Mapping
SPIFFS has a **32-character filename limit** (including `.png` extension). Some spell names are too long when converted directly. This document maps the spell names to their shortened filenames.
## Files That Need Renaming
The following files exceed the 32-character limit and must be renamed:
```bash
# In the /gestures directory, rename these files:
mv the_hair_thickening_growing_charm.png hair_grow_charm.png
```
## Full Spell Name to Filename Mapping
| Spell Name (from SPELL_NAMES) | Filename in SPIFFS | Status |
|-------------------------------|-------------------|---------|
| The_Force_Spell | the_force_spell.png | ✓ OK (19 chars) |
| Colloportus | colloportus.png | ✓ OK (15 chars) |
| Colloshoo | colloshoo.png | ✓ OK (13 chars) |
| The_Hour_Reversal_Reversal_Charm | hour_reversal_rev.png | ⚠️ NEEDS RENAME (21 chars) |
| Evanesco | evanesco.png | ✓ OK (12 chars) |
| Herbivicus | herbivicus.png | ✓ OK (14 chars) |
| Orchideous | orchideous.png | ✓ OK (14 chars) |
| Brachiabindo | brachiabindo.png | ✓ OK (16 chars) |
| Meteolojinx | meteolojinx.png | ✓ OK (15 chars) |
| Riddikulus | riddikulus.png | ✓ OK (14 chars) |
| Silencio | silencio.png | ✓ OK (12 chars) |
| Immobulus | immobulus.png | ✓ OK (13 chars) |
| Confringo | confringo.png | ✓ OK (13 chars) |
| Petrificus_Totalus | petrificus_totalus.png | ✓ OK (22 chars) |
| Flipendo | flipendo.png | ✓ OK (12 chars) |
| The_Cheering_Charm | the_cheering_charm.png | ✓ OK (22 chars) |
| Salvio_Hexia | salvio_hexia.png | ✓ OK (16 chars) |
| Pestis_Incendium | pestis_incendium.png | ⚠️ MISSING |
| Alohomora | alohomora.png | ✓ OK (13 chars) |
| Protego | protego.png | ✓ OK (11 chars) |
| Langlock | langlock.png | ⚠️ MISSING |
| Mucus_Ad_Nauseum | mucus_ad_nauseum.png | ✓ OK (20 chars) |
| Flagrate | flagrate.png | ✓ OK (12 chars) |
| Glacius | glacius.png | ✓ OK (11 chars) |
| Finite | finite.png | ✓ OK (10 chars) |
| Anteoculatia | anteoculatia.png | ✓ OK (16 chars) |
| Expelliarmus | expelliarmus.png | ✓ OK (16 chars) |
| Expecto_Patronum | expecto_patronum.png | ✓ OK (20 chars) |
| Descendo | descendo.png | ⚠️ MISSING |
| Depulso | depulso.png | ⚠️ MISSING |
| Reducto | reducto.png | ✓ OK (11 chars) |
| Colovaria | colovaria.png | ✓ OK (13 chars) |
| Aberto | aberto.png | ✓ OK (10 chars) |
| Confundo | confundo.png | ✓ OK (12 chars) |
| Densaugeo | densaugeo.png | ✓ OK (13 chars) |
| The_Stretching_Jinx | the_stretching_jinx.png | ✓ OK (23 chars) |
| Entomorphis | entomorphis.png | ✓ OK (15 chars) |
| The_Hair_Thickening_Growing_Charm | hair_grow_charm.png | ⚠️ NEEDS RENAME (19 chars) |
| Bombarda | bombarda.png | ✓ OK (12 chars) |
| Finestra | finestra.png | ✓ OK (12 chars) |
| The_Sleeping_Charm | the_sleeping_charm.png | ✓ OK (22 chars) |
| Rictusempra | rictusempra.png | ✓ OK (15 chars) |
| Piertotum_Locomotor | piertotum_locomotor.png | ✓ OK (23 chars) |
| Expulso | expulso.png | ✓ OK (11 chars) |
| Impedimenta | impedimenta.png | ✓ OK (15 chars) |
| Ascendio | ascendio.png | ✓ OK (12 chars) |
| Incarcerous | incarcerous.png | ✓ OK (15 chars) |
| Ventus | ventus.png | ✓ OK (10 chars) |
| Revelio | revelio.png | ✓ OK (11 chars) |
| Accio | accio.png | ✓ OK (9 chars) |
| Melefors | melefors.png | ✓ OK (12 chars) |
| Scourgify | scourgify.png | ✓ OK (13 chars) |
| Wingardium_Leviosa | wingardium_leviosa.png | ✓ OK (22 chars) |
| Nox | nox.png | ✓ OK (7 chars) |
| Stupefy | stupefy.png | ✓ OK (11 chars) |
| Spongify | spongify.png | ⚠️ MISSING |
| Lumos | lumos.png | ✓ OK (9 chars) |
| Appare_Vestigium | appare_vestigium.png | ✓ OK (20 chars) |
| Verdimillious | verdimillious.png | ✓ OK (17 chars) |
| Fulgari | fulgari.png | ✓ OK (11 chars) |
| Reparo | reparo.png | ✓ OK (10 chars) |
| Locomotor | locomotor.png | ⚠️ MISSING |
| Quietus | quietus.png | ✓ OK (11 chars) |
| Everte_Statum | everte_statum.png | ⚠️ MISSING |
| Incendio | incendio.png | ✓ OK (12 chars) |
| Aguamenti | aguamenti.png | ✓ OK (13 chars) |
| Sonorus | sonorus.png | ⚠️ MISSING |
| Cantis | cantis.png | ✓ OK (10 chars) |
| Arania_Exumai | arania_exumai.png | ✓ OK (17 chars) |
| Calvorio | calvorio.png | ✓ OK (12 chars) |
| The_Hour_Reversal_Charm | hour_reversal.png | ⚠️ NEEDS RENAME (17 chars) |
| Vermillious | vermillious.png | ✓ OK (15 chars) |
| The_Pepper-Breath_Hex | pepper_breath_hex.png | ⚠️ NEEDS RENAME (21 chars) |
## Rename Commands
Run these commands in the `/gestures` directory:
```bash
cd gestures
# Rename files that exceed 32 character limit
mv the_hair_thickening_growing_charm.png hair_grow_charm.png
# Create missing/renamed files if you have them with different names
# (You'll need to find or create these missing gesture images)
```
## Implementation
The web interface will use a JavaScript mapping to convert SPELL_NAMES to the actual filenames in SPIFFS.
+106
View File
@@ -0,0 +1,106 @@
# Magic Caster Wand Spell Gestures
<table>
<tr>
<td align="center"><b>Aberto</b><br/><img src="aberto.png" width="180"/></td>
<td align="center"><b>Accio</b><br/><img src="accio.png" width="180"/></td>
<td align="center"><b>Aguamenti</b><br/><img src="aguamenti.png" width="180"/></td>
<td align="center"><b>Alohomora</b><br/><img src="alohomora.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Anteoculatia</b><br/><img src="anteoculatia.png" width="180"/></td>
<td align="center"><b>Appare Vestigium</b><br/><img src="appare_vestigium.png" width="180"/></td>
<td align="center"><b>Arania Exumai</b><br/><img src="arania_exumai.png" width="180"/></td>
<td align="center"><b>Ascendio</b><br/><img src="ascendio.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Bombarda</b><br/><img src="bombarda.png" width="180"/></td>
<td align="center"><b>Brachiabindo</b><br/><img src="brachiabindo.png" width="180"/></td>
<td align="center"><b>Calvorio</b><br/><img src="calvorio.png" width="180"/></td>
<td align="center"><b>Cantis</b><br/><img src="cantis.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Colloportus</b><br/><img src="colloportus.png" width="180"/></td>
<td align="center"><b>Colloshoo</b><br/><img src="colloshoo.png" width="180"/></td>
<td align="center"><b>Colovaria</b><br/><img src="colovaria.png" width="180"/></td>
<td align="center"><b>Confringo</b><br/><img src="confringo.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Confundo</b><br/><img src="confundo.png" width="180"/></td>
<td align="center"><b>Densaugeo</b><br/><img src="densaugeo.png" width="180"/></td>
<td align="center"><b>Entomorphis</b><br/><img src="entomorphis.png" width="180"/></td>
<td align="center"><b>Evanesco</b><br/><img src="evanesco.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Expecto Patronum</b><br/><img src="expecto_patronum.png" width="180"/></td>
<td align="center"><b>Expelliarmus</b><br/><img src="expelliarmus.png" width="180"/></td>
<td align="center"><b>Expulso</b><br/><img src="expulso.png" width="180"/></td>
<td align="center"><b>Finestra</b><br/><img src="finestra.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Finite</b><br/><img src="finite.png" width="180"/></td>
<td align="center"><b>Flagrate</b><br/><img src="flagrate.png" width="180"/></td>
<td align="center"><b>Flipendo</b><br/><img src="flipendo.png" width="180"/></td>
<td align="center"><b>Fulgari</b><br/><img src="fulgari.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Glacius</b><br/><img src="glacius.png" width="180"/></td>
<td align="center"><b>Herbivicus</b><br/><img src="herbivicus.png" width="180"/></td>
<td align="center"><b>Immobulus</b><br/><img src="immobulus.png" width="180"/></td>
<td align="center"><b>Impedimenta</b><br/><img src="impedimenta.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Incarcerous</b><br/><img src="incarcerous.png" width="180"/></td>
<td align="center"><b>Incendio</b><br/><img src="incendio.png" width="180"/></td>
<td align="center"><b>Lumos</b><br/><img src="lumos.png" width="180"/></td>
<td align="center"><b>Lumos Maxima</b><br/><img src="lumos_maxima.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Melefors</b><br/><img src="melefors.png" width="180"/></td>
<td align="center"><b>Meteolojinx</b><br/><img src="meteolojinx.png" width="180"/></td>
<td align="center"><b>Mucus Ad Nauseum</b><br/><img src="mucus_ad_nauseum.png" width="180"/></td>
<td align="center"><b>Nox</b><br/><img src="nox.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Orchideous</b><br/><img src="orchideous.png" width="180"/></td>
<td align="center"><b>Petrificus Totalus</b><br/><img src="petrificus_totalus.png" width="180"/></td>
<td align="center"><b>Piertotum Locomotor</b><br/><img src="piertotum_locomotor.png" width="180"/></td>
<td align="center"><b>Protego</b><br/><img src="protego.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Quietus</b><br/><img src="quietus.png" width="180"/></td>
<td align="center"><b>Reducto</b><br/><img src="reducto.png" width="180"/></td>
<td align="center"><b>Reparo</b><br/><img src="reparo.png" width="180"/></td>
<td align="center"><b>Revelio</b><br/><img src="revelio.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Rictusempra</b><br/><img src="rictusempra.png" width="180"/></td>
<td align="center"><b>Riddikulus</b><br/><img src="riddikulus.png" width="180"/></td>
<td align="center"><b>Salvio Hexia</b><br/><img src="salvio_hexia.png" width="180"/></td>
<td align="center"><b>Scourgify</b><br/><img src="scourgify.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Silencio</b><br/><img src="silencio.png" width="180"/></td>
<td align="center"><b>Stupefy</b><br/><img src="stupefy.png" width="180"/></td>
<td align="center"><b>The Cheering Charm</b><br/><img src="the_cheering_charm.png" width="180"/></td>
<td align="center"><b>The Force Spell</b><br/><img src="the_force_spell.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>The Hair Thickening Growing Charm</b><br/><img src="the_hair_thickening_growing_charm.png" width="180"/></td>
<td align="center"><b>The Hour Reversal Charm</b><br/><img src="the_hour_reversal_charm.png" width="180"/></td>
<td align="center"><b>The Sleeping Charm</b><br/><img src="the_sleeping_charm.png" width="180"/></td>
<td align="center"><b>The Spell Thickening Charm</b><br/><img src="the_spell_thickening_charm.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>The Stretching Jinx</b><br/><img src="the_stretching_jinx.png" width="180"/></td>
<td align="center"><b>Ventus</b><br/><img src="ventus.png" width="180"/></td>
<td align="center"><b>Verdimillious</b><br/><img src="verdimillious.png" width="180"/></td>
<td align="center"><b>Vermillious</b><br/><img src="vermillious.png" width="180"/></td>
</tr>
<tr>
<td align="center"><b>Wingardium Leviosa</b><br/><img src="wingardium_leviosa.png" width="180"/></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+2
View File
@@ -70,7 +70,9 @@ private:
static esp_err_t wifi_scan_handler(httpd_req_t *req); // Scan WiFi networks
static esp_err_t wifi_connect_handler(httpd_req_t *req); // Connect to WiFi
static esp_err_t hotspot_settings_handler(httpd_req_t *req); // Save hotspot settings
static esp_err_t hotspot_get_handler(httpd_req_t *req); // Get hotspot settings
static esp_err_t system_reboot_handler(httpd_req_t *req); // Reboot device
static esp_err_t gesture_image_handler(httpd_req_t *req); // Serve gesture images from SPIFFS
void addWebSocketClient(int fd);
void removeWebSocketClient(int fd);
+3 -2
View File
@@ -1,8 +1,9 @@
# Name, Type, SubType, Offset, Size, Flags
# 8MB Flash layout for ESP32-S3 with TensorFlow Lite
# 8MB Flash layout for ESP32-S3 with TensorFlow Lite and Gesture Images
# Provides OTA support with 2MB apps + 512KB model + 3.3MB SPIFFS for gestures
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x200000,
app1, app, ota_1, 0x210000,0x200000,
model, data, 0x40, 0x410000,0x80000,
spiffs, data, spiffs, 0x490000,0x20000,
spiffs, data, spiffs, 0x490000,0x370000,
1 # Name, Type, SubType, Offset, Size, Flags
2 # 8MB Flash layout for ESP32-S3 with TensorFlow Lite # 8MB Flash layout for ESP32-S3 with TensorFlow Lite and Gesture Images
3 # Provides OTA support with 2MB apps + 512KB model + 3.3MB SPIFFS for gestures
4 nvs, data, nvs, 0x9000, 0x5000,
5 otadata, data, ota, 0xe000, 0x2000,
6 app0, app, ota_0, 0x10000, 0x200000,
7 app1, app, ota_1, 0x210000,0x200000,
8 model, data, 0x40, 0x410000,0x80000,
9 spiffs, data, spiffs, 0x490000,0x20000, spiffs, data, spiffs, 0x490000,0x370000,
+107 -15
View File
@@ -382,19 +382,63 @@ extern "C" void app_main()
.policy = WIFI_COUNTRY_POLICY_AUTO};
ESP_ERROR_CHECK(esp_wifi_set_country(&country));
// Load AP settings from NVS (fallback to config.h defaults)
char ap_ssid[32] = {0};
char ap_password[64] = {0};
uint8_t ap_channel = 6; // Default channel
bool hotspot_enabled = false; // By default use config.h values
nvs_handle_t nvs_handle;
esp_err_t nvs_err = nvs_open("storage", NVS_READONLY, &nvs_handle);
if (nvs_err == ESP_OK)
{
uint8_t hotspot_en = 0;
nvs_get_u8(nvs_handle, "hotspot_enabled", &hotspot_en);
hotspot_enabled = (hotspot_en != 0);
size_t required_size;
nvs_err = nvs_get_str(nvs_handle, "hotspot_ssid", NULL, &required_size);
if (nvs_err == ESP_OK && required_size > 0 && required_size <= sizeof(ap_ssid))
{
nvs_get_str(nvs_handle, "hotspot_ssid", ap_ssid, &required_size);
}
nvs_err = nvs_get_str(nvs_handle, "hotspot_password", NULL, &required_size);
if (nvs_err == ESP_OK && required_size > 0 && required_size <= sizeof(ap_password))
{
nvs_get_str(nvs_handle, "hotspot_password", ap_password, &required_size);
}
uint8_t channel = 6;
nvs_get_u8(nvs_handle, "hotspot_channel", &channel);
if (channel >= 1 && channel <= 13)
{
ap_channel = channel;
}
nvs_close(nvs_handle);
}
// Use config.h defaults if NVS values are empty or hotspot not enabled
if (!hotspot_enabled || strlen(ap_ssid) == 0)
{
strncpy(ap_ssid, AP_SSID, sizeof(ap_ssid) - 1);
strncpy(ap_password, AP_PASSWORD, sizeof(ap_password) - 1);
}
// Configure AP with improved compatibility settings
wifi_config_t wifi_config = {};
strncpy((char *)wifi_config.ap.ssid, AP_SSID, sizeof(wifi_config.ap.ssid) - 1);
strncpy((char *)wifi_config.ap.ssid, ap_ssid, sizeof(wifi_config.ap.ssid) - 1);
wifi_config.ap.ssid[sizeof(wifi_config.ap.ssid) - 1] = '\0';
wifi_config.ap.ssid_len = strlen((char *)wifi_config.ap.ssid);
wifi_config.ap.channel = 6; // Use channel 6 (most compatible)
wifi_config.ap.channel = ap_channel;
wifi_config.ap.max_connection = AP_MAX_CONNECTIONS;
wifi_config.ap.beacon_interval = 100;
// Use WPA2 security (Android often refuses open networks)
if (strlen(AP_PASSWORD) >= 8)
if (strlen(ap_password) >= 8)
{
strncpy((char *)wifi_config.ap.password, AP_PASSWORD, sizeof(wifi_config.ap.password) - 1);
strncpy((char *)wifi_config.ap.password, ap_password, sizeof(wifi_config.ap.password) - 1);
wifi_config.ap.password[sizeof(wifi_config.ap.password) - 1] = '\0';
wifi_config.ap.authmode = WIFI_AUTH_WPA2_PSK;
wifi_config.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP;
@@ -413,13 +457,13 @@ extern "C" void app_main()
ESP_ERROR_CHECK(esp_wifi_set_bandwidth(WIFI_IF_AP, WIFI_BW20));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "✓ WiFi AP started: %s", AP_SSID);
ESP_LOGI(TAG, " Channel: 6 (2.4GHz)");
ESP_LOGI(TAG, "✓ WiFi AP started: %s", ap_ssid);
ESP_LOGI(TAG, " Channel: %d (2.4GHz)", ap_channel);
ESP_LOGI(TAG, " Bandwidth: 20MHz");
if (strlen(AP_PASSWORD) >= 8)
if (strlen(ap_password) >= 8)
{
ESP_LOGI(TAG, " Security: WPA2-PSK");
ESP_LOGI(TAG, " Password: %s", AP_PASSWORD);
ESP_LOGI(TAG, " Password: %s", ap_password);
}
else
{
@@ -427,10 +471,10 @@ extern "C" void app_main()
}
ESP_LOGI(TAG, " IP Address: 192.168.4.1");
ESP_LOGI(TAG, "");
ESP_LOGI(TAG, "Connect your device to '%s' WiFi network", AP_SSID);
if (strlen(AP_PASSWORD) >= 8)
ESP_LOGI(TAG, "Connect your device to '%s' WiFi network", ap_ssid);
if (strlen(ap_password) >= 8)
{
ESP_LOGI(TAG, "Password: %s", AP_PASSWORD);
ESP_LOGI(TAG, "Password: %s", ap_password);
}
ESP_LOGI(TAG, "Then open browser: http://192.168.4.1/");
#else
@@ -462,6 +506,10 @@ extern "C" void app_main()
// Check if MQTT is enabled in NVS settings
bool ha_mqtt_enabled = true; // Default: enabled
char mqtt_broker[128] = {0};
char mqtt_username[64] = {0};
char mqtt_password[64] = {0};
nvs_handle_t nvs_handle;
esp_err_t err = nvs_open("storage", NVS_READONLY, &nvs_handle);
if (err == ESP_OK)
@@ -469,15 +517,59 @@ extern "C" void app_main()
uint8_t ha_mqtt_u8 = 1;
nvs_get_u8(nvs_handle, "ha_mqtt_enabled", &ha_mqtt_u8);
ha_mqtt_enabled = (ha_mqtt_u8 != 0);
// Load MQTT settings from NVS (fallback to config.h defaults)
size_t required_size;
err = nvs_get_str(nvs_handle, "mqtt_broker", NULL, &required_size);
if (err == ESP_OK && required_size > 0 && required_size <= sizeof(mqtt_broker))
{
nvs_get_str(nvs_handle, "mqtt_broker", mqtt_broker, &required_size);
}
err = nvs_get_str(nvs_handle, "mqtt_username", NULL, &required_size);
if (err == ESP_OK && required_size > 0 && required_size <= sizeof(mqtt_username))
{
nvs_get_str(nvs_handle, "mqtt_username", mqtt_username, &required_size);
}
err = nvs_get_str(nvs_handle, "mqtt_password", NULL, &required_size);
if (err == ESP_OK && required_size > 0 && required_size <= sizeof(mqtt_password))
{
nvs_get_str(nvs_handle, "mqtt_password", mqtt_password, &required_size);
}
nvs_close(nvs_handle);
}
// Use defaults from config.h if NVS values are empty
if (strlen(mqtt_broker) == 0)
{
snprintf(mqtt_broker, sizeof(mqtt_broker), "mqtt://%s:%d", MQTT_SERVER, MQTT_PORT);
}
else if (strncmp(mqtt_broker, "mqtt://", 7) != 0)
{
// Add mqtt:// prefix if not present
char temp[121];
strncpy(temp, mqtt_broker, 120);
temp[120] = '\0';
snprintf(mqtt_broker, sizeof(mqtt_broker), "mqtt://%s", temp);
}
if (strlen(mqtt_username) == 0)
{
strncpy(mqtt_username, MQTT_USER, sizeof(mqtt_username) - 1);
}
if (strlen(mqtt_password) == 0)
{
strncpy(mqtt_password, MQTT_PASSWORD, sizeof(mqtt_password) - 1);
}
if (ha_mqtt_enabled)
{
// Initialize MQTT client for Home Assistant
char mqtt_uri[128];
snprintf(mqtt_uri, sizeof(mqtt_uri), "mqtt://%s:%d", MQTT_SERVER, MQTT_PORT);
if (mqttClient.begin(mqtt_uri, MQTT_USER, MQTT_PASSWORD))
// Initialize MQTT client for Home Assistant with settings from NVS or config.h
ESP_LOGI(TAG, "Connecting to MQTT broker: %s", mqtt_broker);
if (mqttClient.begin(mqtt_broker, mqtt_username, mqtt_password))
{
ESP_LOGI(TAG, "✓ MQTT client initialized for Home Assistant");
}
+16 -2
View File
@@ -6,6 +6,9 @@
#include "nvs.h"
#include <string.h>
#include <cmath>
#include "soc/rtc_cntl_reg.h"
#include "rom/ets_sys.h"
#include "esp_private/system_internal.h"
static const char *TAG = "usb_hid";
@@ -300,6 +303,15 @@ void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_
(void)buffer;
(void)bufsize;
}
// TinyUSB CDC line state callback - disabled for manual bootloader entry
// Use BOOT button to enter bootloader mode for flashing
void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts)
{
(void)itf;
(void)dtr;
(void)rts;
}
#endif
USBHIDManager::USBHIDManager()
@@ -367,6 +379,9 @@ bool USBHIDManager::begin()
ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
#if CONFIG_TINYUSB_CDC_ENABLED
// CDC line state callback (tud_cdc_line_state_cb) is automatically called by TinyUSB
// Note: Auto-reset is currently disabled to prevent interference with monitoring
s_prev_log_vprintf = esp_log_set_vprintf(usb_cdc_log_vprintf);
ESP_LOGI(TAG, "USB CDC logging enabled");
#endif
@@ -1085,6 +1100,7 @@ void USBHIDManager::sendSpellGamepadForSpell(const char *spell_name)
#if !USE_USB_HID_DEVICE
// Stub implementations when USB HID is disabled
// Note: setInvertMouseY() and getInvertMouseY() are inline in header - not redefined here
void USBHIDManager::sendMouseReport(int8_t x, int8_t y, int8_t wheel, uint8_t buttons) {}
void USBHIDManager::sendKeyboardReport(uint8_t modifiers, uint8_t keycode) {}
uint8_t USBHIDManager::getKeycodeForSpell(const char *spell_name) { return 0; }
@@ -1095,8 +1111,6 @@ bool USBHIDManager::loadSettings() { return true; }
bool USBHIDManager::saveSettings() { return true; }
bool USBHIDManager::resetSettings() { return true; }
void USBHIDManager::setMouseSensitivityValue(float sensitivity) {}
void USBHIDManager::setInvertMouseY(bool invert) {}
bool USBHIDManager::getInvertMouseY() const { return true; }
void USBHIDManager::updateGamepadFromGesture(float delta_x, float delta_y) {}
void USBHIDManager::setGamepadButtons(uint16_t buttons) {}
void USBHIDManager::setHidMode(HIDMode mode) {}
+684 -64
View File
@@ -5,7 +5,10 @@
#include "usb_hid.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_wifi.h"
#include "esp_spiffs.h"
#include <string.h>
#include <stdio.h>
// Forward declaration from main.cpp
#if USE_USB_HID_DEVICE
@@ -14,6 +17,49 @@ extern USBHIDManager usbHID;
static const char *TAG = "web_server";
// Helper to sanitize strings for JSON output (removes non-printable and non-UTF-8 chars)
static void sanitize_for_json(char *dest, const char *src, size_t max_len)
{
if (!src || !dest || max_len == 0)
{
if (dest && max_len > 0)
dest[0] = '\0';
return;
}
size_t j = 0;
for (size_t i = 0; src[i] != '\0' && j < max_len - 1; i++)
{
unsigned char c = src[i];
// Allow printable ASCII and basic UTF-8 continuation bytes
// Skip control characters, NULL, and invalid UTF-8
if (c >= 32 && c < 127) // Printable ASCII
{
// Escape JSON special characters
if (c == '"' || c == '\\' || c == '/')
{
if (j + 1 < max_len - 1)
{
dest[j++] = '\\';
dest[j++] = c;
}
}
else
{
dest[j++] = c;
}
}
else if (c >= 128) // Potential UTF-8 multi-byte character
{
// Simple UTF-8 validation: just accept bytes >= 128 as-is
// for proper UTF-8 sequences (conservative approach)
dest[j++] = c;
}
// Skip control characters (0-31, 127)
}
dest[j] = '\0';
}
// Helper for broadcasting WebSocket messages with auto-disconnect detection
static void broadcast_to_clients(httpd_handle_t server, int *clients, int *count,
SemaphoreHandle_t mutex, const char *data)
@@ -321,6 +367,102 @@ static const char index_html[] = R"rawliteral(
opacity: 0;
}
}
/* Spell Learning Overlay */
.spell-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
z-index: 9999;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s ease-out;
}
.spell-overlay.active {
display: flex;
}
.spell-overlay-content {
position: relative;
max-width: 90%;
max-height: 90%;
animation: scaleIn 0.4s ease-out;
}
.spell-overlay img {
max-width: 100%;
max-height: 90vh;
border-radius: 10px;
box-shadow: 0 8px 32px rgba(255, 215, 0, 0.4);
border: 3px solid #FFD700;
}
.spell-overlay-title {
position: absolute;
top: -50px;
left: 50%;
transform: translateX(-50%);
font-size: 2em;
color: #FFD700;
font-weight: bold;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.8);
white-space: nowrap;
}
.spell-overlay-close {
position: absolute;
top: -40px;
right: 0;
background: #d32f2f;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 1.2em;
font-weight: bold;
transition: background 0.3s;
}
.spell-overlay-close:hover {
background: #b71c1c;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes scaleIn {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.spell-learning-controls {
background: #333;
padding: 20px;
margin: 20px 0;
border-radius: 5px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.spell-learning-controls select {
flex: 1;
min-width: 250px;
padding: 12px;
background: #222;
color: #fff;
border: 1px solid #555;
border-radius: 5px;
font-size: 1em;
}
</style>
</head>
<body>
@@ -538,6 +680,16 @@ static const char index_html[] = R"rawliteral(
<div id="spell-display">Waiting for spell...</div>
</div>
<div class="ble-controls">
<h3>📚 Spell Learning</h3>
<div class="spell-learning-controls">
<select id="spell-selector">
<option value="">-- Select a spell to practice --</option>
</select>
<button class="button" onclick="practiceSpell()">✨ Practice Spell</button>
</div>
</div>
<h2 style="text-align: center; color: #4CAF50; margin-top: 30px;">Gesture Path</h2>
<canvas id="gesture-canvas" width="600" height="600"></canvas>
@@ -572,6 +724,15 @@ static const char index_html[] = R"rawliteral(
</div>
</div>
<!-- Spell Learning Overlay -->
<div id="spell-overlay" class="spell-overlay" onclick="closeSpellOverlay(event)">
<div class="spell-overlay-content">
<div class="spell-overlay-title" id="spell-overlay-title">Spell Name</div>
<button class="spell-overlay-close" onclick="closeSpellOverlay()">✕ Close</button>
<img id="spell-overlay-image" src="" alt="Spell Gesture">
</div>
</div>
<script>
const canvas = document.getElementById('imu-canvas');
const ctx = canvas.getContext('2d');
@@ -592,22 +753,6 @@ static const char index_html[] = R"rawliteral(
// WebSocket connection
let ws = null;
function showToast(message, type = 'success') {
const toast = document.createElement('div');
const safeType = type === 'error' ? 'error' : 'success';
toast.className = `toast ${safeType}`;
const icon = document.createElement('span');
icon.textContent = safeType === 'error' ? 'X' : 'V';
const text = document.createElement('span');
text.textContent = message;
toast.appendChild(icon);
toast.appendChild(text);
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
function connectWebSocket() {
const wsUrl = `ws://${window.location.host}/ws`;
@@ -884,6 +1029,80 @@ static const char index_html[] = R"rawliteral(
// Initialize gesture canvas
clearGestureCanvas();
// Spell Learning Functions
const SPELL_NAMES = [
"The_Force_Spell", "Colloportus", "Colloshoo", "The_Hour_Reversal_Reversal_Charm",
"Evanesco", "Herbivicus", "Orchideous", "Brachiabindo", "Meteolojinx", "Riddikulus",
"Silencio", "Immobulus", "Confringo", "Petrificus_Totalus", "Flipendo",
"The_Cheering_Charm", "Salvio_Hexia", "Pestis_Incendium", "Alohomora", "Protego",
"Langlock", "Mucus_Ad_Nauseum", "Flagrate", "Glacius", "Finite", "Anteoculatia",
"Expelliarmus", "Expecto_Patronum", "Descendo", "Depulso", "Reducto", "Colovaria",
"Aberto", "Confundo", "Densaugeo", "The_Stretching_Jinx", "Entomorphis",
"The_Hair_Thickening_Growing_Charm", "Bombarda", "Finestra", "The_Sleeping_Charm",
"Rictusempra", "Piertotum_Locomotor", "Expulso", "Impedimenta", "Ascendio",
"Incarcerous", "Ventus", "Revelio", "Accio", "Melefors", "Scourgify",
"Wingardium_Leviosa", "Nox", "Stupefy", "Spongify", "Lumos", "Appare_Vestigium",
"Verdimillious", "Fulgari", "Reparo", "Locomotor", "Quietus", "Everte_Statum",
"Incendio", "Aguamenti", "Sonorus", "Cantis", "Arania_Exumai", "Calvorio",
"The_Hour_Reversal_Charm", "Vermillious", "The_Pepper-Breath_Hex"
];
// Map spell names to SPIFFS filenames (32 char limit including .png)
// Some names are shortened to fit SPIFFS filename restrictions
const SPELL_FILENAME_MAP = {
"The_Hair_Thickening_Growing_Charm": "hair_grow_charm.png",
// Default: use lowercase with underscores
};
function spellNameToFilename(spellName) {
// Check if there's a custom mapping
if (SPELL_FILENAME_MAP[spellName]) {
return SPELL_FILENAME_MAP[spellName];
}
// Default: convert to lowercase
return spellName.toLowerCase() + '.png';
}
function populateSpellSelector() {
const selector = document.getElementById('spell-selector');
SPELL_NAMES.forEach(spell => {
const option = document.createElement('option');
option.value = spell;
option.textContent = spell.replace(/_/g, ' ');
selector.appendChild(option);
});
}
function practiceSpell() {
const selector = document.getElementById('spell-selector');
const selectedSpell = selector.value;
if (!selectedSpell) {
showToast('Please select a spell to practice', 'error');
return;
}
const filename = spellNameToFilename(selectedSpell);
const imageUrl = `/gesture/${filename}`;
// Update overlay
document.getElementById('spell-overlay-title').textContent = selectedSpell.replace(/_/g, ' ');
document.getElementById('spell-overlay-image').src = imageUrl;
// Show overlay
document.getElementById('spell-overlay').classList.add('active');
}
function closeSpellOverlay(event) {
// Close if clicking on overlay background or close button
if (!event || event.target.id === 'spell-overlay' || event.target.className.includes('spell-overlay-close')) {
document.getElementById('spell-overlay').classList.remove('active');
}
}
// Initialize spell selector on page load
populateSpellSelector();
// Toast notification function
function showToast(message, type = 'success') {
// Remove any existing toasts
@@ -1481,10 +1700,29 @@ static const char index_html[] = R"rawliteral(
// Load settings on page load
setTimeout(loadSettings, 2000);
setTimeout(loadHotspotSettings, 2000);
// Load stored MAC on page load
setTimeout(loadStoredMac, 1000);
// Load hotspot settings
function loadHotspotSettings() {
fetch('/hotspot/get')
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('hotspot-enabled').checked = data.enabled || false;
document.getElementById('hotspot-ssid').value = data.ssid || '';
document.getElementById('hotspot-password').value = data.password || '';
document.getElementById('hotspot-channel').value = data.channel || 6;
console.log('Hotspot settings loaded:', data);
}
})
.catch(error => {
console.error('Failed to load hotspot settings:', error);
});
}
// WiFi Management Functions
function scanWifi() {
const btn = event.target;
@@ -1683,10 +1921,51 @@ bool WebServer::begin(uint16_t port)
return true;
}
// Initialize SPIFFS for gesture images
ESP_LOGI(TAG, "Initializing SPIFFS for gesture images...");
esp_vfs_spiffs_conf_t spiffs_conf = {
.base_path = "/spiffs",
.partition_label = "spiffs",
.max_files = 5,
.format_if_mount_failed = true // Auto-format empty partition
};
esp_err_t ret = esp_vfs_spiffs_register(&spiffs_conf);
if (ret != ESP_OK)
{
if (ret == ESP_FAIL)
{
ESP_LOGW(TAG, "SPIFFS mount failed - partition may be corrupted or not flashed");
ESP_LOGW(TAG, "Run './upload_gestures.sh' to flash gesture images");
}
else if (ret == ESP_ERR_NOT_FOUND)
{
ESP_LOGW(TAG, "SPIFFS partition not found - check partition table");
ESP_LOGW(TAG, "Expected: offset=0x490000, size=0x370000 (3.6MB)");
}
else
{
ESP_LOGW(TAG, "SPIFFS init failed (%s) - gesture images unavailable", esp_err_to_name(ret));
}
}
else
{
size_t total = 0, used = 0;
ret = esp_spiffs_info("spiffs", &total, &used);
if (ret == ESP_OK)
{
ESP_LOGI(TAG, "SPIFFS: %d KB total, %d KB used", total / 1024, used / 1024);
if (used == 0)
{
ESP_LOGI(TAG, "SPIFFS is empty - run './upload_gestures.sh' to upload gesture images");
}
}
}
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = port;
config.max_open_sockets = 7;
config.max_uri_handlers = 20; // Support all handlers + buffer for future endpoints
config.max_uri_handlers = 25; // Support all handlers + buffer for future endpoints
config.lru_purge_enable = true;
if (httpd_start(&server, &config) != ESP_OK)
@@ -1894,6 +2173,19 @@ bool WebServer::begin(uint16_t port)
ESP_LOGW(TAG, "Hotspot settings handler registration FAILED");
}
httpd_uri_t hotspot_get = {
.uri = "/hotspot/get",
.method = HTTP_GET,
.handler = hotspot_get_handler,
.user_ctx = nullptr,
.is_websocket = false,
.handle_ws_control_frames = false,
.supported_subprotocol = nullptr};
if (httpd_register_uri_handler(server, &hotspot_get) != ESP_OK)
{
ESP_LOGW(TAG, "Hotspot get handler registration FAILED");
}
httpd_uri_t system_reboot = {
.uri = "/system/reboot",
.method = HTTP_POST,
@@ -1907,9 +2199,23 @@ bool WebServer::begin(uint16_t port)
ESP_LOGW(TAG, "System reboot handler registration FAILED");
}
// Gesture image handler (wildcard for /gesture/*.png)
httpd_uri_t gesture_image = {
.uri = "/gesture/*",
.method = HTTP_GET,
.handler = gesture_image_handler,
.user_ctx = nullptr,
.is_websocket = false,
.handle_ws_control_frames = false,
.supported_subprotocol = nullptr};
if (httpd_register_uri_handler(server, &gesture_image) != ESP_OK)
{
ESP_LOGW(TAG, "Gesture image handler registration FAILED");
}
running = true;
ESP_LOGI(TAG, "Web server started on port %d", port);
ESP_LOGI(TAG, "Registered endpoints: /, /ws, /generate_204, /hotspot-detect.html, /scan, /set_mac, /get_stored_mac, /connect, /disconnect, /settings/get, /settings/save, /settings/reset, /wifi/scan, /wifi/connect, /hotspot/settings, /system/reboot");
ESP_LOGI(TAG, "Registered endpoints: /, /ws, /generate_204, /hotspot-detect.html, /scan, /set_mac, /get_stored_mac, /connect, /disconnect, /settings/get, /settings/save, /settings/reset, /wifi/scan, /wifi/connect, /hotspot/settings, /hotspot/get, /system/reboot, /gesture/*");
return true;
}
@@ -2220,14 +2526,22 @@ void WebServer::broadcastLowConfidence(const char *spell_name, float confidence)
void WebServer::broadcastWandInfo(const char *firmware_version, const char *serial_number,
const char *sku, const char *device_id, const char *wand_type)
{
// Sanitize all input strings
char safe_fw[32], safe_serial[32], safe_sku[32], safe_devid[32], safe_type[32];
sanitize_for_json(safe_fw, firmware_version, sizeof(safe_fw));
sanitize_for_json(safe_serial, serial_number, sizeof(safe_serial));
sanitize_for_json(safe_sku, sku, sizeof(safe_sku));
sanitize_for_json(safe_devid, device_id, sizeof(safe_devid));
sanitize_for_json(safe_type, wand_type, sizeof(safe_type));
// Cache the wand info for new clients
if (xSemaphoreTake(data_mutex, pdMS_TO_TICKS(10)) == pdTRUE)
{
snprintf(cached_data.firmware_version, sizeof(cached_data.firmware_version), "%s", firmware_version ? firmware_version : "");
snprintf(cached_data.serial_number, sizeof(cached_data.serial_number), "%s", serial_number ? serial_number : "");
snprintf(cached_data.sku, sizeof(cached_data.sku), "%s", sku ? sku : "");
snprintf(cached_data.device_id, sizeof(cached_data.device_id), "%s", device_id ? device_id : "");
snprintf(cached_data.wand_type, sizeof(cached_data.wand_type), "%s", wand_type ? wand_type : "");
snprintf(cached_data.firmware_version, sizeof(cached_data.firmware_version), "%s", safe_fw);
snprintf(cached_data.serial_number, sizeof(cached_data.serial_number), "%s", safe_serial);
snprintf(cached_data.sku, sizeof(cached_data.sku), "%s", safe_sku);
snprintf(cached_data.device_id, sizeof(cached_data.device_id), "%s", safe_devid);
snprintf(cached_data.wand_type, sizeof(cached_data.wand_type), "%s", safe_type);
xSemaphoreGive(data_mutex);
}
@@ -2237,19 +2551,11 @@ void WebServer::broadcastWandInfo(const char *firmware_version, const char *seri
char json[1024];
snprintf(json, sizeof(json),
"{\"type\":\"wand_info\",\"firmware\":\"%s\",\"serial\":\"%s\",\"sku\":\"%s\",\"device_id\":\"%s\",\"wand_type\":\"%s\"}",
firmware_version ? firmware_version : "",
serial_number ? serial_number : "",
sku ? sku : "",
device_id ? device_id : "",
wand_type ? wand_type : "");
safe_fw, safe_serial, safe_sku, safe_devid, safe_type);
broadcast_to_clients(server, ws_clients, &ws_client_count, client_mutex, json);
ESP_LOGI(TAG, "Wand info broadcast: FW=%s, Serial=%s, SKU=%s, DevID=%s, Type=%s",
firmware_version ? firmware_version : "--",
serial_number ? serial_number : "--",
sku ? sku : "--",
device_id ? device_id : "--",
wand_type ? wand_type : "--");
safe_fw, safe_serial, safe_sku, safe_devid, safe_type);
}
void WebServer::broadcastButtonPress(bool b1, bool b2, bool b3, bool b4)
@@ -2272,10 +2578,15 @@ void WebServer::broadcastScanResult(const char *address, const char *name, int r
if (!running || !address || !name)
return;
char sanitized_name[64];
char sanitized_address[24];
sanitize_for_json(sanitized_name, name, sizeof(sanitized_name));
sanitize_for_json(sanitized_address, address, sizeof(sanitized_address));
char json[256];
snprintf(json, sizeof(json),
"{\"type\":\"scan_result\",\"address\":\"%s\",\"name\":\"%s\",\"rssi\":%d}",
address, name, rssi);
sanitized_address, sanitized_name, rssi);
broadcast_to_clients(server, ws_clients, &ws_client_count, client_mutex, json);
}
@@ -2530,32 +2841,32 @@ esp_err_t WebServer::settings_get_handler(httpd_req_t *req)
char mqtt_broker[128] = {0};
char mqtt_username[64] = {0};
char mqtt_password[64] = {0};
if (err == ESP_OK)
{
uint8_t ha_mqtt_u8 = 1;
nvs_get_u8(nvs_handle, "ha_mqtt_enabled", &ha_mqtt_u8);
ha_mqtt_enabled = (ha_mqtt_u8 != 0);
size_t required_size;
err = nvs_get_str(nvs_handle, "mqtt_broker", NULL, &required_size);
if (err == ESP_OK && required_size <= sizeof(mqtt_broker))
{
nvs_get_str(nvs_handle, "mqtt_broker", mqtt_broker, &required_size);
}
err = nvs_get_str(nvs_handle, "mqtt_username", NULL, &required_size);
if (err == ESP_OK && required_size <= sizeof(mqtt_username))
{
nvs_get_str(nvs_handle, "mqtt_username", mqtt_username, &required_size);
}
err = nvs_get_str(nvs_handle, "mqtt_password", NULL, &required_size);
if (err == ESP_OK && required_size <= sizeof(mqtt_password))
{
nvs_get_str(nvs_handle, "mqtt_password", mqtt_password, &required_size);
}
nvs_close(nvs_handle);
}
@@ -2568,7 +2879,7 @@ esp_err_t WebServer::settings_get_handler(httpd_req_t *req)
i < 72 ? "," : "");
}
offset += snprintf(buffer + offset, buffer_size - offset,
offset += snprintf(buffer + offset, buffer_size - offset,
"], \"ha_mqtt_enabled\": %s, \"mqtt_broker\": \"%s\", \"mqtt_username\": \"%s\", \"mqtt_password\": \"%s\"}",
ha_mqtt_enabled ? "true" : "false",
mqtt_broker,
@@ -2904,20 +3215,201 @@ esp_err_t WebServer::settings_reset_handler(httpd_req_t *req)
esp_err_t WebServer::wifi_scan_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "wifi_scan_handler called!");
// TODO: Implement actual WiFi scanning using ESP-IDF WiFi APIs
// For now, return a placeholder response
// Get current WiFi mode
wifi_mode_t current_mode;
esp_err_t err = esp_wifi_get_mode(&current_mode);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to get WiFi mode: %s", esp_err_to_name(err));
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\":false,\"message\":\"Failed to get WiFi mode\",\"networks\":[]}");
return ESP_OK;
}
// If in AP-only mode, switch to APSTA mode temporarily
bool mode_changed = false;
if (current_mode == WIFI_MODE_AP)
{
ESP_LOGI(TAG, "Switching from AP to APSTA mode for scanning");
err = esp_wifi_set_mode(WIFI_MODE_APSTA);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set APSTA mode: %s", esp_err_to_name(err));
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\":false,\"message\":\"Failed to set scan mode\",\"networks\":[]}");
return ESP_OK;
}
mode_changed = true;
}
// Configure scan to find all available APs
wifi_scan_config_t scan_config = {};
scan_config.ssid = NULL;
scan_config.bssid = NULL;
scan_config.channel = 0;
scan_config.show_hidden = false;
scan_config.scan_type = WIFI_SCAN_TYPE_ACTIVE;
scan_config.scan_time.active.min = 100;
scan_config.scan_time.active.max = 300;
scan_config.scan_time.passive = 0;
// Start WiFi scan (blocking)
err = esp_wifi_scan_start(&scan_config, true);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "WiFi scan failed: %s", esp_err_to_name(err));
// Restore original mode if we changed it
if (mode_changed)
{
esp_wifi_set_mode(WIFI_MODE_AP);
}
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\":false,\"message\":\"Scan failed\",\"networks\":[]}");
return ESP_OK;
}
// Get number of APs found
uint16_t ap_count = 0;
esp_wifi_scan_get_ap_num(&ap_count);
ESP_LOGI(TAG, "Found %d access points", ap_count);
// Restore original mode if we changed it
if (mode_changed)
{
ESP_LOGI(TAG, "Restoring AP mode");
esp_wifi_set_mode(WIFI_MODE_AP);
}
if (ap_count == 0)
{
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\":true,\"networks\":[]}");
return ESP_OK;
}
// Limit to max 20 networks to avoid memory issues
if (ap_count > 20)
{
ap_count = 20;
}
// Allocate memory for AP records
wifi_ap_record_t *ap_records = (wifi_ap_record_t *)malloc(sizeof(wifi_ap_record_t) * ap_count);
if (!ap_records)
{
ESP_LOGE(TAG, "Failed to allocate memory for AP records");
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\":false,\"message\":\"Memory allocation failed\",\"networks\":[]}");
return ESP_OK;
}
// Get AP records
err = esp_wifi_scan_get_ap_records(&ap_count, ap_records);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to get AP records: %s", esp_err_to_name(err));
free(ap_records);
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\":false,\"message\":\"Failed to get records\",\"networks\":[]}");
return ESP_OK;
}
// Build JSON response
char *response = (char *)malloc(8192);
if (!response)
{
free(ap_records);
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\":false,\"message\":\"Memory allocation failed\",\"networks\":[]}");
return ESP_OK;
}
int offset = snprintf(response, 8192, "{\"success\":true,\"networks\":[");
for (int i = 0; i < ap_count; i++)
{
const char *auth_mode = "OPEN";
switch (ap_records[i].authmode)
{
case WIFI_AUTH_OPEN:
auth_mode = "OPEN";
break;
case WIFI_AUTH_WEP:
auth_mode = "WEP";
break;
case WIFI_AUTH_WPA_PSK:
auth_mode = "WPA";
break;
case WIFI_AUTH_WPA2_PSK:
auth_mode = "WPA2";
break;
case WIFI_AUTH_WPA_WPA2_PSK:
auth_mode = "WPA/WPA2";
break;
case WIFI_AUTH_WPA3_PSK:
auth_mode = "WPA3";
break;
case WIFI_AUTH_WPA2_WPA3_PSK:
auth_mode = "WPA2/WPA3";
break;
default:
auth_mode = "UNKNOWN";
break;
}
// Escape any special characters in SSID
char escaped_ssid[64];
const char *src = (const char *)ap_records[i].ssid;
char *dst = escaped_ssid;
int max_len = sizeof(escaped_ssid) - 1;
while (*src && max_len > 1)
{
if (*src == '"' || *src == '\\')
{
if (max_len > 2)
{
*dst++ = '\\';
max_len--;
}
}
*dst++ = *src++;
max_len--;
}
*dst = '\0';
offset += snprintf(response + offset, 8192 - offset,
"%s{\"ssid\":\"%s\",\"rssi\":%d,\"auth\":\"%s\",\"channel\":%d}",
i > 0 ? "," : "",
escaped_ssid,
ap_records[i].rssi,
auth_mode,
ap_records[i].primary);
if (offset >= 8000)
break;
}
offset += snprintf(response + offset, 8192 - offset, "]}");
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\":true,\"networks\":[]}");
ESP_LOGI(TAG, "WiFi scan not yet implemented - returning empty list");
httpd_resp_sendstr(req, response);
free(response);
free(ap_records);
ESP_LOGI(TAG, "WiFi scan completed successfully with %d networks", ap_count);
return ESP_OK;
}
esp_err_t WebServer::wifi_connect_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "wifi_connect_handler called!");
// Read the POST body
int content_len = req->content_len;
char *buffer = (char *)malloc(content_len + 1);
@@ -2937,11 +3429,11 @@ esp_err_t WebServer::wifi_connect_handler(httpd_req_t *req)
buffer[content_len] = 0;
ESP_LOGI(TAG, "Received WiFi connect request: %s", buffer);
// Parse SSID and password
char ssid[32] = {0};
char password[64] = {0};
char *ssid_ptr = strstr(buffer, "\"ssid\":");
if (ssid_ptr)
{
@@ -2962,7 +3454,7 @@ esp_err_t WebServer::wifi_connect_handler(httpd_req_t *req)
}
}
}
char *password_ptr = strstr(buffer, "\"password\":");
if (password_ptr)
{
@@ -2983,7 +3475,7 @@ esp_err_t WebServer::wifi_connect_handler(httpd_req_t *req)
}
}
}
// Save WiFi credentials to NVS
if (strlen(ssid) > 0)
{
@@ -2998,11 +3490,11 @@ esp_err_t WebServer::wifi_connect_handler(httpd_req_t *req)
ESP_LOGI(TAG, "WiFi credentials saved to NVS: SSID=%s", ssid);
}
}
// TODO: Actually connect to WiFi using ESP-IDF WiFi APIs
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\":true,\"message\":\"WiFi credentials saved. Restart to connect.\"}");
free(buffer);
return ESP_OK;
}
@@ -3010,7 +3502,7 @@ esp_err_t WebServer::wifi_connect_handler(httpd_req_t *req)
esp_err_t WebServer::hotspot_settings_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "hotspot_settings_handler called!");
// Read the POST body
int content_len = req->content_len;
char *buffer = (char *)malloc(content_len + 1);
@@ -3030,13 +3522,13 @@ esp_err_t WebServer::hotspot_settings_handler(httpd_req_t *req)
buffer[content_len] = 0;
ESP_LOGI(TAG, "Received hotspot settings: %s", buffer);
// Parse settings
bool enabled = (strstr(buffer, "\"enabled\":true") != NULL);
char ssid[32] = {0};
char password[64] = {0};
int channel = 1;
char *ssid_ptr = strstr(buffer, "\"ssid\":");
if (ssid_ptr)
{
@@ -3057,7 +3549,7 @@ esp_err_t WebServer::hotspot_settings_handler(httpd_req_t *req)
}
}
}
char *password_ptr = strstr(buffer, "\"password\":");
if (password_ptr)
{
@@ -3078,13 +3570,13 @@ esp_err_t WebServer::hotspot_settings_handler(httpd_req_t *req)
}
}
}
char *channel_ptr = strstr(buffer, "\"channel\":");
if (channel_ptr)
{
sscanf(channel_ptr, "\"channel\":%d", &channel);
}
// Save hotspot settings to NVS
nvs_handle_t nvs_handle;
esp_err_t err = nvs_open("storage", NVS_READWRITE, &nvs_handle);
@@ -3104,25 +3596,153 @@ esp_err_t WebServer::hotspot_settings_handler(httpd_req_t *req)
nvs_close(nvs_handle);
ESP_LOGI(TAG, "Hotspot settings saved: enabled=%d, SSID=%s, channel=%d", enabled, ssid, channel);
}
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\":true,\"message\":\"Hotspot settings saved. Restart to apply.\"}");
free(buffer);
return ESP_OK;
}
esp_err_t WebServer::hotspot_get_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "hotspot_get_handler called!");
// Load hotspot settings from NVS
char hotspot_ssid[32] = {0};
char hotspot_password[64] = {0};
uint8_t hotspot_channel = 6;
bool hotspot_enabled = false;
nvs_handle_t nvs_handle;
esp_err_t err = nvs_open("storage", NVS_READONLY, &nvs_handle);
if (err == ESP_OK)
{
uint8_t enabled = 0;
nvs_get_u8(nvs_handle, "hotspot_enabled", &enabled);
hotspot_enabled = (enabled != 0);
size_t required_size;
err = nvs_get_str(nvs_handle, "hotspot_ssid", NULL, &required_size);
if (err == ESP_OK && required_size > 0 && required_size <= sizeof(hotspot_ssid))
{
nvs_get_str(nvs_handle, "hotspot_ssid", hotspot_ssid, &required_size);
}
err = nvs_get_str(nvs_handle, "hotspot_password", NULL, &required_size);
if (err == ESP_OK && required_size > 0 && required_size <= sizeof(hotspot_password))
{
nvs_get_str(nvs_handle, "hotspot_password", hotspot_password, &required_size);
}
uint8_t channel = 6;
nvs_get_u8(nvs_handle, "hotspot_channel", &channel);
if (channel >= 1 && channel <= 13)
{
hotspot_channel = channel;
}
nvs_close(nvs_handle);
}
// Build JSON response
char response[512];
snprintf(response, sizeof(response),
"{\"success\":true,\"enabled\":%s,\"ssid\":\"%s\",\"password\":\"%s\",\"channel\":%d}",
hotspot_enabled ? "true" : "false",
hotspot_ssid,
hotspot_password,
hotspot_channel);
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, response);
return ESP_OK;
}
esp_err_t WebServer::system_reboot_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "system_reboot_handler called! Rebooting in 2 seconds...");
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\":true,\"message\":\"Rebooting device...\"}");
// Schedule reboot after a short delay to allow response to be sent
vTaskDelay(pdMS_TO_TICKS(2000));
esp_restart();
return ESP_OK;
}
esp_err_t WebServer::gesture_image_handler(httpd_req_t *req)
{
// Extract spell name from URI: /gesture/<spell_name>.png
const char *uri = req->uri;
const char *prefix = "/gesture/";
size_t prefix_len = strlen(prefix);
if (strncmp(uri, prefix, prefix_len) != 0)
{
httpd_resp_send_404(req);
return ESP_FAIL;
}
// Get filename (spell_name.png)
const char *filename_start = uri + prefix_len;
char filepath[128];
snprintf(filepath, sizeof(filepath), "/spiffs/%s", filename_start);
ESP_LOGI(TAG, "Serving gesture image: %s", filepath);
// Open file from SPIFFS
FILE *file = fopen(filepath, "r");
if (!file)
{
ESP_LOGW(TAG, "Gesture image not found: %s", filepath);
httpd_resp_send_404(req);
return ESP_FAIL;
}
// Get file size
fseek(file, 0, SEEK_END);
size_t file_size = ftell(file);
fseek(file, 0, SEEK_SET);
// Set content type and headers
httpd_resp_set_type(req, "image/png");
httpd_resp_set_hdr(req, "Cache-Control", "public, max-age=86400"); // Cache for 1 day
// Allocate buffer for file transfer
char *buffer = (char *)malloc(1024);
if (!buffer)
{
fclose(file);
httpd_resp_send_500(req);
return ESP_FAIL;
}
// Stream file to client
size_t bytes_remaining = file_size;
while (bytes_remaining > 0)
{
size_t chunk_size = (bytes_remaining > 1024) ? 1024 : bytes_remaining;
size_t bytes_read = fread(buffer, 1, chunk_size, file);
if (bytes_read > 0)
{
httpd_resp_send_chunk(req, buffer, bytes_read);
bytes_remaining -= bytes_read;
}
else
{
break; // EOF or error
}
}
// Finish response
httpd_resp_send_chunk(req, NULL, 0);
free(buffer);
fclose(file);
return ESP_OK;
}
+123
View File
@@ -0,0 +1,123 @@
#!/bin/bash
# Upload gesture images to SPIFFS partition on ESP32-S3
# This creates a SPIFFS image and flashes it to the device
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GESTURES_DIR="$SCRIPT_DIR/gestures"
SPIFFS_IMAGE="$SCRIPT_DIR/build/spiffs.bin"
# ESP32-S3 SPIFFS configuration (from partitions-s3.csv)
# SPIFFS partition: 0x490000 to 0x800000 (3.6MB)
SPIFFS_OFFSET=0x490000
SPIFFS_SIZE=0x370000
BLOCK_SIZE=4096
PAGE_SIZE=256
echo "=== ESP32 Gesture Images SPIFFS Uploader ==="
echo "Gestures directory: $GESTURES_DIR"
echo "SPIFFS offset: $SPIFFS_OFFSET"
echo "SPIFFS size: $SPIFFS_SIZE"
echo ""
# Check if gestures directory exists
if [ ! -d "$GESTURES_DIR" ]; then
echo "ERROR: Gestures directory not found: $GESTURES_DIR"
exit 1
fi
# Count PNG files
PNG_COUNT=$(find "$GESTURES_DIR" -name "*.png" | wc -l)
echo "Found $PNG_COUNT gesture images"
if [ $PNG_COUNT -eq 0 ]; then
echo "WARNING: No PNG files found in gestures directory"
exit 1
fi
# Find spiffsgen.py
SPIFFSGEN_PATH=""
if [ -n "$IDF_PATH" ] && [ -f "$IDF_PATH/components/spiffs/spiffsgen.py" ]; then
SPIFFSGEN_PATH="$IDF_PATH/components/spiffs/spiffsgen.py"
elif command -v idf.py &> /dev/null; then
# Try to find ESP-IDF through idf.py
IDF_PY_PATH=$(which idf.py)
POSSIBLE_IDF_PATH=$(dirname $(dirname "$IDF_PY_PATH"))
if [ -f "$POSSIBLE_IDF_PATH/components/spiffs/spiffsgen.py" ]; then
SPIFFSGEN_PATH="$POSSIBLE_IDF_PATH/components/spiffs/spiffsgen.py"
fi
fi
if [ -z "$SPIFFSGEN_PATH" ]; then
echo "ERROR: spiffsgen.py not found."
echo "Make sure ESP-IDF is properly installed and sourced:"
echo " source ~/esp/esp-idf/export.sh"
echo "Or set IDF_PATH environment variable."
exit 1
fi
echo "Using spiffsgen.py: $SPIFFSGEN_PATH"
echo ""
# Create build directory if it doesn't exist
mkdir -p "$(dirname "$SPIFFS_IMAGE")"
# Create SPIFFS image
echo "Creating SPIFFS image..."
python3 "$SPIFFSGEN_PATH" \
$SPIFFS_SIZE \
"$GESTURES_DIR" \
"$SPIFFS_IMAGE" \
--page-size $PAGE_SIZE \
--block-size $BLOCK_SIZE
if [ ! -f "$SPIFFS_IMAGE" ]; then
echo "ERROR: Failed to create SPIFFS image"
exit 1
fi
echo "✓ SPIFFS image created: $SPIFFS_IMAGE"
# Flash SPIFFS image to device
PORT="${1:-/dev/ttyACM0}"
echo ""
echo "Flashing SPIFFS image to device..."
echo "Port: $PORT"
echo ""
# Check if port exists
if [ ! -e "$PORT" ]; then
echo "WARNING: Port $PORT not found!"
echo "Available ports:"
ls -1 /dev/ttyACM* /dev/ttyUSB* 2>/dev/null || echo " No serial ports found"
echo ""
read -p "Enter port (or press Enter to skip flashing): " MANUAL_PORT
if [ -n "$MANUAL_PORT" ]; then
PORT="$MANUAL_PORT"
else
echo "Skipping flash. You can flash manually with:"
echo " esptool.py --chip esp32s3 --port $PORT write_flash $SPIFFS_OFFSET $SPIFFS_IMAGE"
exit 0
fi
fi
echo "Put device in bootloader mode if needed:"
echo " 1. Hold BOOT button"
echo " 2. Press and release RESET button"
echo " 3. Release BOOT button"
echo ""
set +e # Disable exit on error for interactive prompt
read -p "Press Enter to start flashing..." || true
set -e # Re-enable exit on error
if command -v esptool.py &> /dev/null; then
esptool.py --chip esp32s3 --port "$PORT" write_flash $SPIFFS_OFFSET "$SPIFFS_IMAGE"
else
echo "ERROR: esptool.py not found. Install with: pip install esptool"
exit 1
fi
echo ""
echo "=== Upload complete! ==="
echo "Gesture images are now available on the device."