На балконе у меня висит ESP32 с DHT22, и график климата в доме рисуется круглосуточно. Первая версия стучалась в брокера каждые две секунды — из суеверия «чаще значит точнее». Через месяц в базе накопилось полтора миллиона записей, из которых полезных было от силы двадцать тысяч: температура на балконе меняется на глазах у графика, а не у таймера. Счётчик в базе рос, бэкапы толстели, пользы не прибавлялось. Первым устал датчик: DHT22 по паспорту отдаёт новое значение не чаще раза в две секунды, всё, что чаще, — перепев одного и того же.
Интервал 30 секунд я считаю рабочим для климата: 2880 точек в сутки, график за неделю читается без зума, брокер такой трафик не замечает. Но раз в месяц приходит желание посмотреть балкон «вживую» в мороз или отладить вентиляцию — и хочется не снимать плату с балкона и не перешивать её, а сказать: поднажми. Та же история была с порогом проветривания и с ночной подсветкой графика — три настройки, три перепрошивки, три лестницы на балкон; больше не хочу.
В этом уроке публикуем температуру и влажность одним JSON-документом раз в 30 секунд по millis-таймеру, а интервал делаем настройкой: брокер присылает {"interval":60}, скетч проверяет границы, меняет паузу и подтверждает новое значение ответной публикацией. Перепрошивка ради смены интервала после этого урока — признак беды, а не работы.
Таймер публикации и честный датчик
Каркас таймера мы собирали в уроке про publish по millis: отметка lastPub, сравнение now - lastPub >= interval, переполнение millis() такой форме не страшно. Здесь отличий два. Первое: интервал живёт в переменной intervalMs, а не в константе — его меняет чужая команда, приехавшая из сети. Второе: payload собирается ArduinoJson-документом {"t":23.5,"h":48} — оба показания доезжают одним куском, без рассинхрона между температурой и влажностью; сборку документа разбирали в уроке про JSON в payload. Константа периода из старого скетча становится переменной — и это единственное изменение таймера, которое придётся тестировать: сама формула сравнения остаётся нетронутой.
С датчиком честность нужна на обеих сторонах. DHT22 обновляет показания раз в две секунды — частота 0,5 Гц, точность ±0,5 °C и ±2-5 % по влажности, диапазон от −40 до +80; читать его чаще раза в 2 с значит публиковать перепев. Раз в 30 секунд — с запасом. И обратная честность: датчик иногда молчит — питание, длина проводов, внутренний тайминг; readTemperature() тогда возвращает nan, и без проверки в график уезжает точка с мусором. Пропустить отсчёт — норма, этот случай мы разбирали в уроке про диагностику nan. Паспортные ±0,5 °C, к слову, даны при комнатных двадцати пяти: у краёв диапазона погрешность шире, и частые отсчёты датчик точнее не делают.
Подтверждение настройки — вторая половина цикла. Брокер прислал {"interval":60} — скетч зажал значение в границы и ответил в конфиг-топик числом 60: панель видит фактический интервал, а не тот, что хотела отправить. Разница между «отправил» и «применено» — та же, что между заявкой и фактом; без подтверждения спорная настройка живёт только в вашей голове.
Интервал — не единственная настройка, которую стоит отдать брокеру. Порог срабатывания, время полива, глубина ночного затемнения — всё это переменные, которые живут в скетче и меняются раз в сезон. Переведите их в конфиг-топики по образцу урока — и сезонная подстройка превращается из похода с ноутбуком в строку mosquitto_pub из консоли. Границы у каждой настройки свои, и придумать их надо до первого сообщения: честная настройка та, которую нельзя сломать кривым payload.
#include <WiFi.h>#include <PubSubClient.h>#include <ArduinoJson.h>#include <DHT.h> const char* WIFI_SSID = "HomeNet";const char* WIFI_PASS = "supernet123";const char* MQTT_SERVER = "192.168.1.50"; WiFiClient net;PubSubClient client(net);DHT dht(4, DHT22);DynamicJsonDocument doc(192); unsigned long lastPub = 0;unsigned long intervalMs = 30000UL; // 30 секунд по умолчаниюbool ackNeeded = false; // callback поменял настройку - нужен ответ void callback(char* topic, byte* payload, unsigned int length) { String msg = ""; for (unsigned int i = 0; i < length; i++) { msg += (char)payload[i]; } msg.trim(); DeserializationError rc = deserializeJson(doc, msg); if (rc != DeserializationError::Ok) return; // мусор разбираем молча в отказ if (!doc.containsKey("interval")) return; long sec = doc["interval"]; if (sec < 5) sec = 5; // санитарный минимум if (sec > 3600) sec = 3600; // санитарный максимум intervalMs = sec * 1000UL; ackNeeded = true; // подтвердим из loop, не из callback} void reconnect() { while (!client.connected()) { if (client.connect("esp32-balcony")) { client.subscribe("home/climate/config"); } else { delay(3000); } }} void setup() { Serial.begin(115200); dht.begin(); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() != WL_CONNECTED) { delay(500); } client.setServer(MQTT_SERVER, 1883); client.setCallback(callback); reconnect();} void loop() { if (!client.connected()) { reconnect(); } client.loop(); if (ackNeeded && client.connected()) { // подтверждение настройки ackNeeded = false; client.publish("home/climate/config", String(intervalMs / 1000).c_str()); } unsigned long now = millis(); if (now - lastPub >= intervalMs) { lastPub = now; float t = dht.readTemperature(); float h = dht.readHumidity(); if (isnan(t) || isnan(h)) { // датчик иногда молчит Serial.println("отсчёт пропущен: датчик не ответил"); return; } doc.clear(); doc["t"] = t; doc["h"] = h; String out = ""; serializeJson(doc, out); client.publish("home/climate", out.c_str()); }}Разбор по местам. Callback делает три шага: собрать строку байтов по length, разобрать JSON с проверкой кода, зажать значение между 5 и 3600. Нижняя граница — не паранойя: интервал в одну секунду означает публикацию каждый оборот таймера, а выше 0,5 Гц DHT22 не прыгнет — брокер получит поток перепевов; верхняя — час, дальше график превращается в решето. Сам callback ничего не публикует: он ставит флаг ackNeeded, и подтверждение уходит из loop — publish из callback мы разложим по косточкам в уроке про реле, там у этой ловушки интересная механика.
Диагностика после смены интервала — три взгляда. Первый: mosquitto_sub -t home/climate/# -v в терминале — видны и команда, и подтверждение, и публикации с метками времени; по разнице меток посчитайте фактический интервал, он обязан совпасть с подтверждением. Второй: Serial на плате — скетч печатает пропущенные отсчёты, если датчик устал от частого опроса. Третий: график в панели — плотность точек меняется в ту же секунду, что и подтверждение. Если плотность не изменилась, а подтверждение пришло, ищите опечатку в имени конфиг-топика: команда уехала туда, куда скетч не подписан. Имя топика при этом проверяется в двух местах: в подписке скетча и в строке публикации с панели — опечатки живут именно там.
Переживает ли переменная intervalMs перезагрузку? Нет: после ребута скетч вернётся к 30 секундам. Дешёвое лечение — retain на конфиг-команде: брокер помнит последнее {"interval":60} и сам вернёт его новому подключению, механику разбирали в уроке про retained-сообщения. Для ценностей понадёжнее — EEPROM и восстановление после рестарта, это отдельный урок про статусы и счётчики. Для скептиков: retained-значение переживает и перезапуск самого брокера, если тот хранит базу на диске — mosquitto с persistence true делает ровно это.
Число с условием про память: документ на два поля спокойно живёт в DynamicJsonDocument на 192 байта на ESP32 — незаметно. На Uno с её 2 КБ SRAM я бы уже считал каждый байт и разнёс разбор команды и сборку публикации по двум маленьким документам. Второй аргумент за скромный размер: разбор в callback и сборка в loop делят один документ, и двойная ёмкость на Uno уже съедала бы заметную часть стека.
Интервал: таблица решений
| Задача | Интервал | Почему именно так |
|---|---|---|
| График климата за день или неделю | 30-60 с | 2880-1440 точек в сутки: график читается, брокер не замечает трафика |
| Инкубатор, аквариум, теплица с автоматикой | 2-5 с | Скорость реакции важнее трафика; DHT22 быстрее 2 с всё равно не обновится |
| Диагностика нового узла | 5 с на первые дни | Видно дыхание датчика и пропуски; потом вернуть рабочий интервал |
| Счётчик импульсов воды | без таймера | Публикация по событию; таймер оставляют только для пульса соединения |
У балконного узла за полгода интервал менялся трижды: диагностика, эксперимент с вентиляцией, возврат к спокойным 30 секундам. Ни одной перепрошивки. Настройка, которая приезжает по сети, стоит двух десятков строк — и снимает с термометра профессию расходника.
Чего я делать не советую — менять интервал из самого скетча, «адаптивно». Логика «тише — реже, шумнее — чаще» звучит красиво, но за каждым таким трюком стоит второй таймер и ещё одна ветка, которую надо тестировать на границах. Ручной интервал с брокера честнее: вы сами решаете, что важнее сейчас, и видите решение в подтверждении. Если однажды на узле появится второй датчик, он встанет в тот же документ ещё двумя полями — команда интервала останется одна.
- Подвигайте ползунки датчика и дождитесь публикации: документ в мониторе покажет выставленные значения с шумом в десятых.
- Наберите {"interval":2} — таймер перестроится на две секунды, поток в мониторе станет плотным; верните спокойные {"interval":8}.
- Наберите {"interval":99} — потолок симулятора зажмёт значение до 10, и подтверждение напечатает именно 10: оно всегда показывает факт, а не заявку.
- Добавьте в документ третье поле: doc["hi"] = dht.computeHeatIndex(); — индекс жары поедет к брокеру вместе с t и h.
- DHT22 обновляется раз в 2 с (0,5 Гц): интервал короче — перепев; 30 с — комфорт для графика климата
- Интервал живёт в переменной intervalMs и меняется из callback: {"interval":60} с зажимом в границы 5-3600
- Подтверждение в конфиг-топик показывает панелям фактическое значение; публикация — из loop по флагу, не из callback
- isnan-проверка перед публикацией: датчик иногда молчит, и пропуск отсчёта лучше мусора в графике
- retain на конфиг-команде возвращает настройку после перезагрузки платы без EEPROM
- 1. Почему публиковать DHT22 чаще раза в две секунды бессмысленно?
- 2. Где в скетче урока меняется интервал публикации?
- 3. Зачем скетч публикует подтверждение в конфиг-топик?
- 4. Что спасает скетч от команды {"interval":0}?
- 5. Почему перед публикацией стоит проверка isnan?
Частые вопросы по теме урока
- Почему публиковать DHT22 чаще раза в две секунды бессмысленно?
- Частота обновления DHT22 — 0,5 Гц по паспорту. Более частая публикация гоняет по сети одно и то же число и раздувает базу без пользы для графика: новых значений между отсчётами датчика не берётся. Датчик обновляет показания с частотой 0,5 Гц: всё, что чаще, — перепев одного значения
- Где в скетче урока меняется интервал публикации?
- Callback разбирает конфиг-команду, зажимает значение в границы и пишет его в intervalMs; после этого millis-таймер сам перестраивается. Ручкой потенциометра
- Зачем скетч публикует подтверждение в конфиг-топик?
- Разница между «отправил» и «применено» существует всегда. Подтверждение печатает факт: если значение зажали границы, вы увидите именно применённое число. Чтобы брокер не рвал соединение
- Что спасает скетч от команды {"interval":0}?
- Без зажима интервал 0 превратил бы публикацию в шторм из сотен пакетов в секунду. Границы ставятся в callback, до записи в переменную таймера: чужая команда из сети — это ввод, а вводу нужны границы. Границы в callback: значение зажимается до минимума до записи в intervalMs
- Почему перед публикацией стоит проверка isnan?
- DHT22 иногда не отвечает, и readTemperature() возвращает nan. Пропуск одного отсчёта оставляет в графике дыру вместо ложной точки. Молчание датчика не должно превращаться в мусорную точку в графике — отсчёт честно пропускается