/*
   File        : Climate Control
   Author      : leveletech.com / wa 081214584114
   Date        :
   Description :
*/

//--------------------------------- Include Libraries-----------------------------------------------
#include "DHT.h"
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
//--------------------------------- Include Libraries-----------------------------------------------

//--------------------------------- Hardware Setting------------------------------------------------
String label1 = "Leveletech.com";
String label2 = "Climate Control";

//Blynk
#define BLYNK_TEMPLATE_ID "TMPL6g6iGv_X3"
#define BLYNK_TEMPLATE_NAME "ESP32"
#define BLYNK_AUTH_TOKEN "awKh4JUGdgCJamBO60sZCUu0tqdtW-qY"
#define BLYNK_PRINT Serial
#include <WiFi.h>
#include <BlynkSimpleEsp32.h>
char auth[] = BLYNK_AUTH_TOKEN;  //Auth Token
char ssid[] = "Wokwi-GUEST"; //nama hotspot yang digunakan
char pass[] = ""; //password hotspot yang digunakan


//--------------------------------- Hardware Setting------------------------------------------------

//--------------------------------- Define and Constant---------------------------------------------
const int S_num = 4;
const int S_pin[S_num] = {32, 33, 25, 26}; // Array of pins for switch
const int R_num = 4;
const int R_pin[R_num] = {19, 18, 5, 17}; // Array of pins for relays

const int DHT11_pin = 16;
const int MQ135_pin = 4;

//--------------------------------- Define and Constant---------------------------------------------

//--------------------------------- User Define Data Type-------------------------------------------
struct timer_data {
  bool IN;
  bool Q;
  bool TT;
  bool BP1; //button pressed
  bool BP2; //button pressed
  int PT;  //preset time
  int ET;  //elapsed time
  unsigned long TN;//time now
};
struct spwm_data {
  bool IN;
  bool Q;
  bool BP; //button pressed
  int PT_on;  //preset time for ON state
  int PT_off;  //preset time for OFF state
  int ET;  //elapsed time
  unsigned long TN;//time now
};
struct os_data {
  bool IN;
  bool Q;
  bool BP;
};
//--------------------------------- User Define Data Type-------------------------------------------

//----------------------------------Global Variable-------------------------------------------------
//Blynk
int Analog1, Analog2, Analog3;
bool digital1, digital2;
int mode = 0;


bool S[S_num] = {false, false, false, false};
bool S_LastState[S_num] = {false, false, false, false};
unsigned long S_LastDebounceTime[S_num];  // Array to store the last debounce time for each button
unsigned long debounceDelay = 10;      // Debounce delay in milliseconds
/*
  S[0] : Select
  S[1] : UP
  S[2] : Down
  S[3] : OK (confirm)
*/
bool R[R_num] = {false, false, false, false};
/*
  R[0] : Heater Command
  R[1] : Blower Command
  R[2] : Lampu Command
  R[3] : Pompa Air Command
*/
bool B[8] = {false, false, false, false, false, false, false, false};
/*
  B[0] : Heater Active
  B[1] : AM Heater 0 = auto, 1 = manual
  B[2] : M_CMD Heater, manual command
  B[3] : Blower Active
  B[4] : AM Blower 0 = auto, 1 = manual
  B[5] : H_CMD Blower, manual command
  B[6] : Lampu_CMD
  B[7] : Pompa Air_CMD
*/

//DHT11
float humidity, temperature;
DHT dht(DHT11_pin, DHT22);  //simulation using DHT22

//MQ135
float AmoniaPPM;

//lcd
LiquidCrystal_I2C lcd(0x27, 16, 2);
int active_page = 1;
int active_page_selected;
int page3_menu = 0;
int page4_menu = 0;

//instance
timer_data T[9];        //TON and TOF
/*
  T[0] : TON for change page 3 - 4
  T[1] : delay for current tempt < setpoint
  T[2] : delay for current tempt > setpoint

*/
spwm_data spwm_dat[4];  //for timer on off
/*
  pwm_data spwm_dat[0] : On Off for heater
  pwm_data spwm_dat[1] : On off for blower
*/
os_data os_dat[10];     //for one shoot rising and falling
/*
  os_dat[0] : OSF PB Select for changging page 1 - 2
  os_dat[1] : OSR for cahngging to page 3 - 4
  os_dat[2] : OSR heating hysterisis
  os_dat[3] : OSR cooling hysterisis
  os_dat[4] : OSR PB UP
  os_dat[5] : OSR PB DOWN
  os_dat[6] : OSR PB OK

*/

