Proyecto de dispensador de alimento para mascotas
Se muestra el desarrollo de un dispensador de alimentos programable, con un menú en la LCD para programar una o varias horas de alimentación al día. Usa un relevador para accionar el motor del dispensador, y (opcional) un RTC DS3231 para que la hora sea estable incluso sin Wi‑Fi.
Materiales
- Microcontrolador: ESP32 (DevKit v1).
- Pantalla: LCD 16×2 con adaptador I2C (PCF8574).
- Entradas: 3 botones (Arriba, Abajo, OK).
- Salida: Módulo de relevador de 5V/3.3V compatible con señal de 3.3V.
- Alimentación: Fuente 5V (USB) para ESP32 y módulo de relevador según especificación.
- Opcional para hora estable: Módulo RTC DS3231 (altamente preciso y con alarmas) 1.
- Cableado: Jumpers y protoboard.
La LCD I2C simplifica el cableado a solo SDA/SCL, y su dirección I2C se puede identificar y configurar; guías prácticas explican conexión y uso con ESP32 2 3.
Esquema de conexión
- LCD I2C:
- Botones:
- 3 botones a GPIO 32 (Arriba), GPIO 33 (Abajo), GPIO 25 (OK).
- Conectar el otro extremo del botón a GND, y activar pull‑up interno en el código.
- Relevador (motor del dispensador o actuador):
- IN: GPIO 26 del ESP32.
- VCC/GND: según el módulo (típicamente 5V y GND comunes).
- Contactos del relevador al motor/driver del dispensador según su tensión y corriente.
- RTC DS3231 (opcional, recomendado sin Wi‑Fi):
- VCC: 3.3V o 5V (según módulo).
- GND: GND
- SDA: GPIO 21
- SCL: GPIO 22
El DS3231 aporta hora precisa y alarmas; hay tutoriales específicos de integración con ESP32 1.
Si prefieres obtener la hora por NTP, puedes prescindir del RTC, pero perderás exactitud si el ESP32 no tiene conexión. El uso de DS3231 evita esa dependencia y es común en proyectos de temporizado con relevadores 4 1 5.
Lógica del sistema
- Menú en LCD:
- Items: Programar Hora 1, Programar Hora 2, Duración de activación (segundos), Activar/Desactivar horario, Guardar/Salir.
- Navegación con Arriba/Abajo y OK.
- Programación:
- Ajuste de HH:MM para 1 o 2 horarios diarios (puedes ampliar a más).
- Duración: tiempo que el relevador permanece activado para dispensar.
- Tiempo:
- Opción A (recomendada): RTC DS3231 para leer hora precisa todo el tiempo 1.
- Opción B: NTP al inicio y mantener tiempo con RTC interno (menos estable).
- Accionamiento:
- Cuando hora actual == hora programada y el horario está activo, enciende relevador por “Duración”.
- Evita doble disparo dentro del mismo minuto (flag de ejecución diaria).
Librerías necesarias
Código completo de ejemplo (Arduino IDE)
Este ejemplo usa DS3231 si está presente; si no, funciona con un “reloj básico” del ESP32. Menú simple de dos horarios, duración y activación. Ajusta pines si los cambias.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// Opcional RTC
// Instala "RTClib" de Adafruit desde Library Manager
#include <RTClib.h>
LiquidCrystal_I2C lcd(0x27, 16, 2); // Cambia 0x27 si tu LCD usa otra dirección
RTC_DS3231 rtc;
// Pines
const int BTN_UP = 32;
const int BTN_DOWN = 33;
const int BTN_OK = 25;
const int RELAY_PIN = 26;
// Estructura de horario
struct FeedTime {
int hour;
int minute;
bool enabled;
bool ranToday;
};
FeedTime feed1 = {8, 0, true, false};
FeedTime feed2 = {18, 0, true, false};
int dispenseSeconds = 5;
// Menú
enum MenuItem {
MENU_FEED1,
MENU_FEED2,
MENU_DURATION,
MENU_ENABLE_DISABLE,
MENU_SAVE_EXIT
};
MenuItem currentItem = MENU_FEED1;
bool inEdit = false;
int editField = 0; // 0=hour, 1=minute
// Tiempo base si no hay RTC
unsigned long lastSecondTick = 0;
int sysHour = 0, sysMinute = 0, sysSecond = 0;
// Debounce
unsigned long lastKeyTime = 0;
const unsigned long debounceMs = 150;
bool btnPressed(int pin) {
if (digitalRead(pin) == LOW) {
if (millis() - lastKeyTime > debounceMs) {
lastKeyTime = millis();
return true;
}
}
return false;
}
void setupPins() {
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(BTN_OK, INPUT_PULLUP);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW);
}
void setup() {
Wire.begin();
lcd.init();
lcd.backlight();
setupPins();
// RTC init
if (!rtc.begin()) {
// Sin RTC: continuar con reloj básico
lcd.clear();
lcd.setCursor(0,0); lcd.print("Sin RTC, modo");
lcd.setCursor(0,1); lcd.print("tiempo basico");
delay(1200);
} else {
if (rtc.lostPower()) {
// Ajustar hora inicial (ejemplo: 12:00) o pedir al usuario
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
}
lcd.clear();
}
DateTime nowRTC() {
return rtc.now();
}
// Obtener hora actual
void getCurrentTime(int &h, int &m, int &s) {
if (rtc.begin()) {
DateTime n = rtc.now();
h = n.hour();
m = n.minute();
s = n.second();
} else {
// Reloj básico por millis()
unsigned long ms = millis();
if (ms - lastSecondTick >= 1000) {
lastSecondTick += 1000;
sysSecond++;
if (sysSecond >= 60) { sysSecond = 0; sysMinute++; }
if (sysMinute >= 60) { sysMinute = 0; sysHour++; }
if (sysHour >= 24) { sysHour = 0; }
}
h = sysHour; m = sysMinute; s = sysSecond;
}
}
void drawMenu() {
lcd.clear();
switch (currentItem) {
case MENU_FEED1:
lcd.setCursor(0,0); lcd.print("Hora 1 ");
lcd.print(feed1.hour < 10 ? "0" : ""); lcd.print(feed1.hour); lcd.print(":");
lcd.print(feed1.minute < 10 ? "0" : ""); lcd.print(feed1.minute);
lcd.setCursor(0,1); lcd.print(feed1.enabled ? "Activa " : "Inactiva ");
break;
case MENU_FEED2:
lcd.setCursor(0,0); lcd.print("Hora 2 ");
lcd.print(feed2.hour < 10 ? "0" : ""); lcd.print(feed2.hour); lcd.print(":");
lcd.print(feed2.minute < 10 ? "0" : ""); lcd.print(feed2.minute);
lcd.setCursor(0,1); lcd.print(feed2.enabled ? "Activa " : "Inactiva ");
break;
case MENU_DURATION:
lcd.setCursor(0,0); lcd.print("Duracion");
lcd.setCursor(0,1); lcd.print(dispenseSeconds); lcd.print(" seg");
break;
case MENU_ENABLE_DISABLE:
lcd.setCursor(0,0); lcd.print("Activar Horario");
lcd.setCursor(0,1); lcd.print("OK: toggle");
break;
case MENU_SAVE_EXIT:
lcd.setCursor(0,0); lcd.print("Guardar/Salir");
lcd.setCursor(0,1); lcd.print("OK para salir");
break;
}
}
void handleMenu() {
if (btnPressed(BTN_UP)) {
if (!inEdit) {
currentItem = (MenuItem)((currentItem == MENU_FEED1) ? MENU_SAVE_EXIT : (currentItem - 1));
} else {
// Editing
switch (currentItem) {
case MENU_FEED1:
if (editField == 0) { feed1.hour = (feed1.hour + 1) % 24; }
else { feed1.minute = (feed1.minute + 1) % 60; }
break;
case MENU_FEED2:
if (editField == 0) { feed2.hour = (feed2.hour + 1) % 24; }
else { feed2.minute = (feed2.minute + 1) % 60; }
break;
case MENU_DURATION:
dispenseSeconds = constrain(dispenseSeconds + 1, 1, 60);
break;
default: break;
}
}
drawMenu();
}
if (btnPressed(BTN_DOWN)) {
if (!inEdit) {
currentItem = (MenuItem)((currentItem == MENU_SAVE_EXIT) ? MENU_FEED1 : (currentItem + 1));
} else {
switch (currentItem) {
case MENU_FEED1:
if (editField == 0) { feed1.hour = (feed1.hour + 23) % 24; }
else { feed1.minute = (feed1.minute + 59) % 60; }
break;
case MENU_FEED2:
if (editField == 0) { feed2.hour = (feed2.hour + 23) % 24; }
else { feed2.minute = (feed2.minute + 59) % 60; }
break;
case MENU_DURATION:
dispenseSeconds = constrain(dispenseSeconds - 1, 1, 60);
break;
default: break;
}
}
drawMenu();
}
if (btnPressed(BTN_OK)) {
switch (currentItem) {
case MENU_FEED1:
case MENU_FEED2:
inEdit = !inEdit;
editField = (editField + 1) % 2; // alterna entre horas/minutos
break;
case MENU_DURATION:
inEdit = !inEdit;
break;
case MENU_ENABLE_DISABLE:
// Toggle ambos horarios
feed1.enabled = !feed1.enabled;
feed2.enabled = !feed2.enabled;
break;
case MENU_SAVE_EXIT:
// En un proyecto real, guardar en EEPROM/NVS
// Aquí solo mostrar confirmación
lcd.clear();
lcd.setCursor(0,0); lcd.print("Guardado");
delay(800);
break;
}
drawMenu();
}
}
void triggerRelay(int seconds) {
digitalWrite(RELAY_PIN, HIGH); // según tu módulo, podría ser LOW
unsigned long start = millis();
while (millis() - start < (unsigned long)seconds * 1000) {
// Mantener LCD viva si quieres animar
delay(10);
}
digitalWrite(RELAY_PIN, LOW);
}
bool isTimeMatch(int h, int m, const FeedTime &f) {
return f.enabled && h == f.hour && m == f.minute;
}
void dailyResetFlags(int h, int m) {
// Reset a medianoche
if (h == 0 && m == 0) {
feed1.ranToday = false;
feed2.ranToday = false;
}
}
void checkFeeding(int h, int m) {
dailyResetFlags(h, m);
if (isTimeMatch(h, m, feed1) && !feed1.ranToday) {
lcd.clear(); lcd.setCursor(0,0); lcd.print("Dispensando 1");
triggerRelay(dispenseSeconds);
feed1.ranToday = true;
drawMenu();
}
if (isTimeMatch(h, m, feed2) && !feed2.ranToday) {
lcd.clear(); lcd.setCursor(0,0); lcd.print("Dispensando 2");
triggerRelay(dispenseSeconds);
feed2.ranToday = true;
drawMenu();
}
}
void loop() {
static int lastMinuteShown = -1;
int h, m, s;
getCurrentTime(h, m, s);
// Actualiza cabecera con hora cada minuto
if (m != lastMinuteShown) {
lastMinuteShown = m;
lcd.setCursor(0,0);
lcd.print("Hora ");
lcd.print(h < 10 ? "0" : ""); lcd.print(h);
lcd.print(":");
lcd.print(m < 10 ? "0" : ""); lcd.print(m);
}
handleMenu();
checkFeeding(h, m);
delay(20);
}
Notas prácticas
- Dirección I2C de la LCD: muchos módulos usan 0x27 u 0x3F; puedes escanear el bus I2C para confirmar, como indican las guías de LCD con ESP32 2 3.
- DS3231: ofrece hora muy precisa y alarmas útiles; su integración con ESP32 está bien documentada y es ideal para temporizar relevadores sin depender de Wi‑Fi 1.
- Proyectos similares: controlar relevadores por horario con ESP32 y DS3231 es un patrón probado en la comunidad, ya sea mediante web server o interfaz local; te da ideas de expansión (por ejemplo, interfaz web) 4 5.
Siguientes pasos y personalización
- Guardar configuración en NVS/EEPROM: para mantener horarios tras reinicio.
- Más horarios: amplia el arreglo a 3–4 tiempos diarios.
- Seguridad eléctrica: si usas motores/AC, aísla el relevador y usa fuentes adecuadas.
- Sensor de atascos: añade un sensor de corriente o un fin de carrera para detectar bloqueos.
¿Quieres que lo ajustemos para NTP sin RTC, o prefieres añadir el DS3231? También puedo adaptar el menú a 4 horarios diarios o incluir guardado en NVS.
