Build a gas leak alarm with a siren and phone alerts

An MQ-2 gas sensor watches the air in the kitchen. When gas starts to build up, the alarm lights up, sounds a siren and sends a push message to the whole family's phones, even when nobody is home. It also...
  • Level:Advanced
  • Duration: 2 h
  • Ages 12+
  • Platform: ESP32

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build a gas leak alarm with a siren and phone alerts – EEMENTOR STEM™ lesson

Cooking gas is useful, but a leak is dangerous: the gas can catch fire or explode, and it can make people ill. Gas from pipes and bottles has a strong smell added on purpose, so that people notice a leak. But nobody can smell anything while they are asleep or away from home. In this capstone you build a gas leak alarm with the ESP32. An MQ-2 gas sensor checks the air many times a second. When the gas level rises a little, the yellow light comes on and the alarm beeps. When it rises a lot, a loud siren sounds, the red light flashes and every phone that follows your ntfy topic gets an urgent message. The alarm keeps working without Wi-Fi, because the siren is more important than the message. You will also learn how real alarms think about safety: they measure clean air first, never ignore a slowly rising level, and raise a "fault" warning when the sensor itself fails.

What you'll learn

  • How an MQ-2 gas sensor works, and why it needs to warm up
  • How to bring a 5 V sensor signal safely down to the ESP32's 3.3 V with a voltage divider
  • How to compare readings with clean air, with two alarm levels and hysteresis
  • How to design a device that fails safely: local alarm first, messages second, and a fault alarm
  • Where to put a gas alarm, and what to do if you ever smell gas

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • MQ-2 gas sensor module (not in the kits; about 2–4 euros from electronics shops)
  • 3 resistors, 10 kΩ, for the voltage divider (ESP32 IoT Starter Kit)
  • Green, yellow and red LEDs and 3 resistors, 220 Ω (ESP32 IoT Starter Kit)
  • Passive buzzer (ESP32 IoT Starter Kit)
  • A phone with the free ntfy app (Android or iPhone)
  • A USB phone charger, to keep it running all the time
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)

Safety first

This is a learning project, not a certified gas alarm. It can help you understand how gas alarms work and add phone messages, but it must never be the only protection in a home. Homes that use gas should have certified gas and carbon monoxide alarms, fitted as the instructions say. The MQ-2 does not measure carbon monoxide (CO), the invisible gas that faulty boilers and heaters can make, so this project is not a CO alarm.

If you ever smell gas, or any gas alarm goes off:

  1. Don't switch lights or appliances on or off, don't use flames or lighters, and don't unplug anything. A tiny spark can ignite gas.
  2. Open doors and windows, if you can do it on your way out.
  3. If an adult knows how, they turn off the gas at the meter or the gas bottle's valve.
  4. Get everyone out of the house.
  5. From outside, call the gas emergency number for your country, or the emergency services.

Never test the alarm with real gas, a lighter or a gas bottle. The test below uses hand sanitiser, which is much safer. The sensor's heater gets hot, so don't touch its metal mesh while it is on.

Wiring

Breadboard wiring diagram: Build a gas leak alarm with a siren and phone alerts
Breadboard view. Check each connection against the wiring table below.
From To
MQ-2 VCC ESP32 5V
MQ-2 GND GND
MQ-2 AO First 10 kΩ resistor (R1)
Other end of R1 ESP32 GPIO 34, and the second 10 kΩ resistor (R2)
Other end of R2 Third 10 kΩ resistor (R3)
Other end of R3 GND
MQ-2 DO Not connected
ESP32 GPIO 25, 26, 27 220 Ω resistor, then the long leg of the green, yellow and red LED
LEDs' short legs GND
Passive buzzer + ESP32 GPIO 33
Passive buzzer − GND

The MQ-2 runs on 5 V, so its AO output can go up to 5 V. The ESP32's pins only take 3.3 V. The three resistors form a voltage divider: GPIO 34 sits after the first resistor, with two resistors still below it, so it gets two thirds of the voltage: at most 3.3 V. You may know this trick from the ESP32 radar. The mute button is the BOOT button that is already on the ESP32 board.

Where to put it

