// LAST UPDATE: NOVEMBER 2024

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>
#include <Wire.h>
#include <LiquidCrystal.h>
#include <EEPROM.h> // to store number of coffees made and pumped water

AsyncWebServer server(80);

// coffee times (in seconds) and temperature (in C) settings:
// Warning! these are currently hard-coded in the arduino nano
//int rist = 10;
//int espr = 15;
//int lung = 20;
//int amer = 60;
//int desiredTemp = 70;

// support variables used by checktraywater() to interact with EEPROM
int mem0 = 0;
int mem1 = 0;

// This next variable is used to get current "time" in ms and break out of while cycles that need a time limit safeguard
unsigned long startTime;

// WiFi settings
const char* ssid = "LILiK_WiFi";   //replace with your SSID
const char* password = "pippopippo"; //password
String hostname = "PyCoffee";
int wifiStatus = 0; // variable to store status (connected/disconnected, used by LCDprint)

// string to store coffee style from HTML
String input_field = ""; // string for coffee type
String input_field2 = ""; // string for serial commands

// HTML WEB interface
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head>
  <title>PyCoffee HTML interface</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <style>
    html {display: inline-block; text-align: center;}
  </style>
  
</head><body>
  <a>Ciao! Sono la macchina del caffe' della 117bis!</a><br>
  <a>Seleziona il tipo di caffe' e conferma</a><br><br>
  
  <form action="/get">
    <select name="coffee_input">
      <option value="Ristretto">Ristretto</option>
      <option value="Espresso">Espresso</option>
      <option value="Lungo">Lungo</option>
      <option value="Americano">Americano</option>
    </select><br><br>
    <input type="submit" value="Conferma">
  </form>
  <br><br>

  <a>Puoi mandare comandi a Arduino via seriale</a><br>
  <a>usa 'exit' per uscire dall'ultimo comando seriale</a><br><br>

  <form action="/serial">
    <select name="serial_input">
      <option value="exit">Exit Command</option>
      <option value="dryr">PerformDryRun</option>
      <option value="noco">NoCoffeeRun</option>
      <option value="clea">Self-Clean</option>
      <option value="make">Make Coffee</option>
      <option value="pump">pumpWater</option>
      <option value="pres">Press</option>
      <option value="unpr">un-Press</option>
      <option value="f-pr">Force-Press</option>
      <option value="f-un">Force-unpress</option>
      <option value="heat">Heat Boiler</option>
      <option value="grin">GrindPowder</option>
      <option value="drop">Drop Powder</option>
      <option value="rist">setRistretto-15s</option>
      <option value="espr">setEspresso-20s</option>
      <option value="long">setLungo-25s</option>
      <option value="amer">setAmericano-60s</option>
    </select><br><br>
    <input type="submit" value="Invia Comando">
    </form>
  <br><br>
  
  <a>Consulta la pagina Gitea per maggiori informazioni</a><br><br>

  <a>Aggiornamento firmware:</a> <br>
  <a href='/update'>&#11014;&#65039; OTA Firmware_Update &#11014;&#65039;</a><br><br>
  <a>Codice sorgente (su LILiK projects):</a> <br>
  <a href='https://projects.lilik.it/LILiK/PyCoffee_ESP8266'>&#9749;Gitea&#9749;</a><br><br>

</body></html>)rawliteral";

// input parameter for html server
const char* input_paramter1 = "coffee_input";
const char* input_paramter2 = "serial_input";

// enable http coffee making on server request
int httpCoffee = 0;
int serialCommand = 0;

// button pins
const int sel = 0, enter = 2;

// Global string, allows serial in
String readString;

// coffee type variable
String type = "Espresso";

// number of coffees made and seconds of pumped water since last tray empty/water refill
// used by checkTrayWater() function
int coffeesDone = 0;
int pumpedS = 0;
const int MaxCoffees = 10;
const int MaxPumped = 500;

// Support string to store stuff to be sent over serial
String serialOUTbuffer = "none";

// Variables to store lines to be printed on LCD:
String line1 = "";
String line2 = "";
// only refresh LCD when needed
int printed = 0;

