Đo chất lượng không khí trong phòng bằng ESP32: CO2 với SCD40, bụi PM2.5 với PMS5003, AQI giờ

Làm máy đo không khí trong phòng bằng ESP32: CO2 thật với SCD40, bụi PM2.5 với PMS5003, OLED, tính AQI giờ theo VN_AQI và NowCast, biết lúc nào mở cửa, lúc nào bật máy lọc.

Không khí trong phòng có hai thứ đáng đo nhất: CO2 cho biết phòng có đủ thông gió không (ngủ phòng kín dậy nặng đầu, học trong lớp đóng cửa buồn ngủ), và bụi mịn PM2.5 cho biết lúc nào nên bật máy lọc hoặc đóng cửa khi ngoài trời ô nhiễm. Bài này làm máy đo bằng ESP32 với cảm biến CO2 SCD40, cảm biến bụi PMS5003 và màn hình OLED 0.96 inch; chương trình tự đọc cả hai cảm biến không cần thư viện riêng và tính AQI giờ cho PM2.5 theo cách tính VN_AQI.

1. Chọn cảm biến

Bảng so sánh MQ135, SCD40, PMS5003 về nguyên lý, đại lượng đo được, độ chính xác, thời gian khởi động và công dụng
MQ135, SCD40 và PMS5003 đo những thứ khác nhau.
  • SCD40 (Sensirion) đo CO2 bằng nguyên lý quang âm: tia hồng ngoại đúng bước sóng CO2 hấp thụ làm khí trong buồng đo giãn nở, micro nhỏ nghe được "tiếng" đó. Kết quả là nồng độ CO2 thật (ppm), sai số ±(50 ppm + 5 %) trong khoảng 400–2000 ppm theo datasheet, kèm nhiệt độ và độ ẩm. Giao tiếp I2C địa chỉ 0x62.
  • PMS5003 (Plantower) có quạt hút không khí qua buồng đo, đếm hạt bụi bằng tia laser tán xạ, tự tính nồng độ PM1.0, PM2.5, PM10 (µg/m3). Giao tiếp UART 9600 baud, tự gửi số đo khoảng mỗi giây.
  • MQ135 không dùng ở đây. Phần tử oxit kim loại của nó phản ứng với nhiều loại khí và với độ ẩm, nên con số "ppm CO2" đổi từ đường cong mẫu chỉ là ước lượng; MQ135 hợp để báo có khí lạ hơn là đo CO2.

2. Chuẩn bị

3. Nối dây

Sơ đồ nối: SCD40 VDD 3V3, GND, SCL D22, SDA D21; PMS5003 VCC 5V vào VIN, GND, TX vào RX2, RX vào TX2; OLED GND, VCC 3V3, SCL D22, SDA D21
SCD40 và OLED chung I2C; PMS5003 dùng UART2.
  • Quạt của PMS5003 cần 5 V (lấy từ VIN khi cắm USB), còn chân TX, RX là mức 3,3 V nên nối thẳng vào ESP32.
  • Đặt PMS5003 sao cho cửa hút và cửa thổi không bị che, không đặt sát nguồn bụi (máy in, bếp) hay luồng gió mạnh.
  • Đặt SCD40 ở chỗ có không khí lưu thông, ngang tầm thở, xa cửa sổ đang mở và xa chỗ người ngồi thở trực tiếp vào cảm biến.

4. Hai cảm biến nói chuyện thế nào

SCD40 nhận lệnh 16 bit qua I2C. Chương trình gửi lệnh bắt đầu đo định kỳ (0x21B1), sau đó cứ mỗi giây hỏi "đã có số mới chưa" (0xE4B8); có rồi thì đọc 9 byte (0xEC05): CO2, nhiệt độ, độ ẩm, mỗi giá trị 2 byte kèm 1 byte CRC-8. Byte CRC giúp phát hiện dữ liệu bị nhiễu trên dây I2C.

PMS5003 tự gửi khung 32 byte:

Khung 32 byte của PMS5003: 0x42 0x4D, độ dài 28, PM1.0 PM2.5 PM10 chuẩn CF=1, PM1.0 PM2.5 PM10 theo khí quyển, số hạt theo cỡ, phiên bản, mã lỗi, tổng kiểm tra; ví dụ PM2.5 = 14 µg/m3, tổng 30 byte đầu 861 bằng 0x035D
Khung dữ liệu của PMS5003 và cách kiểm tra (số đo ví dụ).

