Appearance
ESP32, SCD40, Go и 4G-модем: система мониторинга воздуха. Часть 2

👁0просмотров
В первой части мы разобрали, зачем вообще нужен мониторинг CO₂ в жилом помещении, и почему для родителей важен простой дашборд, а не сырые графики. Во второй части, будут рассмотрены вопросы прошивки, алгоритмов и затронуты вопросы электроники. Корпус - тема отдельной заметки.
Что внутри:
- Аппаратная платформа: Waveshare ESP32-C6 с экраном 1.47″, SCD40;
- Программная часть: WiFi, временные ряды;
- Алгоритм прогнозирования: медианная фильтрация + Damped Trend Holt;
- Метрики точности: MAE, RMSE, roughness, Парето-фронт;
- Что дальше: NTP и RTC для корректных временных меток.
Аппаратная платформа
За основу разработки был выбран микрокомпьютер Waveshare ESP32-C6 с экраном 1,47' (рисунок 1). Сердцем данного прибора являются два 32-битных микропроцессора RISC-V: высокопроизводительный, с тактовой частотой до 160 МГц и маломощный с частотой до 20МГц.

Микрокомпьютер поддерживает WiFi 6 (802.11ax/b/g/n) в диапазоне 2.4 ГГц, а также Bluetooth 5.3 (LE) и оснащен встроенной антенной.
Также прибор имеет на борту:
- Встроенное ПЗУ (ROM) 320 КБ - постоянная память, хранящая базовый код загрузки (bootloader) и системные функции;
- HP SRAM 512 КБ - основная высокоскоростная оперативная память для выполнения программы и хранения данных;
- LP SRAM 16 КБ - энергоэффективная память, сохраняющая данные в режимах глубокого сна (Low Power / Deep Sleep);
- 4 МБ Внешней Flash-памяти - отдельная микросхема, распаянная на плате рядом с основным чипом. Именно здесь хранятся исполняемый код, файлы и здесь же будет находиться файл базы данных;
- Слот для карты памяти MicroSD (TF card) - на текущем этапе программно запустить его не удалось (физически он инициализируется, но чтение/запись файлов пока не работают);
- 1,47-дюймовый ЖК-экран с разрешением 172x320 пикселей и поддержкой 262 тыс. цветов.
Датчик SCD40
Датчик подключен по шине I²C. SCD40 измеряет концентрацию CO₂ в диапазоне 400–2000 ppm с точностью ±(50 ppm +5% от показания). Также, имеет встроенные датчики температуры (±0,2 °C) и влажности (±1,8 % RH), в том же корпусе
Очень важно датчику задать правильную тактовую частоту шины I²C, т.е. задать скорость обмена данными между микроконтроллером ESP32-C6 и датчиком SCD40.
C
#define I2C_FREQ 100000U // 100 kHz (стандартная скорость для SCD4x)В документации (datasheet) к датчикам серии SCD40/SCD41 компания Sensirion указывает, что датчик гарантированно и стабильно работает на частотах для I²C до 100 кГц.
Если запустить шину на стандартной для многих плат скорости 400 кГц (Fast-mode), датчик SCD40 может начать «теряться», возвращать ошибки чтения или сбоить при инициализации. Поэтому принудительная установка 100000U делает связь максимально надёжной.
C
Wire.begin(I2C_SDA, I2C_SCL, I2C_FREQ);Таблица разделов (для схемы Huge APP)
Поскольку вся Flash-память ограничена объемом 4 МБ (4096 КБ), распределение пространства под разные нужды выглядит следующим образом:
| Раздел | Тип | Что хранит | Размер |
|---|---|---|---|
| nvs | data/nvs | WiFi-ключи, настройки, энергонезависимые переменные | 24 КБ |
| phy_init | data/phy | Данные калибровки радиомодуля (WiFi/BT) | 4 КБ |
| factory | app/factory | Исполняемый код скетча (основная программа) | 3008 КБ (~3 МБ) |
| littlefs | data/spiffs | Файлы LittleFS (под БД проекта) | 1060 КБ (~1 МБ) |
Особенности разметки памяти и OTA
Исторически, в экосистеме ESP32, стандартная схема разметки делит память устройства на два независимых слота - ota_0 и ota_1. Это необходимо для безопасного беспроводного обновления (OTA). Новая прошивка скачивается в соседний слот, и в случае успеха, контроллер переключается на нее.
Однако, как например в нашем случае, если размер скомпилированного бинарного файла не укладывается в отведенные рамки стандартного OTA-слота (обычно это около 1.2-1.5 МБ), ситуацию может спасти выбор схемы Huge APP в инструментах разработки (Arduino IDE или ESP-IDF). В этом случае, полностью отключается механизм OTA-обновлений, а доступное пространство объединяется в один гигантский раздел factory размером до 3 МБ. При этом, под файловую систему (в нашем случае LittleFS), выделяется фиксированный остаток - около 1 МБ. Платой за увеличенный объем под код становится невозможность обновлять устройство беспроводным способом - только через кабель.
Разделы с типом data в таблице разделов ESP32 не равнозначны между собой. Если nvs и spiffs полностью принадлежат разработчику, то раздел phy_init является системным. Попытка записать в этот раздел кастомные данные (например, веса нейросети) гарантированно разрушит калибровку WiFi/Bluetooth и приведет к программной аварии чипа.
Раздел nvs, как и вся Flash-память, имеет физическое ограничение по количеству циклов перезаписи (обычно около 100 000 раз). Хотя архитектура nvs использует алгоритмы сглаживания износа (Wear Leveling), распределяя данные по разным ячейкам, ресурс памяти не бесконечен. При частой смене WiFi сетей (или если шлюз циклически пытается переподключиться к разным точкам доступа), системные библиотеки ESP32 начинают постоянно перезаписывать конфигурационные данные в nvs. Это может привести к преждевременной деградации ячеек Flash-памяти. Чтобы этого избежать, при частой смене сетевых настроек, рекомендуется отключать автоматическое сохранение параметров WiFi во Flash (командой WiFi.persistent(false) в Arduino IDE), либо обновлять данные в nvs строго по факту их реального изменения пользователем, полностью исключив запись в циклическом режиме loop().
Запуск устройства в работу
Прежде чем говорить о данных, HTTP, Golang и Grafana, устройство должно оказаться в сети. Для этого, в коде скетча, используется библиотека WiFiManager. Эта библиотека позволяет в интерактивном режиме, легко и просто, осуществить первичные действия по настройке WiFi соединения.
При первом включении прибора в сеть, библиотека поднимает точку доступа (AP) с заданным SSID, и готовой HTML-страницей (рисунок 2) для ее настройки. Достаточно осуществить поиск WiFi сетей каким-либо устройством: телефоном, компьютером или ноутбуком и выбрать, в списке сетей, имя подключаемой сети WiFi. Конечно, его нужно узнать зарание. В нашем случае, это IIoT_Setup, прописанное в конфигурационном файле скетча (см. конфигурационный файл ниже).