// LCD pins <--> ESP8266 LOLIN D1 pins
const int e_RS = 4, e_EN = 5, e_D6 = 12, e_D7 = 13, e_D5 = 14, e_D8 = 15;
LiquidCrystal lcd(e_RS, e_EN, e_D6, e_D7, e_D5, e_D8);

// Custom character matrix (coffee cup left side)
byte cupL[8] = {0b00001,0b00010,0b00010,0b00001,0b00001,0b00000,0b00111,0b00100};
// Custom character matrix (coffee cup center)
byte cupC[8] = {0b00100,0b00101,0b01001,0b01000,0b00100,0b00000,0b11111,0b00000};
// Custom character matrix (coffee cup right side)
byte cupR[8] = {0b10000,0b00000,0b00000,0b10000,0b10000,0b00000,0b11000,0b01110};
// Custom character matrix (coffee cup left side line 2)
byte cupL1[8] = {0b00100,0b00100,0b00100,0b00010,0b01111,0b01000,0b00111,0b00000};
// Custom character matrix (coffee cup center line 2)
byte cupC1[8] = {0b00000,0b00000,0b00000,0b00000,0b11111,0b00000,0b11111,0b00000};
// Custom character matrix (coffee cup right side line 2)
byte cupR1[8] = {0b01010,0b01010,0b01110,0b10000,0b11100,0b00100,0b11000,0b00000};
// Custom character matrix (WiFi connected/disconnected)
byte wifiok[8] = {0b00000,0b01110,0b10001,0b00100,0b01010,0b00000,0b00000,0b00100};
byte wifino[8] = {0b00000,0b10001,0b01010,0b00100,0b01010,0b10001,0b00000,0b00100};

void notFound(AsyncWebServerRequest *request) {
  request->send(404, "text/plain", "Not found");
}

//_____________________________________________________________________________

// the setup function runs once when you press reset or power the board
void setup(void) {

  // check how many coffees were done and seconds of pumped water from EEPROM
  mem0 = (EEPROM.read(0));
  mem1 = (EEPROM.read(1));

  // protect against under/over-flow due to errors or memory location never having been used
  if (mem0 < 0 or mem0 > MaxCoffees + 1) {
    EEPROM.write(0, 0);
    mem0 = 0;
  }
  if (mem1 < 0 or mem1 > MaxPumped + 50) {
    EEPROM.write(1, 0);
    mem1 = 0;
  }
  coffeesDone = mem0;
  pumpedS = mem1;

  // Set buttons as inputs with internal pull-ups
  pinMode(enter, INPUT_PULLUP);
  pinMode(sel, INPUT_PULLUP);
  
  // initialize serial:
  Serial.begin(9600);

  // Make custom characters available to LCD to paint a cute coffee cup
  lcd.createChar(0, cupL);
  lcd.createChar(1, cupC);
  lcd.createChar(2, cupR);
  lcd.createChar(3, cupL1);
  lcd.createChar(4, cupC1);
  lcd.createChar(5, cupR1);
  lcd.createChar(6, wifiok);
  lcd.createChar(7, wifino);
  lcd.begin(16, 2); // set up number of columns and rows
  lcd.clear();      // Clear LCD

  // Show boot animation on LCD
  bootAnimation();
  
  WiFi.mode(WIFI_STA);
  //  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
  WiFi.setHostname(hostname.c_str()); //define hostname
  WiFi.begin(ssid, password);

  connectedTo();

  // Send web page with input fields to client
  server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
    request->send(200, "text/html", index_html);
  });

  // Get data from menu
  // coffee
  server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
    if (request->hasParam(input_paramter1)) {
      type = request->getParam(input_paramter1)->value();
      httpCoffee = 1;
    }
  request->send_P(200, "text/html", index_html); // Re-send main page, with data!
  });
  // serial commands
  server.on("/serial", HTTP_GET, [] (AsyncWebServerRequest *request) {
    if (request->hasParam(input_paramter2)) {
      if (serialOUTbuffer != "") {
        serialOUTbuffer = request->getParam(input_paramter2)->value();
        serialCommand = 1;
        httpCoffee = 0;
      }
    }
  request->send_P(200, "text/html", index_html); // Re-send main page, with data!
  });
  
  // in case of error:
  server.onNotFound(notFound);
  AsyncElegantOTA.begin(&server);    // Start ElegantOTA
  server.begin();
  Serial.println("HTTP server started");

  // Print selection screen
  printed = 1;
  
  // tell the Arduino we're ready
  serialOUTbuffer = "read";
  delay(100);
  writeSerial();
  
  type = "Espresso";
  line1 = "Selection";
  line2 = type;
  LCDprint();
  delay(500);
}

