Siddhant Kumar
Project 010 · Smart Home

Gas Leak Detector + Auto Shutoff.

An LPG and CO detector that does the thing most projects skip — it acts. On a confirmed leak it closes the gas valve, kills the ignition source, sounds a 100 dB alarm and ventilates, all before it thinks about the network.

Intermediate 8–12 hours 38 min read SafetySensorsRelay
Jump to source Bill of materials
Gas Leak Detector + Auto Shutoff — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Intermediate
Build time
8–12 hours
Indicative cost
₹5,200 – ₹8,000
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Smart Home
Last updated
28 July 2026
Contents — 25 sections

Project Overview

An LPG and CO detector that does the thing most projects skip — it acts. On a confirmed leak it closes the gas valve, kills the ignition source, sounds a 100 dB alarm and ventilates, all before it thinks about the network.

A gas alarm that only beeps is a partial solution. The dangerous window with LPG is short: the lower explosive limit is around 1.8 % by volume, and a leaking regulator can reach that in a small kitchen in minutes. What matters is closing the source and removing the ignition risk, and doing both without waiting for a phone, a router or a person.

The sensing uses two different sensors because they detect different threats. An MQ-2 responds to LPG, propane, methane and hydrogen — the explosion risk. An MQ-7 measures carbon monoxide, which is the poisoning risk from incomplete combustion and is odourless, colourless and kills people in their sleep. Neither substitutes for the other, and a single-sensor build only covers half the problem.

MQ-series sensors are the right tool here despite their limitations, because for a threshold alarm you do not need quantitative accuracy — you need a reliable "concentration is rising fast" signal, which they provide well. But they demand things people routinely skip: a 24–48 hour burn-in when new, a clean-air R0 calibration, a stable 5 V for the heater, and — for the MQ-7 — an alternating heater cycle that most tutorials get wrong.

The action chain is ordered by how fast each step reduces risk and how likely it is to work. Valve closed first, ignition sources cut second, siren third, extraction fan fourth, and only then the network. Every one of the first four works with the Wi-Fi down.

What this project does

  • Detects combustible gas (LPG, methane, propane) and carbon monoxide with separate dedicated sensors.
  • Closes a motorised gas valve within about two seconds of a confirmed leak.
  • Cuts power to a relay-controlled ignition source (electric hob, extractor motor) before ventilating.
  • Sounds a 100 dB siren locally, independent of any network.
  • Runs an extraction fan on a non-sparking circuit to clear the space.
  • Runs the MQ-7 heater on its correct 60 s / 90 s alternating cycle.
  • Publishes alerts and continuous readings over MQTT, and retries until acknowledged.

Real-World Applications

SettingHow it is used
Domestic kitchensThe primary case, especially where an LPG cylinder is used indoors.
Small restaurants and food cartsMultiple cylinders, high usage and often poor ventilation — the highest-risk environment of all.
Boiler and water-heater roomsCO from incomplete combustion is the specific threat here, and it gives no warning.
Basements and voidsLPG is heavier than air and collects at low points, so a detector at ceiling height would never see it.
Workshops using propane or acetyleneCombined with a ventilation interlock so the extractor is proven running before the torch can be lit.
Caravans and boatsSmall sealed spaces with bottled gas, where a leak concentrates very quickly.

Deployment contexts where a build of this kind earns its keep.

Features & Capabilities

  • Dual-gas detection — explosive and toxic threats have separate sensors and separate thresholds.
  • Rate-of-rise detection alongside absolute thresholds, catching a fast leak before it reaches the alarm level.
  • Correct MQ-7 dual-heater cycle (1.4 V for 90 s, 5 V for 60 s) driven by PWM.
  • Clean-air R0 calibration stored in NVS, with a documented recalibration procedure.
  • Ordered action chain with the network last, so nothing safety-critical depends on it.
  • Self-test on boot that verifies each sensor responds and each actuator moves.
  • Sensor-failure detection — a reading pinned at either rail is treated as a fault, not as clean air.
  • Latching alarm requiring a manual reset after a real event.

Difficulty, Time & Required Skills

AttributeValue
Difficulty levelIntermediate
Estimated completion time8–12 hours
Indicative build cost₹5,200 – ₹8,000
Primary disciplineSmart Home
Reference platformESP32 DevKit V1 (ESP-WROOM-32)

Skills you should have (or will pick up)

  • Arduino C++ and analogue reading
  • Understanding of PWM used to control a heater voltage
  • Relay wiring and load switching
  • Basic gas safety awareness — this project touches a real hazard
  • MQTT basics

Bill of Materials

Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.

