// Screen imports
#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
#define TFT_DC 9
#define TFT_CS 10
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);
// Temp sensor imports
#include "DHT.h"
#define DHTPIN 7
#define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321
DHT dht(DHTPIN, DHTTYPE);
#include <stdint.h> // Enables Intellisense for uin16_t types
// global hardware constants
const int pumpPin = 3;
const int lowFanPin = 2;
const int highFanPin = 5;
const int lowHighSwitchPin = 6;
const int pumpSwitchPin = 4;
// global display constants
const int temperatureTextY = 5;
const int temperatureY = 26;
const int statusTextX = 5;
const int statusValueX = 180;
const int setPointY = 130;
const int humidityY = 200;
const int indicatorY = 265;
const int lowFanIndicatorX = 8;
const int highFanIndicatorX = 88;
const int pumpIndicatorX = 173;
const int statusBorderWidth = 4;
// Function to print any number of variables
template <typename T>
void printMe(T arg)
{
Serial.println(arg);
}
template <typename T, typename... Args>
void printMe(T arg, Args... args)
{
Serial.print(arg);
printMe(args...); // Recursive call to print the remaining variables
}
class Thermostat
{
private:
// motor state variables
bool pumpIsOn;
bool lowFanIsOn;
bool highFanIsOn;
// sensor variables
float tempF;
int humidity;
int setPoint;
// temperature thresholds
float lowTempThreshold;
float highTempThreshold;
int lowHumidityThreshold;
int medHumidityThreshold;
int highHumidityThreshold;
// user intent variables
bool pumpIsSelected;
bool lowFanIsSelected;
// fan delay timer variables
int lowFanDelaySeconds;
public:
// constructor
Thermostat()
{
this->lowTempThreshold = 65;
this->highTempThreshold = 82;
this->lowHumidityThreshold = 15;
this->medHumidityThreshold = 40;
this->highHumidityThreshold = 70;
this->pumpIsSelected = true;
this->lowFanIsSelected = true;
this->lowFanDelaySeconds = 3;
}
// public render methods
// Calculate x value to center text horizontally
int centerTextX(int textLength, int textSize)
{
int screenWidth = tft.width();
int textWidth = textLength * textSize * 6;
return (screenWidth - textWidth) / 2;
}
// Renders "Temperature" text centered 5px from top
void renderTemperatureText()
{
String temperatureText = "Temperature";
int len = temperatureText.length();
int textSize = 2;
int x = centerTextX(len, textSize);
int y = temperatureTextY;
tft.setCursor(x, y);
tft.setTextColor(ILI9341_LIGHTGREY, ILI9341_BLACK);
tft.setTextSize(textSize);
tft.println(temperatureText);
}
// Renders "Humidity" left justified
void renderHumidityText()
{
String humidityText = "Humidity";
int textSize = 2;
int x = statusTextX;
int y = humidityY;
tft.setCursor(x, y);
tft.setTextColor(ILI9341_LIGHTGREY, ILI9341_BLACK);
tft.setTextSize(textSize);
tft.println(humidityText);
}
// Renders "Set temp" left justified
void renderSetPointText()
{
int textSize = 2;
String setPointText = "Set temp";
int x = statusTextX;
int y = setPointY;
tft.setCursor(x, y);
tft.setTextColor(ILI9341_LIGHTGREY, ILI9341_BLACK);
tft.setTextSize(textSize);
tft.println(setPointText);
}
// dynamic render methods
// Renders color-coded low fan status indicator
void renderL()
{
tft.setTextColor(ILI9341_LIGHTGREY);
if (lowFanIsOn)
{
tft.setTextColor(ILI9341_GREEN);
}
tft.setCursor(lowFanIndicatorX, indicatorY);
tft.setTextSize(6);
tft.print("L");
}
// Renders color-coded high fan status indicator
void renderH()
{
tft.setTextColor(ILI9341_LIGHTGREY);
if (highFanIsOn)
{
tft.setTextColor(ILI9341_GREEN);
}
tft.setCursor(highFanIndicatorX, indicatorY);
tft.setTextSize(6);
tft.print("H");
}
// Renders color-coded pump status indicator
void renderP()
{
tft.setTextColor(ILI9341_LIGHTGREY);
if (pumpIsOn)
{
tft.setTextColor(ILI9341_GREEN);
}
tft.setCursor(pumpIndicatorX, indicatorY);
tft.setTextSize(6);
tft.print("P");
}
// Renders color-coded status box around selected indicator
void renderSelectionBox(int x, uint16_t color = ILI9341_YELLOW)
{
int textSize = 6;
int statusBorderGap = 5;
int oneBorder = statusBorderWidth + statusBorderGap;
int letterWidth = textSize * 6;
int letterHeight = textSize * 8;
int boxWidth = letterWidth + oneBorder * 2 + statusBorderWidth;
int bottomY = indicatorY + letterHeight;
int leftX = x - oneBorder;
int rightX = x + letterWidth + oneBorder;
int topY = indicatorY - oneBorder;
tft.fillRect(leftX, topY, boxWidth, statusBorderWidth, color); // top
tft.fillRect(rightX, topY, statusBorderWidth, boxWidth, color); // right
tft.fillRect(leftX, bottomY, boxWidth, statusBorderWidth, color); // bottom
tft.fillRect(leftX, topY, statusBorderWidth, boxWidth, color); // left
}
// Renders color-coded temperatureF centered 26px from top
void renderTemperature()
{
int temperature = this->tempF;
String tempString = String(temperature);
// pad with spaces to ensure prev val doesn't leave a ghost
while (tempString.length() < 4)
{
tempString = " " + tempString + " ";
}
int len = tempString.length();
int textSize = 8;
int y = 26;
int x = centerTextX(len, textSize);
tft.setCursor(x, y);
tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
if (temperature < this->lowTempThreshold)
{
tft.setTextColor(ILI9341_BLUE, ILI9341_BLACK);
}
else if (temperature > this->setPoint)
{
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
}
else if (temperature > this->highTempThreshold)
{
tft.setTextColor(ILI9341_RED, ILI9341_BLACK);
}
tft.setTextSize(textSize);
tft.println(tempString);
}
// Renders color-coded humidity value right justified at the same y as "Humidity"
void renderHumidity()
{
int humidity = this->humidity;
String humidityString = String(humidity);
// pad with spaces to overwrite render artifacts from previous value
while (humidityString.length() < 3)
{
humidityString = " " + humidityString;
}
int humidityStringLength = 3;
int textSize = 2;
int y = 200;
int letterWidthPixels = 6;
// x is right justified
int x = tft.width() - (humidityStringLength * textSize * letterWidthPixels) - 5;
tft.setCursor(x, y);
tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
if (humidity < this->lowHumidityThreshold)
{
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
}
else if (humidity > this->highHumidityThreshold)
{
tft.setTextColor(ILI9341_RED, ILI9341_BLACK);
}
tft.setTextSize(textSize);
tft.println(humidityString);
}
// Renders Setpoint value right justified at the same y as "Set temp"
void renderSetPoint()
{
int setPoint = this->setPoint;
String setPointString = String(setPoint);
// pad with spaces to overwrite render artifacts from previous value
while (setPointString.length() < 3)
{
setPointString = " " + setPointString;
}
int setPointLength = 3;
int letterWidthPixels = 6;
int textSize = 2;
int x = tft.width() - (setPointLength * textSize * letterWidthPixels) - 5;
int y = setPointY;
tft.setCursor(x, y);
tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
if (setPoint < this->lowTempThreshold)
{
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
}
else if (setPoint > this->highTempThreshold)
{
tft.setTextColor(ILI9341_RED, ILI9341_BLACK);
}
tft.setTextSize(textSize);
tft.println(setPointString);
}
// Renders "..." animation to indicate delay is ongoing
void renderDelayAnimation(bool shouldReset = false)
{
static int count = 0;
static int prevTime = 0;
// Above the selected fan indicator, render count number of "."
// only render a new "." every 500ms
// render a maximum of 3 ".", then reset back to 1
// reset count to 0 when delay is complete
// if shouldReset, reset count to 0 and render " " to reset UI
if (shouldReset)
{
int activeFanIndicatorX = lowFanIsSelected ? lowFanIndicatorX : highFanIndicatorX;
int inactiveFanIndicatorX = !lowFanIsSelected ? lowFanIndicatorX : highFanIndicatorX;
count = 0;
prevTime = 0;
renderSelectionBox(activeFanIndicatorX);
renderSelectionBox(inactiveFanIndicatorX, ILI9341_BLACK);
return;
}
int currentTime = millis();
if (currentTime - prevTime > 500)
{
prevTime = currentTime;
count++;
if (count > 3)
{
count = 1;
renderDelayAnimation(true);
renderDelayAnimation();
}
if (this->lowFanIsSelected)
{
tft.setCursor(lowFanIndicatorX, indicatorY - 19);
}
else
{
tft.setCursor(highFanIndicatorX, indicatorY - 19);
}
tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
tft.setTextSize(2);
for (int i = 0; i < count; i++)
{
tft.print(".");
}
}
}
// public action methods
// Updates inputs and calls render functions only on updated elements
void updateInputs()
{
int newTempF = dht.readTemperature(true);
int newHumidity = dht.readHumidity();
int newSetPoint = map(analogRead(A0), 0, 1023, 65, 85);
// update and render only if new
if (newTempF != this->tempF)
{
this->tempF = newTempF;
this->renderTemperature();
}
if (newHumidity != this->humidity)
{
this->humidity = newHumidity;
this->renderHumidity();
}
if (newSetPoint != this->setPoint)
{
this->setPoint = newSetPoint;
this->renderTemperature();
this->renderSetPoint();
}
}
// Turns pump on if selected
void turnPumpOn()
{
if (pumpIsSelected)
{
digitalWrite(pumpPin, HIGH);
this->pumpIsOn = true;
this->renderP();
}
}
// Turns pump off
void turnPumpOff()
{
digitalWrite(pumpPin, LOW);
this->pumpIsOn = false;
this->renderP();
}
// Turns low fan on if selected
void turnLowFanOn()
{
if (lowFanIsSelected)
{
digitalWrite(highFanPin, LOW);
this->highFanIsOn = false;
digitalWrite(lowFanPin, HIGH);
this->lowFanIsOn = true;
this->renderL();
this->renderH();
}
}
// Turns low fan off
void turnLowFanOff()
{
digitalWrite(lowFanPin, LOW);
this->lowFanIsOn = false;
this->renderL();
}
// Turns high fan on if selected
void turnHighFanOn()
{
if (!lowFanIsSelected)
{
digitalWrite(lowFanPin, LOW);
this->lowFanIsOn = false;
digitalWrite(highFanPin, HIGH);
this->highFanIsOn = true;
this->renderH();
this->renderL();
}
}
// Turns high fan off
void turnHighFanOff()
{
digitalWrite(highFanPin, LOW);
this->highFanIsOn = false;
this->renderH();
}
// Turns cooler on
void turnCoolerOn()
{
if (pumpIsSelected)
{
this->turnPumpOn();
}
pumpTimer();
}
// Turns cooler off
void turnCoolerOff()
{
this->turnPumpOff();
this->turnLowFanOff();
this->turnHighFanOff();
}
// Turn vent on
void turnVentOn()
{
this->turnPumpOff();
if (lowFanIsSelected)
{
this->turnLowFanOn();
}
else
{
this->turnHighFanOn();
}
}
// Check current temperature and humidity and turn on/off cooler/vent
void checkConditions()
{
// if humidity is too high, turn on vent instead of cooler
if (tempF > setPoint + 2 && humidity < highHumidityThreshold)
{
this->turnCoolerOn();
}
else if (tempF > setPoint)
{
this->turnVentOn();
}
else
{
this->turnCoolerOff();
}
}
// Debounced latch for low/high switch
void debouncedLowIsSelectedLatch()
{
// if switch is pressed three consecutive calls
// new state is set on falling pressed
// signal and static variables are reset
static bool pressedOnce = false;
static bool pressedTwice = false;
// reverse read state due to INPUT_PULLUP
bool switchIsPressed = !digitalRead(lowHighSwitchPin);
// set new selection on falling signal
if (!switchIsPressed && pressedOnce && pressedTwice)
{
// set new state
this->lowFanIsSelected = !this->lowFanIsSelected;
// ensure correct fan is on
if (lowFanIsSelected && highFanIsOn)
{
this->turnLowFanOn();
}
else if (!lowFanIsSelected && lowFanIsOn)
{
this->turnHighFanOn();
}
// determine active and inactive colors
int activeFanIndicatorX = lowFanIsSelected ? lowFanIndicatorX : highFanIndicatorX;
int inactiveFanIndicatorX = !lowFanIsSelected ? lowFanIndicatorX : highFanIndicatorX;
// rerender both status indicators to ensure UI aligns with state
renderSelectionBox(activeFanIndicatorX);
renderSelectionBox(inactiveFanIndicatorX, ILI9341_BLACK);
// reset local static variables
pressedOnce = false;
pressedTwice = false;
}
else if (switchIsPressed && !pressedOnce)
{
pressedOnce = true;
}
else if (switchIsPressed && pressedOnce && !pressedTwice)
{
pressedTwice = true;
}
}
// Debounced latch for pump switch
void debouncedPumpIsSelectedLatch()
{
static bool pressedOnce = false;
static bool pressedTwice = false;
// reverse read state due to INPUT_PULLUP
bool switchIsPressed = !digitalRead(pumpSwitchPin);
if (!switchIsPressed && pressedOnce && pressedTwice)
{
// set new state
this->pumpIsSelected = !this->pumpIsSelected;
if (!this->pumpIsSelected)
{
this->turnPumpOff();
}
// determine color
uint16_t color = pumpIsSelected ? ILI9341_YELLOW : ILI9341_BLACK;
// render status indicator
renderSelectionBox(pumpIndicatorX, color);
// reset local static variables
pressedOnce = false;
pressedTwice = false;
}
else if (switchIsPressed && !pressedOnce)
{
pressedOnce = true;
}
else if (switchIsPressed && pressedOnce && !pressedTwice)
{
pressedTwice = true;
}
}
// create a timer that will allow the pump to run for 5 seconds before turning the selected fan on
void pumpTimer()
{
static int time = 0;
// do nothing if pump is not on and neither fan is on
if (pumpIsOn && !lowFanIsOn && !highFanIsOn)
{
// if timer is unset, begin timer by setting it and exit early
if (!time)
{
time = millis();
return;
}
// if timer is set and timer has expired, execute callback and reset timer
if (time && (millis() - time) / 1000 > lowFanDelaySeconds)
{
bool shouldReset = true;
if (lowFanIsSelected)
{
this->turnLowFanOn();
renderDelayAnimation(shouldReset);
time = 0;
}
else
{
this->turnHighFanOn();
renderDelayAnimation(shouldReset);
time = 0;
}
}
else
{
renderDelayAnimation();
}
}
}
void init()
{
tft.begin();
dht.begin();
Serial.begin(115200);
printMe("init");
pinMode(pumpPin, OUTPUT);
pinMode(lowFanPin, OUTPUT);
pinMode(highFanPin, OUTPUT);
pinMode(lowHighSwitchPin, INPUT_PULLUP);
pinMode(pumpSwitchPin, INPUT_PULLUP);
pinMode(A0, INPUT_PULLUP);
renderTemperatureText();
renderHumidityText();
renderSetPointText();
renderL();
renderH();
renderP();
renderSelectionBox(lowFanIndicatorX);
renderSelectionBox(pumpIndicatorX);
}
};
Thermostat thermostat;
void setup()
{
thermostat.init();
}
void loop()
{
thermostat.updateInputs();
thermostat.debouncedLowIsSelectedLatch();
thermostat.debouncedPumpIsSelectedLatch();
thermostat.checkConditions();
delay(10);
}