Collaborators: Bhavneet Bola, Jeeda Sabri
<aside> đź’ˇ
Arduino-based design and software-driven Mealy FSM designed to manage medication access through scheduled or password-based logic.
The design integrates safety checks and ensures user responsiveness using hardware interrupts and timers. It emphasizes secure, user-aware interaction with embedded control systems.
</aside>
System features integrated in our dispensing system include:

System diagram of the functionality of the dispenser.

Pictorial diagram showcasing the buttons, buzzers, LEDs connected to the Arduino.

/* Assigning pin numbers for the buttons: Inputs */
const int empty_button = 11;
const int low_button = 12;
const int full_button = 13;
const int motion_button = A0;
const int time_button = A1;
const int password_button = A2;
const int password_button2 = A3;
/* Interrupt pin*/
const int interrupt_Pin = 2;
/* Assigning Pin numbers for the LEDs and buzzer: Outputs */
const int buzzer = 10;
const int time_led = 9;
const int password_led = 8;
const int full_led = 7;
const int low_led = 5;
const int empty_led = 6;
const int motion_led = 4;
/* Setting up flags for the interrupt loop, initializing all as 0 */
volatile int time_flag = 0;
volatile int password_flag = 0;
volatile int password2_flag = 0;
volatile int full_flag = 0;
volatile int low_flag = 0;
volatile int empty_flag = 0;
volatile int motion_flag = 0;
volatile int dispense_count = 0;
// counter used to determine if medication has been dispensed or not
volatile unsigned long dispense_time = 0; // timestamp for when medication dispensed
volatile bool motion_timer_active = false; // initializes the motion timer as OFF
volatile bool motion_alarm_active = false; // initializes the motion alarm as OFF
volatile int password_stage = 0; // initializes password stage as OFF
volatile unsigned long lastCapture = 0; // start point
/* timestamp used in the custom delay function, as a comparison point to determine if there has been a 1000ms delay between start and end point */
volatile unsigned long delaytimer = 0; // initializes timer for delay and is end point
volatile unsigned long maintimer = 0;
// initializes overarching timer for the system (ms)
volatile unsigned long debounceDelay = 50; // time range for debounce delay (ms)
volatile unsigned long lastInterruptTime = 0;
// initializes end point for the interrupt loop to create a debounce for the buttons
volatile unsigned long motiontimer = 0; // initializes timer that is tracking motion
volatile unsigned long milliscounter = 0;
// initializes timer that converts time from ms to second
volatile unsigned long secondcounter = 0;
// initializes variable to store # of seconds that have passed using milliscounter
void setup() {
// initializes buttons and declares their mode as inputs
pinMode(empty_button, INPUT_PULLUP);
pinMode(low_button, INPUT_PULLUP);
pinMode(full_button, INPUT_PULLUP);
pinMode(motion_button, INPUT_PULLUP);
pinMode(time_button, INPUT_PULLUP);
pinMode(password_button, INPUT_PULLUP);
pinMode(password_button2, INPUT_PULLUP);
// initializes interrupt pin as an INPUT
pinMode(interrupt_Pin, INPUT);
// initializes LEDs and buzzer as OUTPUT
pinMode(time_led, OUTPUT);
pinMode(password_led, OUTPUT);
pinMode(full_led, OUTPUT);
pinMode(low_led, OUTPUT);
pinMode(empty_led, OUTPUT);
pinMode(motion_led, OUTPUT);
pinMode(buzzer, OUTPUT);
// starts the serial monitor
Serial.begin(9600);
// setting up the interrupt loop (related to pin 2 which is the interrupt pin, related to the void interrupt() function, on the falling edge of the increments)
attachInterrupt(digitalPinToInterrupt(interrupt_Pin), interrupt, FALLING);
cli(); /* disables interrupts by clearing the flag bit (0) of global interrupt flag in CPU status register */
TCCR1A = 0;
TCCR1B = (1 << WGM12) | (1 << CS12);
OCR1A = 63;
TIMSK1 = (1 << OCIE1A);
sei(); /* enable interrupts by setting the flag bit (1) of global interrupt flag in CPU status register */
}
ISR(TIMER1_COMPA_vect) {
delaytimer++; // starts the timer (ms) that is used in the custom delay function
milliscounter++; // starts the timer (ms) that is used in the second counter loop
maintimer++; // starts the main system timer (ms)
if (milliscounter == 1000){ // once the system has counted 1000ms, enter this loop
secondcounter++; // increment the secondcounter by 1 to count number of seconds
milliscounter = 0; // reset the milliscounter to 0 to restart this process
}
}
void myDelay1(){ // a custom delay function that uses the timer1 we just adjusted
lastCapture = delaytimer; // timing of when it was called is captured for comparison
while((delaytimer - lastCapture) <= 1000){
}
// code stuck in this while loop until the timer has a 1000ms gap from the previous value
}
void loop() { // enter the main loop
if (motion_timer_active) { // if the motion timer is activated
if (motion_flag == 1) { // and if motion button has been pressed (motion detected)
motion_timer_active = false; // then turn off the motion timer
motion_alarm_active = false; // and also turn off the motion alarm
motion_flag = 0; // and set the motion flag as 0 for the interrupt loop
digitalWrite(motion_led, HIGH); // turn motion LED ON
Serial.println("Motion detected after dispensing");
myDelay1();
digitalWrite(motion_led, LOW); // turn motion LED OFF
}
else if (secondcounter - dispense_time >= 10) {
// if time interval between dispensed time and current time is >= 10 seconds
if (!motion_alarm_active) {
// if the motion alarm is not active
motion_alarm_active = true; // then make it active
Serial.println("No motion detected within 10 seconds! Alarm started");
}
// Sound the alarm until motion is detected
tone(buzzer, 1000); // blare the alarm continuously
myDelay1();
noTone(buzzer);
myDelay1();
// Check if motion was detected during the alarm
if (motion_flag == 1) {
// however, if now the motion button is clicked
motion_timer_active = false; // then set the timer OFF, stop timing
motion_alarm_active = false; // and turn the alarm OFF
motion_flag = 0; // and reset the motion flag
digitalWrite(motion_led, HIGH); // turn motion LED ON
Serial.println("Motion detected during alarm");
myDelay1();
digitalWrite(motion_led, LOW); // turn motion LED OFF
}
}
}
// Handle flag-based LED states
if (low_flag == 1) {
// if storage is low detected, turn the yellow LED ON until another supply button ON
digitalWrite(low_led, HIGH);
digitalWrite(full_led, LOW);
digitalWrite(empty_led, LOW);
Serial.print("low flag: ");
Serial.println(low_flag);
low_flag = 0;
}
else if (empty_flag == 1) {
// if storage is empty, turn the Red LED ON until another supply button ON
digitalWrite(empty_led, HIGH);
digitalWrite(low_led, LOW);
digitalWrite(full_led, LOW);
if (time_flag == 1) {
// if time button is pressed when empty, blare alarm once,
tone(buzzer, 1000);
myDelay1();
noTone(buzzer);
Serial.print("Not dispensing due to empty ");
Serial.println(dispense_count);
time_flag = 0; // reset the time flag
}
if (low_flag == 1 || full_flag == 1){
// if low or full button are clicked, then reset the empty flag
empty_flag = 0;
}
}
else if (full_flag == 1) {
// if storage is full, turn the Green LED ON until another supply button ON
digitalWrite(full_led, HIGH);
digitalWrite(low_led, LOW);
digitalWrite(empty_led, LOW);
Serial.print("full flag: ");
Serial.println(full_flag);
full_flag = 0; // reset the full flag
}
// if time button is pressed and has not dispensed previously
if (time_flag == 1 && dispense_count == 0) {
digitalWrite(time_led, HIGH); // Turn ON white LED
Serial.print("time flag: ");
Serial.println(time_flag);
myDelay1();
dispense_count = 1;
// turn dispense count up by 1 to ensure it doesn’t dispense again
Serial.print("Dispensing ");
Serial.println(dispense_count);
digitalWrite(time_led, LOW); //Turn OFF white LED
time_flag = 0; // reset the time flag
dispense_time = secondcounter;
// start motion timer to keep track of dispense time (when the pill was dispensed)
motion_timer_active = true; // make the motion timer ON
motion_alarm_active = false; // set the motion alarm as OFF
Serial.println("Motion timer started");
}
else if (time_flag == 1 && dispense_count == 1) {
// case where time button is clicked, but it has already been dispensed once already
tone(buzzer, 1000); // alarm goes off
myDelay1();
noTone(buzzer);
Serial.print("Not dispensing due to time ");
Serial.println(dispense_count);
time_flag = 0; // reset time flag
}
if (password_flag == 1) {
// if button 1 is clicked
if (password_stage == 0 || password_stage == 2) {
// if we are in stage 0 or in stage 2 when button 1 should be clicked
password_stage++; // then advance the button stage to the next
Serial.println("Button 1 pressed correctly");
} else {
// Wrong button in the sequence
password_stage = 0; // reset the password stage, go back to the start
digitalWrite(password_led, LOW);
Serial.println("Not dispensing due to wrong password");
tone(buzzer, 1000); // blare the buzzer once
myDelay1();
noTone(buzzer);
}
password_flag = 0; // reset the password flag
}
if (password2_flag == 1) {
// if button 2 is clicked
if (password_stage == 1 || password_stage == 3) {
// if we are in stage 1 or in stage 3 when button 2 should be clicked
password_stage++; // then advance the button stage to the next
Serial.println("Button 2 pressed correctly");
// Check if the full sequence is complete
if (password_stage == 4) {
// password was filled in correctly
if (dispense_count == 0 && empty_flag == 0) {
// the medication hasn’t been previously dispensed and the storage not empty
digitalWrite(password_led, HIGH);
Serial.println("Password correct! Dispensing"); // then dispense
myDelay1();
digitalWrite(password_led, LOW);
// Reset for next time
password_stage = 0; // reset the password stage back to 0
dispense_count = 1; // increment the counter, has been dispensed already
// Start the motion timer when dispensing via password
dispense_time = secondcounter;
motion_timer_active = true; // initiate the motion timer ON again
motion_alarm_active = false; // initiate motion alarm as OFF
Serial.println("Motion timer started");
}
else if (dispense_count == 1) {
// if password was correct, but the medication was already dispensed before
Serial.println("Password correct! Not dispensing, dispensed previously");
tone(buzzer, 1000); // blare the alarm once
myDelay1();
noTone(buzzer);
password_stage = 0; // reset the password stage
}
else if (empty_flag == 1) {
// if password was correct, but the storage is empty
Serial.println("Password correct! Not dispensing, because it is empty");
tone(buzzer, 1000); // blare the alarm once
myDelay1();
noTone(buzzer);
password_stage = 0;//reset the password stage
}
}
} else {
// wrong buttons were clicked, incorrect password
password_stage = 0; // reset the password stage
digitalWrite(password_led, LOW);
Serial.println("Not dispensing due to wrong password");
tone(buzzer, 1000); // blare the alarm once
myDelay1();
noTone(buzzer);
}
password2_flag = 0; //password2_flag reset
}
}
void interrupt() {
// the interrupt loop with all the flags to initiate buttons
if((maintimer - lastInterruptTime) > debounceDelay) {
/* if the time gap between main timer value and the timestamp when last interrupt happened is greater than 50ms, that means appropriate debounce has happened and we can proceed with the next step */
lastInterruptTime = maintimer;
// basically saying that the current timestamp is the new last interrupt time, update when another interrupt occurs
// if any button is clicked, then the appropriate flag value is incremented by 1
if (digitalRead(time_button) == LOW) {
time_flag = 1;
}
if (digitalRead(password_button) == LOW) {
password_flag = 1;
}
if (digitalRead(password_button2) == LOW) {
password2_flag = 1;
}
if (digitalRead(full_button) == LOW) {
full_flag = 1;
}
if (digitalRead(low_button) == LOW) {
low_flag = 1;
}
if (digitalRead(empty_button) == LOW) {
empty_flag = 1;
}
if (digitalRead(motion_button) == LOW) {
motion_flag = 1;
}
}
}