//process paramater
int H_SP, B_SP, H_T, B_T;
int H_SP_S, B_SP_S, H_T_S, B_T_S;
int H_SP_Max = 15;
int H_SP_Min = 5;
int B_SP_Max = 100;
int B_SP_Min = 25;
int H_T_Max = 15;
int H_T_Min = 1;
int B_T_Max = 15;
int B_T_Min = 1;
/*
  B  : Blower
  H  : Heater
  SP : Setpoint in C
  T  : timer on off for intermitend mode in Second
*/


//----------------------------------Global Variable-------------------------------------------------

//----------------------------------User Defined Function Stage 1-----------------------------------
//Blynk
BLYNK_WRITE(V0) {
  Analog1 = param.asInt();
}
BLYNK_WRITE(V1) {
  Analog2 = param.asInt();
}
BLYNK_WRITE(V2) {
  Analog3 = param.asInt();
  mode = Analog3;
}
BLYNK_WRITE(V3) {
  digital1 = param.asInt();
}
BLYNK_WRITE(V4) {
  digital2 = param.asInt();
}

void blynk_update() {
  switch (mode) {
    case 0:
      Blynk.virtualWrite(V0, temperature);
      Blynk.virtualWrite(V1, humidity);
      break;
    case 1:
      Blynk.virtualWrite(V0, AmoniaPPM);
      Blynk.virtualWrite(V1, AmoniaPPM);
      break;
  }
}

void TON(timer_data & Var) {       //Timer On delay function
  if (Var.IN)
  {
    if (!Var.BP1)
    {
      Var.TN = millis();
      Var.BP1 = true;
      Var.TT = true;
    }
    if (!Var.Q)
    {
      Var.ET = (millis() - Var.TN); //bagi 1000 untuk jadi detik
      if (Var.ET  >= Var.PT) {
        Var.Q = true;
        Var.TT = false;
      }
    }
  } else
  {
    Var.Q = false;
    Var.BP1 = false;
    Var.ET = 0;
    Var.TT = false;
  }
}
void spwm(spwm_data & Var) {       // Software PWM or Timer ON OFF
  if (Var.IN)
  {
    if (!Var.BP)
    {
      Var.TN = millis();
      Var.BP = true;
      Var.Q = true; // Initially turn ON the output
    }

    Var.ET = (millis() - Var.TN);  //bagi 1000 untuk jadi detik
    if (Var.Q && Var.ET >= Var.PT_on) {
      Var.Q = false; // If ON time is over, turn OFF the output
      Var.TN = millis(); // Reset the timer for the next state
    }
    else if (!Var.Q && Var.ET >= Var.PT_off) {
      Var.Q = true; // If OFF time is over, turn ON the output
      Var.TN = millis(); // Reset the timer for the next state
    }
  }
  else
  {
    Var.Q = false; // If input is inactive, turn OFF the output
    Var.BP = false; // Reset button pressed flag
    Var.ET = 0; // Reset elapsed time
  }
}
void OSR(os_data & Var) {
  if (Var.IN) {
    if (!Var.BP) {
      Var.Q = true;
      Var.BP = true;
    } else {
      Var.Q = false;
    }
  } else {
    Var.BP = false;
    Var.Q = false;
  }
}
void OSF(os_data & Var) {
  if (!Var.IN) {               // Check if the input is false
    if (Var.BP) {              // If the previous state was true (falling edge detected)
      Var.Q = true;            // Trigger the output
      Var.BP = false;          // Update the previous state to false
    } else {
      Var.Q = false;           // Do not trigger the output
    }
  } else {
    Var.BP = true;             // Update the previous state to true
    Var.Q = false;             // Do not trigger the output
  }
}

