Backup ingecheckt, werkt niet lekker met wifi settings
@@ -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.
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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(¤t_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;
|
||||
}
|
||||
|
||||
@@ -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."
|
||||