#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include "RTClib.h"
#include "DHT.h"
// ============================================================================
// PIN DEFINITIONS
// ============================================================================
// TFT Display
#define TFT_CS 10
#define TFT_DC 8
#define TFT_RST 9
// DHT22 Sensors
#define DHTPIN_E_FAN 2
#define DHTPIN_G_TEMP 3
#define DHTPIN_G_HUM 4
#define DHTTYPE DHT22
// Button
#define BUTTON_PAGE_PIN 7
// ============================================================================
// RELAY OUTPUTS
// ACTIVE LOW
// HIGH = OFF
// LOW = ON
// ============================================================================
#define RELAY_ELECTRONICS_FAN_PIN 23
#define RELAY_HEATER_PIN 25
#define RELAY_COOLER_PIN 27
#define RELAY_INLET_FAN_PIN 29
#define RELAY_EXHAUST_FAN_PIN 31
#define RELAY_HUMIDIFIER_PIN 33
#define RELAY_LIGHT_PIN 35
// ============================================================================
// DISPLAY PAGES
// ============================================================================
enum DisplayPage {
PAGE_1,
PAGE_2,
PAGE_3,
PAGE_4,
PAGE_5,
PAGE_6,
PAGE_7,
PAGE_8
};
// ============================================================================
// STATE MACHINES
// ============================================================================
enum SensorState {
SENSOR_INIT,
SENSOR_IDLE,
SENSOR_READING,
SENSOR_ERROR
};
enum ClockState {
CLOCK_INIT,
CLOCK_UPDATE
};
enum ButtonState {
BTN_IDLE,
BTN_DEBOUNCE,
BTN_PRESSED,
BTN_WAIT_RELEASE
};
enum LightState {
LIGHT_INIT,
LIGHT_EVALUATE
};
// ============================================================================
// HARDWARE OBJECTS
// ============================================================================
Adafruit_ILI9341 tft =
Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
RTC_DS3231 rtc;
DHT electronicsFanSensor(DHTPIN_E_FAN, DHTTYPE);
DHT growTentTempSensor(DHTPIN_G_TEMP, DHTTYPE);
DHT growTentHumSensor(DHTPIN_G_HUM, DHTTYPE);
// ============================================================================
// GLOBAL VARIABLES
// ============================================================================
DisplayPage currentPage = PAGE_1;
DisplayPage lastPage = PAGE_8;
SensorState dhtState = SENSOR_INIT;
ClockState rtcState = CLOCK_INIT;
ButtonState btnState = BTN_IDLE;
LightState lightState = LIGHT_INIT;
DateTime globalNow;
// ============================================================================
// SENSOR DATA STRUCTURE
// ============================================================================
struct SensorData {
float temperature;
float humidity;
bool isValid;
};
SensorData eFanData = {0.0, 0.0, false};
SensorData gTempData = {0.0, 0.0, false};
SensorData gHumData = {0.0, 0.0, false};
// ============================================================================
// TIMING
// ============================================================================
unsigned long lastDisplayMillis = 0;
const uint32_t SENSOR_RTC_INTERVAL = 3;
const unsigned long DISPLAY_INTERVAL = 250;
const unsigned long DEBOUNCE_DELAY = 50;
// ============================================================================
// LIGHT SCHEDULE
// 12/12 CYCLE
// 07:00 ON
// 19:00 OFF
// ============================================================================
const int LIGHT_ON_HOUR = 7;
const int LIGHT_OFF_HOUR = 19;
// ============================================================================
// ELECTRONICS FAN CONTROL
// SENSOR PIN 2
// RELAY PIN 23
// ============================================================================
const float E_FAN_ON_TEMP = 35.0;
const float E_FAN_OFF_TEMP = 33.0;
bool electronicsFanRelayState = false;
// ============================================================================
// FORWARD DECLARATIONS
// ============================================================================
void runClockStateMachine();
void runSensorStateMachine();
void runDisplayStateMachine();
void runButtonStateMachine();
void runLightCycleStateMachine();
void drawPageStaticLayout(DisplayPage page);
void updatePage1Dynamic();
void updatePage2Dynamic();
void updatePage3Dynamic();
void updatePage4Dynamic();
void updatePage5Dynamic();
void updatePage6Dynamic();
void updatePage7Dynamic();
void updatePage8Dynamic();
// ============================================================================
// SETUP
// ============================================================================
void setup() {
Serial.begin(9600);
// --------------------------------------------------------------------------
// BUTTON
// --------------------------------------------------------------------------
pinMode(BUTTON_PAGE_PIN, INPUT_PULLUP);
// --------------------------------------------------------------------------
// RELAYS
// --------------------------------------------------------------------------
pinMode(RELAY_ELECTRONICS_FAN_PIN, OUTPUT);
digitalWrite(RELAY_ELECTRONICS_FAN_PIN, HIGH);
pinMode(RELAY_HEATER_PIN, OUTPUT);
digitalWrite(RELAY_HEATER_PIN, HIGH);
pinMode(RELAY_COOLER_PIN, OUTPUT);
digitalWrite(RELAY_COOLER_PIN, HIGH);
pinMode(RELAY_INLET_FAN_PIN, OUTPUT);
digitalWrite(RELAY_INLET_FAN_PIN, HIGH);
pinMode(RELAY_EXHAUST_FAN_PIN, OUTPUT);
digitalWrite(RELAY_EXHAUST_FAN_PIN, HIGH);
pinMode(RELAY_HUMIDIFIER_PIN, OUTPUT);
digitalWrite(RELAY_HUMIDIFIER_PIN, HIGH);
pinMode(RELAY_LIGHT_PIN, OUTPUT);
digitalWrite(RELAY_LIGHT_PIN, HIGH);
// --------------------------------------------------------------------------
// TFT DISPLAY
// --------------------------------------------------------------------------
tft.begin();
tft.setRotation(0);
tft.fillScreen(ILI9341_BLACK);
// --------------------------------------------------------------------------
// RTC
// --------------------------------------------------------------------------
if (!rtc.begin()) {
while (1);
}
if (rtc.lostPower()) {
rtc.adjust(
DateTime(F(__DATE__), F(__TIME__))
);
}
globalNow = rtc.now();
// --------------------------------------------------------------------------
// DHT22 SENSORS
// --------------------------------------------------------------------------
electronicsFanSensor.begin();
growTentTempSensor.begin();
growTentHumSensor.begin();
}
// ============================================================================
// MAIN LOOP
// ============================================================================
void loop() {
runClockStateMachine();
runSensorStateMachine();
runLightCycleStateMachine();
runButtonStateMachine();
runDisplayStateMachine();
}
// ============================================================================
// CLOCK STATE MACHINE
// ============================================================================
void runClockStateMachine() {
globalNow = rtc.now();
}
// ============================================================================
// SENSOR STATE MACHINE
// ============================================================================
void runSensorStateMachine() {
static unsigned long lastRead = 0;
if (millis() - lastRead >= SENSOR_RTC_INTERVAL * 1000) {
lastRead = millis();
// ------------------------------------------------------------------------
// READ SENSOR VALUES
// ------------------------------------------------------------------------
eFanData.temperature =
electronicsFanSensor.readTemperature();
eFanData.humidity =
electronicsFanSensor.readHumidity();
gTempData.temperature =
growTentTempSensor.readTemperature();
gTempData.humidity =
growTentTempSensor.readHumidity();
gHumData.temperature =
growTentHumSensor.readTemperature();
gHumData.humidity =
growTentHumSensor.readHumidity();
// ------------------------------------------------------------------------
// VALIDATE SENSOR VALUES
// ------------------------------------------------------------------------
eFanData.isValid =
!isnan(eFanData.temperature) &&
!isnan(eFanData.humidity);
gTempData.isValid =
!isnan(gTempData.temperature) &&
!isnan(gTempData.humidity);
gHumData.isValid =
!isnan(gHumData.temperature) &&
!isnan(gHumData.humidity);
// ------------------------------------------------------------------------
// ELECTRONICS FAN RELAY CONTROL
// RELAY PIN 23
// ACTIVE LOW
// ------------------------------------------------------------------------
if (eFanData.isValid) {
// TURN ON AT OR ABOVE 35C
if (!electronicsFanRelayState &&
eFanData.temperature >= E_FAN_ON_TEMP) {
electronicsFanRelayState = true;
}
// TURN OFF AT OR BELOW 33C
else if (electronicsFanRelayState &&
eFanData.temperature <= E_FAN_OFF_TEMP) {
electronicsFanRelayState = false;
}
digitalWrite(
RELAY_ELECTRONICS_FAN_PIN,
electronicsFanRelayState ? LOW : HIGH
);
}
// ------------------------------------------------------------------------
// FAIL SAFE
// SENSOR ERROR = RELAY OFF
// ------------------------------------------------------------------------
else {
electronicsFanRelayState = false;
digitalWrite(
RELAY_ELECTRONICS_FAN_PIN,
HIGH
);
}
}
}
// ============================================================================
// LIGHT CYCLE STATE MACHINE
// ============================================================================
void runLightCycleStateMachine() {
int currentHour = globalNow.hour();
bool shouldBeOn =
(LIGHT_ON_HOUR < LIGHT_OFF_HOUR) ?
(currentHour >= LIGHT_ON_HOUR &&
currentHour < LIGHT_OFF_HOUR)
:
(currentHour >= LIGHT_ON_HOUR ||
currentHour < LIGHT_OFF_HOUR);
digitalWrite(
RELAY_LIGHT_PIN,
shouldBeOn ? LOW : HIGH
);
}
// ============================================================================
// BUTTON STATE MACHINE
// ============================================================================
void runButtonStateMachine() {
bool pinReading =
digitalRead(BUTTON_PAGE_PIN);
static unsigned long debounceTime = 0;
switch (btnState) {
case BTN_IDLE:
if (pinReading == LOW) {
debounceTime = millis();
btnState = BTN_DEBOUNCE;
}
break;
case BTN_DEBOUNCE:
if (millis() - debounceTime >= DEBOUNCE_DELAY) {
if (digitalRead(BUTTON_PAGE_PIN) == LOW) {
btnState = BTN_PRESSED;
}
else {
btnState = BTN_IDLE;
}
}
break;
case BTN_PRESSED:
currentPage =
(DisplayPage)((int)currentPage + 1);
if (currentPage > PAGE_8) {
currentPage = PAGE_1;
}
btnState = BTN_WAIT_RELEASE;
break;
case BTN_WAIT_RELEASE:
if (pinReading == HIGH) {
btnState = BTN_IDLE;
}
break;
}
}
// ============================================================================
// DISPLAY STATE MACHINE
// ============================================================================
void runDisplayStateMachine() {
if (currentPage != lastPage) {
tft.fillScreen(ILI9341_BLACK);
drawPageStaticLayout(currentPage);
lastPage = currentPage;
}
if (millis() - lastDisplayMillis >= DISPLAY_INTERVAL) {
lastDisplayMillis = millis();
switch (currentPage) {
case PAGE_1:
updatePage1Dynamic();
break;
case PAGE_2:
updatePage2Dynamic();
break;
case PAGE_3:
updatePage3Dynamic();
break;
case PAGE_4:
updatePage4Dynamic();
break;
case PAGE_5:
updatePage5Dynamic();
break;
case PAGE_6:
updatePage6Dynamic();
break;
case PAGE_7:
updatePage7Dynamic();
break;
case PAGE_8:
updatePage8Dynamic();
break;
}
}
}
// ============================================================================
// STATIC PAGE LAYOUTS
// ============================================================================
void drawPageStaticLayout(DisplayPage page) {
tft.setTextColor(ILI9341_GREEN);
// --------------------------------------------------------------------------
// PAGE 1
// --------------------------------------------------------------------------
if (page == PAGE_1) {
tft.setTextSize(4);
tft.setCursor(36, 45);
tft.print("WELCOME");
tft.setTextSize(2);
tft.setCursor(42, 135);
tft.print("MUSHROOM GROW");
tft.setCursor(54, 165);
tft.print("ENVIRONMENT");
}
// --------------------------------------------------------------------------
// PAGE 2
// --------------------------------------------------------------------------
else if (page == PAGE_2) {
tft.setTextSize(2);
String title = "DATE AND TIME";
int titleWidth =
title.length() * 12;
int titleX =
(240 - titleWidth) / 2;
tft.setCursor(titleX, 15);
tft.print(title);
tft.drawFastHLine(
0,
40,
240,
ILI9341_GREEN
);
}
// --------------------------------------------------------------------------
// OTHER PAGES
// --------------------------------------------------------------------------
else {
tft.setTextSize(2);
String title;
switch (page) {
case PAGE_3:
title = "ELECTRONICS FAN";
break;
case PAGE_4:
title = "GROW TENT";
break;
case PAGE_5:
title = "GROW TENT";
break;
default:
title = "PAGE UNASSIGNED";
break;
}
int titleWidth =
title.length() * 12;
int titleX =
(240 - titleWidth) / 2;
tft.setCursor(titleX, 15);
tft.print(title);
if (page != PAGE_3 &&
page != PAGE_4 &&
page != PAGE_5) {
tft.drawFastHLine(
0,
45,
240,
ILI9341_GREEN
);
}
}
}
// ============================================================================
// PAGE 1
// ============================================================================
void updatePage1Dynamic() {
}
// ============================================================================
// PAGE 2 - DATE & TIME
// ============================================================================
void updatePage2Dynamic() {
char timeBuf[16];
char dateBuf[16];
sprintf(
timeBuf,
"%02d:%02d:%02d",
globalNow.hour(),
globalNow.minute(),
globalNow.second()
);
sprintf(
dateBuf,
"%02d/%02d/%04d",
globalNow.day(),
globalNow.month(),
globalNow.year()
);
tft.setTextSize(4);
tft.setTextColor(
ILI9341_GREEN,
ILI9341_BLACK
);
tft.setCursor(24, 95);
tft.print(timeBuf);
tft.setTextSize(3);
tft.setCursor(30, 175);
tft.print(dateBuf);
}
// ============================================================================
// PAGE 3 - ELECTRONICS FAN TEMPERATURE
// ============================================================================
void updatePage3Dynamic() {
static String lastValueStr = "";
static bool firstDraw = true;
if (firstDraw) {
tft.fillRect(
0,
50,
240,
220,
ILI9341_BLACK
);
tft.setTextSize(3);
tft.setTextColor(
ILI9341_GREEN,
ILI9341_BLACK
);
String label = "TEMPERATURE";
int labelWidth =
label.length() * 18;
int labelX =
(240 - labelWidth) / 2;
tft.setCursor(labelX, 65);
tft.print(label);
firstDraw = false;
}
String valueStr =
eFanData.isValid ?
String(eFanData.temperature, 1) + "C"
:
"ERROR";
if (valueStr != lastValueStr) {
tft.fillRect(
0,
125,
240,
100,
ILI9341_BLACK
);
tft.setTextSize(6);
tft.setTextColor(
eFanData.isValid ?
ILI9341_GREEN
:
ILI9341_RED,
ILI9341_BLACK
);
int valueWidth =
valueStr.length() * 36;
int valueX =
(240 - valueWidth) / 2;
tft.setCursor(valueX, 145);
tft.print(valueStr);
lastValueStr = valueStr;
}
}
// ============================================================================
// PAGE 4 - GROW TENT TEMPERATURE
// ============================================================================
void updatePage4Dynamic() {
static String lastValueStr = "";
static bool firstDraw = true;
if (firstDraw) {
tft.fillRect(
0,
50,
240,
220,
ILI9341_BLACK
);
tft.setTextSize(3);
tft.setTextColor(
ILI9341_GREEN,
ILI9341_BLACK
);
String label = "TEMPERATURE";
int labelWidth =
label.length() * 18;
int labelX =
(240 - labelWidth) / 2;
tft.setCursor(labelX, 65);
tft.print(label);
firstDraw = false;
}
String valueStr =
gTempData.isValid ?
String(gTempData.temperature, 1) + "C"
:
"ERROR";
if (valueStr != lastValueStr) {
tft.fillRect(
0,
125,
240,
100,
ILI9341_BLACK
);
tft.setTextSize(6);
tft.setTextColor(
gTempData.isValid ?
ILI9341_GREEN
:
ILI9341_RED,
ILI9341_BLACK
);
int valueWidth =
valueStr.length() * 36;
int valueX =
(240 - valueWidth) / 2;
tft.setCursor(valueX, 145);
tft.print(valueStr);
lastValueStr = valueStr;
}
}
// ============================================================================
// PAGE 5 - GROW TENT HUMIDITY
// ============================================================================
void updatePage5Dynamic() {
static String lastValueStr = "";
static bool firstDraw = true;
if (firstDraw) {
tft.fillRect(
0,
50,
240,
220,
ILI9341_BLACK
);
tft.setTextSize(3);
tft.setTextColor(
ILI9341_GREEN,
ILI9341_BLACK
);
String label = "HUMIDITY";
int labelWidth =
label.length() * 18;
int labelX =
(240 - labelWidth) / 2;
tft.setCursor(labelX, 65);
tft.print(label);
firstDraw = false;
}
String valueStr =
gHumData.isValid ?
String(gHumData.humidity, 0) + "%"
:
"ERROR";
if (valueStr != lastValueStr) {
tft.fillRect(
0,
125,
240,
100,
ILI9341_BLACK
);
tft.setTextSize(6);
tft.setTextColor(
gHumData.isValid ?
ILI9341_GREEN
:
ILI9341_RED,
ILI9341_BLACK
);
int valueWidth =
valueStr.length() * 36;
int valueX =
(240 - valueWidth) / 2;
tft.setCursor(valueX, 145);
tft.print(valueStr);
lastValueStr = valueStr;
}
}
// ============================================================================
// PAGE 6
// ============================================================================
void updatePage6Dynamic() {
tft.setTextSize(2);
tft.setCursor(10, 70);
tft.print("Page 6 Data...");
}
// ============================================================================
// PAGE 7
// ============================================================================
void updatePage7Dynamic() {
tft.setTextSize(2);
tft.setCursor(10, 70);
tft.print("Page 7 Data...");
}
// ============================================================================
// PAGE 8
// ============================================================================
void updatePage8Dynamic() {
tft.setTextSize(2);
tft.setCursor(10, 70);
tft.print("Page 8 Data...");
}