Báo rò khí gas qua Telegram với ESP32 và MQ-2: hai mức cảnh báo, còi, báo lỗi cảm biến, tự kiểm tra
Làm máy báo rò khí gas với ESP32 và MQ-2: hai mức cảnh báo, còi tại chỗ, nhắn Telegram nhắc lại mỗi 2 phút, báo lỗi cảm biến, tự kiểm tra mỗi sáng, cách đặt cảm biến và lưu ý an toàn.
Rò khí gas trong bếp nguy hiểm nhất là lúc không có ai ở nhà, hoặc cả nhà đang ngủ. Bài này làm một máy báo rò gas bằng ESP32 và cảm biến MQ-2: phát hiện khí LPG (gas bình), kêu còi tại chỗ và nhắn Telegram tới điện thoại theo hai mức cảnh báo và nguy hiểm, nhắc lại mỗi 2 phút cho tới khi hết khí, báo khi cảm biến hỏng dây, và mỗi sáng tự nhắn một tin kiểm tra để biết máy vẫn đang chạy.
1. Đọc trước: an toàn với khí gas
- Máy tự làm trong bài là công cụ học tập và cảnh báo bổ sung, không thay thế thiết bị báo gas đã được kiểm định. Cảm biến MQ-2 có sai số lớn, trôi theo thời gian, và cũng phản ứng với cồn, khói.
- Khi ngửi thấy mùi gas hoặc nhận cảnh báo: khóa van bình gas, mở cửa cho thoáng, không bật tắt công tắc điện, quạt, đèn, không dùng lửa, không gọi điện trong phòng có gas; ra khỏi nhà và gọi người có chuyên môn nếu mùi gas nồng.
- Máy chỉ báo, không đóng ngắt thiết bị điện trong bếp: tiếp điểm relay, công tắc có thể đánh lửa trong vùng có khí gas.
2. Máy hoạt động thế nào
- MQ-2 có lớp oxit thiếc được dây nung làm nóng. Gặp khí cháy (LPG, khói, cồn), điện trở
Rscủa lớp này giảm; module đổi điện trở thành điện áp ở chân AO. - ESP32 đọc AO, tính
Rs, chia choR0(điện trở trong không khí sạch, có sau khi hiệu chuẩn), rồi đổi ra ppm LPG theo đường cong của datasheet, giống bài MQ-2 trong loạt mô-đun. - Hai mức: từ 1000 ppm là cảnh báo, từ 2000 ppm là nguy hiểm. Giới hạn cháy nổ dưới của LPG vào khoảng 2 % thể tích (20.000 ppm), nên hai mức này vào khoảng 5 % và 10 % giới hạn đó: báo sớm, còn xa ngưỡng có thể bắt lửa.

3. Chuẩn bị
- ESP32 DevKit V1, dây USB, cục sạc USB 5 V từ 1 A
- Module MQ-2, điện trở 10 kΩ và 20 kΩ cho cầu phân áp
- Còi chủ động 5 V, transistor NPN (S8050), điện trở 1 kΩ
- Breadboard và dây cắm
- Thư viện ArduinoJson 7 (Library Manager)
4. Nối dây

- Module MQ-2 chạy 5 V nên chân AO có thể lên gần 5 V, quá mức chân ESP32 chịu được: luôn dùng cầu phân áp 10 kΩ / 20 kΩ.
- Còi chủ động tự kêu khi có điện; ESP32 bật tắt nó qua transistor vì còi kéo dòng lớn hơn khả năng của một chân GPIO.
- Đèn trên bo (D2) nháy khi báo động, sáng đứng khi lỗi cảm biến.
5. Đặt cảm biến ở đâu