ComponentKey specificationQtyApprox. cost
ESP32 DevKit V1 (ESP-WROOM-32)
Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC1₹450
MQ-2 combustible gas / smoke sensor
Needs 24–48 h burn-in and a stable 5 V; the heater alone draws ~150 mA.
300–10000 ppm LPG, propane, methane, hydrogen, smoke; analogue + digital output1₹150
MQ-7 carbon monoxide sensor
Requires an alternating 60 s / 90 s heater cycle to read correctly — a constant 5 V gives wrong numbers.
20–2000 ppm CO, dual-heater cycle (1.4 V / 5 V), analogue output1₹260
4-channel opto-isolated relay board
All four coils energised draw ~280 mA — do not power from the MCU 5 V pin.
4 × SPDT, 10 A @ 250 VAC, active-low inputs, LED per channel1₹280
0.96″ SSD1306 OLED display
Static images burn in — invert or scroll the screen periodically.
128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C1₹250
LM2596 adjustable buck converter module
Set the output voltage with no load connected before wiring the board.
4.5–40 V in, 1.25–37 V out, 2 A (3 A peak), ~92 % efficiency1₹90
12 V 5 A SMPS adapter
Fuse the 12 V rail at ~1.5× your calculated draw.
100–240 VAC in, 12 V ±5 %, 5 A, 60 W, DC 5.5 × 2.1 mm barrel1₹650
Double-sided perfboard 7 × 9 cm + headers
Solder female headers so the MCU can be swapped without desoldering.
FR-4, 0.1″ pitch, plated through-holes, 24 × 18 grid1₹60
IP65 ABS junction enclosure 158 × 90 × 60 mm
Fit cable glands, not drilled holes, or the IP rating means nothing.
IP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugs1₹260
12 V motorised gas ball valve
Must be rated for gas, not just water. A water valve's seals will not do.
DN15/DN20, brass, 8 W, with limit switches1₹2,800
100 dB piezo siren12 V, 110 mA1₹320
Explosion-rated extraction fan (or existing hood)
A standard fan motor brush can be the ignition source you are trying to avoid.
Non-sparking motor1₹1,800
Manual reset button (key or recessed)NO momentary1₹90

Estimated total: ₹7,460, excluding tools, shipping and consumables.

Tools and consumables

  • Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
  • Digital multimeter — continuity, DC volts and current ranges
  • Wire strippers, flush cutters and a small set of precision screwdrivers
  • Heat-shrink tubing and a heat gun (or a lighter, carefully)
  • A laptop with a USB port and the toolchain listed above

Hardware Specifications

PartSpecificationSupplyInterfaceReference
ESP32 DevKit V1 (ESP-WROOM-32)Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
MQ-2 combustible gas / smoke sensor300–10000 ppm LPG, propane, methane, hydrogen, smoke; analogue + digital output5 V (heater)Analogue + comparator digitalDatasheet
MQ-7 carbon monoxide sensor20–2000 ppm CO, dual-heater cycle (1.4 V / 5 V), analogue output5 VAnalogue (PWM heater)Datasheet
4-channel opto-isolated relay board4 × SPDT, 10 A @ 250 VAC, active-low inputs, LED per channel5 V coil4× digitalDatasheet
0.96″ SSD1306 OLED display128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C3.3–5 VI²C (0x3C)Datasheet
LM2596 adjustable buck converter module4.5–40 V in, 1.25–37 V out, 2 A (3 A peak), ~92 % efficiency4.5–40 VScrew terminals + trimmerDatasheet
12 V 5 A SMPS adapter100–240 VAC in, 12 V ±5 %, 5 A, 60 W, DC 5.5 × 2.1 mm barrel12 VDC barrelDatasheet
Double-sided perfboard 7 × 9 cm + headersFR-4, 0.1″ pitch, plated through-holes, 24 × 18 gridDatasheet
IP65 ABS junction enclosure 158 × 90 × 60 mmIP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugsDatasheet

Consolidated electrical and interface specifications for every active part in the build.

Power Budget & Supply Sizing

Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
MQ-2 combustible gas / smoke sensor5 V (heater)150Needs 24–48 h burn-in and a stable 5 V; the heater alone draws ~150 mA.
MQ-7 carbon monoxide sensor5 V150Requires an alternating 60 s / 90 s heater cycle to read correctly — a constant 5 V gives wrong numbers.
4-channel opto-isolated relay board5 V coil280All four coils energised draw ~280 mA — do not power from the MCU 5 V pin.
0.96″ SSD1306 OLED display3.3–5 V20Static images burn in — invert or scroll the screen periodically.
LM2596 adjustable buck converter module4.5–40 V8Set the output voltage with no load connected before wiring the board.
12 V 5 A SMPS adapter12 V5000Fuse the 12 V rail at ~1.5× your calculated draw.

Summed typical draw is 5768 mA. With a 1.5× design margin the supply should deliver at least 8700 mA continuously at the stated rail voltage.

Software Requirements & Development Environment

Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.

  • Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
  • Add https://espressif.github.io/arduino-esp32/package_esp32_index.json under File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager.
  • Set the correct port under Tools → Port. On Linux add yourself to the dialout group: sudo usermod -aG dialout $USER and log out and back in.
  • Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
  • Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.

Required libraries

LibraryWhy it is neededInstall
WiFi (ESP32 core) bundledStation/AP connection management for the ESP32.Bundled with the ESP32 Arduino core
PubSubClient 2.8Lightweight MQTT 3.1.1 client for constrained devices.Library Manager → "PubSubClient" by Nick O'Leary
ArduinoJson 7.xZero-allocation JSON serialisation and parsing.Library Manager → "ArduinoJson" by Benoit Blanchon
Adafruit SSD1306 + GFX 2.5.xFramebuffer and text/graphics primitives for the OLED.Library Manager → "Adafruit SSD1306"
Preferences (NVS) bundledWear-levelled key/value storage in ESP32 flash for settings.Bundled with the ESP32 core

