Мониторинг качества воздуха: CO2, PM2.5 и VOC своими руками на ESP32

Соберите систему мониторинга качества воздуха своими руками на ESP32. Измеряйте CO2, PM2.5 и VOC. Подробное руководство: схема, код, интеграция с Home Assistant.
Введение
Качество воздуха в помещении — вещь, о которой обычно не задумываешься, пока не начинаешь смотреть на реальные цифры. Особенно это заметно в небольшой комнате или домашней мастерской: окна закрыты, работает техника, иногда используется химия, а через несколько часов становится душно.
Самый очевидный параметр здесь — CO2, но одним углекислым газом картина не ограничивается. В воздухе могут находиться мелкие частицы PM2.5, а краски, растворители, чистящие средства, мебель и некоторые виды пластика могут быть источниками летучих органических соединений.
В этой статье соберём на ESP32 небольшую станцию, которая будет измерять:
- реальную концентрацию CO2;
- PM1.0, PM2.5 и PM10;
- TVOC;
- температуру и влажность.
Начнём с локального вывода показаний через Serial Monitor, а затем разберём, как передавать их по MQTT, например в Home Assistant.
Что именно будем измерять
Перед сборкой стоит разобраться, что показывают наши датчики. CO2, PM2.5 и VOC — совершенно разные параметры, и один сенсор не может полноценно заменить другой.
CO2
Концентрация углекислого газа измеряется в ppm — количестве молекул CO2 на миллион молекул воздуха.
В обычной комнате CO2 хорошо показывает эффективность вентиляции. Когда людей много, а притока свежего воздуха мало, концентрация постепенно увеличивается.
Условно можно ориентироваться на следующие значения:
- около 400–600 ppm — уровень, близкий к свежему наружному воздуху;
- 600–1000 ppm — нормальный уровень для помещения;
- выше 1000 ppm — уже имеет смысл обратить внимание на вентиляцию;
- 1500 ppm и выше — помещение явно стоит проветрить.
Это не датчик «ядовитости воздуха», а прежде всего хороший индикатор вентиляции.
PM2.5
PM2.5 — частицы размером до 2,5 мкм.
Источниками могут быть:
- дым;
- аэрозоли;
- пыль;
- приготовление пищи;
- работа некоторых инструментов;
- процессы горения.
Из-за очень небольшого размера такие частицы способны проникать глубоко в дыхательные пути, поэтому их концентрация является важным показателем качества воздуха.
Для проекта используем Plantower PMS5003. Внутри него есть небольшой вентилятор, который прокачивает воздух через оптическую измерительную камеру.
Кроме PM2.5 датчик выдаёт PM1.0 и PM10.
VOC
VOC — volatile organic compounds, или летучие органические соединения.
Их источниками могут быть:
- растворители;
- спирты;
- краски;
- клеи;
- чистящие средства;
- новая мебель;
- некоторые пластики и покрытия.
В проекте используется SGP30.
Здесь есть важный нюанс: SGP30 не измеряет настоящий CO2. Значение eCO2, которое он выдаёт, вычисляется алгоритмом на основании показаний газового сенсора.
Поэтому для настоящего измерения CO2 мы используем отдельный NDIR-датчик SCD30.
Что понадобится
Для сборки используем следующие компоненты.
1. ESP32 DevKit
ESP32 будет центральным контроллером устройства.
У него уже есть:
- Wi-Fi;
- несколько UART;
- I2C;
- достаточная производительность;
- большое количество GPIO.
Для такой задачи ESP32 подходит с большим запасом.
Если интересно сравнение с предыдущим поколением, можно посмотреть статью ESP32 vs ESP8266: что выбрать для своего DIY-проекта.
2. Sensirion SCD30
SCD30 — NDIR-датчик настоящего CO2.
Он измеряет:
- CO2;
- температуру;
- относительную влажность.
Диапазон измерения CO2 составляет примерно 400–10000 ppm.
В отличие от дешёвых VOC-сенсоров, концентрация CO2 здесь определяется непосредственно оптическим методом.
3. Plantower PMS5003
Оптический датчик взвешенных частиц.
Получаем:
- PM1.0;
- PM2.5;
- PM10.
Связь с ESP32 выполняется через UART.
4. SGP30
SGP30 используем для оценки TVOC.
Если используется готовый модуль, например breakout-плата Adafruit или аналогичная плата со стабилизатором и преобразованием уровней, её можно подключать к ESP32 согласно документации конкретного модуля.
Важно не путать готовый модуль и сам чип SGP30: непосредственно микросхема SGP30 рассчитана примерно на 1,8 В.
5. Остальное
Также понадобятся:
- макетная плата;
- соединительные провода;
- источник питания 5 В;
- USB-кабель;
- корпус, если устройство собирается стационарно.
Для постоянной эксплуатации лучше не оставлять всё на Dupont-проводах, а собрать устройство на монтажной или собственной печатной плате.
Схема подключения
В этом варианте используем стандартные GPIO ESP32 DevKit V1.
I2C
SCD30 и SGP30 работают на одной I2C-шине:
ESP32 GPIO21 (SDA)
├── SCD30 SDA
└── SGP30 SDA
ESP32 GPIO22 (SCL)
├── SCD30 SCL
└── SGP30 SCL
GND
├── SCD30 GND
└── SGP30 GND
PMS5003
Для PMS5003 используем отдельный UART:
PMS5003 TX -> ESP32 GPIO16 (RX)
PMS5003 RX -> ESP32 GPIO17 (TX)
PMS5003 GND -> ESP32 GND
PMS5003 VCC -> 5V
TX одного устройства всегда подключается к RX другого.
Питание
PMS5003 питаем от 5 В.
SCD30 допускает питание в диапазоне 3,3–5,5 В, поэтому его можно питать от 5 В.
С SGP30 нужно быть внимательнее. Если используется готовая breakout-плата, смотрите маркировку и документацию именно на неё. Сам сенсор SGP30 является низковольтным устройством, поэтому подавать произвольное напряжение непосредственно на чип нельзя.
Все устройства должны иметь общую землю.
Нужны ли дополнительные подтягивающие резисторы I2C
На большинстве готовых модулей подтяжки SDA/SCL уже установлены.
Поэтому сначала стоит проверить конкретные платы. Добавлять ещё пару резисторов 4,7 кОм автоматически не нужно: несколько параллельных подтяжек могут дать слишком маленькое итоговое сопротивление.
Устанавливаем PlatformIO
Проект будем собирать через PlatformIO.
Нам понадобятся:
- VS Code;
- расширение PlatformIO;
- новый проект для
ESP32 Dev Module.
После создания проекта открываем файл:
platformio.ini
и приводим его к следующему виду:
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
sparkfun/SparkFun SCD30 Arduino Library @ ^1.0.20
adafruit/Adafruit SGP30 Sensor @ ^2.0.3
avaldebe/PMserial @ ^1.5.0
PlatformIO самостоятельно скачает библиотеки вместе с необходимыми зависимостями.
Прошивка ESP32
Теперь сделаем нормальную рабочую версию прошивки.
SGP30 желательно опрашивать регулярно, поэтому для него используем отдельный интервал. SCD30 и PMS5003 можно читать реже.
Создаём:
src/main.cpp
и вставляем:
#include <Arduino.h>
#include <Wire.h>
#include <SparkFun_SCD30_Arduino_Library.h>
#include <Adafruit_SGP30.h>
#include <PMserial.h>
// --------------------------------------------------
// Датчики
// --------------------------------------------------
SCD30 scd30;
Adafruit_SGP30 sgp30;
// PMS5003:
// GPIO16 = RX ESP32, подключается к TX датчика
// GPIO17 = TX ESP32, подключается к RX датчика
SerialPM pms(PMSx003, 16, 17);
// --------------------------------------------------
// Последние полученные значения
// --------------------------------------------------
uint16_t co2 = 0;
float temperature = 0.0;
float humidity = 0.0;
uint16_t tvoc = 0;
uint16_t eco2 = 0;
uint16_t pm1 = 0;
uint16_t pm25 = 0;
uint16_t pm10 = 0;
// --------------------------------------------------
// Таймеры
// --------------------------------------------------
unsigned long lastSGPRead = 0;
unsigned long lastSCDRead = 0;
unsigned long lastPMSRead = 0;
const unsigned long SGP_INTERVAL = 1000;
const unsigned long SCD_INTERVAL = 2000;
const unsigned long PMS_INTERVAL = 5000;
// --------------------------------------------------
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.println();
Serial.println("ESP32 Air Monitor");
Serial.println("-----------------");
// I2C: SDA = GPIO21, SCL = GPIO22
Wire.begin(21, 22);
// --------------------------------------------------
// SCD30
// --------------------------------------------------
Serial.print("SCD30: ");
if (!scd30.begin())
{
Serial.println("ERROR");
Serial.println("Проверьте питание и I2C.");
while (true)
{
delay(1000);
}
}
Serial.println("OK");
// Интервал измерения SCD30 — 2 секунды
scd30.setMeasurementInterval(2);
// --------------------------------------------------
// SGP30
// --------------------------------------------------
Serial.print("SGP30: ");
if (!sgp30.begin())
{
Serial.println("ERROR");
Serial.println("Проверьте питание и I2C.");
while (true)
{
delay(1000);
}
}
Serial.println("OK");
// --------------------------------------------------
// PMS5003
// --------------------------------------------------
Serial.print("PMS5003: ");
pms.init();
Serial.println("инициализирован");
Serial.println();
Serial.println("Запуск измерений...");
Serial.println();
}
// --------------------------------------------------
void loop()
{
unsigned long now = millis();
// ==================================================
// SGP30
// ==================================================
if (now - lastSGPRead >= SGP_INTERVAL)
{
lastSGPRead = now;
if (sgp30.IAQmeasure())
{
tvoc = sgp30.TVOC;
eco2 = sgp30.eCO2;
Serial.printf(
"[SGP30] TVOC: %u ppb | eCO2: %u ppmn",
tvoc,
eco2
);
}
else
{
Serial.println("[SGP30] Ошибка чтения");
}
}
// ==================================================
// SCD30
// ==================================================
if (now - lastSCDRead >= SCD_INTERVAL)
{
lastSCDRead = now;
if (scd30.dataAvailable())
{
co2 = scd30.getCO2();
temperature = scd30.getTemperature();
humidity = scd30.getHumidity();
Serial.printf(
"[SCD30] CO2: %u ppm | T: %.1f C | RH: %.1f %%n",
co2,
temperature,
humidity
);
}
}
// ==================================================
// PMS5003
// ==================================================
if (now - lastPMSRead >= PMS_INTERVAL)
{
lastPMSRead = now;
pms.read();
if (pms)
{
pm1 = pms.pm01;
pm25 = pms.pm25;
pm10 = pms.pm10;
Serial.printf(
"[PMS5003] PM1.0: %u | PM2.5: %u | PM10: %u ug/m3n",
pm1,
pm25,
pm10
);
}
else
{
Serial.println("[PMS5003] Нет корректных данных");
}
}
delay(10);
}
После загрузки прошивки открываем Serial Monitor со скоростью:
115200
Если всё подключено правильно, вывод будет примерно таким:
ESP32 Air Monitor
-----------------
SCD30: OK
SGP30: OK
PMS5003: инициализирован
Запуск измерений...
[SGP30] TVOC: 12 ppb | eCO2: 415 ppm
[SCD30] CO2: 687 ppm | T: 23.8 C | RH: 41.2 %
[SGP30] TVOC: 14 ppb | eCO2: 420 ppm
[PMS5003] PM1.0: 3 | PM2.5: 5 | PM10: 7 ug/m3
Обратите внимание на два разных значения CO2:
SCD30 CO2
SGP30 eCO2
Для оценки реальной концентрации углекислого газа используем только показания SCD30.
eCO2 от SGP30 можно считать дополнительным расчётным параметром.
Проверяем датчики
Перед установкой станции имеет смысл проверить каждый канал отдельно.
Проверка CO2
Можно сделать несколько выдохов рядом с входными отверстиями SCD30.
Не нужно дуть непосредственно внутрь сенсора с расстояния пары сантиметров. Достаточно изменить состав воздуха рядом с ним.
Через некоторое время значение CO2 должно заметно увеличиться.
После проветривания оно постепенно вернётся к обычному уровню.
Проверка PMS5003
Лучший вариант — просто посмотреть на реакцию датчика на реальные источники частиц.
Например:
- приготовление пищи;
- дым;
- аэрозоль;
- пыль при уборке или работе инструмента.
Создавать облако пыли непосредственно перед вентилятором датчика не нужно.
Если рядом появился источник аэрозольных частиц, PM2.5 и PM10 должны отреагировать.
Проверка VOC
SGP30 хорошо реагирует на многие летучие вещества.
Для проверки можно на некотором расстоянии открыть ёмкость с изопропиловым или этиловым спиртом.
Подносить жидкость непосредственно к плате и тем более проливать её на электронику не нужно.
TVOC должен заметно увеличиться.
Калибровка и особенности SCD30
У SCD30 есть автоматическая самокалибровка ASC.
Принцип её работы предполагает, что датчик периодически оказывается в помещении с воздухом, концентрация CO2 в котором приближается к наружному уровню.
Поэтому для постоянно закрытого помещения автоматическая калибровка может оказаться не лучшим вариантом.
Если устройство постоянно работает в жилой комнате, которая регулярно проветривается, ASC обычно имеет смысл оставить включённой.
Также SCD30 позволяет учитывать:
- высоту над уровнем моря;
- атмосферное давление;
- температурное смещение.
Для обычного домашнего мониторинга это необязательно, но при необходимости точность можно дополнительно улучшить.
Особенности SGP30
SGP30 требует времени для стабилизации.
После первого запуска не стоит делать выводы о качестве воздуха по первым нескольким минутам работы.
Кроме того, сенсор поддерживает baseline — базовые значения алгоритма, которые можно периодически сохранять в энергонезависимую память ESP32 и восстанавливать после перезапуска.
Для постоянно работающей станции это имеет смысл реализовать отдельно.
Ещё раз напомню: значение eCO2 у SGP30 не является измерением CO2 методом NDIR и не заменяет SCD30.
Добавляем Wi-Fi и MQTT
Локальный Serial Monitor хорош для проверки, но постоянно подключать компьютер к ESP32 бессмысленно.
Гораздо интереснее отправлять данные в MQTT.
После этого их можно использовать:
- в Home Assistant;
- Grafana;
- Node-RED;
- собственном веб-интерфейсе;
- системе хранения истории показаний.
Для MQTT добавляем в platformio.ini:
lib_deps =
sparkfun/SparkFun SCD30 Arduino Library @ ^1.0.20
adafruit/Adafruit SGP30 Sensor @ ^2.0.3
avaldebe/PMserial @ ^1.5.0
knolleary/PubSubClient @ ^2.8
Теперь добавляем:
#include <WiFi.h>
#include <PubSubClient.h>
Настройки сети:
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_PASSWORD";
const char* MQTT_HOST = "192.168.1.100";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = "airmonitor";
const char* MQTT_PASSWORD = "password";
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
Подключение Wi-Fi:
void connectWiFi()
{
if (WiFi.status() == WL_CONNECTED)
return;
Serial.printf("Wi-Fi: подключение к %s", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Wi-Fi подключён. IP: ");
Serial.println(WiFi.localIP());
}
Подключение MQTT:
void connectMQTT()
{
while (!mqtt.connected())
{
Serial.print("MQTT: подключение... ");
if (mqtt.connect(
"ESP32-AirMonitor",
MQTT_USER,
MQTT_PASSWORD))
{
Serial.println("OK");
}
else
{
Serial.printf(
"ошибка %d, повтор через 5 секундn",
mqtt.state()
);
delay(5000);
}
}
}
В setup():
connectWiFi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
connectMQTT();
В начале loop():
if (WiFi.status() != WL_CONNECTED)
{
connectWiFi();
}
if (!mqtt.connected())
{
connectMQTT();
}
mqtt.loop();
А после получения показаний можно публиковать их так:
char value[32];
snprintf(value, sizeof(value), "%u", co2);
mqtt.publish("airmonitor/co2", value, true);
snprintf(value, sizeof(value), "%.1f", temperature);
mqtt.publish("airmonitor/temperature", value, true);
snprintf(value, sizeof(value), "%.1f", humidity);
mqtt.publish("airmonitor/humidity", value, true);
snprintf(value, sizeof(value), "%u", tvoc);
mqtt.publish("airmonitor/tvoc", value, true);
snprintf(value, sizeof(value), "%u", pm25);
mqtt.publish("airmonitor/pm25", value, true);
snprintf(value, sizeof(value), "%u", pm10);
mqtt.publish("airmonitor/pm10", value, true);
В MQTT появятся топики:
airmonitor/co2
airmonitor/temperature
airmonitor/humidity
airmonitor/tvoc
airmonitor/pm25
airmonitor/pm10
Дальше их уже можно подключать к Home Assistant или сохранять в базу данных.
Размещение датчиков в корпусе
На практике корпус влияет на результат сильнее, чем может показаться.
Не стоит просто складывать все датчики вплотную друг к другу в закрытую коробку.
Особенно важно учитывать нагрев ESP32.
Если SCD30 находится прямо над стабилизатором питания или ESP32, его встроенный датчик температуры может показывать температуру выше реальной.
Лучше разместить:
- ESP32 отдельно от измерительной зоны;
- SCD30 рядом с вентиляционными отверстиями;
- SGP30 так, чтобы вокруг него свободно проходил воздух;
- вход и выход PMS5003 не перекрывать стенками корпуса.
PMS5003 сам прокачивает через себя воздух, поэтому для его входного и выходного отверстий нужно оставить свободное пространство.
Что делать, если устройство не работает
ESP32 не видит SCD30 или SGP30
Проверьте:
SDA -> GPIO21
SCL -> GPIO22
GND -> общий GND
Полезно отдельно загрузить I2C Scanner и посмотреть, какие адреса действительно обнаруживаются на шине.
Для SCD30 стандартный адрес:
0x61
Если устройство не обнаруживается вообще, сначала проверяем питание и проводку, а уже потом код.
PMS5003 не выдаёт данные
Проверьте UART:
PMS5003 TX -> GPIO16
PMS5003 RX -> GPIO17
Очень распространённая ошибка — соединить TX с TX и RX с RX.
Также PMS5003 должен получать нормальное питание 5 В.
Показания температуры завышены
Скорее всего, SCD30 нагревается от:
- ESP32;
- стабилизатора;
- блока питания;
- других компонентов внутри корпуса.
Попробуйте вынести его дальше от источников тепла и обеспечить естественную циркуляцию воздуха.
CO2 постоянно показывает странные значения
Дайте SCD30 поработать некоторое время и проверьте устройство в хорошо проветриваемом помещении.
Если используется автоматическая самокалибровка, учитывайте условия, необходимые для её корректной работы.
VOC постоянно высокий
Проверьте, нет ли рядом:
- спирта;
- растворителей;
- свежей краски;
- клея;
- чистящих средств;
- других источников органических паров.
SGP30 достаточно чувствителен к таким веществам.
Что можно добавить дальше
Базовая станция уже позволяет получить довольно подробную картину воздуха в помещении, но проект легко расширяется.
Веб-интерфейс
ESP32 может самостоятельно отдавать веб-страницу с текущими значениями.
Можно сделать локальный интерфейс с графиками через:
ESPAsyncWebServer
Chart.js
Тогда для просмотра показаний не понадобится Home Assistant.
Home Assistant
Через MQTT можно построить историю:
- CO2;
- температуры;
- влажности;
- PM2.5;
- VOC.
А дальше сделать автоматизацию.
Например:
CO2 > 1200 ppm
↓
включить вентиляцию
или:
PM2.5 резко вырос
↓
включить очиститель воздуха
Дисплей
Можно добавить небольшой OLED SSD1306 и выводить текущие значения прямо на корпусе.
Например:
CO2 742 ppm
PM2.5 8 ug/m3
TVOC 34 ppb
TEMP 23.7 C
RH 42.1 %
Собственный сервер
Если устройств несколько, ESP32 может отправлять данные на отдельный сервер.
Получится распределённая система:
ESP32 спальня ──┐
│
ESP32 кухня ────┼── MQTT ── сервер ── база данных
│
ESP32 гараж ────┘
Так уже можно сравнивать качество воздуха между помещениями и смотреть историю за недели или месяцы.
Итог
В результате получилась станция, которая измеряет сразу несколько независимых характеристик воздуха:
SCD30
├── настоящий CO2
├── температура
└── влажность
PMS5003
├── PM1.0
├── PM2.5
└── PM10
SGP30
├── TVOC
└── eCO2
ESP32
├── обработка данных
├── Wi-Fi
└── MQTT
Главное преимущество такой конструкции даже не в экономии по сравнению с готовыми устройствами.
Здесь мы точно знаем, какой датчик откуда получает данные и что именно означает каждое значение.
А дальше устройство можно адаптировать под конкретную задачу: поставить в мастерской, гараже, спальне, рядом с 3D-принтерами или встроить в общую систему домашнего мониторинга.
Если интересны другие проекты на ESP32, можно посмотреть ESP32-CAM: веб-камера и распознавание лиц своими руками или проект по измерению температуры, влажности и давления на одной плате.
Понравилась статья?
Поставьте лайк и сохраните полезный материал в закладки.
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
DIY умный звонок с камерой на ESP32-CAM и Home Assistant
Умный аквариум своими руками: автодолив, контроль температуры и освещение по расписанию
DIY ИБП для Raspberry Pi: как не дать умному дому «уснуть» при отключении света
GPS-трекер на ESP32 для велосипеда или автомобиля: DIY-проект для умного дома
Умный счётчик электроэнергии своими руками: мониторинг потребления в реальном времени
Умная кормушка для кота на ESP32: кормим питомца по расписанию