Most of us have exactly zero awareness of what we're breathing indoors. A slow LPG leak from a kitchen regulator, a spike of carbon monoxide from a faulty burner, or just stale, VOC-heavy air from paint, cleaning sprays and poor ventilation — none of it is visible, and none of it announces itself until it's a problem.
I wanted a single small device that does two jobs at once:
- Acts as a gas-leak alarm — detecting combustible and toxic gases (LPG/propane, methane/natural gas, carbon monoxide) and screaming for attention the moment levels cross a danger threshold.
- Acts as an air-quality station — continuously reporting an Air Quality Index, equivalent CO₂ and total VOCs, so I can see when a room needs ventilating.
And crucially — it had to reach me even when I'm not looking at it. A buzzer is useless if you're in another room or out of the house. So, the whole thing talks to T***gram over WiFi: it sends a push notification to my phone when it detects a leak, and an "all clear" when things return to normal.
The brain is a DFRobot Beetle ESP32-C6 — a coin-sized board with built-in WiFi, which is exactly what makes the phone alerts possible. Every sensor is from DFRobot's plug-and-play Gravity ecosystem, so the entire build is solder-free. And to turn a bare stack of PCBs into something you'd actually want on a shelf, it lives inside a charming 3D-printed house-shaped enclosure by JUSTWAY, whose honeycomb vent pattern doubles as the perfect airflow path to the sensors.
Here's how to build your own.
What It Does (at a glance)- Continuously measures CO, CH₄ (methane), LPG/propane and more via a MEMS gas sensor.
- Continuously measures AQI (1–5), eCO₂ (ppm), TVOC (ppb) via a dedicated air-quality sensor.
- Shows a live dashboard on a 128×64 OLED, with a big SAFE / WARNING /! ALARM! banner.
- On a leak, the screen inverts and flashes, the onboard LED blinks, and a T***gram alert is pushed to your phone with the exact readings.
- Sends an "all clear" message when the air is safe again.
- Runs happily standalone — if WiFi drops, the local alarm still works; it just skips the phone notification.
Hardware
- 1× DFRobot Beetle ESP32-C6 — The MCU. WiFi + Bluetooth, tiny footprint, USB-C.
- 1× DFRobot Gravity MEMS Gas Sensor (MiCS-4514) — Detects CO, CH₄, LPG, C₂H₅OH, H₂, NH₃, NO₂. I²C.
- 1× DFRobot Gravity ENS160 Air Quality Sensor — AQI, eCO₂, TVOC. I²C.
- 1× DFRobot Gravity OLED-2864 Display (128×64, SSD1306) — The dashboard. I²C.
- 1× Gravity I²C Hub / splitter (or short chaining cables) — To put all three I²C devices on one bus.
- Gravity 4-pin cables — Usually included with each sensor.
- 1× USB-C cable + 5V supply / power bank — Power and programming.
- 1× 3D-printed enclosure by JUSTWAY — The "little house" shown in this build.
Software & Tools
- Arduino IDE (or arduino-cli)
- ESP32 board support (Espressif "esp32" core)
- DFRobot libraries: DFRobot_MICS, DFRobot_ENS160
- Adafruit libraries: Adafruit_SSD1306, Adafruit_GFX
- A T***gram account + the @BotFather bot
- A 3D printer (or a printing service) for the enclosure
Understanding the split between the two gas sensors is the single most important concept in this build — they are not redundant, they're complementary.
The MiCS-4514 (gas-leak sensor) is a MEMS metal-oxide sensor with two heated sensing elements — an oxidising channel and a reducing channel. By measuring how each element's resistance changes, its library estimates concentrations (in ppm) of specific gases: carbon monoxide, methane, LPG/propane, hydrogen, ethanol, ammonia and nitrogen dioxide. This is your safety / leak sensor. It answers: "Is there a dangerous amount of a specific gas right now?"
The ENS160 (air-quality sensor) is a modern digital multi-gas sensor that runs its own algorithms to output a single Air Quality Index (1–5), an equivalent CO₂ value and total VOC concentration. This is your comfort / wellness sensor. It answers: "Is this air pleasant and healthy to breathe over time?"
Put together, you get both the smoke-alarm-style "get out now" signal and the "open a window" nudge, from one little house.
One thing to internalize: the MiCS-4514 needs a ~3-minute warm-up in clean air every time it powers on. During that window it measures and stores its clean-air baseline — every later reading is relative to it. So always power the device up somewhere with fresh air and let the countdown finish before you test it with gas.
Step 1 — Print the Enclosure (JUSTWAY design)The enclosure for this build is a 3D-printed "little house" designed by JUSTWAY, and it's honestly what elevates the project from a breadboard experiment into a finished object.
A few things make this print perfect for a gas/air monitor — it's worth appreciating the design intent:
- Honeycomb vent pattern. Both angled roof faces are peppered with a field of hexagonal holes. These aren't just decorative — they're the airflow path. Ambient air (and any leaking gas) passes freely through the honeycomb to reach the MiCS-4514 and ENS160 sensing surfaces inside. A sealed box would blind the sensors; this vented roof keeps them "breathing."
- A gable window. One end of the house has a small rectangular window cut into the gable. Line the OLED up behind it and your live readings glow out of it like light from a tiny cottage.
- A round porthole + slot. The front face has a round opening (great for letting the status LED shine through, or poking a sensor face out) and a slim slot that works nicely as the USB-C access point for power.
- Internal mounting rings. Inside, the print includes circular mounting features on a shelf wall — handy for seating round modules and keeping the stack from rattling around.
- Open bottom. The house simply lifts off like a cloche, so you can drop it over the electronics and lift it back off any time you need to re-flash or rewire.
Print settings (suggested)
- Material: PLA (PETG if it'll sit in a warm spot)
- Layer height: 0.2 mm
- Walls / perimeters: 3
- Infill: 15–20%
- Supports: usually none — the honeycomb bridges fine; add minimal supports only if your printer struggles with the window overhang
- Orientation: print the house upright (open side down) for the cleanest vent holes
The two-tone cream-and-white look in the photos comes from a simple filament change partway up the print (a "colour swap" at a set layer height). Totally optional, but it looks fantastic.
Tip: Do not cover the honeycomb vents with tape, stickers or paint fill. Keeping them fully open is what lets the sensors detect a leak quickly.Step 2 — Wire It Up (solder-free)
Every board here speaks I²C, and I²C is a bus — all devices share the same four wires (SDA, SCL, VCC/3V3, GND) and are distinguished by unique addresses. That means you can hang all three modules off the Beetle's single I²C bus using a Gravity I²C hub (or by chaining the Gravity connectors with splitter cables).
Connect everything in parallel to the Beetle ESP32-C6's I²C pins:
- SDA → Beetle SDA (default Wire) → SDA on OLED + ENS160 + MiCS
- SCL → Beetle SCL (default Wire) → SCL on OLED + ENS160 + MiCS
- 3V3 → Beetle 3V3 → VCC on all modules
- GND → Beetle GND → GND on all modules
Each device sits at its own address, so there are no conflicts:
- OLED-2864 (SSD1306): 0x3C
- ENS160 Air Quality: 0x53
- MiCS-4514 Gas: 0x75
Sanity check before you go further: upload a simple I²C scanner sketch and confirm the serial monitor lists exactly 0x3C, 0x53, and 0x75. If one is missing, it's almost always a loose Gravity cable — reseat it. Getting all three to show up here saves a lot of head-scratching later.
A note on power: the MiCS-4514 has an internal heater that draws a noticeable amount of current. Power the Beetle from a decent USB-C supply or a power bank, and use a proper data-quality USB cable. A thin charge-only cable can cause brown-outs that make the board randomly disconnect.
Step 3 — Set Up the Software Environment- Install the Arduino IDE.
- Add ESP32 board support (Espressif's "esp32" core) via the Boards Manager.
- Select the board "DFRobot Beetle ESP32-C6" and the correct COM port.
- Install these libraries from the Library Manager (or DFRobot's GitHub): DFRobot_MICS, DFRobot_ENS160, Adafruit SSD1306, Adafruit GFX
Two small, important settings that will save you hours:
- Enable "USB CDC On Boot". On the Beetle ESP32-C6, the Serial Monitor is silent over USB unless this is turned on in the board menu (Tools → USB CDC On Boot → Enabled). Do this now if you want to watch the readings scroll by while debugging.
- The DFRobot_MICS library has a tiny quirk that stops it compiling on the ESP32-C6 out of the box (it references an analog-pin name the C6 core doesn't define). The fix is a one-line guard added at the top of the library's header — if you hit a build error mentioning A0, that's the culprit.
The complete firmware is provided in the Code section as GasAirMonitor.ino. You don't need to understand every line — but there is one block at the top you must personalize before uploading. It holds your WiFi and T***gram details:
#define WIFI_SSID "YOUR_WIFI_NAME"
#define WIFI_PASS "YOUR_WIFI_PASSWORD"
#define TG_TOKEN "YOUR_BOT_TOKEN" // from @BotFather
#define TG_CHAT_ID "YOUR_CHAT_ID" // your numeric T***gram IDYou can also tune the alarm sensitivity here if you like — the thresholds for CO and methane, and the AQI level that counts as "poor", are all plain #defines near the top. The defaults are sensible for a home:
- Carbon monoxide (CO) — Warning: 35 ppm, Alarm: 100 ppm
- Methane (CH₄) — Warning: 1000 ppm, Alarm: 5000 ppm (~10% LEL)
- LPG / Propane — Alarm: Detected
- Air Quality Index — Warning: AQI ≥ 4
Fill in your WiFi + T***gram values (the next step gets you the T***gram ones), then hit Upload. That's it — the code side is deliberately hands-off.
/*
* GasAirMonitor — Beetle ESP32-C6
* Gas-leak monitor (DFRobot MICS / MiCS-4514, I2C 0x75)
* Air-quality monitor (DFRobot ENS160, I2C 0x53)
* Output on Gravity I2C OLED 128x64 (SSD1306, 0x3C)
*
* Build/upload:
* arduino-cli compile --fqbn esp32:esp32:dfrobot_beetle_esp32c6:CDCOnBoot=cdc -u -p COM18 GasAirMonitor
*
* NOTE ON WARM-UP:
* The MiCS-4514 must warm up ~3 min IN CLEAN AIR. At the end of warm-up the
* library latches the baseline resistance (R0); every gas reading is relative
* to it. So power it on somewhere with fresh air, let the countdown finish,
* THEN test with gas. The ENS160 also self-conditions (Warm-Up / Start-Up).
*/
#include <Wire.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "DFRobot_MICS.h" // patched header defines A0 fallback for ESP32-C6
#include "DFRobot_ENS160.h"
// ---------------- I2C addresses (confirmed by scan) ----------------
#define OLED_ADDR 0x3C
#define ENS160_ADDR 0x53
#define MICS_ADDR MICS_ADDRESS_0 // 0x75
// ---------------- Warm-up ----------------
#define WARMUP_MIN 3 // MiCS-4514 warm-up minutes (clean air!)
// ---------------- Alarm thresholds (tune to taste) ----------------
// Carbon monoxide (ppm): OSHA ceiling ~50, dangerous >100
#define CO_WARN 35.0f
#define CO_ALARM 100.0f
// Methane / natural gas (ppm): LEL is 50000ppm. Library floor is 1000ppm.
#define CH4_WARN 1000.0f // any detectable reading = early warning
#define CH4_ALARM 5000.0f // ~10% LEL
// Air quality index from ENS160 (1=Excellent ... 5=Unhealthy)
#define AQI_WARN 4
#define AQI_ALARM 5
// Onboard LED (Beetle ESP32-C6). Fallback to GPIO15 if core doesn't define it.
#ifndef LED_BUILTIN
#define LED_BUILTIN 15
#endif
// ---------------- WiFi + te**gram ----------------
#define WIFI_SSID ""
#define WIFI_PASS "Air@"
#define TG_TOKEN ":"
#define TG_CHAT_ID "" // private chat (Murdock)
#define ALERT_COOLDOWN_MS 60000UL // min gap between repeat alerts while leak persists
bool wifiOK = false;
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
DFRobot_MICS_I2C mics(&Wire, MICS_ADDR);
DFRobot_ENS160_I2C ens160(&Wire, ENS160_ADDR);
bool micsOK = false, ensOK = false;
enum Level { LVL_OK = 0, LVL_WARN = 1, LVL_ALARM = 2 };
// -------- readings --------
struct Reading {
float co, ch4, c2h5oh, nh3, h2, no2; // MICS gases (ppm)
bool lpg; // MICS propane present
uint8_t aqi; // ENS160 1-5
uint16_t tvoc, eco2; // ENS160 ppb / ppm
uint8_t ensStatus; // ENS160 operating status
} r;
// ---------------- helpers ----------------
void oledMsg(const char* l1, const char* l2 = "", const char* l3 = "") {
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
oled.setTextSize(1);
oled.setCursor(0, 0); oled.println(l1);
oled.setCursor(0, 12); oled.println(l2);
oled.setCursor(0, 24); oled.println(l3);
oled.display();
}
const char* aqiWord(uint8_t a) {
switch (a) {
case 1: return "Excellent";
case 2: return "Good";
case 3: return "Moderate";
case 4: return "Poor";
case 5: return "Unhealthy";
default: return "--";
}
}
// ---------------- WiFi + te**gram ----------------
String urlEncode(const String &s) {
String out;
char buf[4];
for (size_t i = 0; i < s.length(); i++) {
char c = s[i];
if (isalnum((unsigned char)c) || c == '-' || c == '_' || c == '.' || c == '~') {
out += c;
} else {
sprintf(buf, "%%%02X", (uint8_t)c);
out += buf;
}
}
return out;
}
void connectWiFi() {
Serial.printf("WiFi: connecting to %s ...\n", WIFI_SSID);
oledMsg("Connecting WiFi...", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
uint32_t start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) {
digitalWrite(LED_BUILTIN, (millis() / 250) % 2);
delay(250);
}
digitalWrite(LED_BUILTIN, LOW);
wifiOK = (WiFi.status() == WL_CONNECTED);
if (wifiOK) {
Serial.printf("WiFi OK, IP %s\n", WiFi.localIP().toString().c_str());
oledMsg("WiFi connected", WiFi.localIP().toString().c_str());
} else {
Serial.println("WiFi FAILED (monitor runs offline, no te**gram)");
oledMsg("WiFi FAILED", "Runs offline");
}
delay(800);
}
bool sendte**gram(const String &text) {
if (!wifiOK || WiFi.status() != WL_CONNECTED) return false;
WiFiClientSecure client;
client.setInsecure(); // skip cert validation (fine for this use)
HTTPClient https;
String url = "https://api.te**gram.org/bot" TG_TOKEN "/sendMessage";
if (!https.begin(client, url)) return false;
https.addHeader("Content-Type", "application/x-www-form-urlencoded");
String body = "chat_id=" TG_CHAT_ID "&text=" + urlEncode(text);
int code = https.POST(body);
https.end();
Serial.printf("te**gram POST -> HTTP %d\n", code);
return code == 200;
}
void setup() {
Serial.begin(115200);
delay(300);
Serial.println("\nGasAirMonitor starting");
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
Wire.begin();
// --- OLED ---
if (!oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println("OLED not found @0x3C");
while (true) { Serial.println("no OLED"); delay(1000); }
}
oled.setRotation(0);
oledMsg("Gas + Air Monitor", "Booting sensors...");
delay(600);
// --- ENS160 ---
if (ens160.begin() == NO_ERR) {
ensOK = true;
ens160.setPWRMode(ENS160_STANDARD_MODE);
ens160.setTempAndHum(25.0f, 50.0f); // nominal compensation (no T/RH sensor yet)
Serial.println("ENS160 OK @0x53");
} else {
Serial.println("ENS160 init FAILED @0x53");
}
// --- MICS ---
if (mics.begin()) {
micsOK = true;
if (mics.getPowerState() == SLEEP_MODE) mics.wakeUpMode();
Serial.println("MICS OK @0x75");
} else {
Serial.println("MICS init FAILED @0x75");
}
// --- WiFi + te**gram online ping ---
connectWiFi();
if (sendte**gram("\xF0\x9F\x9F\xA2 Gas+Air monitor online. Warming up (3 min)...")) {
Serial.println("te**gram online ping sent");
}
// --- MICS warm-up (clean air) ---
if (micsOK) {
while (!mics.warmUpTime(WARMUP_MIN)) {
uint32_t leftMs = (uint32_t)WARMUP_MIN * 60000UL - millis();
int s = (int)(leftMs / 1000);
if (s < 0) s = 0;
char line2[24];
snprintf(line2, sizeof(line2), "MICS warmup %02d:%02d", s / 60, s % 60);
oledMsg("Warming up (clean air)", line2, ensOK ? "ENS160 ready" : "ENS160 FAIL");
// heartbeat LED during warm-up
digitalWrite(LED_BUILTIN, (millis() / 500) % 2);
Serial.printf("warmup %ds left\n", s);
delay(400);
}
digitalWrite(LED_BUILTIN, LOW);
Serial.println("MICS warm-up complete, R0 latched");
}
oledMsg("Ready", "Monitoring...");
delay(600);
}
Level readAndScore(String &reason) {
Level level = LVL_OK;
reason = "AIR OK";
// ---- MICS gases ----
if (micsOK) {
r.co = mics.getGasData(CO);
r.ch4 = mics.getGasData(CH4);
r.c2h5oh = mics.getGasData(C2H5OH);
r.nh3 = mics.getGasData(NH3);
r.h2 = mics.getGasData(H2);
r.no2 = mics.getGasData(NO2);
r.lpg = (mics.getGasExist(C3H8) == EXIST);
if (r.co >= CO_WARN) { level = max(level, LVL_WARN); reason = "CO high"; }
if (r.co >= CO_ALARM) { level = LVL_ALARM; reason = "CO DANGER"; }
if (r.ch4 >= CH4_WARN) { level = max(level, LVL_WARN); reason = "GAS leak?"; }
if (r.ch4 >= CH4_ALARM){ level = LVL_ALARM; reason = "GAS LEAK!"; }
if (r.lpg) { level = max(level, LVL_ALARM); reason = "LPG LEAK!"; }
}
// ---- ENS160 air quality ----
if (ensOK) {
r.ensStatus = ens160.getENS160Status();
r.aqi = ens160.getAQI();
r.tvoc = ens160.getTVOC();
r.eco2 = ens160.getECO2();
if (r.aqi >= AQI_WARN && level < LVL_WARN) { level = LVL_WARN; reason = "Air poor"; }
if (r.aqi >= AQI_ALARM && level < LVL_ALARM) { level = LVL_WARN; reason = "Air unhealthy"; }
}
return level;
}
void drawScreen(Level level, const String &reason) {
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
oled.setTextSize(1);
// Air quality block
oled.setCursor(0, 0);
if (ensOK) {
oled.printf("AQI %d %s", r.aqi, aqiWord(r.aqi));
oled.setCursor(0, 10);
oled.printf("eCO2 %u TVOC %u", r.eco2, r.tvoc);
} else {
oled.print("ENS160: FAIL");
oled.setCursor(0, 10);
}
// Gas block
oled.setCursor(0, 24);
if (micsOK) {
oled.printf("CO %.0f CH4 %.0f", r.co, r.ch4);
oled.setCursor(0, 34);
oled.printf("LPG %s", r.lpg ? "YES" : "no");
if (r.c2h5oh > 0 || r.nh3 > 0) {
oled.printf(" EtOH%.0f NH3%.0f", r.c2h5oh, r.nh3);
}
} else {
oled.print("MICS: FAIL");
}
// Status banner (bottom, inverted box)
oled.fillRect(0, 48, 128, 16, SSD1306_WHITE);
oled.setTextColor(SSD1306_BLACK);
oled.setTextSize(2);
const char* banner = (level == LVL_ALARM) ? "! ALARM !"
: (level == LVL_WARN) ? "WARNING"
: "SAFE";
int16_t x1, y1; uint16_t w, h;
oled.getTextBounds(banner, 0, 0, &x1, &y1, &w, &h);
oled.setCursor((128 - w) / 2, 49);
oled.print(banner);
oled.setTextColor(SSD1306_WHITE);
oled.display();
}
void loop() {
static uint32_t last = 0;
static bool blink = false;
String reason;
Level level = readAndScore(reason);
// Alarm signalling: fast blink + inverted screen on ALARM, slow on WARN
if (level == LVL_ALARM) {
blink = !blink;
digitalWrite(LED_BUILTIN, blink);
oled.invertDisplay(blink);
} else if (level == LVL_WARN) {
digitalWrite(LED_BUILTIN, (millis() / 500) % 2);
oled.invertDisplay(false);
} else {
digitalWrite(LED_BUILTIN, LOW);
oled.invertDisplay(false);
}
drawScreen(level, reason);
Serial.printf("[%s] CO=%.1f CH4=%.0f LPG=%d | AQI=%d eCO2=%u TVOC=%u (%s)\n",
level == LVL_ALARM ? "ALARM" : level == LVL_WARN ? "WARN" : "OK",
r.co, r.ch4, r.lpg, r.aqi, r.eco2, r.tvoc, reason.c_str());
// ---- te**gram alerting (edge-triggered on ALARM, with cooldown) ----
static bool inAlarm = false; // are we currently in an alerted leak?
static uint32_t lastAlertMs = 0;
if (level == LVL_ALARM) {
if (!inAlarm || (millis() - lastAlertMs) > ALERT_COOLDOWN_MS) {
char msg[220];
snprintf(msg, sizeof(msg),
"\xF0\x9F\x9A\xA8 GAS ALERT: %s\nCO %.0f ppm | CH4 %.0f ppm | LPG %s\nAir: AQI %d, eCO2 %u ppm, TVOC %u ppb",
reason.c_str(), r.co, r.ch4, r.lpg ? "YES" : "no", r.aqi, r.eco2, r.tvoc);
sendte**gram(msg);
lastAlertMs = millis();
inAlarm = true;
}
} else if (inAlarm && level == LVL_OK) {
sendte**gram("\xE2\x9C\x85 All clear \xE2\x80\x94 gas levels back to safe.");
inAlarm = false;
}
delay(1000);
}Step 5 — Create Your T***gram Bot & Get the Alerts FlowingThis is the part that makes the project feel magical — a real push notification on your phone. It takes about three minutes.
- Create a bot. In T***gram, search for @BotFather, start a chat, and send /newbot. Follow the prompts (give it a name and a username). BotFather replies with a bot token that looks like 123456789:AAE.... Copy it into TG_TOKEN.
- Say hi to your bot. Open the new bot and send it any message (e.g. "hi"). A bot cannot message you first until you've messaged it — this step is what "opens the door."
- Find your chat ID. The easiest way: message @userinfobot, which instantly replies with your numeric ID. Copy that number into TG_CHAT_ID. (Alternatively, you can read it from your bot's getUpdates API endpoint.)
- Re-upload the sketch with all four values filled in.
Now, the moment the device boots and connects to WiFi, you'll get a friendly "Gas + Air monitor online" ping. That confirmation message is your proof that the whole WiFi → T***gram pipeline works — before you've even tested a single gas.
Want the alert in a group chat instead of a private message? Add the bot to the group and use the group's chat ID (it'll be a negative number). Everyone in the group then gets the leak alerts.
Step 6 — First Power-Up & Warm-UpPlace the device somewhere with fresh, clean air and power it via USB-C. Here's what you'll see:
- A brief boot screen, then "Connecting WiFi…", then your local IP address once connected.
- Your phone buzzes with the "monitor online" T***gram message.
- A 3-minute warm-up countdown appears on the OLED (MICS warmup 02:59…). This is the MiCS-4514 establishing its clean-air baseline — let it finish. The onboard LED gently pulses during this phase.
- When the countdown hits zero, the display switches to the live dashboard.
The dashboard shows, at a glance:
- Top: AQI (with a word like Good / Moderate), plus eCO2 and TVOC.
- Middle: CO and CH4 in ppm, and whether LPG is detected.
- Bottom: a bold status banner — SAFE, WARNING, or ! ALARM !.
In clean indoor air you should see the gas values sitting at 0, eCO₂ near ~400–500 ppm (the fresh-air baseline), a low TVOC, and a nice green-light SAFE banner.
Time to make it shout. The safest household way to trigger the combustible-gas channels is with an unlit lighter:
- With the device warmed up and showing SAFE, hold an unlit lighter close to the honeycomb vents and press the gas button for a couple of seconds to release a small puff of butane/LPG (do not ignite it, and do this in a ventilated area away from flames).
- Within a second or two, the LPG field flips to YES, the banner jumps to ! ALARM !, the OLED inverts and flashes, and the LED blinks rapidly.
- Your phone buzzes: "GAS ALERT: LPG LEAK!" with the live readings attached.
- Move the lighter away and let fresh air reach the vents. Once levels fall back, the banner returns to SAFE and you get an "All clear" message.
To avoid spam, repeat alerts are rate-limited (about one message per minute) while a leak persists — you get told immediately, then reminded, without your phone melting.
If it doesn't trip as easily as you'd like during testing, the gas thresholds at the top of the sketch are yours to lower. Just re-flash and try again.
Step 8 — Final Assembly- Arrange the OLED behind the gable window so the readings are visible from outside.
- Position the MiCS and ENS160 sensors under the honeycomb roof so their sensing faces get maximum airflow. Keep the vents unobstructed.
- Route the USB-C cable out through the front slot/porthole for power.
- Tidy the Gravity cables so nothing blocks a vent, then lower the JUSTWAY house over the whole stack.
Done — you now have an adorable little cottage quietly standing guard over your air.
Wrapping UpFor a solder-free afternoon build, this punches well above its weight: it's a genuine dual-purpose safety alarm and air-quality station, it reaches you anywhere via T***gram, and thanks to the JUSTWAY enclosure it looks like something you'd happily leave on the kitchen counter. The Beetle ESP32-C6's built-in WiFi is what ties it all together — turning a stack of sensors into a connected guardian for the air you breathe. If you build one, I'd love to see your color combos on the JUSTWAY house. Stay safe and breathe easy!











Comments