DoseMate – Pastillero Inteligente

AUTORES: Álvaro Bravo Pareja,Carlos Asensio Trujillo, Jorge Naranjo Ballesteros y Pablo Valdés Colomo.

Pastillero
Pastillero

Introducción:

Se propone el desarrollo de un pastillero inteligente basado en Arduino, orientado a facilitar el seguimiento de tratamientos médicos y reducir olvidos en la toma de medicación. El sistema será capaz de recordar al usuario la hora de la toma, dispensar automáticamente la pastilla correspondiente y registrar la confirmación de que la medicación ha sido tomada.

Para ello, utilizará como entradas botones de confirmación y fotorresistencias (LDR) para detectar la presencia de pastillas. Como salidas, incorporará una pantalla LCD I2C 1602 para mostrar el estado del sistema, LEDs y un buzzer para generar alertas sonoras, un servo motor como tapa y un motor paso a paso encargado del mecanismo de compartimentos.

Cuando llegue la hora programada, el sistema avisará al usuario, abrirá el compartimento correspondiente y actualizará el estado tras la confirmación de la toma.

Además, se plantea la integración con una aplicación externa que permita consultar la información del tratamiento mediante un registro de tomas (tanto a tiempo como tardías) y modificar la hora de toma directamente desde la aplicación.

Reparto del trabajo:

En el trabajo, todos hemos trabajado en todos los ámbitos, pero cada miembro nos hemos centrado principalmente en una cosa:

Álvaro: Código y montaje del circuito.

Pablo: Desarrollo de la aplicación móvil.

Carlos: Investigación de componentes, funcionamiento y conexiones.

Jorge: Diseñar el pastillero y empotrar el circuito en él.

Material:

COMPONENTE USO
1 x ELEGOO UNO R3 Control principal del sistema
1 x Protoboard Montaje del circuito
1 x LCD 16×2 (I2C) Mostrar información del sistema
1 x Módulo Bluetooth HC-05 Comunicación con la aplicación
1 x Módulo RTC DS3231 Mantener la hora del sistema y guardar hora de toma
1 x Servo motor Apertura y cierre de la tapa del compartimento compartimento
1 x Motor paso a paso Giro del compartimento del pastillero
1 x LED Indicador visual
1 x Buzzer Alerta sonora
4 x Pulsadores Interacción del usuario (configuración y confirmación)
1 x LDR Detectar retirada de la pastilla
1 x Resistencia 2kΩ Divisor de tensión (HC05)
1 x Resistencia 1kΩ Divisor de tensión (HC05)
1 x Resistencia 10kΩ Resistencia para LDR
1 x Resistencia 220Ω Protección del LED
1 x Cable USB Arduino Alimentación y programación
Cables dupont femenino a femenino, masculino a femenino y masculino a masculino Conexión entre componentes
1 x Driver ULN2003 Controlar el motor paso a paso
2x Pilas 1,5V + porta pilas Alimentación del motor stepper
1 x Pila reloj Para el módulo RTC
Carton pluma Caja pastillero
Bisagras Puerta trasera pastillero
Porexpan Material compartimentos de pastillas
Silicona Montaje caja
Cinta aislante Sujeción cables

La gran mayoría de los materiales fueron proporcionados por el profesor, sin embargo tuvimos que comprar los componentes que no teníamos y necesitábamos para la implementación

Costes material no entregado:

COMPONENTE PRECIO
Módulo RTC 3,75€
Pantalla LCD I2C 6€
Módulo Bluetooth HC-05 6€
Cables Dupont 8,5€
Pilas 1,5V 3€
Pila reloj 2€
Gastos de envío ~5€

TOTAL: ~31€

Además, hay que sumar los gastos del material usado para la construcción:

MATERIAL PRECIO
Porta pilas 5€
Cartón pluma 15€
Bisagras 3€
Porexpan 3€
Silicona 3€
Cinta aislante 3€

TOTAL: ~30€