Gas Used in Put the alarm
Natural gas (mostly methane) Homes with a gas pipe from the street High up, about 30 cm below the ceiling: methane is lighter than air and rises
LPG (butane or propane) Gas bottles and cylinders Low down, about 30 cm above the floor: LPG is heavier than air and sinks

Put it 1 to 4 metres away from the cooker, not right above it: steam, grease and cooking smells can cause false alarms. Keep it away from windows, fans and the cooker hood, which blow the gas away from the sensor, and not inside a cupboard.

Set up ntfy

The phone messages go through ntfy, a free notification service that needs no account. The ntfy lesson explains it step by step.

  1. Install the ntfy app from Google Play or the App Store.
  2. Make up a topic name that nobody could guess, such as eementor-gas-7f3k9q. Use only letters, numbers, - and _.
  3. In the app, tap +, type your topic name and tap Subscribe. Everyone who should get the messages subscribes to the same topic on their own phone.
  4. Test it: open https://ntfy.sh/your-topic-name in a browser, type a message and send it. It should pop up on the phones.

Important: on the free ntfy.sh server, anyone who knows your topic name can read your messages. Keep the name long and random, and never send personal details through it.

The sketch

Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).

Put your topic name in TOPIC and the room in PLACE. Everything else is built into the ESP32 board package.

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>

const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TOPIC = "YOUR_SECRET_TOPIC";      // for example eementor-gas-7f3k9q
const char* PLACE = "Kitchen";                // where the alarm is

const int GAS_PIN = 34;                       // MQ-2 AO, through the voltage divider
const int GREEN_PIN = 25, YELLOW_PIN = 26, RED_PIN = 27, BUZZER_PIN = 33;
const int MUTE_PIN = 0;                       // the BOOT button on the ESP32 board

const unsigned long WARM_UP = 120000;         // the sensor's heater needs 2 minutes
const int WARNING_RISE = 400;                 // this much above clean air: warning
const int ALARM_RISE = 1000;                  // this much above clean air: alarm
const unsigned long MUTE_TIME = 300000;       // BOOT silences the buzzer for 5 minutes

enum Level { WARMING, CLEAR, WARNING, ALARM, FAULT };
const char* LEVEL_NAMES[] = {"Warming up", "All clear", "WARNING", "ALARM", "SENSOR FAULT"};
Level level = WARMING;
Level sentLevel = CLEAR;                      // the last level the phones heard about

float reading = 0;                            // smoothed sensor reading
float cleanAir = 0;                           // what clean air reads
unsigned long warmStart = 0, clearSince = 0, faultSince = 0, muteUntil = 0;
unsigned long lastWifiTry = 0, lastSendTry = 0, lastPrint = 0, lastSirenSwap = 0;
int buzzerFreq = 0;

int readGas() {                               // average of 16 readings
  long sum = 0;
  for (int i = 0; i < 16; i++) sum += analogRead(GAS_PIN);
  return sum / 16;
}

void buzzer(int freq) {                       // only talk to the buzzer when the sound changes
  if (freq == buzzerFreq) return;
  buzzerFreq = freq;
  if (freq == 0) noTone(BUZZER_PIN);
  else tone(BUZZER_PIN, freq);
}

bool notify(const char* title, String message, const char* priority, const char* tags) {
  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;
  http.setTimeout(5000);
  http.begin(client, String("https://ntfy.sh/") + TOPIC);
  http.addHeader("Title", title);
  http.addHeader("Priority", priority);
  http.addHeader("Tags", tags);
  int code = http.POST(message);
  http.end();
  Serial.printf("ntfy answered %d\n", code);
  return code == 200;
}

bool sendLevel(Level l) {
  String where = String(" in the ") + PLACE + ".";
  String numbers = " (Reading " + String((int)reading) + ", clean air " + String((int)cleanAir) + ")";
  if (l == ALARM)
    return notify("GAS ALARM", "Very high gas level" + where + numbers + " Don't touch switches or flames. Open windows if it's safe, get everyone out and call the gas emergency number from outside.", "urgent", "rotating_light,warning");
  if (l == WARNING)
    return notify("Gas warning", "The gas level is rising" + where + numbers + " Check for a gas smell and open a window.", "high", "warning");
  if (l == FAULT)
    return notify("Gas alarm fault", "The gas sensor" + where + " isn't working. Check its wiring.", "high", "wrench");
  return notify("Gas level OK", "The gas level is back to normal" + where + numbers, "default", "white_check_mark");
}

