//============================================================ Deklarasi Library ===========================================================
#include <WiFi.h> //Pemanggilan Library WiFi
#include <PubSubClient.h> //Pemanggilan Library PubSubClient
#include <LiquidCrystal_I2C.h> //Pemanggilan Library LiquidCrystal_I2C


//======================================================= Inisialisasi: Constructor ========================================================
LiquidCrystal_I2C lcd(0x27, 16, 2); //Constructor LiquidCrystal_I2C
WiFiClient espClient; //Constructor WiFiClient
PubSubClient client(espClient); //Constructor PubSubClient


//===================================================== Deklarasi Variabel: Tipe Data ======================================================
//Tipe data Char
char payload_Publish[4];

//Tipe data Float
float payload_Subscribe;
float old_pHValue = 0, pHValue;

//Tipe data Boolean
bool relayON = LOW;
bool relayOFF = HIGH;
bool isPumpOn = false;
unsigned long currentMillis;
unsigned long startTime1 = 0;
unsigned long startTime2 = 0;
const unsigned long delayTime1 = 1000;
const unsigned long delayTime2 = 5000;
unsigned long pumpStartTime1 = 0;
unsigned long pumpStartTime2 = 0;
const unsigned long pumpDuration1 = 10000;
const unsigned long pumpDuration2 = 25000;
bool isMillisFinished = false;


//============================================================= Define Variabel ============================================================
#define ssid "Wokwi-GUEST" //Nama wifi router
#define password "" //Password wifi router
#define mqtt_server "broker.emqx.io" //Nama Platform IoT (Broker)
#define mqtt_port 1883 //Port Io-t.net
#define mqtt_username "" //Username Io-t.net
#define mqtt_password "" //Password Io-t.net
#define mqtt_clientID "client-001" //Client ID Io-t.net
#define INrespYes "INrespYes" //Callback Inline Respon Iya ke-1 
#define INrespNo "INrespNo" //Callback Inline Respon Tidak ke-1 
#define INrespYes1 "INrespYes1" //Callback Inline Respon Iya ke-2 
#define INrespYes2 "INrespYes2" //Callback Inline Respon Iya ke-3 
#define INrespNo1 "INrespNo1" //Callback Inline Respon Tidak ke-2
#define INrespYes3 "INrespYes3" //Callback Inline Respon Iya ke-4 
#define INrespYes4 "INrespYes4" //Callback Inline Respon Iya ke-5
#define INrespNo2 "INrespNo2" //Callback Inline Respon Tidak ke-3
#define INrespYes5 "INrespYes5" //Callback Inline Respon Iya ke-6
#define INrespYes6 "INrespYes6" //Callback Inline Respon Iya ke-7 
#define INrespNo3 "INrespNo3" //Callback Inline Respon Tidak ke-4
#define Topic "detect" //Topic MQTT : detect pH


//============================================================== Method Setup ===============================================================
void setup(){
  LCDinit(); //Memanggil method LCDinit
  Serial.begin(9600); //Memulai komunikasi serial dengan baud rate 9600
  connectWiFi(); //Memanggil method connectWiFi
  connectIoT(); //Memanggil method connectIoT (i-ot.net)
  Loading(); //LCD view Loading

  //Atur waktu agar fungsi millis langsung menyala
  startTime1 = millis() - delayTime1;
  startTime2 = millis() - delayTime2;
  pumpStartTime1 = millis() - pumpDuration1;
  pumpStartTime2 = millis() - pumpDuration2;
}


//============================================================== Method Loop ===============================================================
void loop(){
  //Ambil waktu saat ini
  currentMillis = millis();
  
  //Jika proses sudah selesai, hentikan pemeriksaan millis
  if (isMillisFinished) {
    return; //Keluar dari loop() tanpa melakukan apa-apa
  }
  
  //Pertahankan koneksi IoT
  if (!client.connected()) { reconnect(); }
  client.loop();

  //Jika waktu sekarang dikurangi waktu terakhir lebih besar dari 1 detik maka :
  if ((currentMillis - startTime1) > delayTime1) {
    readPublishPH(); //Memanggil method readPublishPH
    startTime1 = currentMillis; //Perbarui waktu terakhir dijalankan
  } 

  // Viewnow();
  LCDAllpHON();
  // LCDAllpHOFF();
  // LCDpHUpON();
  // LCDpHUpOFF();
  // LCDpHDownON();
  // LCDpHDownOFF();
}


