// version lista 1.0
// estacion de riego
// creado por p.ramos.p
// este proyecto cuenta con los siguientes componentes
// arduino mega, rtc, bmp280, wemos d1 mini, encoder, 4 botones fisicos y dos convertidor de señal logico 5v a 3,3v
//
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_BMP280.h>
#include <RTClib.h>
#include <EEPROM.h>
// Definición física de pines (Tus conexiones de mesa fijas)
#define pinCLK 2
#define pinDT 3
#define pinSW 4
#define pinBTN1 5
#define pinBTN2 6
#define pinBTN3 7
#define pinBTN4 8
#define pinV23 44
#define pinV24 45
#define pinInicioValvulas 22
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); // Tu objeto original "oled"
Adafruit_BMP280 bmp;
//Bme280TwoWire bmp; // Mantenemos el nombre "bmp" para que no tengas que cambiarlo en el resto del código
RTC_DS3231 rtc;
// Estructura de parámetros EEPROM
struct Config{
int estadoValvulas[22];
int horaAM, minutoAM;
int horaMD, minutoMD;
int horaPM, minutoPM;
int tiempoAM, tiempoMD, tiempoPM;
int tiempoTest;
int tempCorte;
int topePresion;
bool modoInvierno;
} config;
// Constantes de intervalos
//const unsigned long INTERVALO_BMP = 900000UL; // 15 minutos en ms
const unsigned long INTERVALO_BMP = 90000UL; // 15 minutos en ms
const unsigned long TIEMPO_ESPERA_PANTALLA = 120000UL; // 2 minutos en ms
// Variables globales de control nativas de tu bitácora
enum EstadosMenu { MODO_INICIO, MENU_PRINCIPAL, SUBMENU_HORARIOS, SUBMENU_HORAS_RIEGO, SUBMENU_MINUTOS_RIEGO, SUBMENU_TESTEO, SUBMENU_INVIERNO, SUBMENU_BLOQUEO, SUBMENU_TEMP };
EstadosMenu estadoActual = MODO_INICIO;
enum EstadosRiego { APAGADO, ENCENDIDO_SEGURO_ON, SOLAPADO, APAGADO_SEGURO_OFF, MANUAL_V23_ON, MANUAL_V23_RUN };
EstadosRiego estadoRiego = APAGADO;
// ============================================================================
// DECLARACIÓN DE VARIABLES GLOBALES NATIVAS (RECONSTRUCCIÓN DE TU BITÁCORA)
// ============================================================================
// --- VARIABLES DE ENERGÍA Y TIEMPO (PANTALLA OLED FÍSICA) ---
unsigned long tiempoUltimaActividad = 0; // Controla la regla de los 2 minutos de inactividad
bool mostrarTextoPausa = false; // Maneja el parpadeo del letrero *** PAUSA ***
unsigned long ultimoParpadeoPausa = 0; // Temporizador asíncrono para el texto parpadeante
unsigned long ultimoMuestreoBMP = 0;
// --- CLIMA Y SENSORES (BMP) ---
float ultimaTempBMP = 10.0; // Guarda la temperatura para mostrarla en el MODO_INICIO
float ultimaPresionBMP = 1010.0; // Guarda la presión para mostrarla en el MODO_INICIO
// --- BANDERAS GRÁFICAS DE RIEGO ACTIVO (PANTALLA LOCAL) ---
bool modoManualActivo = false; // Se vuelve true al regar con manguera manual
int faseManual = 0; // Controla los estados internos del segundero de manguera
bool secuenciaActiva = false; // Se vuelve true cuando corre un riego automático o test
bool modoTesteo = false; // Se vuelve true únicamente si el riego activo es un TEST
int valvulaActivaActual = -1; // Guarda el número de zona que se está dibujando en el cristal
int turnoActual = 0; // Guarda qué ciclo corre (1 = AM, 2 = MD, 3 = PM)
// --- VARIABLES MECÁNICAS DE NAVEGACIÓN DEL ENCODER ---
int opcionPrincipal = 0; // Reemplaza a posicionMenu (Mueve las 6 opciones del Menú Principal)
int opcionSubmenu = 0; // Reemplaza a posicionSubMenu (Mueve todas las listas de los submenús)
bool modoEdicion = false; // Reemplaza a editandoValor (Activa el asterisco '*' para cambiar datos)
int pasoEdicion = 0; // Reemplaza a pasoEdicionHora (Controla si editas Horas o Minutos)
// VARIABLES GLOBALES DE CONTROL SINCRO COMPILADAS SIN ERRORES
int valoresReles[22] = {0}; // Matriz nativa que guarda la cantidad de riegos del 1 al 22
//unsigned long cronometroPaso = 0; // Reloj asíncrono para el minutero regresivo de cada sector
// --- VARIABLES AUXILIARES DE TIEMPOS DE CONFIGURACIÓN ---
int configMinutosTesteo = 0; // Guarda los minutos del test para la pantalla física
int configPresionBloqueo = 0; // Guarda los hPa de sobrepresión para la pantalla física
int duracionRiegoAM = 0; // Guarda los minutos AM para la pantalla física
int duracionRiegoMD = 0; // Guarda los minutos MD para la pantalla física
int duracionRiegoPM = 0; // Guarda los minutos PM para la pantalla física
// --- ESTRUCTURA DE RELOJ PROPIO PARA CADA TURNO HORARIO ---
struct RiegoHorario {
int hora;
int minuto;
};
RiegoHorario riegoAM; // Horario del Ciclo AM
RiegoHorario riegoMD; // Horario del Ciclo MD
RiegoHorario riegoPM; // Horario del Ciclo PM
// Variables dinámicas de terreno
volatile int posicionEncoder = 0;
int ultimaPosicionEncoder = 0;
//unsigned long tiempoUltimaActividad = 0; // Tu reloj nativo de energía
bool pantallaEncendida = true;
float tempActual = 10.0;
float presionActual = 800.0;
unsigned long ultimaLecturaBmp = 0;
unsigned long ultimoRefrescoPantalla = 0;
// Variables hidráulicas unificadas
bool enPausa = false;
unsigned long cronometroSecuencia = 0;
unsigned long duracionRiegoZonaMs = 0;
unsigned long tiempoRestanteEnPausa = 0;
bool enSolapamiento = false;
unsigned long cronometroSolapado = 0;
int valvulaActualIndice = -1;
int valvulaAnteriorIndice = -1;
int tipoRiegoActivo = 0;
unsigned long cronometroPaso = 0; // Tu sincronizador de tiempo nativo
int duracionPasoActual = 0; // Almacena los minutos activos del bloque
unsigned long cronometroManual = 0; // Tu segundero nativo de manguera
int faseSecuencia = 0;
bool riegoAM_Ejecutado = false;
bool riegoMD_Ejecutado = false;
bool riegoPM_Ejecutado = false;
const char* opcionesMenuPrincipal[] = {
"1.Riego x Valv", "2.Horas riego", "3.Minutos riego",
"4.Minutos test", "5.Modo Invierno", "6.Presion bloqueo",
"7.Temp corte", "8.Salir & Guardar"
};
// Prototipos de Función obligatorios
void apagarTodoFisico();
void ejecutarStop();
void iniciarSecuenciaRiego(int tipo, int minutos, int idxBloque);
int buscarSiguienteValvula(int inicioIdx, int tipo);
bool esValvulaApta(int indice, int tipoRiego);
void actualizarPantalla();
void manejarGiroEncoder(int pasos);
void manejarClickEncoder();
void controlarCicloRiego();
void controlarRiegoManual();
void muestreoEncoder();
void verificarInactividad();
void registrarActividad();
// ============================================================================
// INTERRUPCIÓN NATIVA Y MANEJO COMPLETO DEL ENCODER MECHANICAL
// ============================================================================
void registrarActividad() {
tiempoUltimaActividad = millis(); // Corregido: Usa tu variable real
if (!pantallaEncendida) {
oled.ssd1306_command(SSD1306_DISPLAYON);
pantallaEncendida = true;
oled.clearDisplay();
oled.display();
delay(10);
}
}
void apagarTodoFisico() {
digitalWrite(pinV23, HIGH); digitalWrite(pinV24, HIGH);
for (int i = 0; i < 22; i++) digitalWrite(pinInicioValvulas + i, HIGH);
}
void ejecutarStop() {
apagarTodoFisico(); estadoRiego = APAGADO;
modoManualActivo = false; secuenciaActiva = false; modoTesteo = false; enPausa = false;
valvulaActualIndice = -1; valvulaActivaActual = -1; faseManual = 0; faseSecuencia = 0;
Serial3.println(F("CMD:STOP"));
}
// ============================================================================
// FILTROS HIDRÁULICOS INTELIGENTES REINCORPORADOS
// ============================================================================
bool esValvulaApta(int indice, int tipoRiego) {
int cantidadRiegos = config.estadoValvulas[indice];
if (cantidadRiegos == 0) return false;
if (tipoRiego == 1) return (cantidadRiegos == 1 || cantidadRiegos == 2 || cantidadRiegos == 3); // AM
if (tipoRiego == 2) return (cantidadRiegos == 3); // MD
if (tipoRiego == 3) return (cantidadRiegos == 2 || cantidadRiegos == 3); // PM
if (tipoRiego == 4) return (cantidadRiegos == 1 || cantidadRiegos == 2 || cantidadRiegos == 3); // TEST
return false;
}
int buscarSiguienteValvula(int inicioIdx, int tipo) {
for (int i = inicioIdx; i < 22; i++) {
if (esValvulaApta(i, tipo)) return i; // Vinculado a tu filtro original
}
return -1;
}
void iniciarSecuenciaRiego(int tipo, int minutos, int valvulaInicial) {
registrarActividad();
tipoRiegoActivo = tipo; duracionRiegoZonaMs = (unsigned long)minutos * 60000UL;
duracionPasoActual = minutos; turnoActual = tipo;
int primera = buscarSiguienteValvula(0, tipo);
if (primera != -1) {
estadoRiego = ENCENDIDO_SEGURO_ON; cronometroSecuencia = millis(); cronometroPaso = millis();
valvulaActualIndice = primera; valvulaActivaActual = primera; secuenciaActiva = true;
if (tipo == 4) modoTesteo = true; else modoTesteo = false;
digitalWrite(pinInicioValvulas + primera, LOW); // Abre la primera zona (LOW = ON)
}
}
// ============================================================================
// SETUP ORIGINAL CON FILTROS E INTERRUPCIÓN EMITIDA
// ============================================================================
void setup() {
Serial.begin(115200);
Serial3.begin(115200); // Conexión por hardware dedicada a los pines 14 y 15 hacia la Wemos
Serial3.setTimeout(50); // <-- ACELERADOR: Si el cable se corta, el Mega se libera e
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
oled.clearDisplay();
oled.display();
bmp.begin(0x76);
rtc.begin();
// ==========================================================================
// SOLUCIÓN AL CONFLICTO DE TIEMPO: ACELERADOR DEL BUS I2C COMPARTIDO
// ==========================================================================
Wire.setClock(100000UL); // Fuerza a la pantalla a transmitir rápido y liberar el reloj
pinMode(pinCLK, INPUT_PULLUP); pinMode(pinDT, INPUT_PULLUP); pinMode(pinSW, INPUT_PULLUP);
pinMode(pinBTN1, INPUT_PULLUP); pinMode(pinBTN2, INPUT_PULLUP);
pinMode(pinBTN3, INPUT_PULLUP); pinMode(pinBTN4, INPUT_PULLUP);
pinMode(pinV23, OUTPUT); pinMode(pinV24, OUTPUT);
for(int i = 0; i < 22; i++) {
pinMode(pinInicioValvulas + i, OUTPUT);
digitalWrite(pinInicioValvulas + i, HIGH); // Mantiene los relés apagados desde el milisegundo cero
}
// LÍNEA INTERRUPTORA REPUESTA EN SU LUGAR FIJO ORIGINAL (EVITA BLOQUEOS):
//attachInterrupt(digitalPinToInterrupt(pinCLK), muestreoEncoder, FALLING);
attachInterrupt(digitalPinToInterrupt(pinCLK), muestreoEncoder, RISING);
EEPROM.get(0, config);
// Inicialización de parámetros seguros por EEPROM virgen (Filtro anterior)
if(config.topePresion < 500 || config.topePresion > 1030) {
config.topePresion = 1013; config.horaAM = 7; config.minutoAM = 0;
config.horaMD = 13; config.minutoMD = 0; config.horaPM = 19; config.minutoPM = 0;
config.tiempoAM = 10; config.tiempoMD = 10; config.tiempoPM = 10; config.tiempoTest = 2;
config.tempCorte = 3; config.modoInvierno = false;
for(int i=0; i<22; i++) config.estadoValvulas[i] = 1;
}
// COMENTA AQUÍ LA LÍNEA PARA ACTUALIZAR: ####################33
// rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
// #########################################################33
EEPROM.put(0, config);
configMinutosTesteo = config.tiempoTest;
configPresionBloqueo = config.topePresion;
riegoAM.hora = config.horaAM; riegoAM.minuto = config.minutoAM;
riegoMD.hora = config.horaMD; riegoMD.minuto = config.minutoMD;
riegoPM.hora = config.horaPM; riegoPM.minuto = config.minutoPM;
presionActual = bmp.readPressure() / 100.0F;
tempActual = bmp.readTemperature();
tiempoUltimaActividad = millis();
}
// ============================================================================
void loop() {
unsigned long tiempoActual = millis();
// 0. MONITOREO reloj por conflicto
// 1. MONITOREO AMBIENTAL ASÍNCRONO (CADA 15 MINUTOS EXACTOS)
if (tiempoActual - ultimaLecturaBmp >= INTERVALO_BMP) {
ultimaLecturaBmp = tiempoActual;
tempActual = bmp.readTemperature();
presionActual = bmp.readPressure() / 100.0F;
ultimaTempBMP = tempActual; // Sincroniza con tus variables de pantalla
ultimaPresionBMP = presionActual;
}
// 2. Temporizador para ahorro de energía en la pantalla OLED
// Corta la energía de los píxeles de forma estricta a los 2 minutos de inactividad.
if (pantallaEncendida && (tiempoActual - tiempoUltimaActividad > TIEMPO_ESPERA_PANTALLA)) {
pantallaEncendida = false;
oled.ssd1306_command(SSD1306_DISPLAYOFF); // Apaga físicamente el cristal I2C
oled.clearDisplay();
}
// 3. VERIFICACIÓN AUTOMÁTICA DE HORARIOS DE RIEGO INTELIGENTE
if (estadoRiego == APAGADO) {
DateTime ahora = rtc.now();
if (ahora.second() == 0) {
if ( config.modoInvierno == true ) {faseSecuencia = 1;}
if ( tempActual < config.tempCorte ) {faseSecuencia = 2;}
if ( presionActual > config.topePresion ) {faseSecuencia = 3;}
if (ahora.hour() == config.horaAM && ahora.minute() == config.minutoAM && !riegoAM_Ejecutado) {
riegoAM_Ejecutado = true;
if (!config.modoInvierno && tempActual > config.tempCorte && presionActual < config.topePresion) {
iniciarSecuenciaRiego(1, config.tiempoAM, 0); faseSecuencia = 0;
}
}
else if (ahora.hour() == config.horaMD && ahora.minute() == config.minutoMD && !riegoMD_Ejecutado) {
riegoMD_Ejecutado = true;
if (!config.modoInvierno && tempActual > config.tempCorte && presionActual < config.topePresion) {
iniciarSecuenciaRiego(2, config.tiempoMD, 1); faseSecuencia = 0;
}
}
else if (ahora.hour() == config.horaPM && ahora.minute() == config.minutoPM && !riegoPM_Ejecutado) {
riegoPM_Ejecutado = true;
if (!config.modoInvierno && tempActual > config.tempCorte && presionActual < config.topePresion) {
iniciarSecuenciaRiego(3, config.tiempoPM, 2); faseSecuencia = 0;
}
}
}
// Reset diario automático a la medianoche exacta
if (ahora.hour() == 0 && ahora.minute() == 0 && ahora.second() == 1) {
riegoAM_Ejecutado = false; riegoMD_Ejecutado = false; riegoPM_Ejecutado = false;
}
}
// 4. CONTROL DIRECTO DE BOTONES LOCALES DE CAMPO (RELES INVERTIDOS: LOW = ON)
// --- BOTÓN : STOP SEGURO DE EMERGENCIA (Pin 8) ---
if (digitalRead(pinBTN4) == LOW) {
delay(50);
if (digitalRead(pinBTN4) == LOW) {
// registrarActividad();
ejecutarStop();
while (digitalRead(pinBTN4) == LOW);
}
}
// --- BOTÓN : PAUSA / REANUDAR HIDRÁULICO DE CAMPO (Pin 7) ---
if (digitalRead(pinBTN3) == LOW) {
delay(50);
if (digitalRead(pinBTN3) == LOW) {
registrarActividad();
if (estadoRiego != APAGADO && estadoRiego != ENCENDIDO_SEGURO_ON && estadoRiego != APAGADO_SEGURO_OFF) {
enPausa = !enPausa;
if (enPausa) {
tiempoRestanteEnPausa = duracionRiegoZonaMs - (tiempoActual - cronometroSecuencia);
enSolapamiento = false;
digitalWrite(pinV24, HIGH); // APAGA ÚNICAMENTE LA BOMBA EN LA PAUSA (HIGH = OFF)
}
// CASO B: Al salir de la Pausa (Reanudación instantánea de presión)
else {
cronometroSecuencia = millis() - (duracionRiegoZonaMs - tiempoRestanteEnPausa);
if (estadoRiego == MANUAL_V23_RUN || estadoRiego == MANUAL_V23_ON) {
digitalWrite(pinV23, LOW); // Reabre la manguera manual (LOW = ON)
digitalWrite(pinV24, LOW); // Reenciende la bomba principal en verde (LOW = ON)
}
else if (estadoRiego == SOLAPADO && valvulaActualIndice != -1) {
digitalWrite(pinInicioValvulas + valvulaActualIndice, LOW); // Reabre zona automática (LOW = ON)
digitalWrite(pinV24, LOW); // Reenciende la bomba principal en verde (LOW = ON)
}
}
Serial3.println(F("CMD:PAUSA"));
}
while (digitalRead(pinBTN3) == LOW);
}
}
// BOTÓN 1: MANGUERA MANUAL FÍSICA (PIN 5 - FILTRADO ASÍNCRONO SIN BLOQUEOS)
static int ultimoEstadoBTN1 = HIGH; // Almacena el estado de la vuelta anterior
int estadoBTN1 = digitalRead(pinBTN1);
// Detecta el momento exacto en que el botón pasa de estar suelto (HIGH) a presionado (LOW)
if (estadoRiego == APAGADO && estadoBTN1 == LOW && ultimoEstadoBTN1 == HIGH) {
delay(50); // Filtro antirebote mecánico estándar
if (digitalRead(pinBTN1) == LOW) {
registrarActividad(); // Despierta eléctricamente los píxeles de la OLED local
estadoRiego = MANUAL_V23_ON;
cronometroSecuencia = millis();
cronometroManual = millis(); // Sincroniza el cronómetro de tu pantalla oled nativa
modoManualActivo = true; // Enciende el letrero gráfico 'manual' en tu mesa
faseManual = 2;
duracionRiegoZonaMs = 7200000UL;
digitalWrite(pinV23, LOW); // Abre la electroválvula de manguera (LOW = ON)
digitalWrite(pinV24, HIGH); // Arranca el contactor de la bomba principal (HIGH = ON)
// Despacha los dos paquetes por el puerto serie Serial3 sin interrupciones
Serial3.println(F("CMD:MANUAL"));
}
}
ultimoEstadoBTN1 = estadoBTN1; // Actualiza la bandera en la RAM para la siguiente vuelta libre del loop
// --- BOTÓN : RIEGO TEST LOCAL (Pin 6) ---
if (estadoRiego == APAGADO && digitalRead(pinBTN2) == LOW) {
delay(50);
if (digitalRead(pinBTN2) == LOW) {
registrarActividad();
iniciarSecuenciaRiego(4, config.tiempoTest, 9);
Serial3.println(F("CMD:TEST"));
while (digitalRead(pinBTN2) == LOW);
}
}
// 5. GESTIÓN DEL ENCODER FÍSICO ASÍNCRONO (TUS VARIABLES REALES DEL COMPILADOR)
if (posicionEncoder != ultimaPosicionEncoder) {
int pasos = posicionEncoder - ultimaPosicionEncoder;
ultimaPosicionEncoder = posicionEncoder;
// Si la pantalla estaba apagada por inactividad, el primer giro solo la despierta de forma segura
if (!pantallaEncendida) {
registrarActividad();
}
else {
registrarActividad(); // Refresca el temporizador de inactividad de campo
if (estadoRiego == APAGADO) {
// ------------------------------------------------------------------------
// CONDICIÓN A: MODO EDICIÓN ACTIVO (ASTERISCO '*'). MODIFICA VALORES EN RAM
// ------------------------------------------------------------------------
if (modoEdicion) {
// 1. SUBMENÚ CANT. RIEGOS: Cambia cíclicamente de 0 a 3 sin mover la flecha
if (estadoActual == SUBMENU_HORARIOS && opcionSubmenu < 22) {
config.estadoValvulas[opcionSubmenu] = constrain(config.estadoValvulas[opcionSubmenu] + pasos, 0, 3);
}
// 2. SUBMENÚ HORAS DE RIEGO: Modifica HH o MM según pasoEdicion en el renglón estático
else if (estadoActual == SUBMENU_HORAS_RIEGO) {
if (pasoEdicion == 1) { // Edición de Horas (0 a 23)
if (opcionSubmenu == 0) config.horaAM = constrain(config.horaAM + pasos, 0, 23);
else if (opcionSubmenu == 1) config.horaMD = constrain(config.horaMD + pasos, 0, 23);
else if (opcionSubmenu == 2) config.horaPM = constrain(config.horaPM + pasos, 0, 23);
}
else if (pasoEdicion == 2) { // Edición de Minutos (0 a 59)
if (opcionSubmenu == 0) config.minutoAM = constrain(config.minutoAM + pasos, 0, 59);
else if (opcionSubmenu == 1) config.minutoMD = constrain(config.minutoMD + pasos, 0, 59);
else if (opcionSubmenu == 2) config.minutoPM = constrain(config.minutoPM + pasos, 0, 59);
}
}
// 3. SUBMENÚ DURACIÓN DE RIEGO: Modifica de 0 a 45 min el programa congelado
else if (estadoActual == SUBMENU_MINUTOS_RIEGO) {
if (opcionSubmenu == 0) config.tiempoAM = constrain(config.tiempoAM + pasos, 0, 45);
else if (opcionSubmenu == 1) config.tiempoMD = constrain(config.tiempoMD + pasos, 0, 45);
else if (opcionSubmenu == 2) config.tiempoPM = constrain(config.tiempoPM + pasos, 0, 45);
}
// 4. SUBMENÚ MINUTO TEST: Ajusta de 0 a 45 min y sincroniza el lienzo local
else if (estadoActual == SUBMENU_TESTEO) {
config.tiempoTest = constrain(config.tiempoTest + pasos, 0, 45);
configMinutosTesteo = config.tiempoTest; // Actualización en vivo de la pantalla
}
// 5. SUBMENÚ MODO INVIERNO: Conmuta limpiamente entre true/false con el giro
else if (estadoActual == SUBMENU_INVIERNO) {
config.modoInvierno = !config.modoInvierno;
}
// 6. SUBMENÚ PRESION BLOQUEO: Sube o baja de 5 en 5 hPa sin saltar al botón Atrás
else if (estadoActual == SUBMENU_BLOQUEO) {
config.topePresion = constrain(config.topePresion + pasos * 5, 500, 1030);
configPresionBloqueo = config.topePresion; // Actualización en vivo de la pantalla
}
// 7. SUBMENÚ TEMP DE CORTE: Modifica estrictamente de 1 a 20 grados
else if (estadoActual == SUBMENU_TEMP) {
config.tempCorte = constrain(config.tempCorte + pasos, 1, 20);
}
}
// ------------------------------------------------------------------------
// CONDICIÓN B: MODO NAVEGACIÓN (SIN ASTERISCO). EL ENCODER MUEVE LAS FLECHAS
// ------------------------------------------------------------------------
else {
if (estadoActual == MENU_PRINCIPAL) {
opcionPrincipal = constrain(opcionPrincipal + pasos, 0, 7); // Menú de 8 opciones
}
else if (estadoActual == SUBMENU_HORARIOS) {
opcionSubmenu = constrain(opcionSubmenu + pasos, 0, 22); // 22 Válvulas + Botón Salir
}
else if (estadoActual == SUBMENU_HORAS_RIEGO) {
opcionSubmenu = constrain(opcionSubmenu + pasos, 0, 3); // 3 Programas + Botón Salir
}
else if (estadoActual == SUBMENU_MINUTOS_RIEGO) {
opcionSubmenu = constrain(opcionSubmenu + pasos, 0, 3); // 3 Duraciones + Botón Salir
}
// Filtro para submenús cortos de dos líneas (0 = Ajustar Parámetro, 1 = Botón Atrás)
else if (estadoActual == SUBMENU_TESTEO || estadoActual == SUBMENU_INVIERNO ||
estadoActual == SUBMENU_BLOQUEO || estadoActual == SUBMENU_TEMP) {
opcionSubmenu = constrain(opcionSubmenu + pasos, 0, 1);
}
}
}
}
}
// --- CONTROL DEL CLIC DEL ENCODER (PULSADOR SW - BLINDADO SIN RETURNS TRABADORES) ---
static int ultimoEstadoSW = HIGH;
int estadoSW = digitalRead(pinSW);
if (estadoSW == LOW && ultimoEstadoSW == HIGH) {
delay(50); // Filtro antirebote mecánico rápido estándar de Arduino
if (digitalRead(pinSW) == LOW) {
if (!pantallaEncendida) {
registrarActividad();
} else {
registrarActividad(); // Refresca el reloj de inactividad al pulsar
if (estadoRiego == APAGADO) {
switch (estadoActual) {// Switch compacto unificado que opera con tus variables unificadas reales de RAM
case MODO_INICIO:
estadoActual = MENU_PRINCIPAL;
opcionPrincipal = 0;
break;
case MENU_PRINCIPAL:
if (opcionPrincipal == 0) { estadoActual = SUBMENU_HORARIOS; opcionSubmenu = 0; modoEdicion = false; }
else if (opcionPrincipal == 1) { estadoActual = SUBMENU_HORAS_RIEGO; opcionSubmenu = 0; modoEdicion = false; pasoEdicion = 0; }
else if (opcionPrincipal == 2) { estadoActual = SUBMENU_MINUTOS_RIEGO; opcionSubmenu = 0; modoEdicion = false; }
else if (opcionPrincipal == 3) { estadoActual = SUBMENU_TESTEO; opcionSubmenu = 0; modoEdicion = true; }
else if (opcionPrincipal == 4) { estadoActual = SUBMENU_INVIERNO; opcionSubmenu = 0; modoEdicion = true; }
else if (opcionPrincipal == 5) { estadoActual = SUBMENU_BLOQUEO; opcionSubmenu = 0; modoEdicion = true; }
else if (opcionPrincipal == 6) { estadoActual = SUBMENU_TEMP; opcionSubmenu = 0; modoEdicion = true; }
else if (opcionPrincipal == 7) {
EEPROM.put(0, config);
estadoActual = MODO_INICIO; // Graba de forma segura y regresa directo al reloj de inicio
}
break;
case SUBMENU_HORARIOS:
// Opciones 0 a 21: El usuario edita la asignación de riegos de las 22 zonas
if (opcionSubmenu < 22) {
if (modoEdicion) {
// Segundo Clic: Confirma el valor (0-3), apaga el asterisco y graba físicamente de inmediato
EEPROM.put(0, config);
modoEdicion = false; // Libera el encoder para volver a mover la flecha >
} else {
// Primer Clic: Enciende el asterisco '*' para congelar el cursor y poder cambiar de 0 a 3
modoEdicion = true;
}
}
// Opción 22: El usuario bajó con scroll hasta "Atras..." y presionó el botón
else if (opcionSubmenu == 22) {
modoEdicion = false;
EEPROM.put(0, config); // Guardado de resguardo final antes de salir
estadoActual = MENU_PRINCIPAL; // Regresa de forma segura al menú de 8 opciones
opcionSubmenu = 0; // Limpia el índice para la próxima navegación
}
break;
case SUBMENU_HORAS_RIEGO:
// Opciones 0, 1 o 2: El usuario edita los programas AM, MD o PM
if (opcionSubmenu < 3) {
pasoEdicion++; // Incrementa: 1 = Edita Horas, 2 = Edita Minutos
// Si ya pasó de 2, significa que terminó la edición fina de ese turno
if (pasoEdicion > 2) {
pasoEdicion = 0;
modoEdicion = false; // Apaga el asterisco y libera la flecha >
EEPROM.put(0, config);
} else {
modoEdicion = true; // Activa el asterisco de edición activa
}
}
// Opción 3: El usuario está parado sobre "Atras..." y presionó el botón
else {
modoEdicion = false;
pasoEdicion = 0;
estadoActual = MENU_PRINCIPAL; // Regresa de forma segura al menú de 8 opciones
opcionSubmenu = 0; // Limpia el índice para la próxima navegación
}
break;
case SUBMENU_MINUTOS_RIEGO:
if (opcionSubmenu == 3) { EEPROM.put(0, config); estadoActual = MENU_PRINCIPAL; } else modoEdicion = !modoEdicion;
break;
case SUBMENU_TESTEO:
// Si el usuario está parado en la opción 0 (Tiempo de Test)
if (opcionSubmenu == 0) {
if (modoEdicion) {
// Segundo Clic: Acepta el valor, lo asegura en la EEPROM y apaga el asterisco
config.tiempoTest = configMinutosTesteo; // Sincroniza la estructura final
EEPROM.put(0, config);
modoEdicion = false;
} else {
// Primer Clic: Enciende el asterisco para editar de 0 a 45 min
modoEdicion = true;
}
}
// Si el usuario bajó a la opción 1 (Atras...) y hace clic
else if (opcionSubmenu == 1) {
modoEdicion = false;
estadoActual = MENU_PRINCIPAL; // Te libera al menú principal de forma segura
opcionSubmenu = 0; // Resetea el índice para el siguiente ingreso
}
break;
case SUBMENU_INVIERNO:
// Opción 0: El usuario está parado sobre la línea "Estado: ON/OFF"
if (opcionSubmenu == 0) {
if (modoEdicion) {
// Segundo Clic: Confirma el estado seleccionado, graba en EEPROM y apaga el asterisco
EEPROM.put(0, config);
modoEdicion = false;
} else {
// Primer Clic: Enciende el asterisco '*' para bloquear la flecha y poder conmutar con el encoder
modoEdicion = true;
}
}
// Opción 1: El usuario bajó la flecha '>' a la línea "Atras..." y presionó el botón
else if (opcionSubmenu == 1) {
modoEdicion = false;
estadoActual = MENU_PRINCIPAL; // Regresa de forma segura al menú de 8 opciones
opcionSubmenu = 0; // Limpia el índice para la próxima navegación
}
break;
case SUBMENU_BLOQUEO:
// Opción 0: El usuario está parado sobre la línea "Tope: XXXX hPa"
if (opcionSubmenu == 0) {
if (modoEdicion) {
// Segundo Clic: Confirma el valor de hPa, lo sella en la estructura y graba físicamente en EEPROM
config.topePresion = configPresionBloqueo; // Sincronización absoluta de RAM hacia EEPROM
EEPROM.put(0, config);
modoEdicion = false;
} else {
// Primer Clic: Enciende el asterisco '*' para congelar el cursor y poder cambiar los hPa con el encoder
modoEdicion = true;
}
}
// Opción 1: El usuario bajó la flecha '>' a la línea "Atras..." y presionó el botón
else if (opcionSubmenu == 1) {
modoEdicion = false;
estadoActual = MENU_PRINCIPAL; // Regresa de forma segura al menú de 8 opciones
opcionSubmenu = 0; // Limpia el índice para la próxima navegación
}
break;
case SUBMENU_TEMP:
// Opción 0: El usuario está parado sobre la línea "Corte a: XX C"
if (opcionSubmenu == 0) {
if (modoEdicion) {
// Segundo Clic: Confirma los grados, los sella en la estructura y graba físicamente en EEPROM
EEPROM.put(0, config);
modoEdicion = false;
} else {
// Primer Clic: Enciende el asterisco '*' para congelar el cursor y poder cambiar los grados con el encoder
modoEdicion = true;
}
}
// Opción 1: El usuario bajó la flecha '>' a la línea "Atras..." y presionó el botón
else if (opcionSubmenu == 1) {
modoEdicion = false;
estadoActual = MENU_PRINCIPAL; // Regresa de forma segura al menú de 8 opciones
opcionSubmenu = 0; // Limpia el índice para la próxima navegación
}
break;
}
}
}
}
}
ultimoEstadoSW = estadoSW; // Sincroniza la bandera abajo para eliminar el encierro permanente
// 6. RECEPCIÓN DE COMANDOS WEB (DESDE LA WEMOS EN CALIBRACIÓN INVERTIDA: LOW = ON)
if (Serial3.available() > 0) {
registrarActividad();
String cmdWeb = Serial3.readStringUntil('\n'); cmdWeb.trim();
if (cmdWeb == "CMD:STOP") {
ejecutarStop();
modoManualActivo = false; faseManual = 0; secuenciaActiva = false; // Baja tus banderas gráficas
}
else if (cmdWeb == "CMD:MANUAL" && estadoRiego == APAGADO) {
estadoRiego = MANUAL_V23_ON;
cronometroSecuencia = millis();
cronometroManual = millis();
modoManualActivo = true; faseManual = 2; // Activa los letreros decrecientes de tu pantalla nativa
duracionRiegoZonaMs = 7200000UL; // Fija las 2 horas de resguardo matemático (7.200.000 ms)
digitalWrite(pinV23, LOW); // Abre la electroválvula de la manguera manual (LOW = ON)
digitalWrite(pinV24, LOW); // Activa el contactor de la bomba principal (LOW = ON)
}
else if (cmdWeb == "CMD:TEST" && estadoRiego == APAGADO) {
iniciarSecuenciaRiego(4, config.tiempoTest, 9);
}
else if (cmdWeb == "CMD:PAUSA") {
if (estadoRiego != APAGADO && estadoRiego != ENCENDIDO_SEGURO_ON && estadoRiego != APAGADO_SEGURO_OFF) {
enPausa = !enPausa;
if (enPausa) {
tiempoRestanteEnPausa = duracionRiegoZonaMs - (tiempoActual - cronometroSecuencia);
enSolapamiento = false;
digitalWrite(pinV24, HIGH); // Corta la bomba principal en la pausa remota (HIGH = OFF)
}
else {
cronometroSecuencia = millis() - (duracionRiegoZonaMs - tiempoRestanteEnPausa);
if (estadoRiego == MANUAL_V23_RUN || estadoRiego == MANUAL_V23_ON) {
digitalWrite(pinV23, LOW); digitalWrite(pinV24, LOW); // Reenciende manguera y bomba
} else {
if (estadoRiego == SOLAPADO && valvulaActualIndice != -1) digitalWrite(pinInicioValvulas + valvulaActualIndice, LOW);
digitalWrite(pinV24, LOW); // Reenciende bomba automática
}
}
}
}
}
// 7. MAQUINA DE ESTADOS HIDRÁULICA INDUSTRIAL (SIN INTERFERENCIA DE NOMBRES)
if (!enPausa && estadoRiego != APAGADO) {
switch (estadoRiego) {
case ENCENDIDO_SEGURO_ON:
if (tiempoActual - cronometroSecuencia >= 2000) {
digitalWrite(pinV24, LOW); estadoRiego = SOLAPADO;
cronometroSecuencia = millis();
cronometroPaso = millis();
secuenciaActiva = true; valvulaActivaActual = valvulaActualIndice; // Sincroniza visor local
}
break;
case SOLAPADO:
if (!enSolapamiento) {
if (tiempoActual - cronometroSecuencia >= duracionRiegoZonaMs) {
int proximaValvula = buscarSiguienteValvula(valvulaActualIndice + 1, tipoRiegoActivo);
if (proximaValvula != -1) {
digitalWrite(pinInicioValvulas + proximaValvula, LOW);
valvulaAnteriorIndice = valvulaActualIndice;
valvulaActualIndice = proximaValvula;
valvulaActivaActual = proximaValvula;
enSolapamiento = true; cronometroSolapado = millis();
} else {
digitalWrite(pinV24, HIGH); // Fin de secuencia, apaga presión primero
estadoRiego = APAGADO_SEGURO_OFF; cronometroSecuencia = millis();
}
}
} else {
if (tiempoActual - cronometroSolapado >= 2000) {
if (valvulaAnteriorIndice != -1) digitalWrite(pinInicioValvulas + valvulaAnteriorIndice, HIGH);
enSolapamiento = false; cronometroSecuencia = millis(); cronometroPaso = millis();
}
}
break;
case APAGADO_SEGURO_OFF:
if (tiempoActual - cronometroSecuencia >= 2000) {
if (valvulaActualIndice != -1) digitalWrite(pinInicioValvulas + valvulaActualIndice, HIGH);
apagarTodoFisico(); estadoRiego = APAGADO; secuenciaActiva = false;
}
break;
case MANUAL_V23_ON:
if (tiempoActual - cronometroSecuencia >= 2000) {
digitalWrite(pinV24, LOW);
estadoRiego = MANUAL_V23_RUN;
cronometroSecuencia = millis();
}
break;
// CASO ADICIONAL AGREGADO: MANGUERA MANUAL EN RÉGIMEN DE MARCHA (2 HORAS)
case MANUAL_V23_RUN:
// Monitorea de forma continua si ya se cumplieron las 2 horas máximas de resguardo en terreno
if (tiempoActual - cronometroSecuencia >= duracionRiegoZonaMs) {
ejecutarStop(); // Al cumplirse el plazo, ejecuta el corte total de seguridad en el jardín
modoManualActivo = false; faseManual = 0; secuenciaActiva = false; // Limpia las banderas nativas
}
break;
default: break;
}
}
// 8. TRANSMISIÓN DE TELEMETRÍA DATA HACIA LA WEMOS CADA 2 SEGUNDOS
static unsigned long ultimoEnvioWeb = 0;
if (tiempoActual - ultimoEnvioWeb >= 2000) {
ultimoEnvioWeb = tiempoActual;
DateTime ahora = rtc.now();
unsigned long tiempoTranscurrido = 0;
unsigned long tiempoRestanteMs = 0;
// CORRECCIÓN CRÍTICA DE MEMORIA: Arreglo de 12 bytes para evitar que el Mega se congele o reinicie
char bufRes[12];
Serial3.print(F("DATA:"));
if(ahora.hour()<10) Serial3.print("0"); Serial3.print(ahora.hour()); Serial3.print(":");
if(ahora.minute()<10) Serial3.print("0"); Serial3.print(ahora.minute());
Serial3.print(" "); Serial3.print(ahora.day()); Serial3.print("/");
Serial3.print(ahora.month());
Serial3.print(","); Serial3.print(tempActual, 1); Serial3.print(",");
Serial3.print(presionActual, 1);
// Sincronización absoluta: Manda un "1" verde a la web únicamente si la máquina está regando en manual
Serial3.print(","); Serial3.print((estadoRiego == MANUAL_V23_RUN || estadoRiego == MANUAL_V23_ON) ? "1" : "0"); // Manguera Manual
Serial3.print(","); Serial3.print((digitalRead(pinV24) == LOW) ? "1" : "0"); // Bomba Principal (LOW = ON)
Serial3.print(","); Serial3.print(config.tiempoTest);
// char bufRes;
if (enPausa) {
unsigned long tot = tiempoRestanteEnPausa / 1000;
sprintf(bufRes, "%02lu:%02lu:%02lu", tot / 3600, (tot % 3600) / 60, tot % 60);
Serial3.print(","); Serial3.print(bufRes);
}
else if (estadoRiego != APAGADO) {
unsigned long tiempoTranscurrido = 0;
unsigned long tiempoRestanteMs = 0;
}
// Si el riego activo es la manguera manual, calcula el descuento usando cronometroManual
if (estadoRiego == MANUAL_V23_RUN || estadoRiego == MANUAL_V23_ON) {
tiempoTranscurrido = tiempoActual - cronometroManual;
tiempoRestanteMs = (7200000UL > tiempoTranscurrido) ? (7200000UL - tiempoTranscurrido) : 0; // son dos horas
}
// Si es riego automático o modo test, usa la base de la secuencia
else {
tiempoTranscurrido = tiempoActual - cronometroSecuencia;
tiempoRestanteMs = (duracionRiegoZonaMs > tiempoTranscurrido) ? (duracionRiegoZonaMs - tiempoTranscurrido) : 0;
}
unsigned long tot = tiempoRestanteMs / 1000;
sprintf(bufRes, "%02lu:%02lu:%02lu", tot / 3600, (tot % 3600) / 60, tot % 60);
Serial3.print(","); Serial3.print(bufRes);
Serial3.print(F(","));
for (int i = 0; i < 22; i++) {
if(valvulaActualIndice == i && (estadoRiego == SOLAPADO || estadoRiego == ENCENDIDO_SEGURO_ON)) Serial3.print("1");
else Serial3.print("0");
}
// Transmisión de fase secuencia para datos históricos
Serial3.print(F(","));
Serial3.print(faseSecuencia);
Serial3.print(F(","));
for (int i = 0; i < 22; i++) Serial3.print(config.estadoValvulas[i]);
// ==========================================================================
// NUEVO: AGREGADO DE LOS 6 VALORES DE HORARIOS CONFIGURADOS (AM, MD, PM)
// Se envían separados por comas para que la Wemos los pueda rescatar de forma aislada
// ==========================================================================
Serial3.print(F(",")); Serial3.print(config.horaAM);
Serial3.print(F(",")); Serial3.print(config.minutoAM);
Serial3.print(F(",")); Serial3.print(config.horaMD);
Serial3.print(F(",")); Serial3.print(config.minutoMD);
Serial3.print(F(",")); Serial3.print(config.horaPM);
Serial3.print(F(",")); Serial3.print(config.minutoPM);
Serial3.println();
}
// 9. FIVE-CORE ASÍNCRONO ORIGINAL (TUS CINCO LÍNEAS NATIVAS DE FÁBRICA)
verificarInactividad(); // Mantiene la cuenta estricta de los 2 minutos de inactividad
if (pantallaEncendida) {
actualizarPantalla(); // REENLAZADO: Llama a tu función real con el objeto oled
} else {
oled.ssd1306_command(SSD1306_DISPLAYOFF); // Candado de resguardo para corte de energía I2C
}
delay(10);
}
// ============================================================================
// GESTIÓN ENERGÉTICA: APAGADOR AUTOMÁTICO SEGURO DE PANTALLA OLED
// ============================================================================
void verificarInactividad() {
if (estadoActual == MODO_INICIO && pantallaEncendida && !secuenciaActiva && !modoManualActivo) {
// CORRECCIÓN MAESTRA: Cambiado 'ultimoTiempoActividad' por 'tiempoUltimaActividad'
if (millis() - tiempoUltimaActividad >= TIEMPO_ESPERA_PANTALLA) {
oled.ssd1306_command(SSD1306_DISPLAYOFF); // Corta la energía de los píxeles de la OLED
pantallaEncendida = false;
}
}
}
//============================================================================
// INTERRUPCIÓN ULTRA-RÁPIDA Y ESTABLE DEL ENCODER ORIGINAL
// ============================================================================
void muestreoEncoder() {
if (digitalRead(pinCLK) != digitalRead(pinDT)) posicionEncoder++;
else posicionEncoder--;
}
// ============================================================================
// MOTOR DE GIRO: MANEJO MECÁNICO DE PASOS (TU FUNCIÓN ORIGINAL DIRECTA)
// ============================================================================
void manejarGiroEncoder(int pasos) {
if (!modoEdicion) {
if (estadoActual == MENU_PRINCIPAL) opcionPrincipal = constrain(opcionPrincipal + pasos, 0, 7); // 8 Opciones (0 al 7)
else if (estadoActual == SUBMENU_HORARIOS) opcionSubmenu = constrain(opcionSubmenu + pasos, 0, 22); // Guardar abajo es el 22
else if (estadoActual == SUBMENU_HORAS_RIEGO) opcionSubmenu = constrain(opcionSubmenu + pasos, 0, 3);
else if (estadoActual == SUBMENU_MINUTOS_RIEGO) opcionSubmenu = constrain(opcionSubmenu + pasos, 0, 3);
else if (estadoActual == SUBMENU_TESTEO) opcionSubmenu = constrain(opcionSubmenu + pasos, 0, 1);
else if (estadoActual == SUBMENU_BLOQUEO) opcionSubmenu = constrain(opcionSubmenu + pasos, 0, 1);
else if (estadoActual == SUBMENU_INVIERNO) opcionSubmenu = constrain(opcionSubmenu + pasos, 0, 1);
else if (estadoActual == SUBMENU_TEMP) opcionSubmenu = constrain(opcionSubmenu + pasos, 0, 1);
} else {
if (estadoActual == SUBMENU_HORARIOS) config.estadoValvulas[opcionSubmenu] = constrain(config.estadoValvulas[opcionSubmenu] + pasos, 0, 3);
else if (estadoActual == SUBMENU_HORAS_RIEGO) {
RiegoHorario* hSel = (opcionSubmenu==0)?&riegoAM:(opcionSubmenu==1)?&riegoMD:&riegoPM;
if (pasoEdicion == 1) hSel->hora = constrain(hSel->hora + pasos, 0, 23);
if (pasoEdicion == 2) hSel->minuto = constrain(hSel->minuto + pasos, 0, 59);
}
else if (estadoActual == SUBMENU_MINUTOS_RIEGO) {
if (opcionSubmenu == 0) duracionRiegoAM = constrain(duracionRiegoAM + pasos, 0, 45);
if (opcionSubmenu == 1) duracionRiegoMD = constrain(duracionRiegoMD + pasos, 0, 45);
if (opcionSubmenu == 2) duracionRiegoPM = constrain(duracionRiegoPM + pasos, 0, 45);
}
else if (estadoActual == SUBMENU_TESTEO && opcionSubmenu == 0) configMinutosTesteo = constrain(configMinutosTesteo + pasos, 0, 45);
else if (estadoActual == SUBMENU_INVIERNO && opcionSubmenu == 0) config.modoInvierno = !config.modoInvierno;
else if (estadoActual == SUBMENU_BLOQUEO && opcionSubmenu == 0) configPresionBloqueo = constrain(configPresionBloqueo + pasos, 500, 1030);
else if (estadoActual == SUBMENU_TEMP && opcionSubmenu == 0) config.tempCorte = constrain(config.tempCorte + pasos, 0, 20);
}
}
// ============================================================================
// MOTOR DE SELECCIÓN: CONTROL DE CLIC (TU FUNCIÓN ORIGINAL DIRECTA)
// ============================================================================
void manejarClickEncoder() {
switch (estadoActual) {
case MODO_INICIO: estadoActual = MENU_PRINCIPAL; opcionPrincipal = 0; break;
case MENU_PRINCIPAL:
if (opcionPrincipal == 0) { estadoActual = SUBMENU_HORARIOS; opcionSubmenu = 0; modoEdicion = false; }
else if (opcionPrincipal == 1) { estadoActual = SUBMENU_HORAS_RIEGO; opcionSubmenu = 0; modoEdicion = false; }
else if (opcionPrincipal == 2) { estadoActual = SUBMENU_MINUTOS_RIEGO; opcionSubmenu = 0; modoEdicion = false; }
else if (opcionPrincipal == 3) { estadoActual = SUBMENU_TESTEO; opcionSubmenu = 0; modoEdicion = false; }
else if (opcionPrincipal == 4) { estadoActual = SUBMENU_INVIERNO; opcionSubmenu = 0; modoEdicion = false; } // 5. Invierno
else if (opcionPrincipal == 5) { estadoActual = SUBMENU_BLOQUEO; opcionSubmenu = 0; modoEdicion = false; } // 6. Presión
else if (opcionPrincipal == 6) { estadoActual = SUBMENU_TEMP; opcionSubmenu = 0; modoEdicion = false; } // 7. Temperatura
else if (opcionPrincipal == 7) { config.tiempoTest = configMinutosTesteo; config.topePresion = configPresionBloqueo; EEPROM.put(0, config); estadoActual = MODO_INICIO; } // 8. Salir
break;
case SUBMENU_HORARIOS: if (opcionSubmenu >= 22) { EEPROM.put(0, config); estadoActual = MENU_PRINCIPAL; } else modoEdicion = !modoEdicion; break;
case SUBMENU_HORAS_RIEGO: if (opcionSubmenu == 3) { EEPROM.put(0, config); estadoActual = MENU_PRINCIPAL; } else { modoEdicion = true; pasoEdicion++; if(pasoEdicion > 2) { modoEdicion = false; pasoEdicion = 0; EEPROM.put(0, config); } } break;
case SUBMENU_MINUTOS_RIEGO: if (opcionSubmenu == 3) { config.tiempoAM=duracionRiegoAM; config.tiempoMD=duracionRiegoMD; config.tiempoPM=duracionRiegoPM; EEPROM.put(0, config); estadoActual = MENU_PRINCIPAL; } else modoEdicion = !modoEdicion; break;
case SUBMENU_TESTEO: if (opcionSubmenu == 1) { EEPROM.put(0, config); estadoActual = MENU_PRINCIPAL; } else modoEdicion = !modoEdicion; break;
case SUBMENU_INVIERNO: if (opcionSubmenu == 1) { EEPROM.put(0, config); estadoActual = MENU_PRINCIPAL; } else modoEdicion = !modoEdicion; break;
case SUBMENU_BLOQUEO: if (opcionSubmenu == 1) { EEPROM.put(0, config); estadoActual = MENU_PRINCIPAL; } else modoEdicion = !modoEdicion; break;
case SUBMENU_TEMP: if (opcionSubmenu == 1) { EEPROM.put(0, config); estadoActual = MENU_PRINCIPAL; } else modoEdicion = !modoEdicion; break;
}
}
void controlarRiegoManual() {
if (estadoRiego == MANUAL_V23_ON) {
if (millis() - cronometroSecuencia >= 2000) { // 2 segundos unificados estrictos
digitalWrite(pinV24, LOW); estadoRiego = MANUAL_V23_RUN; cronometroSecuencia = millis();
}
}
else if (estadoRiego == MANUAL_V23_RUN && !enPausa) {
if (millis() - cronometroSecuencia >= duracionRiegoZonaMs) ejecutarStop();
}
}
void controlarCicloRiego() {
unsigned long tiempoActual = millis();
if (!enPausa && secuenciaActiva) {
switch (estadoRiego) {
case ENCENDIDO_SEGURO_ON:
if (tiempoActual - cronometroSecuencia >= 2000) { digitalWrite(pinV24, LOW); estadoRiego = SOLAPADO; cronometroSecuencia = millis(); }
break;
case SOLAPADO:
if (!enSolapamiento) {
if (tiempoActual - cronometroSecuencia >= duracionRiegoZonaMs) {
int proxima = buscarSiguienteValvula(valvulaActualIndice + 1, tipoRiegoActivo);
if (proxima != -1) {
digitalWrite(pinInicioValvulas + proxima, LOW);
valvulaAnteriorIndice = valvulaActualIndice; valvulaActualIndice = proxima; valvulaActivaActual = proxima;
enSolapamiento = true; cronometroSolapado = millis(); cronometroPaso = millis();
} else {
digitalWrite(pinV24, HIGH); estadoRiego = APAGADO_SEGURO_OFF; cronometroSecuencia = millis();
}
}
} else {
if (tiempoActual - cronometroSolapado >= 2000) {
if (valvulaAnteriorIndice != -1) digitalWrite(pinInicioValvulas + valvulaAnteriorIndice, HIGH);
enSolapamiento = false; cronometroSecuencia = millis();
}
}
break;
case APAGADO_SEGURO_OFF:
if (tiempoActual - cronometroSecuencia >= 2000) {
if (valvulaActualIndice != -1) digitalWrite(pinInicioValvulas + valvulaActualIndice, HIGH);
apagarTodoFisico(); estadoRiego = APAGADO; secuenciaActiva = false; modoTesteo = false;
}
break;
default: break;
}
}
}
// ============================================================================
// NÚCLEO GRÁFICO ORIGINAL COMPLETO CON LAS 8 OPCIONES SINCRO EN TERRENO
// ============================================================================
void actualizarPantalla() {
if (!pantallaEncendida) return; // Filtro nativo de energía
// ==========================================================================
// ESCUDO DE ESTABILIDAD ASÍNCRONO (Cada 250ms)
// Evita que oled.display() bloquee el procesador al recibir datos de la Wemos
// ==========================================================================
if (millis() - ultimoRefrescoPantalla < 250) return;
ultimoRefrescoPantalla = millis();
oled.clearDisplay();
oled.setTextSize(1);
oled.setTextColor(SSD1306_WHITE);
// ==========================================================================
// 1. MODO_INICIO (Pantalla Base de Descanso de tu Bitácora)
// ==========================================================================
if (estadoActual == MODO_INICIO) {
DateTime ahora = rtc.now();
// Título superior centralizado
oled.setCursor(24, 0);
oled.print(F("Sistema Riego"));
// Línea del Reloj RTC
oled.setCursor(0, 12);
oled.print(F("H:")); // Tu código externo concatena los dígitos del RTC aquí
if(ahora.hour() < 10) oled.print(F("0")); oled.print(ahora.hour()); oled.print(F(":"));
if(ahora.minute() < 10) oled.print(F("0")); oled.print(ahora.minute()); oled.print(F(" F:"));
if(ahora.day() < 10) oled.print(F("0")); oled.print(ahora.day()); oled.print(F("/"));
if(ahora.month() < 10) oled.print(F("0")); oled.print(ahora.month());
// CONDICIÓN A: SISTEMA EN REPOSO (MUESTRA CLIMA Y CONTEO EN VIVO)
if (!secuenciaActiva && !modoManualActivo) {
oled.setCursor(0, 22);
oled.print(F("Temp: ")); oled.print(tempActual, 1); oled.print(F(" C"));
oled.setCursor(0, 32);
oled.print(F("Pres: ")); oled.print(presionActual, 1); oled.print(F(" hPa"));
// Conteo físico de válvulas asignadas por turno
int contR1 = 0; // Turno AM
int contR2 = 0; // Turno MD
int contR3 = 0; // Turno PM
for (int i = 0; i < 22; i++) {
int tipo = config.estadoValvulas[i];
if (tipo == 1) contR1++;
if (tipo == 3) contR3++;
if (tipo == 2) contR2++;
}
// Formato exacto solicitado: "R1: 02 R2: 18 R3: 02"
oled.setCursor(0, 56);
oled.print(F("R1:")); if(contR1 < 10) oled.print(F("0")); oled.print(contR1);
oled.print(F(" R2:")); if(contR2 < 10) oled.print(F("0")); oled.print(contR2);
oled.print(F(" R3:")); if(contR3 < 10) oled.print(F("0")); oled.print(contR3);
}
// CONDICIÓN B: MODO TEST O RIEGO AUTOMÁTICO (AM, MD, PM)
if (secuenciaActiva) {
// Si el riego activo es un test de válvulas
if (modoTesteo) {
oled.setCursor(0, 22);
oled.print(F("Modo Test"));
oled.setCursor(0, 32);
oled.print(F("Bomba: V24")); // Estado de la bomba fija
oled.setCursor(0, 42);
oled.print(F("Valvula Activa:")); oled.print(valvulaActivaActual + 1);
}
// Si es un riego por turnos programados
else {
oled.setCursor(0, 22);
oled.print(F("Riego Normal"));
// Filtros de bloqueo / Saltos hidráulicos activos según faseSecuencia
if (faseSecuencia == 1) {
oled.setCursor(0, 32); oled.print(F("NO X PRESION"));
} else if (faseSecuencia == 2) {
oled.setCursor(0, 32); oled.print(F("NO X TEMP."));
} else if (faseSecuencia == 3) {
oled.setCursor(0, 32); oled.print(F("NO X INVIERNO"));
} else {
oled.setCursor(0, 32); oled.print(F("Bomba: V24"));
oled.setCursor(0, 42); oled.print(F("Valvula Activa:")); oled.print(valvulaActivaActual + 1);
}
}
// Cálculo del tiempo restante en minutos y segundos
unsigned long tiempoTranscurrido = millis() - cronometroPaso;
long tiempoRestanteMs = (duracionPasoActual * 60000UL) - tiempoTranscurrido;
if (tiempoRestanteMs < 0) tiempoRestanteMs = 0;
int minRestantes = tiempoRestanteMs / 60000UL;
int segRestantes = (tiempoRestanteMs % 60000UL) / 1000UL;
// Despliegue del tiempo y control asíncrono de la variable enPausa
if (enPausa) {
if (millis() - ultimoParpadeoPausa >= 500) {
ultimoParpadeoPausa = millis();
mostrarTextoPausa = !mostrarTextoPausa;
}
if (mostrarTextoPausa) {
oled.setCursor(24, 54); oled.print(F("** PAUSA **"));
}
} else {
oled.setCursor(0, 54);
oled.print(F("Quedan: "));
if(minRestantes < 10) oled.print(F("0")); oled.print(minRestantes); oled.print(F(":"));
if(segRestantes < 10) oled.print(F("0")); oled.print(segRestantes);
}
}
// CONDICIÓN C: RIEGO MANUAL (MANGUERA) - LÍMITE MÁXIMO 2 HORAS
if (!secuenciaActiva && modoManualActivo) {
oled.setCursor(0, 22);
oled.print(F("Valvula 23: ON")); // Estado de la manguera manual
// Cuenta regresiva basada en el límite máximo de 120 minutos (2 horas)
unsigned long tiempoManualTranscurrido = millis() - cronometroManual;
long tiempoManualRestanteMs = (120 * 60000UL) - tiempoManualTranscurrido;
if (tiempoManualRestanteMs < 0) tiempoManualRestanteMs = 0;
int minManual = tiempoManualRestanteMs / 60000UL;
int segManual = (tiempoManualRestanteMs % 60000UL) / 1000UL;
oled.setCursor(0, 34);
oled.print(F("T.Max: 02:00:00"));
// Control de pausa manual con congelamiento de pantalla
if (enPausa) {
if (millis() - ultimoParpadeoPausa >= 500) {
ultimoParpadeoPausa = millis();
mostrarTextoPausa = !mostrarTextoPausa;
}
if (mostrarTextoPausa) {
oled.setCursor(24, 52); oled.print(F("** PAUSA **"));
}
} else {
oled.setCursor(0, 52);
oled.print(F("Quedan: "));
if(minManual < 10) oled.print(F("0")); oled.print(minManual); oled.print(F(":"));
if(segManual < 10) oled.print(F("0")); oled.print(segManual);
}
}
}
// ==========================================================================
// 2. MENU_PRINCIPAL (Con Scroll Dinámico y Cursor '>')
// ==========================================================================
if (estadoActual == MENU_PRINCIPAL) {
oled.setCursor(20, 0);
oled.print(F("Menu Principal"));
for (int i = 0; i < 8; i++) {
// Ventana de scroll matemático para que quepan las 8 opciones en 64 píxeles de alto
int filaVisible = i - (opcionPrincipal > 5 ? opcionPrincipal - 5 : 0);
int yScroll = 12 + (filaVisible * 8);
if (yScroll >= 12 && yScroll <= 60) {
if (opcionPrincipal == i) {
oled.setCursor(0, yScroll);
oled.print(F(">"));
}
oled.setCursor(10, yScroll);
oled.print(opcionesMenuPrincipal[i]);
}
}
}
// ==========================================================================
// 3. SUBMENU_HORARIOS (Título " Cant. Riegos" - 22 Válvulas con Scroll y Selección)
// ==========================================================================
if (estadoActual == SUBMENU_HORARIOS) {
oled.setCursor(18, 0);
oled.print(F(" Cant. Riegos"));
// Recorremos 23 opciones totales (0 a 21 = Válvulas, 22 = Opción Atras)
for (int i = 0; i < 23; i++) {
int filaVisible = i - (opcionSubmenu > 5 ? opcionSubmenu - 5 : 0);
int yScroll = 12 + (filaVisible * 8);
if (yScroll >= 12 && yScroll <= 60) {
if (opcionSubmenu == i) {
oled.setCursor(0, yScroll);
oled.print(F(">"));
}
oled.setCursor(10, yScroll);
// Renderiza el renglón de cada una de las 22 válvulas
if (i < 22) {
oled.print(F("VALVULA -")); oled.print(i + 1);
oled.print(F(": ")); oled.print(config.estadoValvulas[i]); // Muestra valores de 0 a 3
if (modoEdicion && opcionSubmenu == i) {
oled.print(F(" *"));
}
}
// Renderiza la última línea de salida
else {
oled.print(F("Atras..."));
}
}
}
}
// ==========================================================================
// 4. SUBMENU_HORAS_RIEGO (Título "Horas de Riego" - Configuración HH:MM)
// ==========================================================================
if (estadoActual == SUBMENU_HORAS_RIEGO) {
// Título superior fijo solicitado
oled.setCursor(16, 0);
oled.print(F("Horas de Riego"));
// Recorremos las 4 opciones fijas del submenú (0=AM, 1=MD, 2=PM, 3=Atras)
for (int i = 0; i < 4; i++) {
int yPos = 16 + (i * 12); // Separación vertical perfecta de 12 píxeles
// Dibuja la flecha '>' guiada por el encoder en modo navegación
if (opcionSubmenu == i) {
oled.setCursor(0, yPos);
oled.print(F(">"));
}
oled.setCursor(10, yPos);
// --- 1. HORA AM ---
if (i == 0) {
oled.print(F("1.- HoraAM "));
// Si estás editando la HORA (pasoEdicion == 1), dibuja el asterisco protector
if (modoEdicion && opcionSubmenu == 0 && pasoEdicion == 1) oled.print(F("*"));
if(config.horaAM < 10) oled.print(F("0")); oled.print(config.horaAM);
oled.print(F(":"));
// Si pasaste a editar los MINUTOS (pasoEdicion == 2), el asterisco se mueve aquí
if (modoEdicion && opcionSubmenu == 0 && pasoEdicion == 2) oled.print(F("*"));
if(config.minutoAM < 10) oled.print(F("0")); oled.print(config.minutoAM);
}
// --- 2. HORA MD ---
if (i == 1) {
oled.print(F("2.- HoraMD "));
if (modoEdicion && opcionSubmenu == 1 && pasoEdicion == 1) oled.print(F("*"));
if(config.horaMD < 10) oled.print(F("0")); oled.print(config.horaMD);
oled.print(F(":"));
if (modoEdicion && opcionSubmenu == 1 && pasoEdicion == 2) oled.print(F("*"));
if(config.minutoMD < 10) oled.print(F("0")); oled.print(config.minutoMD);
}
// --- 3. HORA PM ---
if (i == 2) {
oled.print(F("3.- HoraPM "));
if (modoEdicion && opcionSubmenu == 2 && pasoEdicion == 1) oled.print(F("*"));
if(config.horaPM < 10) oled.print(F("0")); oled.print(config.horaPM);
oled.print(F(":"));
if (modoEdicion && opcionSubmenu == 2 && pasoEdicion == 2) oled.print(F("*"));
if(config.minutoPM < 10) oled.print(F("0")); oled.print(config.minutoPM);
}
// --- 4. OPCIÓN ATRÁS ---
if (i == 3) {
oled.print(F("4.- Atras..."));
}
}
}
// ==========================================================================
// 5. SUBMENU_MINUTOS_RIEGO (Título "Duracion de Riego" - Rango: 0 a 45 min)
// ==========================================================================
if (estadoActual == SUBMENU_MINUTOS_RIEGO) {
oled.setCursor(12, 0);
oled.print(F("Duracion Riego"));
for (int i = 0; i < 4; i++) {
int yPos = 16 + (i * 12);
if (opcionSubmenu == i) { oled.setCursor(0, yPos); oled.print(F(">")); }
oled.setCursor(10, yPos);
if (i == 0) { oled.print(F("Min AM: ")); oled.print(config.tiempoAM); oled.print(F(" min")); }
if (i == 1) { oled.print(F("Min MD: ")); oled.print(config.tiempoMD); oled.print(F(" min")); }
if (i == 2) { oled.print(F("Min PM: ")); oled.print(config.tiempoPM); oled.print(F(" min")); }
if (i == 3) { oled.print(F("Atras...")); }
if (modoEdicion && opcionSubmenu == i && i < 3) oled.print(F(" *"));
}
}
// ==========================================================================
// 6. SUBMENU_TESTEO (Título " Minuto Test" - Rango: 0 a 45 min)
// ==========================================================================
if (estadoActual == SUBMENU_TESTEO) {
oled.setCursor(24, 0);
oled.print(F(" Minuto Test"));
// Opción 1: Ajuste de minutos de prueba
if (opcionSubmenu == 0) { oled.setCursor(0, 24); oled.print(F(">")); }
oled.setCursor(10, 24);
oled.print(F("Tiempo: ")); oled.print(configMinutosTesteo); oled.print(F(" min"));
if (modoEdicion && opcionSubmenu == 0) oled.print(F(" *"));
// Opción 2: Salir
if (opcionSubmenu == 1) { oled.setCursor(0, 48); oled.print(F(">")); }
oled.setCursor(10, 48);
oled.print(F("Atras..."));
}
// ==========================================================================
// 7. SUBMENU_INVIERNO (Título " Modo invierno" - Estado: ON / OFF y Salir)
// ==========================================================================
if (estadoActual == SUBMENU_INVIERNO) {
oled.setCursor(24, 0);
oled.print(F(" Modo invierno"));
// Opción 1: Interruptor ON/OFF
if (opcionSubmenu == 0) { oled.setCursor(0, 24); oled.print(F(">")); }
oled.setCursor(10, 24);
oled.print(F("Estado: "));
if (config.modoInvierno) oled.print(F("ON"));
else oled.print(F("OFF"));
if (modoEdicion && opcionSubmenu == 0) oled.print(F(" *"));
// Opción 2: Salir
if (opcionSubmenu == 1) { oled.setCursor(0, 48); oled.print(F(">")); }
oled.setCursor(10, 48);
oled.print(F("Atras..."));
}
// ==========================================================================
// 8. SUBMENU_BLOQUEO (Título "Presion Bloqueo" - Rango: 800 INACTIVO a 1030 hPa)
// ==========================================================================
if (estadoActual == SUBMENU_BLOQUEO) {
oled.setCursor(18, 0);
oled.print(F("Presion Bloqueo"));
// Opción 1: Ajuste numérico del transductor hPa
if (opcionSubmenu == 0) { oled.setCursor(0, 24); oled.print(F(">")); }
oled.setCursor(10, 24);
oled.print(F("Tope: "));
if (configPresionBloqueo <= 800) oled.print(F("INACTIVO"));
else {
oled.print(configPresionBloqueo);
oled.print(F(" hPa"));
}
if (modoEdicion && opcionSubmenu == 0) oled.print(F(" *"));
// Opción 2: Salir
if (opcionSubmenu == 1) { oled.setCursor(0, 48); oled.print(F(">")); }
oled.setCursor(10, 48);
oled.print(F("Atras..."));
}
// ==========================================================================
// 9. SUBMENU_TEMP (Título "Temp.de Corte" - Rango: 1 a 20 Grados y Salir)
// ==========================================================================
if (estadoActual == SUBMENU_TEMP) {
oled.setCursor(24, 0);
oled.print(F("Temp.de Corte"));
// Opción 1: Ajuste por heladas
if (opcionSubmenu == 0) { oled.setCursor(0, 24); oled.print(F(">")); }
oled.setCursor(10, 24);
oled.print(F("Corte a: ")); oled.print(config.tempCorte); oled.print(F(" C"));
if (modoEdicion && opcionSubmenu == 0) oled.print(F(" *"));
// Opción 2: Salir
if (opcionSubmenu == 1) { oled.setCursor(0, 48); oled.print(F(">")); }
oled.setCursor(10, 48);
oled.print(F("Atras..."));
}
// ==========================================================================
// REFRESCO FÍSICO FINAL OBLIGATORIO
// ==========================================================================
oled.display();
}type or paste code here
El problema es que la wemos no carga pagina , al estar conectada con el sensor bmp280 logre que el sensor funcione pero se traba la wemos , para que funcione separe todas las entradas de vcc y gnd toda en comun, los componentes son arduino mega, pantalla oled 128x64, wemos d1 mini, encoder, rtc, sensor bmp280 color morado, 2 divisores de voltage logico y 4 botones fisicos, 3 modulos rele 8 salidas,, la wemos esta conectada por rx y tx conectados los pines de 3.3v y 5v y gnd, el bmp280 y pantalla por i2c,, al desconectar el bmp funciona la wemos , nose si el problema es de conexion o del programa por la forma , quien me puede ayudar, la wemos esta conectada de esta forma y me funciono la entrada 5v , 3,3 y conectado el micro usb
Can you post an annotated schematic showing exactly how this is wired. be sure to show all power sources, and hardware devices.
Você pode postar um esquema elétrico anotado mostrando exatamente como tudo está conectado? Certifique-se de incluir todas as fontes de alimentação e todos os dispositivos de hardware.
He trasladado su tema de una categoría de idioma inglés del foro a la categoría International > Español @progketrawe.
En adelante por favor usar la categoría apropiada a la lengua en que queráis publicar. Esto es importante para el uso responsable del foro, y esta explicado aquí la guía "How to get the best out of this forum".
Este guía contiene mucha información útil. Por favor leer.
De antemano, muchas gracias por cooperar.
Gilshultz: por que medio es mejor, estoy tratando de armar en fritzing, pero no etan los divisores de voltaje y pantalla oled por i2c,
los reles no estan conectados , para los efectos de espacio y revisar porque la wemos no carga la pagina, esta todo separado y con gnd en comun, el bmp280 esta conectado a 3,3 y gnd para fijar direccion a 077, la wemos esta conectada por micro usb y pin 3,3 y vin para que funcione,
¿Con tres tensiones al mismo tiempo?
Explica por favor.
Tu esquema no sirve de mucho, siempre pedimos que no suban esquemas de Fritzing porque no ayudan. Encima tiene baja resolución y se entiende menos aún.
Es mejor que hagas un esquema a mano, bien detallado con números/funciones de los pines y cada línea de alimentación.
En principio no hay razón para que la D1 no trabaje (salvo problemas de tensión), el BMP no debería interferir en nada.
Adjunta el código de la D1 sino no te podemos ayudar.
// ultimo saneado web super bueno 1.0
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
// ============================================================================
// CONFIGURACIÓN DE RED WIFI E IP ESTÁTICA ORIGINAL
// ============================================================================
const char* ssid = "ingresatured";
const char* password = "password"; // Pon aquí tu contraseña de red
IPAddress local_IP(192, 168, 1, 10);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
ESP8266WebServer server(80);
// ============================================================================
// VARIABLES VOLÁTILES ALIMENTADAS POR LA TELEMETRÍA DEL MEGA
// ============================================================================
String horaFecha = "00:00:00 00/00";
float temperatura = 0.0;
float presion = 0.0;
bool v23Manual = false;
bool v24Principal = false;
int tiempoTestConfig = 0;
String tiempoRestanteWeb = "00:00:00";
bool estadoValvulas[22];
int modoValvulas[22];
int faseSecuenciaWeb = 0;
// Arreglo global en la Wemos para los 3 días x 3 turnos (Inicia con "--" en formato texto)
// h[0]=AM_Hoy, h[1]=MD_Hoy, h[2]=PM_Hoy
// h[3]=AM_Ayer, h[4]=MD_Ayer, h[5]=PM_Ayer
// h[6]=AM_Ante, h[7]=MD_Ante, h[8]=PM_Ante
String h[9] = {"--", "--", "--", "--", "--", "--", "--", "--", "--"};
int hAM = 0; int mAM = 0;
int hMD = 0; int mMD = 0;
int hPM = 0; int mPM = 0;
// Prototipos de Rutinas Web
void handleRoot();
void enviarDatosJson();
void manejarAccionesWeb();
// ============================================================================
// CONFIGURACIÓN BASE INICIAL (SETUP)
// ============================================================================
void setup() {
// Configura el puerto serie nativo de hardware a la velocidad veloz del Mega
Serial.begin(115200);
// Inicialización de matrices en reposo seguro
for(int i=0; i<22; i++) {
estadoValvulas[i] = false;
modoValvulas[i] = 0;
}
// Intenta forzar la configuración de red fija
if (!WiFi.config(local_IP, gateway, subnet)) {
// Si falla la IP fija, continúa en modo DHCP automático
}
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
// 4. VACIADO DE SEGURIDAD EXPLICITO DEL PUERTO SERIE (LIMPIA EL BUFFER)
// Esto descarta la basura acumulada antes de abrir las compuertas de la red
while (Serial.available() > 0) {
Serial.read();
}
// Activa el dominio local mDNS: http://riego-ketrawe.local
if (MDNS.begin("riego-ketrawe")) {
// Servidor mDNS iniciado con éxito
}
// Enrutador del Servidor Web (Enlaces exactos de tu sistema anterior)
server.on("/", handleRoot);
server.on("/status", enviarDatosJson);
server.on("/control", manejarAccionesWeb);
server.begin();
}
String cadenaEntradaWemos = "";
// ============================================================================
// BUCLE DE PROCESAMIENTO PRINCIPAL (LOOP)
// ============================================================================
void loop() {
server.handleClient(); // Mantiene la página web abierta y respondiendo al teléfono
MDNS.update();
// --- RECEPTOR DE TELEMETRÍA ASÍNCRONO ORIGINAL DESDE EL MEGA ---
if (Serial.available() > 0) {
// Lee la línea completa enviada por el Serial3 del Mega de forma ordenada
String linea = Serial.readStringUntil('\n');
linea.trim(); // Limpia espacios o caracteres invisibles de seguridad
if (linea.length() > 0) {
// Procesa el paquete unificado que comienza con tu protocolo "DATA:"
if (linea.startsWith("DATA:")) {
String datos = linea.substring(5);
// Descompone los fragmentos separados por comas de tu sistema
int c1 = datos.indexOf(',');
int c2 = datos.indexOf(',', c1 + 1);
int c3 = datos.indexOf(',', c2 + 1);
int c4 = datos.indexOf(',', c3 + 1);
int c5 = datos.indexOf(',', c4 + 1);
int c6 = datos.indexOf(',', c5 + 1);
int c7 = datos.indexOf(',', c6 + 1);
int c8 = datos.indexOf(',', c7 + 1);
int c9 = datos.indexOf(',', c8 + 1);
// ==========================================================================
// NUEVAS COMAS: Para aislar los 6 valores de horarios que vienen al final
// ==========================================================================
int c10 = datos.indexOf(',', c9 + 1);
int c11 = datos.indexOf(',', c10 + 1);
int c12 = datos.indexOf(',', c11 + 1);
int c13 = datos.indexOf(',', c12 + 1);
int c14 = datos.indexOf(',', c13 + 1);
int c15 = datos.indexOf(',', c14 + 1);
// Sincronización absoluta: Verificamos que todas las comas existan en el paquete
if (c1 != -1 && c2 != -1 && c3 != -1 && c4 != -1 && c5 != -1 && c6 != -1 &&
c7 != -1 && c8 != -1 && c9 != -1 && c10 != -1 && c11 != -1 && c12 != -1 &&
c13 != -1 && c14 != -1 && c15 != -1) {
horaFecha = datos.substring(5, c1);
temperatura = datos.substring(c1 + 1, c2).toFloat();
presion = datos.substring(c2 + 1, c3).toFloat();
v23Manual = (datos.substring(c3 + 1, c4) == "1");
v24Principal = (datos.substring(c4 + 1, c5) == "1");
tiempoTestConfig = datos.substring(c5 + 1, c6).toInt();
tiempoRestanteWeb = datos.substring(c6 + 1, c7);
// Extrae la tira continua de 22 estados de relés activos (unos y ceros)
String tEst = datos.substring(c7 + 1, c8);
for(int i = 0; i < 22 && i < tEst.length(); i++) {
estadoValvulas[i] = (tEst.charAt(i) == '1');
}
// Extrae el valor de fase secuencia desde el Arduino Mega
int faseSecuenciaWeb = datos.substring(c8 + 1, c9).toInt();
// Extrae la tira continua de modos asignados en la EEPROM
String tMod = datos.substring(c9 + 1, c10); // Terminamos en c10 para no pisar las horas
for(int i = 0; i < 22 && i < tMod.length(); i++) {
modoValvulas[i] = String(tMod.charAt(i)).toInt();
}
// ==========================================================================
// NUEVO: EXTRACCIÓN DE LOS 6 PARÁMETROS HORARIOS DESDE EL MEGA
// ==========================================================================
hAM = datos.substring(c10 + 1, c11).toInt();
mAM = datos.substring(c11 + 1, c12).toInt();
hMD = datos.substring(c12 + 1, c13).toInt();
mMD = datos.substring(c13 + 1, c14).toInt();
hPM = datos.substring(c14 + 1, c15).toInt();
mPM = datos.substring(c15 + 1).toInt(); // Lee hasta el salto de línea final
// ==========================================================================
// AUTOMATIZACIÓN DEL BANCO HISTÓRICO EN RAM (9 CELDAS)
// ==========================================================================
if (horaFecha.length() >= 5) {
String horaMinuto = horaFecha.substring(0, 5);
String txtHistorial = "--";
if (faseSecuenciaWeb == 0) txtHistorial = "OK";
else if (faseSecuenciaWeb == 1) txtHistorial = "INV";
else if (faseSecuenciaWeb == 2) txtHistorial = "PRE";
else if (faseSecuenciaWeb == 3) txtHistorial = "TEM";
// ver aqui las veriables nuevas que tenemos-----------------------------
if (horaMinuto == "06:00") h[0] = txtHistorial;
else if (horaMinuto == "14:00") h[1] = txtHistorial;
else if (horaMinuto == "18:00") h[2] = txtHistorial;
static bool yaSaltoMedianoche = false;
if (horaFecha.substring(0, 8) == "00:00:00") {
if (!yaSaltoMedianoche) {
yaSaltoMedianoche = true;
h[6] = h[3]; h[7] = h[4]; h[8] = h[5];
h[3] = h[0]; h[4] = h[1]; h[5] = h[2];
h[0] = "--"; h[1] = "--"; h[2] = "--";
}
} else {
yaSaltoMedianoche = false;
}
}
}
}
}
} // Cierre del receptor de red
}
// ============================================================================
// MAQUETACIÓN VISUAL AZUL Y CALIPSO EN BLOQUES
// ============================================================================
void handleRoot() {
char bufAM[10], bufMD[10], bufPM[10];
sprintf(bufAM, "%02d:%02d", hAM, mAM);
sprintf(bufMD, "%02d:%02d", hMD, mMD);
sprintf(bufPM, "%02d:%02d", hPM, mPM);
String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";
html += "<title> Riego KETRAWE 2.0</title>";
html += "<style>";
html += "body { background-color: #002B49; color: white; font-family: Arial, sans-serif; margin: 15px; padding: 0; }";
html += "hr { border: 1px solid rgba(255,255,255,0.15); margin: 15px 0; }";
html += ".contenedor-sensores { display: flex; gap: 12px; justify-content: space-between; margin-bottom: 15px; }";
html += ".recuadro-calipso { background-color: #00A8CC; color: white; padding: 12px; border-radius: 8px; flex: 1; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }";
html += ".grid-reles { display: grid; grid-template-columns: repeat(8, 1fr); gap: 8px; margin-bottom: 15px; }";
html += ".valvula-card { background: #1C334D; border-radius: 6px; padding: 6px; text-align: center; border: 1px solid rgba(255,255,255,0.1); }";
html += ".circulo-estado { width: 14px; height: 14px; border-radius: 50%; background-color: #7F8C8D; margin: 4px auto; transition: background 0.3s; }";
html += ".tiempo-destacado { grid-column: span 2; background-color: #1A1A1A; border: 2px solid #FF3838; border-radius: 6px; padding: 4px; text-align: center; display: flex; flex-direction: column; justify-content: center; }";
html += ".bomba-panel { background-color: #FF3838; color: white; text-align: center; padding: 12px; font-size: 18px; font-weight: bold; border-radius: 8px; text-transform: uppercase; margin-bottom: 15px; transition: background 0.3s; }";
html += ".contenedor-botones { display: flex; gap: 12px; margin-bottom: 15px; }";
html += ".btn-control { border: 4px solid transparent; font-size: 15px; font-weight: bold; color: white; padding: 12px; border-radius: 8px; cursor: pointer; flex: 1; text-align: center; transition: all 0.2s; text-transform: uppercase; }";
html += ".borde-activo { border-color: #FFFFFF !important; box-shadow: 0 0 12px #FFFFFF; }";
html += ".cintillo-horarios { background-color: #00A8CC; color: white; padding: 8px; border-radius: 6px; text-align: center; font-family: monospace; font-size: 13px; font-weight: bold; margin-bottom: 12px; }";
html += "table { width: 100%; border-collapse: collapse; text-align: center; font-size: 12px; }";
html += "th, td { padding: 8px; border: 1px solid rgba(255,255,255,0.1); }";
html += "th { background-color: #15283D; }";
html += "td.label-dia { font-weight: bold; background-color: rgba(0,168,204,0.15); }";
html += "</style></head><body>";
html += "<h1>Riego KETRAWE</h1><hr>";
// --- BLOQUE 3 RECUADROS CALIPSOS ---
html += "<div class='contenedor-sensores'>";
html += " <div class='recuadro-calipso'><div id='web-hora' style='font-size:20px; font-weight:bold;'>00:00:00</div></div>";
html += " <div class='recuadro-calipso'><div style='font-size:11px; text-transform:uppercase;'>Temperatura</div><div id='web-temp-act' style='font-size:20px; font-weight:bold;'>--.- °C</div></div>";
html += " <div class='recuadro-calipso'><div style='font-size:11px; text-transform:uppercase;'>Presión</div><div id='web-pres-act' style='font-size:20px; font-weight:bold;'>----.- hPa</div></div>";
html += "</div><hr>";
// --- MATRIZ DE RELES 3x8 ---
html += "<div class='grid-reles'>";
for(int i = 1; i <= 22; i++) {
html += " <div class='valvula-card'><div style='font-size:10px; font-weight:bold;'>v:" + String(i) + "</div><div id='circ-v" + String(i) + "' class='circulo-estado'></div><div id='rieg-v" + String(i) + "' style='font-size:9px; opacity:0.8;'>R:0</div></div>";
}
// RECUADRO COMBINADO DE TIEMPO RESTANTE
html += " <div class='tiempo-destacado'><div style='color:#FF3838; font-size:9px; font-weight:bold; text-transform:uppercase;'>Tiempo Restante</div><div id='web-tiempo-restante' style='color:#FF3838; font-size:15px; font-weight:bold; font-family:monospace; margin-top:2px;'>00:00:00</div></div>";
html += "</div><hr>";
// --- PANEL DE LA BOMBA ---
html += "<div id='bloque-bomba' class='bomba-panel'>Estado Bomba: <span id='txt-bomba'>OFF</span></div><hr>";
// --- CUATRO BOTONES DE CONTROL ---
html += "<div class='contenedor-botones'>";
html += " <div id='btn-manguera' class='btn-control' style='background-color:#00A8CC;' onclick='comandar(\"MANUAL\")'>Manguera</div>";
html += " <div id='btn-test' class='btn-control' style='background-color:#34495E;' onclick='comandar(\"TEST\")'>Test (<span id='web-txt-test'>0</span>m)</div>";
html += " <div id='btn-pausa' class='btn-control' style='background-color:#E67E22;' onclick='comandar(\"PAUSA\")'>Pausa</div>";
html += " <div id='btn-stop' class='btn-control' style='background-color:#FF3838;' onclick='comandar(\"STOP\")'>Stop</div>";
html += "</div><hr>";
// --- HISTORIAL Y HORARIOS ---
html += "<h3>Historial de RIEGOS</h3>";
// REEMPLAZO POR TUS VARIABLES INCORPORADAS: AM: hAM:mAM - MD: hMD:mMD - PM: hPM:mPM
html += "<div class='cintillo-horarios'>AM: " + String(bufAM) + " - MD: " + String(bufMD) + " - PM: " + String(bufPM) + "</div>";
//html += "<div class='cintillo-horarios'>AM: 06:00 - MD: 14:00 - PM: 18:00</div>";
html += "<table>";
html += " <thead><tr><th>Día / Ciclo</th><th>Riego AM</th><th>Riego MD</th><th>Riego PM</th></tr></thead>";
html += " <tbody>";
html += " <tr><td class='label-dia'>Hoy</td><td id='h0'>--</td><td id='h1'>--</td><td id='h2'>--</td></tr>";
html += " <tr><td class='label-dia'>Ayer</td><td id='h3'>--</td><td id='h4'>--</td><td id='h5'>--</td></tr>";
html += " <tr><td class='label-dia'>Anteayer</td><td id='h6'>--</td><td id='h7'>--</td><td id='h8'>--</td></tr>";
html += " </tbody></table>";
// --- SCRIPT AJAX INVISIBLE ABIERTO CORRECTAMENTE ---
html += "<script>";
html += "let ultimoBotonActivoRemoto = null;";
html += "function cargarDatos() {";
html += " fetch('/status').then(res => res.json()).then(data => {";
html += " document.getElementById('web-hora').innerText = data.fecha;";
html += " document.getElementById('web-temp-act').innerText = data.temp.toFixed(1) + ' °C';";
html += " document.getElementById('web-pres-act').innerText = data.pres.toFixed(1) + ' hPa';";
html += " document.getElementById('web-txt-test').innerText = data.ttest;";
html += " document.getElementById('web-tiempo-restante').innerText = data.restante;";
// Condicional de la Bomba
html += " let bPanel = document.getElementById('bloque-bomba');";
html += " let bTxt = document.getElementById('txt-bomba');";
html += " if(data.v24) { bPanel.style.backgroundColor = '#2ECC71'; bTxt.innerText = 'ON'; } else { bPanel.style.backgroundColor = '#FF3838'; bTxt.innerText = 'OFF'; }";
// Condicional de los marcos activos por Manguera Manual
html += " const m = document.getElementById('btn-manguera');";
html += " if(data.v23) { m.classList.add('borde-activo'); ultimoBotonActivoRemoto='MANUAL'; } else { m.classList.remove('borde-activo'); }";
// Actualiza los estados de los 22 círculos
html += " for(let i=0; i<22; i++) {";
html += " let circ = document.getElementById('circ-v' + (i+1));";
html += " let txtR = document.getElementById('rieg-v' + (i+1));";
html += " if(data.estados.charAt(i) === '1') circ.style.backgroundColor = '#2ECC71'; else circ.style.backgroundColor = '#7F8C8D';";
html += " txtR.innerText = 'R:' + data.modos.charAt(i);";
html += " }";
html += " }).catch(err => console.log('Sincronizando...'));";
html += "}";
html += "function comandar(cmd) {";
html += " const m = document.getElementById('btn-manguera'); const t = document.getElementById('btn-test'); const p = document.getElementById('btn-pausa');";
html += " if(cmd==='STOP') { m.classList.remove('borde-activo'); t.classList.remove('borde-activo'); p.classList.remove('borde-activo'); ultimoBotonActivoRemoto=null; }";
html += " else if(cmd==='PAUSA') { p.classList.toggle('borde-activo'); if(p.classList.contains('borde-activo')) { m.classList.remove('borde-activo'); t.classList.remove('borde-activo'); } else { if(ultimoBotonActivoRemoto==='MANUAL') m.classList.add('borde-activo'); else if(ultimoBotonActivoRemoto==='TEST') t.classList.add('borde-activo'); } }";
html += " else if(cmd==='MANUAL') { m.classList.add('borde-activo'); t.classList.remove('borde-activo'); p.classList.remove('borde-activo'); ultimoBotonActivoRemoto='MANUAL'; }";
html += " else if(cmd==='TEST') { t.classList.add('borde-activo'); m.classList.remove('borde-activo'); p.classList.remove('borde-activo'); ultimoBotonActivoRemoto='TEST'; }";
html += " fetch('/control?cmd=' + cmd);";
html += "}";
html += "cargarDatos(); setInterval(cargarDatos, 2000);";
html += "</script></body></html>";
server.send(200, "text/html", html);
}
// ============================================================================
// TRANSMISIÓN FRACCIONADA DE ALTA COMPATIBILIDAD ORIGINAL (sendContent)
// ============================================================================
void enviarDatosJson() {
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
// Cabecera obligatoria de permisos universales contra bloqueos de celulares
server.sendHeader("Access-Control-Allow-Origin", "*");
server.send(200, "application/json", "{");
// Se usan barras invertidas (\") para que el compilador entienda las comillas internas
server.sendContent("\"fecha\":\"" + horaFecha + "\",");
server.sendContent("\"temp\":" + String(temperatura, 1) + ",");
server.sendContent("\"pres\":" + String(presion, 1) + ",");
server.sendContent("\"v23\":" + String(v23Manual ? "true" : "false") + ",");
server.sendContent("\"v24\":" + String(v24Principal ? "true" : "false") + ",");
server.sendContent("\"ttest\":" + String(tiempoTestConfig) + ",");
server.sendContent("\"restante\":\"" + tiempoRestanteWeb + "\",");
String est = "";
for (int i = 0; i < 22; i++) {
est += estadoValvulas[i] ? "1" : "0";
}
server.sendContent("\"estados\":\"" + est + "\",");
String mod = "";
for (int i = 0; i < 22; i++) {
mod += String(modoValvulas[i]);
}
server.sendContent("\"modos\":\"" + mod + "\","); // <-- AGREGAMOS COMA: Para continuar agregando llaves seguras al JSON
// ==========================================================================
// INYECCIÓN AUTOMÁTICA DEL BANCO DE HISTÓRICOS (H0 A H8)
// Formatea en vivo el JSON para que el teléfono llene las 9 celdas de la tabla
// ==========================================================================
server.sendContent("\"h0\":\"" + h[0] + "\",");
server.sendContent("\"h1\":\"" + h[1] + "\",");
server.sendContent("\"h2\":\"" + h[2] + "\",");
server.sendContent("\"h3\":\"" + h[3] + "\",");
server.sendContent("\"h4\":\"" + h[4] + "\",");
server.sendContent("\"h5\":\"" + h[5] + "\",");
server.sendContent("\"h6\":\"" + h[6] + "\",");
server.sendContent("\"h7\":\"" + h[7] + "\",");
server.sendContent("\"h8\":\"" + h[8] + "\""); // La última llave va sin coma final antes del corchete
// ==========================================================================
// NUEVO: TRANSMISIÓN DE LOS 6 VALORES HORARIOS DINÁMICOS AL JSON /STATUS
// Envía las horas y minutos que el Mega rescató de la EEPROM
// ==========================================================================
server.sendContent("\"ham\":" + String(hAM) + ",");
server.sendContent("\"mam\":" + String(mAM) + ",");
server.sendContent("\"hmd\":" + String(hMD) + ",");
server.sendContent("\"mmd\":" + String(mMD) + ",");
server.sendContent("\"hpm\":" + String(hPM) + ",");
server.sendContent("\"mpm\":" + String(mPM)); // El último valor va sin coma antes del cierre
server.sendContent("}");
}
// ============================================================================
// RETORNO DE COMANDOS DIGITALES (WEMOS -> MEGA)
// ============================================================================
void manejarAccionesWeb() {
if (server.hasArg("cmd")) {
String orden = server.arg("cmd");
orden.trim(); // Limpia espacios por seguridad mecánica
// Escupe la orden rápida por el puerto serial de hardware hacia el Mega
Serial.println("CMD:" + orden);
// FUERZA EL CAMBIO EN EL SERVIDOR: Enciende las luces de inmediato en el celular
if (orden == "MANUAL") {
v23Manual = true; // Activa el recuadro de la manguera manual en tu celular
v24Principal = true; // Pinta la bomba principal en color verde (ON)
tiempoRestanteWeb = "02:00:00"; // Clava el segundero regresivo en su tope de resguardo
}
else if (orden == "STOP") {
v23Manual = false; // Pinta la manguera manual en color gris (OFF)
v24Principal = false; // Pinta la bomba principal en color rojo (OFF)
tiempoRestanteWeb = "00:00:00"; // Limpia el segundero regresivo a ceros
// Nota: El Mega se encargará de resetear faseSecuencia a 0 al recibir el STOP
}
// ==========================================================================
// ADICIÓN DE VELOCIDAD PARA TEST Y PAUSA (OPCIONAL PARA ETAPA DE TABLERO)
// ==========================================================================
else if (orden == "TEST") {
v24Principal = true; // El modo test enciende la bomba V24 de inmediato
v23Manual = false;
// Convierte el valor numérico actual a texto para el segundero web instantáneo
char bufTest[12];
sprintf(bufTest, "%02d:00:00", tiempoTestConfig);
tiempoRestanteWeb = String(bufTest);
}
}
server.send(200, "text/plain", "OK"); // Devuelve el código de éxito de red al celular
}
este es el programa de la wemos d1 mini
- Alimentación Principal
- Arduino Mega: Se alimenta por el Jack DC (7-12V) o por USB.
- Línea de 5V: Conectar el pin 5V del Arduino Mega al riel positivo de la protoboard (alimenta el RTC, el Encoder, los 4 Botones y el lado de alta tensión HV de ambos LLC).
GND: Unificar todas las tierras (GND) de todos los dispositivos en un punto común.
- Bus I2C de 5V (Pantalla OLED y Reloj RTC DS3231)
-
La pantalla OLED (si es compatible con 5V) y el módulo RTC se conectan directamente a los pines I2C nativos del Arduino Mega: SDA pin 20 TX, SCL pin 21 RX
-
Pin 20 (SDA) del Mega → SDA de la Pantalla OLED y SDA del RTC.
-
Pin 21 (SCL) del Mega → SCL de la Pantalla OLED y SCL del RTC.
-
VCC de ambos → Línea de 5V.
-
GND de ambos → Línea GND.
- Bus I2C de 3.3V a través del LLC nº 1 (Sensor BMP280)
- Alimentación del LLC nº 1: Pin HV a 5V, pin LV a 3.3V, pines GND (ambos lados) a GND.
- Lado de Alta Tensión (5V):
- Pin 20 (SDA) del Mega → Entrada HV1 del LLC nº 1.
-Pin 21 (SCL) del Mega → Entrada HV2 del LLC nº 1.
- Pin 20 (SDA) del Mega → Entrada HV1 del LLC nº 1.
- Lado de Baja Tensión (3.3V):
- Salida LV1 del LLC nº 1 → SDA del BMP280.
- Salida LV2 del LLC nº 1 → SCL del BMP280.
- VCC del BMP280 → Línea de 3.3V.
- GND comun
- salida del bmp280 CSB aGnd
- salida bmp280 SDO a 3,3 v
- Bus UART (Serial) de 3.3V a través del LLC nº 2 (Wemos D1 Mini)
- Usaremos el puerto Serial 1 del Arduino Mega para no interferir con la programación por USB. El RX/TX del Wemos opera estrictamente a 3.3V.
- Alimentación del LLC nº 2: Pin HV a 5V, pin LV a 3.3V, pines GND a GND.
- Conexiones de Transmisión (TX del Mega a RX del Wemos):Pin 18 (TX1) del Mega → Entrada HV1 del LLC nº 2.Salida LV1 del LLC nº 2 → Pin RX del Wemos D1 Mini.
- Conexiones de Recepción (TX del Wemos a RX del Mega):Pin TX del Wemos D1 Mini → Entrada LV2 del LLC nº 2.Salida HV2 del LLC nº 2 → Pin 19 (RX1) del Mega.
- Alimentación del Wemos: Pin 5V (o V-In) a la línea de 5V (el Wemos regulará internamente a 3.3V) o pin 3.3V directo a la línea de 3.3V
- Encoder Rotativo (Inicia en Pin 2)
El Arduino Mega maneja interrupciones de hardware usamos el 2 , 3 y 4 para las señales del encoder:
.6. Bloque de 4 Botones Físicos (Pines Digitales)
-
Para ahorrar componentes y simplificar el cableado, se recomienda usar la configuración INPUT_PULLUP en el código de Arduino (utiliza las resistencias internas de chip).
-
Botón 1 → Pin 5 del Mega.
-
Botón 2 → Pin 6 del Mega.
-
Botón 3 → Pin 7 del Mega.
-
Botón 4 → Pin 8 del Mega.
-
Conexión: Un extremo de cada botón va directo a un pin del Mega y el otro extremo va directamente a GND:
estebes el esquema de coneccion
No, eso es una descripción, un esquema electrónico es un dibujo.
Dibuja tu esquema, toma una foto y súbela.
Dejen de usar la IA si no entienden que hay que hacer.
Primero entender las cosas y luego sepan que preguntarle a la IA.