//_____________________________________________________________________________

void loop() {
// the loop function runs over and over again forever
delay(10);
serialFromHTTP();

    // select type of brew by pressing the sel button
    if (digitalRead(sel) == LOW && type == "Espresso") {
      type = "Lungo";
      line1 = "Selection";
      line2 = type;
      LCDprint();
      delay(500);
    }
    else if (digitalRead(sel) == LOW && type == "Lungo") {
      type = "Americano";
      line1 = "Selection";
      line2 = type;
      LCDprint();
      delay(500);
    }
    else if (digitalRead(sel) == LOW && type == "Americano") {
      type = "Ristretto";
      line1 = "Selection";
      line2 = type;
      LCDprint();
      delay(500);
    }
    else if (digitalRead(sel) == LOW && type == "Ristretto") {
      type = "Espresso";
      line1 = "Selection";
      line2 = type;
      LCDprint();
      delay(500);
    }
    else if (Serial.available() > 0) {
      // read the incoming data: (we only care about the first few characters, I've chosen 4)
      readString = "";
      serialRead();
      String incomingData = readString.substring(0,4);
      // are we heating the boiler to vapor temperature?
      if (incomingData == "Vapo") {
        line1 = "Heating";
        line2 = "Vapor";
        LCDprint();
        delay(1000);
      }
      if (incomingData == "reac") {
        line1 = "Vapor";
        line2 = "ok";
        LCDprint();
        delay(1000);
        printed = 1;
        delay(10000);
        serialOUTbuffer = "unpr";
        writeSerial();
        delay(100);
      }
    }
  refreshMenu();
  delay(100);

  // check if data has been sent on serial from Arduino,
  // send commands to arduino and display corresponding messages on LCD
  // this is the main function dedicated to making coffee and reporting errors.
  serialCoffeeCommandLCDInterface();
}


//_____________________________________________________________________________

// this is how we read the input
void serialRead() { 
  while (Serial.available()) {
  delay(1);  
  if (Serial.available() > 0) {
    char c = Serial.read();
    readString += c;}
  }
}

//_____________________________________________________________________________

void bootAnimation() {
  
  delay(1000);
  line1 = "ESP_Coffee";
  line2 = "Release 1.1e";
  LCDprint();
  delay(1000);
  line1 = "JIMBO";
  line2 = "";
  LCDprint();
  delay(1000);
  lcd.setCursor(11,0);  //print coffee cup top using uint8 method
  lcd.write((uint8_t)0);
  delay(10);
  lcd.write((uint8_t)1);
  delay(10);
  lcd.write((uint8_t)2);
  delay(10);
  lcd.setCursor(11,1);  //print coffee cup bottom using uint8 method
  lcd.write((uint8_t)3);
  delay(10);
  lcd.write((uint8_t)4);
  delay(10);
  lcd.write((uint8_t)5);
  delay(2000);
}

//_____________________________________________________________________________

void connectedTo() {
  
  startTime = millis();
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    if (millis() - startTime > 60000) {
      Serial.println("WiFi connection timeout");
      line1 = "Timeout at SSID";
      line2 = ssid;
      LCDprint();
      delay(10000);
      break;
    }
    else {
      wifiStatus = 1;
    }
  }
  
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());   //You can get IP address assigned to ESP

  line1 = "SSID";
  line2 = ssid;
  LCDprint();
  delay(2000);

  line1 = "Connected to IP:";
  line2 = "";
  LCDprint();
  delay(10);
  lcd.setCursor(0, 1);          // move cursor to   (0, 1)
  lcd.print(WiFi.localIP());    // re-print line2 or IP won't print
  delay(2000);
}