void setLevel(Level l) {
  if (l == level) return;
  if (l > level) muteUntil = 0;               // worse than before: sound again
  if (l == WARMING) warmStart = millis();     // start the warm-up timer again
  level = l;
  Serial.print("Level: ");
  Serial.println(LEVEL_NAMES[l]);
}

void setup() {
  Serial.begin(115200);
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(YELLOW_PIN, OUTPUT);
  pinMode(RED_PIN, OUTPUT);
  pinMode(MUTE_PIN, INPUT_PULLUP);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_NAME, WIFI_PASSWORD);       // don't wait: the alarm must work without Wi-Fi
  reading = readGas();
  Serial.println("Gas alarm: warming up for 2 minutes in clean air...");
}

void loop() {
  unsigned long now = millis();
  int raw = readGas();
  reading += 0.2 * (raw - reading);

  // 1. Sensor fault: if the sensor is unplugged, the divider pulls GPIO 34 down to 0
  if (raw < 30) {
    if (faultSince == 0) faultSince = now;
    if (now - faultSince > 5000) setLevel(FAULT);
  } else {
    faultSince = 0;
    if (level == FAULT) setLevel(WARMING);    // sensor back: warm up and measure clean air again
  }

  // 2. Warm up, then compare with clean air
  if (level == WARMING) {
    if (now - warmStart > WARM_UP && faultSince == 0) {
      cleanAir = reading;
      Serial.printf("Clean air reads %d\n", (int)cleanAir);
      setLevel(CLEAR);
    }
  } else if (level != FAULT) {
    float rise = reading - cleanAir;
    if (rise >= ALARM_RISE) setLevel(ALARM);
    else if (rise >= WARNING_RISE && level != ALARM) setLevel(WARNING);
    if (rise < WARNING_RISE / 2) {            // well below the warning level again
      if (clearSince == 0) clearSince = now;
      if (level != CLEAR && now - clearSince > 30000) setLevel(CLEAR);
    } else {
      clearSince = 0;
    }
    // A new sensor slowly reads lower for hours: follow it down, but never up,
    // so even a very slow leak can't hide by moving the "clean air" value
    if (reading < cleanAir) cleanAir += 0.01 * (reading - cleanAir);
  }

  // 3. BOOT button: silence the buzzer (the lights keep warning)
  if (digitalRead(MUTE_PIN) == LOW && level >= WARNING) {
    muteUntil = now + MUTE_TIME;
    Serial.println("Buzzer muted for 5 minutes");
  }
  bool quiet = muteUntil != 0 && (long)(muteUntil - now) > 0;

  // 4. Lights and sound
  bool fast = (now / 125) % 2, slow = (now / 500) % 2;
  digitalWrite(GREEN_PIN, level == CLEAR);
  digitalWrite(YELLOW_PIN, level == WARMING ? slow : level == WARNING || (level == FAULT && slow));
  digitalWrite(RED_PIN, level == ALARM ? fast : (level == FAULT && !slow));
  if (level == ALARM && !quiet) {             // two-tone siren
    if (now - lastSirenSwap > 300) {
      lastSirenSwap = now;
      buzzer(buzzerFreq == 1800 ? 1200 : 1800);
    }
  } else if ((level == WARNING || level == FAULT) && !quiet && now % 5000 < 150) {
    buzzer(1500);                             // short beep every 5 seconds
  } else {
    buzzer(0);
  }

  // 5. Wi-Fi and phone messages: retried until they get through
  if (WiFi.status() != WL_CONNECTED && now - lastWifiTry > 30000) {
    lastWifiTry = now;
    WiFi.disconnect();
    WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
  }
  if (level != WARMING && level != sentLevel && WiFi.status() == WL_CONNECTED && now - lastSendTry > 10000) {
    lastSendTry = now;
    Level l = level;
    if (sendLevel(l)) sentLevel = l;
  }

  if (now - lastPrint > 2000) {
    lastPrint = now;
    Serial.printf("Gas %d   clean air %d   rise %d   %s\n", (int)reading, (int)cleanAir, (int)(reading - cleanAir), LEVEL_NAMES[level]);
  }
  delay(20);
}