//Update Input
void S_udpate() {
  for (int i = 0; i < S_num; i++) {
    int currentState = digitalRead(S_pin[i]); // Read the current state of the button
    if (currentState != S_LastState[i]) {     // Check if the button state has changed and debounce it
      S_LastDebounceTime[i] = millis();       // Reset the debounce timer
    }
    if ((millis() - S_LastDebounceTime[i]) > debounceDelay) { // Check if enough time has passed to consider it a valid state change
      S[i] = !currentState;                   // If enough time has passed, update the state
    }
    S_LastState[i] = currentState;            // Store the current state for the next iteration
  }
}
void key_update() {
  os_dat[0].IN = (S[0]); OSF(os_dat[0]);      //OSF PB Select
  T[0].IN = S[0]; TON(T[0]);                  // timer ON pressing 3 second
  if (T[0].Q) os_dat[0].BP = false;           //prevent OSF trigger output
  os_dat[1].IN = T[0].Q; OSR(os_dat[1]);      //OSR trigger after TON
  os_dat[4].IN = (S[1]); OSR(os_dat[4]);      //OSR PB UP
  os_dat[5].IN = (S[2]); OSR(os_dat[5]);      //OSR PB DOWN
  os_dat[6].IN = (S[3]); OSR(os_dat[6]);      //OSR PB OK


  switch (active_page) {
    case 1:
      if (os_dat[0].Q) active_page = 2;
      if (os_dat[1].Q) active_page = 3;
      break;
    case 2:
      if (os_dat[0].Q) active_page = 1;
      if (os_dat[1].Q) active_page = 3;
      break;
    case 3:
      if (os_dat[1].Q) {
        page3_menu = 0;
        active_page = 4;
        H_SP_S = H_SP;   //restore setting if not saved
        B_SP_S = B_SP;
      }
      switch (page3_menu) {
        case 0:
          if (os_dat[0].Q) {
            page3_menu = 1;
            lcd.setCursor(0, 1);
            lcd.print("*");
            Serial.println("page3_menu " + String(page3_menu));
          }
          break;
        case 1:
          if (os_dat[0].Q) {
            page3_menu = 2;
            lcd.setCursor(0, 1);
            lcd.print(" ");
            lcd.setCursor(8, 1);
            lcd.print("*");
            Serial.println("page3_menu " + String(page3_menu));
          }
          if (os_dat[4].Q) {  //Up reference
            B_SP_S++;
            if (B_SP_S > B_SP_Max) B_SP_S =  B_SP_Max;
          }
          if (os_dat[5].Q) {  //Down reference
            B_SP_S--;
            if (B_SP_S < B_SP_Min) B_SP_S =  B_SP_Min;
          }
          if (os_dat[6].Q) {  //OK confirmation
            B_SP = B_SP_S;
            EEPROM.put(2, B_SP); //save to eeprom
            page3_menu = 0;      //back to menu0
            lcd.setCursor(0, 1);
            lcd.print(" ");
            lcd.setCursor(8, 1);
            lcd.print(" ");
          }
          break;
        case 2:
          if (os_dat[0].Q) {
            page3_menu = 0;
            lcd.setCursor(0, 1);
            lcd.print(" ");
            lcd.setCursor(8, 1);
            lcd.print(" ");

            Serial.println("page3_menu " + String(page3_menu));
          }
          if (os_dat[4].Q) {  //Up reference
            H_SP_S++;
            if (H_SP_S > H_SP_Max) H_SP_S =  H_SP_Max;
          }
          if (os_dat[5].Q) {  //Down reference
            H_SP_S--;
            if (H_SP_S < H_SP_Min) H_SP_S =  H_SP_Min;
          }
          if (os_dat[6].Q) {  //OK confirmation
            H_SP = H_SP_S;
            EEPROM.put(0, B_SP); //save to eeprom
            page3_menu = 0;      //back to menu0
            lcd.setCursor(0, 1);
            lcd.print(" ");
            lcd.setCursor(8, 1);
            lcd.print(" ");
          }
          break;
      }
      break;
    case 4:
      if (os_dat[1].Q) {
        page4_menu = 0;
        active_page = 1;
        H_T_S = H_T;   //restore setting if not saved
        B_T_S = B_T;
      }
      switch (page4_menu) {
        case 0:
          if (os_dat[0].Q) {
            page4_menu = 1;
            lcd.setCursor(1, 1);
            lcd.print("*");
            Serial.println("page4_menu " + String(page4_menu));
          }
          break;
        case 1:
          if (os_dat[0].Q) {
            page4_menu = 2;
            lcd.setCursor(1, 1);
            lcd.print(" ");
            lcd.setCursor(8, 1);
            lcd.print("*");
            Serial.println("page4_menu " + String(page4_menu));
          }
          if (os_dat[4].Q) {
            B_T_S++;
            if (B_T_S > B_T_Max) B_T_S =  B_T_Max;
          }
          if (os_dat[5].Q) {
            B_T_S--;
            if (B_T_S < B_T_Min) B_T_S =  B_T_Min;
          }
          if (os_dat[6].Q) {  //OK confirmation
            B_T = B_T_S;
            EEPROM.put(6, B_T); //save to eeprom
            page4_menu = 0;      //back to menu0
            lcd.setCursor(1, 1);
            lcd.print(" ");
            lcd.setCursor(8, 1);
            lcd.print(" ");
            spwm_dat[1].PT_on = (B_T * 1000); //update timer on off
            spwm_dat[1].PT_off = (B_T * 1000);
          }
          break;
        case 2:
          if (os_dat[0].Q) {
            page4_menu = 0;
            lcd.setCursor(1, 1);
            lcd.print(" ");
            lcd.setCursor(8, 1);
            lcd.print(" ");
            Serial.println("page4_menu " + String(page4_menu));
          }
          if (os_dat[4].Q) {
            H_T_S++;
            if (H_T_S > H_T_Max) H_T_S =  H_T_Max;
          }
          if (os_dat[5].Q) {
            H_T_S--;
            if (H_T_S < H_T_Min) H_T_S =  H_T_Min;
          }
          if (os_dat[6].Q) {  //OK confirmation
            H_T = H_T_S;
            EEPROM.put(4, H_T); //save to eeprom
            page4_menu = 0;      //back to menu0
            lcd.setCursor(1, 1);
            lcd.print(" ");
            lcd.setCursor(8, 1);
            lcd.print(" ");
            spwm_dat[0].PT_on = (H_T * 1000); //update timer on off
            spwm_dat[0].PT_off = (H_T * 1000);
          }
          break;
      }
      break;
  }
}
void DHT11_update() {
  humidity = dht.readHumidity();
  temperature = dht.readTemperature();
}
void MQ135_update() {
  int val = analogRead(MQ135_pin);  //get raw data
  //do calibration below
  float CF = 100.0; // example calibaration factor
  AmoniaPPM = val * (5.0 / 4095.0) * CF;
}
//Update Output
void R_udpate() {
  for (int i = 0; i < R_num; i++) {
    digitalWrite(R_pin[i], R[i]);
  }
}

