Overview
In this beginner build you will connect an MQ-2 smoke and gas sensor to the ESP32 and compare the analog reading against a threshold. When smoke is detected, a buzzer sounds and a red LED turns on. A green LED shows the normal status. The Serial Monitor prints the raw sensor value every second so you can calibrate the threshold for your environment. No Wi-Fi is required.
Components
- 1× ESP32 DevKit V1
- 1× MQ-2 smoke and gas sensor module — Analog + digital output; 5 V heater
- 1× Active buzzer 5 V
- 1× Red LED 5 mm — Alarm indicator
- 1× Green LED 5 mm — Normal status indicator
- 2× 220 ohm resistor — LED current limiting
- 1× Breadboard and jumper wires
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| MQ-2 AOUT | GPIO 34 | Analog input; 0-3.3 V (use voltage divider from 5 V output) |
| MQ-2 DOUT | GPIO 35 | Digital threshold output from module |
| MQ-2 VCC | 5 V (Vin) | Heater requires 5 V at 150 mA |
| MQ-2 GND | GND | |
| Red LED anode | GPIO 25 | 220 ohm resistor |
| Green LED anode | GPIO 26 | 220 ohm resistor |
| Buzzer + | GPIO 27 | Active buzzer; drive HIGH to sound |
Arduino Code
// ESP32 Fire Alarm - Beginner
// MQ-2 smoke sensor + LED + buzzer, no Wi-Fi required
const int MQ2_AOUT = 34;
const int MQ2_DOUT = 35;
const int LED_R = 25;
const int LED_G = 26;
const int BUZZER = 27;
// Tune this threshold based on Serial Monitor readings in clean air
const int SMOKE_THRESHOLD = 1800; // out of 4095 (12-bit ADC)
void setup() {
Serial.begin(115200);
pinMode(MQ2_DOUT, INPUT);
pinMode(LED_R, OUTPUT);
pinMode(LED_G, OUTPUT);
pinMode(BUZZER, OUTPUT);
analogReadResolution(12);
// MQ-2 preheat: sensor needs ~30 s warmup for stable readings
Serial.println("Preheating MQ-2 sensor (30 s)...");
digitalWrite(LED_G, HIGH);
delay(30000);
Serial.println("Ready. Monitoring for smoke.");
}
void loop() {
int raw = analogRead(MQ2_AOUT);
bool alarm = (raw > SMOKE_THRESHOLD) || (digitalRead(MQ2_DOUT) == LOW);
Serial.printf("MQ2 raw: %d Alarm: %sn", raw, alarm ? "YES" : "no");
if (alarm) {
digitalWrite(LED_R, HIGH);
digitalWrite(LED_G, LOW);
digitalWrite(BUZZER, HIGH);
} else {
digitalWrite(LED_R, LOW);
digitalWrite(LED_G, HIGH);
digitalWrite(BUZZER, LOW);
}
delay(1000);
}How It Works
MQ-2 Electrochemical Sensing: The MQ-2 contains a tin-dioxide (SnO2) sensing element heated to 300 C by an internal coil. Smoke, LPG, CO, and H2 molecules reduce surface resistance. The lower the resistance, the higher the AOUT voltage — so a rising analog reading indicates gas or smoke.
Dual-Output Detection: The module provides both an analog output (continuous 0-5 V proportional to concentration) and a digital output (LOW when concentration exceeds the onboard potentiometer threshold). Both are read to give redundant detection.
30-Second Preheat: The MQ-2 heater must reach operating temperature before readings stabilise. The 30-second delay on startup prevents false alarms from the cold-sensor transient. In a real installation, leave the sensor powered permanently.
Alarm Logic: An alarm triggers when either the analog reading exceeds SMOKE_THRESHOLD or the digital pin goes LOW. Using both ensures detection even if the analog potentiometer or threshold is miscalibrated.
Applications
- Home smoke detector with local siren
- Kitchen gas leak alert for LPG and propane
- Workshop fire safety monitor with audible alarm
- Server room early smoke detection system
Troubleshooting
Alarm triggers immediately after power-on
The sensor needs 30 seconds to stabilise. Add the preheat delay and avoid testing during warmup. Also check that the MQ-2 AOUT is not connected to the ESP32 ADC directly from the 5 V output; use a voltage divider to scale to 3.3 V.
No alarm even with lighter smoke near sensor
Lower the SMOKE_THRESHOLD constant. Read the baseline value in clean air from the Serial Monitor, then set the threshold 20-30 percent above that value. Also trim the onboard potentiometer for the digital output.
ESP32 ADC reads maximum 4095 always
The MQ-2 AOUT is a 5 V signal. Connect it to the ESP32 ADC through a voltage divider (10 kohm + 20 kohm) to scale to 3.3 V maximum, preventing ADC input damage.
Buzzer sounds continuously in clean air
The baseline gas concentration in the environment (paint fumes, cooking oil vapour) may be above the threshold. Increase SMOKE_THRESHOLD until the alarm is silent in normal conditions, then retest with a smoke source.
Upgrades
- Add a DHT22 temperature sensor and alarm if temperature exceeds 60 C
- Add an OLED display showing gas level bar graph and current reading
- Add Wi-Fi and send a Telegram alert when alarm is triggered
- Add a reset button to silence the buzzer while keeping the LED on
FAQ
You need an ESP32 DevKit, MQ-2 AOUT, MQ-2 DOUT, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Smart City. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
The intermediate build adds a DHT22 temperature and humidity sensor for combined fire detection: an alarm triggers on smoke above threshold OR temperature above 60 C. An SSD1306 OLED displays live readings, current status, and an alarm countdown. Wi-Fi connects to an NTP server for timestamps, and each alarm event is logged to NVS with time, sensor values, and trigger reason for later review.
Components
- 1× ESP32 DevKit V1
- 1× MQ-2 smoke sensor module
- 1× DHT22 temperature and humidity sensor — More accurate than DHT11
- 1× SSD1306 OLED 128x64 I2C
- 1× Active buzzer
- 1× Red and green LED
- 1× Wi-Fi router — NTP timestamp for event log
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| MQ-2 AOUT/DOUT/VCC/GND | Same as beginner | |
| DHT22 DATA | GPIO 4 | 10 kohm pull-up to 3.3 V |
| OLED SDA | GPIO 21 | |
| OLED SCL | GPIO 22 | |
| Buzzer | GPIO 27 | |
| Red LED | GPIO 25 | 220 ohm |
| Green LED | GPIO 26 | 220 ohm |
Arduino Code
// ESP32 Fire Alarm - Intermediate (DHT22 + OLED + NVS event log + NTP)
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <Preferences.h>
#include <WiFi.h>
#include <time.h>
Adafruit_SSD1306 oled(128,64,&Wire,-1);
DHT dht(4, DHT22);
Preferences prefs;
const char* SSID="YourSSID", *PASS="YourPass";
const int MQ2=34, DOUT=35, LED_R=25, LED_G=26, BUZ=27;
const int SMOKE_THRESH=1800;
const float TEMP_THRESH=60.0f;
int alarmCount=0;
void logAlarm(const String &reason, int smoke, float temp){
prefs.begin("alarm",false);
int n=prefs.getInt("count",0);
time_t now=time(nullptr);
String entry=String(now)+","+reason+","+String(smoke)+","+String(temp,1);
prefs.putString(String(n).c_str(),entry);
prefs.putInt("count",n+1);
prefs.end();
alarmCount=n+1;
Serial.println("ALARM logged: "+entry);
}
void setup(){
Serial.begin(115200);
Wire.begin(21,22);
dht.begin();
oled.begin(SSD1306_SWITCHCAPVCC,0x3C);
oled.setTextColor(WHITE);
pinMode(DOUT,INPUT); pinMode(LED_R,OUTPUT);
pinMode(LED_G,OUTPUT); pinMode(BUZ,OUTPUT);
analogReadResolution(12);
WiFi.begin(SSID,PASS);
int tries=0;
while(WiFi.status()!=WL_CONNECTED && tries<20){ delay(500); tries++; }
if(WiFi.status()==WL_CONNECTED) configTime(0,0,"pool.ntp.org");
// Preheat
oled.clearDisplay(); oled.setCursor(0,0); oled.println("Preheating 30s...");
oled.display(); delay(30000);
}
void loop(){
int smoke=analogRead(MQ2);
float temp=dht.readTemperature();
float hum=dht.readHumidity();
bool smokeAlarm=(smoke>SMOKE_THRESH)||(digitalRead(DOUT)==LOW);
bool tempAlarm=(!isnan(temp)&&temp>TEMP_THRESH);
bool alarm=smokeAlarm||tempAlarm;
if(alarm){
String reason=smokeAlarm&&tempAlarm?"SMOKE+TEMP":smokeAlarm?"SMOKE":"TEMP";
logAlarm(reason,smoke,isnan(temp)?-1:temp);
digitalWrite(LED_R,HIGH); digitalWrite(LED_G,LOW); digitalWrite(BUZ,HIGH);
} else {
digitalWrite(LED_R,LOW); digitalWrite(LED_G,HIGH); digitalWrite(BUZ,LOW);
}
oled.clearDisplay(); oled.setTextSize(1); oled.setCursor(0,0);
oled.printf("Smoke: %dn",smoke);
oled.printf("Temp: %.1fCn",isnan(temp)?0:temp);
oled.printf("Hum: %.1f%%n",isnan(hum)?0:hum);
oled.println(alarm?"*** ALARM ***":"Status: OK");
oled.printf("Events: %dn",alarmCount);
oled.display();
delay(2000);
}How It Works
Dual-Sensor Fusion: Smoke alone can be a cooking false alarm; temperature alone can be a hot day false alarm. Combining both reduces false positives significantly. A kitchen fire will trigger both sensors simultaneously, while burning toast may trigger only smoke.
NVS Alarm Event Log: Each alarm event is saved to NVS as an indexed string containing the Unix timestamp, trigger reason, smoke ADC value, and temperature. The log survives power cuts. Up to 100 events can be stored before the NVS namespace fills.
NTP Timestamp: configTime() syncs the ESP32 clock over Wi-Fi. If Wi-Fi is unavailable at boot the alarm system still operates; events are logged with Unix time 0 (epoch) rather than actual time. Wi-Fi is optional, not required for alarm function.
OLED Status Display: The OLED shows live smoke ADC value, temperature, humidity, current status, and total alarm event count. During an alarm the status line changes to "*** ALARM ***" in a single large text row for visibility at a distance.
Applications
- Home fire and gas leak detector with audit log
- Restaurant kitchen safety monitor with chef temperature alert
- Office building fire panel node with multi-sensor fusion
- Industrial boiler room overheat and smoke monitor
Troubleshooting
DHT22 reads NaN
The DHT22 requires a 10 kohm pull-up resistor between DATA and 3.3 V. Check the resistor is present. DHT22 also needs at least 2 seconds between reads; do not call dht.read() faster than once every 2 seconds.
Alarm events fill NVS quickly
Add a minimum 60-second lockout between logged alarms using a lastAlarm timestamp. Continuous alarm events from a persistent smoke source should only log once per minute, not every 2 seconds.
NTP time is unavailable in offline environments
Add a DS3231 RTC module for battery-backed timekeeping. Fall back to the RTC time when Wi-Fi is not available and sync the RTC to NTP when connectivity returns.
Upgrades
- Add a Telegram bot that sends an alert message on every new alarm event
- Add an MQ-7 CO sensor for carbon monoxide detection independent of smoke
- Add a web page to view the full NVS alarm event log from a browser
- Add a test button that temporarily raises the threshold to verify the alarm circuit works
FAQ
You need an ESP32 DevKit, MQ-2 AOUT, MQ-2 DOUT, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Smart City. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
The advanced build publishes all sensor readings and alarm events to an MQTT broker in JSON format so Home Assistant or Node-RED can trigger sirens, notifications, and automated responses across the whole smart home. Multiple ESP32 fire alarm nodes in different rooms subscribe to a central MQTT topic to coordinate — if one room detects fire, all buzzers activate simultaneously. A web admin page shows a live floor plan with per-room status indicators.
Components
- 1× ESP32 DevKit V1 per room — Minimum 2 for multi-room demo
- 1× MQ-2 smoke sensor — Per unit
- 1× DHT22 sensor — Per unit
- 1× MQTT broker (Mosquitto) — Central coordinator
- 1× Active buzzer per unit
- 1× Home Assistant or Node-RED — For dashboard and automation
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| All sensors and outputs | Same as intermediate | Replicate per room node |
Arduino Code
// ESP32 Fire Alarm - Advanced (MQTT multi-room coordination)
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <DHT.h>
DHT dht(4,DHT22);
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
const char* SSID="YourSSID", *PASS="YourPass";
const char* MQTT_HOST="192.168.1.100";
const char* ROOM_ID="living-room"; // Unique per node
const char* TOPIC_STATUS ="fire/status";
const char* TOPIC_ALARM ="fire/alarm";
const char* TOPIC_CMD ="fire/command";
const int MQ2=34, DOUT=35, LED_R=25, BUZ=27;
const int SMOKE_THRESH=1800;
const float TEMP_THRESH=60.0f;
bool localAlarm=false, globalAlarm=false;
void mqttCallback(char* topic, byte* payload, unsigned int len){
String msg((char*)payload,len);
// Another room triggered alarm — activate buzzer here too
if(msg!="CLEAR") globalAlarm=true;
else globalAlarm=false;
}
void publishStatus(int smoke, float temp, bool alarm){
StaticJsonDocument<128> doc;
doc["room"] = ROOM_ID;
doc["smoke"] = smoke;
doc["temp"] = temp;
doc["alarm"] = alarm;
char buf[128]; serializeJson(doc,buf);
mqtt.publish(TOPIC_STATUS,buf);
if(alarm) mqtt.publish(TOPIC_ALARM,ROOM_ID);
}
void setup(){
Serial.begin(115200);
dht.begin();
pinMode(DOUT,INPUT); pinMode(LED_R,OUTPUT); pinMode(BUZ,OUTPUT);
analogReadResolution(12);
WiFi.begin(SSID,PASS);
while(WiFi.status()!=WL_CONNECTED) delay(500);
mqtt.setServer(MQTT_HOST,1883);
mqtt.setCallback(mqttCallback);
delay(30000); // preheat
}
void loop(){
if(!mqtt.connected()){ mqtt.connect(ROOM_ID); mqtt.subscribe(TOPIC_ALARM); }
mqtt.loop();
int smoke=analogRead(MQ2);
float temp=dht.readTemperature();
bool smokeAlarm=(smoke>SMOKE_THRESH)||(digitalRead(DOUT)==LOW);
bool tempAlarm=(!isnan(temp)&&temp>TEMP_THRESH);
localAlarm=smokeAlarm||tempAlarm;
publishStatus(smoke,isnan(temp)?-1:temp,localAlarm);
bool anyAlarm=localAlarm||globalAlarm;
digitalWrite(LED_R,anyAlarm?HIGH:LOW);
digitalWrite(BUZ,anyAlarm?HIGH:LOW);
delay(5000); // Publish every 5 seconds
}How It Works
Multi-Room MQTT Coordination: Each room node publishes its sensor readings and alarm status to fire/status and a room ID to fire/alarm when triggered. All other nodes subscribe to fire/alarm and activate their local buzzers when any room publishes an alarm, creating a building-wide alert from any single detection.
JSON Status Publishing: The fire/status JSON payload includes room ID, smoke ADC value, temperature, and alarm boolean. Home Assistant MQTT sensor entities subscribe to this topic and display per-room status on a Lovelace dashboard with coloured indicators.
Global Alarm Propagation: When mqttCallback receives a fire/alarm message, globalAlarm is set true regardless of local sensor readings. This ensures all rooms alert even if the fire is only in one area. Publishing "CLEAR" resets globalAlarm across all nodes.
Home Assistant Integration: Home Assistant MQTT binary sensor entities detect the alarm boolean in the JSON payload. Automations then trigger TTS announcements on smart speakers, send push notifications to phones, and activate Z-Wave or Zigbee sirens throughout the building.
Applications
- Whole-home fire and gas alarm network with central monitoring
- Commercial building fire panel replacement with IoT connectivity
- Multi-unit apartment block with per-unit alarms and central dashboard
- School classroom fire safety network with office admin alert
Troubleshooting
Global alarm does not clear when fire/alarm receives CLEAR
Check that the CLEAR message is published with the retain flag set to false so it does not persist in the broker. Also ensure the mqtt.subscribe(TOPIC_ALARM) call succeeds after reconnection.
Multiple nodes publish conflicting status
Use unique ROOM_ID values per node. Add the room ID to all MQTT topics: fire/status/living-room, fire/status/kitchen, etc. This prevents JSON payload collisions on a shared topic.
Network latency delays alarm propagation
Add mqtt.setSocketTimeout(1) to reduce MQTT network wait time. For critical safety applications add an ESP-NOW fallback that directly broadcasts the alarm without relying on the Wi-Fi router or broker.
Upgrades
- Add a 4G modem so the alarm works even if home internet is down
- Integrate with Google Home or Amazon Alexa for voice alarm announcements
- Add a battery backup and use deep sleep between readings to extend runtime
- Add a local web server as an MQTT broker fallback if the central broker is offline
FAQ
You need an ESP32 DevKit, MQ-2 AOUT, MQ-2 DOUT, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Smart City. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.