Có hai bộ số PM: "CF=1" theo bụi chuẩn của nhà sản xuất và "khí quyển" cho không khí thường. Theo tài liệu của hãng, đo không khí xung quanh thì dùng bộ khí quyển; chương trình dùng bộ này.

5. Tính AQI giờ cho PM2.5

Ở Việt Nam, chỉ số chất lượng không khí VN_AQI tính theo Quyết định 1459/QĐ-TCMT năm 2019 của Tổng cục Môi trường. Với bụi PM2.5, AQI giờ không dùng nồng độ tức thời mà dùng giá trị NowCast từ trung bình 12 giờ gần nhất, rồi đổi sang thang 0–500 theo bảng ngưỡng:

Bảng ngưỡng PM2.5: 0–25 ứng AQI 0–50 tốt, 25–50 ứng 50–100 trung bình, 50–80 ứng 100–150 kém, 80–150 ứng 150–200 xấu, 150–250 ứng 200–300 rất xấu, trên 250 nguy hại; ví dụ 12 giá trị trung bình giờ, w bằng 0,5, NowCast 19,1 µg/m3, AQI 38 mức tốt
Bảng ngưỡng PM2.5 và ví dụ tính NowCast, AQI giờ.
  1. Mỗi giờ, chương trình lấy trung bình mọi số PM2.5 đã đọc trong giờ đó, đẩy vào mảng 12 giờ.
  2. Tính hệ số w* = nhỏ nhất / lớn nhất của 12 giá trị; nếu w* nhỏ hơn hoặc bằng 0,5 thì lấy w = 0,5. Giờ gần nhất có trọng số 1, giờ trước đó w, rồi w²…
  3. NowCast là trung bình có trọng số. Cần ít nhất 2 trong 3 giờ gần nhất có số liệu, nên sau 2 giờ chạy mới có AQI.
  4. Nội suy tuyến tính trong bảng ngưỡng để ra AQI, làm tròn thành số nguyên.

Hướng dẫn viết riêng công thức cho trường hợp w = 0,5; chương trình dùng chung một công thức chia cho tổng trọng số, khi đủ 12 giờ kết quả gần như trùng khớp. VN_AQI dùng cho trạm quan trắc ngoài trời; trong phòng, con số này dùng để so sánh và theo dõi, không thay cho số liệu chính thức.

6. Chương trình

// PYWORLD.VN - Đo chất lượng không khí trong phòng bằng ESP32: CO2 bằng SCD40, bụi PM2.5 bằng PMS5003, hiện trên OLED 0.96 inch
// PYWORLD.VN - SCD40 và OLED chung I2C (SDA D21, SCL D22); PMS5003: TX vào GPIO16, RX nối GPIO17, nguồn 5 V; đèn D2 nháy khi không khí kém
// PYWORLD.VN - Tính AQI giờ cho PM2.5 theo cách của VN_AQI: trung bình mỗi giờ, giá trị NowCast 12 giờ, bảng ngưỡng BP

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const int CHAN_RX_PMS = 16;                // nối chân TX của PMS5003
const int CHAN_TX_PMS = 17;                // nối chân RX của PMS5003
const int CHAN_DEN = 2;
const uint8_t DIA_CHI_SCD = 0x62;
const uint16_t CO2_MO_CUA = 1000;          // ppm: từ mức này đèn nháy nhắc thông gió
const uint32_t MOT_GIO_MS = 3600000;
const float BP_PM25[8] = {0, 25, 50, 80, 150, 250, 350, 500};   // µg/m3, bảng 2 của hướng dẫn VN_AQI
const int I_AQI[8] = {0, 50, 100, 150, 200, 300, 400, 500};

HardwareSerial pms(2);
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
bool coOled = false;
uint16_t co2 = 0;
float nhietDo = NAN, doAm = NAN;
uint16_t pm1 = 0, pm25 = 0, pm10 = 0;
bool coPms = false;
float trungBinhGio[12];                    // [0] là giờ vừa xong (c1), [11] cách đây 12 giờ (c12); NAN = không có
double tongGio = 0;
uint32_t demGio = 0, batDauGio = 0;
int soGioDaQua = 0;