Block Diagram

The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.

Gas Leak Detector + Auto Shutoff — system block diagramFunctional block diagram of the Gas Leak Detector + Auto Shutoff system. DetectMQ-2LPG / methaneMQ-7carbon monoxideAssessppm estimateRs/R0 curveRate of risefast-leak catchActClose valvefirstCut ignitionsecondSiren + fanthirdReportMQTT alertretry to ackPhone pushvia brokerADC + R0confirmedafter acting
Gas Leak Detector + Auto Shutoff — system block diagram

Circuit Diagram & Wiring

Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.

Gas Leak Detector + Auto Shutoff — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBMQ-2 combustible gasGPIO 34Analogue, 0–3.3 Vvia dividerMQ-7 carbon monoxideGPIO 35Analogue, read atend of low-heatphaseManual resetGPIO 32Clears a latchedalarmValve limit switchGPIO 33Confirms the valveactually movedMQ-7 heater controlGPIO 25 (PWM)MOSFET, 1.4 V / 5 VcycleRelay 1 → gas valve closeGPIO 26Momentary driveRelay 2 → ignition sourcecutGPIO 27Normally closedcontactRelay 3 → extraction fanGPIO 14After ignitionsources are cutRelay 4 → 100 dB sirenGPIO 12Immediate
Gas Leak Detector + Auto Shutoff — wiring schematic
PeripheralPeripheral pinController pinSignal
MQ-2 combustible gasAOGPIO 34Analogue, 0–3.3 V via divider
MQ-7 carbon monoxideAOGPIO 35Analogue, read at end of low-heat phase
Manual resetNOGPIO 32Clears a latched alarm
Valve limit switchClosed positionGPIO 33Confirms the valve actually moved
MQ-7 heater controlHeater +GPIO 25 (PWM)MOSFET, 1.4 V / 5 V cycle
Relay 1 → gas valve closeIN1GPIO 26Momentary drive
Relay 2 → ignition source cutIN2GPIO 27Normally closed contact
Relay 3 → extraction fanIN3GPIO 14After ignition sources are cut
Relay 4 → 100 dB sirenIN4GPIO 12Immediate

Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.

Wiring explanation

  • The MQ sensors output up to 5 V on their analogue pin. The ESP32 ADC is 3.3 V maximum — use a 10 kΩ / 20 kΩ divider on each, or you will destroy the input.
  • Both sensors have heaters drawing about 150 mA each. Power them from the 5 V rail directly, never from the ESP32.
  • The MQ-7 heater must alternate between 1.4 V (90 s) and 5 V (60 s). Generate the 1.4 V with PWM at about 28 % duty through a logic-level MOSFET plus a small RC filter — a bare digital pin cannot do this and running it at a constant 5 V gives readings that are simply wrong.
  • The ignition-source relay must open before the extraction fan starts. A fan motor spinning up in a gas-filled room is exactly the spark you are trying to avoid. The firmware enforces this ordering, and your wiring must not defeat it.
  • Mount the MQ-2 low — LPG is heavier than air and pools at floor level. Mount a methane-focused sensor high, since methane is lighter than air. If you use piped natural gas rather than cylinders, the mounting height inverts.
  • The sensors must be in free air with ventilation on all sides. Do not enclose them; the heater needs to reach thermal equilibrium with the ambient air, not with a sealed box.
A single-channel relay module with screw terminals
A relay module. In this build four channels drive the valve, the ignition cut, the extraction fan and the siren, in a deliberately ordered chain. Photograph sourced from Wikimedia Commons — Relay module.jpg. Reused under the licence stated on that page; please check it before republishing.

System Architecture

Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.

Gas Leak Detector + Auto Shutoff — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · MQ-2 combustible gas / smoke sensor ·MQ-7 carbon monoxide sensorDriver layerwifi · pubsub · arduinojson · ssd1306Application logicsampling loop · filtering · thresholds · state machinePresentation layerlocal display · serial console · logged output
Gas Leak Detector + Auto Shutoff — architecture stack

Working Principle

An MQ-series sensor is a heated tin-dioxide semiconductor. At around 300 °C, atmospheric oxygen adsorbs onto the SnO₂ surface and traps electrons, raising the material's resistance. When a reducing gas such as LPG or CO arrives, it reacts with that adsorbed oxygen, releasing the trapped electrons and lowering the resistance. The sensor is therefore a variable resistor whose value falls as gas concentration rises.

The measurement is a ratio, not an absolute. The datasheet curves plot Rs/R0 against concentration, where Rs is the current sensing resistance and R0 is that same sensor's resistance in clean air. Because sensor-to-sensor variation is large, R0 must be measured for your specific unit — using a datasheet nominal value can put you off by a factor of three. That is the calibration step almost every tutorial omits, and it is why so many builds either never alarm or alarm constantly.

The MQ-7 is more involved because carbon monoxide detection requires a dual heater cycle. At the full 5 V heating phase (60 s) the element is cleaned of accumulated contaminants; at the low 1.4 V phase (90 s) the surface temperature drops to a range where CO adsorption dominates, and the reading is taken at the end of that phase. Running the MQ-7 at a constant 5 V, as most examples do, gives a number that responds to something but is not a CO measurement.

