เครื่องวัดฝุ่น 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 (รายการพร้อมลิงก์อยู่ในตารางชิ้นส่วน)

ราคาเป็นค่าประมาณ ราคาจริงขึ้นกับร้านและช่วงเวลา ช่วงหน้าฝุ่น (ธันวาคม–เมษายน) เครื่องวัดและเซนเซอร์มักแพงขึ้น ลิงก์ในบทความนี้บางลิงก์เป็นลิงก์ affiliate (อ่านเพิ่ม)

โปรเจกต์วัดฝุ่นทำเองมีมานานแล้ว ที่รู้จักกันดีคือ Sensor.Community (เดิมชื่อ Luftdaten เริ่มที่เมืองชตุทการ์ต เยอรมนี ในปี 2015) ที่ชวนคนทั่วไปติดเครื่องวัดแล้วส่งค่าขึ้นแผนที่ร่วมกัน ในไทยมี DustBoy ของมหาวิทยาลัยเชียงใหม่ และ AirGradient ที่เริ่มทำเครื่องวัดแบบ open source ในไทย

PMS7003 วัดฝุ่นอย่างไร

พัดลมเล็กๆ ในเซนเซอร์ดูดอากาศผ่านลำแสงเลเซอร์ อนุภาคฝุ่นแต่ละเม็ดกระเจิงแสงเข้าตัวรับแสง เซนเซอร์นับจำนวนอนุภาคแยกตามขนาด แล้วคำนวณเป็นมวลฝุ่นต่อปริมาตร (µg/m³) ส่งออกมาสามค่า: PM1.0, PM2.5 และ PM10 ทุกราว 1 วินาที

ชิ้นส่วน

ชิ้นส่วนจำนวนราคาประมาณร้าน
ESP32 DevKit V1 (30 ขา)1฿130–฿250Shopee
เซนเซอร์ฝุ่น PMS7003 + บอร์ดแปลงสาย
หัวต่อของเซนเซอร์เป็นแบบ 1.27 mm ซื้อพร้อมบอร์ดแปลงเป็นขา 2.54 mm
1฿450–฿700Shopee
OLED SSD1306 0.96" I²C1฿80–฿150Shopee
สายจัมเปอร์ ผู้-ผู้ / ผู้-เมีย ชุด 40 เส้น1฿30–฿60Shopee
รวมโดยประมาณ฿690–฿1,160

ราคาประมาณการ ณ 2026-10-31 ยังไม่ได้ตรวจกับร้าน · บางลิงก์เป็นลิงก์ affiliate (อ่านเพิ่ม)

ใช้ไฟจากอะแดปเตอร์ USB 5 V 1 A ของโทรศัพท์ได้ เซนเซอร์กินกระแสราว 100 mA ตอนพัดลมหมุน ถ้าจะตั้งนอกบ้านต้องมีกล่องกันฝนที่อากาศเข้าออกได้ และไม่ให้แดดส่องตรง

ต่อสาย

ขาบนบอร์ดแปลงของ PMS7003ESP32หมายเหตุ
VCC5V (VIN)พัดลมต้องใช้ 5 V ต่อ 3V3 แล้วพัดลมหมุนไม่พอ ค่าจะผิด
GNDGND
TXGPIO 16 (RX2)ขาสัญญาณของ PMS7003 เป็นระดับ 3.3 V ต่อตรงกับ ESP32 ได้ ไม่ต้องแบ่งแรงดัน
RXGPIO 17 (TX2)TX ต่อกับ RX ไขว้กัน
SETGPIO 4HIGH = ทำงาน, LOW = หลับ (พัดลมหยุด) ไม่ต่อก็ได้ เซนเซอร์จะทำงานตลอด
RESETไม่ต่อ

จอ OLED ต่อแบบเดียวกับบทความ ต่อจอ OLED SSD1306 กับ ESP32: VCC → 3V3, SDA → GPIO 21, SCL → GPIO 22

ข้อมูลที่ PMS7003 ส่งมา

เซนเซอร์ส่งข้อมูลทาง UART 9600 baud เป็นชุดละ 32 ไบต์ ขึ้นต้นด้วย 0x42 0x4D ทุกค่ายาว 2 ไบต์ (ไบต์สูงก่อน):

ไบต์ข้อมูล
0–10x42 0x4D หัวเฟรม
2–3ความยาวที่เหลือ (28)
4–9PM1.0, PM2.5, PM10 แบบ CF=1 (ค่าในห้องทดสอบของโรงงาน)
10–15PM1.0, PM2.5, PM10 แบบบรรยากาศ ค่าที่ใช้กับอากาศทั่วไป
16–27จำนวนอนุภาคใน 0.1 ลิตร แยกตามขนาด 0.3, 0.5, 1.0, 2.5, 5.0 และ 10 µm
28–29สำรอง
30–31checksum = ผลรวมของไบต์ 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);
  }
}

ตั้ง Telegram Bot สำหรับแจ้งเตือน

LINE Notify ที่บทความเก่าหลายบทใช้ ปิดบริการไปแล้วตั้งแต่ 31 มีนาคม 2025 โค้ดที่ส่งเข้า LINE Notify จึงใช้ไม่ได้อีก Telegram Bot ฟรีและตั้งค่าได้ในไม่กี่นาที:

  1. ในแอป Telegram ค้นหา @BotFather พิมพ์ /newbot ตั้งชื่อบอต แล้วจะได้ token รูปแบบ 123456789:AA…
  2. เปิดแชตกับบอตที่สร้าง แล้วพิมพ์ข้อความอะไรก็ได้หนึ่งข้อความ
  3. เปิดเบราว์เซอร์ไปที่ https://api.telegram.org/bot<token>/getUpdates หาตัวเลขหลัง "chat":{"id": นั่นคือ chat id
  4. ใส่ token และ chat id ในโค้ด ถ้าจะส่งเข้ากลุ่มครอบครัว ให้เพิ่มบอตเข้ากลุ่ม พิมพ์ในกลุ่มหนึ่งครั้ง แล้วใช้ chat id ของกลุ่ม (ขึ้นต้นด้วยเครื่องหมายลบ)

token ของบอตใช้ส่งข้อความในนามบอตได้ อย่าโพสต์โค้ดที่มี token จริงขึ้นเว็บหรือ GitHub

ปัญหาที่พบบ่อย

ต่อยอด