From 3a80d5e76bc8c66099436809b13ddd0cce97cc37 Mon Sep 17 00:00:00 2001 From: javiservices Date: Thu, 18 Jun 2026 12:32:35 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20documentar=20deploy=20autom=C3=A1tico?= =?UTF-8?q?=20+=20firmware=20ESP32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 200 ++++++++++++-------------------------- docs/DEPLOY.md | 244 +++++++++++++++++++++++++++++++++++++++++++++++ docs/FIRMWARE.md | 189 ++++++++++++++++++++++++++++++++++++ 3 files changed, 492 insertions(+), 141 deletions(-) create mode 100644 docs/DEPLOY.md create mode 100644 docs/FIRMWARE.md diff --git a/README.md b/README.md index fc15d73..6fd0a03 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,76 @@ -# DummyBot Backend +# DummyBot -API REST y UI web para **DummyBot** — un bot físico de tareas para personas con TDAH, basado en ESP32 + ST7789 + caritas kawaii. +Bot físico de tareas para personas con TDAH, basado en ESP32 + OLED + 3 botones. -Stack: **Laravel 12 · PHP 8.4 · MySQL 8 · Sanctum · Tailwind (CDN) + Alpine.js (CDN)**. +- **Stack backend**: Laravel 12 · PHP 8.4 · MySQL 8 · Sanctum · Tailwind (CDN) + Alpine.js (CDN) +- **Stack firmware**: ESP32 (Arduino framework) · OLED SSD1306 128x64 · TTP223 buttons · vibrador +- **Deploy**: automático vía webhook de Gitea en push a `main` ---- +## Estructura -## 🚀 Setup rápido +``` +dummybot/ +├── app/ # Laravel: controllers, models +├── bootstrap/ # Laravel: bootstrap + middleware config +├── config/ # Laravel: app, db, sanctum, etc. +├── database/ # Migraciones y seeders +├── docs/ # Documentación técnica +│ ├── DEPLOY.md # Pipeline de deploy automático +│ └── FIRMWARE.md # Hardware, pines, flasheo +├── firmware/ # PlatformIO + ESP32 Arduino +│ ├── platformio.ini +│ ├── partitions.csv +│ └── src/main.cpp # Firmware v6.1 +├── resources/ # Blade views + CSS +│ ├── css/ +│ ├── js/ +│ └── views/ +│ ├── layouts/base.blade.php +│ └── pages/ +│ ├── app.blade.php +│ ├── auth.blade.php +│ ├── home.blade.php +│ └── settings.blade.php +├── routes/ +│ ├── api.php +│ └── web.php +├── deploy-runner.sh # Script que ejecuta el webhook +├── docker-compose.yml +├── Dockerfile +└── README.md +``` + +## Documentación adicional + +- [`docs/DEPLOY.md`](docs/DEPLOY.md) — Pipeline de deploy automático +- [`docs/FIRMWARE.md`](docs/FIRMWARE.md) — Hardware, cableado, flasheo del ESP32 +- [`firmware/README.md`](firmware/README.md) — Detalles de la build PlatformIO + +## Quick start (desarrollo local) ```bash -# 1. Crear BD -mysql -u root -p -> CREATE DATABASE dummybot CHARACTER SET utf8mb4; -> CREATE USER 'dummybot'@'localhost' IDENTIFIED BY 'TU_PASSWORD'; -> GRANT ALL ON dummybot.* TO 'dummybot'@'localhost'; - -# 2. Configurar +# Backend +composer install cp .env.example .env -# Edita DB_PASSWORD, APP_URL, etc. - -# 3. Instalar + migrar -composer install --no-dev --optimize-autoloader php artisan key:generate php artisan migrate - -# 4. Arrancar php artisan serve --host=0.0.0.0 --port=8000 + +# Firmware +cd firmware +pio run -t upload --upload-port /dev/ttyUSB0 ``` -Abre `http://localhost:8000/`. Crea una cuenta, ve a **Ajustes → Dispositivos** y crea tu primer DummyBot para obtener un token. +## Despliegue -## 🐳 Docker - -```bash -AUTO_MIGRATE=1 docker compose up -d --build -``` - -Esto levanta `dummybot_app` (PHP-FPM), `dummybot_web` (nginx) y `dummybot_db` (MySQL 8). El puerto 80 queda interno (el proxy externo ya está gestionado en producción). - -## 🧠 Modelo de datos - -| Tabla | Descripción | -|------------|-------------| -| `users` | Usuarios (auth vía Sanctum) | -| `tasks` | Tareas (`title`, `description`, `priority`, `estimate_min`, `deadline`, `recurrence`, `done`, `snooze_until`, `done_at`, `project_id`, `user_id`) | -| `projects` | Agrupa tareas (color + icono) | -| `tags` | Etiquetas transversales (muchas a muchas con tasks) | -| `devices` | Cada ESP32 con su propio `token` y `last_seen_at` | - -## 🔌 API REST - -Todas las rutas devuelven JSON. Auth vía `Authorization: Bearer `. - -### Auth (web) -``` -POST /api/auth/register {name, email, password, password_confirmation} -POST /api/auth/login {email, password} -POST /api/auth/logout (auth) -GET /api/auth/me (auth) -``` - -### Tareas (web, con sesión o token Sanctum) -``` -GET /api/tasks Listar (con project + tags) -POST /api/tasks Crear -PUT /api/tasks/{id} Editar -DELETE /api/tasks/{id} Borrar -POST /api/tasks/{id}/done Marcar como hecha (genera recurrente si aplica) -POST /api/tasks/{id}/snooze Posponer {minutes: N} -POST /api/tasks/{id}/restore Reabrir -GET /api/tasks/stats {pending, today, done_today, overdue, total_done} -``` - -### Proyectos -``` -GET /api/projects -POST /api/projects {name, color, icon} -PUT /api/projects/{id} -DELETE /api/projects/{id} -``` - -### Tags -``` -GET /api/tags -POST /api/tags {name, color} -DELETE /api/tags/{id} -``` - -### Dispositivos (DummyBots) -``` -GET /api/devices Tus DummyBots -POST /api/devices {name, hardware?, firmware_version?} → devuelve token (solo 1 vez) -POST /api/devices/{id}/rotate Invalida y crea nuevo token -DELETE /api/devices/{id} -``` - -### Para el ESP32 (auth.device, con token del dispositivo) -``` -GET /api/device/tasks/pending Polling cada 30s -POST /api/device/tasks Crear tarea (desde bot, ej. comandos) -POST /api/device/tasks/{id}/done -POST /api/device/tasks/{id}/snooze -DELETE /api/device/tasks/{id} -POST /api/device/heartbeat Ping (refresca last_seen_at) -``` - -## 🎨 Frontend (Blade + Alpine) - -- `pages/home.blade.php` — landing -- `pages/login.blade.php` / `pages/register.blade.php` — auth -- `pages/app.blade.php` — **app principal** (lista tareas, filtros, atajos teclado) -- `pages/settings.blade.php` — gestión de dispositivos, proyectos, tags - -### Atajos de teclado (en `/app`) - -- `n` → nueva tarea -- `/` → buscador -- `Esc` → cerrar modal -- `1` / `2` / `3` / `4` → filtro Todas / Pendientes / Hoy / Hechas -- `s` → snooze 5 min (sobre tarea enfocada) - -### TDAH-friendly - -- **Sin gamificación tóxica** (sin puntos, sin streaks). -- **No judgemental**: "sin tareas" muestra un emoji festivo, no un mensaje triste. -- **Dark mode por defecto** con gradientes suaves. -- **Atajos de teclado** para no tener que tocar el ratón. -- **Filtros contextuales** (Hoy, Vencidas, Por proyecto) para evitar el agobio del "todo". - -## 🔌 Webhooks (TODO) - -Pendiente: hook de salida cuando se crea/marca una tarea. Pensado para integrarse con Notion, Zapier, n8n, etc. - -## 📋 Importar / Exportar (TODO) - -Endpoints para CSV/JSON de tareas. - -## 🚀 Despliegue en producción - -Ya está dockerizado. El repo está en `https://git.anabolictrack.com/javiservices/dummybot`. - -```bash -# En el server -cd /root/dummybot -git pull -AUTO_MIGRATE=1 docker compose up -d --build -``` - -Para que el proxy-manager lo exponga en HTTPS, añadir host en `http://157.180.77.232:81` apuntando al contenedor `dummybot_web:80`. - -## Licencia - -MIT +Ver [`docs/DEPLOY.md`](docs/DEPLOY.md). Push a `main` → deploy automático en +~30 segundos. +## API +Ver `resources/views/pages/settings.blade.php` (sección "API") o el README +original. +## Hardware +Ver [`docs/FIRMWARE.md`](docs/FIRMWARE.md) para cableado y pinout completo. diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 0000000..b85640c --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,244 @@ +# Despliegue automático + +DummyBot se despliega automáticamente a producción cuando se hace push a la rama +`main` del repositorio. + +``` +push a main + → Gitea dispara webhook "deploy" + → POST http://dummybot_expose:80/webhook/deploy (red Docker interna) + → Laravel valida HMAC-SHA256 (X-Hub-Signature-256) + → exec('nohup deploy-runner.sh &') + → git pull + composer install + npm ci + npm run build + → php artisan migrate --force + → php artisan optimize + → Healthcheck GET /up + → Log en storage/logs/deploy-YYYYMMDD-HHMMSS.log +``` + +Tiempo total: ~25-35 segundos desde push hasta server actualizado. + +--- + +## Arquitectura + +``` +┌─────────────────────┐ +│ git.anabolictrack │ Gitea 1.22.6 +│ (rootless, 3000) │ - Repos: dummybot, dummybot-firmware +└──────────┬──────────┘ + │ webhook push event + │ POST http://dummybot_expose:80/webhook/deploy + ▼ +┌─────────────────────┐ +│ 157.180.77.232:80 │ nginx-proxy-manager (172.20.0.2) +└──────────┬──────────┘ + │ red "proxy" (172.20.0.0/16) + ▼ +┌─────────────────────┐ ┌──────────────────────┐ +│ dummybot_expose │───▶│ dummybot_web (nginx) │ +│ socat :80→web:80 │ │ /var/www/public │ +│ (172.20.0.8) │ └──────────┬───────────┘ +└─────────────────────┘ │ + ▼ + ┌──────────────────────┐ + │ dummybot_app (php-fpm)│ + │ php artisan │ + │ /var/www │ + └──────────┬───────────┘ + │ + ┌──────────▼───────────┐ + │ dummybot_db (mysql) │ + └──────────────────────┘ +``` + +**Red Docker `proxy`** (172.20.0.0/16): comparten gitea, dummybot_web, +dummybot_expose, nginx-proxy-manager. Gitea hace POST al webhook usando +`http://dummybot_expose:80/webhook/deploy` — resolución DNS por nombre de +container, no atraviesa UFW ni la interfaz pública. + +**Volúmenes clave**: +- `/root/dummybot:/var/www` (en el container) — el repo clonado, mismo path + en host y container. +- `/root/dummybot.env` — backup del `.env` real (no se commitea). +- `/root/dummybot/storage/logs/` — logs de deploy. + +--- + +## Webhook de Gitea + +**Endpoint**: `POST /webhook/deploy` +**Auth**: HMAC-SHA256 con secreto compartido en header `X-Hub-Signature-256`. +**Trigger**: push a rama `main`. +**Branch filter**: `main` (configurado en Gitea). + +**Validación en Laravel** (`app/Http/Controllers/WebhookController.php`): +1. Lee `DEPLOY_WEBHOOK_SECRET` del `.env`. +2. Calcula `hash_hmac('sha256', body, secret)` y compara con header. +3. Decodifica payload, comprueba `ref == "refs/heads/main"`. +4. Lanza `nohup bash deploy-runner.sh > log 2>&1 &`. +5. Responde 202 con path al log. + +**Excluido de CSRF** en `bootstrap/app.php`: +```php +$middleware->validateCsrfTokens(except: ['webhook/*']); +``` + +--- + +## Configuración inicial del server + +### 1. SSH key para acceso de emergencia + +```bash +ssh-keygen -t ed25519 -C "deploy-key" -f ~/.ssh/dummybot_deploy -N "" +ssh-copy-id -i ~/.ssh/dummybot_deploy.pub root@157.180.77.232 +``` + +### 2. Respaldo del .env + +```bash +# En el server, una vez esté configurado +cp /root/dummybot/.env /root/dummybot.env +``` + +El deploy restaura `.env` desde `/root/dummybot.env` en cada push. + +### 3. Generar secreto del webhook + +```bash +openssl rand -hex 32 # → "75f02729eecd38c9..." +``` + +Guardarlo en dos sitios: +- `/root/dummybot/.env`: `DEPLOY_WEBHOOK_SECRET=` +- Configuración del webhook en Gitea (al crearlo). + +### 4. Configurar Gitea para permitir webhooks a hosts internos + +Gitea 1.22 tiene whitelist de hosts (`webhook.ALLOWED_HOST_LIST`). Por defecto +está vacía y rechaza cualquier host. Editar `/etc/gitea/app.ini`: + +```ini +[webhook] +ALLOWED_HOST_LIST = * +SKIP_TLS_VERIFY = false +``` + +Y reiniciar: `docker restart gitea`. + +### 5. Crear el webhook en Gitea + +```bash +SECRET=$(cat /root/dummybot-webhook-secret) + +curl -X POST "https://git.anabolictrack.com/api/v1/repos/javiservices/dummybot/hooks" \ + -u "javiservices:" \ + -H "Content-Type: application/json" \ + -d "{ + \"type\": \"gitea\", + \"config\": { + \"url\": \"http://dummybot_expose:80/webhook/deploy\", + \"content_type\": \"json\", + \"secret\": \"$SECRET\" + }, + \"events\": [\"push\"], + \"active\": true, + \"branch_filter\": \"main\" + }" +``` + +**Importante**: la URL debe usar el **nombre del container** (`dummybot_expose`) +no la IP pública. Si el container se renombra, hay que actualizar el webhook. + +### 6. Primer deploy manual + +Como el endpoint `/webhook/deploy` no existe hasta que se despliega por primera +vez, hay que hacerlo manualmente: + +```bash +ssh root@157.180.77.232 +cd /root/dummybot +git fetch origin main +git reset --hard origin/main +cp /root/dummybot.env .env +docker compose exec -T app php artisan migrate --force +docker compose exec -T app php artisan config:clear route:clear view:clear optimize +docker compose up -d --remove-orphans +curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18080/up # debe ser 200 +``` + +Tras este primer deploy, los siguientes son automáticos. + +--- + +## Verificación + +### Probar manualmente (sin push) + +```bash +SECRET=$(cat /root/dummybot-webhook-secret) +BODY='{"ref":"refs/heads/main","pusher":{"name":"manual"}}' +SIG="sha256=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')" + +curl -X POST "http://157.180.77.232:18080/webhook/deploy" \ + -H "Content-Type: application/json" \ + -H "X-Hub-Signature-256: $SIG" \ + -d "$BODY" +``` + +Respuesta esperada: `{"ok":true,"status":"deploy started","log":"deploy-..."}` con HTTP 202. + +### Ver logs + +```bash +# Logs de deploy +ls -lat /root/dummybot/storage/logs/deploy-*.log | head + +# Log de un deploy concreto +tail -50 /root/dummybot/storage/logs/deploy-20260618-102042.log + +# Laravel general +tail -f /root/dummybot/storage/logs/laravel.log +``` + +### Probar webhook desde Gitea + +```bash +curl -X POST "https://git.anabolictrack.com/api/v1/repos/javiservices/dummybot/hooks/1/tests" \ + -u "javiservices:" +``` + +HTTP 204 = OK. Si hay error de entrega, ver logs del container gitea: +`docker logs gitea --tail 50`. + +--- + +## Troubleshooting + +| Síntoma | Causa | Solución | +|---------|-------|----------| +| Push no dispara deploy | `webhook.ALLOWED_HOST_LIST` no configurado | Editar `/etc/gitea/app.ini`, reiniciar gitea | +| HTTP 419 al webhook | CSRF no excluido | Revisar `bootstrap/app.php` | +| HTTP 401 al webhook | Firma inválida | Verificar `DEPLOY_WEBHOOK_SECRET` en server y Gitea | +| Webhook devuelve "skipped: branch != main" | Bug `substr($ref, 10)` debe ser `11` | Actualizar `WebhookController.php` | +| Deploy log dice "Deploy OK" pero server tiene código viejo | `php artisan optimize` no recarga | Reiniciar container: `docker compose restart app` | +| `docker compose up` falla con "container name already in use" | Container huérfano de un deploy anterior | `docker rm -f dummybot_expose` y reintentar | + +--- + +## Por qué webhook y no Actions + +Gitea 1.22.6 tiene Actions parcial: +- ✓ Registro de runners funciona +- ✓ API de secrets/variables funciona +- ✗ API de dispatch devuelve 404 +- ✗ Ejecución automática de workflows no se dispara + +Gitea Actions se estabilizó en 1.23+. El webhook de Gitea es la solución +estándar pre-Actions y funciona en cualquier versión. + +Si en el futuro se actualiza Gitea, el workflow `.gitea/workflows/deploy.yml` +ya está listo y se puede activar configurando el secreto `DEPLOY_SSH_KEY` en +Gitea (la SSH key pública ya está en el server en +`/root/.ssh/authorized_keys`). diff --git a/docs/FIRMWARE.md b/docs/FIRMWARE.md new file mode 100644 index 0000000..6cac6e2 --- /dev/null +++ b/docs/FIRMWARE.md @@ -0,0 +1,189 @@ +# Firmware del ESP32 + +Firmware para el bot físico DummyBot. ESP32 + OLED SSD1306 128x64 + WiFi ++ polling backend. + +## Hardware + +- **ESP32-D0WD-V3** (DevKitC / WROOM-32, 4 MB flash) +- **Pantalla OLED SSD1306 0.96"** (128x64, I2C) +- **3 botones** (NEXT / DONE / SNOOZE) +- **1 motor vibrador 1027** (3V DC, MOSFET en GPIO 25) + +### Pinout + +| Función | GPIO | Notas | +|------------------|-------|--------------------------------------| +| OLED SDA | 21 | I2C data | +| OLED SCL | 22 | I2C clock | +| OLED addr | 0x3C | I2C address | +| Botón NEXT | 26 | INPUT_PULLDOWN, flanco ascendente | +| Botón DONE | 27 | INPUT_PULLDOWN, flanco ascendente | +| Botón SNOOZE | 14 | INPUT_PULLDOWN, flanco ascendente | +| Vibrador | 25 | OUTPUT, MOSFET gate | +| BOOT (reset cfg) | 0 | INPUT_PULLUP, mantener 5s al arrancar | + +### Cableado OLED + +``` +ESP32 (WROOM-32) OLED SSD1306 0.96" +───────────────────── ───────────────────── +3V3 ───────▶ VDD +GND ───────▶ GND +GPIO 22 ───────▶ SCK (I2C SCL) +GPIO 21 ───────▶ SDA (I2C SDA) +``` + +### Cableado botones + +Los botones se conectan entre GND y el GPIO correspondiente. Con +`INPUT_PULLDOWN` interno del ESP32, el GPIO lee LOW en reposo y HIGH al +pulsar. El firmware detecta el flanco ascendente con debounce de 200 ms. + +``` +ESP32 Botón (entre pin y GND) +───── ───── +GPIO 26 ──────▶ Siguiente +GPIO 27 ──────▶ Hecha +GPIO 14 ──────▶ Snooze 5 min +GND ──────▶ Común +``` + +### Cableado vibrador + +``` +ESP32 MOSFET (AO3400 / 2N7000) Vibrador 1027 +───── ────── ───── +GPIO 25 ──[100Ω]──▶ Gate + │ + ├──▶ Source ──▶ GND + │ + └──▶ Drain ──▶ Vibrador ──▶ 3V3 +``` + +Si solo se dispone de NPN (BC547 / 2N2222 / S8050), esquema alternativo: +motor entre 3V3 y colector, resistencia 1kΩ en base. + +## Build flags (platformio.ini) + +```ini +build_flags = + -DCORE_DEBUG_LEVEL=3 + -DUSER_SETUP_LOADED=1 + -DDEFAULT_WIFI_SSID=\"PHARMACO\" + -DDEFAULT_WIFI_PASS=\"arnoldgym2010\" + -DBACKEND_URL=\"http://157.180.77.232:18080\" + -DDEFAULT_DEVICE_TOKEN=\"\" +``` + +## Build y flasheo + +```bash +# Compilar +pio run + +# Flashear +pio run -t upload --upload-port /dev/ttyUSB0 + +# Monitor serie +pio device monitor --port /dev/ttyUSB0 --baud 115200 +``` + +## Configuración en runtime (captive portal) + +Si no hay config WiFi válida en NVS, el bot abre un AP `DummyBot-Setup` +(pass `dummybot`) en `192.168.4.1`. Desde el móvil, configurar WiFi y token +del dispositivo. El bot se reinicia automáticamente y conecta. + +Para resetear la config: mantener pulsado el botón **BOOT (GPIO 0)** durante +5 segundos al arrancar. + +## Comandos serie (debug) + +Conectar a `115200 baud`: + +| Comando | Acción | +|---------|-------------------------------------------------| +| `S` | Imprime estado (WiFi, IP, tarea actual) | +| `B0` | Simula pulsación botón NEXT (GPIO 26) | +| `B1` | Simula pulsación botón DONE (GPIO 27) | +| `B2` | Simula pulsación botón SNOOZE (GPIO 14) | +| `P` | Lee estado actual de los 3 pines de botón | +| `A` | Lectura continua del ADC en GPIO 34 (60s) | +| `C` | Calibración guiada de umbrales ADC (3 botones) | +| `W` | Watch: monitor rápido de cambios en pines | + +## Arquitectura del firmware + +```cpp +// Namespace principales +namespace Config { void load(); void save(); void clear(); } +namespace UI { showBoot, showConnecting, showIdle, showTask, showPortal } +namespace Vib { buzz(), tick() — patrón 2-buzz al recibir tarea nueva } +namespace Api { pollPending(), markDone(), snooze() } +namespace Input { tick() — polling 3 botones con debounce por flanco } +namespace Portal { start(), stop(), tick() — AP + DNS + WebServer } +namespace Wifi { wifiStartSTA(), wifiTick() — máquina de estados } +``` + +### Máquina de estados WiFi + +``` +BOOT → CONNECTING (timeout 20s) + ├─ OK → CONNECTED (poll cada 30s) + └─ FAIL → PORTAL (AP DummyBot-Setup) + +CONNECTED + └─ WiFi perdido → CONNECTING + +PORTAL + └─ ESP.restart() al guardar config +``` + +### Flujo de polling + +``` +CONNECTED + → Api::pollPending() cada 30s + → GET /api/device/tasks/pending + → Si tiene tareas: muestra primera en OLED + → Si cambia id: buzz (vibrador) +``` + +## Troubleshooting + +### WiFi no conecta + +- Verificar SSID y password (son case-sensitive). +- Si el router está en 5 GHz, el ESP32 solo soporta 2.4 GHz. +- Si la red bloquea nuevos dispositivos (MAC filtering), añadir la MAC del + ESP32 (visible en el log de `WiFi.status()`). + +### Botones no responden + +- Verificar que el botón hace contacto entre el GPIO y GND. +- Comandos serie `P` muestra el estado actual de los pines. Pulsar y ver + si cambia de 0 a 1. +- Si el botón usa active-low (HIGH en reposo, LOW al pulsar), cambiar a + `INPUT_PULLUP` y la lógica de flanco a descendente. + +### OLED no enciende + +- Verificar 3V3 en VDD del OLED. +- Verificar SDA (GPIO 21) y SCL (GPIO 22) — pines fijos I2C del ESP32. +- I2C addr por defecto 0x3C. Si la OLED es 0x3D, cambiar `OLED_ADDR`. + +### API no responde + +- Verificar que el bot tiene acceso a internet (ping al backend desde el + ESP32 con el comando serie `S`). +- Si el backend usa HTTPS con cert auto-firmado, hay que añadir el cert al + firmware (más complejo) o usar HTTP plano. + +## Versiones + +- **v6.1** (actual): producción, sin botones (pendiente nueva placa). + Funcionalidad core: WiFi, OLED, polling, portal cautivo. +- **v6.0** (legacy): intentona con ADC para matriz de botones, no funcionó + en hardware del usuario. +- **v5** (legacy): SPIFFS en lugar de NVS, varios bugs en defaults WiFi.