LPG nặng hơn không khí khoảng 1,5–2 lần, khi rò sẽ chìm xuống và lan sát sàn. Đặt cảm biến thấp, cách sàn khoảng 30 cm, cách bếp và bình gas 1–4 m. Tránh đặt ngay trên bếp (hơi nóng, dầu mỡ làm bẩn cảm biến), cạnh bồn rửa, cạnh cửa sổ hay quạt hút.
6. Chương trình
Điền TEN_WIFI, MAT_KHAU_WIFI, BOT_TOKEN, CHAT_ID (cách tạo bot và lấy chat id như bài chuông cửa ESP32-CAM trong cùng nhóm dự án). Sau khi nạp: để máy làm nóng 3 phút ở chỗ không khí sạch, gõ C trên Serial Monitor để hiệu chuẩn R0. Cảm biến mới nên chạy liên tục 1–2 ngày rồi hiệu chuẩn lại.
// PYWORLD.VN - Báo rò khí gas qua Telegram: MQ-2 đo khí LPG, ESP32 kêu còi, nháy đèn và nhắn tin tới điện thoại theo hai mức
// PYWORLD.VN - MQ-2 cấp 5 V, AO qua cầu phân áp 10 kΩ / 20 kΩ vào D34; còi chủ động qua transistor ở D25; đèn D2; nút BOOT tắt còi 5 phút
// PYWORLD.VN - Gõ C trên Serial Monitor để hiệu chuẩn R0 ở không khí sạch; mỗi sáng 8 giờ nhắn tin tự kiểm tra; lệnh Telegram /gas
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Preferences.h>
#include <time.h>
const char *TEN_WIFI = "TEN_WIFI";
const char *MAT_KHAU_WIFI = "MAT_KHAU_WIFI";
const char *BOT_TOKEN = "123456789:MA_TOKEN_CUA_BOT";
const long long CHAT_ID = 0; // lấy như bài chuông cửa: nhắn /start cho bot, xem Serial
const int CHAN_AO = 34, CHAN_COI = 25, CHAN_DEN = 2, CHAN_NUT = 0;
const float VCC_MODULE = 5.0, HE_SO_PHAN_AP = 1.5, R_TAI_KOHM = 1.0;
const float TI_SO_KHONG_KHI = 9.83, A_LPG = 574.25, B_LPG = -2.222; // đường LPG trên đồ thị datasheet MQ-2 (như bài MQ-2)
const float NGUONG_PPM[3] = {0, 1000, 2000}; // mức 1: có khí gas; mức 2: nguy hiểm (khoảng 5 % và 10 % giới hạn cháy nổ dưới)
const float TRE_PPM = 200; // phải giảm thêm 200 ppm dưới ngưỡng mới hạ mức
const uint32_t THOI_GIAN_NONG_MS = 180000;
const uint32_t NHAC_LAI_MS = 120000; // đang báo động thì 2 phút nhắn lại một lần
const uint32_t TAT_COI_MS = 300000;
const int GIO_TU_KIEM_TRA = 8;
WiFiClientSecure tls;
Preferences boNho;
float r0 = 0, ppm = 0, aoV = 0;
int muc = 0, mucDaBao = 0, ngayTuKiemTra = -1;
bool loiCamBien = false, daBaoKhoiDong = false;
uint32_t lanNhan = 0, tatCoiDen = 0, lanHoi = 0, lanBao = 0;
long long updateTiep = 0;
const char *LOI_KHUYEN = "Khóa van bình gas, mở cửa cho thoáng. Không bật tắt công tắc, quạt, không dùng lửa, không gọi điện trong phòng có gas.";
// ---- MQ-2 (như bài MQ-2, MQ-135) ----
float docAoV() {
uint32_t tong = 0;
for (int i = 0; i < 64; i++) tong += analogReadMilliVolts(CHAN_AO);
return tong / 64.0f / 1000.0f * HE_SO_PHAN_AP;
}
float rsTuAo(float v) {
if (v < 0.01f) v = 0.01f;
return R_TAI_KOHM * (VCC_MODULE - v) / v;
}
void hieuChuan() {
float tong = 0;
for (int i = 0; i < 50; i++) {
tong += rsTuAo(docAoV());
delay(20);
}
r0 = tong / 50 / TI_SO_KHONG_KHI;
boNho.putFloat("r0", r0);
Serial.printf("Hieu chuan MQ-2: R0 = %.2f kOhm\n", r0);
}
// ---- Telegram ----
bool guiTin(const String &noiDung) {
if (WiFi.status() != WL_CONNECTED || CHAT_ID == 0) return false;
HTTPClient http;
http.begin(tls, String("https://api.telegram.org/bot") + BOT_TOKEN + "/sendMessage");
http.addHeader("Content-Type", "application/json");
JsonDocument doc;
doc["chat_id"] = CHAT_ID;
doc["text"] = noiDung;
String than;
serializeJson(doc, than);
int ma = http.POST(than);
http.end();
Serial.printf("Gui tin Telegram: HTTP %d\n", ma);
return ma == 200;
}
String trangThai() {
char s[160];
if (millis() < THOI_GIAN_NONG_MS) snprintf(s, sizeof(s), "Cảm biến đang làm nóng, còn %lu giây.", (unsigned long)((THOI_GIAN_NONG_MS - millis()) / 1000));
else if (loiCamBien) snprintf(s, sizeof(s), "LỖI CẢM BIẾN: AO %.2f V, kiểm tra dây và nguồn 5 V.", aoV);
else if (ppm < 200) snprintf(s, sizeof(s), "Bình thường: khí gas dưới 200 ppm (AO %.2f V).", aoV);
else snprintf(s, sizeof(s), "Khí gas khoảng %.0f ppm, mức %d (AO %.2f V).", ppm, muc, aoV);
return String(s);
}
void docTinMoi() {
HTTPClient http;
char so[24];
snprintf(so, sizeof(so), "%lld", updateTiep);
http.begin(tls, String("https://api.telegram.org/bot") + BOT_TOKEN + "/getUpdates?timeout=0&limit=5&offset=" + so);
int ma = http.GET();
String than = ma == 200 ? http.getString() : String();
http.end();
JsonDocument doc;
if (ma != 200 || deserializeJson(doc, than)) return;
for (JsonObject u : doc["result"].as<JsonArray>()) {
updateTiep = u["update_id"].as<long long>() + 1;
long long chat = u["message"]["chat"]["id"].as<long long>();
String chu = u["message"]["text"] | "";
if (CHAT_ID == 0 || chat != CHAT_ID) {
Serial.printf("Tin tu chat id %lld (bo qua): %s\n", chat, chu.c_str());
continue;
}
chu.trim();
guiTin(chu == "/gas" ? trangThai() : String("Lệnh: /gas xem tình trạng"));
}
}
// ---- Mức báo động, còi, tin nhắn ----
void capNhatMuc() {
aoV = docAoV();
loiCamBien = aoV < 0.05f || aoV > VCC_MODULE - 0.2f; // AO sát 0 V hoặc sát 5 V: đứt dây, chập, hỏng dây nung
if (loiCamBien || r0 <= 0) return;
ppm = A_LPG * powf(rsTuAo(aoV) / r0, B_LPG);
int moi = muc;
while (moi < 2 && ppm >= NGUONG_PPM[moi + 1]) moi++; // lên mức ngay khi vượt ngưỡng
while (moi > 0 && ppm < NGUONG_PPM[moi] - TRE_PPM) moi--; // xuống mức khi đã giảm hẳn
muc = moi;
}
void baoDong() {
static bool daBaoLoi = false; // lỗi cảm biến nhắn một lần, hết lỗi thì cho phép nhắn lại lần sau
if (loiCamBien && !daBaoLoi) daBaoLoi = guiTin(trangThai()) || WiFi.status() != WL_CONNECTED;
if (!loiCamBien) daBaoLoi = false;
bool canNhan = muc > mucDaBao || (muc > 0 && millis() - lanNhan >= NHAC_LAI_MS) || (muc == 0 && mucDaBao > 0);
if (!canNhan) return;
String tin;
if (muc == 0) tin = String("Hết báo động: khí gas đã giảm. ") + trangThai();
else tin = String(muc == 2 ? "NGUY HIỂM: " : "CẢNH BÁO: ") + "phát hiện rò khí gas khoảng " + String(ppm, 0) + " ppm. " + LOI_KHUYEN;
Serial.printf("Bao dong muc %d, %.0f ppm\n", muc, ppm);
lanNhan = millis();
if (guiTin(tin) || WiFi.status() != WL_CONNECTED) mucDaBao = muc; // không có mạng thì vẫn kêu còi, có mạng lại sẽ nhắn
}
void dieuKhienCoi() {
bool coiBat = false;
if (muc > 0 && (int32_t)(millis() - tatCoiDen) >= 0) {
uint32_t chuKy = muc == 2 ? 300 : 1000; // mức 2 kêu dồn dập
coiBat = (millis() % chuKy) < chuKy / 2;
}
digitalWrite(CHAN_COI, coiBat);
digitalWrite(CHAN_DEN, muc > 0 ? (millis() / 250) % 2 : loiCamBien);
if (digitalRead(CHAN_NUT) == LOW && muc > 0) {
tatCoiDen = millis() + TAT_COI_MS;
Serial.println("Tat coi 5 phut (van nhan tin)");
}
}
void tuKiemTra() {
struct tm t;
if (!getLocalTime(&t, 0) || t.tm_hour != GIO_TU_KIEM_TRA || t.tm_yday == ngayTuKiemTra) return;
ngayTuKiemTra = t.tm_yday;
guiTin(String("Tự kiểm tra hằng ngày: ") + trangThai());
}
void setup() {
Serial.begin(115200);
pinMode(CHAN_COI, OUTPUT);
pinMode(CHAN_DEN, OUTPUT);
pinMode(CHAN_NUT, INPUT_PULLUP);
analogSetAttenuation(ADC_11db);
boNho.begin("baogas", false);
r0 = boNho.getFloat("r0", 0);
tls.setInsecure();
WiFi.mode(WIFI_STA);
WiFi.begin(TEN_WIFI, MAT_KHAU_WIFI);
configTzTime("ICT-7", "pool.ntp.org", "time.google.com");
Serial.println("Dang lam nong day nung MQ-2, cho 3 phut...");
}
void loop() {
if (Serial.available()) {
char c = Serial.read();
if ((c == 'C' || c == 'c') && millis() >= THOI_GIAN_NONG_MS) hieuChuan();
}
static uint32_t lanDo = 0;
if (millis() >= THOI_GIAN_NONG_MS && millis() - lanDo >= 1000) {
lanDo = millis();
capNhatMuc();
static uint32_t lanIn = 0;
if (millis() - lanIn >= 5000 || muc > 0) {
lanIn = millis();
if (r0 <= 0) Serial.println("Chua hieu chuan: dat cam bien o khong khi sach roi go C");
else Serial.printf("AO %.2f V, LPG %.0f ppm, muc %d%s\n", aoV, ppm, muc, loiCamBien ? " | LOI CAM BIEN" : "");
}
baoDong();
}
dieuKhienCoi();
if (WiFi.status() != WL_CONNECTED) return;
if (!daBaoKhoiDong && (lanBao == 0 || millis() - lanBao > 10000)) {
lanBao = millis();
if (CHAT_ID == 0) Serial.println("Chua co CHAT_ID: nhan /start cho bot roi xem chat id in ra o day");
daBaoKhoiDong = CHAT_ID == 0 || guiTin("Máy báo gas đã chạy, đang làm nóng cảm biến 3 phút.");
}
tuKiemTra();
if (millis() - lanHoi >= 3000) {
lanHoi = millis();
docTinMoi();
}
}
Giải thích
capNhatMuc()đọc AO mỗi giây. Mức tăng ngay khi vượt ngưỡng, nhưng chỉ giảm khi nồng độ xuống thấp hơn ngưỡng 200 ppm, tránh báo lên báo xuống liên tục.- Lỗi cảm biến: AO gần 0 V (đứt dây, dây nung hỏng) hoặc gần 5 V (chập) thì không tính ppm, báo "LỖI CẢM BIẾN" qua Telegram một lần và đèn D2 sáng đứng. Máy báo gas mà hỏng âm thầm còn nguy hiểm hơn không có máy.
baoDong()nhắn khi lên mức, nhắc lại mỗi 2 phút trong lúc còn báo động, và nhắn "Hết báo động" khi đã về mức 0. Mất mạng thì còi vẫn kêu; có mạng lại sẽ nhắn.- Còi: mức 1 kêu chậm, mức 2 kêu dồn dập. Bấm BOOT để tắt còi 5 phút (vẫn nhắn tin), tiện khi đã biết và đang xử lý.
- Tự kiểm tra lúc 8 giờ sáng mỗi ngày: nhắn tình trạng hiện tại. Sáng nào không nhận được tin này là máy mất điện, mất mạng hoặc treo.
7. Kết quả



