// Knižnice –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

#include <Wire.h>              // I2C (BME280, SSD1306, CCS881)
#include <SPI.h>               // Spostredkovanie komunikácie s SPI
#include <Adafruit_Sensor.h>   // Multiple - Bus
#include <HardwareSerial.h>    // UART

#include <Adafruit_SSD1306.h>  // Základné funkcie pre OLED
#include <Adafruit_GFX.h>      // Pokročilejšie funkcie pre OLED
#include <Adafruit_BME280.h>   // Senzor teploty, vlhkosti a tlaku
#include <Adafruit_CCS811.h>   // CO2 & VOC senzor
#include <PMS.h>               // PM2,5 Senzor
// #include <MHZ19.h>             // CO2 senzor
#include <SD.h>                // Modul pre SD kartu
#include <FS.h>                // File Operations
#include <NTPClient.h>         // Time extraction
#include <WiFiUdp.h>           // from NTP server

// Globálne premenné –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––——–

// Premenná pre merané dáta z PMS5003
PMS::DATA data;

// Save reading number on RTC memory
RTC_DATA_ATTR int readingID = 0;

// Premenná ktorá spája dátum a čas (Time & Date server preparations)
String dataMessage;

// Premenné na ukladanie dátumu a času
String formattedDate;
String dayStamp;
String timeStamp;

// Parametre spánkového režimu ESP32
uint64_t uS_TO_S_FACTOR = 1000000; // Konverzný parameter pre prevod z mikro sekúnd na sekundy
uint64_t TIME_TO_SLEEP = 600; // Spánkový režim na 10 minút (600 sekúnd)

// Definovanie parametrov OLED displeju
#define SCREEN_WIDTH 128 // šírka (v pixeloch)
#define SCREEN_HEIGHT 64 // výška (v pixeloch)
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C // see datasheet

// Chip Select pin prepojený na ESP32 prepojený s modulom SD karty
#define SD_CS 5 

/* Definovanie pinov vysielacej a príjmacej linky na ESP32 pre senzor MHZ-19B
#define RX_PIN 13                          
#define TX_PIN 14                          
#define BAUDRATE 9600                      // (should not be changed)
MHZ19 myMHZ19;                             // Konštruktor z knižnice MHZ19 a objekt myMHZ19
HardwareSerial mySerial(1);                // Aktivácia UART pre MHZ-19B
*/

// Názov (SSID) a heslo pre prihlásenie sa do siete na ESP32 
const char* ssid = "AQM";
const char* password = "Vut@19091899";

// Deklarovanie premien pre časovač merania (30 sekúnd)
unsigned long lastTime = 0;  
unsigned long timerDelay = 30000;

// Priradenie portu serveru na ESP32
AsyncWebServer server(80);
AsyncEventSource events("/events");

// Funkcia ktroá aktualizuje merané veličiny na serveri pri prihlásení
String processor(const String& var){
  int temperature = bme.readTemperature();
  int humidity = bme.readHumidity();
  int pressure = bme.readPressure() / 100;
  int carbDio = ccs.geteCO2();
  
  //Serial.println(var);
  if(var == "TEMPERATURE"){
    return String(temperature);
  }
  else if(var == "HUMIDITY"){
    return String(humidity);
  }
  else if(var == "PRESSURE"){
    return String(pressure);
  }
 else if(var == "CO2"){
    return String(carbDio);
  }
}