//LCD
void startup_display() {
  int startPosLabel1 = (16 - label1.length()) / 2;  // Calculate the starting position for label1 to center it
  for (int i = 0; i < label1.length(); i++) {       // Display label1 on the first row, centered
    lcd.setCursor(startPosLabel1 + i, 0);           // Centered position on row 0
    lcd.print(label1[i]);                           // Print character
    delay(200);                                     // Wait 200ms for the next character
  }
  int startPosLabel2 = (16 - label2.length()) / 2;  // Calculate the starting position for label2 to center it
  for (int i = 0; i < label2.length(); i++) {       // Display label2 on the second row, centered
    lcd.setCursor(startPosLabel2 + i, 1);           // Centered position on row 1
    lcd.print(label2[i]);                           // Print character
    delay(200);                                     // Wait 200ms for the next character
  }
  delay(2000);
  lcd.clear();
}
String format1(float data, String unit, int lengt) {
  String formattedData = String(data, 0); // Convert float to String, no decimal places
  formattedData += " " + unit; // Add space and unit

  // Ensure the result is exactly lengt characters long by padding spaces if needed
  while (formattedData.length() < lengt) {
    formattedData = " " + formattedData;
  }

  return formattedData;
}
String format2(int data, String unit, int lengt) {
  String formattedData = String(data); // Convert float to String, no decimal places
  formattedData += " " + unit; // Add space and unit

  // Ensure the result is exactly lengt characters long by padding spaces if needed
  while (formattedData.length() < lengt) {
    formattedData = " " + formattedData;
  }

  return formattedData;
}
void lcd_update() {
  switch (active_page) {
    case 1:
      if (active_page_selected != active_page ) {  //excecuted once
        Serial.println("page 1");
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Temp. & Humidity");
        lcd.setCursor(0, 1);
        lcd.print("S=");
        lcd.setCursor(8, 1);
        lcd.print("H=");
        active_page_selected = active_page;
      }
      lcd.setCursor(2, 1); //excecuted forever
      lcd.print(format1(temperature, "C", 5));
      lcd.setCursor(10, 1);
      lcd.print(format1(humidity, "%", 5));
      break;
    case 2:

      if (active_page_selected != active_page ) {  //excecuted once
        Serial.println("page 2");
        lcd.clear();
        lcd.setCursor(1, 0);
        lcd.print("Gas Monitoring");
        lcd.setCursor(0, 1);
        lcd.print("Amonia=");
        active_page_selected = active_page;
      }
      lcd.setCursor(8, 1); //excecuted forever
      lcd.print(format1(AmoniaPPM, "ppm", 7));
      break;
    case 3:
      if (active_page_selected != active_page ) {  //excecuted once
        Serial.println("page 3");
        lcd.clear();
        lcd.setCursor(2, 0);
        lcd.print("Target Suhu");
        lcd.setCursor(1, 1);
        lcd.print("B=>");
        lcd.setCursor(9, 1);
        lcd.print("H=<");
        active_page_selected = active_page;
      }
      lcd.setCursor(4, 1); //excecuted forever
      lcd.print(format2(B_SP_S, "C", 4));
      lcd.setCursor(12, 1);
      lcd.print(format2(H_SP_S, "C", 4));
      break;
    case 4:
      if (active_page_selected != active_page ) {  //excecuted once
        Serial.println("page 4");
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Mode Intermitten");
        lcd.setCursor(2, 1);
        lcd.print("B=");
        lcd.setCursor(9, 1);
        lcd.print("H=");
        active_page_selected = active_page;
      }
      lcd.setCursor(4, 1); //excecuted forever
      lcd.print(format2(B_T_S, "m", 4));
      lcd.setCursor(11, 1);
      lcd.print(format2(H_T_S, "m", 4));
      break;
  }

}
void ReadEEPROM() {//Restore from EEPROM
  EEPROM.get(0, H_SP); H_SP = (H_SP <= 0) ? 10 : H_SP;
  EEPROM.get(2, B_SP); B_SP = (B_SP <= 0) ? 30 : B_SP;
  EEPROM.get(4, H_T); H_T = (H_T <= 0) ? 2 : H_T;
  EEPROM.get(6, B_T); B_T = (B_T <= 0) ? 2 : B_T;
  H_SP_S = H_SP;
  B_SP_S = B_SP;
  H_T_S = H_T;
  B_T_S = B_T;
}
//----------------------------------User Defined Function Stage 1-----------------------------------

