Arduino Easy Smart Home Series: Medicine Reminder & Smart Dispenser (RTC + ESP32 Alerts)

Ardunova Maker Series: Medicine Reminder & Smart Dispenser (RTC + ESP32 Alerts)

Affordable smart dispenser that drops medicine at scheduled times (RTC), detects whether it's taken (IR sensor), and if not taken within a timeout, the ESP32 broadcasts an alert across the local Wi-Fi (UDP broadcast).

Table of Contents

  • Introduction
  • Required Hardware
  • How It Works
  • Arduino IDE Setup
  • Wiring
  • ESP32 Code (RTC + Dispenser + UDP Alert)
  • How to Receive Alerts on Devices
  • Testing
  • Advanced Mods
  • Troubleshooting
  • Learning Insights
  • Conclusion

1. Introduction

Automatic reminders + physical dispenser for pills. Uses DS3231 RTC for reliable timekeeping. At scheduled times a servo actuates the dispenser to present the pill. An IR break-beam or reflective sensor watches the pill position; if the pill is not taken within a configurable timeout (e.g., 2 minutes), the ESP32 sends a UDP broadcast alert to the LAN so any listening device can show a notification.

2. Required Hardware

  • ESP32 development board (NodeMCU-32S or similar)
  • DS3231 RTC module (I2C)
  • Micro servo (SG90) or small dispenser mechanism
  • IR break-beam sensor or IR reflective sensor / microswitch
  • 5V servo supply (external recommended)
  • Jumper wires, small breadboard
  • Optional: buzzer, status LED

3. How It Works

  • ESP32 keeps Wi-Fi connected and reads DS3231 time.
  • User defines schedule(s) in code (or via web UI later).
  • At schedule: servo moves to dispense and returns.
  • If IR doesn't detect "taken" within TIMEOUT, ESP32 broadcasts UDP alert on port 4210.
  • Local listeners (phone/PC/other ESP) receive JSON alert and notify user.

4. Arduino IDE Setup

  1. Install Arduino IDE (or VSCode + PlatformIO).
  2. Add ESP32 board support (Espressif) to Boards Manager.
  3. Install libraries: RTClib, Wire, Servo, WiFi, WiFiUdp.
  4. Select board: Tools → Board → "ESP32 Dev Module".

5. Wiring

  • RTC: SDA → GPIO21, SCL → GPIO22 (ESP32 default I2C). Power per module spec (3.3V/5V) and common GND.
  • Servo signal → GPIO18 (or any PWM pin). Servo VCC → 5V external supply. Common GND required.
  • IR receiver digital out → GPIO15 (or any digital input). IR transmitter per module spec.
  • Buzzer (optional) → GPIO19 via transistor if high current.

6. ESP32 Code (RTC + Dispenser + UDP Alert)

Paste this into Arduino IDE. All '<' and '>' are escaped so Blogger won't break the code block. Replace YOUR_SSID and YOUR_PASSWORD, and calibrate servo angles & sensor logic.


#include <Wire.h>
#include "RTClib.h"
#include <WiFi.h>
#include <WiFiUdp.h>
#include <Servo.h>

///// CONFIG /////
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
const int UDP_PORT = 4210;

RTC_DS3231 rtc;
WiFiUDP Udp;
Servo dispenser;

const int servoPin = 18;      // servo signal pin
const int irPin = 15;         // IR receiver digital out (LOW when beam broken or HIGH depending on module)
const int ledPin = 2;         // status LED (onboard)
const unsigned long TAKE_TIMEOUT = 120000UL; // 2 minutes timeout

// Define schedule: {hour, minute}. Add multiple entries.
const int NUM_SCHEDULES = 3;
int schedules[NUM_SCHEDULES][2] = {
  {8, 0},   // 08:00
  {13, 0},  // 13:00
  {20, 0}   // 20:00
};

///// END CONFIG /////

unsigned long lastDispenseMillis = 0;
bool waitingForTake = false;
unsigned long dispenseStartMillis = 0;

void setup() {
  Serial.begin(115200);
  pinMode(irPin, INPUT);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  // init servo
  dispenser.attach(servoPin);
  dispenser.write(0); // locked/idle position

  // RTC init
  Wire.begin();
  if(!rtc.begin()) {
    Serial.println("Couldn't find RTC");
    while(1);
  }
  if (rtc.lostPower()) {
    Serial.println("RTC lost power, set the time!");
    // rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // optional: set compile time
  }

  // connect WiFi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting WiFi");
  int wifiAttempts = 0;
  while (WiFi.status() != WL_CONNECTED && wifiAttempts < 30) {
    delay(500);
    Serial.print(".");
    wifiAttempts++;
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("WiFi connected. IP: ");
    Serial.println(WiFi.localIP());
    Udp.begin(UDP_PORT);
  } else {
    Serial.println("WiFi failed to connect. Alerts disabled.");
  }
}

String makeAlertPayload(const char* type, const char* msg){
  DateTime now = rtc.now();
  char buf[200];
  snprintf(buf, sizeof(buf),
    "{\"type\":\"%s\",\"msg\":\"%s\",\"ts\":\"%04d-%02d-%02d %02d:%02d:%02d\",\"ip\":\"%s\"}",
    type, msg,
    now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second(),
    WiFi.localIP().toString().c_str()
  );
  return String(buf);
}