// Stránka na serveri kde sa zobrazujú merané veličiny
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>BME680 Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <link rel="icon" href="data:,">
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    p {  font-size: 1.2rem;}
    body {  margin: 0;}
    .topnav { overflow: hidden; background-color: #4B1D3F; color: white; font-size: 1.7rem; }
    .content { padding: 20px; }
    .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); }
    .cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
    .reading { font-size: 2.8rem; }
    .card.temperature { color: #0e7c7b; }
    .card.humidity { color: #17bebb; }
    .card.pressure { color: #3fca6b; }
    .card.gas { color: #d62246; }
  </style>
</head>
<body>
  <div class="topnav">
    <h3>AQM SERVER</h3>
  </div>
  <div class="content">
    <div class="cards">
      <div class="card temperature">
        <h4><i class="fas fa-thermometer-half"></i> TEMPERATURE</h4><p><span class="reading"><span id="temp">%TEMPERATURE%</span> &deg;C</span></p>
      </div>
      <div class="card humidity">
        <h4><i class="fas fa-tint"></i> HUMIDITY</h4><p><span class="reading"><span id="hum">%HUMIDITY%</span> &percnt;</span></p>
      </div>
      <div class="card pressure">
        <h4><i class="fas fa-angle-double-down"></i> PRESSURE</h4><p><span class="reading"><span id="pres">%PRESSURE%</span> &hPa</span></p>
      </div>
      <div class="card co2">
        <h4><i class="fas fa-wind"></i> GAS</h4><p><span class="reading"><span id="gas">%CO2%</span> &ppm;</span></p>
      </div>
    </div>
  </div>
<script>
if (!!window.EventSource) {
 var source = new EventSource('/events');
 
 source.addEventListener('open', function(e) {
  console.log("Events Connected");
 }, false);
 source.addEventListener('error', function(e) {
  if (e.target.readyState != EventSource.OPEN) {
    console.log("Events Disconnected");
  }
 }, false);
 
 source.addEventListener('message', function(e) {
  console.log("message", e.data);
 }, false);
 
 source.addEventListener('temperature', function(e) {
  console.log("temperature", e.data);
  document.getElementById("temp").innerHTML = e.data;
 }, false);
 
 source.addEventListener('humidity', function(e) {
  console.log("humidity", e.data);
  document.getElementById("hum").innerHTML = e.data;
 }, false);
 
 source.addEventListener('pressure', function(e) {
  console.log("pressure", e.data);
  document.getElementById("pres").innerHTML = e.data;
 }, false);
 
 source.addEventListener('gas', function(e) {
  console.log("gas", e.data);
  document.getElementById("gas").innerHTML = e.data;
 }, false);
}
</script>
</body>
</html>)rawliteral";

// Inicializácia modulov ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// BME280
Adafruit_BME280 bme;
// CCS811
Adafruit_CCS811 ccs;
//MHZ-19B
// MHZ19 myMHZ19;
// OLED dislpej
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// NTP Klient
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

// Set-Up –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

void setup() {
  Serial.begin(115200); // Nadviazanie komunikácie s ESP32 
  Serial2.begin(115200); // Nadviazanie komunikácie s PMS5003
  WiFi.mode(WIFI_AP_STA) // ESP32 do režimu Access Point

  // Aktivácia WiFi stanice na ESP32
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Setting as a Wi-Fi Station..");
  }
  Serial.print("Station IP Address: ");
  Serial.println(WiFi.localIP());
  Serial.println();

  // Aktivácia NTP klienta pre získavanie dátumu a času
  timeClient.begin();
  timeClient.setTimeOffset(3600); // GMT +1 = 3600 (Stredná Európa)

  // Aktivácia senzoru BME280 (setup)
  while(!Serial); // time to get serial running
  unsigned bmeStatus = bme.begin(0x76);  
  if (!bmeStatus) {
    Serial.println("Could not find a valid BME280 sensor, check wiring, address, sensor ID!");
    while (1) delay(10);
  }

   // Aktivácia senzoru CCS811 (setup)
  unsigned ccsStatus = ccs.begin(0x5A);
  if(!ccsStatus){
    Serial.println("Failed to start sensor! Please check your wiring.");
    while(1);
  }
   while(!ccs.available())

  /* Aktivácia senzoru MHZ-19B (setup)
  mySerial.begin(115200, SERIAL_8N1, RX_PIN, TX_PIN); 
  myMHZ19.begin(mySerial); // *Serial(Stream) refence must be passed to library begin()
  myMHZ19.autoCalibration(); // Turn auto calibration ON (OFF autoCalibration(false))
  */

  // Aktivácia OLED displeju (setup)
  if (!oled.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("failed to start SSD1306 OLED"));
    while (1);
  }
  oled.display();
  delay(2000);         
  oled.clearDisplay(); 

  // Aktivácia SD modulu
  SD.begin(SD_CS);  
  if(!SD.begin(SD_CS)) {
    Serial.println("Card Mount Failed");
    return;
  }
  uint8_t cardType = SD.cardType();
  if(cardType == CARD_NONE) {
    Serial.println("No SD card attached");
    return;
  }

  // Vytvorenie súboru na SD karte a zápis dát
  File file = SD.open("/data.txt");
  if(!file) {
    Serial.println("File doens't exist");
    Serial.println("Creating file...");
    writeFile(SD, "/data.txt", "Reading ID, Date, Hour, Temperature \r\n");
  }
  else {
    Serial.println("File already exists");  
  }
  file.close();

  // Vykonanie meraní a zápis na SD kartu
  getgetCO2VOC();
  getTempHumPress();
  getPM25();
  getTimeStamp();
  logSDCard();

  // ˇˇˇ Web Server ˇˇˇ
  
  // Increment readingID on every new reading
  readingID++;

  // Handle requests
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/html", index_html, processor);
  });

  // Handle Web Server Events
  events.onConnect([](AsyncEventSourceClient *client){
  if(client->lastId()){
    Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
  }
  // send event with message "hello!", id current millis
  // and set reconnect delay to 1 second
  client->send("hello!", NULL, millis(), 10000);
  });
  server.addHandler(&events);
  server.begin();

  // ^^^ Web Server ^^^

  // Časovač zobudenia pre ESP32 (Enable Timer wake_up)
  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);

   // Uvedenie ESP32 do spánkového režimu (Start deep sleep mode)
  Serial.println("DONE! Going to sleep now.");
  esp_deep_sleep_start();

}