//----------------------------------User Defined Function Stage 2-----------------------------------
void MainControl() {
  T[1].IN = (temperature < H_SP ); TON(T[1]); //heater active heating process
  os_dat[2].IN = T[1].Q; OSR(os_dat[2]);
  if (os_dat[2].Q) {
    Serial.println("Heating Active");
    B[0] = true;
    B[3] = false;
  }

  T[2].IN = (temperature > B_SP ); TON(T[2]); //blower active cooling process
  os_dat[3].IN = T[2].Q; OSR(os_dat[3]);
  if (os_dat[3].Q) {
    Serial.println("Cooling Active");
    B[0] = false;
    B[3] = true;
  }

  //heater Control
  //B[0] : Heater Active
  //B[1] : AM Heater 0 = auto, 1 = manual
  //B[2] : M_CMD Heater, manual command
  //R[0] : Heater Command

  spwm_dat[0].IN = (!B[1] & B[0]/*Auto and heater active*/);
  spwm(spwm_dat[0]);
  R[0] = (!B[1] & spwm_dat[0].Q) || (B[1] & B[2]) ;

  //blower Control
  //B[3] : Blower Active
  //B[4] : AM Blower 0 = auto, 1 = manual
  //B[5] : H_CMD Blower, manual command
  //R[1] : Blower Command
  spwm_dat[1].IN = (!B[4] & B[3]/*Auto and heater active*/);
  spwm(spwm_dat[1]);
  R[1] = (!B[4] & spwm_dat[1].Q) || (B[4] & B[25]) ;

  //Lampu Control Remote Only
  //R[2] : Lampu Command
  //B[6] : Lampu_CMD
  R[2] = B[6];

  //Pompa Air Control Remote Only
  //R[3] : Pompa Air Command
  //B[7] : Pompa Air_CMD
  R[3] = B[7];

}

