Первый график температуры в моём дашборде был похож на частокол: я публиковал показания каждые триста миллисекунд, чтобы «ничего не пропустить». За сутки набегало двести восемьдесят восемь тысяч сообщений, журнал брокера на Raspberry пух не по дням, а по часам, а сам график тонул в шуме: температура за триста миллисекунд не меняется, так что я платил трафиком и диском за три сотни одинаковых значений подряд. Через неделю карта памяти намекнула мне на смену профессии.
Вторая шишка была мягче: в топик прилетали значения вида 23.472000 — с шестью хвостыми знаками, из которых правдоподобны максимум полтора. Датчик DHT22 физически не даёт такой точности, но Serial.println(float) по умолчанию печатает сколько дал компилятор, и в график это уходило без фильтра. Дашборд честно рисовал пилу из несуществующих колебаний.
Обе истории — про одно и то же: publish выглядит как одна строчка кода, но за этой строчкой стоят решения о формате, точности и периоде, и каждое из них либо бережёт брокера, либо методично его убивает. В этом уроке пишем публикацию по-взрослому: payload строкой с округлением до честного знака, таймер на millis вместо delay и период, подобранный под скорость явления, а не под тревожность автора.
Payload: просто байты, и это свобода
Payload в MQTT — это нечисловой тип, не объект, не JSON: просто массив байтов с адресом-топиком. Протоколу всё равно, что внутри — «23.5», «включено» или фотография на два мегабайта. Эта свобода превращается в проблему, когда в доме появляется второй читатель: подписчик должен заранее знать формат. Поэтому моё правило: одна публикация — одна величина, строкой, без единиц внутри. Температура — 23.5, влажность — 56, насос — 1 или 0. Единицы зашиты в имя топика — а топик, как мы разбирали в уроке про устройство протокола, это API дома: home/balcony/temp, home/balcony/hum. Так график в любом инструменте строится без препарирования строк.
Формат числа решается в один вызов: String(t, 1) превращает 23.472 в 23.5 — один знак после запятой, ровно то, что DHT22 умеет измерять. У HAM-ов с другим датчиком знак свой: DS18B20 тоже живёт на одном знаке, а вот для своего термистора NTC на формуле Стейнхарта я беру два — там каждый десятый градус честный. Затем payload передаётся в publish через .c_str(): библиотека ждёт C-строку, и компилятор не даст подсунуть объект String напрямую — это не каприз, а вопрос буферов.
Арифметика для понимания цены. Пакет MQTT с коротким топиком и значением весит с заголовками около пятидесяти — шестидесяти байтов. Публикуя раз в десять секунд, вы кладёте в сеть восемь тысяч шестьсот сообщений в сутки — примерно полмегабайта: для домашнего роутера это шум. Раз в триста миллисекунд — та самая сотня мегабайтов в неделю и сотни тысяч записей в журнале брокера: для Raspberry с картой памяти уже вопрос «когда, а не если». Одно и то же устройство, разница только в одной константе PERIOD.
unsigned long lastPublish = 0;const unsigned long PERIOD = 10000; // раз в 10 секунд void loop() { if (!client.connected()) { reconnect(); // из прошлого урока } client.loop(); // обслуживает связь и подписки unsigned long now = millis(); if (now - lastPublish >= PERIOD) { lastPublish = now; float t = dht.readTemperature(); if (isnan(t)) { return; // датчик молчит — пропускаем отсчёт } String payload = String(t, 1); // 23.472 -> "23.5" client.publish("home/balcony/temp", payload.c_str()); Serial.print("home/balcony/temp = "); Serial.println(payload); }}Разбор по строкам, потому что здесь каждая была когда-то багом. Таймер стоит на millis, а не delay: delay(10000) в главном цикле замораживает client.loop(), а окно keepalive в пятнадцать секунд — и связь падает; полный разбор этой ловушки был в предыдущем уроке, а приём в целом — в уроке про millis вместо delay. Форма now - lastPublish >= PERIOD со скобками не случайна: беззнаковое вычитание переживает переполнение millis на сорок девятый день, когда сумма в наивном сравнении сломалась бы ровно один раз — и вы бы полдня искали мистику.
Ранний return при неответившем датчике тоже осознанный: DHT22 иногда молчит, и мой выбор — пропустить отсчёт, а не двигать период; диагностика молчания датчика разобрана отдельно в уроке про DHT и NaN. И обратите внимание, что lastPublish обновляется до публикации: если однажды внутри добавится что-то долгое, период не поплывёт.
Для отладки формата у меня есть привычка из уроков про Serial: печать парами имя=значение. Строка вида home/balcony/temp = 23.5 читается в журнале глазами, а в Serial Plotter числа ложатся ровным рядом, потому что формат пар стандартный. На живой системе я печатаю не каждый отсчёт, а каждый пятый — иначе монитор порта превращается в метель.
Как часто публиковать: цена вопроса
| Сигнал | Мой период | Почему именно так |
|---|---|---|
| Температура комнаты | 30–60 секунд | меняется на 0,1–0,3 градуса в минуту; чаще — график из шума |
| Влажность | 60 секунд | ещё инертнее температуры, точность датчика того же порядка |
| Ток или мощность | 2–5 секунд | на этой скорости видны всплески компрессора и пусковые токи |
| Дверь, окно, протечка | мгновенно, по событию | не период, а publish в момент изменения состояния |
| Счётчик воды | раз в минуту | импульсы копятся в переменной, брокеру уходит сумма |
Таблица — не догма, а стартовые точки, к которым я пришёл на своих устройствах. Общий принцип: период должен быть соизмерим со скоростью явления. Температура в комнате за минуту проходит десятые доли градуса — публиковать её чаще значит писать одно и то же число разными строчками журнала. Ток при этом за две секунды успевает спрыгнуть с нуля до пускового скачка — вот и два-пять секунд.
Для плавных сигналов у меня есть усовершенствование, которое убрало девяносто процентов трафика: публикация по изменению с порогом. Публикую, если значение ушло от предыдущего на полградуса, или если прошло пять минут тишины — вторая половина условия нужна, чтобы дашборд знал, что система жива. Число с условием: на балконном термометре это дало в среднем одно сообщение в две-три минуты вместо десяти в минуту, при этом график не потерял ни одной настоящей ступеньки. Для тока или датчика протечки такой трюк я не применяю: там важен каждый отсчёт, а тишина как раз признак беды.
И про срочные события отдельно: дверь и протечка — это не периодическая телеметрия, а событие. Схема другая: не таймер будит публикацию, а изменение состояния; здесь пригодится неблокирующий опрос кнопок или геркона из кооперативного цикла. Периодический опрос двери раз в минуту — это минута открытой двери, которую вы не увидите; если это звучит как ваша задача — открывайте планировщик задач на millis и вешайте датчики на собственные события.
- Поставьте PERIOD равным 2000, запустите, верните 4000 и поставьте 8000: сравните, как меняется шаг между записями t=. Затем посчитайте вручную, сколько сообщений в сутки даёт каждый из трёх периодов, и сверьте с арифметикой из урока.
- Добавьте второй таймер со своей переменной lastHum и публикацией home/balcony/hum раз в 12 секунд: у одного клиента периодов может быть сколько угодно, каждый живёт на своей паре lastPublish и PERIOD.
- Замените один вызов String(temp) на склейку целой части, точки и случайной дробной части — три вызова String плюс конкатенация — и посмотрите, как усложнился код ради одного десятичного знака; в настоящем скетче то же самое делает один вызов String(t, 1).
- Внедрите публикацию по порогу: публикуйте только если новое значение отличается от прошлого минимум на 2 градуса, либо прошло 20 секунд. В мониторе появится тишина между сообщениями — это и есть экономия трафика вживую.
- Payload — просто байты: моё правило — одна величина, строкой, единицы в имени топика, а не в сообщении
- String(t, 1) округляет до честного знака датчика; в publish строка уходит через .c_str()
- Публикация на millis-таймере: now - lastPublish >= PERIOD, без delay — иначе client.loop() замрёт и keepalive убьёт связь
- Цена сообщения примерно 50–60 байтов: раз в 10 секунд — это полмегабайта в сутки, раз в 300 мс — сотни мегабайтов в неделю
- Для плавных сигналов публикуйте по порогу изменения с контрольной точкой тишины; события вроде двери — publish по факту, не по таймеру
- 1. Почему публикация по delay(10000) рвёт соединение с брокером?
- 2. Какой payload отправит String(23.472, 1)?
- 3. Термометр публикует раз в 10 секунд. Сколько сообщений уходит в брокер за сутки?
- 4. Что вернёт форма now - lastPublish >= PERIOD при переполнении millis?
- 5. Датчик двери подключён к MQTT-умному дому. Как правильнее узнать об открытии?
Частые вопросы по теме урока
- Почему публикация по delay(10000) рвёт соединение с брокером?
- Пинг и разбор входящих пакетов живут внутри client.loop(). Длинный delay оставляет молчание дольше окна keepalive — брокер закрывает соединение. delay блокирует client.loop(), и keepalive не успевает за 15 секунд
- Какой payload отправит String(23.472, 1)?
- Второй аргумент String задаёт число знаков после запятой с округлением: 23.472 превращается в 23.5. Для DHT22 один знак — честная точность. 23.5
- Термометр публикует раз в 10 секунд. Сколько сообщений уходит в брокер за сутки?
- В сутках 86400 секунд; делим на 10 — получаем 8640 сообщений. При весе пакета 50–60 байтов это около полумегабайта в сутки — шум для домашней сети. 8640
- Что вернёт форма now - lastPublish >= PERIOD при переполнении millis?
- Беззнаковая арифметика корректна и при переходе через ноль: разница между отсечками остаётся верной. Вариант с суммой lastPublish + PERIOD ломается на переполнении. Продолжит работать: беззнаковое вычитание даёт верную разницу
- Датчик двери подключён к MQTT-умному дому. Как правильнее узнать об открытии?
- Дверь — событие, а не телеметрия: публикуем в момент изменения. Периодический опрос даёт окно пропуска события и грузит брокера одинаково и в тишине, и в тревоге. Публиковать по событию изменения состояния