control_de_un_jardin_con_piscina_con_esp32
Control de un jardín con piscina con ESP32
#include <WiFi.h> #include <WiFiMulti.h> #include <WebServer.h> #include <ESP32Servo.h> #include <Wire.h> #include <LiquidCrystal_I2C.h> #include <ESPmDNS.h> #include <time.h> // --- CONFIGURACIÓN DE REDES --- WiFiMulti wifiMulti; const char* ssid1 = "Nombre del WiFi Principal"; const char* pass1 = "Clave del WiFi principal"; const char* ssid2 = "Nombre del WiFi secundario"; const char* pass2 = "Clave del WiFi secundario"; const char* usuarioSeguro = "admin"; // Usuario const char* passwordSegura = "1234"; // Contraseña // --- CONFIGURACIÓN NTP (Hora de Internet) --- const char* ntpServer = "pool.ntp.org"; const char* timeZone = "CET-1CEST,M3.5.0,M10.5.0/3"; // --- ASIGNACIÓN DE PINES --- const int pinesCanales[] = {13, 14, 27, 26, 16, 33}; const int pinValvulaMaestra = 32; const int pinLuces = 23; const int pinDepuradora = 19; const int pinServo = 18; const int pinLDR = 35; const int pinRelayPresion = 4; const int pinSensorPresion = 34; // --- CONFIGURACIÓN DEL DISPLAY Y SERVO --- LiquidCrystal_I2C lcd(0x27, 16, 2); Servo miServo; const int posReposo = 0; const int posPresion = 20; unsigned long tiempoServoPulsado = 0; bool servoEnMovimiento = false; // Variables para clonar el texto del LCD a la Web String lcdLinea1 = ""; String lcdLinea2 = ""; // --- VARIABLES DE ESTADO --- bool estados[8] = {false, false, false, false, false, false, false, false}; bool hayLuz = false; bool riegoEnProgreso = false; unsigned long tiempoFinAccesorios[2] = {0, 0}; int canalesParaRegar[6], totalCanalesRiego = 0, indiceRiegoActual = -1; unsigned long tiempoInicioZona = 0, duracionZonaMs = 0; // --- CONTADORES DE TIEMPO POR CANAL Y ACCESORIOS (SEGUNDOS DE USO DIARIO) --- // Índices: 0-5 = Canales C1-C6, 6 = Luz, 7 = Depuradora unsigned long tiempoUsoSegundos[8] = {0, 0, 0, 0, 0, 0, 0, 0}; unsigned long ultimoCalculoUso[8] = {0, 0, 0, 0, 0, 0, 0, 0}; int ultimoDiaMilitar = -1; float ultimaPresion = 0.0; bool midiendoPresion = false; unsigned long tiempoInicioMedida = 0; const unsigned long TIEMPO_ESPERA_PRESION = 1500; unsigned long ultimaRotacion = 0; int pantallaActual = 0; const int intervaloRotacion = 5000; unsigned long ultimaVezCheckWiFi = 0; const unsigned long intervaloCheckWiFi = 15000; unsigned long ultimaActualizacionLCD = 0; WebServer server(80); // --- FUNCIONES DE HARDWARE Y CONTADORES --- void actualizarHardware() { bool algunRiegoActivo = false; unsigned long ahora = millis(); for (int i = 0; i < 6; i++) { digitalWrite(pinesCanales[i], estados[i] ? LOW : HIGH); if (estados[i]) { algunRiegoActivo = true; if (ultimoCalculoUso[i] == 0) ultimoCalculoUso[i] = ahora; } else { if (ultimoCalculoUso[i] > 0) { tiempoUsoSegundos[i] += (ahora - ultimoCalculoUso[i]) / 1000; ultimoCalculoUso[i] = 0; } } } digitalWrite(pinValvulaMaestra, (algunRiegoActivo || riegoEnProgreso) ? LOW : HIGH); // Luz (Índice 6) digitalWrite(pinLuces, estados[6] ? LOW : HIGH); if (estados[6]) { if (ultimoCalculoUso[6] == 0) ultimoCalculoUso[6] = ahora; } else { if (ultimoCalculoUso[6] > 0) { tiempoUsoSegundos[6] += (ahora - ultimoCalculoUso[6]) / 1000; ultimoCalculoUso[6] = 0; } } // Depuradora (Índice 7) digitalWrite(pinDepuradora, estados[7] ? LOW : HIGH); if (estados[7]) { if (ultimoCalculoUso[7] == 0) ultimoCalculoUso[7] = ahora; } else { if (ultimoCalculoUso[7] > 0) { tiempoUsoSegundos[7] += (ahora - ultimoCalculoUso[7]) / 1000; ultimoCalculoUso[7] = 0; } } if (midiendoPresion) { digitalWrite(pinRelayPresion, LOW); } else { digitalWrite(pinRelayPresion, HIGH); } } void actualizarContadoresTiempo() { unsigned long ahora = millis(); for (int i = 0; i < 8; i++) { if (estados[i] && ultimoCalculoUso[i] > 0) { unsigned long transcurrido = (ahora - ultimoCalculoUso[i]) / 1000; if (transcurrido >= 1) { tiempoUsoSegundos[i] += transcurrido; ultimoCalculoUso[i] = ahora; } } } struct tm timeinfo; if (getLocalTime(&timeinfo)) { if (ultimoDiaMilitar == -1) { ultimoDiaMilitar = timeinfo.tm_mday; } else if (ultimoDiaMilitar != timeinfo.tm_mday) { for (int i = 0; i < 8; i++) { tiempoUsoSegundos[i] = 0; if (estados[i]) ultimoCalculoUso[i] = ahora; } ultimoDiaMilitar = timeinfo.tm_mday; // Serial.println("Contadores reseteados a las 00:00"); } } } void realizarLecturaPresion() { long suma = 0; for(int i=0; i<25; i++) { suma += analogRead(pinSensorPresion); delay(2); } float promedio = suma / 25.0; float voltaje = promedio * (3.3 / 4095.0); ultimaPresion = (voltaje - 0.5) * (12.0 / 4.0); if (ultimaPresion < 0) ultimaPresion = 0; } void imprimirLinea(int fila, String texto) { while (texto.length() < 16) texto += " "; if (texto.length() > 16) texto = texto.substring(0, 16); lcd.setCursor(0, fila); lcd.print(texto); if (fila == 0) lcdLinea1 = texto; else if (fila == 1) lcdLinea2 = texto; } String obtenerHoraFormateada() { struct tm timeinfo; if (!getLocalTime(&timeinfo)) return "Sincronizando.."; char buffer[16]; strftime(buffer, sizeof(buffer), "%H:%M:%S", &timeinfo); return String(buffer); } String obtenerFechaFormateada() { struct tm timeinfo; if (!getLocalTime(&timeinfo)) return "--/--/----"; char buffer[16]; strftime(buffer, sizeof(buffer), "%d/%m/%Y", &timeinfo); return String(buffer); } void actualizarLCD() { unsigned long ahora = millis(); if (servoEnMovimiento) { imprimirLinea(0, "EJECUTANDO:"); imprimirLinea(1, "PULSACION SERVO"); return; } if (midiendoPresion) { imprimirLinea(0, "MIDIENDO..."); imprimirLinea(1, "RELE 8 ACTIVO"); return; } bool mostrarRiego = (riegoEnProgreso && indiceRiegoActual >= 0); bool mostrarLuz = estados[6]; bool mostrarDep = estados[7]; bool mostrarManual = (!riegoEnProgreso && (estados[0]||estados[1]||estados[2]||estados[3]||estados[4]||estados[5])); bool mostrarPresion = (ultimaPresion > 0.05); int activas = 1; if (mostrarRiego) activas++; if (mostrarLuz) activas++; if (mostrarDep) activas++; if (mostrarManual) activas++; if (mostrarPresion) activas++; if (ahora - ultimaRotacion >= intervaloRotacion) { ultimaRotacion = ahora; pantallaActual++; } int vista = pantallaActual % activas; int contadorVista = 0; if (contadorVista == vista) { imprimirLinea(0, "HORA: " + obtenerHoraFormateada()); imprimirLinea(1, "FECHA:" + obtenerFechaFormateada()); return; } contadorVista++; if (mostrarRiego) { if (contadorVista == vista) { imprimirLinea(0, "RIEGO: Zona C" + String(canalesParaRegar[indiceRiegoActual] + 1)); unsigned long r = (duracionZonaMs > (ahora - tiempoInicioZona)) ? (duracionZonaMs - (ahora - tiempoInicioZona)) / 1000 : 0; imprimirLinea(1, "Restan: " + String(r) + " seg"); return; } contadorVista++; } if (mostrarPresion) { if (contadorVista == vista) { imprimirLinea(0, "PRESION RED:"); imprimirLinea(1, String(ultimaPresion, 2) + " BAR"); return; } contadorVista++; } if (mostrarLuz) { if (contadorVista == vista) { imprimirLinea(0, "ACC: LUZ ON"); unsigned long rest = (tiempoFinAccesorios[0] > ahora) ? (tiempoFinAccesorios[0] - ahora) / 1000 : 0; imprimirLinea(1, "Off: " + String(rest / 60) + "m " + String(rest % 60) + "s"); return; } contadorVista++; } if (mostrarDep) { if (contadorVista == vista) { imprimirLinea(0, "ACC: DEPURAD. ON"); unsigned long rest = (tiempoFinAccesorios[1] > ahora) ? (tiempoFinAccesorios[1] - ahora) / 1000 : 0; imprimirLinea(1, "Off: " + String(rest / 60) + "m " + String(rest % 60) + "s"); return; } contadorVista++; } if (mostrarManual) { if (contadorVista == vista) { imprimirLinea(0, "ZONAS MANUALES:"); String lista = ""; for(int i=0; i<6; i++) { if(estados[i]) lista += String(i+1) + " "; } imprimirLinea(1, lista); } } } void detenerTodo() { riegoEnProgreso = false; indiceRiegoActual = -1; midiendoPresion = false; digitalWrite(pinRelayPresion, HIGH); for (int i = 0; i < 8; i++) estados[i] = false; tiempoFinAccesorios[0] = 0; tiempoFinAccesorios[1] = 0; actualizarHardware(); } // --- INTERFAZ WEB --- const char PAGE_HTML[] PROGMEM = R"=====( <!DOCTYPE html> <html lang='es'> <head> <meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'> <title>Control de Riego</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, sans-serif; background: #f0f2f5; padding: 10px; font-size: 14px; } h1 { text-align: center; color: #0056b3; font-size: 20px; margin: 10px 0; } .card { background: white; padding: 12px; border-radius: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 10px; position: relative; } h2 { font-size: 15px; margin-bottom: 8px; color: #0056b3; border-bottom: 1px solid #eee; padding-bottom: 4px; } .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; } button { padding: 10px 2px; cursor: pointer; border: 1px solid #ddd; border-radius: 6px; background: #f8f9fa; font-weight: bold; font-size: 11px; } button.active { background: #d9534f; color: white; border-color: #c9302c; } .special-btn { min-width: 100px; padding: 14px 5px; font-size: 11px; } .overlay { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.7); z-index: 10; border-radius: 10px; align-items: center; justify-content: center; font-weight: bold; color: #d9534f; } .sensor-container { display: flex; gap: 8px; margin-top: 5px; } .btn-servo { width: 66%; background: #6f42c1; color: white; border: none; padding: 14px 5px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 12px; } .sensor-status { width: 34%; pointer-events: none; padding: 14px 5px; border-radius: 6px; border: 1px solid #ddd; font-weight: bold; text-align: center; background: #f8f9fa; color: #6c757d; font-size: 12px; display: flex; align-items: center; justify-content: center; } .status-active { background: #28a745; color: white; border-color: #1e7e34; } .row { display: flex; align-items: center; gap: 10px; margin-top: 8px; } .time-label { min-width: 45px; font-size: 12px; font-weight: bold; text-align: right; } .riego-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; text-align: left; margin: 5px 0; } .riego-item { display: flex; align-items: center; gap: 8px; font-weight: bold; font-size: 13px; padding: 5px 0; } input[type="checkbox"] { width: 22px; height: 22px; cursor: pointer; } .btn-main { background: #007bff; color: white; border: none; width: 100%; padding: 14px; margin-top: 10px; border-radius: 6px; font-weight: bold; font-size: 14px; } .btn-stop { background: #ff0000; color: white; border: none; width: 100%; padding: 16px; border-radius: 6px; font-weight: 800; font-size: 16px; box-shadow: 0 4px #b30000; } .btn-presion { background: #17a2b8; color: white; border: none; width: 66%; padding: 14px 5px; border-radius: 6px; font-weight: bold; font-size: 12px; cursor: pointer; } .lcd-box { background: #222; color: #00ff00; font-family: monospace; padding: 2px 6px; border-radius: 6px; text-align: center; font-size: 11px; font-weight: bold; margin-bottom: 10px; letter-spacing: 0.5px; border: 1px solid #444; white-space: pre; line-height: 1.0; max-height: 38px; overflow: hidden; display: flex; flex-direction: column; justify-content: center; } </style> </head> <body onload="actualizarEstados()"> <h1>Control de Riego</h1> <div class='lcd-box'> <div id='lcdL1'>---</div> <div id='lcdL2'>---</div> </div> <div class='card'> <div id='bloqueoManual' class='overlay'>RIEGO EN CURSO</div> <h2>Zonas Manuales</h2> <div class='grid'> <button id="btn0" onclick="toggle(0)">C1 (0s)</button> <button id="btn1" onclick="toggle(1)">C2 (0s)</button> <button id="btn2" onclick="toggle(2)">C3 (0s)</button> <button id="btn3" onclick="toggle(3)">C4 (0s)</button> <button id="btn4" onclick="toggle(4)">C5 (0s)</button> <button id="btn5" onclick="toggle(5)">C6 (0s)</button> </div> </div> <div class='card'> <h2>Equipamiento</h2> <div class='row'> <button id="btn6" class="special-btn" onclick="toggleAcc(6,'tL')">LUZ (0s)</button> <input type="range" id="tL" min="1" max="120" value="1" oninput="vL.innerText=this.value"> <span class="time-label"><span id="vL">1</span>m</span> </div> <div class='row'> <button id="btn7" class="special-btn" onclick="toggleAcc(7,'tD')">DEP (0s)</button> <input type="range" id="tD" min="1" max="360" value="1" oninput="vD.innerText=this.value"> <span class="time-label"><span id="vD">1</span>m</span> </div> </div> <div class='card'><h2>Presión de Agua</h2><div class="sensor-container"><button class='btn-presion' id="btnP" onclick="medirP()">MEDIR PRESIÓN</button><div id="valP" class="sensor-status">-- BAR</div></div></div> <div class='card'><h2>Control y Sensor Luz</h2><div class="sensor-container"><button class='btn-servo' onclick="pulsar()">EJECUTAR PULSACIÓN</button><div id="ldrStatus" class="sensor-status">...</div></div></div> <div class='card'> <h2>Riego Secuencial</h2> <div class='riego-grid'> <div class='riego-item'><input type="checkbox" name="mc" value="0"> C1</div> <div class='riego-item'><input type="checkbox" name="mc" value="1"> C2</div> <div class='riego-item'><input type="checkbox" name="mc" value="2"> C3</div> <div class='riego-item'><input type="checkbox" name="mc" value="3"> C4</div> <div class='riego-item'><input type="checkbox" name="mc" value="4"> C5</div> <div class='riego-item'><input type="checkbox" name="mc" value="5"> C6</div> </div> <div class='row'> <label style="font-size:12px; font-weight:bold;">Tiempo:</label> <input type="range" id="dur" min="1" max="15" value="1" oninput="vR.innerText=this.value"> <span class="time-label"><span id="vR">1</span>m</span> </div> <button class='btn-main' onclick="sendRiego()">INICIAR RIEGO</button> </div> <div class='card'><button class='btn-stop' onclick="stopAll()">PARADA TOTAL</button></div> <script> function fmtSeg(s) { if (s < 60) return s + 's'; let m = Math.floor(s / 60); let seg = s % 60; return m + 'm ' + seg + 's'; } function actualizarEstados() { fetch('/status').then(res => res.json()).then(data => { data.estados.forEach((st, i) => { const btn = document.getElementById('btn' + i); if(btn) st ? btn.classList.add('active') : btn.classList.remove('active'); }); document.getElementById('bloqueoManual').style.display = data.riegoActivo ? 'flex' : 'none'; const ldr = document.getElementById('ldrStatus'); if(data.luz) { ldr.innerText = "Encendido"; ldr.classList.add('status-active'); } else { ldr.innerText = "Apagado"; ldr.classList.remove('status-active'); } const pVal = document.getElementById('valP'); pVal.innerText = data.presion.toFixed(2) + " BAR"; if(data.midiendo) { document.getElementById('btnP').innerText = "ESPERANDO..."; document.getElementById('btnP').style.background = "#ffc107"; } else { document.getElementById('btnP').innerText = "MEDIR PRESIÓN"; document.getElementById('btnP').style.background = "#17a2b8"; } if(data.lcd1 !== undefined) document.getElementById('lcdL1').innerText = data.lcd1; if(data.lcd2 !== undefined) document.getElementById('lcdL2').innerText = data.lcd2; if(data.usoSegundos) { // Actualizar etiquetas en botones C1-C6 for(let i = 0; i < 6; i++) { const btn = document.getElementById('btn' + i); if(btn) btn.innerText = 'C' + (i + 1) + ' (' + fmtSeg(data.usoSegundos[i]) + ')'; } // Actualizar etiqueta Luz const btnLuz = document.getElementById('btn6'); if(btnLuz) btnLuz.innerText = 'LUZ (' + fmtSeg(data.usoSegundos[6]) + ')'; // Actualizar etiqueta Depuradora const btnDep = document.getElementById('btn7'); if(btnDep) btnDep.innerText = 'DEP (' + fmtSeg(data.usoSegundos[7]) + ')'; } }); } function medirP() { fetch('/medirP'); } function toggle(id) { fetch('/toggle?id=' + id).then(actualizarEstados); } function toggleAcc(id, sId) { fetch(`/acc?id=${id}&t=${document.getElementById(sId).value}`).then(actualizarEstados); } function pulsar() { fetch('/pulsar'); } function stopAll() { fetch('/stop').then(actualizarEstados); } function sendRiego() { let sel = []; document.querySelectorAll('input[name="mc"]:checked').forEach(el => sel.push(el.value)); if(!sel.length) return alert('Selecciona zona'); fetch(`/riego?ids=${sel.join(',')}&t=${document.getElementById('dur').value}`).then(actualizarEstados); } setInterval(actualizarEstados, 500); </script> </body> </html> )====="; // --- HANDLERS --- void handleStatus() { if (!server.authenticate(usuarioSeguro, passwordSegura)) return; int lecturaLDR = analogRead(pinLDR); hayLuz = (lecturaLDR > 20); String json = "{\"riegoActivo\":" + String(riegoEnProgreso ? "true" : "false") + ",\"luz\":" + String(hayLuz ? "true" : "false") + ",\"presion\":" + String(ultimaPresion) + ",\"midiendo\":" + String(midiendoPresion ? "true" : "false") + ",\"lcd1\":\"" + lcdLinea1 + "\"" + ",\"lcd2\":\"" + lcdLinea2 + "\"" + ",\"usoSegundos\":["; for (int i = 0; i < 8; i++) { json += String(tiempoUsoSegundos[i]); if (i < 7) json += ","; } json += "],\"estados\":["; for (int i = 0; i < 8; i++) { json += estados[i] ? "true" : "false"; if (i < 7) json += ","; } json += "]}"; server.send(200, "application/json", json); } void setup() { Serial.begin(9600); Wire.begin(21, 22); lcd.init(); lcd.backlight(); imprimirLinea(0, "Sistema Listo"); auto initPinSeguro = [](int p) { digitalWrite(p, HIGH); pinMode(p, OUTPUT); digitalWrite(p, HIGH); }; for(int i=0; i<6; i++) initPinSeguro(pinesCanales[i]); initPinSeguro(pinValvulaMaestra); initPinSeguro(pinLuces); initPinSeguro(pinDepuradora); initPinSeguro(pinRelayPresion); pinMode(pinLDR, INPUT); pinMode(pinSensorPresion, INPUT); miServo.attach(pinServo); miServo.write(posReposo); actualizarHardware(); wifiMulti.addAP(ssid1, pass1); wifiMulti.addAP(ssid2, pass2); while (wifiMulti.run() != WL_CONNECTED) { delay(500); } configTzTime(timeZone, ntpServer); MDNS.begin("riego"); server.on("/", []() { if(server.authenticate(usuarioSeguro, passwordSegura)) server.send(200, "text/html", PAGE_HTML); else server.requestAuthentication(); }); server.on("/status", handleStatus); server.on("/toggle", [](){ if(riegoEnProgreso) return; int id = server.arg("id").toInt(); if (id < 6) { estados[id] = !estados[id]; actualizarHardware(); } server.send(200); }); server.on("/acc", [](){ int id = server.arg("id").toInt(); unsigned long min = server.arg("t").toInt(); estados[id] = !estados[id]; if (estados[id]) tiempoFinAccesorios[id-6] = millis() + (min * 60000); actualizarHardware(); server.send(200); }); server.on("/medirP", [](){ if(!midiendoPresion) { digitalWrite(pinRelayPresion, LOW); midiendoPresion = true; tiempoInicioMedida = millis(); } server.send(200); }); server.on("/pulsar", [](){ miServo.write(posPresion); tiempoServoPulsado = millis() + 500; servoEnMovimiento = true; server.send(200); }); server.on("/stop", [](){ detenerTodo(); server.send(200); }); server.on("/riego", [](){ if (riegoEnProgreso) return; for (int i = 0; i < 6; i++) estados[i] = false; actualizarHardware(); String ids = server.arg("ids"); duracionZonaMs = (unsigned long)server.arg("t").toInt() * 60000; totalCanalesRiego = 0; char str[ids.length() + 1]; strcpy(str, ids.c_str()); char* p = strtok(str, ","); while (p != NULL) { canalesParaRegar[totalCanalesRiego++] = atoi(p); p = strtok(NULL, ","); } if (totalCanalesRiego > 0) { riegoEnProgreso = true; indiceRiegoActual = -1; } server.send(200); }); server.begin(); } void loop() { unsigned long ahora = millis(); server.handleClient(); actualizarContadoresTiempo(); if (ahora - ultimaVezCheckWiFi >= intervaloCheckWiFi) { ultimaVezCheckWiFi = ahora; wifiMulti.run(); } if (ahora - ultimaActualizacionLCD >= 500) { ultimaActualizacionLCD = ahora; actualizarLCD(); } if (midiendoPresion) { if (millis() - tiempoInicioMedida >= TIEMPO_ESPERA_PRESION) { realizarLecturaPresion(); digitalWrite(pinRelayPresion, HIGH); midiendoPresion = false; // Serial.println("Medición terminada, relé apagado."); } } if (riegoEnProgreso) { if (indiceRiegoActual == -1 || (ahora - tiempoInicioZona >= duracionZonaMs)) { if (indiceRiegoActual >= 0) estados[canalesParaRegar[indiceRiegoActual]] = false; indiceRiegoActual++; if (indiceRiegoActual >= totalCanalesRiego) { riegoEnProgreso = false; indiceRiegoActual = -1; } else { estados[canalesParaRegar[indiceRiegoActual]] = true; tiempoInicioZona = ahora; } actualizarHardware(); } } if (estados[6] && ahora >= tiempoFinAccesorios[0]) { estados[6] = false; actualizarHardware(); } if (estados[7] && ahora >= tiempoFinAccesorios[1]) { estados[7] = false; actualizarHardware(); } if (servoEnMovimiento && ahora >= tiempoServoPulsado) { miServo.write(posReposo); servoEnMovimiento = false; } }
control_de_un_jardin_con_piscina_con_esp32.txt · Última modificación: por 127.0.0.1

