/*
Smart Medibox
todo: Implement Interrupts
todo: Organize helper functions, pin and constant definitions into separate header files
todo: Tune UI/UX of menu and setting times in alarms
todo: User onboarding with examples
todo: Alarm snooze functionality
todo: Remember UTC_OFFSET during restarts
todo: Wifi Connection using Mobile
todo: debug mode and analytics
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHTesp.h>
#include <WiFi.h>
// OLED parameters
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
// Pin definitions
#define BUZZER_PIN 5
#define DHT_PIN 12
#define LED_1_PIN 15 // Red LED
#define LED_2_PIN 2 // Yellow LED
// Push button definitions
#define UP_BUTTON_PIN 35 // Go up or previous
#define OK_BUTTON_PIN 32 // Select, accept or options
#define DOWN_BUTTON_PIN 33 // Go down or next
#define CANCEL_BUTTON_PIN 34 // Cancel menu or stop alarm
#define BTN_DELAY 100
// Time parameters
#define NTP_SERVER "pool.ntp.org"
#define UTC_OFFSET 0
#define UTC_OFFSET_DST 0
// Wi-Fi params
#define WIFI_DELAY 200
#define WIFI_SSID "Wokwi-GUEST"
#define WIFI_PWD ""
// Delay time after printing message on display
#define MESSAGE_DELAY 1000
// Delay time for push buttons debounce & other common wait
#define DELAY 200
// Initializing display and sensor objects
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
DHTesp dhtSensor;
// Time parameters to keep hours & minutes to check alarms
int currentHours = 0;
int currentMinutes = 0;
// UTC Offset parameter in seconds
long utcOffsetSec = 0;
// Alarm parameters
struct Alarm {
int hours;
int minutes;
bool triggered;
};
bool alarmEnabled = false;
constexpr int numAlarms = 3;
Alarm alarms[numAlarms] = {
{0, 0, true},
{0, 0, true},
{0, 0, true}
};
// Buzzer tone parameters
constexpr int numNotes = 8;
constexpr int noteC = 262;
constexpr int noteD = 294;
constexpr int noteE = 330;
constexpr int noteF = 348;
constexpr int noteG = 392;
constexpr int noteA = 440;
constexpr int noteB = 494;
constexpr int noteC_H = 523;
int notes[] = {noteC, noteD, noteE, noteF, noteG, noteA, noteB, noteC_H};
constexpr int noteC5 = 523;
constexpr int noteE5 = 659;
constexpr int noteG5 = 784;
constexpr int noteA5 = 880;
constexpr int noteB5 = 988;
constexpr int noteC6 = 1047;
constexpr int noteD6 = 1175;
constexpr int noteE6 = 1319;
constexpr int noteF6 = 1397;
constexpr int noteG6 = 1568;
int welcome_notes[] = {noteE5, noteG5, noteC6, noteE6, noteG6, noteA5, noteB5, noteD6};
// Menu parameters
int currentMode = 0;
constexpr int maxModes = 5;
String modes[] = {
"Set Timezone",
"Set Alarm 1",
"Set Alarm 2",
"Set Alarm 3",
"Disable Alarms"
};
void setup() {
Serial.begin(9600);
// Pin configurations
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_1_PIN, OUTPUT);
pinMode(LED_2_PIN, OUTPUT);
pinMode(CANCEL_BUTTON_PIN, INPUT);
pinMode(OK_BUTTON_PIN, INPUT);
pinMode(UP_BUTTON_PIN, INPUT);
pinMode(DOWN_BUTTON_PIN, INPUT);
// Initialize temperature & humidity sensor
dhtSensor.setup(DHT_PIN, DHTesp::DHT22);
// Initialize the OLED display
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println("SSD1306 allocation failed");
Serial.println("Exiting program...");
return;
}
display.display();
delay(MESSAGE_DELAY);
printLine("MEDI BOXX", 10, 20, 2, true);
delay(MESSAGE_DELAY);
// Initialize the WiFi
WiFi.begin(WIFI_SSID, WIFI_PWD, 6);
while (WiFi.status() != WL_CONNECTED) {
delay(WIFI_DELAY);
printLine("Connecting to WiFi", 0, 0, 2, true);
}
printLine("Connected!", 0, 0, 2, true);
buzzz(200, 2, welcome_notes);
digitalWrite(LED_2_PIN, HIGH);
digitalWrite(LED_1_PIN, HIGH);
delay(DELAY);
digitalWrite(LED_2_PIN, LOW);
digitalWrite(LED_1_PIN, LOW);
// Configure initial time using WiFi network
configTime(UTC_OFFSET, UTC_OFFSET_DST, NTP_SERVER);
}
void loop() {
// Check for menu button pressed
// todo: Improve with interrupt
if (digitalRead(OK_BUTTON_PIN) == LOW) {
delay(DELAY);
goToMenu();
}
updateTime();
checkAlarm();
checkTemperatureHumidity();
}
/**
Function to update time and print it on the OLED display (HH:MM:SS)
todo: Enhance into updateDisplay including humidity and temperature readings
*/
void updateTime() {
struct tm timeinfo;
getLocalTime(&timeinfo);
char timeHour[3];
char timeMinute[3];
char fullTime[9];
strftime(timeHour, 3, "%H", &timeinfo);
strftime(timeMinute, 3, "%M", &timeinfo);
strftime(fullTime, 9, "%T", &timeinfo);
printLine(String(fullTime), 15, 0, 2, true);
currentHours = atoi(timeHour);
currentMinutes = atoi(timeMinute);
}
/**
Function to check alarms and ring if any.
*/
void checkAlarm() {
if (!alarmEnabled) {
return;
}
for (int i = 0; i < numAlarms; i++) {
if (!alarms[i].triggered && alarms[i].hours == currentHours && alarms[i].minutes == currentMinutes) {
ringAlarm();
alarms[i].triggered = true;
}
}
}
/**
Function to ring alarm to notify the user
*/
void ringAlarm() {
printLine("Medicine Time!", 0, 0, 2, true);
digitalWrite(LED_1_PIN, HIGH);
bool canceled = false;
// Buzzer ring alarm until the user presses the cancel button
while (!canceled && digitalRead(CANCEL_BUTTON_PIN) == HIGH) {
for (int i = 0; i < numNotes; i++) {
if (digitalRead(CANCEL_BUTTON_PIN) == LOW) {
delay(DELAY);
canceled = true;
break;
}
tone(BUZZER_PIN, notes[i]);
delay(DELAY);
noTone(BUZZER_PIN);
delay(2);
}
}
digitalWrite(LED_1_PIN, LOW);
display.clearDisplay();
}
// Bezzer helper function
void buzzz(int t, int b, int tones[]) {
for (int i = 0; i < numNotes; i++) {
tone(BUZZER_PIN, tones[i]);
delay(t);
noTone(BUZZER_PIN);
delay(b);
}
}
/**
Function to check menu and run based on user selection
*/
void goToMenu() {
while (digitalRead(CANCEL_BUTTON_PIN) == HIGH) {
printLine(modes[currentMode], 0, 0, 2, true);
int pressedButton = waitForButtonPress();
if (pressedButton == DOWN_BUTTON_PIN) {
delay(BTN_DELAY);
currentMode = (currentMode + 1) % maxModes;
} else if (pressedButton == UP_BUTTON_PIN) {
delay(BTN_DELAY);
currentMode = (currentMode - 1 + maxModes) % maxModes;
} else if (pressedButton == OK_BUTTON_PIN) {
delay(BTN_DELAY);
runMode(currentMode);
} else if (pressedButton == CANCEL_BUTTON_PIN) {
delay(BTN_DELAY);
break;
}
}
}
/**
Function to run the selected menu
*/
void runMode(int mode) {
if (mode == 0) {
setTimezoneAndConfig();
} else if (mode == 1 || mode == 2 || mode == 3) {
setAlarm(mode - 1);
} else if (mode == 4) {
disableAlarms();
}
}
/**
Function to set timezone and config based on user input
*/
void setTimezoneAndConfig() {
int currentOffsetHours = utcOffsetSec / 3600;
int currentOffsetMinutes = (utcOffsetSec % 3600) / 60;
int offsetHours = readValue("Enter offset hours: ", currentOffsetHours, 14, -12);
int offsetMinutes = readValue("Enter offset minutes: ", currentOffsetMinutes, 59, 0);
utcOffsetSec = offsetHours * 3600 + offsetMinutes * 60;
configTime(utcOffsetSec, UTC_OFFSET_DST, NTP_SERVER);
printLine("Timezone set to " + String(offsetHours) + ":" + String(offsetMinutes), 0, 0, 2, true);
delay(MESSAGE_DELAY);
}
/**
Function to set alarm based on user input
alarm - alarm index
*/
void setAlarm(int alarmIndex) {
alarms[alarmIndex].hours = readValue("Enter hour: ", alarms[alarmIndex].hours, 23, 0);
alarms[alarmIndex].minutes = readValue("Enter minute: ", alarms[alarmIndex].minutes, 59, 0);
alarms[alarmIndex].triggered = false;
printLine("Alarm " + String(alarmIndex + 1) + " is set to ", 0, 0, 2, true);
printLine(String(alarms[alarmIndex].hours) + ":" + String(alarms[alarmIndex].minutes), 15, 35, 2, false);
delay(MESSAGE_DELAY);
if (!alarmEnabled) {
printLine("Alarms Disabled !", 0, 0, 2, true);
printLine("Enable?", 0, 35, 2, false);
if (waitForButtonPress() == OK_BUTTON_PIN) {
alarmEnabled = true;
printLine("Alarms are enabled", 0, 0, 2, true);
delay(MESSAGE_DELAY);
}
}
}
/**
Function to disable all the alarms and set to default values
*/
void disableAlarms() {
if (!alarmEnabled) {
printLine("Disabled already!", 0, 0, 2, true);
printLine("Enable?", 0, 35, 2, false);
if (waitForButtonPress() == OK_BUTTON_PIN) {
alarmEnabled = true;
printLine("Alarms are enabled", 0, 0, 2, true);
delay(MESSAGE_DELAY);
return;
}
}
alarmEnabled = false;
// Clear all the alarms
// for (int i = 0; i < numAlarms; i++) {
// alarms[i] = {0, 0, true};
// }
printLine("Alarms are disabled", 0, 0, 2, true);
delay(MESSAGE_DELAY);
}
/**
Function to check temperature and humidity and warn if exceeded recommended limit
*/
void checkTemperatureHumidity() {
TempAndHumidity data = dhtSensor.getTempAndHumidity();
bool warning = false;
if (data.temperature > 32) {
warning = true;
printLine("Temperature HIGH", 0, 40, 1, false);
} else if (data.temperature < 26) {
warning = true;
printLine("Temperature LOW", 0, 40, 1, false);
}
if (data.humidity > 80) {
warning = true;
printLine("Humidity HIGH", 0, 50, 1, false);
} else if (data.humidity < 60) {
warning = true;
printLine("Humidity LOW", 0, 50, 1, false);
}
if (warning) {
digitalWrite(LED_2_PIN, HIGH);
tone(BUZZER_PIN, 256);
delay(DELAY);
digitalWrite(LED_2_PIN, LOW);
noTone(BUZZER_PIN);
}
}
/**
Helper functions
*/
/**
Function to print a message on the OLED Display
text - text message
column - column position on the display
row - row position on the display
textSize - text size
clear - clear the display before printing the text
*/
void printLine(String text, int column, int row, int textSize, bool clear) {
if (clear) {
display.clearDisplay();
}
display.setTextSize(textSize);
display.setTextColor(SSD1306_WHITE);
display.setCursor(column, row);
display.println(text);
display.display();
}
/**
Function to read user input value using push buttons.
Returns int value based on user input
text - text message for the user
initValue - initial value
maxValue - maximum allowed value
minValue - minimum allowed value
*/
int readValue(String text, int initValue, int maxValue, int minValue) {
int tempValue = initValue;
while (true) {
printLine(text + String(tempValue), 0, 0, 2, true);
int pressedButton = waitForButtonPress();
if (pressedButton == DOWN_BUTTON_PIN) {
delay(BTN_DELAY);
tempValue = (tempValue + 1) > maxValue ? minValue : tempValue + 1;
} else if (pressedButton == UP_BUTTON_PIN) {
delay(BTN_DELAY);
tempValue = (tempValue - 1) < minValue ? maxValue : tempValue - 1;
} else if (pressedButton == OK_BUTTON_PIN) {
delay(BTN_DELAY);
return tempValue;
} else if (pressedButton == CANCEL_BUTTON_PIN) {
delay(BTN_DELAY);
break;
}
}
return initValue;
}
/**
Function to wait for the user to press a push button
Returns the pin number for each button when pressed by the user
*/
int waitForButtonPress() {
// todo: Reimplement this with interrupts and bounce times
while (true) {
if (digitalRead(UP_BUTTON_PIN) == LOW) {
delay(BTN_DELAY);
return UP_BUTTON_PIN;
} else if (digitalRead(DOWN_BUTTON_PIN) == LOW) {
delay(BTN_DELAY);
return DOWN_BUTTON_PIN;
} else if (digitalRead(OK_BUTTON_PIN) == LOW) {
delay(BTN_DELAY);
return OK_BUTTON_PIN;
} else if (digitalRead(CANCEL_BUTTON_PIN) == LOW) {
delay(BTN_DELAY);
return CANCEL_BUTTON_PIN;
}
}
}