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ở Rs củ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 cho R0 (đ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.
Đồ thị log-log tỉ số Rs/R0 theo nồng độ LPG của MQ-2, hai điểm ở 1000 và 2000 ppm; bảng với R0 1,6 kΩ: không khí sạch Rs/R0 9,83, AO 0,30 V; mức 1 Rs/R0 0,78, AO 2,23 V; mức 2 Rs/R0 0,57, AO 2,61 V; điện áp ở chân D34 bằng 2/3 AO
Đường cong LPG của MQ-2 và hai ngưỡng báo (R0 là ví dụ).

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

Sơ đồ nối: MQ-2 VCC vào VIN, GND, AO qua cầu phân áp 10k/20k vào D34, DO để trống; mạch còi chủ động IN vào D25, 5 V vào VIN, GND
AO của MQ-2 qua cầu phân áp vào D34; còi ở D25.
  • 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

Hình bếp nhìn ngang: bình gas, bếp, cảm biến đặt thấp trên tường cách sàn khoảng 30 cm và cách bếp 1 đến 4 m, khí LPG lan sát sàn
Gas bình nặng hơn không khí nên cảm biến đặt thấp.

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ô phỏng nồng độ khí gas tăng từ phút 1 tới phút 10 lên khoảng 2500 ppm rồi giảm sau khi khóa van; dải mức 1 và mức 2 bên dưới; các chấm tin nhắn lúc 03:02, 05:02, 06:30, 08:30, 10:30, 12:30 và 13:26
Một lần rò gas mô phỏng: mức báo và thời điểm nhắn tin tính đúng như chương trình.
Tin nhắn trên điện thoại: máy báo gas đã chạy; cảnh báo phát hiện rò khí gas khoảng 1002 ppm kèm lời khuyên khóa van, mở cửa; nhắc lại 1661 ppm; nguy hiểm 2001 ppm; nhắc lại 2331 ppm
Tin nhắn Telegram theo mô phỏng (giao diện vẽ kiểu chung).
Serial Monitor: làm nóng 3 phút, hiệu chuẩn R0 1,60 kOhm, không khí sạch AO 0,30 V; vượt mức 1 ở 1002 ppm gửi tin; vượt mức 2 ở 2001 ppm gửi tin; bấm BOOT tắt còi 5 phút
Serial Monitor theo cùng mô phỏng.

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ượngNguyên nhân thường gặpCách xử lý
Vừa cắm điện đã báo độngChư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óiChờ 3 phút; hiệu chuẩn lại ở chỗ thoáng
Báo nhầm khi nấu ăn, xịt cồnMQ-2 phản ứng với khói, hơi cồnDờ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 tinSai token, CHAT_ID, mất Wi-FiXem Serial: "Gui tin Telegram: HTTP 200"
Số ppm trôi dần sau nhiều thángCảm biến già, bám bẩnHiệ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


Tin tức liên quan

Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE
Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE

Hướng dẫn bật OTA trên ESP32 bằng thư viện ArduinoOTA có sẵn, nạp chương trình qua Wi-Fi từ Arduino IDE 2.x, đặt mật khẩu và các quy tắc để không mất khả năng cập nhật từ xa.

ESP32 điều khiển động cơ không chổi than qua ESC: xung 1000–2000 µs, hiệu chuẩn dải ga, thử an toàn
ESP32 điều khiển động cơ không chổi than qua ESC: xung 1000–2000 µs, hiệu chuẩn dải ga, thử an toàn

Dùng ESP32 phát xung ga 1000–2000 µs bằng LEDC để điều khiển ESC và động cơ không chổi than: nối dây với pin LiPo, hiệu chuẩn dải ga, tăng ga từ từ, khóa động cơ và an toàn khi thử.

Hướng dẫn bắt đầu với Wemos D1 R2 (ESP8266) trên Arduino IDE
Hướng dẫn bắt đầu với Wemos D1 R2 (ESP8266) trên Arduino IDE

Hướng dẫn dùng Wemos D1 R2 (ESP8266 khung Arduino UNO) với Arduino IDE 2.x: sơ đồ chân, cài driver CH340 và gói board ESP8266, nạp code nháy LED, kết nối WiFi và xử lý lỗi thường gặp.

Xe robot ESP32 điều khiển bằng điện thoại qua Wi-Fi: không cần router, không cần cài ứng dụng
Xe robot ESP32 điều khiển bằng điện thoại qua Wi-Fi: không cần router, không cần cài ứng dụng

Làm xe robot hai bánh với ESP32 và L298N, điều khiển bằng trình duyệt điện thoại qua Wi-Fi do ESP32 phát ra: bố trí, nối dây, code web, tự dừng khi mất sóng, lái vi sai, STEM.

Tự làm tay điều khiển flycam bằng ESP32 và ESP-NOW, bộ thu xuất PPM, SBUS cho mạch điều khiển bay
Tự làm tay điều khiển flycam bằng ESP32 và ESP-NOW, bộ thu xuất PPM, SBUS cho mạch điều khiển bay

Tự làm tay điều khiển flycam bằng ESP32: hai joystick, hai công tắc, gửi 8 kênh qua ESP-NOW; bộ thu ESP32 xuất PPM bằng RMT và SBUS bằng UART, có failsafe, dùng với Betaflight.

Màn hình TFT SPI ILI9341 với ESP32: nối dây, lệnh khởi động, vẽ chữ và đồ thị không cần thư viện
Màn hình TFT SPI ILI9341 với ESP32: nối dây, lệnh khởi động, vẽ chữ và đồ thị không cần thư viện

Nối màn hình TFT ILI9341 2.4–2.8 inch với ESP32, các lệnh khởi động và vẽ vùng, màu RGB565, bộ chữ 5x7, chương trình vẽ số lớn và đồ thị điện áp không nhấp nháy, bảng sửa lỗi.

Cân IoT theo dõi bình gas bằng ESP32 và HX711: còn bao nhiêu kg, ước tính ngày hết, báo qua Telegram
Cân IoT theo dõi bình gas bằng ESP32 và HX711: còn bao nhiêu kg, ước tính ngày hết, báo qua Telegram

Làm cân IoT đặt dưới bình gas: ESP32 đọc load cell qua HX711, trừ vỏ bình ra số kg gas còn lại, hồi quy tính mức dùng mỗi ngày, ước tính ngày hết và nhắn Telegram khi sắp hết.

Máy cho cá ăn tự động bằng ESP32: servo SG90, DS3231 giữ giờ khi mất điện, đặt lịch trên web
Máy cho cá ăn tự động bằng ESP32: servo SG90, DS3231 giữ giờ khi mất điện, đặt lịch trên web

Làm máy cho cá ăn tự động bằng ESP32: servo SG90 quay đĩa có lỗ, DS3231 giữ giờ khi mất điện, đồng bộ giờ Internet, trang web đặt 4 giờ cho ăn, không cho ăn lặp sau mất điện.


Bình luận
Đã thêm vào giỏ hàng