// ---- SCD40: lệnh 16 bit, mỗi từ dữ liệu 16 bit kèm 1 byte CRC-8 (đa thức 0x31, giá trị đầu 0xFF) ----
uint8_t crc8(const uint8_t *d, int n) {
  uint8_t crc = 0xFF;
  for (int i = 0; i < n; i++) {
    crc ^= d[i];
    for (int b = 0; b < 8; b++) crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0x31) : (uint8_t)(crc << 1);
  }
  return crc;
}

bool lenhScd(uint16_t lenh) {
  Wire.beginTransmission(DIA_CHI_SCD);
  Wire.write(lenh >> 8);
  Wire.write(lenh & 0xFF);
  return Wire.endTransmission() == 0;
}

bool docScd(uint16_t lenh, uint16_t *tu, uint8_t soTu) {
  if (!lenhScd(lenh)) return false;
  delay(1);                                // chip cần 1 ms sau lệnh đọc
  uint8_t n = soTu * 3;
  if (Wire.requestFrom(DIA_CHI_SCD, n) != n) return false;
  for (uint8_t i = 0; i < soTu; i++) {
    uint8_t b[3] = {(uint8_t)Wire.read(), (uint8_t)Wire.read(), (uint8_t)Wire.read()};
    if (crc8(b, 2) != b[2]) return false;
    tu[i] = (b[0] << 8) | b[1];
  }
  return true;
}

void capNhatScd() {
  uint16_t sanSang;
  if (!docScd(0xE4B8, &sanSang, 1) || (sanSang & 0x07FF) == 0) return;   // 11 bit thấp bằng 0: chưa có số mới
  uint16_t w[3];
  if (!docScd(0xEC05, w, 3)) return;
  co2 = w[0];
  nhietDo = -45 + 175.0f * w[1] / 65535;
  doAm = 100.0f * w[2] / 65535;
}

// ---- PMS5003: khung 32 byte bắt đầu bằng 0x42 0x4D, tổng kiểm tra là tổng 30 byte đầu ----
uint8_t khung[32];
int viTri = 0;

bool docPms() {
  bool coMoi = false;
  while (pms.available()) {
    uint8_t b = pms.read();
    if (viTri == 0 && b != 0x42) continue;
    if (viTri == 1 && b != 0x4D) {
      viTri = 0;
      continue;
    }
    khung[viTri++] = b;
    if (viTri < 32) continue;
    viTri = 0;
    uint16_t tong = 0;
    for (int i = 0; i < 30; i++) tong += khung[i];
    if (tong != ((khung[30] << 8) | khung[31]) || ((khung[2] << 8) | khung[3]) != 28) continue;
    pm1 = (khung[10] << 8) | khung[11];    // dữ liệu 4-6: nồng độ theo điều kiện khí quyển
    pm25 = (khung[12] << 8) | khung[13];
    pm10 = (khung[14] << 8) | khung[15];
    coMoi = true;
  }
  return coMoi;
}

// ---- AQI giờ cho PM2.5 ----
float nowCast() {
  int co = 0;
  for (int i = 0; i < 3; i++) if (!isnan(trungBinhGio[i])) co++;
  if (co < 2) return NAN;                  // cần ít nhất 2 trong 3 giờ gần nhất
  float nho = 1e9, lon = -1;
  for (int i = 0; i < 12; i++) {
    if (isnan(trungBinhGio[i])) continue;
    nho = min(nho, trungBinhGio[i]);
    lon = max(lon, trungBinhGio[i]);
  }
  float w = lon > 0 ? nho / lon : 1;
  if (w <= 0.5f) w = 0.5f;
  float tuSo = 0, mauSo = 0, heSo = 1;     // giờ không có số liệu thì bỏ qua (trọng số 0)
  for (int i = 0; i < 12; i++) {
    if (!isnan(trungBinhGio[i])) {
      tuSo += heSo * trungBinhGio[i];
      mauSo += heSo;
    }
    heSo *= w;
  }
  return tuSo / mauSo;
}