//----------------------------------User Defined Function Stage 2-----------------------------------

//----------------------------------User Defined Function Stage 3-----------------------------------
//----------------------------------User Defined Function Stage 3-----------------------------------

//----------------------------------Testing Function------------------------------------------------
void testing() {
  if (0) {
    for (int i = 0; i < S_num; i++) {
      R[i] = S[i];
    }
  }

  if (0) {
    Serial.print("Temperature: ");
    Serial.print(temperature);
    Serial.print("ºC ");
    Serial.print("Humidity: ");
    Serial.println(humidity);
  }
  if (0) {
    Serial.print("Temperature: ");
    Serial.println(temperature);
    Serial.print("H_SP : ");
    Serial.println(H_SP);

    Serial.print("T[1].IN : ");
    Serial.println( T[1].IN);

    Serial.print("T[1].Q : ");
    Serial.println( T[1].Q);

  }
}
//----------------------------------Testing Function------------------------------------------------


//--------------------------------- Setup----------------------------------------------
void setup() {
  //Pin Initialization
  for (int i = 0; i < S_num; i++) {           // Initialize input pins
    pinMode(S_pin[i], INPUT_PULLUP);
  }
  for (int i = 0; i < R_num; i++) {           // Initialize Output pins
    pinMode(R_pin[i], OUTPUT);
  }

  //Get data from Epprom
  ReadEEPROM();
  //Timer Initialization (dalam ms)
  T[0].PT = 300;                     //  timer for pressing select
  T[1].PT = 300;                     //  delay for current tempt > setpoint
  T[2].PT = 300;                     //  delay for current tempt < setpoint

  //heater intermitent on off
  spwm_dat[0].PT_on = (H_T * 1000);
  spwm_dat[0].PT_off = (H_T * 1000);
  //blower intermitent on off
  spwm_dat[1].PT_on = (B_T * 1000);
  spwm_dat[1].PT_off = (B_T * 1000);

  //Start Services
  Serial.begin(115200);
  dht.begin();
  lcd.init();             //LCD
  lcd.backlight();        // turn on LCD backlight
  Blynk.begin(auth, ssid, pass);  //memulai Blynk
  //Startup
  //startup_display();


  //testing
  if (0) {
    Serial.println(spwm_dat[0].PT_on);
    Serial.println(spwm_dat[0].PT_off);
    Serial.println(spwm_dat[1].PT_on);
    Serial.println(spwm_dat[1].PT_off);
  }

}



//Testing

//--------------------------------- Setup----------------------------------------------


//--------------------------------- Loop-----------------------------------------------
void loop() {
  //Update Input
  Blynk.run();  //menjalankan blynk
  S_udpate();
  DHT11_update();
  MQ135_update();

  //Process
  MainControl();
  testing();

  lcd_update();
  key_update();

  //Update Output
  R_udpate();
  blynk_update();

}



//--------------------------------- Loop-----------------------------------------------


//--------------------------------- Note-----------------------------------------------
/*
  //EEPROM address Mapping
  0 : INT H_SP
  2 : INT B_SP
  4 : INT H_T
  6 : INT B_T

*/
//--------------------------------- Note-----------------------------------------------


NOCOMNCVCCGNDINLED1PWRRelay Module
Heater
NOCOMNCVCCGNDINLED1PWRRelay Module
Blower
NOCOMNCVCCGNDINLED1PWRRelay Module
Lampu
NOCOMNCVCCGNDINLED1PWRRelay Module
Pompa Air
MQ135