// ============================================================
// COMBINED ESP32 FIRMWARE
// BET Electrical – IoT Project
// Combines:
// 1. OpenWeather API fetcher (4 Cape Town suburbs)
// 2. DHT22 threshold alerting with buzzer
// 3. MQTT-controlled PWM LED (pin 13)
// ============================================================
// ── LIBRARIES ───────────────────────────────────────────────
#include <WiFi.h> // Handles WiFi connection on the ESP32
#include <PubSubClient.h> // Handles MQTT publish and subscribe
#include <HTTPClient.h> // Handles HTTP GET requests to OpenWeather API
#include <ArduinoJson.h> // Parses JSON responses from the API
#include <DHT.h> // Reads temperature and humidity from DHT22
// ── WIFI CREDENTIALS ────────────────────────────────────────
const char* ssid = "Wokwi-GUEST"; // Wokwi's built-in virtual WiFi — no password needed
const char* password = "";
// ── MQTT BROKER ─────────────────────────────────────────────
// HiveMQ is a free public broker — no account needed for testing
const char* mqtt_server = "broker.hivemq.com";
// ── MQTT TOPICS ─────────────────────────────────────────────
// PUBLISH topics — ESP32 sends data to these
const char* mqtt_topic_weather = "weather/suburbs"; // One JSON object per suburb, per cycle
const char* mqtt_topic_summary = "weather/summary"; // Avg temp vs local DHT22 reading + deviation
const char* mqtt_topic_pollution = "weather/Pollution"; // AQI + pollutant levels per suburb
const char* mqtt_topic_alert = "alert/threshold"; // Fires only when DHT22 temp > USER_THRESHOLD
const char* mqtt_topic_sensor = "alert/sensor"; // Live DHT22 readings every loop cycle
// SUBSCRIBE topics — ESP32 listens to these for commands from Node-RED
const char* mqtt_topic_threshold = "alert/set"; // Node-RED slider sends new threshold value here
const char* mqtt_topic_buzzer = "alert/buzzer"; // Node-RED sends "ON" or "OFF" here for remote buzzer
const char* mqtt_topic_pwm = "Moola"; // Node-RED sends 0–255 here to set LED brightness
// ── OPENWEATHER API KEY ─────────────────────────────────────
const char* apiKey = "1e4fad19e905cfb8553e1a004d2153d1";
// ── DHT22 SENSOR ────────────────────────────────────────────
#define DHTPIN 26 // GPIO 26 — DHT22 data pin (pull-up resistor r1 = 1kΩ to 3V3 in diagram)
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// ── BUZZER ──────────────────────────────────────────────────
#define BUZZERPIN 15 // GPIO 15 — buzzer positive leg (negative leg goes to GND via diagram)
#define BUZZ_FREQ 1000 // 1000 Hz tone — clearly audible, not irritating
#define BUZZ_RESOLUTION 8 // 8-bit PWM = 256 levels (0–255 duty range)
// ── LED (PWM controlled) ─────────────────────────────────────
// GPIO 13 → 470Ω resistor (r2) → LED anode → LED cathode → GND
// The 470Ω resistor limits current to protect the LED — see resistor check at the bottom
#define LEDPIN 13
#define LED_FREQ 5000 // 5 kHz PWM — fast enough that the LED appears steady to the eye
#define LED_RESOLUTION 8 // 8-bit resolution — matches the 0–255 range sent over MQTT
// ── ALERT THRESHOLD ─────────────────────────────────────────
// Default temperature limit — Node-RED can change this at runtime via mqtt_topic_threshold
float USER_THRESHOLD = 30.0;
// This is used by Code 1 (weather fetcher) to flag when local sensor
// deviates from the suburb average by more than 3.0°C
float ALERT_THRESHOLD = 3.0;
// ── STATE MEMORY ─────────────────────────────────────────────
// Tracks the previous buzzer state so we only print "Buzzer ON/OFF"
// once per transition instead of spamming it every loop cycle
bool lastBuzzerState = false;
// ── WIFI + MQTT CLIENTS ──────────────────────────────────────
WiFiClient espClient; // The underlying TCP/IP connection
PubSubClient client(espClient); // MQTT layer sitting on top of WiFi
// ── SUBURB TABLE ─────────────────────────────────────────────
// Holds the 4 Cape Town suburbs we fetch weather and pollution data for.
// query = city name string for OpenWeather (used when lat/lon = 0)
// lat/lon = used directly when the city name query is unreliable (e.g. Cape Town CBD)
struct Suburb {
const char* name; // Human-readable label published in MQTT payload
const char* query; // OpenWeather "q=" parameter — NULL if using lat/lon instead
float lat;
float lon;
};
Suburb suburbs[] = {
{"Bellville", "Bellville,ZA", 0, 0 },
{"Belhar", "Belhar,ZA", 0, 0 },
{"Cape Town CBD", NULL, -33.9249, 18.4241}, // lat/lon used — city name ambiguous in API
{"Kuilsrivier", "Kuilsrivier,ZA", 0, 0 }
};
const int suburbCount = 4;
// ============================================================
// BUZZER HELPERS
// ============================================================
// Generates a continuous 1000 Hz PWM tone on the buzzer pin
void buzzerOn() {
ledcWriteTone(BUZZERPIN, BUZZ_FREQ);
}
// Stops the PWM signal — buzzer goes silent
void buzzerOff() {
ledcWriteTone(BUZZERPIN, 0); // frequency = 0 stops the tone
}
// ============================================================
// WIFI CONNECTION
// ============================================================
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()); // Print assigned IP — useful for debugging
}
// ============================================================
// MQTT CALLBACK
// Fires automatically whenever a message arrives on any
// subscribed topic. Handles 3 topics:
// 1. alert/set → update temperature threshold
// 2. alert/buzzer → remote buzzer ON/OFF from Node-RED
// 3. Moola → set LED PWM duty cycle 0–255
// ============================================================
void mqttCallback(char* topic, byte* payload, unsigned int length) {
// Reconstruct the message string from raw bytes
String message = "";
for (unsigned int i = 0; i < length; i++) {
message += (char)payload[i];
}
// ── THRESHOLD UPDATE ──────────────────────────────────────
if (String(topic) == mqtt_topic_threshold) {
USER_THRESHOLD = message.toFloat(); // Convert the string "35.5" → float 35.5
Serial.print("Threshold updated to: ");
Serial.println(USER_THRESHOLD);
}
// ── REMOTE BUZZER CONTROL ─────────────────────────────────
if (String(topic) == mqtt_topic_buzzer) {
if (message == "ON") {
buzzerOn();
lastBuzzerState = true;
Serial.println("REMOTE COMMAND: Buzzer turned ON via Node-RED");
} else if (message == "OFF") {
buzzerOff();
lastBuzzerState = false;
Serial.println("REMOTE COMMAND: Buzzer turned OFF via Node-RED");
}
}
// ── PWM LED BRIGHTNESS ────────────────────────────────────
// Node-RED publishes a number 0–255 to topic "Moola"
// 0 = LED fully off, 255 = LED fully on, 128 = 50% brightness
if (String(topic) == mqtt_topic_pwm) {
uint32_t duty = message.toInt(); // Convert "128" → integer 128
duty = constrain(duty, 0, 255); // Clamp: reject any value outside 0–255 range
ledcWrite(LEDPIN, duty); // Apply duty cycle to LED PWM channel
Serial.print("LED PWM duty set to: ");
Serial.println(duty);
}
}
// ============================================================
// MQTT CONNECTION + SUBSCRIPTIONS
// ============================================================
void connectMQTT() {
if (client.connected()) return; // Already connected — nothing to do
Serial.print("Attempting MQTT connection...");
// Random client ID prevents collision if multiple instances run simultaneously
String clientId = "ESP32Client-" + String(random(0xffff), HEX);
if (client.connect(clientId.c_str())) {
Serial.println("Connected to MQTT broker");
// Subscribe to all 3 inbound command topics
client.subscribe(mqtt_topic_threshold); // Listen for threshold changes from Node-RED
client.subscribe(mqtt_topic_buzzer); // Listen for remote buzzer override from Node-RED
client.subscribe(mqtt_topic_pwm); // Listen for LED brightness commands from Node-RED
Serial.println("Subscribed to: alert/set | alert/buzzer | Moola");
} else {
Serial.print("Failed, state: ");
Serial.println(client.state()); // Negative state codes indicate specific errors
// State codes: -4=timeout, -3=connection denied, -2=connection failed, 1–5=protocol errors
}
}
// ============================================================
// FETCH WEATHER — one suburb at a time
// Returns the temperature (float) so the caller can average it.
// Returns -999 on HTTP failure so the caller can skip it.
// ============================================================
float fetchWeather(Suburb s) {
HTTPClient http;
char url[200];
// Build the correct URL depending on whether we use city name or coordinates
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(); // Send the HTTP GET request
if (httpCode == 200) { // 200 = OK — API responded successfully
String response = http.getString();
// Parse the JSON response — 1024 bytes is enough for weather data
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"];
// Build a compact JSON payload and publish it to the weather topic
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); // Respect API rate limits — 1 call per 2 seconds per suburb
return p_temp;
} else {
Serial.print("Weather HTTP Error for ");
Serial.print(s.name);
Serial.print(": ");
Serial.println(httpCode);
http.end();
return -999; // Sentinel value — caller ignores this suburb in the average
}
}
// ============================================================
// FETCH POLLUTION — one suburb at a time
// Publishes AQI + 5 pollutant concentrations to MQTT.
// AQI scale: 1=Good, 2=Fair, 3=Moderate, 4=Poor, 5=Very Poor
// ============================================================
void fetchPollution(Suburb s) {
HTTPClient http;
char url[200];
// Hard-coded coordinates for each suburb
// The pollution API requires lat/lon — city name queries are not supported
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; } // Kuilsrivier
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);
}
// ============================================================
// SETUP — runs once at boot
// ============================================================
void setup() {
Serial.begin(115200);
// ── HARDWARE INIT ─────────────────────────────────────────
// Attach buzzer to LEDC PWM channel
// ledcAttach(pin, frequency, resolution) — new ESP32 Arduino core API
ledcAttach(BUZZERPIN, BUZZ_FREQ, BUZZ_RESOLUTION);
buzzerOff(); // Ensure buzzer is silent at startup — don't wake everyone up on reboot
// Attach LED to a separate LEDC PWM channel
// Different pin, different frequency (5kHz vs 1kHz) — ESP32 handles this automatically
ledcAttach(LEDPIN, LED_FREQ, LED_RESOLUTION);
ledcWrite(LEDPIN, 128); // Boot with LED at 50% brightness as a visual "power on" indicator
// ── SENSOR INIT ───────────────────────────────────────────
dht.begin();
delay(2500); // DHT22 needs ~2 seconds to stabilise before first read is reliable
// ── NETWORK INIT ──────────────────────────────────────────
connectWiFi();
client.setServer(mqtt_server, 1883); // Point MQTT client at HiveMQ broker, port 1883 (standard unencrypted)
client.setCallback(mqttCallback); // Register the callback function for incoming messages
connectMQTT(); // Connect and subscribe to all 3 inbound topics
}
// ============================================================
// LOOP — runs continuously
//
// Every cycle does 4 things in order:
// 1. Read DHT22 → compare to threshold → control buzzer → publish sensor data
// 2. Fetch weather for all 4 suburbs → compute avg temp → publish summary
// 3. Fetch pollution for all 4 suburbs
// 4. Wait 30 seconds before next full cycle
// ============================================================
void loop() {
client.loop(); // MUST be called every loop — processes incoming MQTT messages and keeps connection alive
if (!client.connected()) {
connectMQTT(); // Reconnect immediately if broker dropped the connection
}
// ── SECTION 1: LOCAL DHT22 THRESHOLD ALERTING ─────────────
float p_sensorTemp = dht.readTemperature();
float p_sensorHumidity = dht.readHumidity();
// DHT22 occasionally returns NaN on the first read — retry once before giving up
if (isnan(p_sensorTemp) || isnan(p_sensorHumidity)) {
delay(2000);
p_sensorTemp = dht.readTemperature();
p_sensorHumidity = dht.readHumidity();
}
if (!isnan(p_sensorTemp) && !isnan(p_sensorHumidity)) {
bool b_alert = p_sensorTemp > USER_THRESHOLD; // true = temp has crossed the danger line
// ── LOCAL HARDWARE RESPONSE (immediate, no MQTT delay) ──
// This runs on the chip itself — zero latency, no network required
if (b_alert) {
buzzerOn();
if (lastBuzzerState == false) {
// Only print once per threshold breach — not every single loop cycle
Serial.println("ALERT: Temperature exceeded threshold — Buzzer ON");
lastBuzzerState = true;
}
} else {
buzzerOff();
if (lastBuzzerState == true) {
// Only print once when system returns to safe temperature
Serial.println("Safe: Temperature back below threshold — Buzzer OFF");
lastBuzzerState = false;
}
}
// ── PUBLISH LIVE SENSOR DATA TO NODE-RED ────────────────
// Node-RED receives this every loop cycle for dashboard gauges
char sensorPayload[200];
sprintf(sensorPayload,
"{\"sensor_temp\":%.1f,\"sensor_humidity\":%.1f,\"threshold\":%.1f}",
p_sensorTemp, p_sensorHumidity, USER_THRESHOLD);
client.publish(mqtt_topic_sensor, sensorPayload);
Serial.println(sensorPayload);
// ── PUBLISH ALERT PAYLOAD (only when threshold is exceeded) ─
if (b_alert) {
char alertPayload[300];
sprintf(alertPayload,
"{\"alert\":true,\"sensor_temp\":%.1f,\"threshold\":%.1f,"
"\"message\":\"ALERT: Sensor temp %.1f C exceeds threshold %.1f C\"}",
p_sensorTemp, USER_THRESHOLD, p_sensorTemp, USER_THRESHOLD);
client.publish(mqtt_topic_alert, alertPayload);
Serial.println(alertPayload);
}
} else {
Serial.println("DHT22 read failed after retry — skipping this cycle");
}
// ── SECTION 2: WEATHER FETCH — ALL 4 SUBURBS ──────────────
float n_totalTemp = 0;
int n_validCount = 0;
for (int i = 0; i < suburbCount; i++) {
float n_temp = fetchWeather(suburbs[i]); // Each call publishes its own MQTT message
if (n_temp != -999) { // Only count suburbs that returned valid data
n_totalTemp += n_temp;
n_validCount++;
}
}
// ── PUBLISH SUMMARY: avg API temp vs local DHT22 ──────────
if (n_validCount > 0) {
float n_avgTemp = n_totalTemp / n_validCount;
// Re-read DHT22 here — the weather fetch took ~8 seconds, readings may have changed
float p_freshTemp = dht.readTemperature();
float p_freshHumidity = dht.readHumidity();
if (isnan(p_freshTemp) || isnan(p_freshHumidity)) {
delay(2000);
p_freshTemp = dht.readTemperature();
p_freshHumidity = dht.readHumidity();
}
if (!isnan(p_freshTemp) && !isnan(p_freshHumidity)) {
float n_deviation = p_freshTemp - n_avgTemp;
// b_weatherAlert fires when local sensor is more than ALERT_THRESHOLD (3°C) away from suburb average
bool b_weatherAlert = abs(n_deviation) > ALERT_THRESHOLD;
char summaryPayload[512];
sprintf(summaryPayload,
"{\"avg_temp\":%.1f,"
"\"sensor_temp\":%.1f,"
"\"sensor_humidity\":%.1f,"
"\"deviation\":%.1f,"
"\"alert\":%s}",
n_avgTemp, p_freshTemp, p_freshHumidity, n_deviation,
b_weatherAlert ? "true" : "false");
client.publish(mqtt_topic_summary, summaryPayload);
Serial.println(summaryPayload);
} else {
Serial.println("DHT22 read failed for summary — skipping summary publish");
}
}
// ── SECTION 3: POLLUTION FETCH — ALL 4 SUBURBS ────────────
for (int i = 0; i < suburbCount; i++) {
fetchPollution(suburbs[i]); // Each call publishes its own MQTT message
}
// ── SECTION 4: WAIT BEFORE NEXT CYCLE ─────────────────────
// 30 seconds total — OpenWeather free tier allows 60 calls/minute max.
// 1 weather + 1 pollution call per suburb × 4 suburbs = 8 API calls per cycle.
// At 30-second intervals = 16 calls/minute — well within limits.
delay(30000);
}