IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Series ESP32 & Cảm biến: Bài 31 – Đọc cảm biến MQ-135: đo chất lượng không khí & theo dõi realtime

MQ-135 là cảm biến “đời phổ thông” dùng trong DIY để theo dõi xu hướng chất lượng không khí (air quality) như: khói nhẹ, hơi cồn, VOC, ammonia… Bài này tập trung vào cách làm ổn định – dễ hiển thị dashboard – cảnh báo theo ngưỡng, thay vì cố ra “ppm chuẩn” (vì MQ-135 muốn ppm chuẩn cần hiệu chuẩn nghiêm túc).

1) MQ-135 đo gì? Dùng đúng kỳ vọng

  • MQ-135 nhạy với nhiều loại khí (VOC, NH3, khói, hơi dung môi…).
  • Module thường có:
    • AO (Analog Out): giá trị analog thể hiện “mức khí tổng hợp” (không khí bẩn hơn ⇒ AO thay đổi).
    • DO (Digital Out): bật/tắt theo ngưỡng biến trở trên module.

Kết luận thực tế: MQ-135 phù hợp nhất cho:

  • Theo dõi chỉ số tương đối (index 0–100), phát hiện bất thường theo baseline.
  • Cảnh báo “không khí đang xấu đi” theo thời gian.

2) Chuẩn bị phần cứng

  • ESP32 (DevKit / ESP32-S3/C3 đều được)
  • Module MQ-135
  • Dây jumper
  • Nguồn 5V ổn định (heater của MQ tiêu thụ dòng tương đối)

MQ-135 có heater nên ấm khi chạy. Đặt nơi thoáng, tránh sát nhựa dễ biến dạng.

3) Nối dây chuẩn (dùng AO)

Sơ đồ nối dây

MQ-135ESP32
VCC5V
GNDGND
AOGPIO34 (ADC) (hoặc 32/33/35)
DO (tuỳ chọn)GPIO25 (input)

Lưu ý điện áp AO

  • ESP32 ADC tối đa ~3.3V (tùy config).
  • Nhiều module MQ-135 có AO không lên tới 5V (do mạch chia áp/LM393), nhưng không phải module nào cũng giống nhau. ➡️ Cách an toàn: đo AO bằng đồng hồ khi chạy, nếu AO có thể >3.3V thì cần chia áp trước khi đưa vào ESP32.

4) Chiến lược đọc “realtime nhưng không nhiễu”

MQ sensor thường nhiễu và trôi, nên bạn nên làm 3 lớp:

  1. Warm-up: chờ ổn định (tối thiểu vài phút; lần đầu có thể lâu hơn).
  2. Averaging: đọc nhiều mẫu, lấy trung bình.
  3. Baseline + Index: chuyển sang thang 0–100 để dễ hiển thị.

Ý tưởng:

  • aq_raw: ADC trung bình (0–4095)
  • aq_index: 0–100 (chuẩn hóa theo min/max thực nghiệm)
  • aq_state: Normal / Warning / Danger (cảnh báo theo ngưỡng)

5) Code ESP32 (Arduino) – đọc MQ-135 + publish MQTT realtime

Ví dụ này:

  • Đọc AO bằng ADC
  • Lọc bằng trung bình nhiều mẫu
  • Map ra aq_index 0–100 theo min/max bạn tự chỉnh sau khi chạy thử
  • Publish MQTT mỗi 2 giây theo topic chuẩn IoTLabs

Bạn thay cấu hình Wi-Fi/MQTT cho hệ của bạn.

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

#define PIN_MQ135_AO 34
#define ADC_SAMPLES 30
#define PUBLISH_EVERY_MS 2000

// --- WiFi/MQTT
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_PASS";

const char* MQTT_HOST = "broker.iotlabs.vn"; // ví dụ
const int   MQTT_PORT = 1883;                // TLS: 8883
const char* MQTT_USER = "YOUR_USER";
const char* MQTT_PASS = "YOUR_PASS";

const char* PROJECT_ID = "demo_project";
const char* DEVICE_ID  = "esp32_mq135_01";

WiFiClient espClient;
PubSubClient mqtt(espClient);

unsigned long lastPub = 0;

// Bạn chỉnh 2 giá trị này dựa trên quan sát thực tế
// - AQ_MIN: giá trị raw khi không khí sạch tương đối
// - AQ_MAX: giá trị raw khi có mùi/VOC/khói nhẹ tăng rõ rệt
int AQ_MIN = 900;   // ví dụ
int AQ_MAX = 2800;  // ví dụ

