#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <HX711.h>
#include <Servo.h>
//=====================
// LCD
//=====================
LiquidCrystal_I2C lcd(0x27, 16, 2); // Inicializa el LCD
//=====================
// HX711
//=====================
HX711 balanza; // Crea la balanza para el sensor de peso
const int hx_dt = A2; // línea de datos del HX711
const int hx_sck = A3; //la línea de reloj del HX711
//=====================
// BOTONES
//=====================
const int btn_polvo = 4;
const int btn_inicio = 11;
const int btn_tara = 12;
//=====================
// LEDS
//=====================
const int led_rojo = 7;
const int led_verde = 8;
// LED RGB
const int rgb_red = 5;
const int rgb_green = 6;
const int rgb_blue = 10;
//=====================
// SERVO
//=====================
const int servo_pin = 9; //
Servo compuerta; // objeto del servomotor
//=====================
// BUZZER
//=====================
const int buzzer = 13;
//=====================
// POTENCIOMETROS
//=====================
const int pot_peso = A0; // potenciómetro de peso del paciente
const int pot_edad = A1; // potenciómetro de edad del paciente
//=====================
// VARIABLES
//=====================
float peso_paciente = 0;
int edad_paciente = 0;
float dosis_objetivo = 0;
float peso_real = 0;
bool polvo_seleccionado = false; // verificar si el usuario eligió medicina
bool tara_realizada = false; // validar la calibración de tara inicial
float error_absoluto = 0.0; // absoluta de peso en gramos
float error_porcentual = 0.0; // desviación en porcentaje de la dosis real
int clasificacion_dosis = -1; // Estado de la dosis (-1: nulo, 0: bajo, 1: aceptado, 2: alto)
int subtipo_resultado = 0; // Clasifica la gravedad del error porcentual final
const float umbral_base = 10.0; // Tolerancia para el sistema
//=====================
// MÁQUINA DE ESTADOS
//=====================
enum EstadoSistema { ESPERA, MIDIENDO, CALCULANDO, RESULTADO, ALERTA, RESET_ST }; // Define los estados de la FSM
EstadoSistema estado_actual = ESPERA; // Inicializa la máquina en el estado de reposo ESPERA
unsigned long t_estado = 0;
unsigned long t_efecto = 0;
unsigned long t_debounce = 0;
void setup() {
Serial.begin(9600);
pinMode(btn_polvo, INPUT_PULLUP);
pinMode(btn_inicio, INPUT_PULLUP);
pinMode(btn_tara, INPUT_PULLUP);
pinMode(led_rojo, OUTPUT);
pinMode(led_verde, OUTPUT);
pinMode(rgb_red, OUTPUT);
pinMode(rgb_green, OUTPUT);
pinMode(rgb_blue, OUTPUT);
pinMode(buzzer, OUTPUT);
compuerta.attach(servo_pin);
compuerta.write(0); // Coloca el servo en la posición inicial cerrada a 0 grados
lcd.init();
lcd.backlight();
balanza.begin(hx_dt, hx_sck); // Inicializa HX711
balanza.set_scale(420.5); // Carga el factor numérico de calibración de gramos obtenido en pruebas
balanza.tare(); // Pone a cero absoluto el contador restando el peso propio del platillo
lcd.clear();
lcd.setCursor(0, 0); lcd.print("DISPENSADOR");
lcd.setCursor(0, 1); lcd.print("Seleccione");
}
void loop() {
unsigned long t_actual = millis(); // Registra el tiempo transcurrido en milisegundos desde el encendido
// --- DETECCIÓN DEL BOTÓN RESET (TARA) ---
if (digitalRead(btn_tara) == LOW) { // Evalúa si el botón de tara física fue presionado
if (t_actual - t_debounce >= 300) { // filtro de software anti-rebote de 300 milisegundos
estado_actual = RESET_ST; // Fuerza al estado de reinicio completo
t_estado = t_actual; // Almacena el tiempo de entrada al estado de reset
t_debounce = t_actual; // Actualiza el temporizador
}
}
// máquina de estados
switch (estado_actual) {
case ESPERA: // Estado de reposo y variables del paciente
establecer_rgb(0, 0, 255); // Enciende el LED RGB en color azul
digitalWrite(led_rojo, LOW); //LED indicador rojo apagado
digitalWrite(led_verde, LOW); //LED indicador verde apagado
compuerta.write(0); //Mantiene el servomotor en 0 grados
noTone(buzzer); // Apaga de forma segura cualquier sonido
if (digitalRead(btn_polvo) == LOW) { // Detecta si el usuario pulsa la selección de medicamento
if (t_actual - t_debounce >= 300) { // Protege boton
polvo_seleccionado = true; //habilitar el inicio de dosificación
peso_paciente = map(analogRead(pot_peso), 0, 1023, 23, 120); // Convierte la lectura de 23 a 120 kg
edad_paciente = map(analogRead(pot_edad), 0, 1023, 9, 90); // Convierte la lectura de 9 a 90 años
// Calculo de dosis segun la edad
float ajuste_edad = (edad_paciente < 12) ? 0.9 : ((edad_paciente < 65) ? 1.0 : 0.8);
dosis_objetivo = (peso_paciente * 10.0 * ajuste_edad) / 1000.0; // Aplica fórmula matemática para hallar gramos
Serial.println("\n===============\nPACIENTE");
Serial.print("Peso: "); Serial.print(peso_paciente); Serial.println(" kg");
Serial.print("Edad: "); Serial.println(edad_paciente);
Serial.print("Objetivo Calculado: "); Serial.print(dosis_objetivo, 3); Serial.println(" g");
// Impresión de la pantalla LCD
lcd.clear();
lcd.setCursor(0, 0); lcd.print("POLVO SELEC."); // Indica el fármaco configurado
lcd.setCursor(0, 1); lcd.print(dosis_objetivo, 2); lcd.print(" g"); // Muestra la masa meta
tara_realizada = true; // tara igual a cero
t_debounce = t_actual; // Guarda el tiempo para evitar rebotes cíclicos
}
}
if (digitalRead(btn_inicio) == LOW) { // dispensado activo
if (t_actual - t_debounce >= 300) { // anti-rebote
if (polvo_seleccionado) { // Valida si se definieron la edad y peso del paciente
estado_actual = MIDIENDO; // Cambia la máquina de estados
t_estado = t_actual; // Inicia el reloj del estado
lcd.clear();
} else {
lcd.clear();
lcd.print("Configure Primero"); // Bloqueo si no hay datos
delay(1000);
lcd.clear();
lcd.setCursor(0, 0); lcd.print("DISPENSADOR");
lcd.setCursor(0, 1); lcd.print("Seleccione");
}
t_debounce = t_actual;
}
}
break;
case MIDIENDO: // apertura y flujo de harina
if (t_actual - t_efecto >= 10) { // Temporizador asíncrono para modulación lumínica (10ms)
t_efecto = t_actual; // Resetea el tiempo
int intensidad = 127 + 127 * sin(2 * PI * t_actual / 1000.0); // Onda senoidal para modular brillo PWM
establecer_rgb(intensidad, intensidad, intensidad); // Aplica efecto pulsante blanco en el RGB
}
digitalWrite(led_rojo, HIGH); // Activa el LED rojo indicando que la tolva está abierta 0
digitalWrite(led_verde, LOW); // Mantiene apagado el LED verde
compuerta.write(90); // a compuerta física abierta
peso_real = balanza.get_units(1); // guarda el peso real
lcd.setCursor(0, 0); lcd.print("Dispensando ");
lcd.setCursor(0, 1); lcd.print("P:" + String(peso_real, 2) + " g ");
if (peso_real >= dosis_objetivo || (t_actual - t_estado >= 5000)) {
estado_actual = CALCULANDO; // Transiciona a la etapa procesamiento de errores
t_estado = t_actual; // Actualiza el tiempo
}
break;
case CALCULANDO: // análisis de errores y desactivación de actuadores de potencia
establecer_rgb(255, 255, 0); // olor amarillo en el RGB indicando eestado de calculo
compuerta.write(0); // cerrar la compuerta
lcd.setCursor(0, 0); lcd.print("PROCESANDO ERROR");
lcd.setCursor(0, 1); lcd.print("CALCULANDO... ");
subtipo_resultado = calcularDosis(peso_real); // Llama a la función matemática pasándole la masa real
Serial.println("\n DOSIS COMPLETADA ");
Serial.print("Estado: "); Serial.println(estado_actual);
Serial.print("Objetivo: "); Serial.print(dosis_objetivo, 3); Serial.println(" g");
Serial.print("Real: "); Serial.print(peso_real, 3); Serial.println(" g");
Serial.print("Error Porcentual: "); Serial.print(error_porcentual, 2); Serial.println(" %");
Serial.println("=========================================");
estado_actual = RESULTADO; // estado que visualiza la calidad
t_estado = t_actual; // Almacena el tiempo
break;
case RESULTADO: // Estado evaluador con error de 10%
mostrarEstado(); // Ajusta los colores del LED RGB
lcd.setCursor(0, 0);
if (clasificacion_dosis == 1) { // Verifica si el peso final se mantuvo dentro de la tolerancia del 10%
lcd.print("DOSIS CORRECTA "); // Publica resultado
digitalWrite(led_rojo, LOW); // Mantiene apagado el LED de peligro
digitalWrite(led_verde, HIGH); // valida la dosis
tone(buzzer, 1000, 500); // Genera un pitido de confirmacion
} else {
lcd.print("DOSIS INCORRECTA"); // Muestra el mensaje de rechazo
digitalWrite(led_rojo, HIGH); // Enciende el rojo
digitalWrite(led_verde, LOW); // Apaga led verde de exitoso
}
lcd.setCursor(0, 1);
lcd.print("Err: " + String(error_porcentual, 1) + "%"); // Visualiza el porcentaje de error real
if (t_actual - t_estado >= 3000) {
if (clasificacion_dosis == 1) { // Si la dosis fue exitosa, regresa de forma automática al inicio
estado_actual = ESPERA; // pasa a reposo
polvo_seleccionado = false; // Borra el medicamento previo
lcd.clear();
lcd.setCursor(0, 0); lcd.print("Seleccione"); // Vuelve a invitar al usuario a elegir parámetros
lcd.setCursor(0, 1); lcd.print("Medicamento");
} else {
estado_actual = ALERTA; // Si falló, el sistema al estado pasa a estado emergencia ALERTA
t_estado = t_actual;
}
}
break;
case ALERTA: // Estado crítico de seguridad
activarAlertas(); // suena buzzer
lcd.setCursor(0, 0); lcd.print("ALERTA DE ERROR ");
lcd.setCursor(0, 1); lcd.print("FUERA DE RANGO ");
if (t_actual - t_estado >= 5000) { // Retiene la alarma por 5 segundos
estado_actual = ESPERA; // el tiempo
polvo_seleccionado = false; // Reinicia la máquina
lcd.clear();
lcd.setCursor(0, 0); lcd.print("Seleccione");
lcd.setCursor(0, 1); lcd.print("Medicamento");
}
break;
case RESET_ST: // Estado de restauración forzada por usuario
establecer_rgb(0, 0, 0); // Apaga todos los canales del LED RGB
compuerta.write(0); // Garantiza el servo a cero grados
digitalWrite(led_rojo, LOW); // Desactiva el LED rojo
digitalWrite(led_verde, LOW); // Desactiva el LED verde
lcd.setCursor(0, 0); lcd.print("REINICIANDO SISTEMA... ");
if (t_actual - t_estado < 200) {
tone(buzzer, 1200); //SUENA EL BUZZER
} else {
noTone(buzzer);
balanza.tare(); // Fuerza ejecutar una nueva tara
polvo_seleccionado = false; // Desactiva el fármaco
estado_actual = ESPERA; // vuelve a estado de espera
lcd.clear();
lcd.setCursor(0, 0); lcd.print("DISPENSADOR");
lcd.setCursor(0, 1); lcd.print("Seleccione");
}
break;
}
}
// Función para procesarerrores
int calcularDosis(float lecturaBalanza) {
float error = lecturaBalanza - dosis_objetivo;
error_absoluto = abs(error);
error_porcentual = (error_absoluto / dosis_objetivo) * 100.0;
// Clasifica la dosificación
if (lecturaBalanza < dosis_objetivo * (1.0 - (umbral_base / 100.0))) {
clasificacion_dosis = 0; // Dosificación baja (Masa insuficiente)
} else if (lecturaBalanza > dosis_objetivo * (1.0 + (umbral_base / 100.0))) {
clasificacion_dosis = 2; // Dosificación alta (Masa excesiva con riesgo clínico)
} else {
clasificacion_dosis = 1; // Dosificación correcta (Dentro del margen del 10%)
}
// Define subtipos numéricos para determinar la gravedad final de la desviación
if (error_porcentual <= umbral_base) {
return 0; // (Dosis aprobada)
} else if (error_porcentual > umbral_base && error_porcentual <= (umbral_base + 15.0)) {
return 1; // Error moderado
} else {
return 2; // Error crítico (Fuera de rango severo)
}
}
// Función encargada de actualizar el color del LED RGB al finalizar el dispensado
void mostrarEstado() {
if (subtipo_resultado == 0) {
establecer_rgb(0, 255, 0); // Color Verde: Indica calidad óptima aprobada
} else if (subtipo_resultado == 1) {
establecer_rgb(255, 255, 0); // Color Amarillo: Indica desvío moderado en zona de advertencia
} else {
establecer_rgb(255, 0, 0); // Color Rojo: Indica desborde masivo peligroso
}
}
// Función para controlar parapadeo de leds sin usar delay()
void activarAlertas() {
if ((millis() / 250) % 10 == 0) {
establecer_rgb(255, 0, 0); // Enciende canal rojo RGB (parpadea)
tone(buzzer, 800); // enciende buzzer
} else {
establecer_rgb(0, 0, 0); // Apaga el LED RGB
noTone(buzzer); // Apaga el buzzer creando el efecto intermitente de sirena de laboratorio
}
}
// Función para modular canales usando señales PWM del temporizador
void establecer_rgb(int r, int g, int b) {
analogWrite(rgb_red, r); // Modula el ciclo de trabajo del pin rojo (0-255)
analogWrite(rgb_green, g); // Modula el ciclo de trabajo del pin verde (0-255)
analogWrite(rgb_blue, b); // Modula el ciclo de trabajo del pin azul (0-255)
}