Como se puede ver, el coste total del proyecto ha sido, aproximadamente, unos 60€

Casos de Uso Del Sistema:

Caso de Uso 1: Configurar hora de toma

Descripción: El usuario ajusta la hora y los minutos a los que desea recibir el recordatorio.

Flujo básico:

  1. El usuario pulsa el botón de configuración.
  2. Ajusta la hora con el botón correspondiente.
  3. Ajusta los minutos.
  4. Confirma la configuración.

Resultado: La hora queda guardada en el sistema mediante la memoria EEPROM.

Caso de Uso 2: Recordatorio de medicación

Descripción: El sistema avisa al usuario cuando llega la hora programada.

Flujo básico:

  1. El sistema detecta la hora mediante el módulo RTC.
  2. Se activan el buzzer y el LED.
  3. Se muestra el aviso en la pantalla LCD.

Resultado: El usuario recibe una alerta visual y sonora.

Caso de Uso 3: Dispensar pastilla

Descripción: El sistema abre el compartimento correspondiente para permitir la toma.

Flujo básico:

  1. El usuario pulsa el botón OK.
  2. El motor paso a paso gira hasta el compartimento correspondiente.
  3. El servo motor abre la tapa.

Resultado: La pastilla queda accesible para el usuario.

Caso de Uso 4: Confirmar toma

Descripción: El sistema detecta que la pastilla ha sido retirada.

Flujo básico:

  1. El LDR detecta un cambio de luz.
  2. El sistema verifica la retirada de la pastilla.
  3. Se cierra la tapa del compartimento.

Resultado: La toma queda registrada como realizada.

Caso de Uso 5: Modo recarga

Descripción: Permite rellenar los compartimentos del pastillero.

Flujo básico:

  1. El usuario activa el modo recarga.
  2. El sistema permite girar los compartimentos.
  3. El usuario rellena las pastillas.

Resultado: El pastillero queda preparado para su uso.

Caso de Uso 6: Comunicación con la app

Descripción: El sistema envía información al móvil mediante Bluetooth.

Flujo básico:

  1. Se registra un evento de toma realizada.
  2. El sistema envía los datos en formato JSON.

Resultado: La aplicación recibe y registra la información de la toma.

Diagrama de Casos de Uso:

Este diagrama representa las principales interacciones entre el usuario y el sistema del pastillero inteligente, incluyendo la configuración, el recordatorio, la dispensación y el registro de la toma.

Código del Proyecto:

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Servo.h>
#include <RTClib.h>
#include <SoftwareSerial.h>
#include <AccelStepper.h>
#include <EEPROM.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);
RTC_DS3231 rtc;
SoftwareSerial BT(10, 11);
Servo tapa;
AccelStepper stepper(AccelStepper::FULL4WIRE, 8, 9, A1, A2);

#define ADDR_HORA 0
#define ADDR_MIN  1
#define ADDR_POS  2

const int ledPin       = 5;
const int buzzerPin    = 2;
const int botonOK      = 3;
const int botonHora    = 6;
const int botonMin     = 7;
const int botonRecarga = A3;
const int ldrPin       = A0;
const int pinServo     = 4;

bool sistemaListo = false;

enum Estado { ESPERA, RECORDATORIO, ALARMA, PREPARANDO, ESPERANDO_TOMA, CONFIGURANDO, RECARGA };
Estado estado = ESPERA;

int horaToma = 14, minutoToma = 10, posicionActual = 0;
bool recordatorioActivado = false;
bool tomaFueTarde = false;
unsigned long tEstado = 0;
const int pasosPorCompartimento = 341;

int luzBase = 0;
int contadorLecturas = 0;
unsigned long lastOK = 0, lastHora = 0, lastMin = 0, lastRec = 0;
const int debounceTime = 180;
unsigned long lastBuzz = 0;
bool buzzState = false;

int umbralLuz = 4;

void esperarMovimientoServo(int ms) {
  unsigned long start = millis();
  while (millis() - start < ms) {
    stepper.run();
  }
}

