// ============================================================
// COMBINED ESP32 FIRMWARE — NON-BLOCKING VERSION
// BET Electrical – IoT Project
//
// THE CORE FIX:
// The previous version used delay(30000) which froze the entire
// chip. Nothing could respond — not the buzzer, not the LED,
// not MQTT callbacks — until that 30 seconds expired.
//
// This version uses millis() timers. millis() just reads the
// internal clock — it never pauses the chip. Each subsystem
// checks "has enough time passed?" and runs if yes, skips if no.
// The loop() runs hundreds of times per second so MQTT callbacks
// fire instantly every time.
//
// TIMING SCHEDULE:
// DHT22 + buzzer alert → every 2 seconds
// Weather API fetch → every 30 seconds
// Pollution API fetch → every 35 seconds (offset from weather)
// ============================================================
#include <WiFi.h>
#include <PubSubClient.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <DHT.h>
// ── WIFI ────────────────────────────────────────────────────
const char* ssid = "Wokwi-GUEST";
const char* password = "";
// ── MQTT ────────────────────────────────────────────────────
const char* mqtt_server = "broker.hivemq.com";
// Outbound topics — ESP32 publishes to these
const char* mqtt_topic_weather = "weather/suburbs";
const char* mqtt_topic_summary = "weather/summary";
const char* mqtt_topic_pollution = "weather/Pollution";
const char* mqtt_topic_alert = "alert/threshold";
const char* mqtt_topic_sensor = "alert/sensor";
// Inbound topics — ESP32 listens to these
const char* mqtt_topic_threshold = "alert/set"; // Node-RED slider → new threshold value
const char* mqtt_topic_buzzer = "alert/buzzer"; // Node-RED → "ON" or "OFF" buzzer override
const char* mqtt_topic_pwm = "Moola"; // Node-RED → 0–255 LED brightness
// ── API KEY ─────────────────────────────────────────────────
const char* apiKey = "1e4fad19e905cfb8553e1a004d2153d1";
// ── DHT22 ───────────────────────────────────────────────────
#define DHTPIN 26
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// ── BUZZER ──────────────────────────────────────────────────
#define BUZZERPIN 15
#define BUZZ_FREQ 1000
#define BUZZ_RESOLUTION 8
// ── LED ─────────────────────────────────────────────────────
#define LEDPIN 13
#define LED_FREQ 5000
#define LED_RESOLUTION 8
// ── THRESHOLDS & STATE ──────────────────────────────────────
float USER_THRESHOLD = 30.0; // Updated live by Node-RED slider
float ALERT_THRESHOLD = 3.0; // Max deviation between sensor and suburb average
bool lastBuzzerState = false; // Tracks buzzer state to avoid printing every loop cycle
// ── NON-BLOCKING TIMERS ─────────────────────────────────────
// These store the last time each task ran.
// unsigned long holds up to ~49 days of millis() — safe.
unsigned long lastDHTTime = 0;
unsigned long lastWeatherTime = 0;
unsigned long lastPollutionTime = 0;
// How often each task runs (milliseconds)
const unsigned long DHT_INTERVAL = 2000; // 2 seconds — fast enough for responsive alerts
const unsigned long WEATHER_INTERVAL = 30000; // 30 seconds — API rate limit friendly
const unsigned long POLLUTION_INTERVAL = 35000; // 35 seconds — offset so both don't hit API together
// Suburb average temp — shared between weather fetch and summary publish
float n_avgTemp = 0;
int n_validCount = 0;
// ── CLIENTS ─────────────────────────────────────────────────
WiFiClient espClient;
PubSubClient client(espClient);
// ── SUBURB TABLE ─────────────────────────────────────────────
struct Suburb {
const char* name;
const char* query;
float lat;
float lon;
};
Suburb suburbs[] = {
{"Bellville", "Bellville,ZA", 0, 0 },
{"Belhar", "Belhar,ZA", 0, 0 },
{"Cape Town CBD", NULL, -33.9249, 18.4241},
{"Kuilsrivier", "Kuilsrivier,ZA", 0, 0 }
};
const int suburbCount = 4;
// ============================================================
// BUZZER HELPERS
// ============================================================
void buzzerOn() { ledcWriteTone(BUZZERPIN, BUZZ_FREQ); }
void buzzerOff() { ledcWriteTone(BUZZERPIN, 0); }
// ============================================================
// WIFI
// ============================================================
void connectWiFi() {
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi Connected!");
Serial.println(WiFi.localIP());
}
// ============================================================
// MQTT CALLBACK
// This fires INSTANTLY when a message arrives on any
// subscribed topic — because client.loop() is now called
// every single iteration of loop(), not once every 30 seconds.
// ============================================================
void mqttCallback(char* topic, byte* payload, unsigned int length) {
String message = "";
for (unsigned int i = 0; i < length; i++) {
message += (char)payload[i];
}
// ── NEW THRESHOLD FROM NODE-RED ──────────────────────────
if (String(topic) == mqtt_topic_threshold) {
USER_THRESHOLD = message.toFloat();
Serial.print("Threshold updated to: ");
Serial.println(USER_THRESHOLD);
}
// ── REMOTE BUZZER OVERRIDE FROM NODE-RED ─────────────────
if (String(topic) == mqtt_topic_buzzer) {
if (message == "ON") {
buzzerOn();
lastBuzzerState = true;
Serial.println("REMOTE: Buzzer ON");
} else if (message == "OFF") {
buzzerOff();
lastBuzzerState = false;
Serial.println("REMOTE: Buzzer OFF");
}
}
// ── LED PWM FROM NODE-RED ────────────────────────────────
// Node-RED publishes 0–255 to topic "Moola"
// This now responds immediately — no 30-second wait
if (String(topic) == mqtt_topic_pwm) {
uint32_t duty = message.toInt();
duty = constrain(duty, 0, 255); // Clamp to valid range
ledcWrite(LEDPIN, duty); // Apply immediately — 0 = off, 255 = full brightness
Serial.print("LED PWM set to: ");
Serial.println(duty);
}
}
// ============================================================
// MQTT CONNECT + SUBSCRIBE
// ============================================================
void connectMQTT() {
if (client.connected()) return;
Serial.print("Attempting MQTT connection...");
String clientId = "ESP32Client-" + String(random(0xffff), HEX);
if (client.connect(clientId.c_str())) {
Serial.println("Connected");
client.subscribe(mqtt_topic_threshold);
client.subscribe(mqtt_topic_buzzer);
client.subscribe(mqtt_topic_pwm);
Serial.println("Subscribed to: alert/set | alert/buzzer | Moola");
} else {
Serial.print("Failed, state: ");
Serial.println(client.state());
}
}
// ============================================================
// TASK A — DHT22 READ + BUZZER ALERT
// Runs every DHT_INTERVAL (2 seconds)
// Fastest task — drives the buzzer and publishes sensor data
// ============================================================
void taskDHT() {
float p_temp = dht.readTemperature();
float p_humidity = dht.readHumidity();
// Retry once on NaN — DHT22 occasionally glitches
if (isnan(p_temp) || isnan(p_humidity)) {
delay(500); // short blocking delay ONLY here, inside the 2-second task — acceptable
p_temp = dht.readTemperature();
p_humidity = dht.readHumidity();
}
if (isnan(p_temp) || isnan(p_humidity)) {
Serial.println("DHT22 read failed — skipping");
return;
}
// ── BUZZER LOGIC ─────────────────────────────────────────
// Evaluated every 2 seconds — stops within 2s of temp dropping
bool b_alert = p_temp > USER_THRESHOLD;
if (b_alert) {
buzzerOn();
if (!lastBuzzerState) {
Serial.println("ALERT: Temp exceeded threshold — Buzzer ON");
lastBuzzerState = true;
}
} else {
buzzerOff(); // Turns off within 2 seconds of temp dropping
if (lastBuzzerState) {
Serial.println("Safe: Temp below threshold — Buzzer OFF");
lastBuzzerState = false;
}
}
// ── PUBLISH LIVE SENSOR DATA ─────────────────────────────
char sensorPayload[200];
sprintf(sensorPayload,
"{\"sensor_temp\":%.1f,\"sensor_humidity\":%.1f,\"threshold\":%.1f}",
p_temp, p_humidity, USER_THRESHOLD);
client.publish(mqtt_topic_sensor, sensorPayload);
Serial.println(sensorPayload);
// ── PUBLISH ALERT (only when breached) ───────────────────
if (b_alert) {
char alertPayload[300];
sprintf(alertPayload,
"{\"alert\":true,\"sensor_temp\":%.1f,\"threshold\":%.1f,"
"\"message\":\"ALERT: %.1f C exceeds %.1f C\"}",
p_temp, USER_THRESHOLD, p_temp, USER_THRESHOLD);
client.publish(mqtt_topic_alert, alertPayload);
Serial.println(alertPayload);
}
// ── WEATHER DEVIATION SUMMARY ────────────────────────────
// Only publish summary if we have a valid suburb average
if (n_validCount > 0) {
float n_deviation = p_temp - n_avgTemp;
bool b_weatherAlert = abs(n_deviation) > ALERT_THRESHOLD;
char summaryPayload[300];
sprintf(summaryPayload,
"{\"avg_temp\":%.1f,\"sensor_temp\":%.1f,"
"\"sensor_humidity\":%.1f,\"deviation\":%.1f,\"alert\":%s}",
n_avgTemp, p_temp, p_humidity, n_deviation,
b_weatherAlert ? "true" : "false");
client.publish(mqtt_topic_summary, summaryPayload);
Serial.println(summaryPayload);
}
}
// ============================================================
// TASK B — WEATHER FETCH (one suburb per call)
// Called once per WEATHER_INTERVAL cycle.
// Fetches all 4 suburbs sequentially, updates n_avgTemp.
// The 2-second delay inside fetchWeather is unavoidable
// (API rate limit) but only runs during the weather cycle.
// ============================================================
float fetchWeather(Suburb s) {
HTTPClient http;
char url[200];
if (s.query != NULL) {
sprintf(url,
"https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=metric",
s.query, apiKey);
} else {
sprintf(url,
"https://api.openweathermap.org/data/2.5/weather?lat=%.4f&lon=%.4f&appid=%s&units=metric",
s.lat, s.lon, apiKey);
}
http.begin(url);
int httpCode = http.GET();
if (httpCode == 200) {
String response = http.getString();
StaticJsonDocument<1024> doc;
deserializeJson(doc, response);
float p_temp = doc["main"]["temp"];
float p_humidity = doc["main"]["humidity"];
float p_feelsLike = doc["main"]["feels_like"];
float p_windSpeed = doc["wind"]["speed"];
const char* p_desc= doc["weather"][0]["description"];
float p_lat = doc["coord"]["lat"];
float p_lon = doc["coord"]["lon"];
char payload[512];
sprintf(payload,
"{\"suburb\":\"%s\",\"temp\":%.1f,\"humidity\":%.1f,"
"\"feels_like\":%.1f,\"wind_speed\":%.1f,\"desc\":\"%s\","
"\"lat\":%.4f,\"lon\":%.4f}",
s.name, p_temp, p_humidity, p_feelsLike, p_windSpeed, p_desc, p_lat, p_lon);
client.publish(mqtt_topic_weather, payload);
Serial.println(payload);
http.end();
delay(2000); // API rate limit — only runs during weather cycle, not every loop
return p_temp;
} else {
Serial.print("Weather HTTP error for ");
Serial.print(s.name);
Serial.print(": ");
Serial.println(httpCode);
http.end();
return -999;
}
}
void taskWeather() {
float totalTemp = 0;
int validCount = 0;
for (int i = 0; i < suburbCount; i++) {
float t = fetchWeather(suburbs[i]);
if (t != -999) {
totalTemp += t;
validCount++;
}
}
// Update shared average used by taskDHT() summary
if (validCount > 0) {
n_avgTemp = totalTemp / validCount;
n_validCount = validCount;
}
}
// ============================================================
// TASK C — POLLUTION FETCH
// Runs every POLLUTION_INTERVAL (35 seconds).
// Offset from weather to avoid hammering the API at the
// same moment.
// ============================================================
void fetchPollution(Suburb s) {
HTTPClient http;
char url[200];
float queryLat, queryLon;
if (strcmp(s.name, "Bellville") == 0) { queryLat = -33.9160; queryLon = 18.6282; }
else if (strcmp(s.name, "Belhar") == 0) { queryLat = -33.9167; queryLon = 18.6833; }
else if (strcmp(s.name, "Cape Town CBD") == 0) { queryLat = -33.9249; queryLon = 18.4241; }
else { queryLat = -33.9333; queryLon = 18.7167; }
sprintf(url,
"http://api.openweathermap.org/data/2.5/air_pollution?lat=%.4f&lon=%.4f&appid=%s",
queryLat, queryLon, apiKey);
http.begin(url);
int httpCode = http.GET();
if (httpCode == 200) {
String response = http.getString();
StaticJsonDocument<1024> doc;
deserializeJson(doc, response);
int p_aqi = doc["list"][0]["main"]["aqi"];
float p_co = doc["list"][0]["components"]["co"];
float p_no2 = doc["list"][0]["components"]["no2"];
float p_o3 = doc["list"][0]["components"]["o3"];
float p_pm25 = doc["list"][0]["components"]["pm2_5"];
float p_pm10 = doc["list"][0]["components"]["pm10"];
char payload[512];
sprintf(payload,
"{\"suburb\":\"%s\",\"aqi\":%d,\"co\":%.2f,\"no2\":%.2f,"
"\"o3\":%.2f,\"pm25\":%.2f,\"pm10\":%.2f,\"lat\":%.4f,\"lon\":%.4f}",
s.name, p_aqi, p_co, p_no2, p_o3, p_pm25, p_pm10, queryLat, queryLon);
client.publish(mqtt_topic_pollution, payload);
Serial.println(payload);
} else {
Serial.print("Pollution HTTP error for ");
Serial.print(s.name);
Serial.print(": ");
Serial.println(httpCode);
}
http.end();
delay(1000);
}
void taskPollution() {
for (int i = 0; i < suburbCount; i++) {
fetchPollution(suburbs[i]);
}
}
// ============================================================
// SETUP
// ============================================================
void setup() {
Serial.begin(115200);
// Buzzer PWM channel
ledcAttach(BUZZERPIN, BUZZ_FREQ, BUZZ_RESOLUTION);
buzzerOff();
// LED PWM channel
ledcAttach(LEDPIN, LED_FREQ, LED_RESOLUTION);
ledcWrite(LEDPIN, 128); // 50% on boot as visual indicator
dht.begin();
delay(2500); // DHT22 stabilisation — only at boot
connectWiFi();
client.setServer(mqtt_server, 1883);
client.setCallback(mqttCallback);
connectMQTT();
// Seed timers so tasks fire immediately on first loop
// Setting to 0 means (millis() - 0) will be >= interval straight away
lastDHTTime = 0;
lastWeatherTime = 0;
lastPollutionTime = 0;
}
// ============================================================
// LOOP — non-blocking task scheduler
//
// This loop runs hundreds of times per second.
// client.loop() processes all incoming MQTT messages
// immediately — buzzer and LED respond in milliseconds.
//
// Each task only runs when its timer has expired.
// millis() never pauses the chip — unlike delay().
// ============================================================
void loop() {
// ── KEEP MQTT ALIVE ──────────────────────────────────────
// This MUST be the first thing in loop().
// It handles all incoming messages (threshold, buzzer, LED).
// When this runs 500+ times per second, responses are instant.
if (!client.connected()) {
connectMQTT();
}
client.loop();
unsigned long now = millis(); // Current time in milliseconds since boot
// ── TASK A: DHT22 + BUZZER (every 2 seconds) ─────────────
// Buzzer responds within 2 seconds of any temperature change.
// LED PWM responds instantly via mqttCallback above.
if (now - lastDHTTime >= DHT_INTERVAL) {
lastDHTTime = now; // Reset timer
taskDHT();
}
// ── TASK B: WEATHER FETCH (every 30 seconds) ─────────────
// This takes ~8–10 seconds to complete (4 suburbs × 2s each).
// During this time the buzzer may be slightly slow (up to 2s
// extra delay per suburb fetch). Acceptable tradeoff for free
// API tier. The LED still responds via callback instantly.
if (now - lastWeatherTime >= WEATHER_INTERVAL) {
lastWeatherTime = now;
taskWeather();
}
// ── TASK C: POLLUTION FETCH (every 35 seconds) ───────────
if (now - lastPollutionTime >= POLLUTION_INTERVAL) {
lastPollutionTime = now;
taskPollution();
}
// No delay() here — loop runs as fast as possible
// so client.loop() gets called constantly
}