SaveDose: pastillero automático
Canal de Youtube
https://www.youtube.com/channel/UC-_vfpkI3VTB_ZssYJn1GsQ
Descripción del proyecto
El proyecto consiste en un pastillero automático inteligente que permite la planificación y distribución de pastillas de acuerdo con los horarios programados por el usuario. El dispositivo tiene 7 temporizadores configurables que permiten al usuario programar hasta 7 momentos distintos para la dispensación de pastillas. Se comunica con el usuario a través de una aplicación móvil por medio de Bluetooth (la cual se puede descargar mediante el siguiente enlace ), brindando una experiencia interactiva para gestionar y administrar la toma de medicamentos.
Características del sistema
- Temporizadores Configurables: El pastillero tiene 7 temporizadores independientes que el usuario puede ajustar según sus necesidades.
- Interacción Bluetooth: Se conecta a la aplicación móvil a través de Bluetooth para gestionar el pastillero, programar los temporizadores y gestionar el código de seguridad.
- Código de Seguridad: Al iniciar la aplicación, el usuario debe ingresar un código para poder acceder a la dispensación de pastillas.
- Pantalla LCD: La pantalla LCD muestra la hora en tiempo real y las alertas de los temporizadores.
- Alarma de Recordatorio: Cuando un temporizador se activa, una alarma suena para avisar al usuario de que es momento de recoger la pastilla.
- Monitoreo de Estado mediante semáforo LED: un semáforo muestra el estado del pastillero basándose en la cantidad restante en el pastillero (verde en caso de estar completamente lleno o casi completo, amarillo en caso de que tenga la mitad o menos dosis, rojo en caso de que haga falta rellenarlo).
- Vencimiento de Tiempo: Si el usuario no recoge la pastilla en el tiempo determinado (15 segundos), el sistema indica que el tiempo ha expirado y no dispensará la pastilla.
Implementación y desarollo
Componentes Hardware
Componente | Función |
Elegoo Mega 2560 R3 | Microcontrolador y placa de conexiones. Sustituye a la placa de Arduino dada en clase |
Módulo Bluetooth HC-05 | Para la comunicación inalámbrica con la app. |
Pantalla LCD1602 | Muestra información sobre los temporizadores y el estado del sistema. |
Módulo RTC DS3231 | Para obtener la hora exacta. |
Motor Paso a Paso | Para el dispensado automático de pastillas. Gira para hacer que se mueva la pieza que empuja las pastillas por la rampa. |
Semáforo | Para indicar el estado del pastillero (verde : lleno o semilleno. Amarillo: quedan menos de la mitad de las dosis programadas. Rojo: hay que rellenar el pastillero) |
LED | Para indicar de manera visual que hay que recoger la dosis |
Zumbador | Funciona como alarma que avisa de que hay que recoger la dosis |
Botón | Para que el usuario indique que ya se ha rellenado el pastillero |
Código
#include <Wire.h>
#include <RTClib.h>
#include <SoftwareSerial.h>
#include <LiquidCrystal.h>
#include <Stepper.h>
RTC_DS3231 rtc;
class Fecha {
int anio;
int mes;
int dia;
int hora;
int minuto;
int segundo;
int diasPorMes[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
public:
Fecha() : anio(0), mes(0), dia(0), hora(0), minuto(0), segundo(0) {}
Fecha(int a, int m, int d, int h, int min, int seg) {
anio = a;
mes = m;
dia = d;
hora = h;
minuto = min;
segundo = seg;
}
int getAnio() { return anio; }
int getMes() { return mes; }
int getDia() { return dia; }
int getHora() { return hora; }
int getMinuto() { return minuto; }
int getSegundo() { return segundo; }
void setAnio(int a) { anio = a; }
void setMes(int m) { mes = m; }
void setDia(int d) { dia = d; }
void setHora(int h) { hora = h; }
void setMinuto(int min) { minuto = min; }
void setSegundo(int seg) { segundo = seg; }
bool isLaterThan(DateTime f) {
if (anio != f.year()) return anio > f.year();
if (mes != f.month()) return mes > f.month();
if (dia != f.day()) return dia > f.day();
if (hora != f.hour()) return hora > f.hour();
if (minuto != f.minute()) return minuto > f.minute();
return segundo > f.second();
}
bool isValid() {
if (mes < 1 || mes > 12) return false;
if (dia < 1 || dia > diasPorMes[mes - 1]) return false;
if (hora < 0 || hora > 23) return false;
if (minuto < 0 || minuto > 59) return false;
if (segundo < 0 || segundo > 59) return false;
return true;
}
void assign(Fecha f) {
anio = f.getAnio();
mes = f.getMes();
dia = f.getDia();
hora = f.getHora();
minuto = f.getMinuto();
segundo = f.getSegundo();
}
};
#define bt Serial1
//SoftwareSerial bt(18, 19); // RX, TX
// Configuración de la pantalla LCD
const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
// Pines para semáforo, LED, zumbador, botón y motor paso a paso
int semRojo = 22;
int semAmarillo = 24;
int semVerde = 26;
int ledTomar = 32;
int zumbador = 28;
int botonRellenar = 30;
const int stepsPerRevolution = 290; // Para el motor 28BYJ-48
Stepper myStepper(stepsPerRevolution, 7, 8, 9, 10);
// Variables de control
int pastillasRestantes = 0; // Inicia vacío
String codigoUsuario = "";
Fecha temporizadores[7]; // Los 7 temporizadores se setearán con 'cambiarTemporizador'
int temporizadorActual = 0; // Índice del temporizador actual
// La fecha actual se obtendrá del RTC (aunque tenemos una fecha inicial de respaldo)
Fecha fechaActual(2025, 4, 17, 19, 30, 0);
// flag para evitar múltiples dispensaciones para el mismo temporizador
bool pastillaDispensada = false;
//
// FUNCIONES
//
void mostrarConScrollLCD(String mensaje, int tiempoScroll = 300) {
int largoMensaje = mensaje.length();
for (int i = 0; i <= largoMensaje - 16; i++) {
lcd.clear(); // Limpiar pantalla para mostrar nuevo fragmento
lcd.setCursor(0, 0);
lcd.print(mensaje.substring(i, i + 16)); // Mostrar solo los primeros 16 caracteres
delay(tiempoScroll); // Pausar entre desplazamientos
}
}
// Función que muestra el semáforo según el stock de pastillas.
// Si no hay pastillas, muestra "No hay pastillas / Pulsa el botón" y llama a esperarRelleno()
void controlSemaforo() {
if (pastillasRestantes == 0) { // Pastillero vacío: se debe rellenar
digitalWrite(semRojo, HIGH);
digitalWrite(semAmarillo, LOW);
digitalWrite(semVerde, LOW);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("No hay pastillas");
lcd.setCursor(0, 1);
lcd.print("Pulsa el boton:");
esperarRelleno();
}
else if (pastillasRestantes > 0 && pastillasRestantes <= 4) {
digitalWrite(semRojo, LOW);
digitalWrite(semAmarillo, HIGH);
digitalWrite(semVerde, LOW);
}
else {
digitalWrite(semRojo, LOW);
digitalWrite(semAmarillo, LOW);
digitalWrite(semVerde, HIGH);
}
}
// Función que espera a que el usuario rellene el pastillero,
// mostrando mensajes en Bluetooth y en la pantalla.
void esperarRelleno() {
bt.println("Debes rellenar las pastillas. Una vez lo hayas hecho, dale al botón");
bool rellenado = false;
while (!rellenado) {
if (digitalRead(botonRellenar) == HIGH) {
pastillasRestantes = 7; // Se considera que se han rellenado todas
rellenado = true;
lcd.clear();
}
}
bt.println("Pastillero relleno");
}
// Función para cambiar el código de acceso, similar al código original.
void cambiarCodigo(bool mostrarMensaje) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Cambio codigo:");
bool recibidoValido = false;
if (mostrarMensaje) {
bt.println("Introduce un codigo para recoger tus pastillas:");
}
delay(1000);
while (!recibidoValido) {
if (bt.available()) {
String codigo = bt.readStringUntil('\n');
codigo.trim();
if (codigo.length() == 0) {
Serial.println("Falla: el codigo debe tener al menos un caracter");
} else {
codigoUsuario = codigo;
lcd.setCursor(0, 1);
lcd.print(codigoUsuario);
Serial.println("Nuevo codigo: " + codigoUsuario);
bt.println("Nuevo codigo: " + codigoUsuario + " . Operacion realizada con exito");
recibidoValido = true;
lcd.clear();
}
}
}
}
// Función que dispensa una pastilla (tras validar el código) y muestra el mensaje "Recoja su pastilla".
void dispensarPastillas() {
if (pastillasRestantes > 0) { // Solo procede si hay pastillas
lcd.clear();
lcd.setCursor(0, 0);
mostrarConScrollLCD("Introduce tu codigo personal:");
bt.println("Introduce el codigo personal para recoger tu pastilla:");
unsigned long startTime = millis();
bool recibidoValido = false;
// Espera hasta 10 segundos para recibir el código
while (!recibidoValido && millis() - startTime < 20000) {
if (bt.available()) {
String codigo = bt.readStringUntil('\n');
codigo.trim();
if (codigo.equals(codigoUsuario)) {
lcd.clear();
lcd.setCursor(0, 0);
mostrarConScrollLCD("Recoja su pastilla");
delay(1500); // Se muestra el mensaje para que el usuario lo vea
myStepper.step(stepsPerRevolution); // Dispensa la pastilla
pastillasRestantes--;
recibidoValido = true;
} else {
bt.println("Codigo incorrecto. Intenta de nuevo:");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Codigo incorrecto");
}
} else {
digitalWrite(ledTomar, HIGH);
digitalWrite(zumbador, HIGH);
delay(500);
digitalWrite(zumbador, LOW);
digitalWrite(ledTomar, LOW);
delay(500);
}
}
if (!recibidoValido) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Tiempo agotado");
bt.println("Se ha agotado el tiempo para dispensar la pastilla");
delay(2000);
}
} else {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("No hay pastillas");
controlSemaforo();
delay(2000);
}
// Avanza al siguiente temporizador y marca que se dispensó esta pastilla.
temporizadorActual = (temporizadorActual + 1) % 7;
pastillaDispensada = true;
}
// Función para cambiar el temporizador (fecha/hora) según comando recibido por Bluetooth.
void cambiarTemporizador() {
int numero = 0;
bool recibidoValido = false;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Cambio fecha ");
bt.println("Introduce el temporizador (1-7) a cambiar:");
while (!recibidoValido) {
if (bt.available()) {
String input = bt.readStringUntil('\n');
numero = input.toInt();
if (numero >= 1 && numero <= 7) {
bt.println("Temporizador " + String(numero) + " seleccionado");
recibidoValido = true;
lcd.print(numero);
} else {
bt.println("Numero no valido, intenta de nuevo.");
}
}
}
recibidoValido = false;
bt.println("Introduce una fecha en formato dd/mm/aaaa hh:mm:");
while (!recibidoValido) {
if (bt.available()) {
String mensaje = bt.readStringUntil('\n');
mensaje.trim();
bt.print("Recibido: [");
bt.print(mensaje);
bt.println("]");
Fecha nuevaFecha;
nuevaFecha.setDia(mensaje.substring(0, 2).toInt());
nuevaFecha.setMes(mensaje.substring(3, 5).toInt());
nuevaFecha.setAnio(mensaje.substring(6, 10).toInt());
nuevaFecha.setHora(mensaje.substring(11, 13).toInt());
nuevaFecha.setMinuto(mensaje.substring(14, 16).toInt());
if (nuevaFecha.isValid()) {
temporizadores[numero - 1].assign(nuevaFecha);
bt.println("Operacion correcta.");
recibidoValido = true;
lcd.clear();
} else {
bt.println("Fecha no valida, intenta de nuevo.");
}
}
}
}
// Actualiza la pantalla LCD con la fecha y hora actual obtenida del RTC.
void actualizarFechaEnPantalla() {
lcd.clear();
DateTime now = rtc.now();
lcd.setCursor(0, 0);
if (now.day() < 10) lcd.print("0");
lcd.print(now.day());
lcd.print("/");
if (now.month() < 10) lcd.print("0");
lcd.print(now.month());
lcd.print("/");
lcd.print(now.year());
lcd.print(" Sig:" + String(temporizadorActual + 1));
lcd.setCursor(0, 1);
if (now.hour() < 10) lcd.print("0");
lcd.print(now.hour());
lcd.print(":");
if (now.minute() < 10) lcd.print("0");
lcd.print(now.minute());
lcd.print(":");
if (now.second() < 10) lcd.print("0");
lcd.print(now.second());
}
//
// CONFIGURACIÓN
//
void setup() {
Serial.begin(9600);
Serial1.begin(9600);
lcd.begin(16, 2);
pinMode(semRojo, OUTPUT);
pinMode(semAmarillo, OUTPUT);
pinMode(semVerde, OUTPUT);
pinMode(ledTomar, OUTPUT);
pinMode(zumbador, OUTPUT);
pinMode(botonRellenar, INPUT);
myStepper.setSpeed(60); // revoluciones por minuto
if (!rtc.begin()) {
Serial.println("No se pudo encontrar el RTC");
while (1);
}
if (rtc.lostPower()) {
Serial.println("RTC perdió energía, configurando hora...");
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
digitalWrite(zumbador, LOW);
delay(1000);
bt.println("Bienvenido. Por favor, introduce un codigo para recoger tus pastillas.");
// Configuraciones iniciales: código, temporizador y pastillero vacío.
actualizarFechaEnPantalla();
cambiarCodigo(false);
cambiarTemporizador();
pastillasRestantes = 0; // Inicia vacío
// Mensaje inicial enviado por Bluetooth para que se ingrese el código personal
bt.println("Puedes cambiar 'codigo' o 'tiempo' escribiendo ese comando.");
}
//
// CICLO PRINCIPAL
//
void loop() {
DateTime now = rtc.now();
actualizarFechaEnPantalla();
controlSemaforo();
// Si la hora actual es igual o supera el temporizador programado,
// se procede a dispensar la pastilla (se valida con !isLaterThan).
if (!temporizadores[temporizadorActual].isLaterThan(now)) {
if (!pastillaDispensada) {
dispensarPastillas();
}
} else {
pastillaDispensada = false;
}
// Procesa comandos entrantes por Bluetooth para cambiar el código o el temporizador
if (bt.available()) {
String comando = bt.readStringUntil('\n');
comando.trim();
String codigo;
if (comando.equals("codigo")) {
bt.println("Cambio de codigo. Introduce tu codigo actual:");
bool disponible = false;
while (!disponible) {
if (bt.available()) {
codigo = bt.readStringUntil('\n');
codigo.trim();
disponible = true;
}
}
if (codigo.equals(codigoUsuario)) {
cambiarCodigo(true);
} else {
bt.println("Codigo incorrecto.");
}
}
else if (comando.equals("tiempo")) {
bt.println("Cambio de temporizador:");
cambiarTemporizador();
}
}
delay(100);
}
Planos del sistema
A continuación, se pueden ver los planos iniciales del sistema, los cuales se hicieron cuando se diseñó el pastillero.

Plano de la base del pastillero

Plano de la estructura que gira

Plano de la estructura que gira por abajo

plano de la caja que contiene el pastillero

plano lateral 1 de la caja

plano lateral 2 de la caja
Diseño del circuito en Fritzing

circuito en Fritzing
Casos de uso
ID-CU | Título | Actor | Descripción |
CU-1 | Rellenar pastillero | Usuario | El usuario presiona el botón de «rellenar» para indicar que ha colocado las pastillas en el pastillero. Una vez realizado el relleno, el sistema lo notifica y se restablece el número de dosis 7. |
CU-2 | Cambiar el código de acceso | Usuario | El usuario puede cambiar el código de acceso para retirar las pastillas a través de la comunicación Bluetooth con la app móvil. Se solicita al usuario introducir el código actual y luego se le permite establecer uno nuevo. |
CU-3 | Configurar temporizadores | Uusario | A través de Bluetooth, el usuario puede configurar hasta 7 temporizadores (día, mes, hora, minuto) para programar el dispensado automático de pastillas. Cada temporizador puede configurarse de manera independiente. |
CU-4 | Recoger pastilla | Uusario | Cuando el temporizador alcanza la hora programada, el sistema emite una alarma (zumbador y luz LED) para notificar al usuario. El usuario debe ingresar su código personal a través de la app para recibir la pastilla. |
CU-5 | Verificar estado del pastillero | Usuario | El sistema indica el estado del pastillero mediante un semáforo de LED (Verde: Pastillero lleno o con suficientes pastillas. Amarillo: Pastillero con menos de la mitad de las pastillas. Rojo: Pastillero vacío y necesita ser rellenado.) |
CU-6 | Alarma por tiempo agotado | Usuario | Si el usuario no ingresa el código de acceso para recoger la pastilla dentro de los 10 segundos, el sistema emite una advertencia por Bluetooth y en la pantalla LCD («Tiempo agotado»), y no se dispensa la pastilla. |
CU-7 | Actualizar fecha y hora del sistema | Sistema | El sistema sincroniza la fecha y hora automáticamente usando el módulo RTC DS3231. La hora actual se muestra en la pantalla LCD para el usuario y se usa para gestionar los temporizadores. |
Vídeo del sistema
Fotos del sistema

montaje completo

conexiones en la placa

posición del motor
Costes
Pieza | Unidades | Coste (€) |
ELEGOO MEGA Azul R3 Board ATmega 2560 Compatible Con Arduino | 1 | 25,99 |
AZDelivery HC-05 Bluetooth Wireless Module | 1 | 10,99 |
AZDelivery RTC DS3231 | 1 | 7,49 |
Semáforo LED | 3 | 8,49 |
Láminas de madera de balsa | 2 | 7,5 |
Lámina de madera de la caja | 1 | 10 |
Total | 70,46 |