int aqiPm25(float c) {
  if (c >= BP_PM25[7]) return 500;
  for (int i = 0; i < 7; i++) {
    if (c < BP_PM25[i + 1]) return lroundf((I_AQI[i + 1] - I_AQI[i]) / (BP_PM25[i + 1] - BP_PM25[i]) * (c - BP_PM25[i]) + I_AQI[i]);
  }
  return 500;
}

const char *mucAqi(int aqi) {
  if (aqi <= 50) return "Tot";
  if (aqi <= 100) return "Trung binh";
  if (aqi <= 150) return "Kem";
  if (aqi <= 200) return "Xau";
  if (aqi <= 300) return "Rat xau";
  return "Nguy hai";
}

const char *mucCo2(uint16_t ppm) {         // mức tham khảo thường dùng cho không khí trong phòng
  if (ppm < 800) return "tot";
  if (ppm < 1000) return "nen thong gio";
  if (ppm < 1500) return "can mo cua";
  return "kem, mo cua ngay";
}

void ketThucGio() {
  for (int i = 11; i > 0; i--) trungBinhGio[i] = trungBinhGio[i - 1];
  trungBinhGio[0] = demGio ? tongGio / demGio : NAN;
  tongGio = 0;
  demGio = 0;
  soGioDaQua++;
  float nc = nowCast();
  Serial.printf("Het gio thu %d: PM2.5 trung binh %.1f", soGioDaQua, trungBinhGio[0]);
  if (isnan(nc)) Serial.println(", chua du 2 gio de tinh NowCast");
  else Serial.printf(", NowCast %.1f, AQI gio %d (%s)\n", nc, aqiPm25(nc), mucAqi(aqiPm25(nc)));
}

void veOled() {
  if (!coOled) return;
  float nc = nowCast();
  oled.clearDisplay();
  oled.setTextColor(SSD1306_WHITE);
  oled.setTextSize(1);
  oled.setCursor(0, 0);
  oled.print("CO2 ppm");
  oled.setTextSize(2);
  oled.setCursor(56, 0);
  oled.print(co2);
  oled.setTextSize(1);
  oled.setCursor(0, 20);
  if (coPms) oled.printf("PM2.5 %u ug/m3", pm25);
  else oled.print("PM2.5: cho cam bien");
  oled.setCursor(0, 32);
  if (isnan(nc)) oled.print("AQI gio: sau 2 gio");
  else oled.printf("AQI gio %d %s", aqiPm25(nc), mucAqi(aqiPm25(nc)));
  oled.setCursor(0, 44);
  if (!isnan(nhietDo)) oled.printf("%.1f C  %.0f %%", nhietDo, doAm);
  oled.setCursor(0, 56);
  if (co2 >= CO2_MO_CUA) oled.print("Nen mo cua thong gio");
  else if (!isnan(nc) && aqiPm25(nc) > 100) oled.print("Bui cao: bat may loc");
  oled.display();
}

void setup() {
  Serial.begin(115200);
  pinMode(CHAN_DEN, OUTPUT);
  for (int i = 0; i < 12; i++) trungBinhGio[i] = NAN;
  pms.begin(9600, SERIAL_8N1, CHAN_RX_PMS, CHAN_TX_PMS);   // PMS5003: 9600 baud, tự gửi khung khoảng mỗi giây
  Wire.begin(21, 22);
  coOled = oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  lenhScd(0x3F86);                         // dừng đo (nếu chip đang đo từ lần chạy trước)
  delay(500);
  if (lenhScd(0x21B1)) Serial.println("SCD40: bat dau do, 5 giay mot lan");   // đo định kỳ
  else Serial.println("Khong thay SCD40 o 0x62: kiem tra day I2C");
  batDauGio = millis();
}

