Монитор качества воздуха на ESP32 с датчиком SPS30 и облачными оповещениями

Перевод статьи How to Build an AI-based Air Quality Monitoring using ESP32 — Mohammed Shaheen K S, Circuit Digest.
Воздух нам, людям, жизненно необходим, поэтому следить за ним важно. Качество воздуха оценивают через индекс качества воздуха (AQI, Air Quality Index) — единую числовую шкалу, которая показывает, насколько воздух загрязнён и какие последствия для здоровья это может иметь. Взвешенные частицы (Particulate Matter, PM) — это микроскопические твёрдые или жидкие частицы в воздухе в виде пыли или дыма; именно они — главное, что измеряется в AQI. Поэтому монитор качества воздуха на ESP32 — такой полезный DIY-проект: он превращает невидимое пылевое загрязнение в число, которое можно увидеть, отслеживать и на которое можно реагировать.
В этом руководстве мы соберём монитор качества воздуха на ESP32 с элементами ИИ, чтобы измерять эти взвешенные частицы. Он выводит количество PM разных размеров на TFT-дисплей и в облако CircuitDigest Cloud. Когда показания превышают порог, он сообщает, что окружающий воздух небезопасен, и отправляет оповещение на привязанный номер WhatsApp — это добавляет проекту практический слой безопасности, которого нет у простого вывода с датчика.
Что такое монитор качества воздуха на ESP32 и почему важен AQI#
Монитор качества воздуха на ESP32 — это беспроводное устройство, способное измерять уровень загрязнения и выдавать результат в виде AQI. В отличие от простого пылевого датчика со светодиодом, у этого монитора есть три возможности, которых обычно нет в DIY-проектах: полная спецификация показаний PM, логирование в облако с ИИ-порогами и оповещения в WhatsApp.
Ключевые преимущества#
- Мониторинг в реальном времени — обрабатывает данные по 10 параметрам с интервалом 5 секунд.
- Питание от аккумулятора — может работать от одной батареи 18650 с повышающим преобразователем.
- Облачная панель — каждое показание логируется и отображается на графиках через сервис CircuitDigest Cloud по MQTT.
- Мгновенные уведомления — приходят в WhatsApp, когда AQI переходит в категорию «плохо».
- Дисплей — показания видны на экране 2,4″ без телефона.
Как это работает#
При включении монитор подключается к Wi-Fi, и на TFT-дисплее и в CircuitDigest Cloud (по MQTT) отображаются: индекс AQI, масса PM1.0, PM2.5, PM4, PM10, число частиц PM0.5, PM1.0, PM2.5, PM4.0, PM10 — а также пометка, безопасен ли конкретный параметр. При достижении заданного порога дисплей показывает, что параметр небезопасен, и на номер WhatsApp уходит оповещение.
Таблица порогов#
| № | Параметр | Порог |
|---|---|---|
| 1 | Масса PM1.0 | 30 мкг/м³ |
| 2 | Масса PM2.5 | 60 мкг/м³ |
| 3 | Масса PM4.0 | 80 мкг/м³ |
| 4 | Масса PM10 | 100 мкг/м³ |
| 5 | Число частиц PM0.5 | 1000 частиц/см³ |
| 6 | Число частиц PM1.0 | 800 частиц/см³ |
| 7 | Число частиц PM2.5 | 700 частиц/см³ |
| 8 | Число частиц PM4 | 500 частиц/см³ |
| 9 | Число частиц PM10 | 400 частиц/см³ |
Диапазоны AQI и категории#
| № | Диапазон AQI | Категория |
|---|---|---|
| 1 | 0–50 | Хорошо (Good) |
| 2 | 51–100 | Удовлетворительно (Satisfactory) |
| 3 | 101–200 | Умеренно (Moderate) |
| 4 | 201–300 | Плохо (Poor) |
| 5 | 301–400 | Очень плохо (Very Poor) |
| 6 | 401–500 | Опасно (Severe) |
Компоненты#
| № | Компонент | Характеристика | Кол-во |
|---|---|---|---|
| 1 | Микроконтроллер | ESP32 Dev Kit | 1 |
| 2 | Датчик качества воздуха Sensirion | SPS30 | 1 |
| 3 | TFT-дисплей | 2.4 TFT SPI 240×320 (TJCTM24024-SPI) | 1 |
| 4 | Модуль зарядки | TP4056A | 1 |
| 5 | Повышающий преобразователь | V599 (CKCS BS01) | 1 |
| 6 | Аккумулятор | 18650, 3,7 В | 1 |
Схема: подключение SPS30 к ESP32#

