เครื่องวัดฝุ่น PM2.5 ทำเองด้วย ESP32 + PMS7003 คุ้มกว่าซื้อไหม
ทำเครื่องวัดฝุ่น PM2.5 ด้วย ESP32 และเซนเซอร์เลเซอร์ PMS7003 แสดงค่าบนจอ OLED แบ่งระดับตามเกณฑ์กรมควบคุมมลพิษ แจ้งเตือนเข้า Telegram เทียบราคากับเครื่องใน Shopee
เครื่องวัดฝุ่น PM2.5 สำเร็จรูปใน Shopee ราคาราว ฿400–1,500 เสียบไฟแล้วดูค่าฝุ่นในห้องได้ทันที ถ้าทำเองด้วย ESP32 + เซนเซอร์ PMS7003 + จอ OLED ใช้เงินราว ฿690–1,160 ราคาใกล้กัน สิ่งที่ได้เพิ่มคือส่งค่าเข้ามือถือ แจ้งเตือนเมื่อฝุ่นเกิน เก็บค่าย้อนหลัง และปรับการทำงานเองได้ ถ้าต้องการแค่ดูตัวเลขในห้อง ซื้อสำเร็จสะดวกกว่า
ซื้อหรือทำเอง
ซื้อสำเร็จ: เครื่องวัด PM2.5 แบบมีจอ ราคาประมาณ ฿400–900 สำหรับเครื่องไม่มียี่ห้อ และ ฿1,000–1,500 สำหรับเครื่องมียี่ห้อ
ทำเอง: ใช้ของ 4 อย่าง รวมราว ฿690–1,160 (รายการพร้อมลิงก์อยู่ในตารางชิ้นส่วน)
- บอร์ด ESP32 DevKit: อ่านค่าเซนเซอร์ แสดงผล และต่อ WiFi
- เซนเซอร์ฝุ่นเลเซอร์ PMS7003 พร้อมบอร์ดแปลงสาย: วัด PM1.0, PM2.5 และ PM10
- จอ OLED SSD1306 0.96": แสดงค่าฝุ่นและระดับ
- สายจัมเปอร์ และอะแดปเตอร์ USB 5 V ของโทรศัพท์ที่มีอยู่แล้ว
ราคาเป็นค่าประมาณ ราคาจริงขึ้นกับร้านและช่วงเวลา ช่วงหน้าฝุ่น (ธันวาคม–เมษายน) เครื่องวัดและเซนเซอร์มักแพงขึ้น ลิงก์ในบทความนี้บางลิงก์เป็นลิงก์ affiliate (อ่านเพิ่ม)
โปรเจกต์วัดฝุ่นทำเองมีมานานแล้ว ที่รู้จักกันดีคือ Sensor.Community (เดิมชื่อ Luftdaten เริ่มที่เมืองชตุทการ์ต เยอรมนี ในปี 2015) ที่ชวนคนทั่วไปติดเครื่องวัดแล้วส่งค่าขึ้นแผนที่ร่วมกัน ในไทยมี DustBoy ของมหาวิทยาลัยเชียงใหม่ และ AirGradient ที่เริ่มทำเครื่องวัดแบบ open source ในไทย
PMS7003 วัดฝุ่นอย่างไร
พัดลมเล็กๆ ในเซนเซอร์ดูดอากาศผ่านลำแสงเลเซอร์ อนุภาคฝุ่นแต่ละเม็ดกระเจิงแสงเข้าตัวรับแสง เซนเซอร์นับจำนวนอนุภาคแยกตามขนาด แล้วคำนวณเป็นมวลฝุ่นต่อปริมาตร (µg/m³) ส่งออกมาสามค่า: PM1.0, PM2.5 และ PM10 ทุกราว 1 วินาที
- ความชื้นสูงทำให้ค่าเกินจริง เมื่อความชื้นเกินราว 70–80 % ละอองน้ำและฝุ่นที่อมน้ำจะถูกนับเป็นฝุ่นด้วย ค่าช่วงฝนตกหรือเช้ามืดจึงอาจสูงกว่าสถานีวัดของรัฐ
- พัดลมและเลเซอร์มีอายุจำกัด ถ้าเปิดตลอดเวลาจะเสื่อมเร็วขึ้น โค้ดด้านล่างจึงสั่งให้เซนเซอร์หลับเป็นช่วงๆ
- เทียบกับสถานีใกล้บ้าน เปิดเว็บหรือแอป Air4Thai ของกรมควบคุมมลพิษ ดูค่าสถานีที่ใกล้ที่สุดในวันที่อากาศแห้ง ถ้าค่าห่างกันมากตลอด ให้คูณค่าที่อ่านได้ด้วยตัวปรับแก้ เครื่องราคาถูกที่ไม่มีการปรับเทียบก็มีปัญหานี้เหมือนกัน
ชิ้นส่วน
| ชิ้นส่วน | จำนวน | ราคาประมาณ | ร้าน |
|---|---|---|---|
| ESP32 DevKit V1 (30 ขา) | 1 | ฿130–฿250 | Shopee |
| เซนเซอร์ฝุ่น PMS7003 + บอร์ดแปลงสาย หัวต่อของเซนเซอร์เป็นแบบ 1.27 mm ซื้อพร้อมบอร์ดแปลงเป็นขา 2.54 mm | 1 | ฿450–฿700 | Shopee |
| OLED SSD1306 0.96" I²C | 1 | ฿80–฿150 | Shopee |
| สายจัมเปอร์ ผู้-ผู้ / ผู้-เมีย ชุด 40 เส้น | 1 | ฿30–฿60 | Shopee |
| รวมโดยประมาณ | ฿690–฿1,160 | ||
ราคาประมาณการ ณ 2026-10-31 ยังไม่ได้ตรวจกับร้าน · บางลิงก์เป็นลิงก์ affiliate (อ่านเพิ่ม)
ใช้ไฟจากอะแดปเตอร์ USB 5 V 1 A ของโทรศัพท์ได้ เซนเซอร์กินกระแสราว 100 mA ตอนพัดลมหมุน ถ้าจะตั้งนอกบ้านต้องมีกล่องกันฝนที่อากาศเข้าออกได้ และไม่ให้แดดส่องตรง
ต่อสาย
| ขาบนบอร์ดแปลงของ PMS7003 | ESP32 | หมายเหตุ |
|---|---|---|
VCC | 5V (VIN) | พัดลมต้องใช้ 5 V ต่อ 3V3 แล้วพัดลมหมุนไม่พอ ค่าจะผิด |
GND | GND | |
TX | GPIO 16 (RX2) | ขาสัญญาณของ PMS7003 เป็นระดับ 3.3 V ต่อตรงกับ ESP32 ได้ ไม่ต้องแบ่งแรงดัน |
RX | GPIO 17 (TX2) | TX ต่อกับ RX ไขว้กัน |
SET | GPIO 4 | HIGH = ทำงาน, LOW = หลับ (พัดลมหยุด) ไม่ต่อก็ได้ เซนเซอร์จะทำงานตลอด |
RESET | ไม่ต่อ |
จอ OLED ต่อแบบเดียวกับบทความ ต่อจอ OLED SSD1306 กับ ESP32: VCC → 3V3, SDA → GPIO 21, SCL → GPIO 22
ข้อมูลที่ PMS7003 ส่งมา
เซนเซอร์ส่งข้อมูลทาง UART 9600 baud เป็นชุดละ 32 ไบต์ ขึ้นต้นด้วย 0x42 0x4D ทุกค่ายาว 2 ไบต์ (ไบต์สูงก่อน):
| ไบต์ | ข้อมูล |
|---|---|
| 0–1 | 0x42 0x4D หัวเฟรม |
| 2–3 | ความยาวที่เหลือ (28) |
| 4–9 | PM1.0, PM2.5, PM10 แบบ CF=1 (ค่าในห้องทดสอบของโรงงาน) |
| 10–15 | PM1.0, PM2.5, PM10 แบบบรรยากาศ ค่าที่ใช้กับอากาศทั่วไป |
| 16–27 | จำนวนอนุภาคใน 0.1 ลิตร แยกตามขนาด 0.3, 0.5, 1.0, 2.5, 5.0 และ 10 µm |
| 28–29 | สำรอง |
| 30–31 | checksum = ผลรวมของไบต์ 0–29 |
อ่านเองไม่ยาก โค้ดด้านล่างจึงไม่ใช้ไลบรารีสำหรับเซนเซอร์ และเช็ก checksum ทุกเฟรมเพื่อทิ้งข้อมูลที่เสีย
แบ่งสีตามเกณฑ์กรมควบคุมมลพิษ
| PM2.5 (µg/m³) | ระดับ | สี |
|---|---|---|
| 0–15 | ดีมาก | ฟ้า |
| 15.1–25 | ดี | เขียว |
| 25.1–37.5 | ปานกลาง | เหลือง |
| 37.6–75 | เริ่มมีผลกระทบต่อสุขภาพ | ส้ม |
| มากกว่า 75 | มีผลกระทบต่อสุขภาพ | แดง |
เกณฑ์ของกรมควบคุมมลพิษใช้ค่าเฉลี่ย 24 ชั่วโมง ค่าที่เครื่องอ่านได้แต่ละนาทีขึ้นลงได้มาก จึงใช้ดูแนวโน้มและเตือนล่วงหน้า ไม่ใช่ค่าที่เทียบกับประกาศของรัฐได้ตรงๆ
โค้ด (Arduino IDE)
ติดตั้งไลบรารี Adafruit SSD1306 จาก Library Manager ส่วน WiFi และ HTTPClient มากับบอร์ด ESP32 อยู่แล้ว แก้ชื่อ WiFi, รหัสผ่าน, token และ chat id ของ Telegram (วิธีหาอยู่ในหัวข้อถัดไป) ก่อนอัปโหลด
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const char* WIFI_SSID = "ชื่อ WiFi";
const char* WIFI_PASS = "รหัสผ่าน WiFi";
const char* BOT_TOKEN = "123456789:AA..."; // จาก @BotFather
const char* CHAT_ID = "123456789"; // จาก getUpdates
const int PMS_RX = 16, PMS_TX = 17, PMS_SET = 4;
const unsigned long CYCLE_MS = 5 * 60000UL; // วัดทุก 5 นาที
const unsigned long AWAKE_MS = 60000UL; // เปิดพัดลม 60 วินาทีต่อรอบ
const unsigned long WARMUP_MS = 30000UL; // 30 วินาทีแรกหลังตื่นยังไม่ใช้ค่า
const float ALERT_PM25 = 37.5, CLEAR_PM25 = 25; // เตือนเมื่อเกินส้ม หายเตือนเมื่อกลับเป็นเขียว
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
struct Dust { int pm1, pm25, pm10; };
/* ---------- PMS7003 ---------- */
// อ่านเฟรม 32 ไบต์ล่าสุดที่ checksum ถูก คืน true ถ้าได้ค่าใหม่
bool readDust(Dust &d) {
bool got = false;
while (Serial2.available() >= 32) {
if (Serial2.read() != 0x42 || Serial2.peek() != 0x4D) continue; // หาหัวเฟรม
uint8_t buf[32];
buf[0] = 0x42;
if (Serial2.readBytes(buf + 1, 31) != 31) break;
uint16_t sum = 0;
for (int i = 0; i < 30; i++) sum += buf[i];
auto u16 = [&](int i) { return (buf[i] << 8) | buf[i + 1]; };
if (sum != u16(30)) continue; // ข้อมูลเสีย ทิ้งเฟรมนี้
d.pm1 = u16(10); d.pm25 = u16(12); d.pm10 = u16(14); // ค่าแบบบรรยากาศ
got = true;
}
return got;
}
/* ---------- ระดับฝุ่น ---------- */
// ชื่อระดับบนจอเป็นภาษาอังกฤษ เพราะฟอนต์ในตัวไม่มีภาษาไทย
const char* levelName(float pm25) {
if (pm25 <= 15) return "Very good";
if (pm25 <= 25) return "Good";
if (pm25 <= 37.5) return "Moderate";
if (pm25 <= 75) return "Unhealthy (sensitive)";
return "Unhealthy";
}
void draw(const Dust &d, float avg25, bool sleeping) {
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
oled.setTextSize(1);
oled.setCursor(0, 0);
oled.print("PM2.5 ug/m3");
if (sleeping) oled.print(" zz");
oled.setTextSize(3);
oled.setCursor(0, 14);
oled.print(avg25, 0);
oled.setTextSize(1);
oled.setCursor(0, 42);
oled.print(levelName(avg25));
oled.setCursor(0, 54);
oled.printf("PM1 %d PM10 %d", d.pm1, d.pm10);
oled.display();
}
/* ---------- Telegram ---------- */
bool sendTelegram(const String &text) {
if (WiFi.status() != WL_CONNECTED) return false;
WiFiClientSecure client;
client.setInsecure(); // ไม่ตรวจใบรับรอง: ง่าย แต่ไม่ควรส่งข้อมูลลับ
HTTPClient http;
http.begin(client, String("https://api.telegram.org/bot") + BOT_TOKEN + "/sendMessage");
http.addHeader("Content-Type", "application/json");
int code = http.POST(String("{\"chat_id\":\"") + CHAT_ID + "\",\"text\":\"" + text + "\"}");
http.end();
return code == 200;
}
/* ---------- โปรแกรมหลัก ---------- */
Dust dust = { 0, 0, 0 };
float avg25 = -1; // ค่าเฉลี่ยเคลื่อนที่ของ PM2.5
bool alerted = false;
void setup() {
Serial.begin(115200);
Serial2.begin(9600, SERIAL_8N1, PMS_RX, PMS_TX);
pinMode(PMS_SET, OUTPUT);
Wire.begin(21, 22);
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
WiFi.begin(WIFI_SSID, WIFI_PASS); // ESP32 ต่อ WiFi ใหม่เองถ้าหลุด
}
void loop() {
unsigned long phase = millis() % CYCLE_MS;
bool awake = phase < AWAKE_MS;
digitalWrite(PMS_SET, awake ? HIGH : LOW);
if (readDust(dust) && awake && phase > WARMUP_MS) {
avg25 = avg25 < 0 ? dust.pm25 : avg25 * 0.9 + dust.pm25 * 0.1; // เกลี่ยค่าที่กระโดด
Serial.printf("PM1 %d PM2.5 %d PM10 %d avg %.1f\n", dust.pm1, dust.pm25, dust.pm10, avg25);
if (!alerted && avg25 > ALERT_PM25)
alerted = sendTelegram("PM2.5 ในห้อง " + String(avg25, 0) + " ug/m3 เกินระดับปานกลางแล้ว");
if (alerted && avg25 < CLEAR_PM25)
alerted = !sendTelegram("PM2.5 ลดลงเหลือ " + String(avg25, 0) + " ug/m3 แล้ว");
}
static unsigned long lastDraw = 0;
if (avg25 >= 0 && millis() - lastDraw > 1000) {
lastDraw = millis();
draw(dust, avg25, !awake);
}
}
- วัด 1 นาทีทุก 5 นาที พัดลมทำงานราว 20 % ของเวลา ช่วยยืดอายุเซนเซอร์ ถ้าต้องการค่าต่อเนื่อง ตั้ง
AWAKE_MSเท่ากับCYCLE_MS - เกณฑ์เตือนสองค่า เตือนเมื่อเกิน 37.5 และจะเตือนครั้งใหม่ได้ก็ต่อเมื่อลดลงต่ำกว่า 25 ก่อน ค่าที่แกว่งรอบ 37.5 จึงไม่ส่งข้อความรัวๆ
- ค่าเฉลี่ยเคลื่อนที่ แต่ละค่าใหม่มีน้ำหนัก 10 % ค่าบนจอจึงไม่กระโดดตามฝุ่นก้อนเดียวที่ลอยผ่าน
ตั้ง Telegram Bot สำหรับแจ้งเตือน
LINE Notify ที่บทความเก่าหลายบทใช้ ปิดบริการไปแล้วตั้งแต่ 31 มีนาคม 2025 โค้ดที่ส่งเข้า LINE Notify จึงใช้ไม่ได้อีก Telegram Bot ฟรีและตั้งค่าได้ในไม่กี่นาที:
- ในแอป Telegram ค้นหา @BotFather พิมพ์
/newbotตั้งชื่อบอต แล้วจะได้ token รูปแบบ123456789:AA… - เปิดแชตกับบอตที่สร้าง แล้วพิมพ์ข้อความอะไรก็ได้หนึ่งข้อความ
- เปิดเบราว์เซอร์ไปที่
https://api.telegram.org/bot<token>/getUpdatesหาตัวเลขหลัง"chat":{"id":นั่นคือ chat id - ใส่ token และ chat id ในโค้ด ถ้าจะส่งเข้ากลุ่มครอบครัว ให้เพิ่มบอตเข้ากลุ่ม พิมพ์ในกลุ่มหนึ่งครั้ง แล้วใช้ chat id ของกลุ่ม (ขึ้นต้นด้วยเครื่องหมายลบ)
token ของบอตใช้ส่งข้อความในนามบอตได้ อย่าโพสต์โค้ดที่มี token จริงขึ้นเว็บหรือ GitHub
ปัญหาที่พบบ่อย
- ไม่มีค่าใน Serial Monitor เลย TX / RX ไม่ไขว้กัน หรือพัดลมไม่หมุน (ฟังเสียงพัดลมเบาๆ ได้) ถ้าต่อขา SET แล้ว ให้เช็กว่าอยู่ในช่วงที่โค้ดสั่งให้ทำงาน
- ค่าเป็น 0 ตลอด อากาศในห้องที่เปิดเครื่องฟอกอากาศอาจต่ำจริง ลองเอาไปใกล้ธูปที่จุดแล้วดูว่าค่าขึ้นไหม
- บอร์ดรีเซ็ตตอนส่ง Telegram ไฟจากพอร์ต USB คอมพิวเตอร์บางเครื่องไม่พอสำหรับ WiFi + พัดลม เปลี่ยนเป็นอะแดปเตอร์ 5 V 1 A
- อัปโหลดโค้ดไม่ได้ ดู ESP32 อัปโหลดโค้ดไม่ได้
ต่อยอด
- เพิ่มปุ่มเปลี่ยนหน้าจอระหว่าง PM2.5, PM10 และจำนวนอนุภาค (ดู ต่อปุ่มกดกับ ESP32)
- รวมกับ นาฬิกาตั้งโต๊ะจอสี ให้จอเดียวแสดงทั้งเวลา สภาพอากาศ และฝุ่นในห้อง
- ส่งค่าขึ้น Sensor.Community เพื่อให้เครื่องของคุณขึ้นแผนที่คุณภาพอากาศสาธารณะ