#include <DHT.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "esp_sleep.h"
#define DHTPIN 27
#define MQ135_PIN 34
#define RELAY_PIN 26
// Most cheap relay modules are "low level trigger" - the relay energizes
// when IN is pulled LOW, not HIGH. If your fan turns ON at boot and OFF
// when air gets bad (backwards), flip this to false.
#define RELAY_ACTIVE_HIGH true
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
// Eco/power-saving timing. MQ135's heater stays powered the whole time
// (turning it off would ruin its accuracy - see notes), but the OLED and
// the ESP32's own CPU don't need to be fully active constantly.
#define DISPLAY_ON_MS 2000 // how long each reading stays lit on screen
#define LIGHT_SLEEP_US 3000000UL // 3s CPU light sleep between readings (peripherals/sensors stay powered)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
DHT dht(DHTPIN, DHT22);
// Fixed lists of categories instead of loose strings - keeps comparisons
// reliable (no typo-prone string matching) and avoids the heap
// fragmentation that comes from using the String class in loop().
enum AirQuality { AIR_EXCELLENT, AIR_GOOD, AIR_MODERATE, AIR_DANGEROUS };
enum ComfortLevel { COMFORT_COLD, COMFORT_OK, COMFORT_WARM, COMFORT_DRY, COMFORT_HUMID };
// Thresholds calibrated from real readings after 48h burn-in: clean
// room air sat around 100-200, concentrated fumes (nail polish remover
// held close) spiked to 1000-1200. Re-check these if you change
// rooms/seasons - this sensor isn't precision-calibrated.
AirQuality classifyAir(int rawGas) {
if (rawGas < 1650) return AIR_EXCELLENT;
if (rawGas <= 2450) return AIR_GOOD;
if (rawGas <= 3250) return AIR_MODERATE;
return AIR_DANGEROUS;
}
// Rough indoor comfort bands - not lab-grade, but a legitimate first pass:
// >80% RH promotes mold/mustiness, <30% RH dries out skin/airways,
// 18-28 C covers what most people consider a comfortable room.
// (Threshold raised from 70 to 80 for tropical/year-round-humid climates,
// where 70-80% RH is just normal ambient air, not actually a problem.)
ComfortLevel classifyComfort(float t, float h) {
if (h > 80) return COMFORT_HUMID;
if (h < 30) return COMFORT_DRY;
if (t > 28) return COMFORT_WARM;
if (t < 18) return COMFORT_COLD;
return COMFORT_OK;
}
const char* airLabel(AirQuality a) {
switch (a) {
case AIR_EXCELLENT: return "Excellent";
case AIR_GOOD: return "Good";
case AIR_MODERATE: return "Moderate";
default: return "DANGEROUS!";
}
}
const char* comfortLabel(ComfortLevel c) {
switch (c) {
case COMFORT_COLD: return "Cold";
case COMFORT_WARM: return "Warm";
case COMFORT_DRY: return "Dry";
case COMFORT_HUMID: return "Humid";
default: return "OK";
}
}
// This is the actual "decision" step - combining gas + comfort into one
// verdict is what makes this edge computing instead of a passive display.
const char* overallLabel(AirQuality a, ComfortLevel c) {
if (a == AIR_DANGEROUS) return "VENTILATE NOW!";
if (a == AIR_MODERATE) return "Ventilate soon";
if (c == COMFORT_HUMID) return "Use dehumidifier";
if (c == COMFORT_DRY) return "Try a humidifier";
if (c == COMFORT_COLD || c == COMFORT_WARM) return "Adjust temp";
return "All Good";
}
void setFan(bool on) {
bool level = RELAY_ACTIVE_HIGH ? on : !on;
digitalWrite(RELAY_PIN, level ? HIGH : LOW);
}
void setup() {
Serial.begin(115200);
dht.begin();
pinMode(RELAY_PIN, OUTPUT);
setFan(false); // fan off until the first bad reading says otherwise
Wire.begin(21, 22);
delay(250);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("--- ECO MONITOR ---");
display.println("Warming up sensors...");
display.display();
Serial.println("System Initialized. Starting Real-Time Monitoring...");
delay(2000);
}
void loop() {
float humidity = dht.readHumidity();
float temperature = dht.readTemperature();
int raw_gas_voltage = analogRead(MQ135_PIN);
if (isnan(humidity) || isnan(temperature)) {
Serial.println("Failed to read from DHT sensor! Retrying...");
esp_sleep_enable_timer_wakeup(LIGHT_SLEEP_US);
esp_light_sleep_start();
return;
}
AirQuality air = classifyAir(raw_gas_voltage);
ComfortLevel comfort = classifyComfort(temperature, humidity);
const char* air_status = airLabel(air);
const char* comfort_status = comfortLabel(comfort);
const char* overall_status = overallLabel(air, comfort);
// The actual "edge computing acts on its own decision" step: fan runs
// automatically whenever air quality is Moderate or worse, no cloud,
// no app, no human required.
bool fanOn = (air == AIR_MODERATE || air == AIR_DANGEROUS);
setFan(fanOn);
Serial.printf("Temp: %.1f C | Hum: %.1f %% | Gas ADC: %d | Air: %s | Comfort: %s | Overall: %s | Fan: %s\n",
temperature, humidity, raw_gas_voltage, air_status, comfort_status, overall_status,
fanOn ? "ON" : "OFF");
display.clearDisplay();
display.setCursor(0, 0);
display.printf("Temp: %.1f C\n", temperature);
display.printf("Humidity: %.1f %%\n", humidity);
display.printf("Gas ADC: %d\n", raw_gas_voltage);
display.printf("Air: %s\n", air_status);
display.printf("Comfort: %s\n", comfort_status);
display.println("Overall:");
display.println(overall_status);
display.printf("Fan: %s\n", fanOn ? "ON" : "OFF");
display.display();
// Keep the reading visible for a bit, then sleep the screen and let the
// ESP32's CPU idle until the next reading is due. MQ135's heater, RAM,
// and all other peripherals stay powered through this - only the
// screen backlight and the CPU's own active power draw are affected.
delay(DISPLAY_ON_MS);
display.ssd1306_command(SSD1306_DISPLAYOFF);
esp_sleep_enable_timer_wakeup(LIGHT_SLEEP_US);
esp_light_sleep_start();
display.ssd1306_command(SSD1306_DISPLAYON);
}