Схема монитора качества воздуха на ESP32.
Схема довольно простая. Плюс аккумулятора 18650 (3,7 В) — к клемме B+ модуля TP4056, минус — к B-. Клемму OUT+ модуля TP4056 — к боковой клемме тумблера, средний контакт тумблера — к IN+ повышающего преобразователя, OUT- модуля TP4056 — к IN- преобразователя. OUT+ преобразователя — к пину VIN ESP32, OUT- — к GND ESP32.
VCC датчика SPS30 — к VIN ESP32, GND — к GND ESP32, RX датчика — к TX2 (GPIO 17) ESP32, TX датчика — к RX2 (GPIO 16) ESP32. VCC TFT-дисплея — к 3.3 В ESP32, GND — к GND, CS — к GPIO 2, RESET — к GPIO 4, DC — к GPIO 5, SDI (MOSI) — к GPIO 23, SCK — к GPIO 18, LED — к 3.3 В, SDO (MISO) — к GPIO 19.
SPS30 против PMS5003 и SDS011: как выбрать датчик PM#
Sensirion SPS30 — не единственный датчик PM для ESP32. Ниже — сравнение с двумя другими популярными датчиками на рассеянии лазера.
| Характеристика | Sensirion SPS30 | Plantower PMS5003 | Nova SDS011 |
|---|---|---|---|
| Массовая концентрация | PM1.0, PM2.5, PM4.0, PM10 | PM1.0, PM2.5, PM10 | только PM2.5, PM10 |
| Счётчик числа частиц | Да — 5 диапазонов (0,5–10 мкм) | Да — 6 диапазонов (0,3–10 мкм) | Нет |
| Принцип | Рассеяние лазера | Рассеяние лазера | Рассеяние лазера |
| Интерфейс | UART или I2C | UART | UART или PWM |
| Лучше всего для | Полноспектрального AQI, облачных панелей | Бюджетных комнатных проектов | Простых логгеров PM2.5/PM10 |
Мы выбрали SPS30, потому что это единственный из трёх, кто выдаёт все девять параметров массы и числа частиц, которые нужны нашей панели и логике порогов.
Настройка CircuitDigest Cloud по шагам#
Шаг 1 ⇒ Регистрация / вход. Зарегистрируйтесь или войдите в аккаунт CircuitDigest Cloud, указав действующий email и пароль.

Регистрация и вход в CircuitDigest Cloud.
Шаг 2 ⇒ Добавить устройство. После входа нажмите Dashboard вверху, выберите Device слева, нажмите Add New Device и задайте имя проекта (у нас — Air_Quality_Monitor). Нажмите синюю кнопку Add Device.

Добавление нового устройства.
Шаг 3 ⇒ Добавление переменных. Создав устройство, нажмите Variables слева, выберите созданное устройство из списка и нажмите Add Variable. Сначала создадим переменную для массы PM1.0: задайте имя, выберите ключ (мы выбрали analog-input-1), направление bidirectional, единицу μg/m³ и нажмите Create Variable. То же проделайте ещё для 9 переменных, меняя единицы.

Создание переменных под параметры датчика.
Шаг 4 ⇒ Список переменных. Десять переменных с ключами: analog-input-1 — масса PM1.0, analog-input-2 — масса PM2.5, analog-input-3 — масса PM4.0, analog-input-4 — масса PM10, analog-input-5 — число PM0.5, analog-input-6 — число PM1.0, analog-input-7 — число PM2.5, analog-input-8 — число PM4.0, analog-input-9 — число PM10, analog-input-10 — индекс AQI.

Список из 10 переменных.
Шаг 5 ⇒ Добавление виджета. Нажмите Dashboard слева, выберите устройство. Нажмите Add Widget → Value Display Widget. В меню выберите устройство, задайте имя (PM 1.0 mass), переменную analog-input-1, единицу μg/m³ и нажмите Add Widget. То же для остальных 9 виджетов. Для массы PM единица μg/m³, для числа PM — particles/cm³.

