From 4f37389cc5cf9f9a0576414b722a51022bb222d2 Mon Sep 17 00:00:00 2001 From: Penki Date: Wed, 27 May 2026 09:39:39 -0300 Subject: [PATCH] first commit --- README.md | 323 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ee68bc --- /dev/null +++ b/README.md @@ -0,0 +1,323 @@ +# ESP Project (Modernizado) + +Sistema para gestionar entidades, sectores, dispositivos y puertos con frontend en HTML/TailwindCSS + Bootstrap y backend PHP con router, respuestas JSON unificadas, variables de entorno y MQTT desde backend. Estados en tiempo real vía Server-Sent Events (SSE). + +## Cambios recientes (2025-09-17) + +- Login: agregado checkbox "Recordar" (Tailwind). Si está activo, se guardan cookies `remember_user` y `remember_pass` de larga duración y se prellenan en el próximo acceso. +- Íconos: se centralizó el uso de SVG en `assets/iconos.json` y se inyectan automáticamente vía `layouts/layout.php` (`window.loadIcons()` y `window.applyIcons()`). +- Dashboard: se preserva la IP entre refresh empleando `lastKnownIP`. Se añadió botón "Mqtt" que abre un modal con JSON del dispositivo (estado SSE, IP, pines y firmware si está disponible). +- Firmware: se añadió el campo `firmware` en `data/dispositivos.json`. Tras OTA exitosa (detectada por SSE `reboot`), se persiste la versión del manifest vía `POST /api/edit_dispositivo`. + +## Requisitos +- PHP 8.0+ +- Composer +- Broker MQTT accesible (TLS opcional) + + +## Variables de Entorno (.env) +- MQTT_HOST, MQTT_PORT, MQTT_TLS +- MQTT_USERNAME, MQTT_PASSWORD, MQTT_CLIENT_ID +- CORS_ALLOW_ORIGIN (por defecto `*`) +- API_BASIC_USER, API_BASIC_PASS (para endpoints que lo requieran) + +Ejemplo en `/.env.example`. + + +## Arquitectura + - Presentación: `index.php` (router raíz) que renderiza vistas PHP en `paginas/` con `layouts/layout.php`. + - Páginas clave: `paginas/index.php` (lista y gestión), `paginas/dashboard.php` (detalle), `paginas/puertos.php`, `paginas/flash.php`, `paginas/login.php`. + - Frontend: `assets/js/layout.js`, `assets/js/apiClient.js`, `assets/js/auth.js`, CSS en `assets/css/layout.css`. + - Íconos: `assets/iconos.json` + utilidades globales en `layouts/layout.php`. + - API/Router: `api/index.php` con rutas vía `?r=...`. + - Bootstrap API: `api/bootstrap.php` (CORS, helpers JSON, Dotenv). Si se define `NON_JSON`, no fuerza Content-Type JSON (usado por SSE). + - MQTT Backend: + - Publicar: `api/mqtt_publish.php` usando `php-mqtt/client`. + - Estados SSE: `api/sse_states.php` se suscribe a MQTT y emite eventos `state`, `reboot`, `pin` e `ip`. + - Datos: Archivos JSON (`/data/*.json`). + +## Endpoints (router `api/index.php`) +- Autenticación: salvo `login` y `sse_states`, todas las rutas requieren sesión activa (`api/login`). +- GET `api/index.php?r=get_dispositivos` +- POST `api/index.php?r=add_dispositivo` +- POST `api/index.php?r=edit_dispositivo` +- POST `api/index.php?r=update_dispositivo` +- POST `api/index.php?r=delete_dispositivo` +- GET `api/index.php?r=get_sectores` +- POST `api/index.php?r=add_sector` +- POST `api/index.php?r=update_sector` +- POST `api/index.php?r=delete_sector` +- GET `api/index.php?r=get_puertos` +- POST `api/index.php?r=update_puertos` +- POST `api/index.php?r=asignar_puertos` +- POST `api/index.php?r=mqtt_publish` (publicación MQTT backend) +- POST `api/index.php?r=add_entidad` +- GET `api/index.php?r=sse_states` (SSE: stream de estados desde backend) +- POST `api/index.php?r=login` +- POST `api/index.php?r=logout` +- GET `api/index.php?r=me` + +### Detalles de endpoints + +1) get_dispositivos +- Metodo: GET +- URL: `api/index.php?r=get_dispositivos` +- Body: ninguno +- Respuesta: objeto cuyas claves son `chipid` y valor el dispositivo +```json +{ + "A1B2C3": {"id_disp":1,"chipid":"A1B2C3","tag":"X","nombre":"Y","ciudad":"Z","sector":"S","firmware":"12.9.2024.2222"} +} +``` + +2) add_dispositivo +- Método: POST (JSON) +- URL: `api/index.php?r=add_dispositivo` +- Body: +```json +{"chipid":"A1B2C3","tag":"X","nombre":"Y","ciudad":"Z","sector":"S"} +``` +- Respuesta: `{ "success": true }` o error 400/500 con mensaje. + +3) edit_dispositivo +- Método: POST (JSON) +- URL: `api/index.php?r=edit_dispositivo` +- Body ejemplo: +```json +{"chipid":"A1B2C3","tag":"nuevo","nombre":"modelo","ciudad":"CABA","sector":"Oficina"} +``` +- Respuesta: +```json +{"success":true} +``` + +4) update_dispositivo +- Método: POST (JSON) +- URL: `api/index.php?r=update_dispositivo` +- Body: objeto `{ chipid: { ...campos } }` por cada dispositivo a actualizar. +- Respuesta: `{ "success": true }` o error 500. + +5) delete_dispositivo +- Método: POST (JSON) +- URL: `api/index.php?r=delete_dispositivo` +- Body: +```json +{"chipid":"A1B2C3"} +``` +- Respuesta (éxito): +```json +{"success":true,"msg":"Dispositivo eliminado correctamente","backup":"backups/dispositivos_backup_YYYYmmdd_HHMMSS.json"} +``` + +6) get_sectores +- Método: GET +- URL: `api/index.php?r=get_sectores` +- Respuesta: arreglo de strings +```json +["Oficina","Deposito"] +``` + +7) add_sector +- Método: POST (JSON) +- URL: `api/index.php?r=add_sector` +- Body: +```json +{"sector":"Oficina"} +``` +- Respuesta: `{ "success": true }` o `{ "success": false, "msg": "..." }` + +8) update_sector +- Método: POST (JSON) +- URL: `api/index.php?r=update_sector` +- Body: +```json +{"old_sector":"Oficina","new_sector":"Oficina 2"} +``` +- Respuesta: `{ "success": true }` o error con `msg`. + +9) delete_sector +- Método: POST (JSON) +- URL: `api/index.php?r=delete_sector` +- Body: +```json +{"sector":"Oficina"} +``` +- Respuesta: `{ "success": true }` o error con `msg`. + +10) get_puertos +- Método: GET +- URL: `api/index.php?r=get_puertos&chipid=CHIPID` +- Respuesta (éxito): objeto de configuración por puerto o `{"error":"..."}` si falta `chipid` o no existe. + +11) update_puertos +- Método: POST (JSON) +- URL: `api/index.php?r=update_puertos` +- Body ejemplo (solo actualiza campos existentes): +```json +{ + "A1B2C3": { + "D1": {"disp":"Luz Hall","notas":"","modo":"OUTPUT"}, + "D2": {"disp":"Sensor","notas":"","modo":"INPUT_PULLUP"} + } +} +``` +- Respuesta: `{ "success": true, "message": "Puertos actualizados con éxito" }` + +12) asignar_puertos +- Método: POST (JSON) +- URL: `api/index.php?r=asignar_puertos` +- Body ejemplo (crea/actualiza entradas de puertos; define `gpio`, `disp`, `modo`, `notas`): +```json +{ + "A1B2C3": { + "D1": {"gpio":5, "disp":"Luz Hall", "modo":"OUTPUT", "notas":""}, + "D2": {"gpio":4, "disp":"Sensor", "modo":"INPUT_PULLUP", "notas":"ok"} + } +} +``` +- Respuesta: `{ "success": true, "message": "Dispositivo actualizado con éxito" }` + +13) mqtt_publish +- Método: POST (JSON) +- URL: `api/index.php?r=mqtt_publish` +- Body: +```json +{"topic":"dispositivo/CHIPID/comando/reboot","payload":"1","qos":0,"retain":false} +``` +- Respuesta: `{ "success": true, "data": {"topic":"...","payload":"..."} }` o error 500 con mensaje. + +14) sse_states +- Método: GET (stream SSE) +- URL: `api/index.php?r=sse_states` +- Eventos emitidos: + - `state`: `{ "chipid": "A1B2C3", "estado": "online|offline|..." }` + - `reboot`: `{ "chipid": "A1B2C3", "payload": "0|1" }` + - `pin`: `{ "chipid": "A1B2C3", "alias": "D1", "valor": "ON|OFF|..." }` + - `ip`: `{ "chipid": "A1B2C3", "ip": "192.168.0.10" }` + - Notas: el servidor envía pings cada ~2s. El cliente debe reconectar ante errores. + +15) login +- Método: POST (JSON) +- URL: `api/index.php?r=login` +- Body: +```json +{"usuario":"api","clave":"API#2025"} +``` +- Respuesta: `{ "success": true, "data": { "user": "...", "rol": "admin", "session_id": "..." } }` + +16) logout +- Método: POST (JSON) +- URL: `api/index.php?r=logout` +- Respuesta: `{ "success": true }` + +17) me +- Método: GET +- URL: `api/index.php?r=me` +- Respuesta: `{ "success": true, "data": { "user": "...", "rol": "..." } }` + +18) add_entidad +- Método: POST (JSON) +- URL: `api/index.php?r=add_entidad` +- Body: +```json +{"entidad":"Lampara"} +``` +- Respuesta (éxito): +```json +{"success":true} +``` +- Respuesta (error de validación): +```json +{"success":false,"message":"No se proporcionó entidad."} +``` +- Notas: + - Persiste en `data/entidades.json` agregando al arreglo `entidades`. + - Actualmente no valida duplicados ni tipo, pendiente endurecer validaciones. + +## Formatos de Respuesta +La mayoría de las respuestas (no-SSE) usan: +```json +{ + "success": true, + "message": "OK", + "data": { /* payload */ } +} +``` +En error: +```json +{ + "success": false, + "message": "Error descriptivo", + "data": null +} +``` +- Nota: Algunos GET (p. ej. `get_dispositivos`, `get_sectores`, `get_puertos`) devuelven arreglos/objetos crudos y, en caso de error, pueden responder con formas simples (por ejemplo `{ "error": "..." }`). + +## MQTT desde backend +- Publicar (ejemplo curl): +```bash +curl -X POST "http://127.0.0.1:8000/api/index.php?r=mqtt_publish" \ + -H "Content-Type: application/json" \ + -d '{"topic":"dispositivo/123/comando/reboot","payload":"1","qos":0,"retain":false}' +``` +- Requisitos: variables de entorno MQTT configuradas en `.env`. + +## SSE (Estados en tiempo real) +- Frontend (`index.html`) usa: +```js +const es = new EventSource('api/index.php?r=sse_states'); +es.addEventListener('state', (ev) => { /* actualiza UI */ }); +es.addEventListener('reboot', (ev) => { /* reactiva botón */ }); +``` +- Backend: `api/sse_states.php` se conecta al broker, reemite como SSE y envía pings cada ~2s. +- Reconexión: el cliente reintenta al caer; el servidor mantiene `ignore_user_abort(true)`. + +## Seguridad + - Sesiones PHP para la interfaz web (`index.php` enruta a `paginas/` según sesión). + - Endpoints `login`, `logout` y `me` para autenticación; el resto de rutas requieren sesión activa (excepto `login` y `sse_states`). + - No expongas credenciales MQTT en el frontend. + - Ajusta `CORS_ALLOW_ORIGIN` en producción. + - TLS: `sse_states.php`/`mqtt_publish.php` permiten certs self-signed (`setTlsSelfSignedAllowed(true)`); ajustar según tu CA. + +## Páginas y funcionalidades del sitio + +- Dashboard de dispositivos (`paginas/index.php`): + - Listado con estado online/offline en tiempo real (SSE/MQTT). + - CRUD básico de dispositivos (crear, editar, eliminar) y gestión de sectores. + - Acción de reinicio remoto vía MQTT. +- Dashboard individual (`paginas/dashboard.php`): + - Estado de conexión, última actividad y IP del dispositivo (IP se preserva entre refresh y se actualiza por SSE `ip`). + - Visualización de estado de pines y controles rápidos ON/OFF (vía MQTT) para salidas configuradas. + - Botón "Mqtt" que abre modal con JSON informativo (estado SSE, IP, pines y firmware si está disponible). +- Gestión de puertos (`paginas/puertos.php`): + - Asignación de alias, modo y notas por pin/placa. +- Flasheo de firmware (`paginas/flash.php`): + - Integración con ESP Web Tools y Web Serial para flasheo USB en el navegador. + - Manifiesto: `assets/flash/esp8266-manifest.json` (usa `version` y `builds[].parts[].path`). + - Tras OTA exitosa (SSE `reboot`), se persiste la versión de firmware en `data/dispositivos.json` vía `POST /api/edit_dispositivo`. + +## Compilación de firmware (PlatformIO) + +- Requisitos: Python 3.12+, PlatformIO Core (pio). +- Script: `tools/build_firmware.ps1` compila `firmware/` y copia artefactos a `assets/flash/firmware/`. +- Artefactos esperados: + - `firmware/.pio/build/nodemcuv2/firmware.bin` + - `assets/flash/firmware/esp8266-nodemcuv2-YYYYMMDD-HHMMSS.bin` + +## Cómo ejecutar en local + +1) Configura `.env` (ver Variables de Entorno). +2) Instala dependencias PHP con Composer: + - `composer install` +3) Sirve el proyecto con un servidor PHP o Apache/Nginx que respete `.htaccess`: + - PHP embebido: `php -S 127.0.0.1:8000 -t .` +4) Accede a `http://127.0.0.1:8000/` y loguéate. + +Notas: +- Recomendado PHP 8.0+ (probado en 8.x). Compatible con >=7.4 según dependencias. +- Asegúrate de que el servidor permita conexiones largas para SSE. + +## Troubleshooting +- "Fallo al publicar MQTT": revisa conectividad al broker y credenciales `.env`. +- SSE no conecta: verifica que el servidor PHP permita procesos largos y no cierre conexiones por proxy/reverse-proxy. +- CORS: ajustar `CORS_ALLOW_ORIGIN`.