Rate of rise is what turns a slow alarm into a fast one. A leak from a failed regulator can go from background to the lower explosive limit in a couple of minutes. Waiting for an absolute threshold spends much of that window. Tracking the derivative — more than about 200 ppm-equivalent of change in 30 seconds — triggers action while the concentration is still well below anything dangerous.

Finally, sensor-failure detection. A disconnected analogue pin floats and can read anywhere; a shorted sensor reads at a rail. Both look like plausible values. Treating a reading pinned at 0 or at full scale for more than a few seconds as a fault rather than as clean air is the difference between a detector that fails safe and one that fails silently.

The maths behind it

Sensor resistance from the ADC

plainSensor resistance from the ADC
Voltage divider: MQ sensor Rs in series with load RL (usually 10 kΩ)

  V_out = Vcc × RL / (Rs + RL)
  Rs    = RL × (Vcc − V_out) / V_out

With Vcc = 5 V, RL = 10 kΩ, measured V_out = 1.2 V
  Rs = 10000 × (5 − 1.2) / 1.2 = 31.7 kΩ

If clean-air R0 was measured as 9.8 kΩ:
  Rs/R0 = 3.23  →  read ppm from the datasheet curve

LPG concentration from the MQ-2 curve

plainLPG concentration from the MQ-2 curve
The MQ-2 LPG curve is approximately a power law on log-log axes:

  ppm = a × (Rs/R0)^b      with a ≈ 574.25, b ≈ −2.222

Rs/R0 = 3.23  →  ppm = 574.25 × 3.23^(−2.222) ≈ 43 ppm
Rs/R0 = 1.00  →  ppm = 574 ppm
Rs/R0 = 0.50  →  ppm = 2670 ppm

Lower explosive limit for LPG ≈ 18 000 ppm (1.8 %).
Alarm well below that — 2000 ppm is a sensible threshold.

MQ-7 heater duty cycle

plainMQ-7 heater duty cycle
High phase: 5.0 V for 60 s   (cleaning)
Low  phase: 1.4 V for 90 s   (measurement)

PWM duty for 1.4 V from a 5 V rail:
  D = 1.4 / 5.0 = 0.28 = 28 %

Filter with R = 100 Ω, C = 100 µF:
  τ = RC = 10 ms, at 5 kHz PWM ripple is negligible.

Read the ADC in the final 5 s of the low phase,
when the surface temperature has stabilised.

Program Flowchart

The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.

Gas Leak Detector + Auto Shutoff — firmware flowchartControl flow through the main program loop. Boot: load R0, self-testactuatorsRun MQ-7 heater cycle, sampleMQ-2 at 1 HzAbove threshold or risingfast?yespublish readingConfirm over 3 consecutivesamplesStill elevated?confirmedtransient, clearClose valve → cut ignition →siren → fanPublish alert, repeat untilacknowledgedLatch until manual reset
Gas Leak Detector + Auto Shutoff — firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

Step-by-Step Implementation Guide

Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.

Complete Source Code

The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.

cppgas-leak-shutoff.ino
/* ═══════════════════════════════════════════════════════════════
   Gas Leak Detector with Automatic Shutoff — ESP32 + MQ-2 + MQ-7

   Detects combustible gas and carbon monoxide, and on a confirmed
   event closes the gas valve, cuts ignition sources, sounds a siren
   and ventilates — in that order, all before touching the network.

   This supplements a certified alarm. It does not replace one.
   ══════════════════════════════════════════════════════════════════ */

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>
#include <math.h>

#define WIFI_SSID "YOUR_WIFI"
#define WIFI_PASS "YOUR_PASSWORD"
#define MQTT_HOST "192.168.1.50"
#define DEVICE_ID "gas-kitchen"

#define PIN_MQ2       34
#define PIN_MQ7       35
#define PIN_MQ7_HEAT  25
#define PIN_RESET     32
#define PIN_VALVE_LIM 33
#define PIN_R_VALVE   26
#define PIN_R_IGNITION 27
#define PIN_R_FAN     14
#define PIN_R_SIREN   12

#define RL_OHMS      10000.0f
#define VCC          5.0f
#define DIVIDER      1.5f          // 10k/20k divider: Vadc = Vsensor / 1.5

#define LPG_WARN_PPM   1000
#define LPG_ALARM_PPM  2000        // ~11 % of the lower explosive limit
#define CO_WARN_PPM      35        // 8-hour exposure guideline
#define CO_ALARM_PPM    100
#define RISE_PPM_30S    200        // rate-of-rise trigger

#define MQ7_HIGH_MS  60000UL
#define MQ7_LOW_MS   90000UL

Adafruit_SSD1306 oled(128, 64, &Wire, -1);
WiFiClient   net;
PubSubClient mqtt(net);
Preferences  prefs;

float r0_mq2 = 9800, r0_mq7 = 10000;
float lpgPpm = 0, coPpm = 0;
float lpgHistory[30] = {0};
uint8_t histIdx = 0;
bool alarmLatched = false, faultDetected = false, acknowledged = false;
uint32_t mq7PhaseStart = 0;
bool mq7HighPhase = true;

