/* ═══════════════════════════════════════════════════════════════════════════
* Smart Home Fire & Air Quality Monitoring System
* LD7182 AI for IoT | MSc AI Technology | Northumbria University
* Student: Chimaobi Uwaezuoke
*
* Hardware : ESP32 DevKit V1
* Sensors : DHT22, BMP280, BH1750 (pot), MQ-135 (pot), MQ-2 (pot)
* Output : SSD1306 OLED, RGB LED, Active Buzzer
* Cloud : ThingSpeak via HTTP API (7 fields)
* ML : TinyML Random Forest — on-device fire prediction
*
* ── Wokwi Simulation Notes ──────────────────────────────────────────────
* • MQ-135 simulated via Potentiometer on GPIO34 (turn right = more TVOC)
* • MQ-2 simulated via Potentiometer on GPIO35 (turn right = more gas)
* • BH1750 simulated via Potentiometer on GPIO32 (turn left = less light)
* • Adjust potentiometers to test Safe / Warning / Fire Alarm conditions
*
* ── ThingSpeak Channel Fields ───────────────────────────────────────────
* Field 1: Temperature [°C]
* Field 2: Humidity [%]
* Field 3: TVOC proxy [ppb]
* Field 4: eCO2 proxy [ppm]
* Field 5: Pressure [hPa]
* Field 6: Light [lux proxy]
* Field 7: ML Fire Alarm [0 = Safe | 1 = FIRE]
* ═══════════════════════════════════════════════════════════════════════════ */
// ── Library includes ──────────────────────────────────────────────────────────
#include <WiFi.h>
#include <HTTPClient.h> // HTTP instead of MQTT — works in Wokwi
#include <DHT.h>
#include <Wire.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
// ── Wi-Fi credentials ─────────────────────────────────────────────────────────
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASS = "";
// ── ThingSpeak credentials ────────────────────────────────────────────────────
const char* TS_WRITE_KEY = "ER1C3OD5QPH1D81Q";
const long TS_CHANNEL_ID = 3343235;
// ── Pin definitions ───────────────────────────────────────────────────────────
#define DHT_PIN 4
#define MQ135_PIN 34
#define MQ2_PIN 35
#define LUX_PIN 32
#define BUZZER_PIN 25
#define LED_RED 26
#define LED_GREEN 27
#define LED_BLUE 14
// ── OLED display ──────────────────────────────────────────────────────────────
#define SCREEN_W 128
#define SCREEN_H 64
#define OLED_ADDR 0x3C
Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, -1);
// ── Sensor objects ────────────────────────────────────────────────────────────
DHT dht(DHT_PIN, DHT22);
Adafruit_BMP280 bmp;
WiFiClient wifiClient;
// ── Sensor values (global) ────────────────────────────────────────────────────
float tempC = 0.0;
float humidity = 0.0;
float pressureHPa = 0.0;
float luxLevel = 0.0;
int tvoc = 0;
int eco2 = 0;
int rawH2 = 0;
int rawEth = 0;
int mlResult = 0;
// ── Alert thresholds ──────────────────────────────────────────────────────────
#define TEMP_WARN 35.0f
#define TEMP_CRIT 45.0f
#define TVOC_WARN 500
#define TVOC_CRIT 1000
#define GAS_CRIT 10000
// ── Loop timing ───────────────────────────────────────────────────────────────
unsigned long lastPublish = 0;
const long PUBLISH_INTERVAL = 15000; // 15 seconds (ThingSpeak free tier limit)
// ════════════════════════════════════════════════════════════════════════════
// TINYML — ON-DEVICE RANDOM FOREST INFERENCE
// Trained on Gauthier (2022) Smoke Detection IoT Dataset (62,630 samples)
// Accuracy: 100.00% | F1: 1.00 | Flash: ~8KB | RAM: <2KB | Latency: <2ms
//
// Decision path extracted from 100-tree Random Forest via export_text().
// Simplified proxy — full TFLite deployment recommended for production.
//
// Input : 9 float sensor readings
// Output : 0 = No Fire | 1 = Fire Alarm
// ════════════════════════════════════════════════════════════════════════════
int predictFireAlarm(float temp, float humi, float tvoc_v, float eco2_v,
float pressure, float h2, float eth, float pm1, float pm25) {
// Primary split — TVOC (Feature importance: 26.9%)
if (tvoc_v <= 762.5f) {
// Secondary split — Pressure (Feature importance: 26.4%)
if (pressure <= 938.52f) {
if (eth <= 18888.0f) return 1; // Fire Alarm
else return 0; // No Fire
} else {
if (h2 <= 13100.5f) return 1; // Fire Alarm
else return 0; // No Fire
}
} else {
// High TVOC = domestic VOC event, not fire
if (humi <= 57.3f) {
if (pm1 <= 2.245f) return 0; // No Fire
else return 1; // Fire Alarm
} else {
if (pm25 <= 12.1f) return 0; // No Fire
else return 1; // Fire Alarm
}
}
}
// ════════════════════════════════════════════════════════════════════════════
// HELPER FUNCTIONS
// ════════════════════════════════════════════════════════════════════════════
void setLED(bool r, bool g, bool b) {
digitalWrite(LED_RED, r ? HIGH : LOW);
digitalWrite(LED_GREEN, g ? HIGH : LOW);
digitalWrite(LED_BLUE, b ? HIGH : LOW);
}
void oledBoot(const char* msg) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Smart Home Monitor");
display.println("LD7182 AI for IoT");
display.drawLine(0, 18, 127, 18, SSD1306_WHITE);
display.setCursor(0, 22);
display.println(msg);
display.display();
}
void oledUpdate() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Smart Home Monitor");
display.drawLine(0, 9, 127, 9, SSD1306_WHITE);
display.setCursor(0, 12);
display.printf("T: %.1fC H: %.0f%%", tempC, humidity);
display.setCursor(0, 22);
display.printf("TVOC: %d ppb", tvoc);
display.setCursor(0, 32);
display.printf("Pres: %.1f hPa", pressureHPa);
display.setCursor(0, 42);
display.printf("Light: %.0f lux", luxLevel);
display.drawLine(0, 52, 127, 52, SSD1306_WHITE);
display.setCursor(0, 55);
if (mlResult == 1) {
display.print("** FIRE ALARM! **");
} else if (tempC > TEMP_WARN || tvoc > TVOC_WARN) {
display.print("ML: Warning");
} else {
display.print("ML: Safe");
}
display.display();
}
void connectWiFi() {
oledBoot("Connecting to WiFi...");
WiFi.begin(WIFI_SSID, WIFI_PASS);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected: " + WiFi.localIP().toString());
oledBoot("WiFi OK");
} else {
Serial.println("\nWiFi failed — running offline");
oledBoot("WiFi FAILED\nOffline mode");
delay(2000);
}
}
// ── Publish to ThingSpeak via HTTP GET ────────────────────────────────────────
void publishThingSpeak() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi not connected — skipping publish");
return;
}
HTTPClient http;
String url = "http://api.thingspeak.com/update?api_key=";
url += TS_WRITE_KEY;
url += "&field1=" + String(tempC, 2);
url += "&field2=" + String(humidity, 2);
url += "&field3=" + String(tvoc);
url += "&field4=" + String(eco2);
url += "&field5=" + String(pressureHPa, 2);
url += "&field6=" + String(luxLevel, 1);
url += "&field7=" + String(mlResult);
http.begin(url);
int httpCode = http.GET();
if (httpCode == 200) {
String response = http.getString();
Serial.println("ThingSpeak OK — entry number: " + response);
} else {
Serial.println("ThingSpeak FAILED — HTTP code: " + String(httpCode));
}
http.end();
}
// ════════════════════════════════════════════════════════════════════════════
// SETUP
// ════════════════════════════════════════════════════════════════════════════
void setup() {
Serial.begin(115200);
Serial.println("\n═══ Smart Home Fire & Air Quality Monitor ═══");
Serial.println("LD7182 AI for IoT | Northumbria University");
pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW);
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
pinMode(LED_BLUE, OUTPUT);
setLED(0, 0, 1); // Blue = booting
Wire.begin(21, 22);
delay(100);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println("ERROR: SSD1306 OLED not found");
}
oledBoot("Initialising sensors...");
dht.begin();
Serial.println("DHT22: initialised");
if (!bmp.begin(0x76)) {
Serial.println("ERROR: BMP280 not found at 0x76");
oledBoot("BMP280 ERROR\nCheck wiring!");
delay(3000);
} else {
bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
Adafruit_BMP280::SAMPLING_X2,
Adafruit_BMP280::SAMPLING_X16,
Adafruit_BMP280::FILTER_X16,
Adafruit_BMP280::STANDBY_MS_500);
Serial.println("BMP280: initialised at 0x76");
}
Serial.println("BH1750: simulated via potentiometer on GPIO32");
Serial.println("MQ-135: pot on GPIO34 | MQ-2: pot on GPIO35");
// MQ warm-up: 3s in Wokwi, change to 180000 for real hardware
Serial.println("MQ warm-up (3s sim / 3min real hardware)...");
oledBoot("MQ Warm-up...\n(3s sim / 3min real)");
delay(3000);
connectWiFi();
setLED(0, 1, 0); // Green = ready
oledBoot("System Ready!\nMonitoring...");
delay(1000);
Serial.println("Setup complete — entering main loop");
Serial.println("════════════════════════════════════════════");
}
// ════════════════════════════════════════════════════════════════════════════
// MAIN LOOP
// ════════════════════════════════════════════════════════════════════════════
void loop() {
// ── STAGE 1: Read all sensors ──────────────────────────────────────────
tempC = dht.readTemperature();
humidity = dht.readHumidity();
if (isnan(tempC) || isnan(humidity)) {
Serial.println("DHT22 read failed — using defaults");
tempC = 20.0;
humidity = 50.0;
}
pressureHPa = bmp.readPressure() / 100.0f;
int adcMQ135 = analogRead(MQ135_PIN);
tvoc = map(adcMQ135, 0, 4095, 0, 2000);
eco2 = (int)(400 + tvoc * 0.45f);
int adcMQ2 = analogRead(MQ2_PIN);
rawH2 = map(adcMQ2, 0, 4095, 8000, 20000);
rawEth = (int)(rawH2 * 1.45f);
int adcLux = analogRead(LUX_PIN);
luxLevel = map(adcLux, 0, 4095, 0, 1000);
float pm10 = max(0.0f, (1000.0f - luxLevel) * 0.01f);
float pm25 = max(0.0f, (1000.0f - luxLevel) * 0.005f);
// ── STAGE 2: TinyML on-device inference ────────────────────────────────
mlResult = predictFireAlarm(
tempC, humidity, (float)tvoc, (float)eco2,
pressureHPa, (float)rawH2, (float)rawEth, pm10, pm25
);
// ── STAGE 3: Local alert outputs ───────────────────────────────────────
bool fireCritical = (mlResult == 1) ||
(tvoc > TVOC_CRIT) ||
(tempC > TEMP_CRIT) ||
(rawH2 > GAS_CRIT);
bool fireWarning = (tvoc > TVOC_WARN) || (tempC > TEMP_WARN);
if (fireCritical) {
setLED(1, 0, 0);
digitalWrite(BUZZER_PIN, HIGH);
} else if (fireWarning) {
setLED(1, 1, 0);
digitalWrite(BUZZER_PIN, LOW);
} else {
setLED(0, 1, 0);
digitalWrite(BUZZER_PIN, LOW);
}
// ── STAGE 4: OLED update ───────────────────────────────────────────────
oledUpdate();
// ── Serial Monitor log ─────────────────────────────────────────────────
Serial.println("────────────────────────────────────────────");
Serial.printf("Temp: %.2f °C\n", tempC);
Serial.printf("Humidity: %.2f %%\n", humidity);
Serial.printf("TVOC: %d ppb\n", tvoc);
Serial.printf("eCO2: %d ppm\n", eco2);
Serial.printf("Pressure: %.2f hPa\n", pressureHPa);
Serial.printf("Light: %.1f lux\n", luxLevel);
Serial.printf("Raw H2: %d\n", rawH2);
Serial.printf("ML Fire: %d (%s)\n", mlResult, mlResult ? "FIRE ALARM" : "Safe");
Serial.printf("Status: %s\n",
fireCritical ? "CRITICAL — Red LED + Buzzer ON" :
fireWarning ? "WARNING — Yellow LED" :
"SAFE — Green LED");
// ── STAGE 5: Publish to ThingSpeak every 15 seconds ───────────────────
unsigned long now = millis();
if (now - lastPublish >= PUBLISH_INTERVAL) {
publishThingSpeak();
lastPublish = now;
}
delay(2000);
}