Thử máy an toàn bằng bật lửa gas chưa đánh lửa: nhấn nhả van bật lửa một chút cách cảm biến vài cm, ngoài trời hoặc chỗ thoáng, xa mọi nguồn lửa. Không xả gas từ bình để thử.
8. Lỗi hay gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Vừa cắm điện đã báo động | Chưa làm nóng xong, chưa hiệu chuẩn, hiệu chuẩn lúc không khí có cồn, khói | Chờ 3 phút; hiệu chuẩn lại ở chỗ thoáng |
| Báo nhầm khi nấu ăn, xịt cồn | MQ-2 phản ứng với khói, hơi cồn | Dời cảm biến xa bếp hơn; tăng ngưỡng mức 1 |
| "LỖI CẢM BIẾN" | Đứt dây AO, quên cầu phân áp, nguồn 5 V yếu | Đo AO bằng đồng hồ; kiểm tra cục sạc từ 1 A |
| Không nhận được tin | Sai token, CHAT_ID, mất Wi-Fi | Xem Serial: "Gui tin Telegram: HTTP 200" |
| Số ppm trôi dần sau nhiều tháng | Cảm biến già, bám bẩn | Hiệu chuẩn lại định kỳ; thay cảm biến sau vài năm |
9. Hướng mở rộng
- Thêm van điện từ khóa gas loại chuyên dụng cho khí đốt, do thợ lắp, để máy tự khóa gas khi báo nguy hiểm.
- Kết hợp với cân bình gas trong cùng nhóm dự án: một tin nhắn báo cả lượng gas còn lại lẫn tình trạng rò.
- Chạy dự phòng bằng pin để máy vẫn báo khi mất điện lưới.
10. Xem thêm
- Cảm biến khí ga: MQ, điện hóa, xúc tác, NDIR – cấu tạo, nguyên lý
- Bài MQ-2, MQ-135 trong loạt mô-đun (hiệu chuẩn R0, đường cong); bài chuông cửa ESP32-CAM và bài cân bình gas trong cùng nhóm dự án.
Xem thêm