DIY Air Quality Monitor ด้วย ESP32: ตรวจจับ PM2.5, CO2 และอุณหภูมิ
สร้างเครื่องวัดคุณภาพอากาศ DIY ด้วย ESP32 ตรวจจับฝุ่น PM2.5, ก๊าซ CO2, อุณหภูมิ และความชื้น พร้อมเชื่อมต่อ Home Assistant และแจ้งเตือนอัตโนมัติ
ทำไมต้องสร้าง Air Quality Monitor เอง?
💡 ประโยชน์ของ Air Quality Monitor
- ตรวจสอบคุณภาพอากาศในบ้าน - รู้ว่าอากาศที่หายใจเป็นอย่างไร
- เตือนภัยล่วงหน้า - ได้รับการแจ้งเตือนเมื่ออากาศแย่ลง
- ติดตามแนวโน้ม - เห็นการเปลี่ยนแปลงของคุณภาพอากาศตลอดเวลา
- ประหยัดค่าใช้จ่าย - ถูกกว่าซื้อเครื่องวัดสำเร็จรูป
- ปรับแต่งได้ - เพิ่มเซ็นเซอร์หรือฟีเจอร์ได้ตามต้องการ
โปรเจกต์นี้ได้รับแรงบันดาลใจจาก Project Aura ซึ่งเป็น DIY air quality monitor ที่นิยมในชุมชน IoT เราจะสร้างเวอร์ชันที่:
- ตรวจจับ PM2.5 (ฝุ่นละอองขนาดเล็ก)
- วัด ก๊าซ CO2 (คาร์บอนไดออกไซด์)
- ติดตาม อุณหภูมิและความชื้น
- เชื่อมต่อกับ Home Assistant สำหรับ automation
- มี หน้าจอแสดงผล และ แจ้งเตือน อัตโนมัติ
⚠️ คำเตือน: เครื่องวัด DIY นี้เหมาะสำหรับการใช้งานส่วนบุคคลและการศึกษาเท่านั้น หากต้องการความแม่นยำระดับมืออาชีพ ควรใช้เครื่องวัดที่ได้รับการรับรองมาตรฐาน
อุปกรณ์ที่ต้องใช้
🎯 Core Components
- •ESP32 Development Board
แนะนำ ESP32-WROOM-32 หรือ ESP32-S3
- •Plantower PMS5003
เซ็นเซอร์ฝุ่น PM2.5/PM10 (Laser Particle Sensor)
- •SCD41 or SCD30
เซ็นเซอร์ CO2 (Sensirion)
- •BME280 or DHT22
เซ็นเซอร์อุณหภูมิและความชื้น
⚡ Power & Display
- •OLED Display 0.96" 128x64
แสดงผลข้อมูล (I2C)
- •USB-C Power Adapter
จ่ายไฟ 5V 2A ขึ้นไป
- •Jumper Wires & Breadboard
สำหรับต่อวงจร
- •3D Printed Case (optional)
บรรจุอุปกรณ์
💰 ราคาโดยประมาณ
- ESP32: ~150-200 บาท
- PMS5003: ~350-500 บาท
- SCD41: ~800-1,200 บาท (หรือ SCD30 ~1,500-2,000 บาท)
- BME280: ~150-250 บาท
- OLED Display: ~100-150 บาท
- รวมทั้งหมด: ~1,550-2,300 บาท
การเชื่อมต่อวงจร
ต่อไปนี้คือการเชื่อมต่อระหว่างเซ็นเซอร์กับ ESP32:
// ==================== การเชื่อมต่อเซ็นเซอร์ ====================
// ===== PMS5003 (PM2.5 Sensor) =====
// PMS5003 ESP32
// VCC → 5V
// GND → GND
// TX → GPIO16 (RX2)
// RX → GPIO17 (TX2)
// ===== SCD41 (CO2 Sensor) =====
// SCD41 ESP32
// VCC → 3.3V
// GND → GND
// SDA → GPIO21 (I2C SDA)
// SCL → GPIO22 (I2C SCL)
// ===== BME280 (Temp & Humidity) =====
// BME280 ESP32
// VCC → 3.3V
// GND → GND
// SDA → GPIO21 (I2C SDA)
// SCL → GPIO22 (I2C SCL)
// ===== OLED Display =====
// OLED ESP32
// VCC → 3.3V
// GND → GND
// SDA → GPIO21 (I2C SDA)
// SCL → GPIO22 (I2C SCL)💡 Tip: เซ็นเซอร์ SCD41 และ BME280 ใช้ I2C bus เดียวกันได้ เพราะมี address ต่างกัน
โค้ด Arduino/PlatformIO
platformio.ini
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
; Serial Monitor options
monitor_speed = 115200
; Library dependencies
lib_deps =
adafruit/Adafruit BME280 Library @ ^2.2.2
adafruit/Adafruit Unified Sensor @ ^1.1.9
sparkfun/SparkFun SCD4x Arduino Library @ ^1.0.11
adafruit/Adafruit SSD1306 @ ^2.5.7
adafruit/Adafruit GFX Library @ ^1.11.3
bblanchon/ArduinoJson @ ^6.21.3
; Upload settings
upload_speed = 921600main.cpp
/**
* DIY Air Quality Monitor ด้วย ESP32
* ตรวจจับ PM2.5, CO2, อุณหภูมิ และความชื้น
* เชื่อมต่อ Home Assistant ผ่าน MQTT
*
* Hardware:
* - ESP32 Development Board
* - PMS5003 (PM2.5 Sensor)
* - SCD41 (CO2 Sensor)
* - BME280 (Temp & Humidity)
* - OLED Display 0.96"
*/
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <Adafruit_SSD1306.h>
#include <SparkFun_SCD4x_Arduino_Library.h>
#include <ArduinoJson.h>
// ==================== การตั้งค่า WiFi ====================
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
// ==================== การตั้งค่า MQTT (Home Assistant) ====================
const char* MQTT_BROKER = "homeassistant.local"; // หรือใช้ IP
const int MQTT_PORT = 1883;
const char* MQTT_USER = "homeassistant"; // ถ้ามี
const char* MQTT_PASSWORD = "your_password"; // ถ้ามี
// MQTT Topics
const char* MQTT_TOPIC_PM25 = "home/airquality/pm25";
const char* MQTT_TOPIC_PM10 = "home/airquality/pm10";
const char* MQTT_TOPIC_CO2 = "home/airquality/co2";
const char* MQTT_TOPIC_TEMP = "home/airquality/temperature";
const char* MQTT_TOPIC_HUMIDITY = "home/airquality/humidity";
const char* MQTT_TOPIC_AQI = "home/airquality/aqi";
// ==================== การตั้งค่าเซ็นเซอร์ ====================
#define PMS_RX_PIN 16
#define PMS_TX_PIN 17
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_RESET -1
// ==================== การตั้งค่าการแจ้งเตือน ====================
#define PM25_WARNING_THRESHOLD 50 // µg/m³
#define PM25_DANGER_THRESHOLD 100 // µg/m³
#define CO2_WARNING_THRESHOLD 1000 // ppm
#define CO2_DANGER_THRESHOLD 2000 // ppm
// ==================== Global Objects ====================
WiFiClient espClient;
PubSubClient mqttClient(espClient);
Adafruit_BME280 bme;
SCD4x scd4x;
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);
// ==================== Data Structures ====================
struct SensorData {
float pm25; // PM2.5 (µg/m³)
float pm10; // PM10 (µg/m³)
float co2; // CO2 (ppm)
float temperature; // อุณหภูมิ (°C)
float humidity; // ความชื้น (%)
int aqi; // Air Quality Index
String status; // สถานะคุณภาพอากาศ
};
SensorData currentData;
unsigned long lastSensorRead = 0;
const unsigned long SENSOR_READ_INTERVAL = 2000; // 2 วินาที
// ==================== WiFi Functions ====================
void connectWiFi() {
Serial.println("🔌 กำลังเชื่อมต่อ WiFi...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n✅ เชื่อมต่อ WiFi สำเร็จ!");
Serial.print("📡 IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\n❌ ไม่สามารถเชื่อมต่อ WiFi ได้");
}
}
// ==================== MQTT Functions ====================
void connectMQTT() {
while (!mqttClient.connected()) {
Serial.print("🔗 กำลังเชื่อมต่อ MQTT...");
if (mqttClient.connect("ESP32_AirQuality", MQTT_USER, MQTT_PASSWORD)) {
Serial.println("✅ เชื่อมต่อสำเร็จ!");
} else {
Serial.print("❌ ล้มเหลว (");
Serial.print(mqttClient.state());
Serial.println(")");
delay(5000);
}
}
}
void publishMQTTData() {
// สร้าง JSON payload
StaticJsonDocument<256> doc;
doc["pm25"] = currentData.pm25;
doc["pm10"] = currentData.pm10;
doc["co2"] = currentData.co2;
doc["temperature"] = currentData.temperature;
doc["humidity"] = currentData.humidity;
doc["aqi"] = currentData.aqi;
doc["status"] = currentData.status;
String payload;
serializeJson(doc, payload);
// ส่งข้อมูลแต่ละตัว
mqttClient.publish(MQTT_TOPIC_PM25, String(currentData.pm25).c_str());
mqttClient.publish(MQTT_TOPIC_PM10, String(currentData.pm10).c_str());
mqttClient.publish(MQTT_TOPIC_CO2, String(currentData.co2).c_str());
mqttClient.publish(MQTT_TOPIC_TEMP, String(currentData.temperature).c_str());
mqttClient.publish(MQTT_TOPIC_HUMIDITY, String(currentData.humidity).c_str());
mqttClient.publish(MQTT_TOPIC_AQI, String(currentData.aqi).c_str());
Serial.println("📤 ส่งข้อมูล MQTT แล้ว");
}
// ==================== Sensor Functions ====================
void initSensors() {
Serial.println("🔧 กำลังเริ่มต้นเซ็นเซอร์...");
// เริ่มต้น BME280
if (!bme.begin(0x76)) {
Serial.println("❌ ไม่พบ BME280!");
} else {
Serial.println("✅ BME280 พร้อมใช้งาน");
}
// เริ่มต้น SCD41
Wire.begin();
if (scd4x.begin() == false) {
Serial.println("❌ ไม่พบ SCD41!");
} else {
Serial.println("✅ SCD41 พร้อมใช้งาน");
scd4x.startPeriodicMeasurement();
}
// เริ่มต้น PMS5003
Serial2.begin(9600, SERIAL_8N1, PMS_RX_PIN, PMS_TX_PIN);
Serial.println("✅ PMS5003 พร้อมใช้งาน");
// เริ่มต้น OLED
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("❌ ไม่พบ OLED display!");
} else {
Serial.println("✅ OLED display พร้อมใช้งาน");
}
}
void readSensors() {
// อ่าน PMS5003
if (Serial2.available()) {
// PMS5003 data format: 32 bytes
// ตำแหน่ง bytes: 4-5 (PM1.0), 6-7 (PM2.5), 8-9 (PM10)
uint8_t buffer[32];
if (Serial2.readBytes(buffer, 32) == 32) {
if (buffer[0] == 0x42 && buffer[1] == 0x4d) {
currentData.pm25 = (buffer[12] << 8) | buffer[13];
currentData.pm10 = (buffer[14] << 8) | buffer[15];
}
}
}
// อ่าน SCD41 (CO2)
if (scd4x.readMeasurement()) {
currentData.co2 = scd4x.getCO2();
}
// อ่าน BME280 (อุณหภูมิและความชื้น)
currentData.temperature = bme.readTemperature();
currentData.humidity = bme.readHumidity();
// คำนวณ AQI (Air Quality Index)
calculateAQI();
}
void calculateAQI() {
// ใช้ PM2.5 เป็นตัวคำนวณหลัก
float pm25 = currentData.pm25;
if (pm25 <= 12.0) {
currentData.aqi = map(pm25, 0, 12, 0, 50);
currentData.status = "ดีมาก";
} else if (pm25 <= 35.4) {
currentData.aqi = map(pm25, 12.1, 35.4, 51, 100);
currentData.status = "ดี";
} else if (pm25 <= 55.4) {
currentData.aqi = map(pm25, 35.5, 55.4, 101, 150);
currentData.status = "ปานกลาง";
} else if (pm25 <= 150.4) {
currentData.aqi = map(pm25, 55.5, 150.4, 151, 200);
currentData.status = "ไม่ดีสำหรับกลุ่มเสี่ยง";
} else if (pm25 <= 250.4) {
currentData.aqi = map(pm25, 150.5, 250.4, 201, 300);
currentData.status = "ไม่ดี";
} else {
currentData.aqi = map(pm25, 250.5, 500.4, 301, 500);
currentData.status = "อันตราย";
}
}
// ==================== Display Functions ====================
void updateDisplay() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// หัวข้อ
display.setCursor(0, 0);
display.println("Air Quality Monitor");
// AQI และสถานะ
display.setCursor(0, 16);
display.setTextSize(2);
display.print("AQI: ");
display.println(currentData.aqi);
display.setTextSize(1);
display.setCursor(0, 36);
display.println(currentData.status);
// ข้อมูลอื่นๆ
display.setCursor(0, 48);
display.print("PM2.5: ");
display.print(currentData.pm25, 1);
display.print(" CO2: ");
display.print(currentData.co2, 0);
display.display();
}
// ==================== Alert Functions ====================
void checkAlerts() {
// ตรวจสอบ PM2.5
if (currentData.pm25 > PM25_DANGER_THRESHOLD) {
sendAlert("PM2.5 อันตราย!", currentData.pm25);
} else if (currentData.pm25 > PM25_WARNING_THRESHOLD) {
sendAlert("PM2.5 สูงกว่าปกติ", currentData.pm25);
}
// ตรวจสอบ CO2
if (currentData.co2 > CO2_DANGER_THRESHOLD) {
sendAlert("CO2 อันตราย!", currentData.co2);
} else if (currentData.co2 > CO2_WARNING_THRESHOLD) {
sendAlert("CO2 สูงกว่าปกติ", currentData.co2);
}
}
void sendAlert(String message, float value) {
Serial.println("⚠️ แจ้งเตือน: " + message + " (" + String(value) + ")");
// ส่ง alert ผ่าน MQTT
StaticJsonDocument<128> doc;
doc["alert"] = message;
doc["value"] = value;
doc["timestamp"] = millis();
String payload;
serializeJson(doc, payload);
mqttClient.publish("home/airquality/alert", payload.c_str());
}
// ==================== Setup & Loop ====================
void setup() {
Serial.begin(115200);
Serial.println("🚀 เริ่มต้น Air Quality Monitor...");
// เชื่อมต่อ WiFi
connectWiFi();
// เริ่มต้นเซ็นเซอร์
initSensors();
// ตั้งค่า MQTT
mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
// แสดงข้อความเริ่มต้นบนหน้าจอ
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Air Quality");
display.println("Monitor");
display.println("Starting...");
display.display();
delay(2000);
}
void loop() {
// ตรวจสอบการเชื่อมต่อ
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
}
if (!mqttClient.connected()) {
connectMQTT();
}
mqttClient.loop();
// อ่านเซ็นเซอร์ทุก 2 วินาที
unsigned long currentMillis = millis();
if (currentMillis - lastSensorRead >= SENSOR_READ_INTERVAL) {
lastSensorRead = currentMillis;
// อ่านข้อมูลเซ็นเซอร์
readSensors();
// อัปเดตหน้าจอ
updateDisplay();
// ส่งข้อมูลผ่าน MQTT
publishMQTTData();
// ตรวจสอบและแจ้งเตือน
checkAlerts();
// แสดงข้อมูลใน Serial Monitor
Serial.println("===== Air Quality Data =====");
Serial.print("PM2.5: "); Serial.print(currentData.pm25); Serial.println(" µg/m³");
Serial.print("PM10: "); Serial.print(currentData.pm10); Serial.println(" µg/m³");
Serial.print("CO2: "); Serial.print(currentData.co2); Serial.println(" ppm");
Serial.print("Temperature: "); Serial.print(currentData.temperature); Serial.println(" °C");
Serial.print("Humidity: "); Serial.print(currentData.humidity); Serial.println(" %");
Serial.print("AQI: "); Serial.println(currentData.aqi);
Serial.print("Status: "); Serial.println(currentData.status);
Serial.println("============================");
}
}📝 การติดตั้ง Library
ติดตั้ง libraries ที่จำเป็นใน PlatformIO:
pio lib install "Adafruit BME280 Library" pio lib install "SparkFun SCD4x Arduino Library" pio lib install "Adafruit SSD1306"
เชื่อมต่อ Home Assistant
หลังจากอัปโหลดโค้ดและเริ่มต้น ESP32 แล้ว เพิ่ม configuration ใน Home Assistant:
configuration.yaml
# MQTT Sensors for Air Quality Monitor
mqtt:
sensor:
- name: "Air Quality PM2.5"
state_topic: "home/airquality/pm25"
unit_of_measurement: "µg/m³"
device_class: "pm25"
- name: "Air Quality PM10"
state_topic: "home/airquality/pm10"
unit_of_measurement: "µg/m³"
device_class: "pm10"
- name: "Air Quality CO2"
state_topic: "home/airquality/co2"
unit_of_measurement: "ppm"
device_class: "carbon_dioxide"
- name: "Air Quality Temperature"
state_topic: "home/airquality/temperature"
unit_of_measurement: "°C"
device_class: "temperature"
- name: "Air Quality Humidity"
state_topic: "home/airquality/humidity"
unit_of_measurement: "%"
device_class: "humidity"
- name: "Air Quality Index"
state_topic: "home/airquality/aqi"
unit_of_measurement: "AQI"
# MQTT Alert Sensor
mqtt:
sensor:
- name: "Air Quality Alert"
state_topic: "home/airquality/alert"
value_template: " value_template: "{{ lbrace; value_template: "{{ lbrace; value_json.alert }}"
json_attributes_topic: "home/airquality/alert"
json_attributes_template: " json_attributes_template: "{{ lbrace; json_attributes_template: "{{ lbrace; value_json | tojson }}"Automations (automations.yaml)
# แจ้งเตือนเมื่อคุณภาพอากาศแย่
- alias: "Alert when air quality is poor"
trigger:
- platform: numeric_state
entity_id: sensor.air_quality_pm25
above: 50
action:
- service: notify.mobile_app_phone
data:
title: "⚠️ Air Quality Alert"
message: "PM2.5 is {{ states('sensor.air_quality_pm25') }} µg/m³"
# เปิดเครื่องฟอกอากาศเมื่อ CO2 สูง
- alias: "Turn on air purifier when CO2 is high"
trigger:
- platform: numeric_state
entity_id: sensor.air_quality_co2
above: 1000
action:
- service: switch.turn_on
entity_id: switch.air_purifier
# ปิดเมื่อ CO2 ลดลง
- alias: "Turn off air purifier when CO2 is normal"
trigger:
- platform: numeric_state
entity_id: sensor.air_quality_co2
below: 800
action:
- service: switch.turn_off
entity_id: switch.air_purifier📊 สร้าง Dashboard
สร้าง Lovelace card ใน Home Assistant เพื่อแสดงผลข้อมูลคุณภาพอากาศ โดยใช้:
- Gauge Card - แสดง AQI, PM2.5, CO2
- History Graph - ดูแนวโน้มคุณภาพอากาศ
- Markdown Card - แสดงคำแนะนำ
- Entities Card - รวมข้อมูลทั้งหมด
การสอบเทียบเซ็นเซอร์
🔧 การสอบเทียบ PMS5003
- เปิดเครื่องในบริเวณที่มีอากาศบริสุทธิ์ (นอกบ้านหรือใกล้หน้าต่าง)
- ปล่อยให้ทำงาน 5-10 นาทีเพื่อให้เซ็นเซอร์ stabilise
- เปรียบเทียบค่ากับเครื่องวัดอ้างอิง (ถ้ามี)
- บันทึกค่า offset และปรับในโค้ด
- สอบเทียบซ้ำทุก 6-12 เดือน
🔧 การสอบเทียบ SCD41
- SCD41 สอบเทียบตัวเองอัตโนมัติ (Auto-calibration)
- ต้องเปิดอากาศบริเวณที่มีอากาศสะอาด (400 ppm CO2) เป็นเวลา 7 วัน
- หลีกเยี่ยงจากการวางใกล้แหล่ง CO2 (คน,สัตว์เลี้ยง,เครื่องจักร)
- หากต้องการใช้แบบ manual calibration ใช้ฟังก์ชัน
performForcedCalibration()
💡 Tip: หากต้องการความแม่นยำสูง ควรสอบเทียบกับเครื่องวัดที่ได้รับการรับรองมาตรฐานอย่างน้อยปีละครั้ง
การออกแบบตู้บรรจุ
🎨 หลักการออกแบบ
- การระบายอากาศ - ต้องมีรูระบายอากาศให้เพียงพอเพื่อให้อากาศไหลผ่านเซ็นเซอร์
- ตำแหน่งเซ็นเซอร์ - PMS5003 ต้องวางในแนวดิ่งเพื่อให้ลูกศรชี้ขึ้น
- ตำแหน่งหน้าจอ - OLED ควรอยู่ด้านหน้าที่มองเห็นชัด
- การจัดการสายไฟ - ใช้ zip ties หรือ cable clips จัดระเบียบสาย
- Ventilation - ใช้ fan ขนาดเล็ก (optional) เพื่อกระตุ้นการไหลเวียนของอากาศ
🖨️ 3D Printing Case
หากมีเครื่อง 3D printer สามารถดาวน์โหลด design จาก:
- Thingiverse - ค้นหา "air quality monitor esp32"
- Printables - มีหลายแบบให้เลือก
- Cults3D - มีทั้งแบบง่ายและซับซ้อน
หรือใช้ กล่องพลาสติก หรือ ไม้ ตัดเอาง่ายๆ ก็ได้
สรุปและไอเดียต่อยอด
ในบทความนี้ เราได้สร้างเครื่องวัดคุณภาพอากาศ DIY ที่:
- ตรวจจับ PM2.5, PM10, CO2, อุณหภูมิ และความชื้น
- แสดงผลบน OLED display
- เชื่อมต่อกับ Home Assistant ผ่าน MQTT
- แจ้งเตือนอัตโนมัติเมื่อคุณภาพอากาศแย่
- ใช้งานได้จริงและปรับแต่งได้
🎯 ไอเดียต่อยอด
- เพิ่มเซ็นเซอร์ VOC - ตรวจจับก๊าซอินทรีย์ระเหย
- เพิ่ม buzzer - ส่งเสียงแจ้งเตือน
- เพิ่ม RGB LED - แสดงสถานะคุณภาพอากาศด้วยสี
- ใช้ ESP32-CAM - แสดงผลเป็นกราฟิกบนเว็บ
- เชื่อมต่อ Thingspeak - บันทึกข้อมูลระยะยาว
- สร้างแอปมือถือ - ดูข้อมูลได้ทุกที่
- ใช้แบตเตอรี่ + Solar - วางไว้นอกบ้านได้