/* ── sensor maths ───────────────────────────────────────────── */
float adcToRs(int pin) {
  uint32_t acc = 0;
  for (int i = 0; i < 16; i++) { acc += analogRead(pin); delayMicroseconds(200); }
  float adc = acc / 16.0f;

  if (adc < 20 || adc > 4075) { faultDetected = true; return -1; }  // rail = fault
  float vAdc = (adc / 4095.0f) * 3.3f;
  float vSensor = vAdc * DIVIDER;
  if (vSensor <= 0.01f) { faultDetected = true; return -1; }
  return RL_OHMS * (VCC - vSensor) / vSensor;
}

float lpgFromRatio(float ratio) {          // MQ-2 LPG curve
  if (ratio <= 0) return 0;
  return 574.25f * powf(ratio, -2.222f);
}

float coFromRatio(float ratio) {           // MQ-7 CO curve
  if (ratio <= 0) return 0;
  return 99.042f * powf(ratio, -1.518f);
}

/* ── MQ-7 heater cycle ──────────────────────────────────────── */
void mq7Service() {
  uint32_t now = millis();
  uint32_t phaseLen = mq7HighPhase ? MQ7_HIGH_MS : MQ7_LOW_MS;

  if (now - mq7PhaseStart >= phaseLen) {
    mq7HighPhase = !mq7HighPhase;
    mq7PhaseStart = now;
    // 100 % duty = 5.0 V cleaning; 28 % duty = 1.4 V measuring
    ledcWrite(0, mq7HighPhase ? 255 : 71);
  }

  // Sample only in the last 5 s of the low phase, once stabilised.
  if (!mq7HighPhase && now - mq7PhaseStart > MQ7_LOW_MS - 5000) {
    float rs = adcToRs(PIN_MQ7);
    if (rs > 0) coPpm = coFromRatio(rs / r0_mq7);
  }
}

/* ── calibration ────────────────────────────────────────────── */
void calibrateR0() {
  Serial.println("Calibrating in clean air — do not breathe on the sensors");
  float acc2 = 0;
  int n = 0;
  for (int i = 0; i < 50; i++) {
    float rs = adcToRs(PIN_MQ2);
    if (rs > 0) { acc2 += rs; n++; }
    delay(200);
  }
  if (n > 20) {
    r0_mq2 = (acc2 / n) / 9.83f;      // MQ-2 clean-air Rs/R0 ratio is 9.83
    prefs.putFloat("r0_mq2", r0_mq2);
    Serial.printf("MQ-2 R0 = %.0f ohm\n", r0_mq2);
  }
}

/* ── the action chain ───────────────────────────────────────── */
void emergencyShutdown(const char *reason) {
  if (alarmLatched) return;
  alarmLatched = true;

  // 1. Close the gas valve — removes the source.
  digitalWrite(PIN_R_VALVE, LOW);
  // 2. Cut ignition sources BEFORE anything else starts moving.
  digitalWrite(PIN_R_IGNITION, LOW);
  // 3. Siren — works with no network, no phone, no broker.
  digitalWrite(PIN_R_SIREN, LOW);

  uint32_t t0 = millis();
  while (millis() - t0 < 3000 && digitalRead(PIN_VALVE_LIM) == HIGH) delay(50);
  digitalWrite(PIN_R_VALVE, HIGH);       // stop driving the valve motor

  // 4. Ventilate only after ignition sources are confirmed cut.
  delay(500);
  digitalWrite(PIN_R_FAN, LOW);

  Serial.printf("EMERGENCY SHUTDOWN: %s\n", reason);
}

void publishAlert(const char *reason) {
  JsonDocument d;
  d["device"] = DEVICE_ID;
  d["alarm"]  = reason;
  d["lpg_ppm"] = (int)lpgPpm;
  d["co_ppm"]  = (int)coPpm;
  d["valve_closed"] = digitalRead(PIN_VALVE_LIM) == LOW;
  d["fault"] = faultDetected;
  char buf[224]; size_t n = serializeJson(d, buf, sizeof(buf));
  mqtt.publish("home/gas/" DEVICE_ID "/alert", (uint8_t *)buf, n, true);
}

/* ── detection ──────────────────────────────────────────────── */
bool rateOfRise() {
  float oldest = lpgHistory[histIdx];               // 30 s ago
  float newest = lpgHistory[(histIdx + 29) % 30];
  return oldest > 0 && (newest - oldest) > RISE_PPM_30S;
}

void detectionService() {
  faultDetected = false;

  float rs2 = adcToRs(PIN_MQ2);
  if (rs2 > 0) lpgPpm = lpgFromRatio(rs2 / r0_mq2);

  lpgHistory[histIdx] = lpgPpm;
  histIdx = (histIdx + 1) % 30;

  static uint8_t confirm = 0;
  bool trigger = lpgPpm > LPG_ALARM_PPM || coPpm > CO_ALARM_PPM || rateOfRise();

  if (trigger) {
    if (++confirm >= 3) {                            // 3 s of confirmation
      const char *why = coPpm > CO_ALARM_PPM ? "carbon-monoxide"
                      : rateOfRise()          ? "rapid-rise"
                                              : "combustible-gas";
      emergencyShutdown(why);
      publishAlert(why);
    }
  } else {
    confirm = 0;
  }

  if (faultDetected) {
    static uint32_t faultSince = 0;
    if (!faultSince) faultSince = millis();
    if (millis() - faultSince > 10000) publishAlert("sensor-fault");
  }
}

