#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <DHT.h>
#include <Preferences.h>
#include <WiFi.h>
#include <WebServer.h>
#include <SD.h>
#include <SPI.h>
#define DHTPIN 4
#define DHTTYPE DHT22
#define SOIL_PIN 34
#define LDR_PIN 35
#define FAN_LED 18
#define PUMP_LED 19
#define GROW_LED 23
#define STATUS_LED 5
#define MODE_BUTTON 25
#define EDIT_BUTTON 26
#define LCD_SDA 21
#define LCD_SCL 22
#define LCD_ADDR 0x27
#define SD_CS 15
#define SD_MOSI 13
#define SD_MISO 12
#define SD_SCK 14
#define SD_LOG_FILE "/greenhouse_log.csv"
const int NIGHT_ON_THRESHOLD = 2200; // enter night above this
const int NIGHT_OFF_THRESHOLD = 1500; // leave night below this
const unsigned long PRINT_INTERVAL = 5000;
const unsigned long SCREEN_INTERVAL = 3000;
const unsigned long DEBOUNCE_DELAY = 220;
const unsigned long DHT_INTERVAL = 2000;
const float TEMP_HYSTERESIS = 0.5f;
const int SOIL_HYSTERESIS = 2;
#define LOG_SIZE 1440
#define LOG_INTERVAL_MS 60000UL
const unsigned long DOUBLE_CLICK_TIME = 220;
const unsigned long HOLD_TIME = 600;
const unsigned long HOLD_REPEAT = 180;
const unsigned long MODE_MSG_TIME = 2000;
const float TEMP_DANGER_HIGH = 38.0f;
const int SOIL_DANGER_LOW = 10;
LiquidCrystal_I2C lcd(LCD_ADDR, 16, 2);
DHT dht(DHTPIN, DHTTYPE);
Preferences prefs;
const char* WIFI_SSID = "Apple";
const char* WIFI_PASSWORD = "11142004";
WebServer server(80);
bool wifiConnected = false;
bool sdReady = false;
unsigned long lastPrintTime = 0;
unsigned long lastLogTime = 0;
unsigned long lastScreenTime = 0;
unsigned long lastModePress = 0;
unsigned long lastDHTTime = 0;
int screenIndex = 0;
bool lcdDirty = true;
float tempLog[LOG_SIZE];
float humLog[LOG_SIZE];
int soilLog[LOG_SIZE];
int lightLog[LOG_SIZE];
int modeLog[LOG_SIZE];
int fanLog[LOG_SIZE];
int pumpLog[LOG_SIZE];
int growLog[LOG_SIZE];
int logIndex = 0;
bool logBufferFull = false;
float humidity = 0.0f;
float temperature = 0.0f;
bool dhtReady = false;
bool dhtError = false;
int soilPct = 0;
int lightRaw = 0;
float minTemp24 = 0.0f;
float maxTemp24 = 0.0f;
float minHum24 = 0.0f;
float maxHum24 = 0.0f;
float minTempEdit = 18.0f;
float maxTempEdit = 30.0f;
float minHumEdit = 40.0f;
float maxHumEdit = 80.0f;
float fanThreshold = 30.0f;
int pumpThreshold = 30;
int editItem = 0;
const char* editLabels[] = {
"MinTemp",
"MaxTemp",
"MinHum",
"MaxHum",
"FanThr",
"PumpThr"
};
const int EDIT_ITEM_COUNT = 6;
bool fanState = false;
bool pumpState = false;
bool growState = false;
bool statusSafe = false;
bool tempDangerAlert = false;
bool soilDangerAlert = false;
bool humLowAlert = false;
bool humHighAlert = false;
bool lastModeButtonState = HIGH;
bool lastEditButtonState = HIGH;
bool editPressed = false;
bool holdActive = false;
bool singleClickPending = false;
unsigned long editPressStart = 0;
unsigned long lastHoldRepeat = 0;
unsigned long pendingSingleTime = 0;
// MODE button long press support
bool modePressed = false;
bool modeHoldHandled = false;
unsigned long modePressStart = 0;
char lastModeMsg[17] = "Mode:AUTO";
unsigned long modeMsgUntil = 0;
enum Mode {
AUTO_MODE,
MANUAL_MODE,
EDIT_MODE,
NIGHT_MODE
};
Mode userSelectedMode = AUTO_MODE;
Mode currentMode = AUTO_MODE;
Mode previousUserModeBeforeEdit = AUTO_MODE;
unsigned long modeStartTime = 0;
unsigned long lastDayDuration = 0;
unsigned long lastNightDuration = 0;
// ---------- function prototypes ----------
void saveThresholds();
void loadThresholds();
void factoryReset();
void handleRoot();
void drawLCD();
void updateMode(int rawLight, unsigned long currentTime);
void updateStatusLED();
void incrementEditItem();
void decrementEditItem();
void initSD();
void logToSD();
void computeMinMax24();
void recordModeChange(Mode oldMode, Mode newMode, unsigned long durationMs);
void updateGrowState(int rawLight);
const char* modeToText(Mode mode);
void formatDuration(unsigned long ms, char* buffer);
float getEditValue(int item);
void handleModeButton();
void handleEditButton();
void evaluateAlerts();
void applyActuatorLogic();
void writeOutputs();
// ---------- helpers ----------
const char* modeToText(Mode mode) {
switch (mode) {
case AUTO_MODE: return "AUTO";
case MANUAL_MODE: return "MANUAL";
case EDIT_MODE: return "EDIT";
case NIGHT_MODE: return "NIGHT";
default: return "UNK";
}
}
void formatDuration(unsigned long ms, char* buffer) {
unsigned long totalMins = ms / 60000UL;
sprintf(buffer, "%02lu:%02lu", totalMins / 60UL, totalMins % 60UL);
}
float getEditValue(int item) {
switch (item) {
case 0: return minTempEdit;
case 1: return maxTempEdit;
case 2: return minHumEdit;
case 3: return maxHumEdit;
case 4: return fanThreshold;
case 5: return (float)pumpThreshold;
default: return 0.0f;
}
}
// ---------- edit controls ----------
void decrementEditItem() {
switch (editItem) {
case 0:
minTempEdit -= 1.0f;
if (minTempEdit < 0.0f) minTempEdit = 39.0f;
if (minTempEdit >= maxTempEdit) minTempEdit = maxTempEdit - 1.0f;
break;
case 1:
maxTempEdit -= 1.0f;
if (maxTempEdit <= minTempEdit) maxTempEdit = 40.0f;
if (maxTempEdit > 40.0f) maxTempEdit = 40.0f;
break;
case 2:
minHumEdit -= 5.0f;
if (minHumEdit < 0.0f) minHumEdit = 95.0f;
if (minHumEdit >= maxHumEdit) minHumEdit = maxHumEdit - 5.0f;
break;
case 3:
maxHumEdit -= 5.0f;
if (maxHumEdit <= minHumEdit) maxHumEdit = 100.0f;
if (maxHumEdit > 100.0f) maxHumEdit = 100.0f;
break;
case 4:
fanThreshold -= 1.0f;
if (fanThreshold < 20.0f) fanThreshold = 40.0f;
break;
case 5:
pumpThreshold -= 5;
if (pumpThreshold < 10) pumpThreshold = 100;
break;
}
}
void incrementEditItem() {
switch (editItem) {
case 0:
minTempEdit += 1.0f;
if (minTempEdit >= maxTempEdit) minTempEdit = maxTempEdit - 1.0f;
if (minTempEdit < 0.0f) minTempEdit = 0.0f;
if (minTempEdit > 39.0f) minTempEdit = 0.0f;
break;
case 1:
maxTempEdit += 1.0f;
if (maxTempEdit > 40.0f) maxTempEdit = minTempEdit + 1.0f;
if (maxTempEdit <= minTempEdit) maxTempEdit = minTempEdit + 1.0f;
break;
case 2:
minHumEdit += 5.0f;
if (minHumEdit >= maxHumEdit) minHumEdit = maxHumEdit - 5.0f;
if (minHumEdit < 0.0f) minHumEdit = 0.0f;
if (minHumEdit > 95.0f) minHumEdit = 0.0f;
break;
case 3:
maxHumEdit += 5.0f;
if (maxHumEdit > 100.0f) maxHumEdit = minHumEdit + 5.0f;
if (maxHumEdit <= minHumEdit) maxHumEdit = minHumEdit + 5.0f;
break;
case 4:
fanThreshold += 1.0f;
if (fanThreshold > 40.0f) fanThreshold = 20.0f;
break;
case 5:
pumpThreshold += 5;
if (pumpThreshold > 100) pumpThreshold = 10;
break;
}
}
// ---------- buttons ----------
void handleModeButton() {
bool currentState = digitalRead(MODE_BUTTON);
unsigned long now = millis();
// button pressed
if (lastModeButtonState == HIGH && currentState == LOW) {
if (now - lastModePress > DEBOUNCE_DELAY) {
lastModePress = now;
modePressed = true;
modeHoldHandled = false;
modePressStart = now;
}
}
// long press while holding in EDIT mode => save and exit edit mode
if (modePressed && currentState == LOW && !modeHoldHandled) {
if ((now - modePressStart >= HOLD_TIME) && currentMode == EDIT_MODE) {
saveThresholds();
Mode oldMode = currentMode;
userSelectedMode = previousUserModeBeforeEdit;
currentMode = userSelectedMode;
recordModeChange(oldMode, currentMode, now - modeStartTime);
modeStartTime = now;
modeHoldHandled = true;
modePressed = false;
lastScreenTime = now;
screenIndex = 0;
lcdDirty = true;
}
}
// button released
if (lastModeButtonState == LOW && currentState == HIGH) {
if (modePressed && !modeHoldHandled) {
// short press behavior
if (currentMode == EDIT_MODE) {
editItem = (editItem + 1) % EDIT_ITEM_COUNT;
} else {
if (userSelectedMode == AUTO_MODE) {
userSelectedMode = MANUAL_MODE;
} else if (userSelectedMode == MANUAL_MODE) {
previousUserModeBeforeEdit = MANUAL_MODE;
userSelectedMode = EDIT_MODE;
} else {
previousUserModeBeforeEdit = AUTO_MODE;
userSelectedMode = AUTO_MODE;
}
// entering EDIT_MODE from AUTO
if (currentMode != NIGHT_MODE) {
Mode oldMode = currentMode;
if (userSelectedMode == EDIT_MODE && oldMode != EDIT_MODE) {
previousUserModeBeforeEdit = oldMode;
}
currentMode = userSelectedMode;
recordModeChange(oldMode, currentMode, now - modeStartTime);
modeStartTime = now;
}
lastScreenTime = now;
if (currentMode != EDIT_MODE) {
screenIndex = 0;
}
}
lcdDirty = true;
}
modePressed = false;
modeHoldHandled = false;
}
lastModeButtonState = currentState;
}
void handleEditButton() {
bool currentState = digitalRead(EDIT_BUTTON);
unsigned long now = millis();
if (currentMode != EDIT_MODE) {
lastEditButtonState = currentState;
editPressed = false;
holdActive = false;
singleClickPending = false;
return;
}
if (lastEditButtonState == HIGH && currentState == LOW) {
editPressed = true;
holdActive = false;
editPressStart = now;
lastHoldRepeat = now;
}
if (editPressed && currentState == LOW) {
if (!holdActive && (now - editPressStart >= HOLD_TIME)) {
holdActive = true;
singleClickPending = false;
incrementEditItem();
saveThresholds();
lcdDirty = true;
lastHoldRepeat = now;
} else if (holdActive && (now - lastHoldRepeat >= HOLD_REPEAT)) {
incrementEditItem();
saveThresholds();
lcdDirty = true;
lastHoldRepeat = now;
}
}
if (lastEditButtonState == LOW && currentState == HIGH) {
if (editPressed) {
editPressed = false;
if (!holdActive) {
if (singleClickPending && (now - pendingSingleTime <= DOUBLE_CLICK_TIME)) {
decrementEditItem();
saveThresholds();
lcdDirty = true;
singleClickPending = false;
} else {
singleClickPending = true;
pendingSingleTime = now;
}
} else {
holdActive = false;
singleClickPending = false;
}
}
}
if (singleClickPending && (now - pendingSingleTime > DOUBLE_CLICK_TIME)) {
incrementEditItem();
saveThresholds();
lcdDirty = true;
singleClickPending = false;
}
lastEditButtonState = currentState;
}
// ---------- mode / light ----------
void recordModeChange(Mode oldMode, Mode newMode, unsigned long durationMs) {
char durBuf[6];
formatDuration(durationMs, durBuf);
Serial.print("MODE CHANGE: ");
Serial.print(modeToText(oldMode));
Serial.print(" -> ");
Serial.print(modeToText(newMode));
Serial.print(" | Prev duration: ");
Serial.println(durBuf);
snprintf(lastModeMsg, sizeof(lastModeMsg), "Mode:%s", modeToText(newMode));
modeMsgUntil = millis() + MODE_MSG_TIME;
if (sdReady) {
File f = SD.open(SD_LOG_FILE, FILE_APPEND);
if (f) {
f.print("MODE_EVENT,");
f.print(modeToText(oldMode));
f.print("->");
f.print(modeToText(newMode));
f.print(",duration=");
f.println(durBuf);
f.close();
}
}
}
void updateMode(int rawLight, unsigned long currentTime) {
Mode newMode = currentMode;
// dark = high raw value
if (rawLight > NIGHT_ON_THRESHOLD) {
newMode = NIGHT_MODE;
} else if (rawLight < NIGHT_OFF_THRESHOLD) {
newMode = userSelectedMode;
}
if (newMode != currentMode) {
unsigned long duration = currentTime - modeStartTime;
if (currentMode == NIGHT_MODE) lastNightDuration = duration;
else lastDayDuration = duration;
recordModeChange(currentMode, newMode, duration);
currentMode = newMode;
modeStartTime = currentTime;
lcdDirty = true;
if (currentMode != EDIT_MODE) {
lastScreenTime = currentTime;
screenIndex = 0;
}
}
}
void updateGrowState(int rawLight) {
// same polarity as night mode: dark = higher raw
if (growState) {
if (rawLight < NIGHT_OFF_THRESHOLD) growState = false;
} else {
if (rawLight > NIGHT_ON_THRESHOLD) growState = true;
}
}
// ---------- alerts / outputs ----------
void evaluateAlerts() {
tempDangerAlert = (temperature >= TEMP_DANGER_HIGH);
soilDangerAlert = (soilPct <= SOIL_DANGER_LOW);
humLowAlert = (humidity < minHumEdit);
humHighAlert = (humidity > maxHumEdit);
}
void applyActuatorLogic() {
// Keep same overall functionality:
// MANUAL mode disables fan/pump automatic actuation
// AUTO, NIGHT, EDIT continue environment control
if (currentMode == MANUAL_MODE) {
fanState = false;
pumpState = false;
} else {
if (temperature > (fanThreshold + TEMP_HYSTERESIS)) fanState = true;
else if (temperature < (fanThreshold - TEMP_HYSTERESIS)) fanState = false;
if (soilPct < (pumpThreshold - SOIL_HYSTERESIS)) pumpState = true;
else if (soilPct > (pumpThreshold + SOIL_HYSTERESIS)) pumpState = false;
}
updateGrowState(lightRaw);
}
void updateStatusLED() {
bool anyWarning = fanState || pumpState || growState;
statusSafe = !anyWarning;
digitalWrite(STATUS_LED, statusSafe ? HIGH : LOW);
}
void writeOutputs() {
digitalWrite(FAN_LED, fanState ? HIGH : LOW);
digitalWrite(PUMP_LED, pumpState ? HIGH : LOW);
digitalWrite(GROW_LED, growState ? HIGH : LOW);
updateStatusLED();
}
// ---------- logging ----------
void computeMinMax24() {
int count = logBufferFull ? LOG_SIZE : logIndex;
if (count <= 0) {
minTemp24 = temperature;
maxTemp24 = temperature;
minHum24 = humidity;
maxHum24 = humidity;
return;
}
minTemp24 = tempLog[0];
maxTemp24 = tempLog[0];
minHum24 = humLog[0];
maxHum24 = humLog[0];
for (int i = 1; i < count; i++) {
if (tempLog[i] < minTemp24) minTemp24 = tempLog[i];
if (tempLog[i] > maxTemp24) maxTemp24 = tempLog[i];
if (humLog[i] < minHum24) minHum24 = humLog[i];
if (humLog[i] > maxHum24) maxHum24 = humLog[i];
}
}
void initSD() {
SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS)) {
sdReady = false;
return;
}
sdReady = true;
if (!SD.exists(SD_LOG_FILE)) {
File f = SD.open(SD_LOG_FILE, FILE_WRITE);
if (f) {
f.println("Entry,Temp_C,Humidity_%,Soil_%,Light_raw,Mode,Fan,Pump,Grow");
f.close();
}
}
}
void logToSD() {
if (!sdReady) return;
File f = SD.open(SD_LOG_FILE, FILE_APPEND);
if (!f) return;
f.print(logIndex); f.print(",");
f.print(temperature, 1); f.print(",");
f.print((int)humidity); f.print(",");
f.print(soilPct); f.print(",");
f.print(lightRaw); f.print(",");
f.print(modeToText(currentMode)); f.print(",");
f.print(fanState ? "ON" : "OFF"); f.print(",");
f.print(pumpState ? "ON" : "OFF"); f.print(",");
f.println(growState ? "ON" : "OFF");
f.close();
}
// ---------- storage ----------
void saveThresholds() {
prefs.begin("gh", false);
prefs.putFloat("minT", minTempEdit);
prefs.putFloat("maxT", maxTempEdit);
prefs.putFloat("minH", minHumEdit);
prefs.putFloat("maxH", maxHumEdit);
prefs.putFloat("fanT", fanThreshold);
prefs.putInt("pmpT", pumpThreshold);
prefs.end();
}
void loadThresholds() {
prefs.begin("gh", true);
minTempEdit = prefs.getFloat("minT", 18.0f);
maxTempEdit = prefs.getFloat("maxT", 30.0f);
minHumEdit = prefs.getFloat("minH", 40.0f);
maxHumEdit = prefs.getFloat("maxH", 80.0f);
fanThreshold = prefs.getFloat("fanT", 30.0f);
pumpThreshold = prefs.getInt("pmpT", 30);
prefs.end();
}
void factoryReset() {
prefs.begin("gh", false);
prefs.clear();
prefs.end();
minTempEdit = 18.0f;
maxTempEdit = 30.0f;
minHumEdit = 40.0f;
maxHumEdit = 80.0f;
fanThreshold = 30.0f;
pumpThreshold = 30;
lcdDirty = true;
}
// ---------- display ----------
void drawLCD() {
lcd.clear();
if (modeMsgUntil > millis()) {
lcd.setCursor(0, 0);
lcd.print(lastModeMsg);
lcd.setCursor(0, 1);
lcd.print("Mode changed");
return;
}
if (currentMode == EDIT_MODE) {
lcd.setCursor(0, 0);
lcd.print(editLabels[editItem]);
lcd.setCursor(10, 0);
lcd.print(editItem + 1);
lcd.print("/");
lcd.print(EDIT_ITEM_COUNT);
lcd.setCursor(0, 1);
lcd.print("Val:");
float val = getEditValue(editItem);
if (editItem == 5 || editItem == 2 || editItem == 3) {
lcd.print((int)val);
lcd.print("%");
} else {
lcd.print(val, 1);
lcd.print("C");
}
}
else if (screenIndex == 0) {
lcd.setCursor(0, 0);
if (dhtError) {
lcd.print("DHT Read Error");
} else {
lcd.print("T:");
lcd.print(temperature, 1);
lcd.print(" H:");
lcd.print((int)humidity);
lcd.print("%");
}
lcd.setCursor(0, 1);
lcd.print("S:");
lcd.print(soilPct);
lcd.print("% L:");
lcd.print(lightRaw);
}
else if (screenIndex == 1) {
lcd.setCursor(0, 0);
lcd.print("Tn:");
lcd.print(minTemp24, 1);
lcd.print(" Tx:");
lcd.print(maxTemp24, 1);
lcd.setCursor(0, 1);
lcd.print("Hn:");
lcd.print((int)minHum24);
lcd.print("% Hx:");
lcd.print((int)maxHum24);
lcd.print("%");
}
else if (screenIndex == 2) {
lcd.setCursor(0, 0);
lcd.print("Mode:");
lcd.print(modeToText(currentMode));
lcd.setCursor(0, 1);
lcd.print("F:");
lcd.print(fanState ? "ON " : "OFF");
lcd.print(" P:");
lcd.print(pumpState ? "ON" : "OFF");
}
else if (screenIndex == 3) {
char dayBuf[6], nightBuf[6];
formatDuration(lastDayDuration, dayBuf);
formatDuration(lastNightDuration, nightBuf);
lcd.setCursor(0, 0);
lcd.print("Day: ");
lcd.print(dayBuf);
lcd.setCursor(0, 1);
lcd.print("Night:");
lcd.print(nightBuf);
}
else {
lcd.setCursor(0, 0);
if (tempDangerAlert) lcd.print("ALERT:TEMP HIGH ");
else if (humHighAlert) lcd.print("ALERT:HUM HIGH ");
else if (humLowAlert) lcd.print("ALERT:HUM LOW ");
else lcd.print("Temp/Hum OK ");
lcd.setCursor(0, 1);
if (soilDangerAlert) lcd.print("ALERT:SOIL LOW ");
else if (!statusSafe) lcd.print("Check env range ");
else lcd.print("Soil OK ");
}
}
// ---------- web ----------
void handleRoot() {
String tempColor = fanState ? "#ff6b6b" : "#4ade80";
String soilColor = pumpState ? "#facc15" : "#4ade80";
String lightColor = growState ? "#60a5fa" : "#4ade80";
String safeBadgeClass = statusSafe ? "good" : "bad";
String tempBadgeClass = tempDangerAlert ? "bad" : "neutral";
String soilBadgeClass = soilDangerAlert ? "warn" : "neutral";
String humLowBadgeClass = humLowAlert ? "warn" : "neutral";
String humHighBadgeClass = humHighAlert ? "bad" : "neutral";
String dhtBadgeClass = dhtError ? "bad" : "good";
char dayBuf[6], nightBuf[6];
formatDuration(lastDayDuration, dayBuf);
formatDuration(lastNightDuration, nightBuf);
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="5">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Smart Greenhouse Dashboard</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Arial, sans-serif;
background: #0f172a;
color: #e5e7eb;
}
.layout {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 250px;
background: #111827;
padding: 20px 16px;
border-right: 1px solid #1f2937;
}
.brand {
font-size: 22px;
font-weight: bold;
color: #86efac;
margin-bottom: 6px;
}
.subtitle {
font-size: 12px;
color: #94a3b8;
margin-bottom: 22px;
}
.side-section {
margin-bottom: 22px;
}
.side-title {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #93c5fd;
margin-bottom: 10px;
}
.side-card {
background: #1f2937;
border-radius: 12px;
padding: 12px;
margin-bottom: 10px;
}
.side-row {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 13px;
margin-bottom: 8px;
}
.side-row:last-child { margin-bottom: 0; }
.content {
flex: 1;
padding: 20px;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 18px;
}
.top-title {
font-size: 28px;
font-weight: bold;
color: #f8fafc;
margin: 0;
}
.top-note {
color: #94a3b8;
font-size: 13px;
margin-top: 4px;
}
.mode-pill {
background: #1e293b;
color: #a7f3d0;
padding: 10px 14px;
border-radius: 999px;
font-weight: bold;
font-size: 13px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
gap: 14px;
}
.card {
background: #111827;
border: 1px solid #1f2937;
border-radius: 18px;
padding: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,0.18);
}
.wide {
grid-column: 1 / -1;
}
.card h2 {
margin: 0 0 12px;
color: #bfdbfe;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 9px 0;
border-bottom: 1px solid #1f2937;
}
.row:last-child {
border-bottom: none;
}
.label {
color: #cbd5e1;
font-size: 13px;
}
.value {
font-size: 14px;
font-weight: bold;
}
.badge {
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
display: inline-block;
min-width: 68px;
text-align: center;
}
.good { background: #22c55e; color: #062b12; }
.warn { background: #facc15; color: #3a2d00; }
.bad { background: #ef4444; color: #fff; }
.neutral { background: #374151; color: #f3f4f6; }
.hero {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.metric {
background: #0b1220;
border: 1px solid #1f2937;
border-radius: 16px;
padding: 14px;
}
.metric-label {
font-size: 12px;
color: #94a3b8;
margin-bottom: 8px;
text-transform: uppercase;
}
.metric-value {
font-size: 24px;
font-weight: bold;
}
.legend {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 10px;
}
.legend-box {
background: #0b1220;
border: 1px solid #1f2937;
border-radius: 14px;
padding: 12px;
font-size: 13px;
}
.legend-title {
font-weight: bold;
margin-bottom: 6px;
}
.footer {
margin-top: 18px;
text-align: center;
color: #64748b;
font-size: 11px;
}
@media (max-width: 900px) {
.layout { flex-direction: column; }
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid #1f2937;
}
}
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<div class="brand">Smart Greenhouse</div>
<div class="subtitle">ESP32 live monitor</div>
<div class="side-section">
<div class="side-title">System</div>
<div class="side-card">
<div class="side-row"><span>Mode</span><strong>)rawliteral";
html += String(modeToText(currentMode));
html += R"rawliteral(</strong></div>
<div class="side-row"><span>WiFi</span><strong>)rawliteral";
html += wifiConnected ? "Connected" : "Offline";
html += R"rawliteral(</strong></div>
<div class="side-row"><span>IP</span><strong>)rawliteral";
html += WiFi.localIP().toString();
html += R"rawliteral(</strong></div>
</div>
</div>
<div class="side-section">
<div class="side-title">Storage</div>
<div class="side-card">
<div class="side-row"><span>SD Card</span><strong>)rawliteral";
html += sdReady ? "OK" : "Not found";
html += R"rawliteral(</strong></div>
<div class="side-row"><span>Log Entries</span><strong>)rawliteral";
html += String(logBufferFull ? LOG_SIZE : logIndex);
html += R"rawliteral(</strong></div>
<div class="side-row"><span>Heap</span><strong>)rawliteral";
html += String(ESP.getFreeHeap());
html += R"rawliteral(</strong></div>
</div>
</div>
<div class="side-section">
<div class="side-title">Thresholds</div>
<div class="side-card">
<div class="side-row"><span>Fan</span><strong>)rawliteral";
html += String(fanThreshold, 1);
html += R"rawliteral(C</strong></div>
<div class="side-row"><span>Pump</span><strong>)rawliteral";
html += String(pumpThreshold);
html += R"rawliteral(%</strong></div>
<div class="side-row"><span>Temp</span><strong>)rawliteral";
html += String(minTempEdit, 1) + " - " + String(maxTempEdit, 1) + "C";
html += R"rawliteral(</strong></div>
<div class="side-row"><span>Hum</span><strong>)rawliteral";
html += String(minHumEdit, 1) + " - " + String(maxHumEdit, 1) + "%";
html += R"rawliteral(</strong></div>
</div>
</div>
</aside>
<main class="content">
<div class="topbar">
<div>
<h1 class="top-title">Dashboard</h1>
<div class="top-note">Auto-refresh every 5 seconds</div>
</div>
<div class="mode-pill">Current Mode: )rawliteral";
html += String(modeToText(currentMode));
html += R"rawliteral(</div>
</div>
<div class="grid">
<div class="card wide">
<h2>Live Overview</h2>
<div class="hero">
<div class="metric">
<div class="metric-label">Temperature</div>
<div class="metric-value" style="color:)rawliteral";
html += tempColor;
html += R"rawliteral(;">)rawliteral";
html += String(temperature, 1);
html += R"rawliteral( C</div>
</div>
<div class="metric">
<div class="metric-label">Humidity</div>
<div class="metric-value">)rawliteral";
html += String((int)humidity);
html += R"rawliteral( %</div>
</div>
<div class="metric">
<div class="metric-label">Soil Moisture</div>
<div class="metric-value" style="color:)rawliteral";
html += soilColor;
html += R"rawliteral(;">)rawliteral";
html += String(soilPct);
html += R"rawliteral( %</div>
</div>
<div class="metric">
<div class="metric-label">Light Raw</div>
<div class="metric-value" style="color:)rawliteral";
html += lightColor;
html += R"rawliteral(;">)rawliteral";
html += String(lightRaw);
html += R"rawliteral(</div>
</div>
</div>
</div>
<div class="card">
<h2>Actuators</h2>
<div class="row"><span class="label">Fan / Red</span><span class="badge )rawliteral";
html += fanState ? "bad" : "neutral";
html += R"rawliteral(">)rawliteral";
html += fanState ? "ON" : "OFF";
html += R"rawliteral(</span></div>
<div class="row"><span class="label">Pump / Yellow</span><span class="badge )rawliteral";
html += pumpState ? "warn" : "neutral";
html += R"rawliteral(">)rawliteral";
html += pumpState ? "ON" : "OFF";
html += R"rawliteral(</span></div>
<div class="row"><span class="label">Grow / Blue</span><span class="badge )rawliteral";
html += growState ? "good" : "neutral";
html += R"rawliteral(">)rawliteral";
html += growState ? "ON" : "OFF";
html += R"rawliteral(</span></div>
<div class="row"><span class="label">Safe / Green</span><span class="badge )rawliteral";
html += safeBadgeClass;
html += R"rawliteral(">)rawliteral";
html += statusSafe ? "SAFE" : "WARNING";
html += R"rawliteral(</span></div>
</div>
<div class="card">
<h2>Alerts</h2>
<div class="row"><span class="label">Temp Danger</span><span class="badge )rawliteral";
html += tempBadgeClass;
html += R"rawliteral(">)rawliteral";
html += tempDangerAlert ? "YES" : "NO";
html += R"rawliteral(</span></div>
<div class="row"><span class="label">Soil Danger</span><span class="badge )rawliteral";
html += soilBadgeClass;
html += R"rawliteral(">)rawliteral";
html += soilDangerAlert ? "YES" : "NO";
html += R"rawliteral(</span></div>
<div class="row"><span class="label">Humidity Low</span><span class="badge )rawliteral";
html += humLowBadgeClass;
html += R"rawliteral(">)rawliteral";
html += humLowAlert ? "YES" : "NO";
html += R"rawliteral(</span></div>
<div class="row"><span class="label">Humidity High</span><span class="badge )rawliteral";
html += humHighBadgeClass;
html += R"rawliteral(">)rawliteral";
html += humHighAlert ? "YES" : "NO";
html += R"rawliteral(</span></div>
<div class="row"><span class="label">DHT Status</span><span class="badge )rawliteral";
html += dhtBadgeClass;
html += R"rawliteral(">)rawliteral";
html += dhtError ? "ERROR" : "OK";
html += R"rawliteral(</span></div>
</div>
<div class="card">
<h2>Ranges</h2>
<div class="row"><span class="label">Temperature Range</span><span class="value">)rawliteral";
html += String(minTempEdit, 1) + " - " + String(maxTempEdit, 1) + " C";
html += R"rawliteral(</span></div>
<div class="row"><span class="label">Humidity Range</span><span class="value">)rawliteral";
html += String(minHumEdit, 1) + " - " + String(maxHumEdit, 1) + " %";
html += R"rawliteral(</span></div>
<div class="row"><span class="label">Fan Turns On Above</span><span class="value">)rawliteral";
html += String(fanThreshold, 1);
html += R"rawliteral( C</span></div>
<div class="row"><span class="label">Pump Turns On Below</span><span class="value">)rawliteral";
html += String(pumpThreshold);
html += R"rawliteral( %</span></div>
</div>
<div class="card">
<h2>History</h2>
<div class="row"><span class="label">Min / Max Temp 24h</span><span class="value">)rawliteral";
html += String(minTemp24, 1) + " / " + String(maxTemp24, 1) + " C";
html += R"rawliteral(</span></div>
<div class="row"><span class="label">Min / Max Hum 24h</span><span class="value">)rawliteral";
html += String((int)minHum24) + " / " + String((int)maxHum24) + " %";
html += R"rawliteral(</span></div>
<div class="row"><span class="label">Last Day</span><span class="value">)rawliteral";
html += String(dayBuf);
html += R"rawliteral(</span></div>
<div class="row"><span class="label">Last Night</span><span class="value">)rawliteral";
html += String(nightBuf);
html += R"rawliteral(</span></div>
</div>
<div class="card wide">
<h2>LED Meaning</h2>
<div class="legend">
<div class="legend-box">
<div class="legend-title" style="color:#ff6b6b;">Red LED</div>
Fan active / high temperature response
</div>
<div class="legend-box">
<div class="legend-title" style="color:#facc15;">Yellow LED</div>
Pump active / low soil response
</div>
<div class="legend-box">
<div class="legend-title" style="color:#60a5fa;">Blue LED</div>
Grow light active / low light response
</div>
<div class="legend-box">
<div class="legend-title" style="color:#4ade80;">Green LED</div>
Overall safe status
</div>
</div>
</div>
</div>
<div class="footer">6504-SEPA Smart Greenhouse - ESP32</div>
</main>
</div>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
// ---------- setup / loop ----------
void setup() {
Serial.begin(115200);
delay(100);
dht.begin();
loadThresholds();
pinMode(FAN_LED, OUTPUT); digitalWrite(FAN_LED, LOW);
pinMode(PUMP_LED, OUTPUT); digitalWrite(PUMP_LED, LOW);
pinMode(GROW_LED, OUTPUT); digitalWrite(GROW_LED, LOW);
pinMode(STATUS_LED, OUTPUT); digitalWrite(STATUS_LED, LOW);
pinMode(MODE_BUTTON, INPUT_PULLUP);
pinMode(EDIT_BUTTON, INPUT_PULLUP);
lastModeButtonState = digitalRead(MODE_BUTTON);
lastEditButtonState = digitalRead(EDIT_BUTTON);
Wire.begin(LCD_SDA, LCD_SCL);
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0, 0); lcd.print("Greenhouse");
lcd.setCursor(0, 1); lcd.print("Starting...");
delay(1500);
lcd.clear();
lcd.setCursor(0, 0); lcd.print("WiFi Connect..");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long wifiStart = millis();
while (WiFi.status() != WL_CONNECTED && millis() - wifiStart < 10000UL) {
delay(250);
}
if (WiFi.status() == WL_CONNECTED) {
wifiConnected = true;
server.on("/", handleRoot);
server.begin();
Serial.println("WiFi Connected!");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
lcd.clear();
lcd.setCursor(0, 0); lcd.print("WiFi OK!");
lcd.setCursor(0, 1); lcd.print(WiFi.localIP().toString());
delay(2000);
} else {
lcd.clear();
lcd.setCursor(0, 0); lcd.print("WiFi Failed");
lcd.setCursor(0, 1); lcd.print("Running local");
delay(1500);
}
lcd.clear();
initSD();
modeStartTime = millis();
computeMinMax24();
Serial.println("Smart Greenhouse System Ready");
}
void loop() {
unsigned long currentTime = millis();
if (Serial.available() > 0) {
char cmd = Serial.read();
if (cmd == 'R' || cmd == 'r') {
factoryReset();
}
}
if (wifiConnected) {
server.handleClient();
}
handleModeButton();
handleEditButton();
if (currentTime - lastDHTTime >= DHT_INTERVAL) {
lastDHTTime = currentTime;
float newHum = dht.readHumidity();
float newTemp = dht.readTemperature();
if (isnan(newHum) || isnan(newTemp)) {
dhtError = true;
Serial.println("ERROR: DHT22 read failed - keeping last valid values");
lcdDirty = true;
} else {
humidity = newHum;
temperature = newTemp;
dhtReady = true;
dhtError = false;
lcdDirty = true;
}
}
soilPct = map(analogRead(SOIL_PIN), 0, 4095, 100, 0);
soilPct = constrain(soilPct, 0, 100);
lightRaw = analogRead(LDR_PIN);
if (!dhtReady) {
if (lcdDirty) {
lcdDirty = false;
drawLCD();
}
return;
}
updateMode(lightRaw, currentTime);
applyActuatorLogic();
evaluateAlerts();
writeOutputs();
if (currentTime - lastPrintTime >= PRINT_INTERVAL) {
lastPrintTime = currentTime;
char dayBuf[6], nightBuf[6];
formatDuration(lastDayDuration, dayBuf);
formatDuration(lastNightDuration, nightBuf);
const char* tempStatus = (temperature > maxTempEdit) ? "ABOVE range" :
(temperature < minTempEdit) ? "BELOW range" : "WITHIN range";
const char* humStatus = (humidity > maxHumEdit) ? "ABOVE range" :
(humidity < minHumEdit) ? "BELOW range" : "WITHIN range";
const char* soilStatus = (soilPct < pumpThreshold) ? "BELOW range" :
(soilPct > pumpThreshold + 20) ? "ABOVE range" : "WITHIN range";
const char* lightStatus = growState ? "BELOW range" : "WITHIN range";
Serial.println("----------- SENSOR READINGS -----------");
Serial.print("Mode: "); Serial.println(modeToText(currentMode));
Serial.print("User Mode: "); Serial.println(modeToText(userSelectedMode));
Serial.println("--- Thresholds ---");
Serial.print("Fan ON above: "); Serial.print(fanThreshold); Serial.println(" C");
Serial.print("Pump ON below: "); Serial.print(pumpThreshold); Serial.println(" %");
Serial.print("Temp range: "); Serial.print(minTempEdit,1); Serial.print(" - "); Serial.print(maxTempEdit,1); Serial.println(" C");
Serial.print("Hum range: "); Serial.print(minHumEdit,1); Serial.print(" - "); Serial.print(maxHumEdit,1); Serial.println(" %");
Serial.println("--- Readings & Status ---");
Serial.print("Temperature: "); Serial.print(temperature,1); Serial.print(" C -> "); Serial.println(tempStatus);
Serial.print("Humidity: "); Serial.print(humidity,1); Serial.print(" % -> "); Serial.println(humStatus);
Serial.print("Soil Moisture: "); Serial.print(soilPct); Serial.print(" % -> "); Serial.println(soilStatus);
Serial.print("Light Raw: "); Serial.print(lightRaw); Serial.print(" -> "); Serial.println(lightStatus);
Serial.println("--- Actuators & Status LEDs ---");
Serial.print("RED Fan: "); Serial.println(fanState ? "ON (temp HIGH)" : "OFF");
Serial.print("YELLOW Pump: "); Serial.println(pumpState ? "ON (soil LOW)" : "OFF");
Serial.print("BLUE Grow Light: "); Serial.println(growState ? "ON (light LOW)" : "OFF");
Serial.print("GREEN Status: "); Serial.println(statusSafe ? "ON (all OK)" : "OFF (warning active)");
Serial.println("--- Alerts ---");
Serial.print("Temp Danger: "); Serial.println(tempDangerAlert ? "YES" : "NO");
Serial.print("Soil Danger: "); Serial.println(soilDangerAlert ? "YES" : "NO");
Serial.print("Humidity Low: "); Serial.println(humLowAlert ? "YES" : "NO");
Serial.print("Humidity High: "); Serial.println(humHighAlert ? "YES" : "NO");
Serial.println("--- History ---");
Serial.print("Min/Max Temp 24h: "); Serial.print(minTemp24,1); Serial.print(" / "); Serial.print(maxTemp24,1); Serial.println(" C");
Serial.print("Min/Max Hum 24h: "); Serial.print((int)minHum24); Serial.print(" / "); Serial.print((int)maxHum24); Serial.println(" %");
Serial.print("Last Day: "); Serial.println(dayBuf);
Serial.print("Last Night: "); Serial.println(nightBuf);
Serial.print("SD Card: "); Serial.println(sdReady ? "OK" : "Not found");
Serial.print("WiFi: "); Serial.println(wifiConnected ? "Connected" : "Not connected");
if (wifiConnected) {
Serial.print("IP: ");
Serial.println(WiFi.localIP());
}
Serial.print("Free Heap: "); Serial.print(ESP.getFreeHeap()); Serial.println(" bytes");
Serial.print("Log: "); Serial.print(logBufferFull ? LOG_SIZE : logIndex);
if (logBufferFull) Serial.print(" (FULL - oldest overwritten)");
Serial.println();
Serial.println("--------------------------------------\n");
}
if (currentTime - lastLogTime >= LOG_INTERVAL_MS) {
lastLogTime = currentTime;
tempLog[logIndex] = temperature;
humLog[logIndex] = humidity;
soilLog[logIndex] = soilPct;
lightLog[logIndex] = lightRaw;
modeLog[logIndex] = (int)currentMode;
fanLog[logIndex] = fanState ? 1 : 0;
pumpLog[logIndex] = pumpState ? 1 : 0;
growLog[logIndex] = growState ? 1 : 0;
logIndex++;
if (logIndex >= LOG_SIZE) {
logIndex = 0;
logBufferFull = true;
Serial.println("INFO: Log buffer wrapped - oldest overwritten");
}
computeMinMax24();
Serial.print("LOG #"); Serial.print(logIndex);
Serial.print(" T:"); Serial.print(temperature,1);
Serial.print(" H:"); Serial.print((int)humidity);
Serial.print(" S:"); Serial.print(soilPct);
Serial.print(" L:"); Serial.println(lightRaw);
logToSD();
}
if (currentMode != EDIT_MODE) {
if (currentTime - lastScreenTime >= SCREEN_INTERVAL) {
lastScreenTime = currentTime;
screenIndex = (screenIndex + 1) % 5;
lcdDirty = true;
}
}
if (lcdDirty) {
lcdDirty = false;
drawLCD();
}
}