// Author: M. S. S. Weerasinghe
// Title: Smart Medicine Box

// Include the required libraries
#include <Wire.h>  // For I2C communication
#include <Adafruit_GFX.h> // For the OLED display
#include <Adafruit_SSD1306.h> // For the OLED display
#include <DHTesp.h> // For the DHT sensor
#include <WiFi.h> // For the WiFi communication
#include <PubSubClient.h> // For the MQTT communication
#include <ESP32Servo.h> // For the servo motor

// Define constants for the OLED display 
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // shares reset pin of ESP32
#define SCREEN_ADDRESS 0x3C // I2C address

// Define constants for the hardware pins
#define DHT_PIN 12
#define LED 18
#define BUZZER 19
#define UP_BUTTON 26
#define DOWN_BUTTON 25
#define OK_BUTTON 27
#define CANCEL_BUTTON 33
#define SERVO_PIN 32
#define LDR_RIGHT 34
#define LDR_LEFT 35

// Define MQTT topics
#define TEMPERATURE_TOPIC "medibox-210690B-temperature"
#define HUMIDITY_TOPIC "medibox-210690B-humidity"
#define ALARM_ON_TOPIC "medibox-210690B-alarm-on"
#define ALARM_ON_GET_TOPIC "medibox-210690B-alarm-on-get"
#define ALARM_1_TIME_TOPIC "medibox-210690B-alarm-1-time"
#define ALARM_2_TIME_TOPIC "medibox-210690B-alarm-2-time"
#define ALARM_3_TIME_TOPIC "medibox-210690B-alarm-3-time"
#define ALARM_1_TIME_GET_TOPIC "medibox-210690B-alarm-1-time-get"
#define ALARM_2_TIME_GET_TOPIC "medibox-210690B-alarm-2-time-get"
#define ALARM_3_TIME_GET_TOPIC "medibox-210690B-alarm-3-time-get"
#define LIGHT_LEFT_TOPIC "medibox-210690B-light-left"
#define LIGHT_RIGHT_TOPIC "medibox-210690B-light-right"
#define MOTOR_ANGLE_TOPIC "medibox-210690B-motor-angle"

// Create an instances of the required libraries
DHTesp dht; // DHT sensor
Servo servo;  // Servo motor
WiFiClient espClient;  // WiFi client
PubSubClient mqttClient(espClient); // MQTT client
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // OLED display

// Define global variables
int utcOffset = 0; // UTC offset
int dstOffset = 0; // Daylight saving time offset
int milliTimenow = 0; // Milliseconds since the program started
int initmilliseconds = 0; // Time, the program started in milliseconds
int timenow[3] = {0, 0, 0}; // Current time in hours, minutes, and seconds
const int buzzerToneCount = 6;  // Number of buzzer tones
int buzzerTones[buzzerToneCount] = {262, 294, 330, 349, 392, 440}; // Buzzer tones
float humidity, newHumidity, temperature, newTemperature; // For the DHT sensor
int leftLight, rightLight, newLeftLight, newRightLight; // For the LDR sensors
// Strings to store the values for the MQTT communication
char temperatureStr[6], humidityStr[6], timeInMillisStr[10], lightLeftStr[6], lightRightStr[6];
// Variables to configure the alarms
bool alarmsEnabled = true;
const int alarmCount = 3;  // Number of alarms that can be set
int alarmTimes[alarmCount][2] = {{0, 1}, {1, 0}, {2, 0}}; // Alarm times in hours and minutes
int currentAlarmOption = 0; // Current selected alarm option
bool alarmRingingFinished[alarmCount] = {false, false, false}; // Alarm ringing status
// Variables to configure the menu
const int menuOptionCount = 3;  // Number of menu options
String menuOptions[menuOptionCount] = {"Set Time Zone", "Set Alarm", "Enable/Disable Alarms"}; // Menu options
int currentMenuOption = 0;  // Current selected menu option