На рисунке 2, показан веб-интерфейс конфигурации беспроводной сети микроконтроллера. В верхней части экрана выводится идентификатор (SSID) текущей точки доступа устройства.
Прибор может функционировать как в режиме независимой точки доступа, так и в режиме клиента сторонней WiFi сети. Для перевода устройства в клиентский режим необходимо нажать кнопку Configure WiFi, выбрать из списка необходимую сеть или ввести ее SSID вручную, указать пароль доступа и нажать Сохранить. Конфигурация применится автоматически после перезагрузки шлюза.
Вы уже заметили, принцип простоты, запрошенный в части первой статьи, отрабатывает на 100%.
Но можно сделать проще, скорее удобнее. Существуют разные модели микроконтроллеров ESP32, часть из них, обладают встроенным экраном - очень полезная деталь. Работая с прибором без экрана, трудно понять, включился прибор в работу или ждет авторизации в сети WiFi. А с экраном? А на экране, можно высветить, во-первых, сообщение о том, что система требует инициализации в сети, во-вторых, SSID и IP-адрес – тем самым, помочь пользователю в авторизации. Или написать, что авторизация пройдена успешно.
В коде скетча, за интерактивный процесс авторизации отвечает WiFiManagerWrapper - это тонкая обёртка над библиотекой WiFiManager, которая, в том числе, добавляет работу с экраном в процессе загрузки.
C
bool WiFiManagerWrapper::connect() {
Serial.println("Запуск WiFiManager...");
m_wifiManager.setTimeout(120);
m_wifiManager.setAPCallback([](WiFiManager *myWiFiManager) {
digitalWrite(BACKLIGHT_PIN, HIGH);
tft.fillScreen(0x0000);
tft.setTextColor(0xF800);
tft.drawString("Wi-Fi Setup Needed!", 10, 30, 4);
tft.setTextColor(0xFFFF);
tft.drawString("Connect to Wi-Fi:", 10, 70, 2);
tft.drawString(myWiFiManager->getConfigPortalSSID(), 10, 95, 4);
tft.drawString("Open URL in browser:", 10, 130, 2);
tft.drawString("192.168.4.1", 10, 150, 4);
});
bool connected = m_wifiManager.autoConnect(WIFI_AP_NAME);
digitalWrite(BACKLIGHT_PIN, HIGH);
tft.fillScreen(0x0000);
if (connected) {
Serial.println("Wi-Fi успешно подключён");
tft.setTextColor(0x07E0);
tft.drawString("Wi-Fi Connected!", 10, 30, 4);
delay(1500);
tft.fillScreen(0x0000);
} else {
Serial.println("Wi-Fi не подключён (таймаут ожидания)");
tft.setTextColor(0xF800);
tft.drawString("Wi-Fi Timeout!", 10, 30, 4);
delay(1500);
tft.fillScreen(0x0000);
}
return connected;
}Итак, в функции setup() скетча (рисунок 4), мы поднимаем: SPI для работы с экраном, инициализируем файловую систему, поднимаем WiFi и подключаем датчик по шине I²C. LittleFS монтируется до WiFi, так как файловая система должна быть рабочей, когда WiFi начнёт писать логи.
C
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("ESP32 + SCD40 (v2 — New Way)");
SPI = SPIClass(SPI2_HOST);
pinMode(14, OUTPUT); // LCD_CS
pinMode(4, OUTPUT); // SD_CS
digitalWrite(14, HIGH);
digitalWrite(4, HIGH);
pinMode(BACKLIGHT_PIN, OUTPUT);
digitalWrite(BACKLIGHT_PIN, LOW);
Serial.println("Инициализация файловой системы LittleFS...");
if (!LittleFS.begin(true)) {
Serial.println("Ошибка монтирования LittleFS!");
} else {
Serial.println("LittleFS успешно смонтирована.");
}
delay(100);
Serial.println("Подготовка экрана LovyanGFX (без подсветки)...");
tft.init();
tft.setRotation(3);
tft.fillScreen(TFT_BLACK);
wifiManager.connect();
delay(200);
Serial.println("Инициализация I2C для датчика SCD40...");
Wire.begin(I2C_SDA, I2C_SCL);
scd4x.begin(Wire, 0x62);
scd4x.stopPeriodicMeasurement();
delay(100);
uint16_t error = scd4x.startPeriodicMeasurement();
if (error) {
Serial.printf("Ошибка старта SCD40: %d\n", error);
} else {
Serial.println("Датчик SCD40 успешно переведен в режим измерений");
}
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.drawString("SCD40 Monitor OK", 10, 10, 4);
}В приведенной в setup() последовательнсти инициализации устройств, скрыта серьезная проблема с энергопотреблением и пиковыми нагрузками на линию питания, которая часто приводит к спонтанным перезагрузкам платы ESP32-C6. Но самым опасным, является возможность проседания напряжения во время работы WiFi, из-за которого может пострадать файловая система запоминающего устройства и с очень серьезными последствиями, вплоть до выхода его из строя.
ESP32-C6 в режиме передачи (TX) потребляет в пике до 300–400 мА, а в режиме приема (RX) - около 100 мА. Для сравнения, датчик SCD40 по шине I²C в среднем требует единицы миллиампер (с пиками до 200 мА при работе ИК-эмиттера), а IPS-экран 1.47″ - менее 100 мА вместе с подсветкой.
Когда все модули включаются одновременно, встроенный регулятор платы может не справиться с лавинообразным током. Поэтому, на первый план выходит аппаратная надежность:
- Для стабильной работы, необходим качественный блок питания с напряжением 5 В и силой тока не менее 2 А и USB-кабель с низким сопротивлением.
- В официальном руководстве Espressif по проектированию аппаратного обеспечения (Hardware Design Guidelines) производитель прямо указывает на необходимость обвязки чипа конденсаторами:
- VDDPST1 / VDDPST2 (цифровое питание линий ввода-вывода): строго 0.1 мкФ (керамика) рядом с каждым пином для фильтрации высокочастотных помех.
- VDDA3P3 (аналоговое питание радиомодуля): емкость 10 мкФ (керамическая или танталовая для компенсации TX-пиков тока) в паре с низкоемкостной керамикой 0.1 мкФ / 1 мкФ.
Таким образом, блокировочные конденсаторы у пинов питания - это не «еще один конденсатор», а единственная гарантия того, что импульс WiFi не разрушит данные в запоминающем устройстве.
Для подключения датчика по шине I²C, выделены пины 0 и 1 разводки платы. Назначение пинов производится в конфигурационном файле.
C
// config.h
#ifndef CONFIG_H
#define CONFIG_H
// ====== НАСТРОЙКИ Wi-Fi (через WiFiManager) ======
#define WIFI_AP_NAME "IIoT_Setup" // Точка доступа для настройки
// ====== НАСТРОЙКИ ОБЛАЧНОГО СЕРВЕРА ======
#define SERVER_URL "https://адрес_сервера
#define DEVICE_ID "ESP32_C6_ROOM1" // Имя устройства, с которым будут связаны данные в базе данных
#define DB_FILE "/buffer_db.jsonl" // Имя файла локальной базы данных
// ====== НАСТРОЙКИ ДАТЧИКА ======
// Встроенные заводские значения пинов Waveshare
#define I2C_SDA 1
#define I2C_SCL 0
#define SHA_TYPE_DEFINED 1
#endif // CONFIG_HДатчик, так же мягко включается в работу.
Ну, а далее – периодическая функция loop() (рисунок 6), в которой производятся определенные действия в заданный период и в заданной последовательности.
C
...
error = scd4x.readMeasurement(co2, temp, humid);
if (!error && co2 > 0) {
if (millis() - lastMeasureTime >= MEASURE_INTERVAL || lastMeasureTime == 0) {
lastMeasureTime = millis();
// Расчет математической модели Хольта на 6 шагов вперед (30 минут)
co2_forecast_30m = calculateHoltForecast(co2, 6);
// Сетевая отправка + оффлайн буферизация на SD
handleDataTransmission(co2, temp, humid);
}
// Обновляем графический интерфейс (каждый цикл готовности датчика, ~5 сек)
updateDisplay(co2, co2_forecast_30m, temp, humid);
} else {
Serial.print("Ошибка считывания замеров с SCD40: ");
Serial.println(error);
}
...Какие действия для нас важны и какие мы производим.
Нам необходим расчет математической модели Хольта для прогнозирования значения CO₂ на 30 минут вперед. Это значение мы будем отображать на дисплее цветом, соответствующим порогу безопасности. Кроме того, на экране дисплея, мы будем отображать актуальное значение содержания углекислого газа, температуры и влажности на данный момент. Цвет надписи значения CO₂, также будет коррелировать с границами порогов безопасности, описанных в первой части.
Прогноз Хольта на ESP32
Математика позволяет получить более-менее реалистичное значение прогноза на ограниченных данных. В данном случае, ограниченность объективна и объясняется характером данных, в особенности, исторических меток, которые отсутствуют при включении прибора, а так же при его перезагрузке (история начинается с нуля). Запрошенные исторически данные с сервера не придали бы чувствительной точности, а накладные расходы были бы высокими.
Поэтому, в данной системе, используется специализированная модель, рассчитывающая прогноз локально. На сервер он не отправляется. Сервер хранит сырые данные и может пересчитать прогноз с любыми параметрами и в любое время.
Разработка велась итеративно. Простое экспоненциальное сглаживание отмели сразу, поскольку оно дает огромную ошибку, из-за отсутствия отслеживания тренда, явно присутствующего в линии данных CO₂.
Для таких числовых рядов с явным трендом, хорошо подходит классический двойной экспоненциальный Хольт ( Holt's linear trend). В этом методе, уровень сглаживается с учётом предыдущего тренда, а тренд - с учётом изменения уровня. Константы прогнозирования, указаны в коде.
Математическая функция прогноза Хольта имеет следующий вид (рисунок 7):
C
// Переменные для модели Хольта (Прогнозирование CO₂)
float alpha = 0.3; // Коэффициент сглаживания ряда
float beta = 0.2; // Коэффициент сглаживания тренда
float L_co2 = 0.0; // Базовый уровень Хольта
float T_co2 = 0.0; // Тренд Хольта
bool holtInitialized = false;
int co2_forecast_30m = 0;
// Математическая функция прогноза Хольта
int calculateHoltForecast(uint16_t current_co2, int steps_ahead) {
if (!holtInitialized) { // Инициализация без истории
L_co2 = current_co2;
T_co2 = 0; // Изначально тренд нулевой
holtInitialized = true;
return current_co2;
}
float last_L = L_co2;
// 1. Сглаживание уровня
L_co2 = alpha * current_co2 + (1.0 - alpha) * (last_L + T_co2);
// 2. Сглаживание тренда
T_co2 = beta * (L_co2 - last_L) + (1.0 - beta) * T_co2;
// Прогноз на steps_ahead вперед (на 30 минут это 6 шагов по 5 минут)
float forecast = L_co2 + (steps_ahead * T_co2); // Прогноз — линейное продолжение тренда
return (int)constrain(forecast, 400, 5000); // Ограничиваем физическими лимитами датчика
}Инициализация модели Хольта производится без истории. При первом вызове функции (первом измерении), уровень будет равен текущему значению, следовательно, тренд будет равен нулю. При каждом новом измерении, уровень будет сглаживаться с учетом предыдущего тренда, а тренд также будет сглаживаться с учетом изменения уровня. Поэтому, первые примерно 30 минут (с учетом периода) прогноз будет догонять реальность.
Параметры Хольта
alpha = 0.3- коэффициент сглаживания уровня. 30% веса - новому измерению, 70% - предыдущему уровню с трендом.beta = 0.2- коэффициент сглаживания тренда. 20% веса - изменению уровня, 80% - предыдущему тренду.
Почему именно так? Датчик SCD40 делает замеры раз в 5 минут. Это медленный датчик, и alpha = 0.3 даёт баланс - уровень реагирует на изменения за 15–20 минут, но не «дергается» от каждого измерения.beta = 0.2 делает тренд более инертным, чем уровень. Это защищает от ложных трендов, которые могут возникнуть из-за шума датчика.
Кроме того, так как прогноз строится на небольшом количестве значений, любые флуктуации будут уводить тренд от реалистичных значений и прибор, на 30 минут вперед, будет показывать рост или спад, которых реально не будет. Восстановление же тренда, в этом случае, займет от 5 до 10 измерений и с учетом периода в 5 минут, восстановление составит 25 - 50 минут. Такие флуктуационные выбросы необходимо фильтровать.
Для получения более реалистичных прогнозных значений, в этих ограниченных условиях, перед расчетом модели Хольта, можно применить медианный фильтр, позволяющий срезать «залетные» замеры, спасая ситуацию.
Компромиссом между устойчивостью и запаздыванием для медианного фильтра будет окно с N = 5.
C
// --- Медианный фильтр для CO2 ---
static const int MEDIAN_WINDOW = 5;
uint16_t medianBuffer[MEDIAN_WINDOW] = {0};
int medianIndex = 0;
int medianCount = 0;
// Вставить значение и вернуть медиану
uint16_t medianFilter(uint16_t newValue) {
medianBuffer[medianIndex] = newValue;
medianIndex = (medianIndex + 1) % MEDIAN_WINDOW;
if (medianCount < MEDIAN_WINDOW) medianCount++;
// Копируем в временный массив и сортируем
uint16_t sorted[MEDIAN_WINDOW];
for (int i = 0; i < medianCount; i++) {
sorted[i] = medianBuffer[i];
}
// Простая сортировка вставками (окно маленькое)
for (int i = 1; i < medianCount; i++) {
uint16_t key = sorted[i];
int j = i - 1;
while (j >= 0 && sorted[j] > key) {
sorted[j + 1] = sorted[j];
j--;
}
sorted[j + 1] = key;
}
return sorted[medianCount / 2];
}В результате расчета предложенного алгоритма (Медиана + Хольт), мы должны получить данные прогноза очень близкие к реальным.
Цифровой двойник
Для проверки нашего утверждения был создан цифровой двойник алгоритма прогнозирования прошивки микроконтроллера ESP32. Это программа для ПК, которая позволяет провести исследования, сосредоточившись исключительно на логике самого алгоритма, экономя время, из-за ограниченной вычислительной мощности и скорости работы микроконтроллера. Математическое моделирование проводилось на реальных исторических данных, с помощью языка программирования Golang, в силу его строгой типизации и простоты сборки и использования.
В качестве исторических данных, использовались суточные данные, полученные ранее (в другом проекте), в этом же помещении, тем же датчиком SCD40. Период замеров оригинальных данных составлял ~ 34 сек. Поэтому, в моделирующей программе, осуществлялась выбока данных с периодом 5 минут.
Результаты моделирования представлены на следующем графике:

На графике отлично видно, как ведет себя связка алгоритмов. Медиана + Хольт (черная линия) дает самую гладкую и стабильную траекторию, сглаживая шум датчика.
Однако, на графике заметны две проблемы, которые можно улучшить:
- При резком росте (после 23:34) и падении (после 06:58) все методы сглаживания «отстают» от реальных данных (пурпурная линия).
- В районе 08:48 на реальных данных есть резкий кратковременный всплеск (выброс), из-за которого обычный Хольт и SES дергаются вверх, а Медиана+Хольт хоть и сглаживает его, но все равно отклоняется.
Метод Хольта с демпфированием тренда (Damped Trend Holt)
В классическом Хольте, тренд бесконечно проецируется вперед. Из-за этого, при смене направления (на пике в 06:58), алгоритм по инерции продолжает «думать», что рост продолжается, и выдает сильную ошибку запаздывания, которую называют лагом.
В методе Хольта с демпфированием, вводится коэффициент затухания φ (обычно равный 0.9-0.98), который гасит тренд на каждом шаге. В результате, на пиках и спадах, алгоритм гораздо быстрее реагирует на смену направления, уменьшая запаздывание. Для шлюза добавляется всего одна базовая операция умножения.
И так, нам необходимо минимизировать запаздывание. Оценим его. Будем грызть гранит науки!
Математически, чтобы уменьшить лаг, нужно увеличить чувствительность к свежим данным. Для этого, нужно поднять alpha и заставить тренд быстрее адаптироваться - скорректировать beta.
Вывод лага медианы можно найти в любом учебнике по радиотехнике и DSP в разделе нелинейной фильтрации, а формулу возраста памяти Хольта - в учебниках по эконометрике и статистическому прогнозированию временных рядов.
Наш лаг запаздывания складывается из двух независимых математических причин: запаздывание скользящего медианного окна размером в 5 точек и запаздывания контура Хольта при alpha = 0.3. Первое запаздывание дает (5 - 1) / 2 = 2 шага, второе - (1 - .3) / .3 = 1 шаг (по смыслу инерции контура), 2 + 1 = 3 шага.
У нас есть определенное заветное желание уменьшить лаг с 3 шагов (15 мин.) до ~1 шага (менее 5 мин.), что соответствует пересчетам, по тем же формулам, для alpha, диапазону значений [.35 .. .55].
Путем расчетного смещения констант в сторону «быстроты» и добавления демпфирования φ = 0.88, чтобы убрать перелет на пиках, который всегда возникает при росте alpha, сформировался сбалансированный профиль (0.42 / 0.18 / 0.88).
На графике (рисунок 11) видно, что фаза активного роста или падения CO₂ редко длится непрерывно дольше 30–40 минут. Значение 0.88 подобрано так, чтобы при прогнозе на 30 минут вперед, дальние шаги тренда угасали.
Из теории, вклад тренда через k шагов составит φ^k. При горизонте +30 мин (k = 6 шагам по 5 мин) и φ = 0.88, вклад тренда падает до 0.88⁶ ≈ 0.46 от начального значения, что соответствует физическому торможению процесса. Меньшие φ дают преждевременный разворот прогноза, большие - наоборот, «перелёт» на пиках.
Новая модель, математически, будет ожидать, что физический процесс затормозится, поэтому она не должна «перелетать» вершину графика, она будет разворачиваться синхронно с реальными данными.
Новая математическая модель представлена ниже:
C
// Константы и переменные для модели Хольта с демпфированием (Анти-лаг)
float alpha = 0.42; // Увеличено для быстрой реакции на подъем
float beta = 0.18; // Коэффициент сглаживания тренда
float phi = 0.88; // Математический тормоз тренда (демпфирование)
float L_co2 = 0.0; // Базовый уровень
float T_co2 = 0.0; // Тренд
bool holtInitialized = false;
int co2_forecast_30m = 0;
// Математическая функция прогноза Хольта с демпфированием тренда
int calculateHoltForecast(uint16_t current_co2, int steps_ahead) {
if (!holtInitialized) {
L_co2 = current_co2;
T_co2 = 0; // Изначально тренд нулевой
holtInitialized = true;
return current_co2;
}
float last_L = L_co2;
// 1. Сглаживание уровня с учетом демпфирования тренда (умножаем T_co2 на phi)
L_co2 = alpha * current_co2 + (1.0 - alpha) * (last_L + phi * T_co2);
// 2. Сглаживание тренда с учетом демпфирования (умножаем предыдущий тренд на phi)
T_co2 = beta * (L_co2 - last_L) + (1.0 - beta) * phi * T_co2;
// 3. Прогноз на steps_ahead вперед с затуханием тренда (сумма геометрической прогрессии)
float phi_sum = 0.0;
float current_phi = 1.0;
for (int h = 1; h <= steps_ahead; h++) {
current_phi *= phi;
phi_sum += current_phi;
}
// Вместо (steps_ahead * T_co2) используем затухающую сумму тренда
float forecast = L_co2 + (phi_sum * T_co2);
return (int)constrain(forecast, 400, 5000);
}Результаты сравнения классического и демпфированного подходов представлены на следующем графике:

На графике (рисунок 11) отчетливо видно преимущество новой модели. Кривая «Медиана + Damped Хольт» (черная линия) практически идеально повторяет траекторию реальных данных (пурпурная линия) на этапах резкого роста и падения. Алгоритм больше не «перелетает» вершину пика в 06:58 и разворачивается синхронно с физическим процессом, подтверждая математическую корректность введенного демпфирования тренда.
Профессиональный подбор коэффициентов
Борьба с суммарным запаздыванием прошла успешно. Связка «Медиана + Damped Хольт» показала синхронный разворот на пиках и перестала «перелетать» вершину. Теперь можно перейти от интуитивного подбора параметров к аналитически обоснованному.
Из трех параметров модели alpha, beta и φ, аналитического обоснования требует коэффициент alpha. Он определяет целевой критерий - запаздывание контура сглаживания, которое оценивается как (1-alpha) / alpha шагов. Параметры beta и φ влияют на устойчивость тренда и горизонт прогноза, соответственно. Их диапазоны выбраны из аналитических соображений: beta ∈ [0.10; 0.30] обеспечивает устойчивость тренда, при beta < 0.10 тренд запаздывает на разворотах, при beta > 0.30 становится чувствителен к шуму; φ ∈ [0.82; 0.94] соответствует затуханию тренда на горизонте 6 шагов до φ⁶ ∈ [0.30; 0.69].
В профессиональной практике, параметры сглаживания подбираются одним из двух способов:
- Офлайн-оптимизация (Grid Search). На исторических данных, на компьютере (не на микроконтроллере!), запускается цикл перебора параметров с расчётом MAE и RMSE для каждого варианта. Выбираются параметры, дающие минимальную ошибку на исторических данных.
- Онлайн-адаптация (Метод Тригга-Лича) [8]. Для метода экспоненциального сглаживания со следящим контрольным сигналом, коэффициент
alphaрассчитывается динамически, на основе текущей ошибки. При систематическом смещении прогноза,alphaувеличивается, при стабилизации - уменьшается. Данный подход требует калибровки дополнительных параметров (значения скорости сглаживанияγ, начальных значений ошибокseиsae) и, как показано в литературе [7], может давать избыточные ошибки из-за жёсткости переключения между последовательными значениями. В данной работе, этот метод не применяется, поскольку модель прогнозирования Хольта с демпфированием содержит трендовую компоненту, адаптация которой требует отдельного исследования.
Для актуализации alpha использовался численный перебор по сетке на основе реальных суточных данных SCD40. Пространство поиска было ограничено инженерными требованиями:
alpha ∈ [0.20; 0.60]с шагом 0.05 - верхняя граница соответствует лагу(1−alpha) / alpha ≤ 1.67шага, что в сумме с медианным окном (2 шага) даёт суммарное запаздывание не более 3.67 шага (≈ 18 мин);beta ∈ [0.10; 0.30]с шагом 0.05 - из условия устойчивости тренда;φ ∈ [0.82; 0.94]с шагом 0.02 - из горизонта затухания тренда на 6 шагах.
Всего перебрано 315 комбинаций. Для каждой комбинации на суточных данных рассчитаны MAE и RMSE фильтрации относительно эталонного ряда, полученного медианной фильтрацией окном 15 точек и прогноза на 30 мин. вперед. Построен Парето-фронт по критерию «MAE фильтрации - MAE прогноза», содержащий 51 точку. Из фронта выбраны три характерные конфигурации, соответствующие трём сценариям эксплуатации (таблица 1).
Результаты
Метрики трёх профилей
Для трёх выбранных профилей модели Damped Trend Holt, на суточных данных SCD40 (шаг 5 минут), рассчитаны метрики точности фильтрации и прогноза на 30 минут вперед. Результаты приведены в таблице 1.
| Метрика / Профиль | Профиль 1 (Сбалансированный) | Профиль 2 (Сверхдинамичный) | Профиль 3 (Тяжелая фильтрация) |
|---|---|---|---|
| Назначение | Жилые комнаты / Офисы | Залы совещаний / Лектории | Помещения с высоким уровнем помех |
| Параметры (α, β, φ) | 0.35 / 0.30 / 0.94 | 0.60 / 0.30 / 0.94 | 0.20 / 0.10 / 0.82 |
| MAE фильтрации (шум) | 16.33 ppm | 12.90 ppm | 37.22 ppm |
| RMSE фильтрации | 33.25 ppm | 24.90 ppm | 63.22 ppm |
| MAE прогноза (+30 мин) | 44.76 ppm | 40.11 ppm | 85.96 ppm |
| RMSE прогноза (+30 мин) | 97.52 ppm | 90.04 ppm | 141.57 ppm |
| Roughness | 2.50 ppm | 4.00 ppm | 1.50 ppm |
| Рекомендация к выбору | Оптимален по балансу гладкости | Лучший для динамичных процессов | Использовать только при сбоях датчика |
Компромисс между точностью и гладкостью
Ключевой результат работы - Парето-фронт, построенный по трём критериям: MAE фильтрации, MAE прогноза и roughness (изрезанность сглаженного ряда). На рисунке 12, представлена проекция фронта на плоскость (MAE прогноза, roughness), которая демонстрирует фундаментальный компромисс между точностью прогноза и гладкостью кривой.

Каждая точка, соответствует комбинации alpha, beta, φ, не доминируемой по трём критериям (MAE фильтрации, MAE прогноза, roughness). Фронт демонстрирует фундаментальный компромисс между точностью прогноза и гладкостью кривой: сверхдинамичный профиль (alpha = 0.60) обеспечивает минимальный MAE прогноза (40.11 ppm) при максимальном roughness (4.0), тяжёлая фильтрация (alpha = 0.20) - минимальный roughness (1.5) при MAE прогноза 85.96 ppm, сбалансированный профиль (alpha = 0.35) занимает промежуточное положение (44.76 ppm, roughness 2.5).
Из рисунка 12 видно, что ни один из трёх профилей не доминирует другие по обоим критериям. Это означает, что выбор профиля определяется сценарием эксплуатации, а не абсолютным превосходством одного набора параметров над другим.
Корреляция MAE-метрик
На рисунке 13, представлена проекция того же Парето-фронта на плоскость (MAE фильтрации, MAE прогноза). В отличие от рисунка 12, здесь фронт имеет монотонно возрастающий характер - обе метрики улучшаются одновременно при движении к левому нижнему углу.

Монотонно возрастающий характер проекции отражает корреляцию MAE-метрик. Обе метрики измеряют близость модели к сырым данным и штрафуют сглаживание. Наилучшие значения по обеим метрикам снова демонстрирует сверхдинамичный профиль (12.90 и 40.11 ppm соответственно), наихудшие - тяжёлая фильтрация (37.22 и 85.96 ppm).
Рисунок 13 не показывает компромисса - он показывает, что MAE фильтрации и MAE прогноза согласованы. Компромисс проявляется только при добавлении критерия roughness (рисунок 12), который наказывает изрезанность кривой.
Поведение профилей на реальных данных
На рисунке 14, представлено поведение трёх профилей на суточных данных SCD40 в сравнении с реальными измерениями.

Сверхдинамичный профиль точнее отслеживает резкие изменения, но сильнее реагирует на шум. Тяжёлая фильтрация даёт наиболее гладкую кривую, но запаздывает на разворотах. Сбалансированный профиль занимает промежуточное положение.
Выводы по результатам
- Все три профиля Парето-оптимальны по трём критериям (MAE фильтрации, MAE прогноза, roughness) - ни один не доминирует перед другим.
- Сверхдинамичный профиль (
alpha = 0.60, beta = 0.30, φ = 0.94) обеспечивает минимальные MAE прогноза (40.11 ppm) и MAE фильтрации (12.90 ppm), но максимальный roughness (4.0). Рекомендуется для помещений с быстрой динамикой (залы совещаний, лектории). - Сбалансированный профиль (
alpha = 0.35, beta = 0.30, φ = 0.94) занимает промежуточное положение по всем трём критериям. Рекомендуется для жилых и офисных помещений. - Тяжёлая фильтрация (
alpha = 0.20, beta = 0.10, φ = 0.82) обеспечивает минимальный roughness (1.5), но максимальные MAE (37.22 и 85.96 ppm). Рекомендуется только для режима обнаружения сбоев датчика, когда приоритетом является робастность, а не точность.
Вывод
Для задачи прогнозирования CO₂ на ESP32-шлюзе метод Хольта с демпфированием тренда является достаточным и оптимальным решением. Он обеспечивает:
- точность на уровне LSTM/Prophet (40-45 ppm MAE), соответствующую пределу точности датчика SCD40;
- работу на ESP32-C6 без истории, RTC и сетевого подключения;
- лаг ~1 шаг (5 минут) вместо 3 шагов, как у классического Хольта;
- защиту от выбросов за счёт медианного фильтра N=5;
- стоимость в ~25 flops и ~120 байт ОЗУ.
Более сложные модели (LSTM, Prophet) требуют либо значительных вычислительных ресурсов (Prophet - от 1 GB RAM при обучении), либо существенного времени инференса на микроконтроллере (LSTM - до 2 секунд на ESP32-S3). Хотя, в литературе⁶, LSTM демонстрирует более высокую точность на среднесрочных и долгосрочных горизонтах, для задачи краткосрочного прогноза на 30 минут, с целью управления вентиляцией, выигрыш в точности не оправдывает усложнения архитектуры. Метод Хольта с демпфированием обеспечивает достаточную точность при времени инференса менее 1 мс и потреблении памяти менее 1 KB, что на порядки превосходит LSTM по эффективности на встраиваемых системах.
Настоящий Edge Computing - это не попытка любой ценой запихнуть тяжелую нейросеть в микроконтроллер. Это создание универсальных, сверхлегких математических моделей, которые переводят IoT-устройство от реактивного срабатывания к проактивному прогнозированию. Для этого не нужны терабайты логов, облачные кластеры или сложные фреймворки - достаточно грамотно настроенной классической статистики.
Исходный код проекта, тестовые логи и аналитический контур полностью открыты и доступны в репозитории https://github.com/IIot-Platform/esp32-co2-forecasting.
Методология
Постановка задачи
Цель работы - построить локальную модель прогнозирования концентрации CO₂ на ESP32-шлюзе, обеспечивающую приемлемую точность фильтрации и прогноза на 30 минут вперед, при минимальных вычислительных затратах. Модель должна работать на микроконтроллере без доступа к облачным вычислениям и без истории измерений при холодном старте.
Архитектура модели
Модель состоит из двух последовательных этапов: медианной фильтрации и экспоненциального сглаживания с демпфированным трендом (Damped Trend Holt).
Этап 1. Медианная фильтрация. Скользящее медианное окно размером N = 5 точек (25 минут при шаге 5 минут). Медиана выбрана вместо среднего из-за устойчивости к выбросам, так как единичный скачок датчика не смещает оценку. Запаздывание медианного окна оценивается как ( N − 1 ) / 2 = 2 шага.
Этап 2. Damped Trend Holt. Классический метод Хольта с демпфированием тренда:
| (1) | |
| (2) | |
| (3) |
где y(t) - значение после медианной фильтрации, L(t) - уровень, T(t) - тренд, α, β, φ - параметры модели.
Введение коэффициента демпфирования φ ограничивает «перелёт» на пиках и синхронизирует разворот прогноза с физическим процессом. При φ < 1 вклад тренда на горизонте h шагов убывает как φʰ.
Инициализация и прогрев модели
Модель Хольта не требует истории измерений при холодном старте. При первом вызове функции уровень инициализируется текущим значением (L(0) = y(0)), тренд принимается нулевым (T(0) = 0). При каждом последующем измерении уровень сглаживается с учётом предыдущего тренда, а тренд - с учётом изменения уровня (1)(2). Начальное значение тренда восстанавливается по первым двум измерениям: T(1) = y(1) − y(0).
Поскольку, при холодном старте тренд неизвестен, первые ~30 минут прогноз отстаёт от реальности. Модель «догоняет» данные по мере накопления веса в alpha и beta. Это физически ожидаемое поведение - без истории модель не может определить направление процесса. Для исключения эффекта прогрева из расчёта метрик первые MedianWindow точек (и первые ReferenceWindow точек для метрики фильтрации) исключаются из оценки. Таким образом, метрики точности характеризуют установившийся режим работы модели, а не переходный процесс.
Пространство поиска параметров
Пространство поиска параметров ограничено инженерными требованиями:
alpha ∈ [0.20; 0.60]с шагом 0.05. Верхняя граница соответствует запаздыванию контура Хольта(1−alpha)/alpha ≤ 1.67шага, что в сумме с медианным окном (2 шага(5−1) / 2 = 2) даёт суммарное запаздывание не более 3.67 шага (≈ 18 мин). Нижняя граница соответствует запаздыванию(1−alpha)/alpha ≤ 4шага - предельно допустимому запаздыванию для задачи управления вентиляцией.beta ∈ [0.10; 0.30]с шагом 0.05. Диапазон выбран из условия устойчивости тренда. Приbeta < 0.10тренд не успевает реагировать на развороты, приbeta > 0.30становится чувствителен к шуму датчика. Этот диапазон согласуется с классическими рекомендациями для damped trend exponential smoothing [6].φ ∈ [0.82; 0.94]с шагом 0.02. Диапазон выбран из условия затухания тренда на горизонте прогноза +30 минут (6 шагов). Приφ = 0.82вклад тренда падает до0.82⁶ ≈ 0.30, приφ = 0.94 - до 0.94⁶ ≈ 0.69. Меньшиеφдают преждевременный разворот прогноза, большие - «перелёт» на пиках.
Всего перебрано 315 комбинаций (9 × 5 × 7).
Эталонный ряд для метрики фильтрации
Метрика MAE между сглаженным и сырым рядом некорректна. Она измеряет не качество сглаживания, а близость к шумному сигналу. Чем меньше сглаживание, тем меньше MAE - вплоть до вырожденного случая alpha = 1, где MAE = 0.
Для корректного сравнения профилей с разной степенью сглаживания введён эталонный ряд - результат медианной фильтрации исходных данных окном 15 точек (75 минут). Такой эталон сохраняет физический тренд CO₂, но подавляет высокочастотный шум датчика. Эталон рассчитывается однократно перед запуском перебора, независимо от параметров модели, что обеспечивает сопоставимость метрик между всеми 315 комбинациями.
Roughness как мера гладкости
Метрики MAE и RMSE штрафуют сглаживание. Обе характеристики улучшаются с ростом alpha, beta и φ. Это фундаментальное свойство метрик, а не недостаток модели. Для учёта гладкости введён третий критерий - roughness, средняя абсолютная вторая производная сглаженного ряда:
| (4) |
Чем меньше roughness, тем глаже кривая. Roughness наказывает изрезанность, которую MAE поощряет.
Парето-фронт
По результатам перебора для каждой комбинации рассчитываются три критерия:
- MAE фильтрации - точность относительно эталона.
- MAE прогноза на 30 мин вперед - точность прогноза.
- Roughness - гладкость сглаженного ряда.
Комбинация A доминирует комбинацию B, если A не хуже B по всем трём критериям и строго лучше хотя бы по одному. Парето-фронт - множество комбинаций, не доминируемых другими.
Из Парето-фронта автоматически выбираются три характерные точки:
- Точка Сбалансированного режима - минимум нормированной суммы трёх критериев (ближайший к «идеалу»);
- Точка Сверхдинамичного режима - минимум MAE прогноза;
- Точка Тяжёлой фильтрации - минимум MAE фильтрации.
Метрики точности
MAE фильтрации - средняя абсолютная ошибка между сглаженным рядом и эталоном:
| (5) |
MAE прогноза на 30 мин вперед - средняя абсолютная ошибка между прогнозом, сделанным в момент t на 6 шагов вперёд, и фактическим значением в момент t + 6:
| (6) |
RMSE - корень из средней квадратичной ошибки, рассчитывается аналогично для фильтрации и прогноза.
Первые ReferenceWindow точек исключаются из расчёта MAE фильтрации, поскольку эталон на них ещё не определён.
Программная реализация
Симулятор написан на языке программирования Go и воспроизводит логику прошивки ESP32 на ПК. Это позволяет провести исследование без ограничений по вычислительной мощности микроконтроллера и без многократной перезаписи прошивки.
Исходные данные - суточная запись данных измерений датчика SCD40 с периодом ~34 секунды. Симулятор выполняет выборку с шагом 5 минут, что соответствует периоду работы шлюза. Для каждой комбинации параметров выполняется полный прогон по суточному ряду с расчётом метрик фильтрации и прогноза.
Результаты перебора сохраняются в файл grid_search_results.csv, Парето-оптимальные точки - в pareto_front.csv, данные для графиков - в pareto_front_roughness.csv, метрики трёх профилей - в three_profiles.csv.
Перспективным направлением работы является адаптация коэффициента демпфирования φ к текущему уровню CO₂. При концентрациях выше 1500 ppm асимметрия рисков оправдывает ослабление демпфирования (φ --> 1). Запаздывание прогноза, в этом режиме, опаснее его перелёта, поскольку вентиляция должна включаться на максимум как можно раньше. Однако, такая адаптация требует отдельных исследований устойчивости при колебаниях вокруг порога, обоснования самого порога и данных о поведении системы при высоких концентрациях. В текущей выборке, пиков выше 1500 ppm недостаточно для статистически значимых выводов, поэтому адаптивное управление φ оставлено для будущей работы.
Большинство современных IoT-систем работают реактивно. Схема проста: считывается показание датчика, сравнивается с порогом и, если нужно, срабатывает реле. Но физический мир инертен. К тому моменту, когда датчик температуры, вибрации или CO₂ пересечет критическую отметку, авария или негативный процесс уже наступят. Технические системы могут быть и должны быть проактивными и действовать на опережение.
Стандартный ответ индустрии сегодня - машинное обучение. «Возьмем LSTM...», «используем Prophet...» или «будем стримить сырые данные в облако, пусть сервер строит прогноз». Для задач, где есть данные, вычислительный бюджет и канал связи, это оправдано, например, TinyML и edge-forecasting. Но для задачи, которую мы решаем - краткосрочный прогноз на MCU без истории, без облака и с бюджетом в сотни байт RAM, - эти подходы избыточны. Не потому, что ML «не работает», а потому что цена решения на порядки выше, чем у классической статистики, при сопоставимой точности на этом классе сигналов.
P.S. Для корректного сбора истории измерений CO₂ на сервере и её дальнейшего анализа необходимо, чтобы каждая запись содержала достоверную временную метку. ESP32 не имеет аппаратных часов, сохраняющих время при отключении питания. При холодном старте, системное время инициализируется нулевым значением, и даже при наличии сети NTP-синхронизация занимает несколько секунд, в течение которых датчик уже формирует измерения. В результате, первые записи могут получить некорректные метки, что делает невозможным сопоставление данных по времени, анализ динамики CO₂ и обучение моделей. Решение этой проблемы требует комбинации аппаратных и программных мер, которые будут рассмотрены в следующей статье.
Список литературы:
- Грешилов, А. А. Математические методы построения прогнозов : учебное пособие / А. А. Грешилов, В. А. Стакун. – Москва : Радио и связь, 1997. – 112 с. – Текст : непосредственный.
- Кендэл, М. Временные ряды / М. Кендэл ; перевод с английского под редакцией и с предисловием Ю. П. Лукашина. – Москва : Финансы и статистика, 1981. – 199 с. – Текст : непосредственный.
- Лукашин, Ю. П. Адаптивные методы краткосрочного прогнозирования временных рядов : учебное пособие / Ю. П. Лукашин. – Москва : Финансы и статистика, 2003. – 416 с. – Текст : непосредственный.
- Марпл-мл., С. Л. Цифровой спектральный анализ и его приложения / С. Л. Марпл-мл. ; перевод с английского под редакцией А. П. Соколова. – Москва : Мир, 1990. – 584 с. – Текст : непосредственный.
- Оппенгейм, А. Цифровая обработка сигналов / А. Оппенгейм, Р. Шафер ; перевод с английского под редакцией С. Я. Шаца. – Москва : Связь, 1979. – 416 с. – Текст : непосредственный.
- Hyndman, R. J. Forecasting: Principles and Practice / R. J. Hyndman, G. Athanasopoulos. – 3rd ed. – Melbourne, Australia : OTexts, 2021. – URL: https://otexts.com/fpp3/ (date of access: 23.09.2026). – Text : electronic.
- Smith, D. E. Adaptive Response for Exponential Smoothing: Comparative System Analysis / D. E. Smith // Journal of the Operational Research Society. – 1974. – Vol. 25, no. 3. – P. 421–435. – DOI: 10.1057/jors.1974.76. – Text : direct.
- Trigg, D. W. Exponential smoothing with an adaptive response rate / D. W. Trigg, A. G. Leach // Operational Research Quarterly. – 1967. – Vol. 18, no. 1. – P. 53–59. – Text : direct.
Как вам статья?
ℹ️ Вы можете выбрать любую реакцию (доступен один голос на статью).
Обсуждение статьи (0)
Пока нет комментариев. Будьте первым!