//============================================================= Method LCD Init ============================================================
void LCDinit(){
  //Memulai komunikasi serial dengan LCD
  lcd.init();
  //Start LCD
  lcd.clear(); lcd.backlight(); lcd.setCursor(1,0); lcd.print("Memulai"); lcd.setCursor(1,1); lcd.print("Sistem pH..."); delay(1000);
  //Welcome LCD
  lcd.clear(); lcd.backlight(); lcd.setCursor(1,0); lcd.print("Welcome to"); lcd.setCursor(1,1); lcd.print("PHIOTNET...."); delay(1000);
}


//========================================================== Method Konfigurasi WiFi =======================================================
void connectWiFi(){
  Serial.println("\n==================================================================================");
  Serial.print("[Konfigurasi Wi-Fi]\nmencoba menghubungkan ke Wi-Fi : "); 
  Serial.println(ssid); 
  WiFi.begin(ssid, password);
  
  while(WiFi.status() != WL_CONNECTED){
    delay(500);
    Serial.print(".");
  }
  
  Serial.println();
  Serial.println("\nstatus :\nWi-Fi berhasil tersambung");
  Serial.println(WiFi.localIP());
  
  WiFi.setAutoReconnect(true); //Menyambungkan kembali secara otomatis setelah sambungan terputus
  WiFi.persistent(true); //Menyambungkan kembali ke Access Point
  delay(1000);
}


//============================================================= Method Koneksi IoT =========================================================
void connectIoT(){
  Serial.print("\n[Konfigurasi IoT]\nmencoba menghubungkan ke Platform : "); 
  Serial.println(mqtt_server);
  client.setServer(mqtt_server, mqtt_port); 
  client.setCallback(receivedCallback);
}


//==================================================== Method Menghubungkan Ulang Jaringan =================================================
void reconnect(){
  while(!client.connected()){        
    if(client.connect(mqtt_clientID, mqtt_username, mqtt_password)){
      Serial.println("\nstatus :"); Serial.print(mqtt_server);
      Serial.println(" berhasil tersambung");  
      client.subscribe(Topic);
    } 
    else{
      Serial.println("\nstatus :"); Serial.print(mqtt_server);
      Serial.print(" gagal tersambung (" + String(client.state()) + ")\nmenyambungkan kembali");
      LCDfailIoT(); //LCD view Fail
      while(!client.connect(mqtt_clientID, mqtt_username, mqtt_password)){
        delay(500);
        Serial.print(".");
      }
      delay(1000);
    }
  } 
}


//===================================================== Method Pemanggilan Topik Subscribe =================================================
void receivedCallback(char* topic, byte* payload, unsigned int length) {  
  char message[length + 1]; //Membuat variable array untuk menampung data payload
  for (int i = 0; i < length; i++) { message[i] = (char)payload[i]; } //Menampung data payload
  message[length] = '\0'; //Null terminator
  payload_Subscribe = atof(message); //Ubah string ke float dan masukkan ke variabel payload_Subscribe
}


//==================================================== Method Debugging untuk Subscribe ==================================================
void debuggingSubscribe(){
  if(payload_Subscribe != 0){
    Serial.println("\n[Pemeriksaan Subscribe MQTT]"); 
    Serial.println("Payload: " + String(payload_Subscribe));
    Serial.println("\n==================================================================================");
  }
}


//============================================================== Method Read pH ============================================================
void readPublishPH(){
  pHValue = 7.53; //Menyimpan nilai ke variabel pHValue

  //Cek nilai pH ada perubahan atau tidak, jika ada perubahan maka:
  if(pHValue != old_pHValue){
    dtostrf(pHValue, 4, 2, payload_Publish); //Float -> String 
    client.publish(Topic, payload_Publish, true); //Publish topik beserta payloadnya menggunakan retain message
    // debuggingSubscribe(); //Memanggil method debuggingSubscribe => Jika tidak digunakan sebaiknya dibuat komentar saja
    old_pHValue = pHValue; //Menyimpan nilai pH saat ini ke variabel old_pHValue
  }
}