Добавление виджета вывода значения.
Шаг 6 ⇒ Интеграция WhatsApp API. Нажмите вкладку Home вверху, прокрутите вниз до WhatsApp notification и нажмите. Выберите Link Number, укажите свой номер — придёт OTP-код. Подтвердите его — настройка WhatsApp API завершена.

Интеграция WhatsApp API для оповещений.
Разбор прошивки#
Прошивка написана в Arduino IDE. Ниже — разбор ключевых блоков кода.
#define WIFI_SSID "xxxxx" //your Wi-Fi SSID
#define WIFI_PASS "xxxxx" //your Wi-Fi password
#define DEVICE_ID "xxxxxxxxxx" // Physical Device ID
#define CONNECTION_KEY "xxxxxxxxxx" // Connection Key
#define API_KEY "xxxxxxxxxx" // API Key
// WhatsApp Notification Settings
#define WHATSAPP_PHONE "xxxxxxxxxx" // Linked phone number with country code
Данные сети и авторизация в облаке#
Здесь хранятся учётные данные для подключения к Wi-Fi и облаку: имя сети, пароль, ID устройства, ключ подключения и API-ключ. Не забудьте заменить все значения на свои.
// --- Layout: 2 columns x 5 rows = 10 boxes, 240x320 Portrait ---
const int boxW = 120; // 2 cols * 120 = 240px width
const int boxH = 64; // 5 rows * 64 = 320px height
const int cols = 2;
const int rows = 5;
struct ParamBox {
const char* label;
const char* unit;
float threshold;
bool isCPCB;
};
ParamBox params[10] = {
{ "PM1.0 MASS", "ug/m3", 30.0, false },
{ "PM2.5 MASS", "ug/m3", 60.0, true },
{ "PM4.0 MASS", "ug/m3", 80.0, false },
{ "PM10 MASS", "ug/m3", 100.0, true },
{ "PM0.5 NUM", "#/cm3", 1000.0,false },
{ "PM1.0 NUM", "#/cm3", 800.0, false },
{ "PM2.5 NUM", "#/cm3", 700.0, false },
{ "PM4.0 NUM", "#/cm3", 500.0, false },
{ "PM10 NUM", "#/cm3", 400.0, false },
{ "CPCB AQI", "", 100.0, true }
};
Разметка TFT-экрана и привязка метрик#
Этот участок настраивает интерфейс TFT-дисплея — сетку 2×5 из ячеек по 120×64 пикселя. Здесь же заданы пороги параметров.
Последовательность запуска#
void setup() {
// 1. Disable brownout detector to prevent rebooting on battery voltage dips
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
Serial.begin(115200);
delay(2000); // Initial 2s delay for power supply voltage stabilization
...
}
Эта функция выполняет поэтапный «мягкий старт», чтобы избежать просадки питания (brownout) от батареи: сначала 2 секунды на выход повышающего преобразователя на 5 В, затем 5 секунд на включение дисплея и ещё 5 секунд на запуск датчика SPS30. После этого включается Wi-Fi, а его мощность передачи снижается до 15 дБм, чтобы уменьшить пиковый ток.
Отказоустойчивое переподключение Wi-Fi#
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Wi-Fi Disconnected!");
Serial.println("Shutting down sensor and restarting system to execute staged soft-start...");
// Stop SPS30 fan before resetting system
sensor.stopMeasurement();
// 2-second delay to settle power grid before rebooting
delay(2000);
// Trigger full ESP32 microcontroller software reboot
ESP.restart();
}
Этот блок следит за состоянием Wi-Fi. Если связь пропала, он немедленно останавливает мотор датчика, чтобы сберечь батарею, и перезагружает систему.
Отправка в облако по MQTT и контроль порога AQI#
// 4. Check CPCB AQI Threshold for WhatsApp Alert (Poor Category > 200)
if (cpcbAqi > AQI_POOR_THRESHOLD) {
if (!whatsappAlertTriggered) {
Serial.println("CPCB AQI in POOR Category (>200)! Triggering WhatsApp Alert...");
sendWhatsAppAlert(cpcbAqi);
whatsappAlertTriggered = true; // Lock alert until AQI recovers below 200
}
} else {
// Reset alert state when AQI drops back to Safe/Moderate range (<= 200)
whatsappAlertTriggered = false;
}
Основной цикл выполняется каждые 5 секунд: он снимает массовую концентрацию, число частиц и индекс AQI с датчика SPS30, обрабатывая ошибки связи. Затем устройство публикует все 10 значений в CDcloud по MQTT двумя последовательными JSON-пакетами с интервалом 200 мс (чтобы избежать потери пакетов) и проверяет AQI на порог 200. При превышении инициируется оповещение в WhatsApp с блокировкой, чтобы не спамить сообщениями.
Результат#