void onMessage(char *topic, byte *payload, unsigned int len) {
  if (len >= 3 && !strncmp((char *)payload, "ACK", 3)) acknowledged = true;
}

/* ── setup / loop ───────────────────────────────────────────── */
void setup() {
  Serial.begin(115200);
  pinMode(PIN_RESET, INPUT_PULLUP);
  pinMode(PIN_VALVE_LIM, INPUT_PULLUP);
  int outs[] = { PIN_R_VALVE, PIN_R_IGNITION, PIN_R_FAN, PIN_R_SIREN };
  for (int p : outs) { pinMode(p, OUTPUT); digitalWrite(p, HIGH); }   // active-low idle

  ledcSetup(0, 5000, 8);
  ledcAttachPin(PIN_MQ7_HEAT, 0);
  ledcWrite(0, 255);                       // start in the high-heat phase
  mq7PhaseStart = millis();

  analogSetPinAttenuation(PIN_MQ2, ADC_11db);
  analogSetPinAttenuation(PIN_MQ7, ADC_11db);

  Wire.begin(21, 22);
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);

  prefs.begin("gas", false);
  r0_mq2 = prefs.getFloat("r0_mq2", 9800);
  r0_mq7 = prefs.getFloat("r0_mq7", 10000);

  WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS);
  for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) delay(250);
  mqtt.setServer(MQTT_HOST, 1883);
  mqtt.setCallback(onMessage);

  // Sensors need 3 minutes of heater warm-up before readings mean anything.
  Serial.println("Warming up sensors — 180 s");
  for (int i = 180; i > 0; i--) {
    oled.clearDisplay(); oled.setTextColor(SSD1306_WHITE);
    oled.setTextSize(1); oled.setCursor(0, 20);
    oled.printf("Warming up\n%d s remaining", i);
    oled.display();
    delay(1000);
  }
  Serial.println("Gas detector armed");
}

void loop() {
  if (!mqtt.connected() && WiFi.status() == WL_CONNECTED) {
    if (mqtt.connect(DEVICE_ID)) mqtt.subscribe("home/gas/" DEVICE_ID "/cmd");
  }
  mqtt.loop();
  mq7Service();

  static uint32_t last = 0;
  if (millis() - last >= 1000) {
    last = millis();
    if (!alarmLatched) detectionService();

    oled.clearDisplay();
    oled.setTextColor(SSD1306_WHITE);
    oled.setTextSize(1);
    oled.setCursor(0, 0);  oled.printf("LPG  %5.0f ppm", lpgPpm);
    oled.setCursor(0, 12); oled.printf("CO   %5.0f ppm", coPpm);
    oled.setCursor(0, 24); oled.printf("MQ7  %s phase", mq7HighPhase ? "clean" : "measure");
    oled.setCursor(0, 40); oled.setTextSize(2);
    oled.print(alarmLatched ? "ALARM" : faultDetected ? "FAULT" : "OK");
    oled.display();

    if (!alarmLatched) {
      JsonDocument d;
      d["lpg_ppm"] = (int)lpgPpm;
      d["co_ppm"]  = (int)coPpm;
      d["fault"]   = faultDetected;
      char b[128]; size_t n = serializeJson(d, b, sizeof(b));
      mqtt.publish("home/gas/" DEVICE_ID "/state", (uint8_t *)b, n, true);
    }
  }

  if (alarmLatched && !acknowledged) {
    static uint32_t lastRepeat = 0;
    if (millis() - lastRepeat > 30000) { lastRepeat = millis(); publishAlert("still-active"); }
  }

  // Manual reset requires the gas to have cleared first.
  if (alarmLatched && digitalRead(PIN_RESET) == LOW && lpgPpm < LPG_WARN_PPM && coPpm < CO_WARN_PPM) {
    delay(50);
    if (digitalRead(PIN_RESET) == LOW) {
      alarmLatched = false; acknowledged = false;
      for (int p : { PIN_R_IGNITION, PIN_R_FAN, PIN_R_SIREN }) digitalWrite(p, HIGH);
      Serial.println("Alarm reset");
    }
  }
}
emergencyShutdown orderingValve, then ignition cut, then siren, then fan — and the network last. Every ordering decision here is a safety decision: starting a fan motor before cutting ignition sources would put a potential spark into a gas-filled room.
adc < 20 || adc > 4075 → faultA disconnected or shorted sensor reads at a rail. Treating that as clean air is how a detector fails silently; treating it as a fault is how it fails safe.
ledcWrite(0, 71) for 1.4 V71/255 is 28 % duty, which through the MOSFET and RC filter gives about 1.4 V across the MQ-7 heater. Running the MQ-7 at constant 5 V, as most tutorials do, does not measure CO at all.
Sample in the last 5 s of the low phaseThe surface temperature needs most of the 90 s low phase to settle. Reading at the start of the phase gives a value dominated by the previous cleaning cycle.
rateOfRise()A fast leak reaches dangerous concentrations in minutes. Waiting for an absolute threshold spends most of that window; the derivative catches it while the concentration is still an order of magnitude below the explosive limit.
Reset requires gas to have clearedA latching alarm that can be silenced while gas is still present is worse than no alarm, because it converts a loud problem into a quiet one.
180 s warm-up in setup()MQ heaters need minutes to reach operating temperature. Readings before that are meaningless, and arming the alarm early produces a false trigger on every power-up.