//============================================================ Method Output LCD ===========================================================
void Loading(){
  lcd.clear(); lcd.backlight(); lcd.setCursor(1,0); lcd.print("Loading...."); delay(5000); Waiting();
}
void Waiting(){
  lcd.clear(); lcd.backlight(); lcd.setCursor(1,0); lcd.print("Menunggu"); lcd.setCursor(1,1); lcd.print("Perintah..."); delay(1000);
}
void LCDfailIoT(){
  lcd.clear(); lcd.backlight(); lcd.setCursor(1,0); lcd.print("IoT Gagal"); lcd.setCursor(1,1); lcd.print("Tersambung..."); delay(5000);
}
void Viewnow(){
  //Jika waktu sekarang dikurangi waktu terakhir lebih besar sama dengan 1 detik maka :
  if ((currentMillis - startTime1) >= delayTime1) {
    lcd.clear(); lcd.backlight(); lcd.setCursor(2,0); lcd.print("pH Air : "+ String(payload_Subscribe));
    startTime1 = currentMillis; //Perbarui waktu terakhir dijalankan
  } Waiting();
}
void LCDAllpHON(){
  //Jika waktu sekarang dikurangi waktu terakhir lebih besar sama dengan 5 detik maka :
  if ((currentMillis - startTime2) >= delayTime2) {
    lcd.clear(); lcd.backlight(); lcd.setCursor(4,0); lcd.print("All pH :"); lcd.setCursor(6,1); lcd.print("(ON)");
    startTime2 = currentMillis; //Perbarui waktu terakhir dijalankan
  } Waiting();
}
void LCDAllpHOFF(){
  //Jika waktu sekarang dikurangi waktu terakhir lebih besar sama dengan 5 detik maka :
  if ((currentMillis - startTime2) >= delayTime2) {
    lcd.clear(); lcd.backlight(); lcd.setCursor(4,0); lcd.print("All pH :"); lcd.setCursor(5,1); lcd.print("(OFF)");
    startTime2 = currentMillis; //Perbarui waktu terakhir dijalankan
  } Waiting();
}
void LCDpHUpON(){
  //Jika waktu sekarang dikurangi waktu terakhir lebih besar sama dengan 5 detik maka :
  if ((currentMillis - startTime2) >= delayTime2) {
    lcd.clear(); lcd.backlight(); lcd.setCursor(4,0); lcd.print("pH Up :"); lcd.setCursor(6,1); lcd.print("(ON)");
    startTime2 = currentMillis; //Perbarui waktu terakhir dijalankan
  } Waiting();
}
void LCDpHUpOFF(){
  //Jika waktu sekarang dikurangi waktu terakhir lebih besar sama dengan 5 detik maka :
  if ((currentMillis - startTime2) >= delayTime2) {
    lcd.clear(); lcd.backlight(); lcd.setCursor(4,0); lcd.print("pH Up :"); lcd.setCursor(5,1); lcd.print("(OFF)");
    startTime2 = currentMillis; //Perbarui waktu terakhir dijalankan
  } Waiting();
}
void LCDpHDownON(){
  //Jika waktu sekarang dikurangi waktu terakhir lebih besar sama dengan 5 detik maka :
  if ((currentMillis - startTime2) >= delayTime2) {
    lcd.clear(); lcd.backlight(); lcd.setCursor(4,0); lcd.print("pH Down:"); lcd.setCursor(6,1); lcd.print("(ON)");
    startTime2 = currentMillis; //Perbarui waktu terakhir dijalankan
  } Waiting();
}
void LCDpHDownOFF(){
  //Jika waktu sekarang dikurangi waktu terakhir lebih besar sama dengan 5 detik maka :
  if ((currentMillis - startTime2) >= delayTime2) {
    lcd.clear(); lcd.backlight(); lcd.setCursor(4,0); lcd.print("pH Down:"); lcd.setCursor(6,1); lcd.print("(OFF)");
    startTime2 = currentMillis; //Perbarui waktu terakhir dijalankan
  } Waiting();
}