#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <ESP32Servo.h>
#include <Preferences.h>

// prodleva mezi čteními DS18B20 a uložením dat do paměti v milisekundách - 
// můžete zkusit snížit ale čím menší bude prodleva tím méně budou reagovat rotační enkódery 
// správně, enkódery potřebují co nejmenší prodlevu mezi čteními k zachycení signálu
#define prodleva_DS 3000 

// LCD nastaveni
LiquidCrystal_I2C lcd1(0x27, 20, 4);
LiquidCrystal_I2C lcd2(0x28, 20, 4);

// DS18B20 nastaveni
#define ONE_WIRE_BUS 4
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

#define ONE_WIRE_BUS2 13
OneWire oneWire2(ONE_WIRE_BUS2);
DallasTemperature sensors2(&oneWire2);

// Servo nastaveni
Servo myservo;
#define SERVO_PIN 16
Servo myservo2;
#define SERVO_PIN2 17

// Rotacni enkoder nastaveni
#define ENCODER_CLK 26
#define ENCODER_DT 25
#define ENCODER_SW 23

#define ENCODER2_CLK 32
#define ENCODER2_DT 33
#define ENCODER2_SW 27

bool menuOption, menuOption2; // prepinani menu na displeji
float temperature, temperature2; // promenna pro nacteni teploty na displeji
float lastTemperature = 0.0, lastTemperature2 = 0.0; // promenna pro prepsani displeje pri zmene teploty
int angle, angle2; // promenna pro uhel serva
float minTemp = 50.0, minTemp2 = 50.0; // promenna pro minimalni rozsah teploty
float maxTemp = 70.0, maxTemp2 = 70.0; // promenna pro minimalni rozsah teploty
float minTempsave, maxTempsave; // promenna pro minimalni rozsah teploty (ukladani)
float minTemp2save, maxTemp2save; // promenna pro minimalni rozsah teploty 2 (ukladani)

// promenne pro otaceni rotacniho enkoderu
int currentStateCLK, currentStateCLK2;
int lastStateCLK, lastStateCLK2;
unsigned long lastButtonPress, lastButtonPress2; // promenna pro zmacknuti cudliku na enkoderu

unsigned long dsread = 0; // promenna pro cteni cidla 

Preferences preferences;  // nastaveni pameti pro ukladani / nacitani z pameti

// symbol stupne
byte degreeSymbol[8] = {
  0b00111,
  0b00101,
  0b00111,
  0b00000,
  0b00000,
  0b00000,
  0b00000,
  0b00000
};

void setup() {
  // Nastaveni enkoderu jako input
  pinMode(ENCODER_CLK, INPUT);
  pinMode(ENCODER_DT, INPUT);
  pinMode(ENCODER_SW, INPUT_PULLUP);
  pinMode(ENCODER2_CLK, INPUT);
  pinMode(ENCODER2_DT, INPUT);
  pinMode(ENCODER2_SW, INPUT_PULLUP);

  // Precteni aktualniho stavu enkoderu
  lastStateCLK = digitalRead(ENCODER_CLK);
  lastStateCLK2 = digitalRead(ENCODER2_CLK);

  // Inicializace LCD
  lcd1.init();
  lcd2.init();
  lcd1.backlight();
  lcd2.backlight();

  // nacteni ulozenych dat z pameti
  preferences.begin("nastaveni", false); 
  minTemp = preferences.getFloat("min1", 50);
  minTemp2 = preferences.getFloat("min2", 50);
  maxTemp = preferences.getFloat("max1", 70);
  maxTemp2 = preferences.getFloat("max2", 70);

  minTempsave = minTemp;
  maxTempsave = maxTemp;
  minTemp2save = minTemp2;
  maxTemp2save = maxTemp2;

  // Vytvoreni znaku pro stupne v pameti
  lcd1.createChar(0, degreeSymbol);
  lcd2.createChar(0, degreeSymbol);

  // Inicializace teplotniho cidla
  sensors.begin();
  sensors2.begin();

  // Inicializace serva
  myservo.attach(SERVO_PIN);
  myservo2.attach(SERVO_PIN2);

  // Prvni cteni z cidel
  sensors.requestTemperatures();
  temperature = sensors.getTempCByIndex(0);
  sensors2.requestTemperatures();
  temperature2 = sensors2.getTempCByIndex(0);

  updateDisplay1();
  updateDisplay2();
}