void guardarConfig() {
  EEPROM.update(ADDR_HORA, horaToma);
  EEPROM.update(ADDR_MIN, minutoToma);
  EEPROM.update(ADDR_POS, posicionActual);
}

void cargarConfig() {
  int h = EEPROM.read(ADDR_HORA);
  int m = EEPROM.read(ADDR_MIN);
  int p = EEPROM.read(ADDR_POS);

  if (h >= 0 && h < 24) horaToma = h;
  if (m >= 0 && m < 60) minutoToma = m;
  if (p >= 0 && p < 6)  posicionActual = p;
}

bool botonPulso(int pin, unsigned long &lastTime) {
  if (digitalRead(pin) == LOW && millis() - lastTime > debounceTime) {
    lastTime = millis();
    return true;
  }
  return false;
}

int leerLuz() {
  long suma = 0;

  for (int i = 0; i < 10; i++) {
    suma += analogRead(ldrPin);
    delay(2);
  }

  return suma / 10;
}

void enviarRespuesta(String msg) {
  BT.println("OK: " + msg);
}

void enviarEventoToma(bool tarde) {
  BT.print(F("{\"evento\":\"TOMA\""));
  BT.print(F(",\"ts\":"));
  BT.print(millis());
  BT.print(F(",\"tarde\":"));
  BT.print(tarde ? F("true") : F("false"));
  BT.print(F(",\"hora_toma\":"));
  BT.print(horaToma);
  BT.print(F(",\"min_toma\":"));
  BT.print(minutoToma);
  BT.print(F(",\"pos\":"));
  BT.print(posicionActual);
  BT.println(F("}"));
}

void procesarBluetooth() {

  if (BT.available()) {

    delay(50);

    String cmd = BT.readString();
    cmd.trim();
    cmd.toUpperCase();

    if (cmd.indexOf("SET:") != -1) {

      int p = cmd.indexOf(':');

      horaToma = cmd.substring(p + 1, p + 3).toInt();
      minutoToma = cmd.substring(p + 4, p + 6).toInt();

      guardarConfig();
      recordatorioActivado = false;

      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Nueva hora:");

      char buf[16];
      sprintf(buf, "    %02d:%02d", horaToma, minutoToma);

      lcd.setCursor(0, 1);
      lcd.print(buf);

      enviarRespuesta("HORA CAMBIADA");

      delay(2000);

      lcd.clear();
    }

    else if (cmd.indexOf("STATUS") != -1) {

      DateTime now = rtc.now();

      String s =
        "RELOJ:" + String(now.hour()) + ":" + String(now.minute()) +
        " | TOMA:" + String(horaToma) + ":" + String(minutoToma) +
        " | POS:" + String(posicionActual);

      enviarRespuesta(s);
    }

    else if (cmd.indexOf("PING") != -1) {
      enviarRespuesta("CONECTADO");
    }
  }
}

void moverASiguiente() {
  posicionActual = (posicionActual + 1) % 6;
  guardarConfig();
  stepper.move(pasosPorCompartimento);
}

void iniciarToma() {

  tomaFueTarde = (estado == ALARMA);

  moverASiguiente();

  noTone(buzzerPin);

  estado = PREPARANDO;

  lcd.clear();
}