Живые показания всех 10 параметров PM и AQI на TFT-дисплее.
Монитор выводит показания всех 10 параметров взвешенных частиц в виде сетки из 5 строк и 2 столбцов, а также отображает и хранит данные в CircuitDigest Cloud. При достижении заданного порога AQI отправляется оповещение в WhatsApp на номер из кода (он должен быть зарегистрирован в WhatsApp API CircuitDigest Cloud).
Устранение неполадок#
| Проблема | Решение |
|---|---|
| Датчик не включается после подачи питания | Убедитесь, что VCC у SPS30 подключён к 5 В или VIN ESP32, а не к 3.3 В |
| Какой аккумулятор использовать | Избегайте LiPo на 800 мА·ч 3,7 В — вентилятор SPS30 при старте берёт около 800 мА. Используйте элемент 18650 |
| Система не подключается к Wi-Fi | Настройте точку доступа/роутер на 2,4 ГГц — ESP32 не поддерживает 5 ГГц |
| Куда ставить выключатель | Не ставьте выключатель между OUT+ преобразователя и VIN ESP32 — преобразователь остаётся включённым. Ставьте между OUT+ TP4056 и IN+ преобразователя |
| Мерцание TFT-дисплея | Обычно указывает на проблему с питанием; проверьте чистые 3.3 В с пина 3.3V ESP32 |
Полный код проекта#
// Copyright (c) 2026 Jobit Joseph, Circuit Digest
// SPDX-License-Identifier: MIT
// =====================================================================
// ESP32 + SPS30 (UART) + ILI9341 + CircuitDigestCloud + WhatsApp Alert
// 2x5 Dashboard (PORTRAIT MODE) + Dual-batch MQTT + Indian CPCB AQI
// Extended 5-Second Soft-Start, Brownout Handling & Auto-Reset on Wi-Fi Loss
// =====================================================================
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <SPI.h>
#include <SensirionUartSps30.h>
#include <CircuitDigestCloud.h>
#include <WiFiClientSecure.h>
// ESP32 System Headers for Brownout Detector Configuration
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
// ── Fill in your credentials ────────────────────────────────────────────────
#define WIFI_SSID "your-Wi-Fi SSID" // your Wi-Fi SSID
#define WIFI_PASS "your-Wi-Fi password" // your Wi-Fi password
#define DEVICE_ID "your-device-id" // Physical Device ID
#define CONNECTION_KEY "your-connection-key" // Connection Key
#define API_KEY "your-api-key" // API Key
// WhatsApp Notification Settings
#define WHATSAPP_PHONE "your-registered-phone-number" // Linked phone number with country code
#define AQI_POOR_THRESHOLD 200.0f // CPCB limit for POOR Air Quality (AQI > 200)
// Cloud Variable Keys
#define KEY_PM1_0_MASS "analog-input-1"
#define KEY_PM2_5_MASS "analog-input-2"
#define KEY_PM4_0_MASS "analog-input-3"
#define KEY_PM10_MASS "analog-input-4"
#define KEY_PM0_5_NUM "analog-input-5"
#define KEY_PM1_0_NUM "analog-input-6"
#define KEY_PM2_5_NUM "analog-input-7"
#define KEY_PM4_0_NUM "analog-input-8"
#define KEY_PM10_NUM "analog-input-9"
#define KEY_AQI "analog-input-10" // Indian CPCB AQI
// ────────────────────────────────────────────────────────────────────────────
#define SENSOR_SERIAL_INTERFACE Serial2 // ESP32 HW Serial2 -> RX16 / TX17
#ifdef NO_ERROR
#undef NO_ERROR
#endif
#define NO_ERROR 0
CircuitDigestCloud CDcloud;
SensirionUartSps30 sensor;
static char errorMessage[64];
static int16_t error;
// Flag to track alert state and prevent spamming WhatsApp messages
static bool whatsappAlertTriggered = false;
// --- TFT Pin Configurations ---
#define TFT_CS 2 // GPIO 2
#define TFT_DC 5 // GPIO 5
#define TFT_RST 4 // GPIO 4
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
// --- Layout: 2 columns x 5 rows = 10 boxes, 240x320 Portrait ---
const int boxW = 120; // 2 cols * 120 = 240px width
const int boxH = 64; // 5 rows * 64 = 320px height
const int cols = 2;
const int rows = 5;
struct ParamBox {
const char* label;
const char* unit;
float threshold;
bool isCPCB;
};
ParamBox params[10] = {
{ "PM1.0 MASS", "ug/m3", 30.0, false },
{ "PM2.5 MASS", "ug/m3", 60.0, true },
{ "PM4.0 MASS", "ug/m3", 80.0, false },
{ "PM10 MASS", "ug/m3", 100.0, true },
{ "PM0.5 NUM", "#/cm3", 1000.0,false },
{ "PM1.0 NUM", "#/cm3", 800.0, false },
{ "PM2.5 NUM", "#/cm3", 700.0, false },
{ "PM4.0 NUM", "#/cm3", 500.0, false },
{ "PM10 NUM", "#/cm3", 400.0, false },
{ "CPCB AQI", "", 100.0, true }
};
void drawDashboardGrid();
void updateBox(int i, float value);
void sendWhatsAppAlert(float aqiValue);
float calculateAQI_CPCB(float pm25, float pm10);
void setup() {
// 1. Disable brownout detector to prevent rebooting on battery voltage dips
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
Serial.begin(115200);
delay(2000); // Initial 2s delay for power supply voltage stabilization
// --- Step 1: Initialize Display ---
Serial.println("[Boot Step 1/3] Starting TFT Display...");
tft.begin();
tft.setRotation(0); // Portrait mode, 240x320
tft.fillScreen(ILI9341_BLACK);
drawDashboardGrid();
Serial.println("Waiting 5 seconds for display power stabilization...");
delay(5000); // 5-second pause to isolate display power draw
// --- Step 2: Initialize SPS30 Sensor ---
Serial.println("[Boot Step 2/3] Starting SPS30 Sensor...");
SENSOR_SERIAL_INTERFACE.begin(115200, SERIAL_8N1, 16, 17);
sensor.begin(SENSOR_SERIAL_INTERFACE);
sensor.stopMeasurement();
int8_t serialNumber[32] = {0};
error = sensor.readSerialNumber(serialNumber, 32);
if (error != NO_ERROR) {
Serial.print("Error reading serial number: ");
errorToString(error, errorMessage, sizeof errorMessage);
Serial.println(errorMessage);
}
error = sensor.startMeasurement(SPS30_OUTPUT_FORMAT_OUTPUT_FORMAT_FLOAT);
if (error != NO_ERROR) {
Serial.print("Error starting measurement: ");
errorToString(error, errorMessage, sizeof errorMessage);
Serial.println(errorMessage);
} else {
Serial.println("SPS30 Measurement started successfully.");
}
Serial.println("Waiting 5 seconds for SPS30 internal fan to stabilize...");
delay(5000); //5-second pause to let fan motor reach full operational speed
// --- Step 3: Lower Wi-Fi TX Power & Connect to Cloud ---
Serial.println("[Boot Step 3/3] Turning on Wi-Fi & Connecting to Cloud...");
// Lower Wi-Fi transmit power to prevent voltage sags on battery power
WiFi.setTxPower(WIFI_POWER_15dBm);
if (!CDcloud.begin(WIFI_SSID, WIFI_PASS, DEVICE_ID, CONNECTION_KEY, API_KEY)) {
Serial.println("CDcloud begin() failed — check credentials. Rebooting...");
delay(2000);
ESP.restart(); // Reset system if initial cloud connection fails
} else {
Serial.println("CDcloud initialized successfully.");
}
Serial.println("Boot sequence complete. Entering main loop.");
}
void loop() {
// Drive WiFi reconnect, MQTT connection, and auto-heartbeat
CDcloud.loop();
// ── Wi-Fi Disconnection & Full System Reboot Handler ───────────────────────
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Wi-Fi Disconnected!");
Serial.println("Shutting down sensor and restarting system to execute staged soft-start...");
// Stop SPS30 fan before resetting system
sensor.stopMeasurement();
// 2-second delay to settle power grid before rebooting
delay(2000);
// Trigger full ESP32 microcontroller software reboot
ESP.restart();
}
// ──────────────────────────────────────────────────────────────────────────
static uint32_t lastPublish = 0;
if (millis() - lastPublish >= 5000) { // Update every 5 seconds
lastPublish = millis();
float mc1p0 = 0, mc2p5 = 0, mc4p0 = 0, mc10p0 = 0;
float nc0p5 = 0, nc1p0 = 0, nc2p5 = 0, nc4p0 = 0, nc10p0 = 0;
float typicalParticleSize = 0;
error = sensor.readMeasurementValuesFloat(mc1p0, mc2p5, mc4p0, mc10p0,
nc0p5, nc1p0, nc2p5, nc4p0,
nc10p0, typicalParticleSize);
if (error != NO_ERROR) {
Serial.print("Error reading SPS30 values: ");
errorToString(error, errorMessage, sizeof errorMessage);
Serial.println(errorMessage);
return;
}
// Compute Indian CPCB AQI based on PM2.5 and PM10
float cpcbAqi = calculateAQI_CPCB(mc2p5, mc10p0);
float values[10] = {
mc1p0, mc2p5, mc4p0, mc10p0,
nc0p5, nc1p0, nc2p5, nc4p0, nc10p0,
cpcbAqi
};
// 1. Refresh local TFT screen boxes
for (int i = 0; i < 10; i++) {
updateBox(i, values[i]);
}
// 2. Publish BATCH 1 (5 Parameters)
CDcloud.publish({
{KEY_PM1_0_MASS, mc1p0},
{KEY_PM2_5_MASS, mc2p5},
{KEY_PM4_0_MASS, mc4p0},
{KEY_PM10_MASS, mc10p0},
{KEY_PM0_5_NUM, nc0p5}
});
delay(200);
// 3. Publish BATCH 2 (5 Parameters)
CDcloud.publish({
{KEY_PM1_0_NUM, nc1p0},
{KEY_PM2_5_NUM, nc2p5},
{KEY_PM4_0_NUM, nc4p0},
{KEY_PM10_NUM, nc10p0},
{KEY_AQI, cpcbAqi}
});
Serial.println("SPS30 metrics published to Cloud.");
// 4. Check CPCB AQI Threshold for WhatsApp Alert (Poor Category > 200)
if (cpcbAqi > AQI_POOR_THRESHOLD) {
if (!whatsappAlertTriggered) {
Serial.println(" CPCB AQI in POOR Category (>200)! Triggering WhatsApp Alert...");
sendWhatsAppAlert(cpcbAqi);
whatsappAlertTriggered = true; // Lock alert until AQI recovers below 200
}
} else {
// Reset alert state when AQI drops back to Safe/Moderate range (<= 200)
whatsappAlertTriggered = false;
}
}
}
// ---------------------------------------------------------------
// Indian CPCB AQI Calculation Sub-functions
// ---------------------------------------------------------------
float getSubIndex_PM25(float pm25) {
if (pm25 <= 0.0f) return 0.0f;
if (pm25 <= 30.0f) return (50.0f / 30.0f) * pm25;
if (pm25 <= 60.0f) return 51.0f + ((100.0f - 51.0f) / (60.0f - 30.0f)) * (pm25 - 30.0f);
if (pm25 <= 90.0f) return 101.0f + ((200.0f - 101.0f) / (90.0f - 60.0f)) * (pm25 - 60.0f);
if (pm25 <= 120.0f) return 201.0f + ((300.0f - 201.0f) / (120.0f - 90.0f)) * (pm25 - 90.0f);
if (pm25 <= 250.0f) return 301.0f + ((400.0f - 301.0f) / (250.0f - 120.0f)) * (pm25 - 120.0f);
return 401.0f + ((500.0f - 401.0f) / (150.0f)) * (pm25 - 250.0f);
}
float getSubIndex_PM10(float pm10) {
if (pm10 <= 0.0f) return 0.0f;
if (pm10 <= 50.0f) return (50.0f / 50.0f) * pm10;
if (pm10 <= 100.0f) return 51.0f + ((100.0f - 51.0f) / (100.0f - 50.0f)) * (pm10 - 50.0f);
if (pm10 <= 250.0f) return 101.0f + ((200.0f - 101.0f) / (250.0f - 100.0f)) * (pm10 - 100.0f);
if (pm10 <= 350.0f) return 201.0f + ((300.0f - 201.0f) / (350.0f - 250.0f)) * (pm10 - 250.0f);
if (pm10 <= 430.0f) return 301.0f + ((400.0f - 301.0f) / (430.0f - 350.0f)) * (pm10 - 430.0f);
return 401.0f + ((500.0f - 401.0f) / (70.0f)) * (pm10 - 430.0f);
}
float calculateAQI_CPCB(float pm25, float pm10) {
float subPM25 = getSubIndex_PM25(pm25);
float subPM10 = getSubIndex_PM10(pm10);
return (subPM25 > subPM10) ? subPM25 : subPM10;
}
// ---------------------------------------------------------------
// Function to send WhatsApp HTTP REST API alert
// ---------------------------------------------------------------
void sendWhatsAppAlert(float aqiValue) {
WiFiClientSecure client;
client.setInsecure(); // Bypass SSL certificate verification
if (!client.connect("www.circuitdigest.cloud", 443)) {
Serial.println("WhatsApp connection failed!");
return;
}
String payload =
"{\"phone_number\":\"" + String(WHATSAPP_PHONE) + "\","
"\"template_id\":\"threshold_violation_alert\","
"\"variables\":{"
"\"device_name\":\"ESP32 Air Monitor\","
"\"parameter\":\"CPCB AQI\","
"\"measured_value\":\"" + String((int)aqiValue) + "\","
"\"limit\":\"200 (POOR Category)\","
"\"location\":\"Indoor Room\"}}";
client.println("POST /api/v1/whatsapp/send HTTP/1.1");
client.println("Host: www.circuitdigest.cloud");
client.println("X-API-Key: " + String(API_KEY));
client.println("Content-Type: application/json");
client.println("Connection: close");
client.print("Content-Length: ");
client.println(payload.length());
client.println();
client.print(payload);
Serial.println("WhatsApp API Payload Sent. Server Response:");
while (client.connected() || client.available()) {
if (client.available()) {
String line = client.readStringUntil('\n');
Serial.println(line);
}
}
client.stop();
}
// ---------------------------------------------------------------
// Draw static 2x5 grid dashboard (Portrait Layout)
// ---------------------------------------------------------------
void drawDashboardGrid() {
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(1);
for (int i = 0; i < 10; i++) {
int col = i % cols;
int row = i / cols;
int x = col * boxW;
int y = row * boxH;
tft.drawRect(x, y, boxW, boxH, ILI9341_DARKGREY);
tft.setCursor(x + 5, y + 4);
tft.print(params[i].label);
}
}
// ---------------------------------------------------------------
// Update content area of one dashboard slot (Portrait Layout)
// ---------------------------------------------------------------
void updateBox(int i, float value) {
int col = i % cols;
int row = i / cols;
int x = col * boxW;
int y = row * boxH;
// Clear value and status area inside the box
tft.fillRect(x + 2, y + 16, boxW - 4, boxH - 18, ILI9341_BLACK);
bool hasThreshold = params[i].threshold > 0;
bool safe = true;
uint16_t valColor = ILI9341_CYAN;
if (hasThreshold) {
safe = (value <= params[i].threshold);
valColor = safe ? ILI9341_GREEN : ILI9341_RED;
}
// --- Value Line ---
tft.setTextSize(2);
tft.setTextColor(valColor);
tft.setCursor(x + 5, y + 20);
// Format AQI without decimals, keep 1 decimal for other values
if (i == 9) {
tft.print(value, 0);
} else {
tft.print(value, 1);
}
// --- Unit Line ---
tft.setTextSize(1);
tft.setCursor(x + 5, y + 38);
tft.setTextColor(ILI9341_WHITE);
tft.print(params[i].unit);
// --- Status Banner Line ---
tft.setCursor(x + 5, y + 50);
if (!hasThreshold) {
tft.setTextColor(ILI9341_YELLOW);
tft.print("INFO ONLY");
} else if (safe) {
tft.setTextColor(ILI9341_GREEN);
tft.print(params[i].isCPCB ? "SAFE (CPCB)" : "SAFE");
} else {
tft.setTextColor(ILI9341_RED);
tft.print(params[i].isCPCB ? "NOT SAFE(CPCB)" : "NOT SAFE");
}
}
Исходный код, детали схемы и инструкции по сборке доступны в репозитории проекта на GitHub (ссылка — в оригинале статьи).