// Funnkcia pre získanie a výpis CO2 || VOC na OLED displeji
void getCO2VOC() {
  if (ccs.available()) 
  {
    if (!ccs.readData()) 
    {
      int co2 = ccs.geteCO2();
      int voc = ccs.getTVOC(); 

      Serial.print("CO2: ");
      Serial.print(co2);
      Serial.print("ppm, VOC: ");
      Serial.println(voc);
      Serial.println("—————————————————————————————————————");
 
      oled.clearDisplay();
      oled.setTextSize(1);
      oled.setTextColor(WHITE);    // set text color

      oled.setCursor(20, 0);
      oled.print("Air Quality");
      oled.setTextSize(2);
      oled.setCursor(0, 20);
      oled.print("CO2:");
      oled.print(co2);
      oled.setTextSize(1);
      oled.print(" ppm");
      oled.setTextSize(2);
      oled.setCursor(0, 50);
      oled.print("VOC:");
      oled.print(voc);
      
      oled.display();
      oled.clearDisplay();
      delay(2000);
    }
    else 
    {
      Serial.println("ERROR!");
      
      oled.clearDisplay();
      oled.setTextSize(2);
      oled.setTextColor(WHITE);    // set text color
      oled.setCursor(0, 5);
      oled.print("ERROR!");
      while (1);
    }  
  }

  delay(500);

// Funnkcia pre získanie a výpis Teploty || Vlhkosti || Barometrického tlaku na OLED displeji
void getTempHumPress(){
  int temp = bme.readTemperature();
  int hum = bme.readHumidity();
  int press = bme.readPressure() / 100;

  Serial.print("Temperature = ");
  Serial.print(temp);
  Serial.println(" °C");
  Serial.print("Pressure = ");
  Serial.print(press);
  Serial.println(" hPa");
  Serial.print("Humidity = ");
  Serial.print(hum);
  Serial.println(" %");
  Serial.println("—————————————————————————————————————");

  oled.clearDisplay();
  oled.setTextSize(1);         // set text size
  oled.setTextColor(WHITE);    // set text color

  oled.setCursor(10, 10);        // set position to display
  oled.print("Temperature = ");
  oled.print(temp);
  oled.println(" °C");
  oled.setCursor(10, 30);
  oled.print("Pressure = ");
  oled.print(press);
  oled.println(" hPa");
  oled.setCursor(10, 50);
  oled.print("Humidity = ");
  oled.print(hum);
  oled.println(" %");

  oled.display(); 
  oled.clearDisplay();
  delay(2000);
}

delay(500);

// Funnkcia pre získanie a výpis prachových častíc PM1 || PM2,5 || PM10 na OLED displeji
void getPM25() {
  if (pms.read(data))
  {
    int pm1 = data.PM_AE_UG_1_0;
    int pm25 = data.PM_AE_UG_2_5;
    int pm10 = data.PM_AE_UG_10_0;
 
    Serial.println("Air Quality Monitor");
    Serial.println("PM1.0 :" + pm1 + "(ug/m3)");
    Serial.println("PM2.5 :" + pm25 + "(ug/m3)");
    Serial.println("PM10  :" + pm10 + "(ug/m3)");
    Serial.println("—————————————————————————————————————");
 
    oled.clearDisplay();
    oled.setTextSize(1);         // set text size
    oled.setTextColor(WHITE);    // set text color

    oled.setCursor(10, 10);
    oled.print("PM1.0: ");
    oled.print(pm1);
    oled.print(" ug/m3");
    oled.setCursor(10, 30);
    oled.print("PM2.5: ");
    oled.print(pm25);
    oled.print(" ug/m3");
    oled.setCursor(10, 50);
    oled.print("PM10 : ");
    oled.print(pm10);
    oled.print(" ug/m3");
 
    oled.display();
    delay(2000);
  }
}

// Funkcia pre získanie dátumu a času z NTP servera
void getTimeStamp() {
  while(!timeClient.update()) {
    timeClient.forceUpdate();
  }
  formattedDate = timeClient.getFormattedDate(); // Formát má tvar: 2018-05-28T16:00:13Z
  Serial.println(formattedDate);
  // Príkaz pre získanie dátumu z NTP servera
  int splitT = formattedDate.indexOf("T");
  dayStamp = formattedDate.substring(0, splitT);
  Serial.println(dayStamp);
  // Príkaz pre získanie času z NTP servera
  timeStamp = formattedDate.substring(splitT+1, formattedDate.length()-1);
  Serial.println(timeStamp);
}

// Formátovanie logovaných dát na SD kartu
void logSDCard() {
  dataMessage = String(readingID) + "," + String(dayStamp) + "," + String(timeStamp) + "," + 
                String(co2) + String(voc) + String(temp) + String(hum) + String(press) + 
                String(pm1) + String(pm25) + String(pm10)"\r\n";
  Serial.print("Save data: ");
  Serial.println(dataMessage);
  appendFile(SD, "/data.txt", dataMessage.c_str());
}

// Funkcia pre zápis dát na SD kartu (NEUPRAVOVAŤ FUNKCIU!!!)
void writeFile(fs::FS &fs, const char * path, const char * message) {
  Serial.printf("Writing file: %s\n", path);

  File file = fs.open(path, FILE_WRITE);
  if(!file) {
    Serial.println("Failed to open file for writing");
    return;
  }
  if(file.print(message)) {
    Serial.println("File written");
  } else {
    Serial.println("Write failed");
  }
  file.close();
}

// Funkcia pre doplnenie nových dát na SD kartu (NEUPRAVOVAŤ FUNKCIU!!!)
void appendFile(fs::FS &fs, const char * path, const char * message) {
  Serial.printf("Appending to file: %s\n", path);

  File file = fs.open(path, FILE_APPEND);
  if(!file) {
    Serial.println("Failed to open file for appending");
    return;
  }
  if(file.print(message)) {
    Serial.println("Message appended");
  } else {
    Serial.println("Append failed");
  }
  file.close();
}

// Funkcia loop obsahuje iba aktualizovanie meraných dát na serveri
void loop() {
  if ((millis() - lastTime) > timerDelay) {
    getBME680Readings();
    Serial.printf("Temperature = %.2f ºC \n", temperature);
    Serial.printf("Humidity = %.2f % \n", humidity);
    Serial.printf("Pressure = %.2f hPa \n", pressure);
    Serial.printf("CO2 = %.2f ppm \n", carbDio);
    Serial.println();

    // Send Events to the  Server with the Sensor Readings
    events.send("ping",NULL,millis());
    events.send(String(temperature).c_str(),"temperature",millis());
    events.send(String(humidity).c_str(),"humidity",millis());
    events.send(String(pressure).c_str(),"pressure",millis());
    events.send(String(carbDio).c_str(),"CO2",millis());
    
    lastTime = millis();
  }
}