#include <LiquidCrystal_I2C.h>
#include <Preferences.h>
#include <WiFi.h>
#include <WebServer.h>
#include <time.h>
// ---------- Configuración de pines ----------
// Relés (motores)
const int RELAY_MOTOR1 = 19; // Motor 1
const int RELAY_MOTOR2 = 18; // Motor 2
// Botones (todos con pull-up interno)
const int BTN_DOSIS_INC = 5; // Aumenta dosis (motor1)
const int BTN_DOSIS_DEC = 17; // Disminuye dosis
const int BTN_VECES_INC = 4; // Aumenta veces (motor2)
const int BTN_VECES_DEC = 2; // Disminuye veces
const int BTN_SAVE = 16; // Guarda configuración en ROM
// Finales de carrera (limit switches, activan a LOW)
const int LIMIT_MOTOR1 = 13; // Cuenta vueltas motor1
const int LIMIT_MOTOR2 = 12; // Cuenta vueltas motor2
// ---------- Configuración horaria ----------
const int HORA_ALIMENTACION = 7; // 7:00 AM
const int MINUTO_ALIMENTACION = 0;
// ---------- Objetos globales ----------
LiquidCrystal_I2C lcd(0x27, 16, 2);
Preferences prefs;
WebServer server(80);
// ---------- Variables del sistema ----------
int dosisObjetivo = 1; // Cantidad de alimento (motor1)
int vecesObjetivo = 1; // Número de veces (motor2)
int dosisRestante = 0; // Lo que falta por dispensar en motor1
int vecesRestante = 0; // Lo que falta por motor2
bool alimentando = false; // ¿Se está ejecutando la rutina de comida?
int motorActivo = 1; // 1 = motor1, 2 = motor2
unsigned long ultimoPulsoMotor1 = 0;
unsigned long ultimoPulsoMotor2 = 0;
const unsigned long DEBOUNCE_LIMIT = 50; // ms para antirrebote
// Control de alimentación diaria
bool yaAlimentoHoy = false;
unsigned long ultimaFechaAlimentacion = 0; // timestamp (segundos desde epoch)
// Tiempo RTC interno (se sincroniza por NTP si hay WiFi)
unsigned long tiempoInicio = 0;
bool tiempoSincronizado = false;
int offsetSegundos = 0; // diferencia entre millis() y tiempo real
// WiFi
bool wifiConectado = false;
String ssid = "ESP32_Feeder"; // AP por defecto
String password = "12345678";
bool modoAP = true;
// ---------- Funciones de tiempo ----------
void iniciarRTC() {
// Configurar zona horaria (GMT-3 Argentina, ajustar según necesidad)
configTime(-3 * 3600, 0, "pool.ntp.org", "time.google.com");
Serial.print("Esperando sincronización horaria");
int intentos = 0;
struct tm timeinfo;
while (!getLocalTime(&timeinfo) && intentos < 20) {
Serial.print(".");
delay(500);
intentos++;
}
if (intentos < 20) {
Serial.println("\n✅ Hora sincronizada vía NTP");
tiempoSincronizado = true;
// Guardar el timestamp actual como base para millis()
time_t now = time(nullptr);
tiempoInicio = millis();
offsetSegundos = now - (tiempoInicio / 1000);
} else {
Serial.println("\n⚠️ No se pudo obtener hora por NTP. Se usará RTC interno sin sincronía.");
// Intentar cargar última hora conocida desde Preferences
prefs.begin("feeder", false);
time_t ultimaHoraConocida = prefs.getLong("lastTime", 0);
prefs.end();
if (ultimaHoraConocida > 0) {
offsetSegundos = ultimaHoraConocida - (millis() / 1000);
tiempoSincronizado = true;
Serial.println("Hora restaurada desde memoria.");
} else {
// Fecha por defecto: 1 de enero 2025, 00:00:00
struct tm tm_default = {0};
tm_default.tm_year = 2025 - 1900;
tm_default.tm_mon = 0;
tm_default.tm_mday = 1;
tm_default.tm_hour = 0;
tm_default.tm_min = 0;
tm_default.tm_sec = 0;
time_t defaultTime = mktime(&tm_default);
offsetSegundos = defaultTime - (millis() / 1000);
}
}
}
time_t obtenerTiempoActual() {
if (tiempoSincronizado) {
return (millis() / 1000) + offsetSegundos;
} else {
return 0; // Sin tiempo confiable
}
}
void obtenerHoraLocal(struct tm &timeinfo) {
time_t now = obtenerTiempoActual();
if (now == 0) {
// Si no hay tiempo, usar estructura vacía
memset(&timeinfo, 0, sizeof(timeinfo));
timeinfo.tm_hour = 0;
timeinfo.tm_min = 0;
return;
}
localtime_r(&now, &timeinfo);
}
// ---------- Guardado en memoria ROM ----------
void guardarConfiguracion() {
prefs.begin("feeder", false);
prefs.putInt("dosisObj", dosisObjetivo);
prefs.putInt("vecesObj", vecesObjetivo);
prefs.putInt("dosisRest", dosisRestante);
prefs.putInt("vecesRest", vecesRestante);
prefs.putBool("alimentando", alimentando);
prefs.putInt("motorActivo", motorActivo);
prefs.putBool("yaAlimento", yaAlimentoHoy);
prefs.putLong("ultimaFecha", ultimaFechaAlimentacion);
prefs.putLong("offsetTime", offsetSegundos);
prefs.putLong("millisBase", tiempoInicio);
prefs.end();
Serial.println("Configuración guardada en ROM");
// Mostrar mensaje en LCD
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Guardado en ROM");
delay(1000);
actualizarLCD();
}
void cargarConfiguracion() {
prefs.begin("feeder", true);
dosisObjetivo = prefs.getInt("dosisObj", 1);
vecesObjetivo = prefs.getInt("vecesObj", 1);
dosisRestante = prefs.getInt("dosisRest", dosisObjetivo);
vecesRestante = prefs.getInt("vecesRest", vecesObjetivo);
alimentando = prefs.getBool("alimentando", false);
motorActivo = prefs.getInt("motorActivo", 1);
yaAlimentoHoy = prefs.getBool("yaAlimento", false);
ultimaFechaAlimentacion = prefs.getLong("ultimaFecha", 0);
offsetSegundos = prefs.getLong("offsetTime", 0);
tiempoInicio = prefs.getLong("millisBase", 0);
prefs.end();
// Ajustar límites
if (dosisObjetivo < 1) dosisObjetivo = 1;
if (dosisObjetivo > 9) dosisObjetivo = 9;
if (vecesObjetivo < 1) vecesObjetivo = 1;
if (vecesObjetivo > 9) vecesObjetivo = 9;
if (dosisRestante < 0) dosisRestante = 0;
if (vecesRestante < 0) vecesRestante = 0;
Serial.printf("Cargado: dosis=%d, veces=%d, restante d=%d, v=%d, alimentando=%d\n",
dosisObjetivo, vecesObjetivo, dosisRestante, vecesRestante, alimentando);
}
// ---------- Control de motores y finales de carrera ----------
void iniciarAlimentacion() {
if (alimentando) return;
alimentando = true;
// Reiniciar restantes según los objetivos (si no había proceso interrumpido)
if (dosisRestante == 0 && vecesRestante == 0) {
dosisRestante = dosisObjetivo;
vecesRestante = vecesObjetivo;
}
motorActivo = 1; // Comenzar con motor1
digitalWrite(RELAY_MOTOR1, HIGH);
digitalWrite(RELAY_MOTOR2, LOW);
Serial.println("Iniciando alimentación - Motor1 ON");
guardarConfiguracion();
actualizarLCD();
}
void detenerAlimentacion() {
digitalWrite(RELAY_MOTOR1, LOW);
digitalWrite(RELAY_MOTOR2, LOW);
alimentando = false;
yaAlimentoHoy = true;
ultimaFechaAlimentacion = obtenerTiempoActual();
Serial.println("Alimentación finalizada");
guardarConfiguracion();
actualizarLCD();
}
void procesarPulsoMotor1() {
if (!alimentando || motorActivo != 1) return;
if (dosisRestante > 0) {
dosisRestante--;
Serial.printf("Pulso motor1, dosis restante: %d\n", dosisRestante);
guardarConfiguracion();
actualizarLCD();
if (dosisRestante == 0) {
// Cambiar al motor2
digitalWrite(RELAY_MOTOR1, LOW);
motorActivo = 2;
if (vecesRestante > 0) {
digitalWrite(RELAY_MOTOR2, HIGH);
Serial.println("Motor1 completado, iniciando Motor2");
} else {
// Si vecesRestante ya es 0, terminar
detenerAlimentacion();
}
guardarConfiguracion();
actualizarLCD();
}
}
}
void procesarPulsoMotor2() {
if (!alimentando || motorActivo != 2) return;
if (vecesRestante > 0) {
vecesRestante--;
Serial.printf("Pulso motor2, veces restante: %d\n", vecesRestante);
guardarConfiguracion();
actualizarLCD();
if (vecesRestante == 0) {
detenerAlimentacion();
}
}
}
// ---------- Lectura de botones con antirrebote ----------
unsigned long lastBtnTime[7] = {0};
void leerBotones() {
unsigned long now = millis();
// Botón dosis +
if (digitalRead(BTN_DOSIS_INC) == LOW && (now - lastBtnTime[0]) > 200) {
lastBtnTime[0] = now;
if (!alimentando && dosisObjetivo < 9) {
dosisObjetivo++;
dosisRestante = dosisObjetivo; // Opcional: actualizar restante si no está alimentando
guardarConfiguracion();
actualizarLCD();
}
}
// Botón dosis -
if (digitalRead(BTN_DOSIS_DEC) == LOW && (now - lastBtnTime[1]) > 200) {
lastBtnTime[1] = now;
if (!alimentando && dosisObjetivo > 1) {
dosisObjetivo--;
dosisRestante = dosisObjetivo;
guardarConfiguracion();
actualizarLCD();
}
}
// Botón veces +
if (digitalRead(BTN_VECES_INC) == LOW && (now - lastBtnTime[2]) > 200) {
lastBtnTime[2] = now;
if (!alimentando && vecesObjetivo < 9) {
vecesObjetivo++;
vecesRestante = vecesObjetivo;
guardarConfiguracion();
actualizarLCD();
}
}
// Botón veces -
if (digitalRead(BTN_VECES_DEC) == LOW && (now - lastBtnTime[3]) > 200) {
lastBtnTime[3] = now;
if (!alimentando && vecesObjetivo > 1) {
vecesObjetivo--;
vecesRestante = vecesObjetivo;
guardarConfiguracion();
actualizarLCD();
}
}
// Botón guardar
if (digitalRead(BTN_SAVE) == LOW && (now - lastBtnTime[4]) > 200) {
lastBtnTime[4] = now;
guardarConfiguracion();
}
// Finales de carrera (solo cuentan si alimentando)
if (digitalRead(LIMIT_MOTOR1) == LOW && (now - ultimoPulsoMotor1) > DEBOUNCE_LIMIT) {
ultimoPulsoMotor1 = now;
procesarPulsoMotor1();
}
if (digitalRead(LIMIT_MOTOR2) == LOW && (now - ultimoPulsoMotor2) > DEBOUNCE_LIMIT) {
ultimoPulsoMotor2 = now;
procesarPulsoMotor2();
}
}
// ---------- Verificar hora para iniciar alimentación ----------
void verificarHoraAlimentacion() {
struct tm timeinfo;
obtenerHoraLocal(timeinfo);
if (timeinfo.tm_hour == HORA_ALIMENTACION && timeinfo.tm_min == MINUTO_ALIMENTACION) {
if (!yaAlimentoHoy && !alimentando) {
// También verificar que no haya pasado más de 1 minuto desde la hora exacta
iniciarAlimentacion();
}
}
// Reiniciar flag "yaAlimentoHoy" al cambiar de día (medianoche)
static int ultimoDia = -1;
if (timeinfo.tm_mday != ultimoDia && timeinfo.tm_hour == 0 && timeinfo.tm_min == 0) {
yaAlimentoHoy = false;
ultimoDia = timeinfo.tm_mday;
guardarConfiguracion();
}
if (ultimoDia == -1) ultimoDia = timeinfo.tm_mday;
}
// ---------- Pantalla LCD ----------
void actualizarLCD() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("D:");
lcd.print(dosisObjetivo);
lcd.print(" V:");
lcd.print(vecesObjetivo);
struct tm timeinfo;
obtenerHoraLocal(timeinfo);
lcd.setCursor(10, 0);
char buffer[6];
sprintf(buffer, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
lcd.print(buffer);
lcd.setCursor(0, 1);
if (alimentando) {
if (motorActivo == 1)
lcd.print("M1: ");
else
lcd.print("M2: ");
if (motorActivo == 1) {
lcd.print(dosisRestante);
lcd.print(" ");
} else {
lcd.print(vecesRestante);
lcd.print(" ");
}
} else {
lcd.print("Esperando 7:00 ");
if (yaAlimentoHoy) lcd.print("OK");
}
}
// ---------- Servidor web para configuración ----------
void handleRoot() {
String html = "<html><head><meta charset='UTF-8'><title>Comedero ESP32</title>";
html += "<style>body{font-family:Arial;margin:20px;} input{margin:5px;} button{margin-top:10px;}</style></head><body>";
html += "<h1>Configuración del Comedero</h1>";
html += "<form action='/set' method='GET'>";
html += "Dosis (Motor1): <input type='number' name='dosis' min='1' max='9' value='"+String(dosisObjetivo)+"'><br>";
html += "Veces (Motor2): <input type='number' name='veces' min='1' max='9' value='"+String(vecesObjetivo)+"'><br>";
html += "<button type='submit'>Guardar</button>";
html += "</form>";
html += "<br><form action='/reset' method='GET'><button type='submit'>Reiniciar alimentación (forzar nueva rutina)</button></form>";
html += "<br><a href='/status'>Ver estado</a>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleSet() {
if (server.hasArg("dosis")) {
int newDosis = server.arg("dosis").toInt();
if (newDosis >= 1 && newDosis <= 9) {
dosisObjetivo = newDosis;
if (!alimentando) dosisRestante = dosisObjetivo;
}
}
if (server.hasArg("veces")) {
int newVeces = server.arg("veces").toInt();
if (newVeces >= 1 && newVeces <= 9) {
vecesObjetivo = newVeces;
if (!alimentando) vecesRestante = vecesObjetivo;
}
}
guardarConfiguracion();
actualizarLCD();
String msg = "<html><body><h2>Configuración guardada</h2><a href='/'>Volver</a></body></html>";
server.send(200, "text/html", msg);
}
void handleReset() {
// Forzar reinicio del ciclo de alimentación (útil si hubo error)
if (alimentando) detenerAlimentacion();
yaAlimentoHoy = false;
dosisRestante = dosisObjetivo;
vecesRestante = vecesObjetivo;
guardarConfiguracion();
actualizarLCD();
String msg = "<html><body><h2>Ciclo reiniciado. Próxima alimentación a las 7:00.</h2><a href='/'>Volver</a></body></html>";
server.send(200, "text/html", msg);
}
void handleStatus() {
struct tm timeinfo;
obtenerHoraLocal(timeinfo);
char tiempoStr[30];
strftime(tiempoStr, sizeof(tiempoStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
String html = "<html><body><h1>Estado actual</h1>";
html += "Hora: " + String(tiempoStr) + "<br>";
html += "Dosis objetivo: " + String(dosisObjetivo) + "<br>";
html += "Veces objetivo: " + String(vecesObjetivo) + "<br>";
html += "Dosis restante: " + String(dosisRestante) + "<br>";
html += "Veces restante: " + String(vecesRestante) + "<br>";
html += "Alimentando: " + String(alimentando ? "Sí" : "No") + "<br>";
html += "Motor activo: " + String(motorActivo) + "<br>";
html += "Ya alimentó hoy: " + String(yaAlimentoHoy ? "Sí" : "No") + "<br>";
html += "<br><a href='/'>Volver</a></body></html>";
server.send(200, "text/html", html);
}
void iniciarWiFi() {
// Intentar conectar a red guardada
prefs.begin("feeder", true);
String savedSSID = prefs.getString("wifi_ssid", "");
String savedPass = prefs.getString("wifi_pass", "");
prefs.end();
if (savedSSID.length() > 0) {
WiFi.mode(WIFI_STA);
WiFi.begin(savedSSID.c_str(), savedPass.c_str());
Serial.print("Conectando a WiFi");
int intentos = 0;
while (WiFi.status() != WL_CONNECTED && intentos < 20) {
delay(500);
Serial.print(".");
intentos++;
}
if (WiFi.status() == WL_CONNECTED) {
wifiConectado = true;
modoAP = false;
Serial.println("\n✅ Conectado a WiFi");
Serial.println(WiFi.localIP());
return;
}
}
// Si no hay red o falló, crear AP
WiFi.mode(WIFI_AP);
WiFi.softAP(ssid.c_str(), password.c_str());
Serial.println("\n🔁 Modo AP activo");
Serial.print("IP: ");
Serial.println(WiFi.softAPIP());
modoAP = true;
wifiConectado = false;
}
void setupWebServer() {
server.on("/", handleRoot);
server.on("/set", handleSet);
server.on("/reset", handleReset);
server.on("/status", handleStatus);
server.begin();
Serial.println("Servidor web iniciado");
}
// ---------- Setup principal ----------
void setup() {
Serial.begin(115200);
// Configurar pines
pinMode(RELAY_MOTOR1, OUTPUT);
pinMode(RELAY_MOTOR2, OUTPUT);
digitalWrite(RELAY_MOTOR1, LOW);
digitalWrite(RELAY_MOTOR2, LOW);
pinMode(BTN_DOSIS_INC, INPUT_PULLUP);
pinMode(BTN_DOSIS_DEC, INPUT_PULLUP);
pinMode(BTN_VECES_INC, INPUT_PULLUP);
pinMode(BTN_VECES_DEC, INPUT_PULLUP);
pinMode(BTN_SAVE, INPUT_PULLUP);
pinMode(LIMIT_MOTOR1, INPUT_PULLUP);
pinMode(LIMIT_MOTOR2, INPUT_PULLUP);
// Inicializar LCD
lcd.init();
lcd.backlight();
lcd.print("Iniciando...");
delay(1000);
// Cargar configuración desde ROM
cargarConfiguracion();
// Conectar WiFi (no bloqueante para el proceso)
iniciarWiFi();
setupWebServer();
// Inicializar RTC (intenta NTP si hay WiFi)
iniciarRTC();
// Mostrar valores iniciales
actualizarLCD();
Serial.println("Sistema listo");
}
// ---------- Loop principal ----------
void loop() {
static unsigned long lastLCDupdate = 0;
unsigned long now = millis();
// Actualizar LCD cada segundo
if (now - lastLCDupdate > 1000) {
lastLCDupdate = now;
actualizarLCD();
}
// Leer botones y finales de carrera
leerBotones();
// Verificar si hay que iniciar alimentación
if (!alimentando && !yaAlimentoHoy) {
verificarHoraAlimentacion();
}
// Atender cliente web (siempre disponible, incluso en AP)
server.handleClient();
// Pequeña pausa para evitar saturación
delay(10);
}