// 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