/*
* MEDICATION ADHERENCE SYSTEM - DUAL MODE CODE
* Supports both Proteus Simulation (AVR) and Physical Hardware Upload (ESP32)
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <RTClib.h>
#include <Keypad_I2C.h>
#include <Key.h>
#include <Keypad.h>
// ====================================================================
// MASTER SWITCH: Comment out the line below BEFORE uploading to real hardware!
//#define SIMULATION_MODE
// ====================================================================
#ifdef SIMULATION_MODE
// ---------------------------------------------------------
// PROTEUS SIMULATION ONLY (Compiling as Arduino Uno)
// ---------------------------------------------------------
#include <SoftwareSerial.h>
SoftwareSerial GSMSerial(A0, A1); // Proteus GSM RX, TX
//#define I2C_SDA 1 UNO auto uses A4 and A5(SCL)
//#define I2C_SCL 2
#define IR1_PIN 3
#define IR2_PIN 4
#define IR3_PIN 5
#define LED1_PIN 6
#define LED2_PIN 7
#define LED3_PIN 8
#define BUZZER_PIN 9
#else
// ---------------------------------------------------------
// REAL PHYSICAL ESP32 ONLY (Strictly Safe GPIOs)
// ---------------------------------------------------------
#define GSMSerial Serial
#define I2C_SDA 21
#define I2C_SCL 22
#define IR1_PIN 13
#define IR2_PIN 14
#define IR3_PIN 27
#define LED1_PIN 25
#define LED2_PIN 26
#define LED3_PIN 32
#define BUZZER_PIN 33
#define GSM_RX_PIN 16
#define GSM_TX_PIN 17
#endif
// I2C Addresses
#define KEYPAD_I2C_ADDR 0x20
#define LCD_I2C_ADDR 0x27
// ====================== GLOBAL OBJECTS ======================
LiquidCrystal_I2C lcd(LCD_I2C_ADDR, 16, 2);
RTC_DS1307 rtc;
const byte ROWS = 4;
const byte COLS = 4; // Updated to 4 columns
char keys[ROWS][COLS] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
// Direct ESP32 wiring for Wokwi (No PCF8574 needed)
byte rowPins[ROWS] = {19, 18, 5, 15};
byte colPins[COLS] = {2, 4, 12, 23}; // Added pin 14 for the 4th column
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// ====================== TIMING & SCHEDULE CONFIGS ======================
const unsigned long INTERVAL_BETWEEN_PILLS = 10000; // 10 seconds
const unsigned long TIMEOUT_NONE_TAKEN = 60000; // 5 minute
const unsigned long TIMEOUT_LAST_REMAINING = 60000; // 5 minute
struct Schedule {
bool enabled;
int hour;
int minute;
int numCompartments;
};
Schedule schedules[3] = {
{false, 0, 0, 0},
{false, 0, 0, 0},
{false, 0, 0, 0}
};
// ====================== SYSTEM VARIABLES ======================
DateTime now;
int lastSec = -1;
bool sequenceActive = false;
int activeScheduleIdx = -1;
int currentCompIdx = 0;
int totalCompsInSchedule = 0;
unsigned long lastPillTakenTime = 0;
unsigned long alertStartTime = 0;
bool alertActive = false;
bool missedSMSSent = false;
int currentCompNumber = 1;
char phoneNumber[16] = "+2348000000000";
char inputBuffer[20];
int inputIndex = 0;
int tempHour = 0;
int tempMinute = 0;
int selectedSchedule = 0;
enum MenuState {
MENU_IDLE,
MENU_MAIN,
MENU_SET_TIME,
MENU_SET_TIME_AMPM,
MENU_SET_PHONE,
MENU_SELECT_SCHEDULE,
MENU_SET_SCHED_TIME,
MENU_SET_SCHED_AMPM,
MENU_SET_SCHED_COMPS
};
MenuState currentMenu = MENU_IDLE;
// ====================== SETUP PROGRAMME ======================
void setup() {
Serial.begin(115200);
Wire.begin(); // Standard Wire.begin for simulation compatibility
rtc.begin(); // loads the time from computer
lcd.init();
lcd.begin(16, 2);
lcd.backlight();
lcd.print("System Booting..");
keypad.begin();
pinMode(LED1_PIN, OUTPUT);
pinMode(LED2_PIN, OUTPUT);
pinMode(LED3_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(IR1_PIN, INPUT_PULLUP);
pinMode(IR2_PIN, INPUT_PULLUP);
pinMode(IR3_PIN, INPUT_PULLUP);
if (!rtc.begin()) {
lcd.setCursor(0,1);
lcd.print("RTC Error!");
//while (1);
}
rtc.adjust(DateTime(2026, 4, 15, 7, 59, 50)); //force the rtc simulator to start at 07:59:50 AM
lcd.setCursor(0,1);
lcd.print("Init GSM... ");
// Conditional GSM Startup
#ifdef SIMULATION_MODE
GSMSerial.begin(9600);
#else
// GSMSerial.begin(9600, SERIAL_8N1, GSM_RX_PIN, GSM_TX_PIN);
#endif
delay(1000);
GSMSerial.println("AT"); delay(500);
GSMSerial.println("AT+CMGF=1"); delay(500);
lcd.clear();
lcd.print("Ready. Press #");
lcd.setCursor(0,1);
lcd.print("for Menu");
delay(2000);
}
// ====================== MAIN LOOP ======================
void loop() {
now = rtc.now();
// Check for alarm time if no sequence is active
if (!sequenceActive) checkSchedules();
// Process active medication sequence
if (sequenceActive) {
if (!alertActive) {
if (millis() - lastPillTakenTime >= INTERVAL_BETWEEN_PILLS) {
currentCompIdx++;
if (currentCompIdx < totalCompsInSchedule) {
startAlertForCompartment(currentCompIdx + 1);
} else {
stopAlertAndSequence(true); // All pills taken
}
}
} else {
checkPillTaken();
}
}
// Read Keypad
char key = keypad.getKey();
if (key) {
// Debugger: Print the key clicked to the top right of the LCD
//lcd.setCursor(15, 0);
//lcd.print(key);
Serial.print("key Pressed: ");
Serial.println(key);
// Send the key to the menu logic
handleMenu(key);
}
// Update UI if idle
if (currentMenu == MENU_IDLE && !alertActive && !sequenceActive) {
updateLCDdisplay();
}
//delay(50);
}
// ====================== CORE LOGIC FUNCTIONS ======================
void checkSchedules() {
for (int i = 0; i < 3; i++) {
if (!schedules[i].enabled) continue;
// Trigger exactly at second 0
if (now.hour() == schedules[i].hour && now.minute() == schedules[i].minute && now.second() == 0) {
startSchedule(i);
break;
}
}
}
void startSchedule(int idx) {
sequenceActive = true;
activeScheduleIdx = idx;
totalCompsInSchedule = schedules[idx].numCompartments;
currentCompIdx = 0;
missedSMSSent = false;
lastPillTakenTime = millis() - INTERVAL_BETWEEN_PILLS;
startAlertForCompartment(1);
}
void startAlertForCompartment(int compNumber) {
alertActive = true;
currentCompNumber = compNumber;
alertStartTime = millis();
digitalWrite(LED1_PIN, (compNumber==1));
digitalWrite(LED2_PIN, (compNumber==2));
digitalWrite(LED3_PIN, (compNumber==3));
digitalWrite(BUZZER_PIN, HIGH);
lcd.clear();
lcd.print("Open Comp ");
lcd.print(compNumber);
lcd.setCursor(0,1);
lcd.print("Timeout: ");
lcd.print(getTimeoutForCurrentState() / 60000);
lcd.print(" min");
}
void checkPillTaken() {
bool taken = false;
// Read specific IR sensor based on which compartment is currently active
if (currentCompNumber == 1) taken = !digitalRead(IR1_PIN);
if (currentCompNumber == 2) taken = !digitalRead(IR2_PIN);
if (currentCompNumber == 3) taken = !digitalRead(IR3_PIN);
if (taken) {
// Pill was taken on time!
alertActive = false;
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(LED1_PIN, LOW); digitalWrite(LED2_PIN, LOW); digitalWrite(LED3_PIN, LOW);
lastPillTakenTime = millis(); // Record the exact time they took the pill
lcd.clear();
lcd.print("Pill Taken!");
delay(1500);
return;
}
// If the pill hasn't been taken, check if the timeout has expired for THIS specific compartment
if (!missedSMSSent && (millis() - alertStartTime >= getTimeoutForCurrentState())) {
String msg = "ALERT: Missed dose in Compartment " + String(currentCompNumber) +
" at " + String(now.hour()) + ":" + String(now.minute());
sendSMS(msg);
missedSMSSent = true;
stopAlertAndSequence(false); // Stop the entire sequence; they failed adherence
}
}
unsigned long getTimeoutForCurrentState() {
// If they haven't opened the very first compartment, give them 5 minutes.
// If they have already started the sequence (meaning they took comp 1, and are now on comp 2 or 3),
// they only get 5 minutes to take the next one.
if (currentCompIdx == 0) {
return TIMEOUT_NONE_TAKEN; // 5 minutes (300000 ms)
} else {
return TIMEOUT_LAST_REMAINING; // 5 minutes (300000 ms) for ANY subsequent compartment
}
}
void stopAlertAndSequence(bool completed) {
sequenceActive = false;
alertActive = false;
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(LED1_PIN, LOW); digitalWrite(LED2_PIN, LOW); digitalWrite(LED3_PIN, LOW);
lcd.clear();
lcd.print(completed ? "All Doses Taken!" : "Missed Dose!");
delay(3000);
lcd.clear();
lastSec = -1;
}
void sendSMS(String message) {
lcd.clear();
lcd.print("Sending SMS...");
GSMSerial.print("AT+CMGS=\"");
GSMSerial.print(phoneNumber);
GSMSerial.println("\"");
delay(1000);
GSMSerial.print(message);
delay(500);
GSMSerial.write(26); // Ctrl+Z to send
delay(2000);
}
// ====================== UI & MENU LOGIC ======================
void handleMenu(char key) {
// Tactile feedback: Beep every time a key is pressed
digitalWrite(BUZZER_PIN, HIGH); delay(50); digitalWrite(BUZZER_PIN, LOW);
// --- ENTERING THE MAIN MENU ---
if (currentMenu == MENU_IDLE && key == '#') {
currentMenu = MENU_MAIN;
lcd.clear();
lcd.print("1:Time 2:Sched");
lcd.setCursor(0,1);
lcd.print("3:Phone *:Exit");
return;
}
// --- MAIN MENU SELECTION ---
if (currentMenu == MENU_MAIN) {
if (key == '1') {
currentMenu = MENU_SET_TIME;
inputIndex = 0;
lcd.clear();
lcd.print("Set Time (HHMM)");
lcd.setCursor(0,1);
lcd.print("12h format: ");
} else if (key == '2') {
currentMenu = MENU_SELECT_SCHEDULE;
lcd.clear();
lcd.print("Select Sched:");
lcd.setCursor(0,1);
lcd.print("1:Morn 2:Aft 3:Ngt");
} else if (key == '3') {
currentMenu = MENU_SET_PHONE;
inputIndex = 0;
memset(inputBuffer, 0, sizeof(inputBuffer));
lcd.clear();
lcd.print("Set Phone (*=+)");
lcd.setCursor(0,1);
} else if (key == '*') {
currentMenu = MENU_IDLE;
lcd.clear();
}
return;
}
// ==================== 1. SYSTEM TIME CONFIGURATION ====================
if (currentMenu == MENU_SET_TIME) {
if (key >= '0' && key <= '9' && inputIndex < 4) {
inputBuffer[inputIndex++] = key;
lcd.setCursor(12 + inputIndex - 1, 1);
lcd.print(key);
if (inputIndex == 4) { // 4 digits entered, move to AM/PM
inputBuffer[4] = '\0';
int val = atoi(inputBuffer);
tempHour = val / 100;
tempMinute = val % 100;
if (tempHour < 1 || tempHour > 12 || tempMinute > 59) {
lcd.clear(); lcd.print("Invalid Time!"); delay(1500);
currentMenu = MENU_IDLE; lcd.clear();
return;
}
currentMenu = MENU_SET_TIME_AMPM;
lcd.clear();
lcd.print("Is it AM or PM?");
lcd.setCursor(0,1);
lcd.print("* = AM # = PM");
}
}
return;
}
if (currentMenu == MENU_SET_TIME_AMPM) {
if (key == '*' || key == '#') {
bool isPM = (key == '#');
// Convert to 24h for the RTC
if (isPM && tempHour != 12) tempHour += 12;
if (!isPM && tempHour == 12) tempHour = 0;
rtc.adjust(DateTime(2026, 1, 1, tempHour, tempMinute, 0));
lcd.clear(); lcd.print("Time Saved!"); delay(1000);
currentMenu = MENU_IDLE; lcd.clear();
}
return;
}
// ==================== 2. MEDICATION SCHEDULE CONFIGURATION ====================
if (currentMenu == MENU_SELECT_SCHEDULE) {
if (key >= '1' && key <= '3') {
selectedSchedule = (key - '0') - 1; // 0, 1, or 2
currentMenu = MENU_SET_SCHED_TIME;
inputIndex = 0;
lcd.clear();
lcd.print("Set Time (HHMM)");
lcd.setCursor(0,1);
lcd.print("12h format: ");
}
return;
}
if (currentMenu == MENU_SET_SCHED_TIME) {
if (key >= '0' && key <= '9' && inputIndex < 4) {
inputBuffer[inputIndex++] = key;
lcd.setCursor(12 + inputIndex - 1, 1);
lcd.print(key);
if (inputIndex == 4) {
inputBuffer[4] = '\0';
int val = atoi(inputBuffer);
tempHour = val / 100;
tempMinute = val % 100;
if (tempHour < 1 || tempHour > 12 || tempMinute > 59) {
lcd.clear(); lcd.print("Invalid Time!"); delay(1500);
currentMenu = MENU_IDLE; lcd.clear();
return;
}
currentMenu = MENU_SET_SCHED_AMPM;
lcd.clear();
lcd.print("Time: AM or PM?");
lcd.setCursor(0,1);
lcd.print("* = AM # = PM");
}
}
return;
}
if (currentMenu == MENU_SET_SCHED_AMPM) {
if (key == '*' || key == '#') {
bool isPM = (key == '#');
if (isPM && tempHour != 12) tempHour += 12;
if (!isPM && tempHour == 12) tempHour = 0;
schedules[selectedSchedule].hour = tempHour;
schedules[selectedSchedule].minute = tempMinute;
currentMenu = MENU_SET_SCHED_COMPS;
lcd.clear();
lcd.print("How many Comps?");
lcd.setCursor(0,1);
lcd.print("Press 1, 2, or 3");
}
return;
}
if (currentMenu == MENU_SET_SCHED_COMPS) {
if (key >= '1' && key <= '3') {
schedules[selectedSchedule].numCompartments = key - '0';
schedules[selectedSchedule].enabled = true; // Turn the schedule ON
lcd.clear();
lcd.print("Schedule Active!");
delay(1500);
currentMenu = MENU_IDLE; lcd.clear();
}
return;
}
// ==================== 3. PHONE NUMBER CONFIGURATION ====================
if (currentMenu == MENU_SET_PHONE) {
if ((key >= '0' && key <= '9') && inputIndex < 15) {
inputBuffer[inputIndex++] = key;
lcd.setCursor(inputIndex-1,1);
lcd.print(key);
} else if (key == '*' && inputIndex == 0) {
inputBuffer[inputIndex++] = '+';
lcd.setCursor(inputIndex-1,1);
lcd.print('+');
} else if (key == '#') {
inputBuffer[inputIndex] = '\0';
strcpy(phoneNumber, inputBuffer);
lcd.clear(); lcd.print("Phone Saved!"); delay(1000);
currentMenu = MENU_IDLE; lcd.clear();
}
return;
}
}
void updateLCDdisplay() {
// static int lastSec = -1;
if (now.second() != lastSec) {
lastSec = now.second();
lcd.setCursor(0,0);
lcd.print("Time: ");
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());
lcd.setCursor(0,1);
lcd.print("Press # for Menu");
}
}Loading
esp32-devkit-c-v4
esp32-devkit-c-v4