uint32_t nowSeconds() {
  // Nếu bạn có time sync chuẩn (NTP/time service) thì dùng unix seconds thật.
  return (uint32_t)(millis() / 1000);
}

int readMq135Avg() {
  uint32_t sum = 0;
  for (int i = 0; i < ADC_SAMPLES; i++) {
    sum += analogRead(PIN_MQ135_AO);
    delay(5);
  }
  return (int)(sum / ADC_SAMPLES);
}

int clampInt(int x, int lo, int hi) {
  if (x < lo) return lo;
  if (x > hi) return hi;
  return x;
}

int toIndex(int raw) {
  raw = clampInt(raw, AQ_MIN, AQ_MAX);
  long idx = (long)(raw - AQ_MIN) * 100L / (long)(AQ_MAX - AQ_MIN);
  return clampInt((int)idx, 0, 100);
}

const char* toState(int idx) {
  if (idx >= 80) return "danger";
  if (idx >= 60) return "warning";
  return "normal";
}

void wifiConnect() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(300);
}

void mqttConnect() {
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  while (!mqtt.connected()) {
    String clientId = String("iotlabs-") + DEVICE_ID;
    mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS);
    delay(500);
  }
}

void publishTelemetry(int raw, int idx, const char* state) {
  String topic = String("iotlabs/") + PROJECT_ID + "/" + DEVICE_ID + "/telemetry";

  StaticJsonDocument<256> doc;
  doc["ts"] = nowSeconds();

  JsonObject metrics = doc.createNestedObject("metrics");
  metrics["aq_raw"] = raw;
  metrics["aq_index"] = idx;

  JsonObject tags = doc.createNestedObject("tags");
  tags["state"] = state;
  tags["sensor"] = "mq135";

  char payload[256];
  size_t n = serializeJson(doc, payload);

  mqtt.publish(topic.c_str(), payload, n);
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);

  wifiConnect();
  mqttConnect();

  Serial.println("MQ-135 warm-up... (60s demo)");
  delay(60000); // demo; thực tế nên 3-5 phút để ổn định hơn
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) wifiConnect();
  if (!mqtt.connected()) mqttConnect();
  mqtt.loop();

  if (millis() - lastPub >= PUBLISH_EVERY_MS) {
    lastPub = millis();

    int raw = readMq135Avg();
    int idx = toIndex(raw);
    const char* state = toState(idx);

    Serial.printf("AQ raw=%d | index=%d | state=%s\n", raw, idx, state);
    publishTelemetry(raw, idx, state);
  }
}

6) Cách chỉnh AQ_MIN/AQ_MAX nhanh (thực tế nhất)

  1. Để cảm biến chạy ổn định 3–5 phút.
  2. Ghi lại raw trong môi trường bình thường (đóng cửa vừa phải) ⇒ set AQ_MIN quanh giá trị đó.
  3. Tạo tình huống “không khí xấu hơn” nhưng an toàn:
    • mở nắp chai cồn/hand sanitizer gần cảm biến vài giây (không đổ, không hít sát)
    • dùng nước hoa/xịt khử mùi nhẹ ở xa Ghi lại raw khi tăng rõ ⇒ set AQ_MAX quanh giá trị đó.

Mục tiêu: để aq_index chạy 10–40 ở môi trường bình thường, và tăng mạnh khi có VOC/mùi.

7) Dashboard realtime gợi ý (chuẩn IoTLabs)

Card 1: Last value

  • AQ Index: 0–100
  • State badge: normal/warning/danger

Chart 1h / 24h

  • Line chart aq_index
  • Có vùng ngưỡng (>=60 warning, >=80 danger)

Rule cảnh báo

  • Warning nếu aq_index >= 60 liên tục 15s
  • Danger nếu aq_index >= 80 liên tục 10s
  • Cooldown 2–5 phút để tránh spam

8) Khi nào nên nâng cấp cảm biến “chuẩn” hơn?

Nếu bạn cần số liệu “đáng tin” và dễ so sánh:

  • CO2 chuẩn: SCD41 / MH-Z19B
  • Bụi mịn: PMS5003/PMS7003 (PM2.5/PM10)
  • TVOC/IAQ hiện đại: SGP40 / ENS160

MQ-135 vẫn ok cho DIY, nhưng production/khuyến nghị lâu dài thì 3 nhóm trên “đáng tiền” hơn.