//_____________________________________________________________________________

void refreshMenu() {
  if (line1 == "Selection" && printed == 1) {
    lcd.clear();
    lcd.setCursor(0, 0);          // move cursor to   (0, 0)
    lcd.print(line1);             // print line1
    delay(10);
    lcd.setCursor(15, 0);         // move cursor to   (14, 0)
    // print wifi logo depending on connection starus
    if (wifiStatus == 1) {
      lcd.write((uint8_t)6);
      delay(10);
    }
    else if (wifiStatus == 0) {
      lcd.write((uint8_t)7);
      delay(10);
    }
    
    lcd.setCursor(0, 1);          // move cursor to   (0, 1)
    lcd.print(line2);             // print line2
    delay(10);
    printed = 0;
  }
}

//_____________________________________________________________________________

void LCDprint() {
  lcd.clear();
  lcd.setCursor(0, 0);          // move cursor to   (0, 0)
  lcd.print(line1);             // print line1
  delay(10);
  lcd.setCursor(15, 0);         // move cursor to   (14, 0)
  // print wifi logo depending on connection starus
  if (wifiStatus == 1) {
    lcd.write((uint8_t)6);
    delay(10);
  }
  else if (wifiStatus == 0) {
    lcd.write((uint8_t)7);
    delay(10);
  }
  
  lcd.setCursor(0, 1);          // move cursor to   (0, 1)
  lcd.print(line2);             // print line2
  delay(10);
}

//_____________________________________________________________________________

void writeSerial() {
  Serial.println(serialOUTbuffer);
}

//_____________________________________________________________________________

void serialFromHTTP() {
    if (serialCommand == 1){
    serialCommand = 0;
    line1 = "serialCom";
    line2 = serialOUTbuffer;
    LCDprint();
    delay(1000);
    writeSerial();
    while (serialOUTbuffer != "exit" && digitalRead(sel) == HIGH ) {
      
      if (Serial.available() > 0) {
      // read the incoming data: (we only care about the first few characters, I've chosen 4)
      readString = "";
      serialRead();
      String incomingData = readString.substring(0,16);
      Serial.print("serial put from Arduino:\n");
      Serial.print(incomingData);
      Serial.print("\n");
      if (serialOUTbuffer != "none") {
        line1 = serialOUTbuffer;
      }
      line2 = incomingData;
      LCDprint();
      delay(100);
      serialOUTbuffer = "none";
      writeSerial();
      }
    }
    line1 = "Selection";
    line2 = type;
    LCDprint();
    delay(500);
  }
}

//_____________________________________________________________________________

void checkTrayWater() {
  // section dedicated to empty tray and refill water messages
  if (coffeesDone == MaxCoffees) {
    lcd.clear();
    while (digitalRead(enter) == HIGH) {
      line1 = "Empty Tray";
      line2 = "Then hold Make";
      delay(500);
      LCDprint();
    }
    coffeesDone = 0;
    EEPROM.write(0, 0);
    line1 = "Tray emptied";
    line2 = "continuing...";
    delay(1000);
    LCDprint();
  }
  
  if (pumpedS > MaxPumped) {
    lcd.clear();
    while (digitalRead(enter) == HIGH) {
      line1 = "Refill Water";
      line1 = "Then hold Make";
      delay(500);
      LCDprint();
    }
    pumpedS = 0;
    EEPROM.write(1, 0);
    line1 = "Water Refilled";
    line2 = "continuing...";
    delay(1000);
    LCDprint();
  }
  delay(500);
  printed = 1;
  EEPROM.commit();
  line1 = "Selection";
  line2 = type;
}

//_____________________________________________________________________________