Test it safely

  1. Upload the sketch and open the Serial Monitor at 115200 baud. The yellow LED blinks for 2 minutes while the heater warms up. Keep the sensor in clean air, away from people breathing on it. A brand-new sensor may smell a little the first time: let it run for a few hours before you trust its readings.
  2. After the warm-up the green LED comes on and the Serial Monitor shows the clean-air reading, for example Clean air reads 610.
  3. With an adult, away from any flame, put a drop of alcohol hand sanitiser on a cotton pad and hold it 2 to 3 cm from the sensor. The MQ-2 also reacts to alcohol vapour. Watch "rise" climb: above 400 the yellow LED comes on and the phones get a warning; above 1000 the siren starts and the phones get an urgent alarm.
  4. Press the BOOT button: the siren stops for 5 minutes, but the red light keeps flashing. Take the pad away and open a window. When the level has stayed low for 30 seconds, the alarm returns to green and the phones get "Gas level OK".
  5. Test the fault alarm: unplug the AO wire. After 5 seconds the yellow and red LEDs take turns and the phones hear about a sensor fault. Plug it back in and the alarm warms up again.

For quick tests in class, you can set WARM_UP to 20000 (20 seconds). Put it back to at least 2 minutes for real use.

How it works

Inside the MQ-2 is a tiny heater and a layer of tin dioxide. In clean air, oxygen sticks to the hot surface and the layer barely conducts. Gases like methane, propane, butane, hydrogen, alcohol vapour and smoke take that oxygen away, and the layer conducts better. The module turns that into a voltage on AO: more gas, higher voltage. That is also why the heater must warm up first, and why the sensor reacts to other things such as cleaning sprays or cooking with wine.

Every MQ-2 reads a different number in clean air, and the number changes with temperature and humidity. So the alarm doesn't use fixed numbers. After warming up, it remembers what clean air reads, and from then on it only looks at the rise above that. Two levels give an early warning and a real alarm. Going back to "all clear" needs the rise to drop to half the warning level and stay there for 30 seconds. This gap between "switch on" and "switch off" is called hysteresis; without it, a level close to the limit would make the alarm switch on and off all the time.

The clean-air value may only follow the readings down. If it could follow them up, a slow leak would slowly be accepted as "normal" and never trigger the alarm. Real alarms are designed like this: when in doubt, warn.

The order of the code is also a safety decision. The lights and the siren are switched in every pass of loop(), whether there is Wi-Fi or not. The phone message comes last, and if it fails, the sketch tries again every 10 seconds until it gets through. And if the sensor is unplugged, GPIO 34 drops to almost 0 V, a value clean air never gives, so the alarm reports a fault instead of quietly showing "all clear".

client.setInsecure() keeps the connection encrypted but skips checking the server's certificate. That keeps the sketch short and is fine for a learning project. For a device you leave running for years, load the server's root certificate with client.setCACert() instead.

Try this next

  • Switch on an extractor fan in the model kitchen with the relay module when the alarm goes off (low-voltage fan only, never mains).
  • Add a 1602 LCD showing the reading, the clean-air value and the level.
  • Log the readings to a Google Sheet every minute, and look at how the clean-air reading changes over a day.
  • Send a short "I'm alive" message once a day, so you notice if the alarm loses its power or Wi-Fi.

Troubleshooting

  • The reading stays near 0 and it reports a fault: check the MQ-2's VCC on 5V (not 3V3), AO to the first resistor, and the three resistors in a chain from GPIO 34 down to GND.
  • It warns without any gas: the sensor wasn't warm enough when it measured clean air, or someone breathed on it. Let it run for 10 minutes and press EN to restart. Keep it away from the cooker, steam and sprays.
  • It never reacts to the sanitiser: hold the pad closer, and check that the sensor's mesh gets warm. Some sanitisers have little alcohol; try a drop of rubbing alcohol instead.
  • The siren pauses for a moment now and then: that's the ESP32 sending a phone message. The lights and the alarm continue right after.
  • No message on the phone: look for "ntfy answered" in the Serial Monitor. 200 means it was sent: check the topic name in the app letter by letter. -1 means no internet; 429 means too many messages in a short time.
  • It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

ESP32 IoT Starter Kit | Arduino IDE Compatible | Free Tutorials $57.00

Shop the kit