// Description
  // The program controls a heating system using a temperature sensor and a timer.
  // The user can set the target temperature and timer duration via the rotary encoder.
  // A 16x2 LCD display shows the current temperature, target temperature, and timer countdown.
  // The 5V relay module switches the heater based on the temperature and a hysteresis margin.
  // LEDs indicate the system and heater status.
  // Safety features include error handling to prevent overheating by shutting off the system if the maximum temperature is exceeded.

  // Components Overview:

  // Arduino Uno R3 – Controls the system.
  // TMP36 Temperature Sensor – Measures temperature.
  // KY-050 Rotary Encoder – Navigates and adjusts settings.
  // 16x2 I2C LCD Display – Displays temperature and timer info.
  // 5V Relay Module – Switches the heater on/off.
  // Red LED (220 Ω resistor) – Indicates heater status.
  // Green LED (150 Ω resistor) – Indicates system status.

  // 08.01.2025 / Elvin Germann
  
//Declarations #################################################################################################
  #include <LiquidCrystal_I2C.h>
  LiquidCrystal_I2C lcd(0x27, 16, 2);  // LCD-adress and size (16x2)

  // Pins
  int encoderSW = 3;               //DT-Pin
  int encoderCLK = 4;              //CLK-Pin
  int encoderDT = 5;               //DT-Pin
  int systemLED = 11;             // System running / enabled LED
  int heaterLED = 12;             // Heater active LED
  int heaterRelais = 13;          // Heater relais
  const byte tempSensor = A0;     // Temperature sensor

  int encoder_lastCLK = HIGH;
  int encoder_newCLK = LOW;
  int encoder_dtValue = LOW;

  int displayState = 0;      // display-state counter
  int maxDisplayState = 3;  // max. display states

  float T_actual = 0;
  float T_prev = 0;
  float T_target = 40;  
  float T_max = 85;  // max. temperature 

  float hysteresis = 5;       // -------------------------------------------- adjust to system if necessary
  float alpha = 0.01;
  bool heaterOn = false;
  bool systemEnable = false;

  long lastButtonPressTime = 0;  
  long inactivityDuration = 5000;  

  long lastLCDUpdate = 0;   
  long lcdUpdateInterval = 100;

  bool lastButtonState = LOW;
  bool currentButtonState = LOW;

  long timerSeconds = 0;
  long timerMinutes = 0;
  long timerHours = 0;
  long timerStartTime = 0;    
  long timerDuration = 0; 
  long elapsedTime = 0;
  long remainingTime = 0;
  long pausedTime = 0;  
  bool timerPaused = false;            
  bool timerRunning = false;       
  bool timerElapsed = false;

  bool targetTempEditing = false;  
  bool timerEditing = false;  
  bool manualHeater = false;
  bool errorMode = false;
  int editField = 0; // 0 = hours, 1 = minutes, 2 = seconds

//Setup ########################################################################################################
  void setup() {

  // Pin declaration
    pinMode(encoderCLK, INPUT);
    pinMode(encoderDT, INPUT);
    pinMode(systemLED, OUTPUT);
    pinMode(heaterLED, OUTPUT);
    pinMode(heaterRelais, OUTPUT);
    pinMode(encoderSW, INPUT_PULLUP);
    
    // activate interrupt ( ISR, FALLING-Flanke)
    attachInterrupt(digitalPinToInterrupt(encoderSW), buttonISR, FALLING);
 
    lcd.init();
    lcd.clear();
    lcd.backlight();
    getRemainingTime();
    handleHeater();
    updateLCD();
 }