void serialCoffeeCommandLCDInterface() {
  if (digitalRead(enter) == LOW or httpCoffee == 1) {
    httpCoffee = 0;
    if (type == "Espresso") {
      serialOUTbuffer = "espr";
      pumpedS = pumpedS + 20;
     }
     else if (type == "Lungo") {
      serialOUTbuffer = "long";
      pumpedS = pumpedS + 25;
     }
     else if (type == "Ristretto") {
      serialOUTbuffer = "rist";
      pumpedS = pumpedS + 15;
     }
     else if (type == "Americano") {
     serialOUTbuffer = "amer";
     pumpedS = pumpedS + 60;
     }
     writeSerial();
     delay(500);
     serialOUTbuffer = "make";
     writeSerial();
    }
    if (Serial.available() > 0) {
  // read the incoming data: (we only care about the first few characters, I've chosen 4)
      readString = "";
      serialRead();
      String incomingData = readString.substring(0,4);
      Serial.print(incomingData);
      Serial.print("\n");

    if (incomingData == "Maki") {
      // Display making coffee message
      line1 = "Making";
      line2 = type;
      LCDprint();

      delay(10);
      
      lcd.setCursor(11,0);  //print coffee cup top using uint8 method
      lcd.write((uint8_t)0);
      delay(10);
      lcd.write((uint8_t)1);
      delay(10);
      lcd.write((uint8_t)2);
      delay(10);
      lcd.setCursor(11,1);  //print coffee cup bottom using uint8 method
      lcd.write((uint8_t)3);
      delay(10);
      lcd.write((uint8_t)4);
      delay(10);
      lcd.write((uint8_t)5);
      delay(10);

    }

    if (incomingData == "u-Th") {
      Serial.print("Thermocouple: unplugged or failed\n");
      while (true) {
        // Unrecoverable error has occurred during heating, lock the machine
        line1 = "ThermoCouple err";
        line2 = "Unplugged/failed";
        LCDprint();
        delay(10000);
      }
    }
    if (incomingData == "p-Th") {
      Serial.print("Thermocouple: positioning or relay fault\n");
      while (true) {
        // Unrecoverable error has occurred during heating, lock the machine
        line1 = "ThermoCouple err";
        line2 = "Fallen/relayFail";
        LCDprint();
        delay(10000);
      }
    }
    if (incomingData == "Erro") {
      Serial.print("Unrecoverable error in Arduino subsystem. Please restart\n");
      while (true) {
        // Unrecoverable error has occurred during heating, lock the machine
        line1 = "Enrecoverable";
        line2 = "Error. Restart.";
        LCDprint();
        delay(10000);
      }
    }
    if (incomingData == "h-ta") {
      Serial.print("took too long to warm boiler, this is just a warning. Everything should be fine\n");
      line1 = "Heating too long";
      line2 = "Continuing...";
      LCDprint();
    }
    if (incomingData == "p-en") {
      Serial.print("Unable to Press. Please restart\n");
      while (true) {
        // Unrecoverable error has occurred while pressing, lock the machine
        line1 = "Press failed";
        line2 = "Error. Restart.";
        LCDprint();
        delay(10000);
      }
    }
    if (incomingData == "u-en") {
      Serial.print("Unable to unPress. Please restart\n");
      while (true) {
        // Unrecoverable error has occurred while unpressing, lock the machine
        line1 = "unPress failed";
        line2 = "Error. Restart.";
        LCDprint();
        delay(10000);
      }
    }
    if (incomingData == "refi") {
      Serial.print("refill coffee and send cont command\n");
      line1 = "Refill Coffee";
      line2 = "then tap make";
      LCDprint();
      while (true) {
        delay(10);
        if (digitalRead(enter) == LOW) {
          line1 = "Refilled";
          line2 = "continuing";
          LCDprint();
          serialOUTbuffer = "cont";
          writeSerial();
          break;
        }
      }
    }
    if (incomingData == "g-fa") {
      Serial.print("g-failure: second grinding failure, check grinder!\n");
      while (true) {
        // Unrecoverable error has occurred during grinding, lock the machine
        line1 = "Grinding failed";
        line2 = "Check Machine";
        LCDprint();
        delay(10000);
      }
    }
    if (incomingData == "Comp") {
      coffeesDone++;
      Serial.print("Made coffee\n");
      line1 = "Coffee Done :)";
      line2 = "Enjoy!";
      LCDprint();
      delay(5000);
      line1 = "Selection";
      delay(500);
    }
  }
}
