// https://www.hivemq.com/mqtt/public-mqtt-broker/
// Configuración WiFi
//const char* ssid = "Wokwi-GUEST";
//const char* password = "";
/**
* ╔══════════════════════════════════════════════════════════════╗
* ║ SMART AGRO CON ESP32 — Monitoreo y Control ║
* ║ Proyecto académico nivel básico-intermedio ║
* ╠══════════════════════════════════════════════════════════════╣
* ║ Sensores : DHT11 (GPIO 32) · HC-SR04 (TRIG 14 / ECHO 12) ║
* ║ Display : OLED SSD1306 128×64 I2C (SDA=21, SCL=22) ║
* ║ Actuadores: LED Sala (27) · Relay Puerta (26) ║
* ║ Relay Bomba (25) · Relay Portón (33) ║
* ║ Red WiFi : "BRAVO" (abierta, sin contraseña) ║
* ║ Servidor : ESPAsyncWebServer puerto 80 ║
* ╠══════════════════════════════════════════════════════════════╣
* ║ Librerías necesarias (instalar desde Library Manager): ║
* ║ · ESPAsyncWebServer (me-no-dev) ║
* ║ · AsyncTCP (me-no-dev) ║
* ║ · DHT sensor library (Adafruit) ║
* ║ · Adafruit SSD1306 ║
* ║ · Adafruit GFX Library ║
* ╚══════════════════════════════════════════════════════════════╝
*/
// ──────────────────────────────────────────────
// 1. INCLUSIÓN DE LIBRERÍAS Y ARCHIVO DE IMÁGENES
// ──────────────────────────────────────────────
#include <WiFi.h>
#include <ESPAsyncWebServer.h> // Servidor web asíncrono
#include <DHT.h> // Sensor de temperatura y humedad
#include <Wire.h> // Protocolo I2C (OLED)
#include <Adafruit_GFX.h> // Gráficos base para OLED
#include <Adafruit_SSD1306.h> // Driver pantalla OLED SSD1306
#include "myimages.h" // Íconos bitmap y Base64 web
// ──────────────────────────────────────────────
// 2. CONFIGURACIÓN DE RED WiFi
// ──────────────────────────────────────────────
const char* WIFI_SSID = "Wokwi-GUEST"; // Red abierta (sin contraseña)
const char* WIFI_PASS = ""; // Vacío = sin contraseña
// ──────────────────────────────────────────────
// 3. DEFINICIÓN DE PINES
// ──────────────────────────────────────────────
// — Sensor DHT11 —
#define DHT_PIN 32 // GPIO 32 → Data DHT11
#define DHT_TYPE DHT11
// — Sensor ultrasónico HC-SR04 —
#define TRIG_PIN 14 // GPIO 14 → TRIG (disparo)
#define ECHO_PIN 12 // GPIO 12 → ECHO (recepción)
// — Actuadores —
#define PIN_LUZ_SALA 27 // GPIO 27 → LED Luz Sala
#define PIN_LUZ_PUERTA 26 // GPIO 26 → Relay Luz Puerta
#define PIN_BOMBA 25 // GPIO 25 → Relay Bomba de riego
#define PIN_PORTON 33 // GPIO 33 → Relay Portón principal
// — LED de alerta por distancia < 50 cm —
#define PIN_ALERTA 2 // GPIO 2 → LED interno ESP32 (o externo)
// ──────────────────────────────────────────────
// 4. CONSTANTES DEL SISTEMA
// ──────────────────────────────────────────────
#define OLED_WIDTH 128 // Ancho pantalla OLED en px
#define OLED_HEIGHT 64 // Alto pantalla OLED en px
#define OLED_RESET -1 // Reset pin (-1 = usa pin reset Arduino)
#define OLED_ADDRESS 0x3C // Dirección I2C del SSD1306
#define DIST_ALERTA 50.0f // Umbral de alerta en centímetros
#define ICONO_W 15 // Ancho íconos OLED en px
#define ICONO_H 20 // Alto íconos OLED en px
// Intervalos de tiempo (non-blocking con millis())
#define INTERVALO_SENSORES 2000UL // Leer sensores cada 2 s
#define INTERVALO_OLED 1000UL // Actualizar OLED cada 1 s
#define INTERVALO_BLINK 300UL // Parpadeo LED alerta cada 300 ms
// ──────────────────────────────────────────────
// 5. INSTANCIAS DE OBJETOS
// ──────────────────────────────────────────────
DHT dht(DHT_PIN, DHT_TYPE);
Adafruit_SSD1306 oled(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);
AsyncWebServer server(80); // Servidor HTTP en puerto 80
// ──────────────────────────────────────────────
// 6. VARIABLES GLOBALES DE ESTADO
// ──────────────────────────────────────────────
// — Datos de sensores —
float temperatura = 0.0f;
float humedad = 0.0f;
float distancia = 0.0f;
bool alertaDist = false; // true cuando distancia < DIST_ALERTA
// — Estado de actuadores (false = APAGADO) —
bool estadoLuzSala = false;
bool estadoLuzPuerta = false;
bool estadoBomba = false;
bool estadoPorton = false;
// — Marcas de tiempo para millis() —
unsigned long tAnteriorSensores = 0;
unsigned long tAnteriorOLED = 0;
unsigned long tAnteriorBlink = 0;
bool estadoBlink = false; // Estado actual del parpadeo
// ──────────────────────────────────────────────
// 7. PROTOTIPOS DE FUNCIONES
// ──────────────────────────────────────────────
void inicializarPines();
void conectarWiFi();
void inicializarOLED();
void inicializarServidor();
void leerSensores();
void medirDistancia();
void controlarLuzSala(bool estado);
void controlarLuzPuerta(bool estado);
void controlarBomba(bool estado);
void controlarPorton(bool estado);
void actualizarOLED();
void manejarAlerta();
String generarHTML();
String estadoStr(bool s);
// ════════════════════════════════════════════════
// SETUP
// ════════════════════════════════════════════════
void setup() {
Serial.begin(115200);
Serial.println("\n╔═══════════════════════════╗");
Serial.println("║ Smart Agro ESP32 - Boot ║");
Serial.println("╚═══════════════════════════╝");
inicializarPines(); // Configurar GPIOs
dht.begin(); // Inicializar sensor DHT11
inicializarOLED(); // Inicializar pantalla OLED
conectarWiFi(); // Conectar a red "BRAVO"
inicializarServidor();// Configurar y arrancar servidor web
Serial.println("[OK] Sistema listo.");
}
// ════════════════════════════════════════════════
// LOOP PRINCIPAL
// ════════════════════════════════════════════════
void loop() {
unsigned long ahora = millis();
// — Leer sensores cada INTERVALO_SENSORES ms —
if (ahora - tAnteriorSensores >= INTERVALO_SENSORES) {
tAnteriorSensores = ahora;
leerSensores(); // Lee DHT11
medirDistancia(); // Lee HC-SR04
}
// — Actualizar OLED cada INTERVALO_OLED ms —
if (ahora - tAnteriorOLED >= INTERVALO_OLED) {
tAnteriorOLED = ahora;
actualizarOLED();
}
// — Manejar parpadeo de LED de alerta —
manejarAlerta();
}
// ════════════════════════════════════════════════
// SECCIÓN A: INICIALIZACIÓN
// ════════════════════════════════════════════════
/**
* inicializarPines()
* Configura todos los GPIOs como salida o entrada según corresponda.
* Los relays y el LED se inicializan en LOW (apagado).
*/
void inicializarPines() {
// Actuadores como salida, inicialmente apagados
pinMode(PIN_LUZ_SALA, OUTPUT); digitalWrite(PIN_LUZ_SALA, LOW);
pinMode(PIN_LUZ_PUERTA, OUTPUT); digitalWrite(PIN_LUZ_PUERTA, LOW);
pinMode(PIN_BOMBA, OUTPUT); digitalWrite(PIN_BOMBA, LOW);
pinMode(PIN_PORTON, OUTPUT); digitalWrite(PIN_PORTON, LOW);
pinMode(PIN_ALERTA, OUTPUT); digitalWrite(PIN_ALERTA, LOW);
// HC-SR04: TRIG como salida, ECHO como entrada
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
Serial.println("[OK] Pines inicializados.");
}
/**
* conectarWiFi()
* Conecta el ESP32 a la red WiFi "BRAVO" (abierta).
* Espera hasta 10 s antes de continuar.
*/
void conectarWiFi() {
Serial.printf("[WiFi] Conectando a '%s'...\n", WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASS);
uint8_t intentos = 0;
while (WiFi.status() != WL_CONNECTED && intentos < 20) {
delay(500);
Serial.print(".");
intentos++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\n[WiFi] Conectado. IP: %s\n", WiFi.localIP().toString().c_str());
} else {
Serial.println("\n[WiFi] ERROR: No se pudo conectar. Verificar red.");
}
}
/**
* inicializarOLED()
* Configura la pantalla OLED SSD1306 por I2C.
*/
void inicializarOLED() {
if (!oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
Serial.println("[OLED] ERROR: SSD1306 no encontrado.");
return;
}
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
oled.setTextSize(1);
oled.setCursor(10, 25);
oled.println("Smart Agro ESP32");
oled.setCursor(20, 40);
oled.println("Iniciando...");
oled.display();
Serial.println("[OK] OLED inicializada.");
}
// ════════════════════════════════════════════════
// SECCIÓN B: LECTURA DE SENSORES
// ════════════════════════════════════════════════
/**
* leerSensores()
* Lee temperatura y humedad del DHT11.
* Mantiene el último valor válido si la lectura falla.
*/
void leerSensores() {
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) {
temperatura = t;
humedad = h;
Serial.printf("[DHT11] Temp: %.1f°C Hum: %.1f%%\n", temperatura, humedad);
} else {
Serial.println("[DHT11] WARN: Lectura fallida, reintentando...");
}
}
/**
* medirDistancia()
* Emite un pulso ultrasónico y calcula la distancia en cm.
* Fórmula: distancia = (duracion * 0.034) / 2
* Activa la bandera alertaDist si la distancia < DIST_ALERTA.
*/
void medirDistancia() {
// Pulso de disparo de 10 µs
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
// Medir duración del pulso de eco (timeout 30 ms)
long duracion = pulseIn(ECHO_PIN, HIGH, 30000UL);
if (duracion > 0) {
distancia = (duracion * 0.034f) / 2.0f;
alertaDist = (distancia < DIST_ALERTA);
Serial.printf("[HC-SR04] Distancia: %.1f cm Alerta: %s\n",
distancia, alertaDist ? "SI" : "NO");
} else {
Serial.println("[HC-SR04] WARN: Sin eco, objeto fuera de rango.");
}
}
// ════════════════════════════════════════════════
// SECCIÓN C: CONTROL DE ACTUADORES
// ════════════════════════════════════════════════
/**
* controlarLuzSala(estado)
* Activa/desactiva el LED de la sala (GPIO 27).
* @param estado true = encendido, false = apagado
*/
void controlarLuzSala(bool estado) {
estadoLuzSala = estado;
digitalWrite(PIN_LUZ_SALA, estado ? HIGH : LOW);
Serial.printf("[ACT] Luz Sala → %s\n", estado ? "ON" : "OFF");
}
/**
* controlarLuzPuerta(estado)
* Activa/desactiva el relay de la luz de puerta (GPIO 26).
*/
void controlarLuzPuerta(bool estado) {
estadoLuzPuerta = estado;
digitalWrite(PIN_LUZ_PUERTA, estado ? HIGH : LOW);
Serial.printf("[ACT] Luz Puerta → %s\n", estado ? "ON" : "OFF");
}
/**
* controlarBomba(estado)
* Activa/desactiva el relay de la bomba de riego (GPIO 25).
*/
void controlarBomba(bool estado) {
estadoBomba = estado;
digitalWrite(PIN_BOMBA, estado ? HIGH : LOW);
Serial.printf("[ACT] Bomba Riego → %s\n", estado ? "ON" : "OFF");
}
/**
* controlarPorton(estado)
* Activa/desactiva el relay del portón principal (GPIO 33).
*/
void controlarPorton(bool estado) {
estadoPorton = estado;
digitalWrite(PIN_PORTON, estado ? HIGH : LOW);
Serial.printf("[ACT] Portón Principal → %s\n", estado ? "ON" : "OFF");
}
/**
* manejarAlerta()
* Hace parpadear el LED de alerta cuando alertaDist es true.
* Usa millis() para no bloquear el loop.
*/
void manejarAlerta() {
if (!alertaDist) {
// Sin alerta: apagar LED
digitalWrite(PIN_ALERTA, LOW);
estadoBlink = false;
return;
}
// Parpadeo no bloqueante
unsigned long ahora = millis();
if (ahora - tAnteriorBlink >= INTERVALO_BLINK) {
tAnteriorBlink = ahora;
estadoBlink = !estadoBlink;
digitalWrite(PIN_ALERTA, estadoBlink ? HIGH : LOW);
}
}
// ════════════════════════════════════════════════
// SECCIÓN D: PANTALLA OLED
// ════════════════════════════════════════════════
/**
* actualizarOLED()
* Dibuja en la pantalla OLED:
* - Fila 1: íconos temperatura y humedad + valores
* - Fila 2: ícono distancia + valor, ícono alerta si < 50 cm
* - Fila 3: estado de los cuatro actuadores
*
* Layout (128×64 px):
* [0,0]──────────────────────[127,0]
* | [ico_temp] T:xx.x°C [ico_hum] H:xx% |
* | [ico_dist] D:xxx cm [ico_alert] |
* | LS:ON LP:OFF BM:OFF PT:OFF |
* [0,63]─────────────────────[127,63]
*/
void actualizarOLED() {
oled.clearDisplay();
// — Fila 1 (y=0): Temperatura y Humedad —
oled.drawBitmap(0, 0, icon_temp, ICONO_W, ICONO_H, SSD1306_WHITE);
oled.setCursor(17, 4);
oled.printf("%.1fC", temperatura);
oled.drawBitmap(65, 0, icon_hum, ICONO_W, ICONO_H, SSD1306_WHITE);
oled.setCursor(82, 4);
oled.printf("%.0f%%", humedad);
// — Separador horizontal —
oled.drawLine(0, 22, 127, 22, SSD1306_WHITE);
// — Fila 2 (y=24): Distancia + Alerta —
oled.drawBitmap(0, 24, icon_dist, ICONO_W, ICONO_H, SSD1306_WHITE);
oled.setCursor(17, 28);
oled.printf("%.1fcm", distancia);
// Ícono de alerta si distancia < 50 cm
if (alertaDist) {
oled.drawBitmap(95, 24, icon_alert, ICONO_W, ICONO_H, SSD1306_WHITE);
oled.setCursor(75, 28);
oled.print("<!>");
}
// — Separador horizontal —
oled.drawLine(0, 46, 127, 46, SSD1306_WHITE);
// — Fila 3 (y=48): Estado de actuadores —
// Íconos pequeños + etiqueta
oled.setCursor(0, 50);
oled.printf("LS:%s", estadoLuzSala ? "ON " : "OFF");
oled.setCursor(38, 50);
oled.printf("LP:%s", estadoLuzPuerta ? "ON " : "OFF");
oled.setCursor(0, 58);
oled.printf("BM:%s", estadoBomba ? "ON " : "OFF");
oled.setCursor(38, 58);
oled.printf("PT:%s", estadoPorton ? "ON " : "OFF");
oled.display(); // Enviar buffer a pantalla
}
// ════════════════════════════════════════════════
// SECCIÓN E: SERVIDOR WEB
// ════════════════════════════════════════════════
/**
* estadoStr(s)
* Retorna "ON" o "OFF" según el booleano.
*/
String estadoStr(bool s) {
return s ? "ON" : "OFF";
}
/**
* generarHTML()
* Construye y retorna la página web completa como String.
* Diseño en tarjetas (cards) con:
* - Sección de sensores (temp, hum, distancia)
* - Sección de actuadores con botones toggle
* - Imágenes 100×100 px desde Base64 (myimages.h)
* - Auto-refresco de datos cada 10 s con fetch()
* - Control de actuadores con fetch() sin recargar página
*/
String generarHTML() {
// Dirección IP del ESP32 para usar en JavaScript
String ip = WiFi.localIP().toString();
String html = R"rawhtml(
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Smart Agro ESP32</title>
<style>
/* ── Reset y variables ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d1117;
--card: #161b22;
--border: #30363d;
--accent1: #00ff88; /* verde sensor */
--accent2: #ff6b35; /* naranja temp */
--accent3: #239de0; /* azul humedad */
--accent4: #ffd700; /* amarillo luz */
--accent5: #9b59b6; /* morado portón */
--text: #e6edf3;
--subtext: #8b949e;
--on: #00ff88;
--off: #484f58;
--danger: #ff4444;
--radius: 14px;
--shadow: 0 4px 24px rgba(0,0,0,0.5);
--font: 'Courier New', monospace;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font);
min-height: 100vh;
padding: 20px;
}
/* ── Encabezado ── */
header {
text-align: center;
padding: 20px 0 30px;
}
header h1 {
font-size: 2rem;
letter-spacing: 4px;
text-transform: uppercase;
background: linear-gradient(90deg, var(--accent1), var(--accent2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
header p {
color: var(--subtext);
font-size: 0.85rem;
margin-top: 6px;
letter-spacing: 2px;
}
.ip-badge {
display: inline-block;
margin-top: 10px;
padding: 4px 14px;
border: 1px solid var(--accent1);
border-radius: 20px;
color: var(--accent1);
font-size: 0.8rem;
}
/* ── Grid principal ── */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 18px;
max-width: 1100px;
margin: 0 auto;
}
/* ── Tarjeta base ── */
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow);
transition: border-color 0.3s, transform 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.card:hover {
border-color: var(--accent1);
transform: translateY(-3px);
}
.card img {
width: 100px;
height: 100px;
border-radius: 10px;
border: 2px solid var(--border);
}
.card h3 {
font-size: 0.75rem;
letter-spacing: 3px;
text-transform: uppercase;
color: var(--subtext);
}
.valor {
font-size: 2rem;
font-weight: bold;
letter-spacing: 2px;
}
/* Colores por sensor */
.card.temp .valor { color: var(--accent2); }
.card.hum .valor { color: var(--accent3); }
.card.dist .valor { color: var(--accent1); }
.card.dist.alerta { border-color: var(--danger); animation: pulseAlerta 0.6s infinite alternate; }
@keyframes pulseAlerta {
from { box-shadow: 0 0 8px var(--danger); }
to { box-shadow: 0 0 24px var(--danger); }
}
/* ── Indicador ON/OFF ── */
.badge {
display: inline-block;
padding: 3px 14px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: bold;
letter-spacing: 2px;
}
.badge.on { background: rgba(0,255,136,0.15); color: var(--on); border: 1px solid var(--on); }
.badge.off { background: rgba(72,79,88,0.3); color: var(--off); border: 1px solid var(--off); }
/* ── Botón toggle ── */
.btn-toggle {
padding: 10px 28px;
border-radius: 8px;
border: 2px solid var(--accent1);
background: transparent;
color: var(--accent1);
font-family: var(--font);
font-size: 0.9rem;
letter-spacing: 2px;
cursor: pointer;
transition: background 0.25s, color 0.25s, transform 0.15s;
}
.btn-toggle:hover { background: var(--accent1); color: var(--bg); transform: scale(1.05); }
.btn-toggle:active { transform: scale(0.97); }
/* ── Sección título ── */
.seccion-titulo {
text-align: center;
max-width: 1100px;
margin: 30px auto 14px;
color: var(--subtext);
font-size: 0.75rem;
letter-spacing: 4px;
text-transform: uppercase;
border-bottom: 1px solid var(--border);
padding-bottom: 8px;
}
/* ── Footer ── */
footer {
text-align: center;
margin-top: 40px;
color: var(--subtext);
font-size: 0.75rem;
letter-spacing: 2px;
}
/* ── Indicador de actualización ── */
#refresh-indicator {
text-align: center;
margin: 12px auto 0;
color: var(--subtext);
font-size: 0.75rem;
}
.dot {
display: inline-block;
width: 8px; height: 8px;
background: var(--accent1);
border-radius: 50%;
margin-right: 6px;
animation: blink 1s infinite;
}
@keyframes blink { 50% { opacity: 0.2; } }
</style>
</head>
<body>
<!-- ═══ ENCABEZADO ═══ -->
<header>
<h1>Smart Agro</h1>
<p>Monitoreo Ambiental y Control de Actuadores</p>
<div class="ip-badge">ESP32 @ )rawhtml";
html += ip;
html += R"rawhtml(</div>
</header>
<!-- ═══ SECCIÓN SENSORES ═══ -->
<div class="seccion-titulo">◈ Sensores Ambientales ◈</div>
<div class="grid">
<!-- Tarjeta Temperatura -->
<div class="card temp">
<img src=")rawhtml";
html += String(IMG_TEMP);
html += R"rawhtml(" alt="Temperatura">
<h3>Temperatura</h3>
<div class="valor" id="val-temp">)rawhtml";
html += String(temperatura, 1) + "°C";
html += R"rawhtml(</div>
</div>
<!-- Tarjeta Humedad -->
<div class="card hum">
<img src=")rawhtml";
html += String(IMG_HUM);
html += R"rawhtml(" alt="Humedad">
<h3>Humedad Relativa</h3>
<div class="valor" id="val-hum">)rawhtml";
html += String(humedad, 0) + "%";
html += R"rawhtml(</div>
</div>
<!-- Tarjeta Distancia -->
<div class="card dist" id="card-dist">
<img src=")rawhtml";
html += String(IMG_DIST);
html += R"rawhtml(" alt="Distancia">
<h3>Distancia HC-SR04</h3>
<div class="valor" id="val-dist">)rawhtml";
html += String(distancia, 1) + " cm";
html += R"rawhtml(</div>
<span id="badge-alerta" class="badge )rawhtml";
html += alertaDist ? "on\">⚠ ALERTA" : "off\">OK";
html += R"rawhtml(</span>
</div>
</div><!-- /grid sensores -->
<!-- ═══ SECCIÓN ACTUADORES ═══ -->
<div class="seccion-titulo">◈ Control de Actuadores ◈</div>
<div class="grid">
<!-- Tarjeta Luz Sala -->
<div class="card">
<img src=")rawhtml";
html += String(IMG_LED);
html += R"rawhtml(" alt="Luz Sala">
<h3>Luz Sala</h3>
<span id="badge-lsala" class="badge )rawhtml";
html += estadoLuzSala ? "on\">ON" : "off\">OFF";
html += R"rawhtml(</span>
<button class="btn-toggle" onclick="toggleActuador('lsala')">TOGGLE</button>
</div>
<!-- Tarjeta Luz Puerta -->
<div class="card">
<img src=")rawhtml";
html += String(IMG_RELAY);
html += R"rawhtml(" alt="Luz Puerta">
<h3>Luz Puerta</h3>
<span id="badge-lpuerta" class="badge )rawhtml";
html += estadoLuzPuerta ? "on\">ON" : "off\">OFF";
html += R"rawhtml(</span>
<button class="btn-toggle" onclick="toggleActuador('lpuerta')">TOGGLE</button>
</div>
<!-- Tarjeta Bomba de Riego -->
<div class="card">
<img src=")rawhtml";
html += String(IMG_PUMP);
html += R"rawhtml(" alt="Bomba de Riego">
<h3>Bomba de Riego</h3>
<span id="badge-bomba" class="badge )rawhtml";
html += estadoBomba ? "on\">ON" : "off\">OFF";
html += R"rawhtml(</span>
<button class="btn-toggle" onclick="toggleActuador('bomba')">TOGGLE</button>
</div>
<!-- Tarjeta Portón Principal -->
<div class="card">
<img src=")rawhtml";
html += String(IMG_GATE);
html += R"rawhtml(" alt="Portón Principal">
<h3>Portón Principal</h3>
<span id="badge-porton" class="badge )rawhtml";
html += estadoPorton ? "on\">ON" : "off\">OFF";
html += R"rawhtml(</span>
<button class="btn-toggle" onclick="toggleActuador('porton')">TOGGLE</button>
</div>
</div><!-- /grid actuadores -->
<!-- ═══ INDICADOR DE REFRESCO ═══ -->
<div id="refresh-indicator">
<span class="dot"></span>Datos actualizados automáticamente cada 10 s
</div>
<!-- ═══ FOOTER ═══ -->
<footer>
<p>Smart Agro con ESP32 · Proyecto Académico · )rawhtml";
html += WiFi.localIP().toString();
html += R"rawhtml(</p>
</footer>
<!-- ═══ JAVASCRIPT ═══ -->
<script>
/**
* toggleActuador(nombre)
* Envía una petición fetch() al ESP32 para alternar el actuador.
* La URL es /toggle?act=<nombre>
* El servidor responde con JSON: { "estado": true/false }
* No recarga la página ni cambia la URL.
*/
function toggleActuador(nombre) {
fetch('/toggle?act=' + nombre)
.then(r => r.json())
.then(data => {
actualizarBadge('badge-' + nombre, data.estado);
})
.catch(err => console.error('[Toggle] Error:', err));
}
/**
* actualizarBadge(id, estado)
* Actualiza visualmente el badge de estado ON/OFF.
*/
function actualizarBadge(id, estado) {
const badge = document.getElementById(id);
if (!badge) return;
badge.textContent = estado ? 'ON' : 'OFF';
badge.className = 'badge ' + (estado ? 'on' : 'off');
}
/**
* refrescarDatos()
* Obtiene datos actualizados del endpoint /data cada 10 s.
* El servidor responde con JSON de sensores y actuadores.
* Solo actualiza los elementos del DOM afectados.
*/
function refrescarDatos() {
fetch('/data')
.then(r => r.json())
.then(d => {
// Sensores
document.getElementById('val-temp').textContent = d.temp.toFixed(1) + '°C';
document.getElementById('val-hum').textContent = d.hum.toFixed(0) + '%';
document.getElementById('val-dist').textContent = d.dist.toFixed(1) + ' cm';
// Alerta distancia
const cardDist = document.getElementById('card-dist');
const badgeAlert = document.getElementById('badge-alerta');
if (d.alerta) {
cardDist.classList.add('alerta');
badgeAlert.textContent = '⚠ ALERTA';
badgeAlert.className = 'badge on';
} else {
cardDist.classList.remove('alerta');
badgeAlert.textContent = 'OK';
badgeAlert.className = 'badge off';
}
// Actuadores
actualizarBadge('badge-lsala', d.lsala);
actualizarBadge('badge-lpuerta', d.lpuerta);
actualizarBadge('badge-bomba', d.bomba);
actualizarBadge('badge-porton', d.porton);
})
.catch(err => console.error('[Refresh] Error:', err));
}
// Auto-refresco cada 10 segundos
setInterval(refrescarDatos, 10000);
</script>
</body>
</html>
)rawhtml";
return html;
}
/**
* inicializarServidor()
* Define las rutas del servidor web asíncrono:
*
* GET / → Devuelve la página HTML completa
* GET /data → JSON con todos los datos del sistema
* GET /toggle → Alterna un actuador y devuelve su estado
*/
void inicializarServidor() {
// ── Ruta raíz: página HTML completa ──
server.on("/", HTTP_GET, [](AsyncWebServerRequest* req) {
req->send(200, "text/html", generarHTML());
});
// ── Ruta /data: JSON con estado actual ──
server.on("/data", HTTP_GET, [](AsyncWebServerRequest* req) {
String json = "{";
json += "\"temp\":" + String(temperatura, 2) + ",";
json += "\"hum\":" + String(humedad, 2) + ",";
json += "\"dist\":" + String(distancia, 2) + ",";
json += "\"alerta\":" + (alertaDist ? "true" : "false") + ",";
json += "\"lsala\":" + (estadoLuzSala ? "true" : "false") + ",";
json += "\"lpuerta\":" + (estadoLuzPuerta ? "true" : "false") + ",";
json += "\"bomba\":" + (estadoBomba ? "true" : "false") + ",";
json += "\"porton\":" + (estadoPorton ? "true" : "false");
json += "}";
req->send(200, "application/json", json);
});
// ── Ruta /toggle: alterna actuador y responde JSON ──
server.on("/toggle", HTTP_GET, [](AsyncWebServerRequest* req) {
if (!req->hasParam("act")) {
req->send(400, "application/json", "{\"error\":\"param act requerido\"}");
return;
}
String act = req->getParam("act")->value();
bool nuevoEstado = false;
if (act == "lsala") { controlarLuzSala(!estadoLuzSala); nuevoEstado = estadoLuzSala; }
else if (act == "lpuerta") { controlarLuzPuerta(!estadoLuzPuerta); nuevoEstado = estadoLuzPuerta; }
else if (act == "bomba") { controlarBomba(!estadoBomba); nuevoEstado = estadoBomba; }
else if (act == "porton") { controlarPorton(!estadoPorton); nuevoEstado = estadoPorton; }
else {
req->send(404, "application/json", "{\"error\":\"actuador desconocido\"}");
return;
}
String json = "{\"estado\":" + String(nuevoEstado ? "true" : "false") + "}";
req->send(200, "application/json", json);
});
// Iniciar servidor
server.begin();
Serial.println("[OK] Servidor web iniciado en puerto 80.");
Serial.printf("[WEB] Acceder en: http://%s\n", WiFi.localIP().toString().c_str());
}
// ── FIN DEL ARCHIVO ──