// Control de servomotor en "n" posiciones con pantalla LCD I2C 16x2,
// menú controlado por potenciómetro y ejecución con botón.
// Al ejecutar, en cada posición se activa un buzzer activo y un relé durante 2 segundos.
// Al terminar la secuencia el servo vuelve a la posición original (primera posición del array).
// Comentarios en español en cada línea y funciones separadas por procedimiento.
#include <Wire.h> // Biblioteca para I2C
#include <LiquidCrystal_I2C.h> // Biblioteca para LCD I2C 16x2
#include <Servo.h> // Biblioteca para controlar servos
// ---------- Pines según configuración solicitada ----------
const uint8_t PIN_POT = A0; // Pin analógico conectado al potenciómetro
const uint8_t PIN_BOTON = 2; // Pin digital para el botón de selección (INPUT_PULLUP)
const uint8_t PIN_BUZZER = 8; // Pin digital para el buzzer activo (HIGH activa)
const uint8_t PIN_RELAYS = 7; // Pin digital para el relé que controla la válvula
const uint8_t PIN_SERVO = 3; // Pin PWM para el servo (conectado al control del servo)
// ---------- Configuración de LCD ----------
const uint8_t LCD_ADDR = 0x27; // Dirección I2C típica del LCD 16x2
const uint8_t LCD_COLS = 16; // Columnas del LCD
const uint8_t LCD_ROWS = 2; // Filas del LCD
// ---------- Constantes de comportamiento ----------
const unsigned long TIEMPO_DISPENSADO = 2000; // Duración en ms que buzzer y relé están activos por posición
const unsigned long DEBOUNCE_MS = 50; // Tiempo de debounce para el botón en ms
// ---------- Definición de posiciones disponibles (modifica los ángulos aquí) ----------
int posiciones[] = {0, 45, 90, 135, 180}; // Ángulos del servo para cada posición; la cantidad es "n" máxima
const uint8_t MAX_POSICIONES = sizeof(posiciones) / sizeof(posiciones[0]); // Calcula cuántas posiciones hay definidas
// ---------- Variables de estado ----------
uint8_t selecNumeroPosiciones = 1; // Número de posiciones a ejecutar (1..MAX_POSICIONES), seleccionado por el potenciómetro
uint8_t indiceMenu = 0; // Índice visual del menú (muestra la posición actual en la pantalla)
bool estadoBotonAnterior = HIGH; // Estado previo del botón (INPUT_PULLUP => HIGH cuando no presionado)
unsigned long tiempoUltimoCambioBoton = 0; // Tiempo del último cambio detectado (debounce)
// Guardar posición original (inicial) para poder regresar al terminar
int anguloPosicionOriginal = posiciones[0]; // Ángulo original donde debe regresar el servo al finalizar
// ---------- Objetos de hardware ----------
LiquidCrystal_I2C lcd(LCD_ADDR, LCD_COLS, LCD_ROWS); // Objeto LCD I2C
Servo servoDispensador; // Objeto servo
// ---------- Prototipos de funciones (en español) ----------
void inicializarHardware(); // Inicializa pines, LCD y servo
void mostrarMenu(); // Muestra el menú en el LCD con la selección actual
void leerPotenciometroYActualizarSeleccion(); // Lee el potenciómetro y actualiza número de posiciones y menú
bool leerBotonPulsado(); // Lee el botón con debounce, devuelve true si hubo pulsación completa
void ejecutarDispensado(uint8_t cantidad); // Ejecuta el movimiento y activación de actuadores por 'cantidad' posiciones
void moverServoA(uint8_t angulo); // Mueve el servo suavemente al ángulo indicado
void activarBuzzerYRelay(unsigned long duracion); // Activa buzzer y relé durante 'duracion' ms
// ---------- Setup ----------
void setup() {
inicializarHardware(); // Inicializa hardware y periféricos
mostrarMenu(); // Muestra el menú inicial en la pantalla
}
// ---------- Loop principal ----------
void loop() {
leerPotenciometroYActualizarSeleccion(); // Leer potenciómetro y actualizar selección
mostrarMenu(); // Actualizar LCD con la selección actual
if (leerBotonPulsado()) { // Si se detecta pulsación del botón
ejecutarDispensado(selecNumeroPosiciones); // Ejecutar la secuencia para 'n' posiciones seleccionadas
delay(200); // Pequeña pausa tras la secuencia para estabilidad
mostrarMenu(); // Volver a mostrar menú al terminar
}
}
// ---------- Implementación de funciones ----------
// Inicializa pines, comunicaciones y periféricos
void inicializarHardware() {
Wire.begin(); // Inicia la comunicación I2C
lcd.init(); // Inicializa el LCD I2C
lcd.backlight(); // Enciende la retroiluminación del LCD
pinMode(PIN_POT, INPUT); // Configura el pin del potenciómetro como entrada analógica
pinMode(PIN_BOTON, INPUT); // Configura el botón con resistencia interna pull-up
pinMode(PIN_BUZZER, OUTPUT); // Configura el pin del buzzer como salida
pinMode(PIN_RELAYS, OUTPUT); // Configura el pin del relé como salida
digitalWrite(PIN_BUZZER, LOW); // Asegura buzzer apagado al inicio
digitalWrite(PIN_RELAYS, LOW); // Asegura relé apagado al inicio
servoDispensador.attach(PIN_SERVO); // Conecta el servo al pin definido
servoDispensador.write(anguloPosicionOriginal); // Coloca servo en la posición original al iniciar
delay(200); // Pequeña espera para estabilizar hardware
lcd.clear(); // Limpia la pantalla LCD
}
// Muestra en el LCD la selección actual: cuántas posiciones ejecutar y la posición destacada
void mostrarMenu() {
lcd.setCursor(0, 0); // Sitúa cursor en fila 1, columna 0
lcd.print("N pos: "); // Texto fijo indicando número de posiciones
lcd.print(selecNumeroPosiciones); // Muestra el número de posiciones seleccionadas (1..MAX)
lcd.print("/"); // Separador
lcd.print(MAX_POSICIONES); // Muestra el máximo de posiciones disponibles
lcd.setCursor(0, 1); // Sitúa cursor en fila 2, columna 0
lcd.print("Paso "); // Texto "Paso" para indicar la posición visual
lcd.print(indiceMenu + 1); // Muestra el índice visual actual (1-based)
lcd.print("/"); // Separador
lcd.print(selecNumeroPosiciones); // Muestra hasta qué paso se ejecutará (limitado por selecNumeroPosiciones)
lcd.print(" Ang:"); // Indica que a continuación se muestra el ángulo
lcd.print(posiciones[indiceMenu]); // Muestra el ángulo de la posición destacada en el menú
}
// Lee el potenciómetro y actualiza la selección del número de posiciones y el índice de menú
void leerPotenciometroYActualizarSeleccion() {
int valorAnalogico = analogRead(PIN_POT); // Lee valor analógico 0..1023 desde el potenciómetro
// Mapear el valor del potenciómetro al rango 1..MAX_POSICIONES para definir cuántas posiciones ejecutar
uint8_t nuevoNumeroPos = map(valorAnalogico, 0, 1023, 1, MAX_POSICIONES); // Mapea valor a 1..MAX
if (nuevoNumeroPos < 1) nuevoNumeroPos = 1; // Asegurar mínimo 1 por seguridad
if (nuevoNumeroPos > MAX_POSICIONES) nuevoNumeroPos = MAX_POSICIONES; // Asegurar máximo
// Guardar la selección numérica (cantidad de pasos a ejecutar)
if (nuevoNumeroPos != selecNumeroPosiciones) { // Si cambió el número de posiciones seleccionado
selecNumeroPosiciones = nuevoNumeroPos; // Actualizar la variable global
// Si el índice del menú queda fuera del nuevo rango (ej. antes era mayor), ajustarlo
if (indiceMenu >= selecNumeroPosiciones) { // Si el índice visual excede la nueva cantidad
indiceMenu = selecNumeroPosiciones - 1; // Ajustar índice al máximo permitido
}
delay(60); // Pequeña pausa para evitar parpadeo y dar estabilidad visual
}
// Además, mapear el potenciómetro para mover el indicador del menú entre las posiciones visibles (0..selecNumeroPosiciones-1)
// Esto permite que el potenciómetro sirva también para seleccionar cuál de las primeras 'n' posiciones vemos
uint8_t nuevoIndiceMenu = map(valorAnalogico, 0, 1023, 0, selecNumeroPosiciones - 1); // Map a índice visual
if (nuevoIndiceMenu >= selecNumeroPosiciones) nuevoIndiceMenu = selecNumeroPosiciones - 1; // Seguridad
if (nuevoIndiceMenu != indiceMenu) { // Si cambió el índice visual
indiceMenu = nuevoIndiceMenu; // Actualizar índice del menú
delay(30); // Pequeña pausa para estabilidad visual
}
}
// Lee el botón con debounce y devuelve true si hubo una pulsación (presionar y soltar)
bool leerBotonPulsado() {
bool lectura = digitalRead(PIN_BOTON); // Lee estado actual del botón (INPUT_PULLUP => HIGH sin presionar)
unsigned long ahora = millis(); // Tiempo actual para debounce
// Si el estado cambió respecto al anterior, registrar tiempo del cambio
if (lectura != estadoBotonAnterior) { // Detecta borde de señal
tiempoUltimoCambioBoton = ahora; // Guardar tiempo del cambio
estadoBotonAnterior = lectura; // Actualizar estado anterior
}
// Si el estado se ha mantenido estable más tiempo que DEBOUNCE_MS, considerar lectura válida
if ((ahora - tiempoUltimoCambioBoton) > DEBOUNCE_MS) { // Comprobar debounce
// Detectar flanco de no presionado a presionado (HIGH -> LOW) y esperar a la liberación para confirmar pulsación completa
if (lectura == HIGH) { // Botón está siendo presionado (pullup => LOW)
// Esperar a que el usuario suelte el botón para evitar múltiples disparos
while (digitalRead(PIN_BOTON) == HIGH) { // Bloque hasta liberación (evita rebotes adicionales)
delay(10); // Pequeña espera dentro del bucle de bloqueo
}
delay(30); // Retardo post-liberación para seguridad
return true; // Indicar que hubo una pulsación completa
}
}
return false; // No hubo pulsación detectada
}
// Ejecuta el dispensado: recorre las primeras 'cantidad' posiciones del array y activa buzzer/relé por 2s
// Al finalizar, regresa el servo a la posición original guardada en 'anguloPosicionOriginal'
void ejecutarDispensado(uint8_t cantidad) {
if (cantidad < 1) return; // Seguridad: nada que hacer si la cantidad es menor que 1
if (cantidad > MAX_POSICIONES) cantidad = MAX_POSICIONES; // Seguridad: limitar a máximo definido
lcd.clear(); // Limpia la pantalla para mostrar estado de operación
lcd.setCursor(0, 0); // Posiciona cursor en la primera fila
lcd.print("Ejecutando..."); // Mensaje que indica inicio de secuencia
// Recorrer las posiciones 0 .. cantidad-1
for (uint8_t i = 0; i < cantidad; i++) {
lcd.setCursor(0, 1); // Colocar cursor en la segunda fila para actualizar estado
lcd.print("Paso "); // Texto fijo "Paso"
lcd.print(i + 1); // Mostrar número humano del paso (1-based)
lcd.print("/"); // Separador
lcd.print(cantidad); // Mostrar total a ejecutar
lcd.print(" Ang:"); // Texto "Ang:"
lcd.print(posiciones[i]); // Mostrar ángulo actual que se va a mover
moverServoA(posiciones[i]); // Mover servo a la posición i de forma suave
delay(200); // Pequeña espera tras el movimiento antes de activar el actuador
activarBuzzerYRelay(TIEMPO_DISPENSADO); // Activar buzzer y relé durante el tiempo definido (simula dispensado)
delay(300); // Pausa breve entre pasos para estabilizar
}
// Regresar el servo a la posición original definida al inicio del programa
moverServoA(anguloPosicionOriginal); // Mueve servo a la posición original guardada
// Mostrar mensaje final y volver al menú
lcd.clear(); // Limpiar LCD al finalizar
lcd.setCursor(0, 0); // Cursor fila 1
lcd.print("Terminado"); // Mensaje de fin de operación
lcd.setCursor(0, 1); // Cursor fila 2
lcd.print("Volviendo orig."); // Indicar que regresó a la posición original
delay(800); // Mostrar resultado por un momento antes de volver al menú
}
// Mueve el servo suavemente al ángulo objetivo usando pasos pequeños para evitar saltos
void moverServoA(uint8_t angulo) {
int angActual = servoDispensador.read(); // Leer la posición actual conocida por el objeto Servo
if (angActual == angulo) return; // Si ya está en el ángulo pedido, salir sin hacer nada
if (angActual < angulo) { // Si hay que incrementar ángulo
for (int a = angActual; a <= angulo; a += 2) { // Subir en pasos de 2 grados
servoDispensador.write(a); // Escribir ángulo intermedio al servo
delay(12); // Espera breve para permitir movimiento suave
}
} else { // Si hay que decrementar ángulo
for (int a = angActual; a >= angulo; a -= 2) { // Bajar en pasos de 2 grados
servoDispensador.write(a); // Escribir ángulo intermedio al servo
delay(12); // Espera breve para permitir movimiento suave
}
}
servoDispensador.write(angulo); // Asegurar posición final exacta en caso de residuo
delay(40); // Pequeña espera para asentar posición final
}
// Activa el buzzer activo y el relé juntos durante la duración indicada (en ms)
void activarBuzzerYRelay(unsigned long duracion) {
digitalWrite(PIN_RELAYS, HIGH); // Energizar el relé (activar válvula o bomba)
digitalWrite(PIN_BUZZER, HIGH); // Encender buzzer activo para señal sonora
delay(duracion); // Mantener ambos activos durante la duración dada
digitalWrite(PIN_BUZZER, LOW); // Apagar buzzer al terminar el tiempo
digitalWrite(PIN_RELAYS, LOW); // Apagar relé al terminar el tiempo
}
VASO 4
VASO 5
VASO 2
VASO 1
VASO 3