//Loop #########################################################################################################

  void loop() {
    // Check inactivity
    if (millis() - lastButtonPressTime > inactivityDuration && !targetTempEditing && !timerEditing && !manualHeater && !errorMode) {
      if (displayState != 0) {displayState = 0, updateLCD();}
    }

    // Update display time
    if (millis() - lastLCDUpdate >= lcdUpdateInterval && displayState == 0) {
      lastLCDUpdate = millis();
      getRemainingTime();
      updateLCD();
    }

    // Modes
    if (targetTempEditing && !errorMode) {
      adjustTargetTemperature();  // edit target temperature
    } else if (timerEditing && !errorMode) {
        adjustTimer();            // edit timer
    } else if (manualHeater && !errorMode) {
      manualHeaterOn();           // activate heater manual
    } else if (errorMode) {
      errorModeOn();              // error occurred
    } else {
      handleEncoder();            // switch between display states
    }

    // Check button
    if (lastButtonState) {
      handleButton();
      lastButtonState = LOW;  // Zustand zurücksetzen
    }

    // Activate heating
    handleHeater();   
  }

//Functions ####################################################################################################

  //-------------------------------Timer functions----------------------------
  void startTimer() {
    if (timerPaused) {
      timerStartTime = millis() - pausedTime;
      timerPaused = false;
    } else {
      timerStartTime = millis();
    }
    timerRunning = true;
  }

  void pauseTimer() {
    if (timerRunning && !timerPaused) {
      pausedTime = millis() - timerStartTime;  // Verstrichene Zeit speichern
      timerPaused = true;
      timerRunning = false;
    }
  }

  void resetTimer(unsigned long newDuration) {
    timerRunning = false;
    timerPaused = false;
    timerElapsed = false;
    timerDuration = newDuration;  // Neue Dauer setzen
    pausedTime = 0;
  }

  void getRemainingTime() {
    if (!timerRunning && !timerPaused) {
      timerHours = timerDuration / 3600000;
      timerMinutes = (timerDuration % 3600000) / 60000;
      timerSeconds = (timerDuration % 60000) / 1000;
      return;
    }

      elapsedTime = timerPaused ? pausedTime : millis() - timerStartTime;

    if (elapsedTime >= timerDuration) {
      timerRunning = false;  // Timer stop
      timerPaused = false;
      systemEnable = false;  // Deactivate system once timer runs out
      heaterOn = false;  // Turn off heater when timer expires
      timerDuration = 0;
      timerHours = 0;
      timerMinutes = 0;
      timerSeconds = 0;
      timerElapsed = true;
      return;
    }

    remainingTime = timerDuration - elapsedTime;
    timerHours = remainingTime / 3600000;
    timerMinutes = (remainingTime % 3600000) / 60000;
    timerSeconds = (remainingTime % 60000) / 1000;
  }

  //-------------------------------Encoder function----------------------------
  void handleEncoder() {
    encoder_newCLK = digitalRead(encoderCLK);
    if (encoder_newCLK != encoder_lastCLK) {
      // detect change on the CLK pin
      encoder_lastCLK = encoder_newCLK;
      encoder_dtValue = digitalRead(encoderDT);
      if (encoder_newCLK == LOW && encoder_dtValue == HIGH) {
        displayState++;
        if (displayState > maxDisplayState) displayState = 0;  // Wrap-around
          lcd.clear();
          updateLCD();
        }  
      if (encoder_newCLK == LOW && encoder_dtValue == LOW) {
        displayState--;
        if (displayState < 0) displayState = maxDisplayState;  // Wrap-around
          lcd.clear();
          updateLCD();
        }

      lastButtonPressTime = millis();
    }
  }

  //-------------------------------Display functions----------------------------
  void updateLCD() {
      switch (displayState) {
          case 0:
              lcd.setCursor(0, 0);
              lcd.print("Temp.: ");
              lcd.print(T_actual, 0);
              lcd.print("\xDF\C");
              lcd.print("/");
              lcd.print(T_target, 0);
              lcd.print("\xDF\C   ");
              lcd.setCursor(0, 1);
              lcd.print("Timer:  ");
              if (timerHours < 10) lcd.print('0');
              lcd.print(timerHours); // Platzhalter für den Timer
              lcd.print(":");
              if (timerMinutes < 10) lcd.print('0');
              lcd.print(timerMinutes); // Platzhalter für den Timer
              lcd.print(":");
              if (timerSeconds < 10) lcd.print('0');
              lcd.print(timerSeconds); // Platzhalter für den Timer
              break;

          case 1:
              lcd.setCursor(0, 0);
              lcd.print("Set Timer:");
              lcd.setCursor(0, 1);

              // Stunden
              if (timerHours < 10) lcd.print('0');
              lcd.print(timerHours);
              lcd.print(":");

              // Minuten
              if (timerMinutes < 10) lcd.print('0');
              lcd.print(timerMinutes);
              lcd.print(":");

              // Sekunden
              if (timerSeconds < 10) lcd.print('0');
              lcd.print(timerSeconds);
              break;

          case 2:
              lcd.setCursor(0, 0);
              lcd.print("Set Temperature:");
              lcd.setCursor(0, 1);
              lcd.print(T_target,0);
              lcd.print("\xDF\C   ");
              lcd.setCursor(0, 1);
              break;

          case 3:
              lcd.setCursor(0, 0);
              lcd.print("Manual heater:");
              lcd.setCursor(0, 1);
              lcd.print("OFF");
              lcd.setCursor(0, 1);
              break;

          case 99:
              lcd.setCursor(0, 0);
              lcd.print("ERROR:           ");
              lcd.setCursor(0, 1);
              lcd.print("MAX. TEMPERATURE");
              lcd.setCursor(0, 1);
              break;

          default:
              lcd.print("Unknown State");
              break;
      }
  }

  //-------------------------------Button functions----------------------------
  void handleButton() {
    currentButtonState = digitalRead(encoderSW);

    if (lastButtonState == HIGH && currentButtonState == LOW) {
      lastButtonPressTime = millis();  // Timer zurücksetzen

        if (displayState == 0) {
          // System ein-/ausschalten
          if (!systemEnable && timerDuration > 0) {  // Nur aktivieren, wenn Timer > 0
            if (timerElapsed) {
              timerElapsed = false;  // Zurücksetzen, wenn der Timer abgelaufen ist
              resetTimer(timerDuration);  // Timer zurücksetzen
            }
            systemEnable = true;  // System aktivieren (kurz)
            startTimer();  // Timer neu starten
          } else if (!timerElapsed) {
            systemEnable = false;  // Wenn der Timer noch läuft, dann stoppen
            pauseTimer();
          }
        }
  
      if (displayState == 1) {
        // Timer zurücksetzen
        if (!timerEditing) {
        timerEditing = true;
        editField = 1;
        lcd.setCursor(1, 1);
        lcd.blink();
        }
        else if (timerEditing && editField == 1) {
          editField = 2;
          lcd.setCursor(4, 1);
          lcd.blink();
        }
        else if (timerEditing && editField == 2) {
          editField = 3;
          lcd.setCursor(7, 1);
          lcd.blink();
        }
        else {
          editField = 0;
          lcd.setCursor(0, 1);
          lcd.noBlink();  // Cursor blinken ausschalten
          timerEditing = false;
        }
      }

      if (displayState == 2) {
        // Umschalten zwischen Bearbeitungsmodus und Normalmodus
        targetTempEditing = !targetTempEditing;

        // Im Bearbeitungsmodus: Encoder steuert die Zieltemperatur
        if (targetTempEditing) {
          lcd.blink();  // Cursor blinken einschalten
          lcd.setCursor(0, 1);  // Cursor auf die Zeile mit der Zieltemperatur setzen
        }
        else {
          resetTimer(timerDuration);
          lcd.noBlink();  // Cursor blinken ausschalten
        }
      }

        if (displayState == 3) {
        // Umschalten zwischen Bearbeitungsmodus und Normalmodus
        manualHeater = !manualHeater;
        if (manualHeater) {
          lcd.blink();  // Cursor blinken einschalten
          lcd.setCursor(0, 1);  // Cursor auf die Zeile mit der Zieltemperatur setzen
          lcd.print("ON ");
          lcd.setCursor(0, 1);
        } 
        else {
          lcd.setCursor(0, 1);
          lcd.print("OFF");
          lcd.setCursor(0, 1);
          lcd.noBlink();  // Cursor blinken ausschalten
        }
      }

    if (displayState == 99) {
          // Umschalten zwischen Bearbeitungsmodus und Normalmodus
          if (T_actual <= T_max) {
          errorMode = false;
          displayState = 0;
          updateLCD();
          }
        }
      
    }

    lastButtonState = currentButtonState;
  }

  void buttonISR() {
    static unsigned long lastInterruptTime = 0;
    unsigned long interruptTime = millis();
    
    // Debouncing: Nur akzeptieren, wenn genug Zeit seit dem letzten Interrupt vergangen ist
    if (interruptTime - lastInterruptTime > 200) {
      // Zustand speichern, um im Hauptprogramm auszuwerten
      lastButtonState = !lastButtonState;  
      lastButtonPressTime = interruptTime; // Inaktivitäts-Timer zurücksetzen
    }
    lastInterruptTime = interruptTime;
  }

  //-------------------------------Heater function----------------------------
  void handleHeater() {
    // Lese die Temperatur
    int T_measurement = analogRead(tempSensor);
    T_actual = round(1 / (log(1 / (1023.0 / T_measurement - 1)) / 3950 + 1.0 / 298.15) - 273.15); //Sensor calculation  // -------------------------------------------- adjust to sensor if necessary

    // Überprüfe, ob die Temperatur den maximalen Wert überschreitet
    if (T_actual > T_max) {
      errorMode = true;
      systemEnable = false; // Deaktiviere das System
      heaterOn = false;  // Heizung ausschalten
      digitalWrite(systemLED, LOW);  // Schalte den Heizungs-Pin aus
      digitalWrite(heaterLED, LOW);  // Schalte den Heizungs-Pin aus
      digitalWrite(heaterRelais, LOW);  // Schalte den Heizungs-Pin aus
      updateLCD();  // Aktualisiere das Display im Fehlerzustand
      return;
    }

    // Überprüfen, ob das System aktiviert ist
    if (systemEnable && timerDuration > 0) {  // Heizung nur aktiv, wenn Timer > 0
      // Wenn der Timer abgelaufen ist, Heizung sofort ausschalten
      if (remainingTime == 0 && timerRunning) {
        heaterOn = false;  // Heizung ausschalten, wenn der Timer abgelaufen ist
      }
      // Wenn der Timer noch läuft, Temperaturregelung mit Hysterese durchführen
      else if (timerRunning) {
        if (T_actual <= T_target - hysteresis && !heaterOn) {
          heaterOn = true;
        } else if (T_actual >= T_target + hysteresis) {
          heaterOn = false;
        }
      }
    } else {
      heaterOn = false;  // Heizung ausschalten, wenn der Timer abgelaufen ist
    }

    // Schalte die Heizung entsprechend
    digitalWrite(systemLED, systemEnable ? HIGH : LOW);
    digitalWrite(heaterLED, heaterOn ? HIGH : LOW);
    digitalWrite(heaterRelais, heaterOn ? HIGH : LOW);
  }

  //-------------------------------Mode functions----------------------------
  void adjustTargetTemperature() {
    // Encoder-Werte lesen
    encoder_newCLK = digitalRead(encoderCLK);
    if (encoder_newCLK != encoder_lastCLK && encoder_newCLK == HIGH) {  // Prüfen, ob der Zustand geändert wurde und auf steigende Flanke reagieren
      encoder_dtValue = digitalRead(encoderDT);

      // Encoder-Drehrichtung bestimmen
      int increment = (encoder_dtValue == HIGH) ? -1 : 1;

      // Zieltemperatur ändern
      T_target += increment;
      if (T_target < 20) T_target = 20;        // Untere Grenze
      if (T_target > 80) T_target = 80;      // Obere Grenze

      // LCD aktualisieren, um die Änderungen anzuzeigen
      lcd.setCursor(0, 1);
      lcd.print(T_target, 0);    // Zieltemperatur anzeigen
      lcd.setCursor(0, 1);
    }

    encoder_lastCLK = encoder_newCLK;  // Den letzten Zustand aktualisieren
  }

  void manualHeaterOn() {
    // Encoder-Werte lesen

    if (manualHeater) {
      digitalWrite(heaterLED, HIGH);
      digitalWrite(heaterRelais, HIGH);
      }
    else {    
      digitalWrite(heaterLED, LOW);
      digitalWrite(heaterRelais, LOW);
    }
  }

  void adjustTimer() {
    encoder_newCLK = digitalRead(encoderCLK);
    if (encoder_newCLK != encoder_lastCLK && encoder_newCLK == HIGH) {
      encoder_dtValue = digitalRead(encoderDT);
      int increment = (encoder_dtValue == HIGH) ? -1 : 1;

      if (editField == 1) { // Stunden einstellen
        timerHours += increment;
        if (timerHours < 0) timerHours = 0;
        if (timerHours > 99) timerHours = 99;
        lcd.setCursor(0, 1);
        if (timerHours < 10) lcd.print('0');
        lcd.print(timerHours);      
        lcd.setCursor(1, 1);
          }
      else if (editField == 2) { // Minuten einstellen
        timerMinutes += increment;
        if (timerMinutes < 0) timerMinutes = 0;
        if (timerMinutes > 59) timerMinutes = 59;
        lcd.setCursor(3, 1);
        if (timerMinutes < 10) lcd.print('0');
        lcd.print(timerMinutes);      
        lcd.setCursor(4, 1);
      } 
      else if (editField == 3) { // Sekunden einstellen
        timerSeconds += increment;
        if (timerSeconds < 0) timerSeconds = 0;
        if (timerSeconds > 59) timerSeconds = 59;
        lcd.setCursor(6, 1);
        if (timerSeconds < 10) lcd.print('0');
        lcd.print(timerSeconds);      
        lcd.setCursor(7, 1);
      }

      // Timer-Dauer neu berechnen
      timerDuration = (timerHours * 3600000L) + (timerMinutes * 60000L) + (timerSeconds * 1000L);
 
    }
    encoder_lastCLK = encoder_newCLK;
  }

  void errorModeOn() {
      displayState = 99; 
      digitalWrite(heaterLED, LOW);
      digitalWrite(heaterRelais, LOW);  
      systemEnable = false;   
      heaterOn = false;       
      timerRunning = false;  
      timerElapsed = false;   
      timerPaused = false;    
      manualHeater = false;    
      lcd.noBlink();
      updateLCD();          
  }
uno:A5.2
uno:A4.2
uno:AREF
uno:GND.1
uno:13
uno:12
uno:11
uno:10
uno:9
uno:8
uno:7
uno:6
uno:5
uno:4
uno:3
uno:2
uno:1
uno:0
uno:IOREF
uno:RESET
uno:3.3V
uno:5V
uno:GND.2
uno:GND.3
uno:VIN
uno:A0
uno:A1
uno:A2
uno:A3
uno:A4
uno:A5
encoder1:CLK
encoder1:DT
encoder1:SW
encoder1:VCC
encoder1:GND
lcd1:GND
lcd1:VCC
lcd1:SDA
lcd1:SCL
ntc1:GND
ntc1:VCC
ntc1:OUT
led1:A
led1:C
bz1:1
bz1:2
gnd1:GND
gnd2:GND
gnd3:GND
vcc1:VCC
gnd4:GND
vcc2:VCC
vcc3:VCC
gnd5:GND
led2:A
led2:C
gnd6:GND
r1:1
r1:2
r2:1
r2:2