Configuration & Calibration

Configuration steps

  • Run calibrateR0() once, in genuinely clean air, after the sensor has had at least 24 hours of continuous burn-in. A new sensor straight from the bag reads high for a day or two.
  • Set LPG_ALARM_PPM well below the lower explosive limit. 2000 ppm is about 11 % of the LEL — early enough to act, high enough to avoid nuisance alarms from a briefly unlit hob.
  • Set CO thresholds against published guidance: 35 ppm is the 8-hour exposure limit, 100 ppm warrants immediate action.
  • Verify the valve limit switch polarity. The firmware waits for it to confirm closure and gives up after three seconds — if the polarity is inverted it will drive the valve for the full timeout every time.
  • Test the whole chain with a butane lighter (unlit, gas released near the sensor) at least twice a year.

Calibration procedure

An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.

  1. Burn in the sensors

    New MQ sensors need 24–48 hours of continuous power before their readings stabilise. Skipping this makes your R0 measurement wrong and every subsequent ppm figure wrong with it.

  2. Measure R0 in clean air

    With the sensor warmed up and the room ventilated, run the calibration routine. It averages 50 samples and divides by the MQ-2's documented clean-air ratio of 9.83. Record the value; if it changes by more than about 30 % year on year, the sensor is ageing out.

  3. Verify the response

    Release a small amount of butane from an unlit lighter about 30 cm from the MQ-2. The reading should climb sharply within a few seconds and recover over a minute or two. No response means the sensor is dead or the divider is wrong.

  4. Time the valve

    Trigger a manual close and time it. Set the drive timeout in firmware to about 150 % of that, and verify the limit switch reports closure.

Testing Procedure & Expected Output

Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.

TestWhat you should see
Power onA 180-second warm-up countdown, then "armed". No alarm during warm-up.
Read the baseline after warm-upLPG under about 200 ppm and CO under 10 ppm in clean air. Much higher means R0 is wrong.
Watch the MQ-7 phase indicatorAlternating between "clean" for 60 s and "measure" for 90 s, with CO updating at the end of each measure phase.
Release unlit butane near the MQ-2LPG ppm rises within seconds; after three consecutive samples above threshold the full action chain fires.
Observe the action orderValve drives closed first, ignition relay opens, siren sounds, then the fan starts about half a second later. Any other order is a wiring or firmware error.
Try to reset while gas is still presentThe reset is refused until readings fall below the warning thresholds.
Disconnect an MQ sensor"FAULT" on the display and a sensor-fault alert published within ten seconds — not a reading of zero.
Unplug the network and trigger againValve, ignition cut, siren and fan all still operate. Only the MQTT alert is lost.

Bench-test checklist. If a row fails, stop and fix it before moving on.

Expected output

With everything wired and the firmware uploaded, the Serial Monitor at 115200 baud should look similar to the trace below. Values will differ; the shape of the output should not.

An ESP32 development board with the ESP-WROOM-32 module and USB connector
An ESP32 development board running the detection and shutdown logic. Photograph sourced from Wikimedia Commons — ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

Readings are wildly high in clean air

Likely cause. R0 not calibrated for this specific sensor, or the sensor has not been burned in.

Fix. Run 24–48 hours of continuous power, then recalibrate R0 in ventilated clean air. Sensor-to-sensor variation on MQ parts is large enough that a datasheet nominal R0 can be off by a factor of three.

CO readings never change

Likely cause. The MQ-7 heater is running at a constant voltage instead of the alternating cycle.

Fix. Verify the PWM: 255 for 60 s then 71 for 90 s, through a logic-level MOSFET with an RC filter. Measure the heater voltage with a multimeter — you should see it alternate between roughly 5.0 V and 1.4 V.

False alarms when cooking

Likely cause. The MQ-2 responds to alcohol vapour, cooking fumes and smoke, not just LPG.

Fix. Raise the threshold, extend the confirmation from three samples to ten, and mount the sensor away from the hob and the extractor path. Cross-sensitivity is inherent to the sensor type — if it remains a problem, an infrared LPG sensor is selective and does not have this failure mode.

The ESP32 resets when the siren or valve fires

Likely cause. Inrush current and inductive kick on a shared supply.

Fix. Power the 12 V loads from a separate supply with only ground in common, fit flyback diodes across the valve motor and siren, and add a 1000 µF capacitor at the ESP32 input.

The valve does not fully close

Likely cause. Insufficient torque against gas line pressure, or a valve rated for water rather than gas.

Fix. Use a valve explicitly rated for LPG or natural gas with a torque figure that exceeds your line pressure. Water valve seals will not seal gas reliably, and this is not a place to economise.

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