void setup() {

  Wire.begin();

  if (rtc.begin() && rtc.lostPower()) {
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

  cargarConfig();

  BT.begin(9600);
  BT.setTimeout(80);

  lcd.init();
  lcd.backlight();

  stepper.setMaxSpeed(1200);
  stepper.setAcceleration(600);
  stepper.setCurrentPosition(0);

  pinMode(ledPin, OUTPUT);
  pinMode(buzzerPin, OUTPUT);

  pinMode(botonOK, INPUT_PULLUP);
  pinMode(botonHora, INPUT_PULLUP);
  pinMode(botonMin, INPUT_PULLUP);
  pinMode(botonRecarga, INPUT_PULLUP);

  delay(800);

  tapa.attach(pinServo);
  tapa.write(25);

  delay(500);

  tapa.detach();

  sistemaListo = true;

  BT.println(F("PASTILLERO LISTO"));
}

void loop() {

  stepper.run();

  DateTime now = rtc.now();

  procesarBluetooth();

  digitalWrite(
    ledPin,
    (estado == ALARMA ? millis() % 200 < 100 :
    (estado == RECORDATORIO ? millis() % 600 < 300 : LOW))
  );

  switch (estado) {

    case ESPERA: {

      char buf[16];

      sprintf(buf, "Hora: %02d:%02d", now.hour(), now.minute());
      lcd.setCursor(0, 0);
      lcd.print(buf);

      sprintf(buf, "Toma: %02d:%02d", horaToma, minutoToma);
      lcd.setCursor(0, 1);
      lcd.print(buf);

      if (botonPulso(botonOK, lastOK)) {
        estado = CONFIGURANDO;
        lcd.clear();
      }

      if (botonPulso(botonRecarga, lastRec)) {

        estado = RECARGA;

        lcd.clear();

        tapa.attach(pinServo);
        tapa.write(90);

        esperarMovimientoServo(600);

        tapa.detach();
      }

      if (
        now.hour() == horaToma &&
        now.minute() == minutoToma &&
        !recordatorioActivado
      ) {

        estado = RECORDATORIO;

        tEstado = millis();

        recordatorioActivado = true;

        lcd.clear();
      }

      if (
        now.hour() != horaToma ||
        now.minute() != minutoToma
      ) {
        recordatorioActivado = false;
      }

    } break;

    case CONFIGURANDO: {

      lcd.setCursor(0, 0);
      lcd.print("Ajustar Hora");

      char buf[10];

      sprintf(buf, "    %02d:%02d", horaToma, minutoToma);

      lcd.setCursor(0, 1);
      lcd.print(buf);

      if (botonPulso(botonHora, lastHora)) {
        horaToma = (horaToma + 1) % 24;
      }

      if (botonPulso(botonMin, lastMin)) {
        minutoToma = (minutoToma + 1) % 60;
      }

      if (botonPulso(botonOK, lastOK)) {

        guardarConfig();

        estado = ESPERA;

        lcd.clear();
      }

    } break;

    case RECORDATORIO:
    case ALARMA:

      lcd.setCursor(0, 0);

      lcd.print(
        estado == RECORDATORIO ?
        "HORA DE TOMA" :
        "!!! ALARMA !!!"
      );

      if (millis() - lastBuzz > (estado == RECORDATORIO ? 600 : 250)) {

        lastBuzz = millis();

        buzzState = !buzzState;

        if (buzzState) {
          tone(buzzerPin, estado == RECORDATORIO ? 700 : 1200);
        }
        else {
          noTone(buzzerPin);
        }
      }

      if (
        millis() - tEstado > 30000 &&
        estado == RECORDATORIO
      ) {
        estado = ALARMA;
      }

      if (botonPulso(botonOK, lastOK)) {
        iniciarToma();
      }

      break;

    case PREPARANDO:

      lcd.setCursor(0, 0);
      lcd.print("Preparando...");

      lcd.setCursor(0, 1);
      lcd.print("Comp: ");
      lcd.print(posicionActual);

      if (stepper.distanceToGo() == 0) {

        tapa.attach(pinServo);
        tapa.write(95);

        esperarMovimientoServo(600);

        tapa.detach();

        luzBase = leerLuz();

        contadorLecturas = 0;

        estado = ESPERANDO_TOMA;

        lcd.clear();
      }

      break;

    case ESPERANDO_TOMA:

      lcd.setCursor(0, 0);
      lcd.print("Retire pastilla");

      if (abs(leerLuz() - luzBase) > umbralLuz) {
        contadorLecturas++;
      }
      else if (contadorLecturas > 0) {
        contadorLecturas--;
      }

      if (contadorLecturas > 5) {

        tapa.attach(pinServo);
        tapa.write(25);

        esperarMovimientoServo(600);

        tapa.detach();

        noTone(buzzerPin);

        lcd.clear();
        lcd.print("Toma OK");

        enviarEventoToma(tomaFueTarde);

        delay(1500);

        estado = ESPERA;

        lcd.clear();
      }

      break;

    case RECARGA:

      lcd.setCursor(0, 0);
      lcd.print("MODO RECARGA");

      lcd.setCursor(0, 1);
      lcd.print("Pos: ");
      lcd.print(posicionActual);

      if (botonPulso(botonOK, lastOK)) {
        moverASiguiente();
      }

      if (botonPulso(botonRecarga, lastRec)) {

        tapa.attach(pinServo);
        tapa.write(25);

        esperarMovimientoServo(600);

        tapa.detach();

        estado = ESPERA;

        lcd.clear();
      }

      break;
  }
}

A continuación la explicación escrita:

El código completo sigue una lógica bastante clara basada en estados.

En primer lugar, se incluyen las librerías necesarias para poder controlar todos los elementos del sistema, como la pantalla LCD, el módulo RTC para la hora, el Bluetooth, el servo motor, el motor paso a paso y la memoria EEPROM. Después, se definen los pines a los que están conectados los distintos componentes, como los botones, el buzzer, el stepper, etc.

A continuación, se define una máquina de estados que controla el funcionamiento del sistema. Los estados principales son espera, recordatorio, alarma, configuración, recarga y esperando toma.

En cuanto al setup, se inicializan todos los componentes. Se inicia la comunicación con el RTC para obtener la hora real, se carga la hora de la toma desde la EEPROM para no perderla al apagar el sistema, se configura el Bluetooth, se inicializa la pantalla LCD y se establecen los pines de entrada y salida. También se posiciona el servo en su posición inicial para evitar movimientos bruscos al arrancar y se obtiene el ultimo compartimento usado a través de la memoria EEPROM, que también guarda esta información.

En el loop se ejecuta continuamente la lógica del sistema. Primero se obtiene la hora actual mediante el RTC y se comprueba si hay comunicación por Bluetooth para poder modificar la hora de la toma desde una aplicación externa. Después, dependiendo del estado en el que se encuentre el sistema, se ejecutan distintas acciones.

En el estado de espera, el sistema muestra la hora actual y la hora programada en la pantalla. Además, permite entrar en modo configuración o modo recarga mediante los botones.

En el modo configuración, el usuario puede ajustar la hora y los minutos de la toma, y estos valores se guardan en la EEPROM.

Cuando la hora actual coincide con la hora programada, el sistema pasa al estado de recordatorio. En este estado se activa el buzzer y el LED para avisar al usuario. Si el usuario no responde en un tiempo determinado, el sistema pasa al estado de alarma, donde la alerta es más intensa (el buzzer hace más ruido y el LED parpadea más rápido).

Cuando el usuario confirma la toma pulsando el botón, se ejecuta la función de iniciar toma. En este punto, el motor paso a paso gira el pastillero hasta el siguiente compartimento y el servo abre la tapa para permitir el acceso a la pastilla.

Una vez abierta la tapa, el sistema entra en el estado de esperando toma. Aquí se utiliza el sensor LDR para detectar si la pastilla ha sido retirada. Se compara la luz actual con una referencia inicial, y si se detecta un cambio suficiente durante varias lecturas, el sistema interpreta que la pastilla ha sido cogida. En ese momento, se cierra la tapa, se detiene la alarma y se registra la toma. Además, cuando se detecta la toma, se envía un mensaje en formato JSON a través de Bluetooth para que nuestra aplicación pueda registrar la información.

Por último, el modo recarga permite al usuario girar manualmente el pastillero para rellenar los compartimentos. Esto se realiza mediante el botón correspondiente, que va avanzando la posición del motor paso a paso.

Para comprender mejor su funcionamiento, se añade un diagrama de estados:

Y un diagrama de flujo:

También incluimos un video explicando el código:

Hardware e Implementacion:

Este es el circuito montado en wokwi:

Y un video explicando las conexiones:

Una vez montado, el circuito queda así:

Integrarlo dentro de la carcasa fue bastante complicado, tuvimos que hacer cálculos de huecos, extensiones de cables, usar medidas exactas para huecos…

Además, añadiendo que no sabemos soldar, la complejidad del montaje fue aún mayor, teniendo que depender mucho de cinta aislante que de vez en cuando se soltaba.

Finalmente, tras todos estos problemas, conseguimos empotrar el sistema bastante bien, sin tener ningún problema de cables sueltos.

El interior del sistema empotrado queda así:

Incluimos un video explicando el montaje por dentro:

Nuestra aplicación está implementada mediante App Inventor, ya que uno de los miembros de nuestro equipo tenía experiencia previa programando aplicaciones con este programa.

Para explicar el funcionamiento de la aplicación en detalle, hemos hecho este video:

Además, también se añade un diagrama de casos de uso:

Problemas y Posibles Soluciones:

Uno de los principales problemas que tuvimos durante la práctica fue la conexión Bluetooth con la aplicación móvil. Al principio pensábamos que el fallo estaba en el módulo Bluetooth, pero finalmente descubrimos que el problema se encontraba en el código de la aplicación, ya que la gestión de la conexión no estaba bien implementada. Tras varias pruebas conseguimos estabilizar la comunicación entre ambos dispositivos.

Otro problema importante estuvo relacionado con el sensor LDR, ya que nos costó bastante conseguir que detectara correctamente la toma de la pastilla. Tuvimos que implementar una medición de la luz base media del ambiente para obtener la luz antes de retirar una pastilla y posteriormente hacer varias lecturas al retirar la pastilla y si la diferencia entre estas supera el umbral, se cierra la tapa.

También encontramos dificultades al mostrar la información en la pantalla, porque los datos no se imprimían en el momento exacto o aparecían desordenados. Para solucionarlo, tuvimos que refactorizar varias veces el código, estudiar bien en qué momento debíamos imprimir en la pantalla y asegurarnos de que el contenido de esta se borra antes de imprimir para evitar caracteres no deseados.

Por último, el programa se bloqueaba con frecuencia debido al uso de delay(), ya que detenía la ejecución del resto de tareas. Para solucionarlo sustituimos delay() por millis(), consiguiendo que el sistema funcionara de forma más fluida y estable.

Posibles ampliaciones:

Debido a los materiales que nos han entregado, nuestro presupuesto y el tiempo disponible, el pastillero tiene una amplia variedad de posibles mejoras.

La primera de todas sería añadir más huecos para pastillas por compartimento, para esto serían necesarios más LDRs que detecten los demás huecos de las pastillas.

Otra posible mejora sería añadir más tomas diarias. Para esto, sería necesario modificar el código para guardarlas en la memoria EEPROM, modificar la lógica para que el pastillero tenga esto en cuenta a la hora de mostrar las alarmas y cambiar el modo de configuración de hora para poder moverse entre las distintas tomas. Además, también habría que modificar la aplicación.

Por último, se podrían añadir más compartimentos de dos formas distintas:

  • Usar un motor con más potencia capaz de mover un carrusel más grande, sumando el peso de las pastillas
  • Añadir más motores (tanto servo como stepper) para crear más huecos donde recoger pastillas

Video funcionamiento:

Por último, añadimos un video en el que se explica el funcionamiento de nuestro pastillero:

*El vídeo se grabó antes de cambiar las pegatinas de los botones, por eso son distintas a las de la foto.

Video Memoria:

Dejamos adjunto un vídeo tipo memoria en el que juntamos todos los vídeos mostrados anteriormente:

También te podría gustar...

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *