Community project
Gas And Proximity
This project combines gas detection and proximity sensing into a single monitoring system built around an ESP32 microcontroller. The MQ-2 gas sensor detects flammable gases and smoke, while the HC-SR04 ultrasonic sensor measures distance to nearby objects. When either sensor triggers its threshold, the system sounds an audible alarm via a buzzer and displays real-time readings and alerts on a small OLED screen. A push button allows users to silence the alarm for 30 seconds without disabling the sensors.
This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for connecting all components to the ESP32. The included firmware handles sensor calibration, dual-alarm logic, display updates, and button debouncing. Builders will learn how to integrate analog and digital sensors, drive an I2C display, and manage multiple sensor inputs with priority-based alerting.
Wiring diagram

Gather all the parts
Assemble it in 6 steps
1. Place the ESP32 and make power rails
Put the ESP32 DevKit v1 on the breadboard so its two rows of pins are on separate sides. Use one breadboard rail for 5 V and another for GND. Connect the board's VIN/5V pin to the 5 V rail and a GND pin to the GND rail; later, power the board through its USB connector so these rails carry its 5 V and ground.
- Keep every ground connection on the same GND rail; the sensors cannot communicate correctly without this shared return path.
- Do not connect the 5 V rail to the ESP32 3V3 pin — applying 5 V there can damage the board.
2. Wire the gas sensor and its protection resistors
Connect mq2_1 VCC to the 5 V rail (power) and mq2_1 GND to the GND rail (ground). Connect mq2_1 AO to one lead of gas_divider_top_1, the 1.8 kΩ resistor (signal). Join the other lead of that 1.8 kΩ resistor, one lead of gas_divider_bottom_1, the 3.3 kΩ resistor, and a jumper to ESP32 GPIO32 in one breadboard row (protected gas signal). Connect the remaining lead of the 3.3 kΩ resistor to GND (ground). Leave mq2_1 DO unconnected.
- The three joined points form one row: the free end of the 1.8 kΩ resistor, the free end of the 3.3 kΩ resistor, and the jumper to GPIO32.
- The MQ-2 needs time to warm up; the screen shows a 30-second countdown before its gas alarm is used.
- Do not run the MQ-2 AO pin straight to GPIO32 — its 5 V output can damage the ESP32 input. The two resistors lower that signal to a safe voltage.
3. Wire the distance sensor and its protection resistors
Connect hcsr04_1 VCC to the 5 V rail (power) and hcsr04_1 GND to the GND rail (ground). Connect hcsr04_1 TRIG to GPIO27 (signal). Connect hcsr04_1 ECHO to one lead of echo_divider_top_1, the 1.8 kΩ resistor (signal). Join the other lead of that resistor, one lead of echo_divider_bottom_1, the 3.3 kΩ resistor, and a jumper to GPIO26 in one breadboard row (protected echo signal). Connect the remaining lead of the 3.3 kΩ resistor to GND (ground).
- Point the two round sensor openings toward the area you want to watch, with no object closer than about 2 cm.
- The resistor junction is the same style as the MQ-2 connection: three leads meet in one breadboard row.
- Do not connect HC-SR04 ECHO directly to GPIO26 — ECHO is a 5 V signal and can damage the 3.3 V ESP32 input.
4. Wire the OLED screen
Connect oled_1 VCC to ESP32 3V3 (power), oled_1 GND to GND (ground), oled_1 SDA to GPIO21 (data), and oled_1 SCL to GPIO22 (clock).
- Read the labels printed on the back of the display; some boards label the power pin as VDD instead of VCC.
- Keep these four wires short if the display shows random pixels.
- Make sure VCC and GND are not swapped — swapped power can damage the screen. Do not connect this 3.3 V OLED to the 5 V rail.
5. Wire the buzzer and mute button
Connect buzzer_1 SIGNAL to GPIO25 (alarm signal) and buzzer_1 GND to GND (ground). Connect one leg of mute_button_1 to GPIO33 (button signal) and the leg directly across from it to GND (ground).
- A four-leg tactile button has the two legs on each same side already joined inside; use legs on opposite sides, not two legs beside each other.
- The code uses the ESP32's built-in pull-up, so no separate button resistor is needed.
- Use a 3.3 V active buzzer. A larger buzzer or siren can draw too much current from GPIO25 and needs a transistor driver.
6. Inspect before powering up
With USB unplugged, check that the two 1.8 kΩ and two 3.3 kΩ resistors are in the intended rows and that no 5 V sensor output reaches an ESP32 GPIO without passing through its resistor pair. Then plug the ESP32 into USB.
- After deployment, the OLED should show the startup bar, then the dashboard. A quick button press mutes an active alarm for 30 seconds.
- This is a learning monitor, not a certified gas detector. Do not rely on it as the only protection against flammable or poisonous gases.
Review all connections
1. Connections between "mq2_1" and "ESP32"
2. Connections between "gas_divider_bottom_1" and "ESP32"
3. Connections between "hcsr04_1" and "ESP32"
4. Connections between "echo_divider_top_1" and "ESP32"
5. Connections between "echo_divider_bottom_1" and "ESP32"
6. Connections between "oled_1" and "ESP32"
7. Connections between "buzzer_1" and "ESP32"
8. Connections between "mute_button_1" and "ESP32"
9. Connections between "gas_divider_top_1" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
// Forward declarations
float readDistanceCm();
void updateGraph();
void drawHeader(const char *title);
void drawStartup();
void drawSafeDashboard();
void drawGraph(int x, int y, int width, const int *values, int maximum);
void drawGraphScreen();
void drawAlarmScreen();
void drawMutedScreen();
void updateDisplay();
void updateButton(uint32_t now);
constexpr uint8_t GAS_ADC_PIN = 32;
constexpr uint8_t ULTRASONIC_TRIG_PIN = 27;
constexpr uint8_t ULTRASONIC_ECHO_PIN = 26;
constexpr uint8_t BUZZER_PIN = 25;
constexpr uint8_t SILENCE_BUTTON_PIN = 33;
constexpr uint8_t OLED_SDA_PIN = 21;
constexpr uint8_t OLED_SCL_PIN = 22;
constexpr int GAS_ALARM_THRESHOLD = 1800; // Adjust after testing clean-air readings.
constexpr float DISTANCE_ALARM_CM = 50.0f;
constexpr uint32_t SENSOR_INTERVAL_MS = 250;
constexpr uint32_t DISPLAY_INTERVAL_MS = 50;
constexpr uint32_t SCREEN_CHANGE_MS = 5000;
constexpr uint32_t SILENCE_TIME_MS = 30000;
constexpr uint32_t MQ2_WARMUP_MS = 30000;
constexpr uint32_t BUTTON_DEBOUNCE_MS = 40;
constexpr int GRAPH_POINTS = 60;
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
int gasReading = 0;
float distanceCm = -1.0f;
bool gasAlarm = false;
bool proximityAlarm = false;
bool alarmActive = false;
bool lastButtonRaw = HIGH;
bool stableButtonState = HIGH;
uint32_t buttonChangedAt = 0;
uint32_t silenceUntil = 0;
uint32_t startedAt = 0;
uint32_t lastSensorUpdate = 0;
uint32_t lastDisplayUpdate = 0;
uint32_t lastScreenChange = 0;
uint32_t animationFrame = 0;
uint8_t currentScreen = 0;
int gasHistory[GRAPH_POINTS] = {};
int distanceHistory[GRAPH_POINTS] = {};
int graphIndex = 0;
float readDistanceCm() {
digitalWrite(ULTRASONIC_TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(ULTRASONIC_TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(ULTRASONIC_TRIG_PIN, LOW);
const uint32_t duration = pulseIn(ULTRASONIC_ECHO_PIN, HIGH, 30000);
return duration == 0 ? -1.0f : (duration * 0.0343f) / 2.0f;
}
void updateGraph() {
gasHistory[graphIndex] = gasReading;
distanceHistory[graphIndex] = distanceCm < 0 ? 0 : constrain((int)distanceCm, 0, 200);
graphIndex = (graphIndex + 1) % GRAPH_POINTS;
}
void drawHeader(const char *title) {
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(0, 9, title);
u8g2.drawHLine(0, 12, 128);
}
void drawStartup() {
for (int progress = 0; progress <= 116; progress += 4) {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_helvB12_tf);
u8g2.drawStr(8, 20, "SMART SAFETY");
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(35, 32, "SYSTEM");
u8g2.drawFrame(4, 42, 120, 10);
u8g2.drawBox(6, 44, progress, 6);
u8g2.sendBuffer();
delay(15);
}
}
void drawSafeDashboard() {
u8g2.clearBuffer();
drawHeader("SMART SAFETY");
for (int i = 0; i < 8; ++i) u8g2.drawPixel((animationFrame * (i + 1) / 3 + i * 23) % 128, 14 + ((i * 17) % 45));
u8g2.drawStr(0, 23, "GAS");
char text[22];
snprintf(text, sizeof(text), "%d", gasReading);
u8g2.drawStr(27, 23, text);
const int percent = constrain((gasReading * 100) / 4095, 0, 100);
snprintf(text, sizeof(text), "%d%%", percent);
u8g2.drawStr(102, 23, text);
u8g2.drawFrame(27, 27, 101, 6);
u8g2.drawBox(29, 29, constrain(map(gasReading, 0, 4095, 0, 97), 0, 97), 2);
u8g2.drawStr(0, 41, "DIST");
if (distanceCm < 0) u8g2.drawStr(27, 41, "NO DATA");
else { snprintf(text, sizeof(text), "%.1f cm", distanceCm); u8g2.drawStr(27, 41, text); }
u8g2.drawFrame(91, 36, 37, 6);
if (distanceCm > 0) u8g2.drawBox(93, 38, constrain((int)(distanceCm * 33.0f / 200.0f), 0, 33), 2);
u8g2.drawFrame(0, 46, 128, 18);
u8g2.setFont(u8g2_font_helvB10_tf);
if (millis() - startedAt < MQ2_WARMUP_MS) {
const uint32_t left = (MQ2_WARMUP_MS - (millis() - startedAt) + 999) / 1000;
snprintf(text, sizeof(text), "WARM UP %lus", (unsigned long)left);
u8g2.drawStr(27, 59, text);
} else {
u8g2.drawStr(27, 59, "SYSTEM SAFE");
}
u8g2.sendBuffer();
}
void drawGraph(int x, int y, int width, const int *values, int maximum) {
u8g2.drawFrame(x, y, width, 18);
for (int i = 1; i < GRAPH_POINTS; ++i) {
const int a = (graphIndex + i - 1) % GRAPH_POINTS;
const int b = (graphIndex + i) % GRAPH_POINTS;
const int x1 = x + 2 + ((i - 1) * (width - 4)) / (GRAPH_POINTS - 1);
const int x2 = x + 2 + (i * (width - 4)) / (GRAPH_POINTS - 1);
const int y1 = y + 16 - map(constrain(values[a], 0, maximum), 0, maximum, 0, 14);
const int y2 = y + 16 - map(constrain(values[b], 0, maximum), 0, maximum, 0, 14);
u8g2.drawLine(x1, y1, x2, y2);
}
}
void drawGraphScreen() {
u8g2.clearBuffer();
drawHeader("LIVE SENSOR GRAPH");
char text[24];
snprintf(text, sizeof(text), "GAS %d", gasReading);
u8g2.drawStr(0, 22, text);
drawGraph(28, 15, 100, gasHistory, 4095);
if (distanceCm < 0) u8g2.drawStr(0, 43, "DIST ---");
else { snprintf(text, sizeof(text), "DIST %.1fcm", distanceCm); u8g2.drawStr(0, 43, text); }
drawGraph(45, 36, 83, distanceHistory, 200);
if ((animationFrame / 5) % 2 == 0) u8g2.drawDisc(5, 59, 2);
u8g2.drawStr(11, 62, "LIVE");
u8g2.sendBuffer();
}
void drawAlarmScreen() {
u8g2.clearBuffer();
const bool flash = ((animationFrame / 5) % 2) == 0;
if (flash) { u8g2.drawFrame(0, 0, 128, 64); u8g2.drawFrame(2, 2, 124, 60); }
u8g2.setFont(u8g2_font_helvB14_tf);
u8g2.drawStr(31, 20, "ALARM!");
u8g2.setFont(u8g2_font_6x10_tf);
if (gasAlarm && proximityAlarm) u8g2.drawStr(4, 32, "GAS + OBJECT DETECTED");
else if (gasAlarm) u8g2.drawStr(4, 32, "GAS LEVEL HIGH!");
else u8g2.drawStr(4, 32, "OBJECT TOO CLOSE!");
char text[22];
snprintf(text, sizeof(text), "GAS:%d", gasReading); u8g2.drawStr(4, 44, text);
snprintf(text, sizeof(text), distanceCm < 0 ? "DIST:---" : "DIST:%.1f", distanceCm); u8g2.drawStr(72, 44, text);
if (flash) { u8g2.drawBox(4, 51, 120, 11); u8g2.setDrawColor(0); }
else u8g2.drawFrame(4, 51, 120, 11);
u8g2.drawStr(37, 60, "!! DANGER !!");
u8g2.setDrawColor(1);
u8g2.sendBuffer();
}
void drawMutedScreen() {
u8g2.clearBuffer();
drawHeader("SMART SAFETY");
u8g2.setFont(u8g2_font_helvB10_tf);
u8g2.drawStr(24, 30, "ALARM MUTED");
const uint32_t remaining = silenceUntil > millis() ? (silenceUntil - millis() + 999) / 1000 : 0;
char text[20]; snprintf(text, sizeof(text), "%lu seconds", (unsigned long)remaining);
u8g2.setFont(u8g2_font_6x10_tf); u8g2.drawStr(35, 43, text);
u8g2.drawFrame(4, 50, 120, 10);
u8g2.drawBox(6, 52, constrain((int)(remaining * 116UL / 30UL), 0, 116), 6);
u8g2.sendBuffer();
}
void updateDisplay() {
++animationFrame;
if (millis() < silenceUntil) drawMutedScreen();
else if (alarmActive) drawAlarmScreen();
else if (currentScreen == 0) drawSafeDashboard();
else drawGraphScreen();
}
void updateButton(uint32_t now) {
const bool raw = digitalRead(SILENCE_BUTTON_PIN);
if (raw != lastButtonRaw) { lastButtonRaw = raw; buttonChangedAt = now; }
if (now - buttonChangedAt >= BUTTON_DEBOUNCE_MS && raw != stableButtonState) {
stableButtonState = raw;
if (stableButtonState == LOW) {
silenceUntil = now + SILENCE_TIME_MS;
Serial.println("Alarm muted for 30 seconds");
}
}
}
void setup() {
Serial.begin(115200);
pinMode(ULTRASONIC_TRIG_PIN, OUTPUT);
pinMode(ULTRASONIC_ECHO_PIN, INPUT);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
pinMode(SILENCE_BUTTON_PIN, INPUT_PULLUP);
analogReadResolution(12);
Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN);
Wire.setClock(100000);
u8g2.begin();
drawStartup();
startedAt = millis();
lastScreenChange = startedAt;
Serial.println("Smart Safety System ready; MQ-2 warming for 30 seconds.");
}
void loop() {
const uint32_t now = millis();
updateButton(now);
if (now - lastSensorUpdate >= SENSOR_INTERVAL_MS) {
lastSensorUpdate = now;
gasReading = analogRead(GAS_ADC_PIN);
distanceCm = readDistanceCm();
const bool gasReady = now - startedAt >= MQ2_WARMUP_MS;
gasAlarm = gasReady && gasReading >= GAS_ALARM_THRESHOLD;
proximityAlarm = distanceCm > 0 && distanceCm <= DISTANCE_ALARM_CM;
alarmActive = (gasAlarm || proximityAlarm) && now >= silenceUntil;
digitalWrite(BUZZER_PIN, alarmActive ? HIGH : LOW);
updateGraph();
Serial.printf("Gas: %d | Distance: %.1f cm | Status: %s\n", gasReading, distanceCm, alarmActive ? "ALARM" : "SAFE");
}
if (!alarmActive && now >= silenceUntil && now - lastScreenChange >= SCREEN_CHANGE_MS) {
lastScreenChange = now;
currentScreen = (currentScreen + 1) % 2;
}
if (now - lastDisplayUpdate >= DISPLAY_INTERVAL_MS) {
lastDisplayUpdate = now;
updateDisplay();
}
}Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.