void setup() {
  // Start the serial communication
  Serial.begin(115200);
  // Initialize the OLED display
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    // If the display cannot be initialized, print an error message
    Serial.println(F("SSD1306 allocation failed"));
    for (;;); // Loop indefinitely
  }

  // Initialize the hardware pins
  pinMode(LED, OUTPUT);
  pinMode(BUZZER, OUTPUT);
  pinMode(UP_BUTTON, INPUT);
  pinMode(DOWN_BUTTON, INPUT);
  pinMode(OK_BUTTON, INPUT);
  pinMode(CANCEL_BUTTON, INPUT);
  pinMode(LDR_LEFT, INPUT);
  pinMode(LDR_RIGHT, INPUT);
  pinMode(SERVO_PIN, OUTPUT);

  servo.attach(SERVO_PIN);  // Initialize the servo motor
  dht.setup(DHT_PIN, DHTesp::DHT22);  // Initialize the DHT sensor

  display.display();
  delay(2000);

  // Connect to the WiFi network
  WiFi.begin("Wokwi-GUEST", "", 6); // Change the SSID and password to your WiFi network
  // Loop and display waiting message until the WiFi network is connected
  while (WiFi.status() != WL_CONNECTED) {
    delay(250);
    display.clearDisplay();
    display.println("Connecting to WiFi...");
    display.display();
  }

  // Successfully connected to the WiFi network
  display.clearDisplay();
  display.println("Connected to the WiFi network");

  setupMqtt();  // Setup the MQTT communication
  updateTime();  // Update initial time (program start time) from the NTP server

  display.display();
  delay(2000);

  display.clearDisplay();

  // Display the welcome message
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("Welcome to Medibox");

  display.display();
  delay(2000);

}


// Main loop
void loop() {
  if (!mqttClient.connected()) connectToBroker();

  updateTimeAndTemp();
  updateLightIntensity();

  // Check if the alarm time has been reached and ring the alarm
  if (alarmsEnabled) {
    checkAlarmReached();
  }
  // Check if the user wants to go to the menu
  if (digitalRead(OK_BUTTON) == LOW) {
    goToMenu();
  }

  mqttClient.loop();
  delay(500);
}


// Function to setup the MQTT communication
void setupMqtt() {
  mqttClient.setServer("test.mosquitto.org", 1883); // Set the MQTT broker (host, port)
  mqttClient.setCallback(receiveCallback);  // Set the callback function for receiving messages
}


// Function to connect to the MQTT broker
void connectToBroker() {
  // Loop until connected to the MQTT broker
  while (!mqttClient.connected()) {
    Serial.print("Attempting MQTT connection....");

    if (mqttClient.connect("ESP-6783-876345267")) { // "ESP-6783-876345267" is the client ID
      // Successfully connected to the MQTT broker
      Serial.println("connected to MQTT broker");
      // Subscribe to the MQTT topics
      mqttClient.subscribe(ALARM_ON_GET_TOPIC);
      mqttClient.subscribe(ALARM_1_TIME_GET_TOPIC);
      mqttClient.subscribe(ALARM_2_TIME_GET_TOPIC);
      mqttClient.subscribe(ALARM_3_TIME_GET_TOPIC);
      mqttClient.subscribe(MOTOR_ANGLE_TOPIC);

      // Publish initial configuration to the MQTT broker
      // Alarms enabled/disabled
      if (alarmsEnabled)
        mqttClient.publish(ALARM_ON_TOPIC, "true");
      else
        mqttClient.publish(ALARM_ON_TOPIC, "false");

      // Alarm times - convert hours and minutes to a milliseconds string before publishing
      sprintf(timeInMillisStr, "%d", alarmTimeToMillis(alarmTimes[0][0], alarmTimes[0][1]));
      mqttClient.publish(ALARM_1_TIME_TOPIC, timeInMillisStr);
      sprintf(timeInMillisStr, "%d", alarmTimeToMillis(alarmTimes[1][0], alarmTimes[1][1]));
      mqttClient.publish(ALARM_2_TIME_TOPIC, timeInMillisStr);
      sprintf(timeInMillisStr, "%d", alarmTimeToMillis(alarmTimes[2][0], alarmTimes[2][1]));
      mqttClient.publish(ALARM_3_TIME_TOPIC, timeInMillisStr);
    }
    else {
      // Failed to connect to the MQTT broker
      Serial.print("failed, rc=");
      Serial.print(mqttClient.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}


// Function to convert hours and minutes to milliseconds
int alarmTimeToMillis(int hour, int minute) {
  return hour * 60 * 60 * 1000 + minute * 60 * 1000;
}


// Function to convert milliseconds to hours and minutes
void millisToAlarmTime(int millis, int alarmIndex) {
  // updates global alarm times array
  alarmTimes[alarmIndex][0] = (millis / 1000 / 60 / 60) % 24; // hours
  alarmTimes[alarmIndex][1] = (millis / 1000 / 60) % 60;  // minutes
}


// Function to receive and handle messages from the MQTT broker
void receiveCallback(char* topic, byte* payload, unsigned int length) {
  // Print the received message on the serial monitor
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++)
    Serial.print((char)payload[i]);
  Serial.println();

  // Check if the received message is to enable/disable the alarms
  if (strcmp(topic, ALARM_ON_GET_TOPIC) == 0) {
    // Update the alarm status based on the received message
    if (strcmp((char*)payload, "true") == 0)
      alarmsEnabled = true;
    else
      alarmsEnabled = false;
  }
  // Check if the received message is to update the alarm times
  else if (strcmp(topic, ALARM_1_TIME_GET_TOPIC) == 0)  // If the message is to update alarm 1 time
    millisToAlarmTime(atoi((char*)payload), 0);  // Update the alarm 1 time
  else if (strcmp(topic, ALARM_2_TIME_GET_TOPIC) == 0)  // If the message is to update alarm 2 time
    millisToAlarmTime(atoi((char*)payload), 1);  // Update the alarm 2 time
  else if (strcmp(topic, ALARM_3_TIME_GET_TOPIC) == 0)  // If the message is to update alarm 3 time
    millisToAlarmTime(atoi((char*)payload), 2);  // Update the alarm 3 time
  // Check if the received message is to update the motor angle
  else if (strcmp(topic, MOTOR_ANGLE_TOPIC) == 0)
    servo.write(atoi((char*)payload));  // Update the motor angle
}


// Function to print a given text on the OLED display
void printTextLine(String text, int row = 0, int column = 0, int textSize = 1) {
  display.setTextSize(textSize);
  display.setCursor(column, row); // Set the cursor position
  display.println(text);
  display.display();
}


// Function to display the menu
void showMenu() {
  display.clearDisplay();
  printTextLine("Menu", 0, 0, 2);
  for (int i = 0; i < menuOptionCount; i++) {
    if (i == currentMenuOption)
      printTextLine("-> " + menuOptions[i], 20 + i * 10, 0); // Highlight the current menu option
    else
      printTextLine(menuOptions[i], 20 + i * 10, 0);
  }
}


// Function to change the time using the NTP server
void updateTime() {
  // Connect to the WiFi network
  configTime(utcOffset * 3600, dstOffset * 3600, "pool.ntp.org"); // (UTC offset, daylight saving time offset, NTP host)
  struct tm timeinfo; // struct to store the time information
  // Get the current time from the NTP server
  if (!getLocalTime(&timeinfo)) {
    // If the time cannot be obtained, print an error message
    Serial.println("Failed to obtain time from NTP server");
    return;
  }
  // Get the current time in hours, minutes, and seconds from the timeinfo struct
  char timeSecond[3], timeMinute[3], timeHour[3];
  strftime(timeSecond, 3, "%S", &timeinfo);
  strftime(timeMinute, 3, "%M", &timeinfo);
  strftime(timeHour, 3, "%H", &timeinfo);
  // Calculate started time in milliseconds, corresponding to the current timezone
  initmilliseconds = atoi(timeSecond) * 1000 + atoi(timeMinute) * 60 * 1000 + atoi(timeHour) * 60 * 60 * 1000 - millis();
  display.clearDisplay();
}


// Function to display the current time zone
void displayTimeZone(int utcOffset) {
  display.clearDisplay();
  if (utcOffset >= 0)
    printTextLine("Current Time Zone: \nUTC+" + String(utcOffset), 0, 0);  // Add + sign for positive UTC offset
  else
    printTextLine("Current Time Zone: \nUTC" + String(utcOffset), 0, 0);
}


// Function to set the time zone
void setTimeZone() {
  // Display the current time zone at the start
  int tempUtcOffset = utcOffset;
  displayTimeZone(tempUtcOffset);
  delay(200);
  // Loop until the user sets the time zone
  while (true) {
    if (digitalRead(UP_BUTTON) == LOW) {
      // Increase the UTC offset
      tempUtcOffset++;
      if (tempUtcOffset > 14) // Maximum UTC offset is 14; set offset to -12
        tempUtcOffset = -12;
      displayTimeZone(tempUtcOffset);
    }
    else if (digitalRead(DOWN_BUTTON) == LOW) {
      // Decrease the UTC offset
      tempUtcOffset--;
      if (tempUtcOffset < -12)  // Minimum UTC offset is -12; set offset to 14
        tempUtcOffset = 14;
      displayTimeZone(tempUtcOffset);
    }
    else if (digitalRead(OK_BUTTON) == LOW) {
      // Set the selected UTC offset
      utcOffset = tempUtcOffset;
      display.clearDisplay();
      if (utcOffset >= 0)  // Add + sign for positive UTC offset
        printTextLine("Time Zone set to \nUTC+" + String(utcOffset), 0, 0);
      else
        printTextLine("Time Zone set to \nUTC" + String(utcOffset), 0, 0);
      delay(2000);
      updateTime();  // Update the time based on the new time zone
      break;
    }
    else if (digitalRead(CANCEL_BUTTON) == LOW) {
      break;  // Cancel the time zone setting
    }
    delay(200);
  }
  display.clearDisplay();
  showMenu();  // Display the main menu after setting/not setting the time zone
  delay(200);
}


// Function to display the alarm menu
void displayAlarmMenu() {
  display.clearDisplay();
  printTextLine("Select alarm to set", 0, 0);
  // Display the alarm options
  for (int i = 0; i < alarmCount; i++) {
    if (i == currentAlarmOption)
      printTextLine("-> Alarm " + String(i + 1) + ": " + String(alarmTimes[i][0]) + ":" + String(alarmTimes[i][1]), 10 + i * 10, 0);  // Highlight the current alarm option
    else
      printTextLine("Alarm " + String(i + 1) + ": " + String(alarmTimes[i][0]) + ":" + String(alarmTimes[i][1]), 10 + i * 10, 0);
  }
}


// Function to display selected alarm time in hours or minutes
void displayAlarmUnit(int timeIndex, int value) {
  display.clearDisplay();
  if (timeIndex == 0) {
    printTextLine("Set hour ", 0, 0);
    printTextLine("-> " + String(value), 10, 0);  // Display the selected hour
  } else if (timeIndex == 1) {
    printTextLine("Set minute ", 0, 0);
    printTextLine("-> " + String(value), 10, 0);  // Display the selected minute
  }
}


// Function to set the alarm time in hours or minutes
bool setAlarmTimeUnit(int alarmIndex, int timeIndex) {
  int value = alarmTimes[alarmIndex][timeIndex]; // Get the current alarm time (hour or minute) of the selected alarm
  display.clearDisplay();
  // Set the alarm time in hours
  if (timeIndex == 0) {
    displayAlarmUnit(timeIndex, value);  // Display the selected hour
    while (true) {
      if (digitalRead(UP_BUTTON) == LOW) {  // Increase the hour
        value = (value + 1) % 24;
        displayAlarmUnit(timeIndex, value);
      } else if (digitalRead(DOWN_BUTTON) == LOW) { // Decrease the hour
        value = (value - 1) % 24;
        if (value == -1) { value = 23; }
        displayAlarmUnit(timeIndex, value);
      } else if (digitalRead(OK_BUTTON) == LOW) { // Set the selected hour
        alarmTimes[alarmIndex][timeIndex] = value;
        return true;
      } else if (digitalRead(CANCEL_BUTTON) == LOW) { // Cancel the alarm time setting
        return false;
      }
      delay(200);
    }
  }
  // Set the alarm time in minutes
  else if (timeIndex == 1) {
    displayAlarmUnit(timeIndex, value);  // Display the selected minute
    while (true) {
      if (digitalRead(UP_BUTTON) == LOW) {  // Increase the minute
        value = (value + 1) % 60;
        displayAlarmUnit(timeIndex, value);
      } else if (digitalRead(DOWN_BUTTON) == LOW) { // Decrease the minute
        value = (value - 1) % 60;
        if (value == -1) { value = 59; }
        displayAlarmUnit(timeIndex, value);
      } else if (digitalRead(OK_BUTTON) == LOW) { // Set the selected minute
        alarmTimes[alarmIndex][timeIndex] = value;
        return true;
      } else if (digitalRead(CANCEL_BUTTON) == LOW) { // Cancel the alarm time setting
        return false;
      }
      delay(200);
    }
  }
  return false;
}


// Function to set the alarm time
void setAlarmTime(int alarmIndex) {
  display.clearDisplay();
  printTextLine("Set alarm " + String(alarmIndex + 1) + " time", 0, 0);  // Display the selected alarm
  int prev_hour = alarmTimes[alarmIndex][0];
  // Update alarm time only if setting both hours and minutes is successful
  // Set the alarm time in hours
  if (setAlarmTimeUnit(alarmIndex, 0)) {
    // Set the alarm time in minutes
    if (setAlarmTimeUnit(alarmIndex, 1)) {
      // Alarm time set successfully
      display.clearDisplay();
      printTextLine("Alarm " + String(alarmIndex + 1) + " time set to " + String(alarmTimes[alarmIndex][0]) + ":" + String(alarmTimes[alarmIndex][1]), 0, 0);
      // publish the new alarm time to the MQTT broker
      if (!mqttClient.connected())  // Connect to the MQTT broker if not connected
        connectToBroker();
      // Convert the alarm time to milliseconds string before publishing
      sprintf(timeInMillisStr, "%d", alarmTimeToMillis(alarmTimes[alarmIndex][0], alarmTimes[alarmIndex][1]));
      // Check the alarm index and publish the alarm time to the relevant topic
      if (alarmIndex == 0)
        mqttClient.publish(ALARM_1_TIME_TOPIC, timeInMillisStr);
      else if (alarmIndex == 1)
        mqttClient.publish(ALARM_2_TIME_TOPIC, timeInMillisStr);
      else if (alarmIndex == 2)
        mqttClient.publish(ALARM_3_TIME_TOPIC, timeInMillisStr);
      delay(2000);
    }
    // If setting the alarm time in minutes is not successful, set the alarm time in hours to the previous value
    else {
      alarmTimes[alarmIndex][0] = prev_hour;
    }
  }
  delay(1000);
}


// Function to select alarm option for updating the alarm time
void setAlarm() {
  while (true) {
    if (digitalRead(UP_BUTTON) == LOW) {
      // Decrement the alarm option
      currentAlarmOption = (currentAlarmOption - 1) % alarmCount;
      if (currentAlarmOption == -1) { currentAlarmOption = alarmCount - 1; }
      displayAlarmMenu(); // Display the alarm menu
    } else if (digitalRead(DOWN_BUTTON) == LOW) {
      // Increment the alarm option
      currentAlarmOption = (currentAlarmOption + 1) % alarmCount;
      displayAlarmMenu(); // Display the alarm menu
    } else if (digitalRead(OK_BUTTON) == LOW) {
      // Set the alarm time for the selected alarm option
      setAlarmTime(currentAlarmOption);
      displayAlarmMenu(); // Display the alarm menu after setting the alarm time
    } else if (digitalRead(CANCEL_BUTTON) == LOW) {
      // Return to the main screen
      display.clearDisplay();
      break;
    }
    delay(200);
  } 
}


// Function to navigate to main menu
void goToMenu() {
  showMenu();  // Display the main menu
  while (true) {
    // Check if the user wants to navigate the menu
    if (digitalRead(UP_BUTTON) == LOW) {
      currentMenuOption = (currentMenuOption - 1) % menuOptionCount;  // Decrement the menu option
      if (currentMenuOption == -1) { currentMenuOption = menuOptionCount - 1; }
      showMenu();  // Display updated menu
    }
    else if (digitalRead(DOWN_BUTTON) == LOW) {
      currentMenuOption = (currentMenuOption + 1) % menuOptionCount;  // Increment the menu option
      showMenu();  // Display updated menu
    }
    // Navigate to relevant menu option based on user input
    else if (digitalRead(OK_BUTTON) == LOW) {
      if (currentMenuOption == 0) {
        setTimeZone();  // Navigate to the set time zone menu
      } else if (currentMenuOption == 1) {
        setAlarm();  // Navigate to the set alarms menu
      } else if (currentMenuOption == 2) {
        // Enable/disable the alarms
        display.clearDisplay();
        if (!mqttClient.connected())  // Connect to the MQTT broker if not connected
          connectToBroker();
        // If the alarms are disabled, enable them
        if (!alarmsEnabled) {
          // Publish new alarm status to the MQTT broker
          mqttClient.publish(ALARM_ON_TOPIC, "true");
          printTextLine("Alarms enabled", 0, 0);
          alarmsEnabled = true;  // Enable the alarms
        }
        // If the alarms are enabled, disable them
        else {
          // Publish new alarm status to the MQTT broker
          mqttClient.publish(ALARM_ON_TOPIC, "false");
          printTextLine("Alarms disabled", 0, 0);
          alarmsEnabled = false; // Disable the alarms
        }
        delay(2000);
        showMenu();  // Display the main menu after enabling/disabling the alarms
      }
    }
    else if (digitalRead(CANCEL_BUTTON) == LOW) {
      // Return to the main screen
      display.clearDisplay();
      return;
    }
  }
}


// Function to check if the temperature has changed
bool temperatureChanged() {
  if (newTemperature != temperature) {
    temperature = newTemperature; // Update the temperature
    return true;
  }
  return false;
}

// Function to check if the humidity has changed
bool humidityChanged() {
  if (newHumidity != humidity) {
    humidity = newHumidity; // Update the humidity
    return true;
  }
  return false;
}

// Function to check if the intensityof left LDR sensor has changed
bool leftLightChanged() {
  if (newLeftLight != leftLight) {
    leftLight = newLeftLight; // Update the left light intensity
    return true;
  }
  return false;
}

// Function to check if the intensity of right LDR sensor has changed
bool rightLightChanged() {
  if (newRightLight != rightLight) {
    rightLight = newRightLight; // Update the right light intensity
    return true;
  }
  return false;
}

// Function to publish updated light intensity
void updateLightIntensity() {
  // Read the light intensity from the LDR sensors
  newLeftLight = analogRead(LDR_LEFT);
  newRightLight = analogRead(LDR_RIGHT);
  // Check if the light intensity has changed
  if (leftLightChanged()) {
    // Publish the light intensity to the MQTT broker
    String(leftLight).toCharArray(lightLeftStr, 6);
    mqttClient.publish(LIGHT_LEFT_TOPIC, lightLeftStr);
  }
  if (rightLightChanged()) {
    // Publish the light intensity to the MQTT broker
    String(rightLight).toCharArray(lightRightStr, 6);
    mqttClient.publish(LIGHT_RIGHT_TOPIC, lightRightStr);
  }
}

// Function to update temperature, humidity, and time on the OLED display
void updateTimeAndTemp() {
  // Read the temperature and humidity from the DHT sensor
  newHumidity = dht.getHumidity();
  newTemperature = dht.getTemperature();

  // Check if the humidity has changed
  if (humidityChanged()) {
    String(humidity).toCharArray(humidityStr, 6);
    // Publish the humidity to the MQTT broker
    mqttClient.publish(HUMIDITY_TOPIC, humidityStr);
  }
  // Check if the temperature has changed
  if (temperatureChanged()) {
    String(temperature).toCharArray(temperatureStr, 6);
    // Publish the temperature to the MQTT broker
    mqttClient.publish(TEMPERATURE_TOPIC, temperatureStr);
  }

  // Calculate the current time (= program start time + milliseconds since the program started)
  milliTimenow = initmilliseconds + millis();

  // Calculate the current time in hours, minutes, and seconds
  timenow[0] = (milliTimenow / 1000 / 60 / 60) % 24;    // Hours
  timenow[1] = (milliTimenow / 1000 / 60) % 60;   // Minutes
  timenow[2] = (milliTimenow / 1000) % 60;    // Seconds

  display.clearDisplay();
  // Display updated time, temperature, and humidity on the OLED display
  printTextLine("Time: " + String(timenow[0]) + ":" + String(timenow[1]) + ":" + String(timenow[2]), 0, 0);
  printTextLine("Temperature: " + String(temperature) + "C", 10, 0);
  printTextLine("Humidity: " + String(humidity) + "%", 20, 0);
  // Check if the temperature and humidity are within the acceptable range
  // If not, display a warning message and turn on the LED
  if ((temperature > 32 || temperature < 26) && (humidity > 80 || humidity < 60)) {
    printTextLine("Temperature and humidity out of range!", 30, 0);
    display.display();
    digitalWrite(LED, HIGH);
  } else if (temperature > 32 || temperature < 26) {
    printTextLine("Temperature out of range!", 30, 0);
    display.display();
    digitalWrite(LED, HIGH);
  } else if (humidity > 80 || humidity < 60) {
    printTextLine("Humidity out of range!", 30, 0);
    display.display();
    digitalWrite(LED, HIGH);
  } else {  // Temperature and humidity are within the acceptable range
    digitalWrite(LED, LOW);
    display.display();
    delay(500);
  }
}

// Function to ring the alarm
void ringAlarm(int alarmIndex) {
  digitalWrite(LED, HIGH);
  // Keep ringing the alarm until the user stops it
  while (!alarmRingingFinished[alarmIndex]) {
    for (int j = 0; j < buzzerToneCount; j++) {
      // Check if the user has stopped the alarm
      if (digitalRead(CANCEL_BUTTON) == LOW) {
        alarmRingingFinished[alarmIndex] = true; // Update ringing status to exit the loop
        digitalWrite(LED, LOW);
        break;
      }
      // Ring the buzzer with different tones
      tone(BUZZER, buzzerTones[j]); delay(400);
      noTone(BUZZER); delay(2);
    }
  }
}

// Function to check if the alarm time has been reached
void checkAlarmReached() {
  // For each alarm, check if the alarm time has reached (hours and minutes match the current time)
  for (int i = 0; i < alarmCount; i++) {
    if (alarmTimes[i][0] == timenow[0] && alarmTimes[i][1] == timenow[1]) {
      if (!alarmRingingFinished[i]) {
        display.clearDisplay();
        printTextLine("Medicine time!", 0, 0, 2);
        ringAlarm(i);
      }
    } else {
      // After Exit from the loop in ringAlarm function, reset the alarm ringing status
      alarmRingingFinished[i] = false;
    }
  }
}

$abcdeabcde151015202530354045505560fghijfghij