void loop() {
  if (dsread + prodleva_DS < millis()) {
    sensors.requestTemperatures();
    temperature = sensors.getTempCByIndex(0);
    sensors2.requestTemperatures();
    temperature2 = sensors2.getTempCByIndex(0);
    dsread = millis();
    
    if(minTempsave != minTemp) {
      minTempsave = minTemp;
      preferences.putFloat("min1", minTemp);
    }
    if(minTemp2save != minTemp2) {
      minTemp2save = minTemp2;
      preferences.putFloat("min2", minTemp2);
    }
    if(maxTempsave != maxTemp) {
      maxTempsave = maxTemp;
      preferences.putFloat("max1", maxTemp);
    }
    if(maxTemp2save != maxTemp2) {
      maxTemp2save = maxTemp2;
      preferences.putFloat("max2", maxTemp2);
    }
  }

  // Premapovani rozsahu cidla na rozsah serva
  angle = map(temperature, minTemp, maxTemp, 0, 90);
  angle = constrain(angle, 0, 90);
  myservo.write(angle);

  // Premapovani rozsahu cidla 2 na rozsah serva 2
  angle2 = map(temperature2, minTemp2, maxTemp2, 0, 90);
  angle2 = constrain(angle2, 0, 90);
  myservo2.write(angle2);

  // Pokud se hodnota teploty zmenila, aktualizuj displej
  if (temperature != lastTemperature)
    updateDisplay1();

  if (temperature2 != lastTemperature2)
    updateDisplay2();

  // Precti enkoder a pokud se zmenil uprav promennou dle aktualniho menu
  currentStateCLK = digitalRead(ENCODER_CLK);

  if (currentStateCLK == 1 && currentStateCLK != lastStateCLK) {

    // na kterou stranu se enkoder pohnul:
    if (digitalRead(ENCODER_DT) != currentStateCLK) {
      if (!menuOption) minTemp = minTemp - 0.5;
      else maxTemp = maxTemp - 0.5;
    } else {
      if (!menuOption) minTemp = minTemp + 0.5;
      else maxTemp = maxTemp + 0.5;
    }

    // akutalizuj displej
    updateDisplay1();
  }

  // Zapamatuj si aktualni stav enkoderu
  lastStateCLK = currentStateCLK;

  // Precti enkoder 2 a pokud se zmenil uprav promennou dle aktualniho menu
  currentStateCLK2 = digitalRead(ENCODER2_CLK);

  if (currentStateCLK2 == 1 && currentStateCLK2 != lastStateCLK2) {

    // na kterou stranu se enkoder 2 pohnul:
    if (digitalRead(ENCODER2_DT) != currentStateCLK2) {
      if (!menuOption2) minTemp2 = minTemp2 - 0.5;
      else maxTemp2 = maxTemp2 - 0.5;
    } else {
      if (!menuOption2) minTemp2 = minTemp2 + 0.5;
      else maxTemp2 = maxTemp2 + 0.5;
    }

    // akutalizuj displej
    updateDisplay2();
  }

  // Zapamatuj si aktualni stav enkoderu
  lastStateCLK2 = currentStateCLK2;

  // Precti aktualni stav tlacitka
  int btnState = digitalRead(ENCODER_SW);

  // Pokud je zmacknuto a nebylo zmacknuti zaregistrovano v poslednich 50 ms, proved zmenu menu a aktualizuj displej
  if (btnState == LOW) {
    if (millis() - lastButtonPress > 50) {
      menuOption = !menuOption;
      updateDisplay1();
    }

    // Zapamatuj si kdy bylo tlacitko zmacknuto
    lastButtonPress = millis();
  }

  // Precti aktualni stav tlacitka
  int btnState2 = digitalRead(ENCODER2_SW);

  // Pokud je zmacknuto a nebylo zmacknuti zaregistrovano v poslednich 50 ms, proved zmenu menu a aktualizuj displej
  if (btnState2 == LOW) {
    if (millis() - lastButtonPress2 > 50) {
      menuOption2 = !menuOption2;
      updateDisplay2();
    }

    // Zapamatuj si kdy bylo tlacitko zmacknuto
    lastButtonPress2 = millis();
  }

  // Delay pro debouncing enkoderu
  delay(1);
}

// Funkce pro napsani udaju na displej vcetne akutalniho menu
void updateDisplay1() {
  lastTemperature = temperature;

  lcd1.setCursor(0, 0);
  lcd1.print("Teplota: ");
  lcd1.print(temperature, 1);
  lcd1.write(byte(0));
  lcd1.print("C ");

  lcd1.setCursor(0, 1);
  lcd1.print("Uhel serva: ");
  lcd1.print(angle, 1);
  lcd1.write(byte(0));
  lcd1.print("  ");

  lcd1.setCursor(0, 2);
  if (menuOption == 0) lcd1.print(">Min: ");
  else lcd1.print(" Min: ");
  lcd1.print(minTemp, 1);
  lcd1.write(byte(0));
  lcd1.print("C ");

  lcd1.setCursor(0, 3);
  if (menuOption == 1) lcd1.print(">Max: ");
  else lcd1.print(" Max: ");
  lcd1.print(maxTemp, 1);
  lcd1.write(byte(0));
  lcd1.print("C ");
}

// Funkce pro napsani udaju na druhy displej vcetne akutalniho menu
void updateDisplay2() {
  lastTemperature2 = temperature2;

  lcd2.setCursor(0, 0);
  lcd2.print("Teplota: ");
  lcd2.print(temperature2, 1);
  lcd2.write(byte(0));
  lcd2.print("C ");

  lcd2.setCursor(0, 1);
  lcd2.print("Uhel serva: ");
  lcd2.print(angle2, 1);
  lcd2.write(byte(0));
  lcd2.print("  ");

  lcd2.setCursor(0, 2);
  if (menuOption2 == 0) lcd2.print(">Min: ");
  else lcd2.print(" Min: ");
  lcd2.print(minTemp2, 1);
  lcd2.write(byte(0));
  lcd2.print("C ");

  lcd2.setCursor(0, 3);
  if (menuOption2 == 1) lcd2.print(">Max: ");
  else lcd2.print(" Max: ");
  lcd2.print(maxTemp2, 1);
  lcd2.write(byte(0));
  lcd2.print("C ");
}