An I²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an I²C scanner sketch first — it should print the device address. Most breakout boards include 4.7 kΩ pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Performance Optimisation

  • Average 16 ADC samples per reading. The ESP32 ADC is noisy and a single sample can swing several percent, which on a power-law curve becomes a large ppm error.
  • Keep the detection loop at 1 Hz. MQ sensors have a response time of seconds; faster sampling adds noise, not information.
  • Store R0 in NVS so a reboot does not require recalibration — and log it, so you can see the sensor ageing.
  • Replace every delay() with a millis() comparison — blocking delays are the single most common cause of dropped readings.
  • Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
  • Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
  • Use uint8_t / uint16_t where the range allows; on an 8-bit AVR a 32-bit add costs four times as much.
  • For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.
  • Profile before optimising — print micros() deltas around each stage and fix the slowest one first.

Safety Precautions

  • This is not a certified safety device. Fit a professionally certified gas alarm as your primary protection and treat this as an additional layer.
  • Never test with a naked flame. Use unlit gas from a lighter, well away from any ignition source, and ventilate afterwards.
  • The extraction fan must have a non-sparking motor and must never start before ignition sources are cut. A brushed motor spinning up in a gas-filled room is an ignition source.
  • Gas valve installation on a real supply line must be done by a licensed gas fitter. In most jurisdictions this is a legal requirement.
  • If you smell gas: do not operate any electrical switch, ventilate, close the cylinder valve by hand, and leave. Trust your nose over any electronics, including this.
  • Mains voltage kills. Anything on the load side of the relay is at 230 V. Do not work on a powered circuit, and never leave exposed mains wiring on a bench where someone could touch it.
  • Keep at least 6 mm of creepage between the mains and low-voltage sides of any board you make, and never route mains tracks under the microcontroller.
  • Have a qualified electrician do the final installation into a consumer unit or wall fitting. In most jurisdictions this is a legal requirement, not a suggestion.
  • Fit an RCD/RCBO upstream and fuse the load appropriately for its rating.
  • MQ-series sensors run a hot element. They get genuinely hot, need ventilation, and must never be enclosed in a sealed plastic box.
  • Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
  • Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
  • Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.

Maintenance

  • Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
  • Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
  • Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.

Future Improvements & Upgrades

A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.

  • Replace the MQ-2 with an infrared LPG sensor, which is selective, does not drift, and has no cross-sensitivity to cooking fumes.
  • Add an electrochemical CO sensor for genuinely accurate carbon monoxide measurement — MQ-7 is a threshold detector, not an instrument.
  • Add a battery backup so the detector works through a power cut, which is exactly when a gas appliance is most likely to be relit incorrectly.
  • Add flame detection with an IR sensor for a second independent confirmation of a fire.
  • Add a mesh of detectors so a leak in one room triggers valve closure at the source regardless of where the sensor is.
  • Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
  • Add connectivity — an ESP32 and an MQTT publish turn a local gadget into something you can graph, alert on and analyse over months.
  • Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
  • Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
  • Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
  • Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.

Frequently Asked Questions

How accurate are the ppm figures really?

Not very, and you should not rely on them as measurements. MQ sensors give perhaps ±50 % after careful calibration, with cross-sensitivity to alcohols and cooking fumes and drift over time. What they are good at is detecting a rapid change from a known baseline, which is all a threshold alarm actually needs. If you need real numbers, an infrared or electrochemical sensor is the right tool.

Where should the sensor be mounted?

It depends entirely on the gas. LPG (propane and butane) is heavier than air and pools at floor level, so mount 30 cm from the floor. Natural gas (methane) is lighter and rises, so mount 30 cm from the ceiling. CO has nearly the same density as air and mixes, so mount at head height. Getting this wrong means the detector never sees the gas.

Why does it need three minutes to warm up?

The sensing element must reach about 300 °C for the surface chemistry to work. Until it does, the resistance is dominated by temperature rather than gas concentration. Arming the alarm during warm-up produces a false trigger on every power-up, which trains people to ignore it.

Can I skip the R0 calibration?

You can, and your ppm figures will be meaningless. Manufacturing variation between MQ sensors is large — two units from the same batch can have clean-air resistances differing by a factor of two. R0 calibration is the step that makes the ratio curve apply to your sensor.

Is automatic valve closure a good idea, or is it a new hazard?

It is a real trade-off and worth thinking about. A false trigger that closes the gas is an inconvenience; a real leak that is not stopped is a catastrophe. The asymmetry strongly favours closing. But the valve must fail closed rather than fail open, it must be reopenable by hand, and its installation must be done properly on a real supply line.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. MQ-2 semiconductor sensor for combustible gas — datasheet and curvesHanwei Electronics
  2. MQ-7 carbon monoxide sensor — datasheet and heater cycle specificationHanwei Electronics
  3. Flammability limits of LPG and natural gasEngineering ToolBox
  4. Carbon monoxide exposure limits and health effectsUS CDC / NIOSH
  5. EN 50194 — electrical apparatus for the detection of combustible gases in domestic premisesCEN
  6. Metal oxide semiconductor gas sensors — operating principleFigaro Engineering