void broadcastAlert(const char* type, const char* msg) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Not on WiFi: can't send alert");
    return;
  }
  String payload = makeAlertPayload(type, msg);
  Serial.print("Broadcasting: "); Serial.println(payload);
  IPAddress broadcastIp = ~WiFi.subnetMask() | WiFi.gatewayIP(); // get broadcast
  // fallback to 255.255.255.255 if above is wrong:
  Udp.beginPacket(broadcastIp, UDP_PORT);
  Udp.write(payload.c_str());
  Udp.endPacket();

  // also send to 255.255.255.255 (some networks require this)
  Udp.beginPacket(IPAddress(255,255,255,255), UDP_PORT);
  Udp.write(payload.c_str());
  Udp.endPacket();
}

void dispenseOnce(){
  Serial.println("Dispensing pill...");
  digitalWrite(ledPin, HIGH);
  // Move servo to dispense position (adjust angles to your mechanism)
  dispenser.write(90);
  delay(700);            // adjust for your dispenser
  dispenser.write(0);
  delay(300);
  digitalWrite(ledPin, LOW);

  waitingForTake = true;
  dispenseStartMillis = millis();
  lastDispenseMillis = millis();
}

bool checkScheduleAndTrigger(){
  DateTime now = rtc.now();
  for(int i=0;i<NUM_SCHEDULES;i++){
    if(now.hour() == schedules[i][0] && now.minute() == schedules[i][1] && now.second() == 0){
      // avoid double trigger: ensure last dispense was not the same minute
      if(millis() - lastDispenseMillis > 60000UL){
        return true;
      }
    }
  }
  return false;
}

bool pillTaken(){
  // depends on sensor: adjust logic for your module
  // For typical break-beam IR module: when beam is broken, sensor might read LOW or HIGH.
  // Calibrate in your hardware. Here we assume DIGITAL LOW means beam broken (pill removed).
  int v = digitalRead(irPin);
  // Serial.printf("IR=%d\n", v);
  // If using reflective sensor or switch, invert logic accordingly.
  if(v == LOW) return true;
  return false;
}

void loop() {
  // check schedule
  if(!waitingForTake && checkScheduleAndTrigger()){
    dispenseOnce();
  }

  // if waiting for take, monitor sensor and timeout
  if(waitingForTake){
    if(pillTaken()){
      Serial.println("Pill taken detected.");
      waitingForTake = false;
      // optional: send confirmation broadcast
      broadcastAlert("TAKEN", "Medicine taken on time.");
    } else {
      // not yet taken: check timeout
      if(millis() - dispenseStartMillis > TAKE_TIMEOUT){
        waitingForTake = false;
        Serial.println("Pill NOT taken: timeout. Sending alert.");
        broadcastAlert("MISSED", "Medicine not taken within timeout!");
        // you can also retry dispense or blink LED or escalate
      }
    }
  }

  // small delay
  delay(200);
}

7. How to Receive Alerts on Devices

ESP32 broadcasts UDP packets on port 4210. Devices on same Wi-Fi can listen. Example listeners: Android UDP app, small Python script on PC, or another ESP/ESP32.

Simple Python UDP listener (for PC)


# Python 3 UDP listener example
import socket

PORT = 4210
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("0.0.0.0", PORT))
print("Listening for alerts on port", PORT)
while True:
    data, addr = sock.recvfrom(1024)
    print("Alert from", addr, ":", data.decode())

8. Testing

  1. Power ESP32 and confirm RTC time; set RTC once if needed.
  2. Set schedules a minute ahead for quick test.
  3. Upload code, open serial monitor, run a UDP listener on PC/phone.
  4. At scheduled time servo should move and dispenser activate.
  5. If you break the IR beam (take pill) listener gets "TAKEN"; if not, after timeout listener gets "MISSED".

9. Advanced Mods

  • Web UI on ESP32 to set schedules and timeout from browser.
  • MQTT integration to Home Assistant or local broker.
  • ESP32-CAM capture on missed doses (privacy warning).
  • Multiple compartments with servo selector or stepper motor.
  • Battery backup + low-power sleep between checks.

10. Troubleshooting

  • No Wi-Fi: Check SSID/password; use 2.4GHz; move board closer to router.
  • RTC wrong: Replace CR2032; set time with rtc.adjust()
  • Servo jitter: Use separate 5V supply, common ground, add capacitor across servo Vcc/GND.
  • IR false: Reposition sensor or use microswitch/weight sensor for reliability.
  • UDP blocked: Try 255.255.255.255 broadcast (code sends both) or use MQTT/HTTP.

11. Learning Insights

  • Combine RTC, networking, and mechanical actuation.
  • UDP broadcast is a lightweight local alert mechanism.
  • Sensor choice affects reliability—test with real pills and mechanism.
  • Keep alerts local if privacy is a concern; cloud services optional.

12. Conclusion

This medicine reminder + smart dispenser is practical and affordable. Start with a single dispenser + ESP32 + DS3231, then expand with web UI, MQTT, or multiple compartments. Calibrate sensors and servo angles for your hardware. Replace Wi-Fi alerts with MQTT or cloud push services if broadcasts are blocked by router.

By Kaushal Haladi

Comments

Popular Posts