#include <Arduino.h>
#include "DHT.h"
#include <WiFi.h>
#include <PubSubClient.h>
#include <esp_task_wdt.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// ══════════════════════════════════════════════════════════════
// RELAY CONFIGURATION — READ BEFORE FLASHING
// Most single channel relay modules → Active HIGH (default)
// Multi channel relay boards (2/4/8ch) → May need Active LOW
// If humidifier turns ON at boot unexpectedly → swap the values
// ══════════════════════════════════════════════════════════════
#define RELAY_ON HIGH
#define RELAY_OFF LOW
// ── WiFi Credentials ───────────────────────────────────────────
#define WIFI_SSID "Wokwi-GUEST"
#define WIFI_PASSWORD ""
// ── MQTT Broker ────────────────────────────────────────────────
#define MQTT_BROKER "test.mosquitto.org"
#define MQTT_PORT 1883
// ── MQTT Topics ────────────────────────────────────────────────
#define TOPIC_TELEMETRY "greenhouse/room1/telemetry"
#define TOPIC_FAULT "greenhouse/room1/fault"
#define TOPIC_SYSTEM "greenhouse/room1/system"
#define TOPIC_STATUS "greenhouse/room1/status"
#define TOPIC_COMMAND "greenhouse/room1/command"
#define TOPIC_MODE "greenhouse/room1/mode"
#define TOPIC_TEMP "greenhouse/room1/telemetry/temp"
#define TOPIC_HUMIDITY "greenhouse/room1/telemetry/humidity"
#define TOPIC_HUMIDIFIER_STATE "greenhouse/room1/telemetry/humidifier"
// ── Pin Definitions ────────────────────────────────────────────
#define DHTPIN 4 // DHT22 data
#define DHTTYPE DHT22
#define HUMIDIFIER_PIN 18 // Relay
// I2C uses default pins — Wire.begin() with no arguments
// SDA → GPIO21 SCL → GPIO22
// ── Button Pins ────────────────────────────────────────────────
// GPIO32/33 have weaker internal pullups
// For long wire runs add external 10kΩ pullup resistors
#define BTN_AUTO 32
#define BTN_FORCED_ON 33
#define BTN_FORCED_OFF 25
#define BTN_SCREEN 26
// ── LED Pins ───────────────────────────────────────────────────
// Wiring: GPIO → 220-330Ω → LED → GND
// GPIO5 avoided — boot strapping pin
#define LED_WIFI 16 // Blue — solid=connected blink=connecting
#define LED_MQTT 17 // Yellow — solid=connected blink=reconnecting
#define LED_HUMIDIFIER 27 // White — solid=relay ON
#define LED_AUTO 19 // Green — solid=AUTO active
#define LED_FORCED_ON 23 // Orange — solid=FORCED_ON active
#define LED_FORCED_OFF 14 // Red — solid=FORCED_OFF fast blink=fault
// ── LCD ────────────────────────────────────────────────────────
#define LCD_ADDR 0x27 // Common I2C address — try 0x3F if blank
#define LCD_COLS 16
#define LCD_ROWS 2
// ── Timing ─────────────────────────────────────────────────────
#define BAUD_RATE 115200
#define READ_INTERVAL 2000
#define SENSOR_TIMEOUT 30000
#define SYSTEM_INTERVAL 60000
#define HEARTBEAT_MAX 30000
#define WIFI_TIMEOUT 15000
#define MQTT_TIMEOUT 15000
#define DEBOUNCE_MS 40
#define FAULT_BLINK_MS 100 // 5Hz fast blink for sensor fault
#define WIFI_BLINK_MS 500
#define MQTT_BLINK_MS 500
#define LCD_REFRESH_MS 500 // LCD update interval — avoids flicker
#define SCREEN_INTERVAL 5000 // Auto scroll every 5 seconds
#define MANUAL_PAUSE_MS 15000 // Pause auto scroll 15s after button press
// ── Humidity Thresholds ────────────────────────────────────────
#define HUMIDITY_ON_THRESHOLD_DEFAULT 70.0
#define HUMIDITY_OFF_THRESHOLD_DEFAULT 90.0
#define HUMIDITY_MIN_VALID 40.0
#define HUMIDITY_MAX_VALID 99.0
#define TRIGGER_DELAY 10000
// ── Publish ────────────────────────────────────────────────────
#define PUBLISH_DELTA 0.5
#define BACKOFF_MIN 2000
#define BACKOFF_MAX 60000
#define MQTT_MAX_PAYLOAD 128
// ══════════════════════════════════════════════════════════════
// CONTROL MODE
// ══════════════════════════════════════════════════════════════
enum ControlMode {
AUTO,
FORCED_ON,
FORCED_OFF
};
DHT dht(DHTPIN, DHTTYPE);
WiFiClient espClient;
PubSubClient mqtt(espClient);
LiquidCrystal_I2C lcd(LCD_ADDR, LCD_COLS, LCD_ROWS);
// ── Runtime State ──────────────────────────────────────────────
ControlMode mode = AUTO;
bool humidifierOn = false;
bool sensorFaultActive = false;
bool lowHumTimerActive = false;
// ── Thresholds ─────────────────────────────────────────────────
float humidityOnThreshold = HUMIDITY_ON_THRESHOLD_DEFAULT;
float humidityOffThreshold = HUMIDITY_OFF_THRESHOLD_DEFAULT;
// ── Sensor Values (shared with LCD) ───────────────────────────
float currentTemp = 0;
float currentHumidity = 0;
// ── Timestamps ─────────────────────────────────────────────────
unsigned long lastReadTime = 0;
unsigned long lastValidReadTime = 0;
unsigned long lastSystemPublish = 0;
unsigned long lastTelemetryPublish = 0;
unsigned long lowHumStartTime = 0;
unsigned long lastReconnectAttempt = 0;
unsigned long backoffDelay = BACKOFF_MIN;
unsigned long bootTime = 0;
// ── Button State ───────────────────────────────────────────────
unsigned long btnAutoLastChange = 0;
unsigned long btnForcedOnLastChange = 0;
unsigned long btnForcedOffLastChange = 0;
unsigned long btnScreenLastChange = 0;
bool btnAutoLastState = HIGH;
bool btnForcedOnLastState = HIGH;
bool btnForcedOffLastState = HIGH;
bool btnScreenLastState = HIGH;
bool btnAutoArmed = true;
bool btnForcedOnArmed = true;
bool btnForcedOffArmed = true;
bool btnScreenArmed = true;
// ── LED Blink State ────────────────────────────────────────────
unsigned long lastWifiBlinkTime = 0;
unsigned long lastMqttBlinkTime = 0;
unsigned long lastFaultBlinkTime = 0;
bool wifiBlinkState = false;
bool mqttBlinkState = false;
bool faultBlinkState = false;
// ── LCD State ──────────────────────────────────────────────────
uint8_t currentScreen = 0; // 0=Environment 1=Network 2=System
unsigned long lastScreenChange = 0; // Last auto scroll time
unsigned long lastButtonPress = 0; // Last screen button press time
bool manualPauseActive = false; // True = auto scroll paused
unsigned long lastLCDRefresh = 0; // Last LCD update time
// ── Last Published Values ──────────────────────────────────────
float lastPublishedTemp = -999;
float lastPublishedHumidity = -999;
bool lastPublishedHumState = false;
// ── Forward Declarations ───────────────────────────────────────
void setHumidifier(bool state, const char* reason);
void publishTelemetry(float tempC, float humidity);
void publishSystem();
void publishFault(const char* message);
void publishModeChange(const char* source);
void handleCommand(const char* payload);
void updateLEDs();
void handleButtons();
void updateLCD();
void lcdPrintPadded(const char* text, uint8_t col, uint8_t row, uint8_t width);
// ── Generate unique MQTT client ID ────────────────────────────
void getMQTTClientId(char* buffer, size_t size) {
uint64_t mac = ESP.getEfuseMac();
snprintf(buffer, size, "ESP32_Humidifier_%04X%08X",
(uint16_t)(mac >> 32),
(uint32_t)mac
);
}
// ── LCD padded print — overwrites in place, no lcd.clear() ────
// Pads with spaces to erase leftover characters from previous value
// Prevents flicker caused by clearing the whole display
void lcdPrintPadded(const char* text, uint8_t col, uint8_t row, uint8_t width) {
lcd.setCursor(col, row);
uint8_t len = strlen(text);
lcd.print(text);
for (uint8_t i = len; i < width; i++) lcd.print(' ');
}
// ── Set relay output ──────────────────────────────────────────
void setHumidifier(bool state, const char* reason) {
if (humidifierOn == state) return;
humidifierOn = state;
digitalWrite(HUMIDIFIER_PIN, state ? RELAY_ON : RELAY_OFF);
if (mqtt.connected()) {
mqtt.publish(TOPIC_HUMIDIFIER_STATE,
humidifierOn ? "true" : "false", true);
}
Serial.printf(">> Humidifier %s — %s\n", state ? "ON" : "OFF", reason);
}
// ── Publish mode change with source tag ───────────────────────
void publishModeChange(const char* source) {
if (!mqtt.connected()) return;
char payload[96];
snprintf(payload, sizeof(payload),
"{\"mode\":\"%s\",\"source\":\"%s\"}",
mode == AUTO ? "AUTO" : mode == FORCED_ON ? "FORCED_ON" : "FORCED_OFF",
source
);
mqtt.publish(TOPIC_MODE, payload);
Serial.printf(">> Mode published: %s\n", payload);
}
// ── Publish telemetry ─────────────────────────────────────────
void publishTelemetry(float tempC, float humidity) {
char jsonPayload[128];
snprintf(jsonPayload, sizeof(jsonPayload),
"{\"temperature\":%.2f,\"humidity\":%.2f,\"humidifier\":%s,\"mode\":\"%s\"}",
tempC, humidity,
humidifierOn ? "true" : "false",
mode == AUTO ? "AUTO" : mode == FORCED_ON ? "FORCED_ON" : "FORCED_OFF"
);
mqtt.publish(TOPIC_TELEMETRY, jsonPayload);
char buf[16];
snprintf(buf, sizeof(buf), "%.2f", tempC);
mqtt.publish(TOPIC_TEMP, buf);
snprintf(buf, sizeof(buf), "%.2f", humidity);
mqtt.publish(TOPIC_HUMIDITY, buf);
mqtt.publish(TOPIC_HUMIDIFIER_STATE,
humidifierOn ? "true" : "false", true);
lastPublishedTemp = tempC;
lastPublishedHumidity = humidity;
lastPublishedHumState = humidifierOn;
lastTelemetryPublish = millis();
Serial.printf(">> Telemetry published: %s\n", jsonPayload);
}
// ── Publish system diagnostics ────────────────────────────────
void publishSystem() {
char payload[128];
snprintf(payload, sizeof(payload),
"{\"rssi\":%d,\"heap\":%lu,\"uptime\":%lu}",
WiFi.RSSI(),
(unsigned long)ESP.getFreeHeap(),
(millis() - bootTime) / 1000
);
mqtt.publish(TOPIC_SYSTEM, payload);
Serial.printf(">> System published: %s\n", payload);
lastSystemPublish = millis();
}
// ── Publish fault ─────────────────────────────────────────────
void publishFault(const char* message) {
char payload[128];
snprintf(payload, sizeof(payload),
"{\"fault\":\"%s\",\"uptime\":%lu}",
message,
(millis() - bootTime) / 1000
);
mqtt.publish(TOPIC_FAULT, payload);
Serial.printf(">> FAULT published: %s\n", payload);
}
// ── MQTT command handler ───────────────────────────────────────
void handleCommand(const char* payload) {
StaticJsonDocument<128> doc;
DeserializationError err = deserializeJson(doc, payload);
if (err) {
Serial.printf(">> Command parse error: %s\n", err.c_str());
return;
}
if (doc.containsKey("override")) {
const char* val = doc["override"];
if (strcmp(val, "ON") == 0) {
mode = FORCED_ON;
setHumidifier(true, "manual override ON via MQTT");
publishModeChange("mqtt");
} else if (strcmp(val, "OFF") == 0) {
mode = FORCED_OFF;
setHumidifier(false, "manual override OFF via MQTT");
publishModeChange("mqtt");
} else if (strcmp(val, "AUTO") == 0) {
mode = AUTO;
Serial.println(">> Mode set to AUTO via MQTT");
publishModeChange("mqtt");
} else {
Serial.println(">> Unknown override value — ignored");
}
}
float newOn = doc.containsKey("threshold_on") ? (float)doc["threshold_on"] : humidityOnThreshold;
float newOff = doc.containsKey("threshold_off") ? (float)doc["threshold_off"] : humidityOffThreshold;
if (doc.containsKey("threshold_on") || doc.containsKey("threshold_off")) {
if (newOn < HUMIDITY_MIN_VALID || newOn > HUMIDITY_MAX_VALID ||
newOff < HUMIDITY_MIN_VALID || newOff > HUMIDITY_MAX_VALID ||
newOn >= newOff) {
Serial.printf(">> Threshold rejected — invalid (on:%.1f off:%.1f)\n", newOn, newOff);
} else {
humidityOnThreshold = newOn;
humidityOffThreshold = newOff;
Serial.printf(">> Thresholds updated — ON:%.1f%% OFF:%.1f%%\n",
humidityOnThreshold, humidityOffThreshold);
}
}
}
// ── MQTT message callback ──────────────────────────────────────
void onMQTTMessage(char* topic, byte* payload, unsigned int length) {
if (length > MQTT_MAX_PAYLOAD) {
Serial.printf(">> Command rejected — payload too large (%u bytes)\n", length);
return;
}
char message[MQTT_MAX_PAYLOAD + 1];
unsigned int copyLen = min(length, (unsigned int)MQTT_MAX_PAYLOAD);
memcpy(message, payload, copyLen);
message[copyLen] = '\0';
Serial.printf(">> Command received [%s]: %s\n", topic, message);
handleCommand(message);
}
// ── Non-blocking reconnect ────────────────────────────────────
void maintainConnections() {
if (millis() - lastReconnectAttempt < backoffDelay) return;
lastReconnectAttempt = millis();
if (WiFi.status() != WL_CONNECTED) {
Serial.printf(">> WiFi not connected — reconnecting (backoff: %lus)\n",
backoffDelay / 1000);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
backoffDelay = min(backoffDelay * 2, (unsigned long)BACKOFF_MAX);
return;
}
if (!mqtt.connected()) {
Serial.printf(">> MQTT not connected — reconnecting (backoff: %lus)\n",
backoffDelay / 1000);
char clientId[32];
getMQTTClientId(clientId, sizeof(clientId));
if (mqtt.connect(clientId, TOPIC_STATUS, 1, true, "offline")) {
mqtt.publish(TOPIC_STATUS, "online", true);
mqtt.subscribe(TOPIC_COMMAND);
Serial.println(">> MQTT reconnected — subscribed to command topic");
backoffDelay = BACKOFF_MIN;
} else {
Serial.printf(">> MQTT failed RC=%d — will retry\n", mqtt.state());
backoffDelay = min(backoffDelay * 2, (unsigned long)BACKOFF_MAX);
}
return;
}
backoffDelay = BACKOFF_MIN;
}
// ── Humidifier auto control ────────────────────────────────────
void controlHumidifier(float humidity) {
if (mode != AUTO) return;
if (!humidifierOn) {
if (humidity < humidityOnThreshold) {
if (!lowHumTimerActive) {
lowHumStartTime = millis();
lowHumTimerActive = true;
Serial.printf(">> Humidity %.1f%% below %.1f%% — starting 10s timer\n",
humidity, humidityOnThreshold);
} else {
unsigned long elapsed = millis() - lowHumStartTime;
if (elapsed >= TRIGGER_DELAY) {
setHumidifier(true, "humidity below threshold for 10s");
lowHumTimerActive = false;
} else {
Serial.printf(">> Timer running: %lus / 10s\n", elapsed / 1000);
}
}
} else {
if (lowHumTimerActive) {
lowHumTimerActive = false;
Serial.println(">> Humidity recovered — 10s timer reset");
}
}
} else {
if (humidity >= humidityOffThreshold) {
setHumidifier(false, "humidity reached off threshold");
}
}
}
// ── Sensor failsafe ───────────────────────────────────────────
void checkSensorFailsafe() {
if (millis() - lastValidReadTime <= SENSOR_TIMEOUT) return;
if (!sensorFaultActive) {
sensorFaultActive = true;
if (mode == AUTO) {
setHumidifier(false, "sensor fault failsafe");
Serial.println(">> SENSOR FAULT — humidifier forced OFF (AUTO mode)");
if (mqtt.connected()) publishFault("sensor_timeout_auto_off");
} else if (mode == FORCED_ON) {
Serial.println(">> SENSOR FAULT — humidifier stays ON (FORCED_ON active)");
Serial.println(" WARNING: Operating blind — consider switching to AUTO");
if (mqtt.connected()) publishFault("sensor_timeout_override_active");
} else {
Serial.println(">> SENSOR FAULT — humidifier already OFF (FORCED_OFF mode)");
if (mqtt.connected()) publishFault("sensor_timeout_already_off");
}
}
}
// ── Sensor recovery ───────────────────────────────────────────
void handleSensorRecovery() {
if (!sensorFaultActive) return;
sensorFaultActive = false;
lowHumTimerActive = false;
Serial.println(">> Sensor recovered — resuming normal operation");
if (mqtt.connected()) publishFault("sensor_recovered");
}
// ══════════════════════════════════════════════════════════════
// BUTTON HANDLING
// Debounce: 40ms stable window
// Edge detection: fires once per press, waits for release
// Network independent — always runs
// ══════════════════════════════════════════════════════════════
void handleButtons() {
unsigned long now = millis();
// ── AUTO button ───────────────────────────────────────────
bool autoRead = digitalRead(BTN_AUTO);
if (autoRead != btnAutoLastState) {
btnAutoLastChange = now;
btnAutoLastState = autoRead;
}
if ((now - btnAutoLastChange) >= DEBOUNCE_MS) {
if (autoRead == LOW && btnAutoArmed) {
// Do not immediately toggle relay
// Control loop decides based on sensor reading
mode = AUTO;
lowHumTimerActive = false;
btnAutoArmed = false;
Serial.println(">> AUTO mode set via button");
publishModeChange("button");
}
if (autoRead == HIGH) btnAutoArmed = true;
}
// ── FORCED_ON button ──────────────────────────────────────
bool forcedOnRead = digitalRead(BTN_FORCED_ON);
if (forcedOnRead != btnForcedOnLastState) {
btnForcedOnLastChange = now;
btnForcedOnLastState = forcedOnRead;
}
if ((now - btnForcedOnLastChange) >= DEBOUNCE_MS) {
if (forcedOnRead == LOW && btnForcedOnArmed) {
mode = FORCED_ON;
setHumidifier(true, "manual override ON via button");
btnForcedOnArmed = false;
publishModeChange("button");
}
if (forcedOnRead == HIGH) btnForcedOnArmed = true;
}
// ── FORCED_OFF button ─────────────────────────────────────
bool forcedOffRead = digitalRead(BTN_FORCED_OFF);
if (forcedOffRead != btnForcedOffLastState) {
btnForcedOffLastChange = now;
btnForcedOffLastState = forcedOffRead;
}
if ((now - btnForcedOffLastChange) >= DEBOUNCE_MS) {
if (forcedOffRead == LOW && btnForcedOffArmed) {
mode = FORCED_OFF;
setHumidifier(false, "manual override OFF via button");
btnForcedOffArmed = false;
publishModeChange("button");
}
if (forcedOffRead == HIGH) btnForcedOffArmed = true;
}
// ── SCREEN button ─────────────────────────────────────────
// Fault active — ignore screen button entirely
bool screenRead = digitalRead(BTN_SCREEN);
if (screenRead != btnScreenLastState) {
btnScreenLastChange = now;
btnScreenLastState = screenRead;
}
if ((now - btnScreenLastChange) >= DEBOUNCE_MS) {
if (screenRead == LOW && btnScreenArmed && !sensorFaultActive) {
currentScreen = (currentScreen + 1) % 3; // Cycle 0→1→2→0
lastButtonPress = now;
manualPauseActive = true; // Pause auto scroll
btnScreenArmed = false;
Serial.printf(">> Screen → %d (manual)\n", currentScreen);
}
if (screenRead == HIGH) btnScreenArmed = true;
}
}
// ══════════════════════════════════════════════════════════════
// LED MANAGER
// Priority:
// 1. Sensor fault → fast blink red (overrides all mode LEDs)
// 2. FORCED_OFF → solid red
// 3. FORCED_ON → solid orange
// 4. AUTO → solid green
// WiFi, MQTT, Humidifier LEDs are independent
// ══════════════════════════════════════════════════════════════
void updateLEDs() {
unsigned long now = millis();
// ── WiFi LED ─────────────────────────────────────────────
if (WiFi.status() == WL_CONNECTED) {
digitalWrite(LED_WIFI, HIGH);
} else {
if (now - lastWifiBlinkTime >= WIFI_BLINK_MS) {
wifiBlinkState = !wifiBlinkState;
lastWifiBlinkTime = now;
digitalWrite(LED_WIFI, wifiBlinkState ? HIGH : LOW);
}
}
// ── MQTT LED ─────────────────────────────────────────────
if (mqtt.connected()) {
digitalWrite(LED_MQTT, HIGH);
} else {
if (now - lastMqttBlinkTime >= MQTT_BLINK_MS) {
mqttBlinkState = !mqttBlinkState;
lastMqttBlinkTime = now;
digitalWrite(LED_MQTT, mqttBlinkState ? HIGH : LOW);
}
}
// ── Humidifier LED ────────────────────────────────────────
digitalWrite(LED_HUMIDIFIER, humidifierOn ? HIGH : LOW);
// ── Mode LEDs with priority ──────────────────────────────
if (sensorFaultActive) {
// Priority 1 — fast blink red, all other mode LEDs off
digitalWrite(LED_AUTO, LOW);
digitalWrite(LED_FORCED_ON, LOW);
if (now - lastFaultBlinkTime >= FAULT_BLINK_MS) {
faultBlinkState = !faultBlinkState;
lastFaultBlinkTime = now;
digitalWrite(LED_FORCED_OFF, faultBlinkState ? HIGH : LOW);
}
} else if (mode == FORCED_OFF) {
// Priority 2 — solid red
digitalWrite(LED_AUTO, LOW);
digitalWrite(LED_FORCED_ON, LOW);
digitalWrite(LED_FORCED_OFF, HIGH);
} else if (mode == FORCED_ON) {
// Priority 3 — solid orange
digitalWrite(LED_AUTO, LOW);
digitalWrite(LED_FORCED_ON, HIGH);
digitalWrite(LED_FORCED_OFF, LOW);
} else {
// Priority 4 — AUTO solid green
digitalWrite(LED_AUTO, HIGH);
digitalWrite(LED_FORCED_ON, LOW);
digitalWrite(LED_FORCED_OFF, LOW);
}
}
// ══════════════════════════════════════════════════════════════
// LCD MANAGER
// Screens:
// 0 — Environment: temp, humidity, mode, humidifier state
// 1 — Network: WiFi and MQTT status
// 2 — System: heap and uptime
// Fault override: shows SENSOR ERROR, ignores button
// Auto scroll: every 5s, pauses 15s after manual button press
// No lcd.clear() in loop — overwrites in place with space padding
// ══════════════════════════════════════════════════════════════
void updateLCD() {
unsigned long now = millis();
// Throttle LCD updates to avoid flicker
if (now - lastLCDRefresh < LCD_REFRESH_MS) return;
lastLCDRefresh = now;
// ── Resume auto scroll after manual pause ────────────────
if (manualPauseActive && (now - lastButtonPress >= MANUAL_PAUSE_MS)) {
manualPauseActive = false;
}
// ── Auto scroll — only when not manually paused ──────────
if (!manualPauseActive && !sensorFaultActive) {
if (now - lastScreenChange >= SCREEN_INTERVAL) {
currentScreen = (currentScreen + 1) % 3;
lastScreenChange = now;
}
}
char line1[17]; // 16 chars + null
char line2[17];
// ── Fault override — highest priority ────────────────────
if (sensorFaultActive) {
lcdPrintPadded("SENSOR ERROR ", 0, 0, 16);
lcdPrintPadded("Check DHT22 ", 0, 1, 16);
return;
}
// ── Screen 0 — Environment ───────────────────────────────
if (currentScreen == 0) {
snprintf(line1, sizeof(line1), "T:%.1fC H:%.1f%%",
currentTemp, currentHumidity);
snprintf(line2, sizeof(line2), "%-8s HUM:%s",
mode == AUTO ? "AUTO" : mode == FORCED_ON ? "F-ON" : "F-OFF",
humidifierOn ? "ON " : "OFF");
lcdPrintPadded(line1, 0, 0, 16);
lcdPrintPadded(line2, 0, 1, 16);
}
// ── Screen 1 — Network ───────────────────────────────────
else if (currentScreen == 1) {
snprintf(line1, sizeof(line1), "WiFi: %s",
WiFi.status() == WL_CONNECTED ? "OK " : "---- ");
snprintf(line2, sizeof(line2), "MQTT: %s",
mqtt.connected() ? "OK " : "---- ");
lcdPrintPadded(line1, 0, 0, 16);
lcdPrintPadded(line2, 0, 1, 16);
}
// ── Screen 2 — System ────────────────────────────────────
else {
unsigned long upSec = (now - bootTime) / 1000;
unsigned long upMin = upSec / 60;
unsigned long upHour = upMin / 60;
upMin = upMin % 60;
upSec = upSec % 60;
snprintf(line1, sizeof(line1), "Heap:%luk",
(unsigned long)ESP.getFreeHeap() / 1024);
snprintf(line2, sizeof(line2), "Up:%02lu:%02lu:%02lu",
upHour, upMin, upSec);
lcdPrintPadded(line1, 0, 0, 16);
lcdPrintPadded(line2, 0, 1, 16);
}
}
void setup() {
Serial.begin(BAUD_RATE);
delay(1000);
bootTime = millis();
// ── Watchdog ────────────────────────────────────────────
esp_task_wdt_deinit();
esp_task_wdt_config_t wdt_config = {
.timeout_ms = 60000,
.idle_core_mask = 0,
.trigger_panic = true
};
esp_task_wdt_init(&wdt_config);
esp_task_wdt_add(NULL);
Serial.println("Watchdog registered");
// ── Humidifier pin ──────────────────────────────────────
pinMode(HUMIDIFIER_PIN, OUTPUT);
digitalWrite(HUMIDIFIER_PIN, RELAY_OFF);
Serial.println("Humidifier OFF on boot");
// ── Button pins ─────────────────────────────────────────
pinMode(BTN_AUTO, INPUT_PULLUP);
pinMode(BTN_FORCED_ON, INPUT_PULLUP);
pinMode(BTN_FORCED_OFF, INPUT_PULLUP);
pinMode(BTN_SCREEN, INPUT_PULLUP);
// ── LED pins ────────────────────────────────────────────
pinMode(LED_WIFI, OUTPUT);
pinMode(LED_MQTT, OUTPUT);
pinMode(LED_HUMIDIFIER, OUTPUT);
pinMode(LED_AUTO, OUTPUT);
pinMode(LED_FORCED_ON, OUTPUT);
pinMode(LED_FORCED_OFF, OUTPUT);
// ── Startup LED test ────────────────────────────────────
// All LEDs ON 400ms — confirms wiring and LED health on boot
digitalWrite(LED_WIFI, HIGH);
digitalWrite(LED_MQTT, HIGH);
digitalWrite(LED_HUMIDIFIER, HIGH);
digitalWrite(LED_AUTO, HIGH);
digitalWrite(LED_FORCED_ON, HIGH);
digitalWrite(LED_FORCED_OFF, HIGH);
delay(400);
digitalWrite(LED_WIFI, LOW);
digitalWrite(LED_MQTT, LOW);
digitalWrite(LED_HUMIDIFIER, LOW);
digitalWrite(LED_AUTO, LOW);
digitalWrite(LED_FORCED_ON, LOW);
digitalWrite(LED_FORCED_OFF, LOW);
Serial.println("LED test complete");
// ── LCD init ────────────────────────────────────────────
// Uses default I2C pins — SDA:GPIO21 SCL:GPIO22
// Wire.begin() with no arguments — maximum library compatibility
Wire.begin();
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("Greenhouse v3.0 ");
lcd.setCursor(0, 1);
lcd.print("Initializing... ");
Serial.println("LCD initialized");
// ── DHT22 ───────────────────────────────────────────────
dht.begin();
lastValidReadTime = millis();
// ── WiFi with 15s timeout ───────────────────────────────
Serial.printf("Connecting to WiFi: %s\n", WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long wifiStart = millis();
while (WiFi.status() != WL_CONNECTED) {
esp_task_wdt_reset();
if (millis() - wifiStart > WIFI_TIMEOUT) {
Serial.println("\nWiFi timeout — continuing. Humidifier runs regardless.");
break;
}
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\nWiFi connected — IP: %s\n", WiFi.localIP().toString().c_str());
}
// ── MQTT with 15s timeout ───────────────────────────────
if (WiFi.status() == WL_CONNECTED) {
mqtt.setServer(MQTT_BROKER, MQTT_PORT);
mqtt.setCallback(onMQTTMessage);
char clientId[32];
getMQTTClientId(clientId, sizeof(clientId));
Serial.printf("Connecting to MQTT as: %s\n", clientId);
unsigned long mqttStart = millis();
while (!mqtt.connected()) {
esp_task_wdt_reset();
if (millis() - mqttStart > MQTT_TIMEOUT) {
Serial.println("MQTT timeout — continuing. Humidifier runs regardless.");
break;
}
esp_task_wdt_reset();
bool connected = mqtt.connect(clientId, TOPIC_STATUS, 1, true, "offline");
esp_task_wdt_reset();
if (connected) {
mqtt.publish(TOPIC_STATUS, "online", true);
mqtt.subscribe(TOPIC_COMMAND);
Serial.println("MQTT connected — subscribed to command topic");
backoffDelay = BACKOFF_MIN;
break;
}
esp_task_wdt_reset();
delay(1000);
Serial.print(".");
}
}
// ── Clear LCD and show ready ─────────────────────────────
lcd.setCursor(0, 0);
lcd.print("System Ready ");
lcd.setCursor(0, 1);
lcd.print(" ");
delay(800);
Serial.println("--------------------------------------------------");
Serial.println("Version 3.0 Ready — ESP32 Humidifier Controller");
Serial.printf("Thresholds — ON:%.1f%% OFF:%.1f%%\n",
humidityOnThreshold, humidityOffThreshold);
Serial.println("Mode: AUTO");
Serial.println("Buttons : GPIO32/33/25/26");
Serial.println("LEDs : GPIO16/17/27/19/23/14");
Serial.println("LCD I2C : SDA=GPIO21 SCL=GPIO22");
Serial.println("Relay : GPIO18");
Serial.println("--------------------------------------------------");
}
void loop() {
esp_task_wdt_reset();
// Network maintenance — non-blocking
maintainConnections();
if (mqtt.connected()) mqtt.loop();
// Sensor failsafe — always runs
checkSensorFailsafe();
// Button handling — always runs, network independent
handleButtons();
// LED update — always runs
updateLEDs();
// LCD update — always runs, throttled to 500ms
updateLCD();
// ── Sensor read ─────────────────────────────────────────
if (millis() - lastReadTime < READ_INTERVAL) return;
lastReadTime = millis();
float humidity = dht.readHumidity();
float tempC = dht.readTemperature();
if (isnan(humidity) || isnan(tempC) ||
humidity == 0 || tempC == 0 ||
humidity < 1.0 || humidity > 99.9 ||
tempC < 5.0 || tempC > 60.0) {
Serial.printf("WARNING: Reading rejected — Temp:%.2f Humidity:%.2f\n",
tempC, humidity);
return;
}
// Store for LCD use
currentTemp = tempC;
currentHumidity = humidity;
lastValidReadTime = millis();
handleSensorRecovery();
Serial.printf("Temp: %.2f°C | Humidity: %.2f%% | Humidifier: %s | Mode: %s\n",
tempC, humidity,
humidifierOn ? "ON" : "OFF",
mode == AUTO ? "AUTO" : mode == FORCED_ON ? "FORCED_ON" : "FORCED_OFF"
);
controlHumidifier(humidity);
if (mqtt.connected()) {
bool tempChanged = abs(tempC - lastPublishedTemp) >= PUBLISH_DELTA;
bool humChanged = abs(humidity - lastPublishedHumidity) >= PUBLISH_DELTA;
bool stateChanged = humidifierOn != lastPublishedHumState;
bool heartbeatElapsed = (millis() - lastTelemetryPublish) >= HEARTBEAT_MAX;
if (tempChanged || humChanged || stateChanged || heartbeatElapsed) {
publishTelemetry(tempC, humidity);
}
if (millis() - lastSystemPublish >= SYSTEM_INTERVAL) {
publishSystem();
}
}
}