#include "constants.h"
#include "images.cpp"
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ArduinoJson.h>
#include <Preferences.h>
#include <DHT.h>
#include <PubSubClient.h>
// #include <Adafruit_Sensor.h>
#include <WiFi.h>
#include <ESP32Servo.h>
DHT dhtSensor(DHT_PIN, DHTTYPE);
WiFiClient espClient;
PubSubClient mqttClient(espClient);
JsonDocument data;
Preferences settings;
Servo servo_motor;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
String wifi_username = DEF_WIFI_SSID;
String wifi_password = DEF_WIFI_PWD;
int servo_angle = 0;
struct tm timeinfo; // contains time data. pre defined struct type.
int temp_offset_hours;
int temp_offset_minutes;
// Time parameters to keep hours & minutes to check alarms
int currentHours = 0;
int currentMinutes = 0;
// UTC Offset parameter in seconds
long utcOffsetSec = 0;
// menu parameters
int current_mode = 0;
const int max_modes = 6;
const String modes[] = {"Set Time Zone", "Set Alarm 1", "Set Alarm 2", "Set Alarm 3", "Disable Alarms", "Reset Settings"};
bool alarm_enabled;
const int n_alarms = 3;
int alarm_hours[n_alarms];
int alarm_minutes[n_alarms];
bool alarm_triggered[n_alarms];
// buzzer tone parameters
const int notes_num = 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};
int feedback_note[] = {noteD};
void println(String text, int column, int row, int text_size, bool display_now = false, int color = WHITE);
void println(tm timeinfo, const char *text, int column, int row, int text_size, bool display_now = false, int color = WHITE);
void setup() {
// serial setup
Serial.begin(115200);
Serial.println("hi! from serial monitor...");
// pin config
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_1_PIN, OUTPUT);
pinMode(CANCEL_BUTTON_PIN, INPUT);
pinMode(DOWN_BUTTON_PIN, INPUT);
pinMode(OK_BUTTON_PIN, INPUT);
pinMode(UP_BUTTON_PIN, INPUT);
pinMode(LDR_1_PIN, INPUT);
pinMode(LDR_2_PIN, INPUT);
// pinMode(SERVO_PIN, OUTPUT);
dhtSensor.begin();
servo_motor.attach(SERVO_PIN, 500, 2400);
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS))
{
Serial.println(F("ssd1306 allocation failure"));
Serial.println("exiting program...");
return;
}
Serial.println("display setup complete");
display.clearDisplay();
// This is not a modalpage. It is a splash screen. text is embedded to a bitmap.
display.drawBitmap(0, 0, splashScreen, 128, 64, WHITE);
display.display();
// loads data using preferences object. defaults assigned if not stored in preferences.
load_user_settings();
Serial.println("user settings loaded");
delay(MESSAGE_DELAY);
setup_wifi();
buzzz(200, 2, welcome_notes);
digitalWrite(LED_2_PIN, HIGH);
digitalWrite(LED_1_PIN, HIGH);
delay(3000);
digitalWrite(LED_2_PIN, LOW);
digitalWrite(LED_1_PIN, LOW);
Serial.println("configuring mqtt");
mqtt_setup();
Serial.println("configuring time");
configTime(temp_offset_hours * 3600 + temp_offset_minutes * 60, UTC_OFFSET_DST, NTP_SERVER);
display.clearDisplay();
}
void loop() {
// delay(1000);
// Serial.println("main loop running");
// wifi_check();
update_time_with_check_alarm();
mqtt_connect();
send_mqtt_data();
if (digitalRead(OK_BUTTON_PIN) == LOW)
{
delay(200);
buzzz(20, 0, feedback_note);
go_to_menu();
}
delay(10); // this speeds up the simulation apparantly
}
//
// alarms
//
void ring_alarm()
{
// Turning the LED ON
digitalWrite(LED_1_PIN, HIGH);
show_modal_page(medicine_time, 200, "It's Medicine Time!", 8);
// Ringing the buzzer
bool break_happened = false;
while (digitalRead(CANCEL_BUTTON_PIN) == HIGH && break_happened == false)
{
for (int i = 0; i < notes_num; i++)
{
if (digitalRead(CANCEL_BUTTON_PIN) == LOW)
{
delay(200); // to prevent bouncing of buzzer.
break_happened = true;
break;
}
tone(BUZZER_PIN, notes[i]);
delay(500);
noTone(BUZZER_PIN);
delay(2);
}
}
digitalWrite(LED_1_PIN, LOW);
display.clearDisplay();
}
void set_alarm(int alarm)
{
int temp_hour = alarm_hours[alarm];
int temp_minute = alarm_minutes[alarm];
while (true)
{
display.clearDisplay();
display.fillRoundRect(23, 12, 39, 34, 4, WHITE);
println(formatNumber(temp_hour), 26, 18, 3, false, BLACK);
println(formatNumber(temp_minute), 76, 18, 3, true, WHITE);
println(":", 60, 18, 3, true, WHITE);
int pressed = wait_for_button_press();
if (pressed == UP_BUTTON_PIN)
{
delay(50);
temp_hour++;
temp_hour = temp_hour % 24;
}
else if (pressed == DOWN_BUTTON_PIN)
{
delay(50);
temp_hour--;
temp_hour = temp_hour % 24;
if (temp_hour < 0)
{
temp_hour = 23;
}
}
else if (pressed == OK_BUTTON_PIN)
{
delay(50);
alarm_hours[alarm] = temp_hour;
break;
}
else if (pressed == CANCEL_BUTTON_PIN)
{
delay(50);
return;
}
}
while (true)
{
display.fillRoundRect(23, 12, 39, 34, 4, BLACK);
println(formatNumber(temp_hour), 26, 18, 3, false, WHITE);
// Above two lines removes the white background around the hour setting state.(i.e. inverts the hour part of the display back.)
display.fillRoundRect(73, 12, 39, 34, 4, WHITE);
println(formatNumber(temp_minute), 76, 18, 3, true, BLACK);
int pressed = wait_for_button_press();
if (pressed == UP_BUTTON_PIN)
{
delay(50);
temp_minute++;
temp_minute = temp_minute % 60;
}
else if (pressed == DOWN_BUTTON_PIN)
{
delay(50);
temp_minute--;
temp_minute = temp_minute % 60;
if (temp_minute < 0)
{
temp_minute = 59;
}
}
else if (pressed == OK_BUTTON_PIN)
{
delay(50);
alarm_minutes[alarm] = temp_minute;
if (!alarm_enabled)
{
alarm_enabled = true;
save_is_alarm_enabled();
for (int i = 0; i < n_alarms; i++)
{
alarm_triggered[i] = true;
}
}
alarm_triggered[alarm] = false;
show_modal_page(alarm_ring, 1000, "Alarm set to " + formatNumber(temp_hour) + ":" + formatNumber(temp_minute), 10);
save_alarm(alarm);
break;
}
else if (pressed == CANCEL_BUTTON_PIN)
{
delay(50);
break;
}
}
}
//
// dht
//
float check_temp()
{
float temperature = dhtSensor.readTemperature();
float humidity = dhtSensor.readHumidity();
bool temp_in_range = false;
// check temperature
if (temperature > 32.0)
{
println("TEMP HIGH " + String(temperature) + "C", 14, 46, 1);
}
else if (temperature < 26.0)
{
println("TEMP LOW " + String(temperature) + "C", 18, 46, 1);
}
else
{
temp_in_range = true;
}
// check humidity
if (humidity > 80.0)
{
println("HUMIDITY HIGH " + String(humidity) + "%", 4, 56, 1);
}
else if (humidity < 60.0)
{
println("HUMIDITY LOW " + String(humidity) + "%", 8, 56, 1);
}
display.display();
return temperature;
}
//
// helpers
//
String formatNumber(int num)
{ // formats a given number to have two digits if its between 0 and 9.
if (num >= 0 && num <= 9)
{
return "0" + String(num); // Prepend 0 if num is a single digit
}
else
{
return String(num); // No need to modify if num is already two or more digits
}
}
void println(String text, int column, int row, int text_size, bool display_now, int color)
{
display.setTextSize(text_size);
display.setTextColor(color);
display.setCursor(column, row);
display.println(text);
if (display_now)
{
display.display();
}
}
void println(tm timeinfo, const char *text, int column, int row, int text_size, bool display_now, int color)
{
display.setTextSize(text_size);
display.setTextColor(color);
display.setCursor(column, row);
display.println(&timeinfo, text);
if (display_now)
{
display.display();
}
}
void run_mode(int mode)
{
if (mode == 1 || mode == 2 || mode == 3)
{
set_alarm(mode - 1); // Notice that the alarm number is equal to the mode number -1.
}
else if (mode == 4)
{
alarm_enabled = false;
save_is_alarm_enabled();
show_modal_page(alarm_disable, 1000, "Alarms Disabled!", 20);
}
else if (mode == 0)
{
set_time_zone();
}
else if (mode == 5)
{
reset_preferences();
}
}
void display_menu(int active_mode)
{
int row = 0;
const int padding_top = 4;
int page_number = 1 + active_mode / 5;
display.clearDisplay();
for (int i = 5 * (page_number - 1); i < min(5 * page_number, max_modes); i++)
{ // Only 5 menu items displayed at a time in the display.
if (i == active_mode)
{
display.fillRoundRect(0, row, 128, 14, 2, WHITE);
println(modes[i], 0, row + padding_top, 1, false, BLACK);
}
else
{
println(modes[i], 0, row + padding_top, 1, false, WHITE);
}
row += 12;
}
display.display();
}
void go_to_menu()
{
while (digitalRead(CANCEL_BUTTON_PIN) == HIGH)
{
display_menu(current_mode);
int pressed = wait_for_button_press();
if (pressed == DOWN_BUTTON_PIN)
{
delay(50);
current_mode++;
current_mode = current_mode % max_modes;
}
else if (pressed == UP_BUTTON_PIN)
{
delay(50);
current_mode--;
current_mode = current_mode % max_modes;
if (current_mode < 0)
{
current_mode = max_modes - 1;
}
}
else if (pressed == OK_BUTTON_PIN)
{
delay(50);
run_mode(current_mode);
break;
}
else if (pressed == CANCEL_BUTTON_PIN)
{
delay(50);
break;
}
}
}
int wait_for_button_press()
{
while (true)
{
if (digitalRead(UP_BUTTON_PIN) == LOW)
{
delay(100);
buzzz(20, 0, feedback_note);
return UP_BUTTON_PIN;
}
else if (digitalRead(DOWN_BUTTON_PIN) == LOW)
{
delay(100);
buzzz(20, 0, feedback_note);
return DOWN_BUTTON_PIN;
}
else if (digitalRead(OK_BUTTON_PIN) == LOW)
{
delay(100);
buzzz(20, 0, feedback_note);
return OK_BUTTON_PIN;
}
else if (digitalRead(CANCEL_BUTTON_PIN) == LOW)
{
delay(100);
buzzz(20, 0, feedback_note);
return CANCEL_BUTTON_PIN;
}
}
}
void update_time_with_check_alarm()
{
update_time();
print_time_now();
if (alarm_enabled == true)
{
for (int i = 0; i < n_alarms; i++)
{
// Serial.println("Alarm No: " + String(i) + "Already Triggered : " + String(alarm_triggered[i])); // for debugging.
// Serial.println("H : " + String(timeinfo.tm_hour) + " Alarm hours : " + String(alarm_hours[i])); // for debugging.
// Serial.println("M : " + String(timeinfo.tm_min) + " Alarm hours : " + String(alarm_minutes[i])); // for debugging.
if (alarm_triggered[i] == false && alarm_hours[i] == timeinfo.tm_hour && alarm_minutes[i] == timeinfo.tm_min)
{
ring_alarm();
alarm_triggered[i] = true;
}
}
}
}
void show_modal_page(const unsigned char *bitmap, int period, String text, int x_offset)
{ // This function is responsible for showing a provided bitmap above a given text, in full screen.
display.clearDisplay();
display.drawBitmap(0, 0, bitmap, 128, 64, WHITE);
println(text, x_offset, 50, 1, true);
if (period > 0)
{
delay(period);
}
}
// buzzer helper function
void buzzz(int t, int b, int tones[])
{
for (int i = 0; i < notes_num; i++)
{
tone(BUZZER_PIN, tones[i]);
delay(t);
noTone(BUZZER_PIN);
delay(b);
}
}
void setup_wifi()
{
WiFi.begin("Wokwi-GUEST", "", 6);
Serial.println("connecting to wifi");
unsigned long currentMillis = millis();
unsigned long previousMillis = currentMillis;
while (WiFi.status() != WL_CONNECTED)
{
delay(WIFI_DELAY);
Serial.println("...");
show_modal_page(wifi, 0, "Waiting For Wifi", 18);
currentMillis = millis();
if (currentMillis - previousMillis >= WIFI_TIMEOUT) // IF the limit gets exceeded, then the ESP32 will restart after getting wifi reconfigured.
{
// Serial.println("Failed to connect.");
show_modal_page(wifi_fail, 1000, "Wifi Failure!", 26);
// config_wifi(); // This function is defined in webServer.cpp
ESP.restart();
}
}
show_modal_page(tick, 100, "Wifi Connected!", 20);
}
void wifi_check()
{
if (WiFi.status() != WL_CONNECTED) {
setup_wifi();
}
else {
// Serial.println("wifi connected?");
}
}
float calc_luminance(int ldr_pin)
{
int analog_val = analogRead(ldr_pin);
//calculate light intensity in 0-1 range
float voltage = analog_val / 1024. * 5;
float resistance = 2000 * voltage / (1 - voltage / 5);
float maxlux = pow(RL10 * 1e3 * pow(10, GAMMA) / 322.58, (1 / GAMMA));
float lux = pow(RL10 * 1e3 * pow(10, GAMMA) / resistance, (1 / GAMMA)) / maxlux;
return (lux);
}
void turn_servo_motor(int angle)
{
if (servo_angle != angle)
{
// the calculation of the angle is handled in node-red with the help of javascript
servo_motor.write(angle); // servo motor can only turn between 0 and 180 degrees.
delay(20);
Serial.println("turned to an angle of: " + String(angle) + " degrees.");
servo_angle = angle;
}
else
{
Serial.println("already at the desired angle.");
}
}
//
// mqtt
//
void mqtt_connect()
{
if (!mqttClient.connected()) {
while (!mqttClient.connected())
{
Serial.println("attempting mqtt connection...");
// Serial.println(MQTT_DEVICE_ID);
if (mqttClient.connect(MQTT_DEVICE_ID))
{
Serial.println("connected");
// Serial.print("for receiving subscribing to ");
// Serial.println(MEDIBAY_REC_TOPIC);
mqttClient.subscribe(MEDIBAY_REC_TOPIC);
}
else
{
Serial.print("failed with ");
Serial.println(mqttClient.state());
// wifi_check();
delay(5000);
}
}
}
mqttClient.loop();
}
void mqtt_callback(char *topic, byte *message, unsigned int length)
{
Serial.print("message arrived [");
Serial.print(topic);
Serial.println("] ");
String degree;
for (int i = 0; i < length; i++)
{
degree += (char)message[i]; // Adjust this code if you are sending something more than just the degree of rotation of the servo motor.
}
Serial.print("motor degree: ");
Serial.println(degree);
if (strcmp(topic, MEDIBAY_REC_TOPIC) == 0)
{
turn_servo_motor(degree.toInt());
}
else
{
Serial.println("invalid command");
}
}
void send_mqtt_data()
{
char dataJson[100];
float temperature = check_temp();
float ldr1 = calc_luminance(LDR_1_PIN);
float ldr2 = calc_luminance(LDR_2_PIN);
// using arduino json library to culminate all readings and send via mqtt
if (fabs(data["LDR1"].as<float>() - ldr1) >= EPSILON || fabs(data["LDR2"].as<float>() - ldr2) >= EPSILON || fabs(data["Temperature"].as<float>() - temperature) >= TEMPSILON)
{
data["LDR1"] = ldr1;
data["LDR2"] = ldr2;
data["Temperature"] = temperature;
serializeJson(data, dataJson);
Serial.print("sending: ");
Serial.println(dataJson);
if (!mqttClient.connected())
{
mqtt_connect();
}
// publish readings to the mqtt topic.
// mqttClient.publish("esp32test210657g/temp",data["LDR1"]);
mqttClient.publish(MEDIBAY_PUB_TOPIC, dataJson);
}
else if (NOT_FILTERING) {
// Serial.println("not much change");
data["LDR1"] = ldr1;
data["LDR2"] = ldr2;
data["Temperature"] = temperature;
serializeJson(data, dataJson);
Serial.print("sending: ");
Serial.println(dataJson);
if (!mqttClient.connected())
{
mqtt_connect();
}
// publish readings to the mqtt topic.
mqttClient.publish(MEDIBAY_PUB_TOPIC, dataJson);
}
}
void mqtt_setup()
{
mqttClient.setServer(MQTT_SERVER, MQTT_PORT);
mqttClient.setCallback(mqtt_callback);
}
//
// settings
//
void load_user_settings()
{
settings.begin("settings", true);
for (int i = 0; i < n_alarms; i++)
{
alarm_hours[i] = settings.getInt("alarm_hours_" + i, 0); // Default(when no data in EEPROM) alarm time is 00:00. But will not trigger until user sets, since alarm_triggered is false.
alarm_minutes[i] = settings.getInt("alarm_minutes_" + i, 00);
alarm_triggered[i] = settings.getBool("alarm_triggered_" + i, false);
}
alarm_enabled = settings.getBool("alarm_enabled", false);
temp_offset_hours = settings.getInt("utc_offset_h", DEFAULT_UTC_OFFSET_H);
temp_offset_minutes = settings.getInt("utc_offset_m", DEFAULT_UTC_OFFSET_M);
wifi_username = settings.getString("wifi_username", "");
wifi_password = settings.getString("wifi_password", "");
settings.end();
}
void save_alarm(int alarm)
{
settings.begin("settings", false);
settings.putInt("alarm_hours_" + alarm, alarm_hours[alarm]);
settings.putInt("alarm_minutes_" + alarm, alarm_minutes[alarm]);
settings.putBool("alarm_triggered_" + alarm, alarm_triggered[alarm]);
settings.end();
}
void save_time_zone()
{
settings.begin("settings", false);
settings.putInt("utc_offset_h", temp_offset_hours);
settings.putInt("utc_offset_m", temp_offset_minutes);
settings.end();
}
void save_is_alarm_enabled()
{
settings.begin("settings", false);
settings.putBool("alarm_enabled", alarm_enabled);
settings.end();
}
void save_wifi_credentials(String username, String password)
{
settings.begin("settings", false);
settings.putString("wifi_username", username);
settings.putString("wifi_password", password);
settings.end();
}
void reset_preferences()
{
settings.begin("settings", false);
settings.clear();
settings.end();
show_modal_page(reset, 300, "Resetting Medibox!", 16);
ESP.restart();
}
//
// time
//
void set_time_zone()
{
while (true)
{
display.clearDisplay();
display.fillRoundRect(30, 12, 39, 34, 4, WHITE);
if (temp_offset_hours < 0) // Sign is handled separately since it should be formatted inversed in display.
{
println("-", 9, 18, 3, false, WHITE);
}
else if (temp_offset_hours > 0)
{
println("+", 9, 18, 3, false, WHITE);
}
println(formatNumber(abs(temp_offset_hours)), 33, 18, 3, false, BLACK); // absoluting the hours to display because sign is handled above separately.
println(formatNumber(abs(temp_offset_minutes)), 83, 18, 3, true, WHITE); // No sense of printing sign here again for minutes.
println(":", 67, 18, 3, true, WHITE);
int pressed = wait_for_button_press();
if (pressed == UP_BUTTON_PIN)
{
delay(50);
temp_offset_hours++;
if (temp_offset_hours > 14)
{ // 14 hours multiplies by 60.
temp_offset_hours = -12; // 12 hours multiplies by 60.
}
}
else if (pressed == DOWN_BUTTON_PIN)
{
delay(50);
temp_offset_hours--;
if (temp_offset_hours < -12)
{
temp_offset_hours = 14;
}
}
else if (pressed == OK_BUTTON_PIN)
{
delay(50); // since the offset is finally a single variable counted in seconds, setting it here globally is unnecessary. It will be set after taking the minutes as well.
break;
}
else if (pressed == CANCEL_BUTTON_PIN)
{
delay(50);
return;
}
}
bool is_edge_case = (temp_offset_hours == 14 || temp_offset_hours == -12);
while (true)
{
display.fillRoundRect(30, 12, 39, 34, 4, BLACK);
println(formatNumber(abs(temp_offset_hours)), 33, 18, 3, false, WHITE); // absoluting the hours to display because sign is handled above separately.
// Above two lines removes the white background around the hour setting state.(i.e. inverts the hour part of the display back.)
display.fillRoundRect(80, 12, 39, 34, 4, WHITE);
println(formatNumber(abs(is_edge_case ? temp_offset_minutes = 0 : temp_offset_minutes)), 83, 18, 3, true, BLACK); // This will always make the user see "00" as minutes if the hours are set to 14 or -12. Actual change of variable will be done later in thecode. And the abs is used because no need to print the sign of the minutes.
int pressed = wait_for_button_press();
if (pressed == OK_BUTTON_PIN || is_edge_case && pressed != CANCEL_BUTTON_PIN) // when press ok or edge case is true and not press cancel, have to update the time accordingly.
{
// Serial.println("Setting time zone..." + String(temp_offset_hours) + ":" + String(temp_offset_minutes)); //Uncomment this line for debugging.
delay(50);
configTime(temp_offset_hours * 3600 + temp_offset_minutes * 60, UTC_OFFSET_DST, NTP_SERVER);
update_time();
show_modal_page(time_zone, 1000, "Time Zone Set!", 23);
save_time_zone();
break;
}
else if (pressed == UP_BUTTON_PIN)
{
delay(50);
temp_offset_minutes += temp_offset_minutes / abs(temp_offset_minutes); // this is because when the minute offset is negative, since the shown value is a positive value, the up button is supposed to increment the absolute value. i.e. decrement the actual value.
temp_offset_minutes = temp_offset_minutes % 60;
}
else if (pressed == DOWN_BUTTON_PIN)
{
delay(50);
temp_offset_minutes -= temp_offset_minutes / abs(temp_offset_minutes);
temp_offset_minutes = temp_offset_minutes % 60;
if (temp_offset_minutes < 0)
{
temp_offset_minutes = 59;
}
}
else if (pressed == CANCEL_BUTTON_PIN)
{
delay(50);
break;
}
}
}
void print_time_now()
{
display.clearDisplay();
println(timeinfo, "%H:%M:%S", 18, 0, 2);
println(timeinfo, "%d %B %Y", 25, 22, 1);
}
void update_time()
{
if (!getLocalTime(&timeinfo))
{
display.clearDisplay();
println("Failed to fetch time from server!", 0, 0, 2, true);
}
}