void loop() {
  if (docPms()) {
    coPms = true;
    tongGio += pm25;
    demGio++;
  }
  static uint32_t lanDoc = 0, lanIn = 0;
  if (millis() - lanDoc >= 1000) {
    lanDoc = millis();
    capNhatScd();
    veOled();
  }
  if (millis() - lanIn >= 5000) {
    lanIn = millis();
    float nc = nowCast();
    Serial.printf("CO2 %u ppm (%s), %.1f C, %.0f %% | PM1.0 %u, PM2.5 %u, PM10 %u ug/m3", co2, mucCo2(co2), nhietDo, doAm, pm1, pm25, pm10);
    if (isnan(nc)) Serial.println();
    else Serial.printf(" | AQI gio %d\n", aqiPm25(nc));
  }
  if (millis() - batDauGio >= MOT_GIO_MS) {
    batDauGio += MOT_GIO_MS;
    ketThucGio();
  }
  float nc = nowCast();
  bool kem = co2 >= CO2_MO_CUA || (!isnan(nc) && aqiPm25(nc) > 100);
  digitalWrite(CHAN_DEN, kem && (millis() / 500) % 2);   // nháy 1 lần mỗi giây
}

Giải thích

  • docScd() gửi lệnh, chờ 1 ms, đọc từng từ 3 byte và kiểm tra CRC-8 (đa thức 0x31, giá trị đầu 0xFF). Nhiệt độ đổi theo công thức của datasheet: −45 + 175 × số đọc / 65535; độ ẩm 100 × số đọc / 65535.
  • docPms() đọc từng byte: chờ đúng 0x42 rồi 0x4D mới bắt đầu gom, đủ 32 byte thì kiểm tra độ dài 28 và tổng kiểm tra. Khung lỗi bị bỏ, không làm sai số đo.
  • ketThucGio() chạy mỗi giờ: tính trung bình giờ, dịch mảng 12 giờ, in NowCast và AQI giờ.
  • Đèn D2 nháy khi CO2 từ 1000 ppm hoặc AQI giờ trên 100. Màn hình ghi rõ nên làm gì: CO2 cao thì mở cửa thông gió; bụi cao thì bật máy lọc (mở cửa lúc ngoài trời đang ô nhiễm sẽ làm bụi trong phòng tăng thêm).
  • SCD40 mặc định bật tự hiệu chuẩn: nó giả định trong mỗi tuần có lúc được thông gió ra không khí ngoài trời (khoảng 400 ppm). Phòng đóng kín cả tuần thì số đo có thể trôi; khi đó nên mở cửa định kỳ.

7. Kết quả và cách đọc

Serial Monitor: SCD40 bắt đầu đo; dòng CO2 812 ppm nên thông gió, 27,4 độ C, 68 %, PM2.5 14; hết giờ thứ nhất chưa đủ để tính NowCast; hết giờ thứ hai NowCast 15,6, AQI giờ 31 mức tốt; CO2 1124 ppm cần mở cửa
Serial Monitor (số đo ví dụ; NowCast và AQI tính đúng như chương trình).

Các mức CO2 thường dùng để tham khảo cho không khí trong phòng (không phải giới hạn an toàn):

CO2 (ppm)Ý nghĩa
khoảng 420Không khí ngoài trời
dưới 800Phòng thông gió tốt
800–1000Nên thông gió
1000–1500Thiếu thông gió, dễ buồn ngủ, kém tập trung: mở cửa
trên 1500Không khí kém: mở cửa ngay, bật quạt hút
Đồ thị mô hình CO2 trong phòng ngủ 30 mét khối có hai người từ 22 giờ tới 6 giờ: đóng kín cửa CO2 lên hơn 3000 ppm, hé cửa phòng khoảng 1000 ppm, bật quạt hút khoảng 700 ppm
CO2 trong phòng ngủ qua một đêm với ba mức trao đổi khí (mô hình, không phải số đo).

Phòng ngủ đóng kín và bật điều hòa gần như không trao đổi khí với bên ngoài, vì máy điều hòa treo tường chỉ làm lạnh không khí trong phòng, không đưa khí tươi vào. Đặt máy đo một đêm sẽ thấy rõ CO2 tăng thế nào và hé cửa, quạt hút giúp được bao nhiêu.

