// ============================================================
// UNIVERSAL ESP32 Zone Firmware v4 — EcoSense IoT System
// 3 DEDICATED POTENTIOMETERS per board:
// Pin 32 → PM2.5 sensor (dedicated)
// Pin 34 → CO2 sensor (dedicated)
// Pin 35 → Zone-specific sensor (SO2/O3/NH3/CO)
// Pin 4 → DHT22 (Temp + Humidity)
// Pin 13 → LED (Actuator)
// Zone auto-detected via last hex digit of MAC address
// Dashboard control: zones/<id>/control → {"state":"ON"|"OFF"}
// Threshold updates: zones/<id>/threshold → {"pm25":50,"co2":600,"spec":9}
// ============================================================
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <DHT.h>
#define MQTT_BUFFER 512
#define DHTPIN 4
#define DHTTYPE DHT22
#define PM25_PIN 32 // Dedicated PM2.5 potentiometer
#define CO2_PIN 34 // Dedicated CO2 potentiometer
#define SPEC_PIN 35 // Zone-specific sensor potentiometer
#define ACT_PIN 13 // LED actuator
const char* SSID = "Wokwi-GUEST";
const char* PASS = "";
const char* BROKER = "broker.emqx.io";
const int PORT = 1883;
struct Zone {
const char* id;
const char* name;
const char* location;
int pm25Lo, pm25Hi;
int co2Lo, co2Hi;
int specLo, specHi;
const char* specKey;
const char* specLabel;
const char* specUnit;
float pm25ThreshDefault;
float co2ThreshDefault;
float specThreshDefault;
const char* actuator;
};
const Zone ZONES[5] = {
{ "residential","Residential","RK Puram",
0,80, 400,700, 0,20, "co","MQ135 CO","ppm",
50.0, 600.0, 9.0, "Ventilation Fans" },
{ "industrial","Industrial","Anand Vihar",
30,150, 600,2000, 0,300, "so2","SO2 Sensor","ug/m3",
75.0, 1000.0, 125.0, "Air Scrubbers" },
{ "commercial","Commercial","ITO",
0,100, 400,900, 0,250, "o3","O3 Ozone","ug/m3",
50.0, 700.0, 100.0, "Air Purifiers" },
{ "agriculture","Agricultural","Narela",
0,60, 400,1200, 0,100, "nh3","NH3 MQ137","ppm",
40.0, 600.0, 25.0, "Mist Sprayers" },
{ "construction","Construction","Rohini",
0,300, 400,650, 0,100, "co","MQ7 CO","ppm",
150.0, 600.0, 35.0, "Water Sprinklers" }
};
// ── Runtime state ──────────────────────────────────────────
int zoneId = 0;
const Zone* Z = nullptr;
char clientId[64], dataTopic[40], ctrlTopic[40], threshTopic[40];
// Thresholds (mutable by MQTT from dashboard)
float pm25Threshold;
float co2Threshold;
float specThreshold;
DHT dht(DHTPIN, DHTTYPE);
WiFiClient net;
PubSubClient mqtt(net);
unsigned long lastMsg = 0;
bool manualOverride = false;
unsigned long overrideStart = 0;
const long OVERRIDE_DUR = 30000; // 30 s manual override window
// ───────────────────────────────────────────────────────────
void updateZone() {
Z = &ZONES[zoneId];
// Reset thresholds to defaults on zone change
pm25Threshold = Z->pm25ThreshDefault;
co2Threshold = Z->co2ThreshDefault;
specThreshold = Z->specThreshDefault;
uint64_t chipId = ESP.getEfuseMac();
uint32_t macLow = chipId & 0xFFFFFFFF;
snprintf(clientId, sizeof(clientId), "esp32_%s_%08X", Z->id, macLow);
snprintf(dataTopic, sizeof(dataTopic), "zones/%s/data", Z->id);
snprintf(ctrlTopic, sizeof(ctrlTopic), "zones/%s/control", Z->id);
snprintf(threshTopic,sizeof(threshTopic),"zones/%s/threshold", Z->id);
Serial.println("\n╔══════════════════════════════════╗");
Serial.printf( "║ ACTIVE ZONE : %-17s ║\n", Z->name);
Serial.printf( "║ Location : %-17s ║\n", Z->location);
Serial.printf( "║ Actuator : %-17s ║\n", Z->actuator);
Serial.println("╚══════════════════════════════════╝\n");
Serial.printf("[THRESH] PM2.5=%.1f CO2=%.1f %s=%.1f\n",
pm25Threshold, co2Threshold, Z->specLabel, specThreshold);
}
// ── WiFi ───────────────────────────────────────────────────
void connectWiFi() {
WiFi.begin(SSID, PASS);
Serial.print("WiFi connecting");
while(WiFi.status() != WL_CONNECTED){ delay(400); Serial.print("."); }
Serial.println(" ✓ IP=" + WiFi.localIP().toString());
}
// ── MQTT callback ──────────────────────────────────────────
void onMessage(char* topic, byte* payload, unsigned int len) {
String msg;
for(unsigned int i = 0; i < len; i++) msg += (char)payload[i];
StaticJsonDocument<256> doc;
if(deserializeJson(doc, msg)) return; // parse error → ignore
// ── Actuator control ─────────────────────────
if(String(topic) == String(ctrlTopic)) {
const char* s = doc["state"];
if(s) {
bool on = strcmp(s,"ON") == 0;
digitalWrite(ACT_PIN, on ? HIGH : LOW);
manualOverride = true;
overrideStart = millis();
Serial.println(on
? "🟢 MANUAL → " + String(Z->actuator) + " ON"
: "⛔ MANUAL → " + String(Z->actuator) + " OFF");
}
}
// ── Threshold update from dashboard ──────────
if(String(topic) == String(threshTopic)) {
bool changed = false;
if(doc.containsKey("pm25")) { pm25Threshold = doc["pm25"].as<float>(); changed = true; }
if(doc.containsKey("co2")) { co2Threshold = doc["co2"].as<float>(); changed = true; }
if(doc.containsKey("spec")) { specThreshold = doc["spec"].as<float>(); changed = true; }
if(changed) {
Serial.printf("[THRESH UPDATE] PM2.5=%.1f CO2=%.1f %s=%.1f\n",
pm25Threshold, co2Threshold, Z->specLabel, specThreshold);
}
}
}
// ── MQTT reconnect ────────────────────────────────────────
void ensureConnected() {
while(!mqtt.connected()) {
Serial.print("MQTT reconnecting...");
if(mqtt.connect(clientId)) {
Serial.println(" ✓");
mqtt.subscribe(ctrlTopic);
mqtt.subscribe(threshTopic);
} else {
Serial.printf(" ✗ (rc=%d) retry in 5s\n", mqtt.state());
delay(5000);
}
}
}
// ───────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
delay(200);
// Derive zone from last nibble of MAC
WiFi.mode(WIFI_STA);
String mac = WiFi.macAddress();
char last = mac.charAt(mac.length() - 1);
zoneId = (last >= '0' && last <= '9') ? (last - '0') : (toupper(last) - 'A' + 10);
if(zoneId < 0 || zoneId >= 5) zoneId = 0;
updateZone();
pinMode(ACT_PIN, OUTPUT);
dht.begin();
connectWiFi();
mqtt.setServer(BROKER, PORT);
mqtt.setBufferSize(MQTT_BUFFER);
mqtt.setCallback(onMessage);
randomSeed(analogRead(0));
}
// ───────────────────────────────────────────────────────────
void loop() {
ensureConnected();
mqtt.loop();
if(millis() - lastMsg >= 5000) {
lastMsg = millis();
// ── Read sensors ──────────────────────────────────
float temp = dht.readTemperature();
float hum = dht.readHumidity();
if(isnan(temp)) temp = 28.0f + zoneId * 1.5f;
if(isnan(hum)) hum = 60.0f - zoneId * 4.0f;
// Each sensor has its own dedicated potentiometer
float pm25 = map(analogRead(PM25_PIN), 0, 4095, Z->pm25Lo, Z->pm25Hi);
float co2 = map(analogRead(CO2_PIN), 0, 4095, Z->co2Lo, Z->co2Hi);
float spec = map(analogRead(SPEC_PIN), 0, 4095, Z->specLo, Z->specHi);
// ── Build JSON payload ────────────────────────────
StaticJsonDocument<256> doc;
doc["pm25"] = round(pm25 * 10) / 10.0;
doc["co2"] = round(co2 * 10) / 10.0;
doc["temperature"] = round(temp * 10) / 10.0;
doc["humidity"] = round(hum * 10) / 10.0;
doc[Z->specKey] = round(spec * 10) / 10.0;
doc["zone"] = Z->id;
// Include current thresholds so dashboard can sync on reconnect
doc["thr_pm25"] = pm25Threshold;
doc["thr_co2"] = co2Threshold;
doc["thr_spec"] = specThreshold;
char buf[350];
serializeJson(doc, buf);
mqtt.publish(dataTopic, buf);
Serial.printf("[%s] PM2.5=%.1f(thr:%.0f) CO2=%.1f(thr:%.0f) %s=%.1f(thr:%.0f) T=%.1f H=%.1f\n",
Z->id, pm25, pm25Threshold, co2, co2Threshold,
Z->specLabel, spec, specThreshold, temp, hum);
// ── Threshold check & actuator logic ─────────────
bool alert = (pm25 > pm25Threshold) ||
(co2 > co2Threshold) ||
(spec > specThreshold);
// Manual override expires after OVERRIDE_DUR ms
if(manualOverride && (millis() - overrideStart > OVERRIDE_DUR)) {
manualOverride = false;
Serial.println("🔄 Auto-mode resumed");
}
if(!manualOverride) {
digitalWrite(ACT_PIN, alert ? HIGH : LOW);
if(alert) {
Serial.println("🚨 THRESHOLD EXCEEDED → " + String(Z->actuator) + " ACTIVATED");
// Report which sensor triggered
if(pm25 > pm25Threshold) Serial.printf(" PM2.5 %.1f > %.1f\n", pm25, pm25Threshold);
if(co2 > co2Threshold) Serial.printf(" CO2 %.1f > %.1f\n", co2, co2Threshold);
if(spec > specThreshold) Serial.printf(" %s %.1f > %.1f\n", Z->specLabel, spec, specThreshold);
}
}
}
}
ESP32 #1 — RESIDENTIAL (RK Puram) | PM2.5(32), CO2(34), MQ135(35)
ESP32 #2 — INDUSTRIAL (Anand Vihar) | PM2.5(32), CO2(34), SO2(35)
ESP32 #3 — COMMERCIAL (ITO) | PM2.5(32), CO2(34), O3(35)
ESP32 #4 — AGRICULTURAL (Narela) | PM2.5(32), CO2(34), NH3(35)
ESP32 #5 — CONSTRUCTION (Rohini) | PM2.5(32), CO2(34), CO(35)