8. Lỗi hay gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
"Khong thay SCD40 o 0x62"Sai dây SDA/SCL, module chưa có nguồnChạy chương trình quét I2C; kiểm tra VDD
CO2 đứng yên ở 0SCD40 chưa bắt đầu đo, hoặc CRC sai do dây dàiDây I2C ngắn dưới khoảng 30 cm; khởi động lại
CO2 ngoài trời đo được 500–600 ppmCảm biến mới chưa tự hiệu chuẩn xong, hoặc gần chỗ người thởChạy liên tục vài ngày, có thông gió; tránh thở vào cảm biến
PM2.5 luôn bằng 0Nhầm TX, RX; quạt không quay vì thiếu 5 VTX của PMS5003 vào GPIO16; đo 5 V ở chân VCC
PM2.5 rất cao lúc trời nồmĐộ ẩm rất cao làm hạt nước bị đếm như bụiSố đo lúc độ ẩm trên khoảng 90 % chỉ để tham khảo
Nhiệt độ của SCD40 cao hơn thực tếCảm biến tự nóng và gần ESP32Đặt xa ESP32; nhiệt độ chỉ là số phụ

9. Hướng mở rộng

  • Thêm trang web và đồ thị 24 giờ như bài trạm thời tiết mini cùng nhóm dự án.
  • Điều khiển quạt hút qua module relay: CO2 trên 1000 ppm thì bật, dưới 800 ppm thì tắt.
  • Để tiết kiệm tuổi thọ quạt và laser của PMS5003, kéo chân SET xuống thấp để cảm biến ngủ, mỗi vài phút đánh thức 30 giây để đo.

10. Xem thêm


Tin tức liên quan

Đo tốc độ động cơ bằng encoder và bộ đếm xung PCNT của ESP32: vòng/phút và số vòng
Đo tốc độ động cơ bằng encoder và bộ đếm xung PCNT của ESP32: vòng/phút và số vòng

Hướng dẫn đọc encoder Hall hai kênh của động cơ DC bằng bộ đếm xung PCNT của ESP32: đếm 4 cạnh, tính vòng/phút và số vòng, lập bảng tốc độ theo duty trong giờ học STEM.

Hướng dẫn giao tiếp I2C trên ESP32: quét địa chỉ, đổi chân SDA/SCL và nối nhiều thiết bị
Hướng dẫn giao tiếp I2C trên ESP32: quét địa chỉ, đổi chân SDA/SCL và nối nhiều thiết bị

Hướng dẫn dùng bus I2C trên ESP32 với Arduino IDE: quét địa chỉ thiết bị, đổi chân SDA và SCL, nối màn hình OLED và ADS1115 chung một bus, xử lý khi hai thiết bị trùng địa chỉ.

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.

Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32
Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32

Hướng dẫn dùng Bluetooth Classic của ESP32 trên Arduino IDE: ghép đôi với điện thoại Android, gửi nhận chữ bằng BluetoothSerial và điều khiển module relay bằng lệnh ON, OFF.

Ghi dữ liệu ESP32 lên Google Sheets bằng Apps Script: điện áp pin, Wi-Fi, không cần máy chủ riêng
Ghi dữ liệu ESP32 lên Google Sheets bằng Apps Script: điện áp pin, Wi-Fi, không cần máy chủ riêng

Tạo bảng tính, triển khai Apps Script thành web app, đo pin 18650 qua cầu phân áp và chương trình ESP32 gửi JSON lên Google Sheets mỗi phút, kèm bảng xử lý lỗi.

Hướng dẫn dùng ngắt GPIO trên ESP32: đọc nút nhấn, chống dội và đếm xung
Hướng dẫn dùng ngắt GPIO trên ESP32: đọc nút nhấn, chống dội và đếm xung

Hướng dẫn dùng ngắt GPIO trên ESP32 với Arduino IDE: attachInterrupt, các kiểu kích ngắt, chống dội nút nhấn, đếm xung đo tần số và quy tắc viết hàm ngắt an toàn.

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.

Cảm biến nhịp tim MAX30102 với ESP32: tín hiệu PPG, đếm nhịp không cần thư viện và giới hạn của SpO2
Cảm biến nhịp tim MAX30102 với ESP32: tín hiệu PPG, đếm nhịp không cần thư viện và giới hạn của SpO2

Nguyên lý PPG, nối MAX30102 với ESP32, cấu hình thanh ghi, lọc tín hiệu và đếm nhịp tim không cần thư viện, tỉ số R của SpO2 và vì sao kết quả tự làm chỉ để học.


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