Connect прошёл, subscribe встал, в копилке шестьдесят точек — и первый инстинкт всегда один: цикл по буферу без пауз, пускай брокер разбирается. Мой рекорд глупости — четыреста восемьдесят точек одним махом после суток на батарейном узле: стек Wi-Fi поперхнулся очередью, часть пакетов ушла в ретрансмиссии, брокер увидел бурю с пропусками, а дашборд заикался на перерисовке. Ни одна точка не потерялась окончательно — TCP дотащил, — но три минуты система кашляла так, будто лежала.
Шквал бьёт по трём этажам сразу. У стека Wi-Fi свои очереди на десятки пакетов — переполнение отбрасывает в лучшем случае, в худшем тянет ретрансмиссии. У брокера свой бюджет на клиента, и пачка с одного узла делится на всех подписчиков: решётка и широкие маски умножают копию каждой точки на число слушателей — цену решётки я считал в уроке про схему топиков. У дашборда — перерисовка, которая на бурю отвечает залипанием.
Граница применимости честная: если дашборд живёт только настоящим и историю никто не читает, копилка и раздача — лишние две сотни строк, и я их не ставлю. Ставлю там, где по истории считают, сверяют или ищут виноватого: отопление, счётчики с графика за месяц, расследования «а что было ночью». Срок окупаемости у буфера смешной: одна ночь без сети с дырой на графике, по которой спорили на кухне, окупила мне весь код сразу — и с тех пор копилка стоит по умолчанию в любом узле, где есть хоть один агрегат.
Здесь разбираю темп раздачи, метки времени в payload и порядок точек. Копилка и её переполнение — из прошлого урока; reconnect-таймер и backoff — из урока про реконнект, я встраиваю раздачу в его машину состояний, не пересказывая.
Темп: пятьдесят миллисекунд между публикациями
Пауза тридцать-пятьдесят миллисекунд между публикациями — число, к которому я пришёл на своих узлах: шестьдесят точек при пятидесяти миллисекундах разъезжаются за три секунды, и никто в доме не замечает. Почему не ноль: Wi-Fi-стеку нужно время выгрузить пакет в радио, брокеру — разобрать и раздать, подписчику — переварить. Пятьсот миллисекунд — другая крайность: раздача часа истории тянется полминуты, и свежие значения встают в очередь за древними, хотя могли бы идти сразу.
Цикл for с delay(50) внутри — снова блокировка: сирена и кнопки на эти три секунды оглохнут, и кооперативный цикл из уроков про millis спустит в унитаз всю экономию. Раздача живёт по той же схеме, что и публикация из урока про millis вместо delay: метка времени и по одной точке за оборот loop. Loop не останавливается ни на миллисекунду — просто между обычными делами скетч успевает выдавать точку каждые пятьдесят миллисекунд.
Домашнему брокеру на малине я этот темп, честно скажу, могу и не давать: он переварит и шквал. Но привычка ровной раздачи платит дважды. Облачные брокеры считают сообщения и гигабайты — бесплатный тариф HiveMQ Cloud живёт с лимитами трафика, и раздача копилки как раз тот случай, когда счётчик улетает за вечер. А в общем эфире дома пока один узел тащит архив, соседний узел ждёт своего слота: сетка одна на всех, и учтивость к эфиру — форма уважения к соседям по дому.
#include <WiFi.h>#include <PubSubClient.h> WiFiClient net;PubSubClient client(net); const int PIN_SIREN = 8; // сирена не должна оглохнуть long bufTs[60];int bufT[60];int head = 0;int count = 0;int razIdx = 0; // сколько точек раздалиbool razdacha = false; // идёт ли догоняющая раздачаunsigned long lastFlush = 0;unsigned long lastPub = 0;unsigned long lastSample = 0; void addReading(long ts, int t) { bufTs[head] = ts; bufT[head] = t; head = head + 1; if (head >= 60) { head = 0; } if (count < 60) { count = count + 1; }} void setup() { Serial.begin(115200); pinMode(PIN_SIREN, OUTPUT); WiFi.begin(WIFI_SSID, WIFI_PASS); client.setServer(MQTT_SERVER, 1883); client.setSocketTimeout(4); // короткий таймаут: подробности в соседнем уроке} void loop() { client.loop(); unsigned long now = millis(); // сирена опрашивается всегда - раздача не имеет права её блокировать checkSiren(); // опрос и копилка - как в уроке про буфер if (now - lastSample >= 60000) { lastSample = now; if (!client.connected()) { addReading(now / 1000, readTemp()); } } if (client.connected() && !razdacha && count > 0) { razdacha = true; // вернулись и в копилке есть что раздавать razIdx = 0; Serial.println("razdacha nachalas"); } if (razdacha && now - lastFlush >= 50) { // одна точка в 50 мс lastFlush = now; char msg[40]; snprintf(msg, sizeof(msg), "{\"ts\":%ld,\"t\":%d}", bufTs[razIdx], bufT[razIdx]); client.publish("home/teplica/temp", msg); razIdx = razIdx + 1; if (razIdx >= count) { // раздали всё - копилка пуста count = 0; head = 0; razdacha = false; Serial.println("razdacha zakonchena"); } } // свежие значения на паузе, пока едет раздача: иначе перепутается порядок if (client.connected() && !razdacha && now - lastPub >= 60000) { lastPub = now; publishCurrent(); }}Метки времени: ts в каждом payload
История без меток — не история, а столбик чисел: дашборд рисует по времени получения, и догоняющая пачка легла бы одним кустом на момент reconnect. Метка ts в каждом payload отдает точке её час: формат «объект с двумя полями» весит двадцать шесть-тридцать байт на точку, шестьдесят точек — под два килобайта трафика, это меньше одной фотографии. Настенное время берётся с RTC и синхронизируется по сети — как заводить часы от NTP и почему верить плате на слово стоит после сверки, разобрано в уроке про синхронизацию NTP и RTC.
Пачку не собрать в один publish: буфер публикации PubSubClient — те самые двести пятьдесят шесть или пятьсот двенадцать байт, о которых я писал в уроке про размер буфера; два килобайта истории в него не влезут, и библиотека молча вернёт ноль. Поэтому раздача едет по одной точке — что заодно и даёт честный темп из первого раздела. Порядок — старое первым: графики любят монотонный ход, а самая свежая точка уезжает последней и заодно ложится в retained-кэш брокера, закрывая дыру последним известным значением из урока про retained-статус.
Признак конца раздачи — обычный таймер публикации снова тикает: счётчик обнулился, флаг снят, свежие значения пошли как ни в чём не бывало. Пока флаг стоит, свежая публикация на паузе — иначе свежая точка уедет раньше догоняющих, и график запутается в порядке. Про старт и финиш раздачи я пишу по строке в журнал — полчаса раздачи в нём видны с первого взгляда и не путаются с телеметрией.
Масштаб сверху: сутки минутных точек на ESP32 — это тысяча четыреста записей по тридцать байт, сорок три килобайта и семьдесят две секунды раздачи при пятидесяти миллисекундах. Стоит ли оно того? Мой вердикт — агрегировать перед раздачей: из двадцати минутных точек сделать четыре пятиминутных средних прямо на плате. Для графика глубина сохраняется, объём и время раздачи падают в пять раз, и копилка агрегатов из прошлого урока выстреливает ровно здесь: семьдесят две секунды превращаются в пятнадцать, а счётчик сообщений у облачного брокера — в пять раз скромнее.
Раздача в живом доме: очередь и наблюдение
Живой дом усложняет картину одним движением: после рестарта брокера пытаются раздавать копилки сразу все узлы — у каждого они накопились за ту же ночь, и backoff из урока про реконнект растаскивает connect-ы, но не раздачи. Два узла с шестьюдесятью точками стартуют с разницей в пять секунд — и делят эфир по очереди сами: паузы в пятьдесят миллисекунд достаточно редки, чтобы два потока не столкнулись лбами. Три и больше узлов — уже очередь с мнениями: тяжёлые копилки я отпускаю последними, повышая стартовую паузу узла пропорционально его точкам, и шторма не видел с тех пор ни разу.
Наблюдать за раздачей удобно числом, а не ощущением: счётчик точек в журнале, делящийся на время, даёт фактическую скорость — при пятидесяти миллисекундах это около двадцати точек в секунду, и если счётчик стоит, раздача не едет, а застряла. Застревание — это publish, вернувший ноль на живом соединении: буфер не принял, точка остаётся в копилке, и retry на следующем тике. Именно поэтому индекс раздачи я не двигаю при неудаче: точка, которую брокер не взял, не считается отправленной — иначе копилка врёт о собственной глубине, и верить ей потом бесполезно.
Одна деталь из практики, которую я до сих пор вижу в чужих скетчах: раздача стартует на первой же успешной попытке connect — на сыром TCP, который только что поднялся и может оборваться повторно. Тогда копилка делится на два захода, между которыми в график втыкается свежая точка, и порядок пачки ломается. Моё правило: после connect даю соединению устояться десять-пятнадцать секунд с обычной публикацией, и только потом отпускаю раздачу — за это время видно, держит ли сеть; связка из уроков про keepalive и машину состояний тут работает на нас.
| Темп раздачи | 60 точек уедут за | Что происходит на этажах |
|---|---|---|
| 0 мс, цикл без пауз | меньше секунды | шквал: стек отбрасывает, брокер и дашборд захлёбываются, сирена оглохла в for-цикле |
| 10 мс | 0,6 секунды | на грани: на ESP32 с хорошей связью живёт, на Uno с модулем Wi-Fi — ретрансмиссии |
| 50 мс | 3 секунды | мой выбор: никто не замечает, loop жив, сирена дышит |
| 500 мс | 30 секунд | раздача часа истории тянется полминуты, свежие значения стоят в очереди |
- 1. Шестьдесят точек копилки при паузе 50 мс раздаю за оборот loop по одной. Сколько длится раздача?
- 2. Почему догоняющую пачку нельзя собрать в один publish с длинным payload?
- 3. Какой QoS я ставлю догоняющей пачке температурных точек и почему?
- 4. Порядок точек при раздаче копилки?
- 5. Что во время раздачи происходит со свежей публикацией по таймеру?
- Пачку без пауз не отправляйте: шквал бьёт по стеку Wi-Fi, брокеру и дашборду одновременно; мой темп — 50 мс между точками.
- Раздача — по точке за оборот loop с millis-меткой: for с delay блокирует сирену и кнопки на все три секунды.
- Каждая точка едет с меткой ts: без неё дашборд рисует историю по времени получения и кучует пачку в момент reconnect.
- Буфер публикации 256-512 байт — длинный payload молча вернёт ноль; два килобайта истории едут по одной публикации.
- Порядок старое-первым, свежая точка последней и в retained; на время раздачи свежая публикация на паузе, остальное живёт.
Частые вопросы по теме урока
- Шестьдесят точек копилки при паузе 50 мс раздаю за оборот loop по одной. Сколько длится раздача?
- 60 × 50 мс = 3 секунды — незаметно для дома. Цикл без пауз даёт шквал и отброшенные пакеты, полсекунды на точку — тридцать секунд ожидания. Три секунды
- Почему догоняющую пачку нельзя собрать в один publish с длинным payload?
- Два килобайта истории не влезают в буфер публикации; библиотека молча возвращает ноль. Раздача по одной точке заодно даёт честный темп раздачи. Буфер публикации PubSubClient — 256-512 байт, библиотека молча вернёт ноль
- Какой QoS я ставлю догоняющей пачке температурных точек и почему?
- Нулевой уровень честен для истории: потерянная при повторном обрыве точка — дыра на графике, не пропавший литр. QoS1 умножает время раздачи на паршивой связи. Нулевой: точка не устаревает, а подтверждения растягивают раздачу в два-три раза
- Порядок точек при раздаче копилки?
- Графики любят монотонный ход: старое-первым. Самая свежая точка уезжает последней и ложится в retained-кэш, закрывая последнее известное значение. Старое первым, свежая точка последней и в retained
- Что во время раздачи происходит со свежей публикацией по таймеру?
- Флаг раздачи глушит только свежую публикацию. Сирена, кнопки, опрос и client.loop() живут на каждом обороте — иначе это снова блокировка. На паузе до конца раздачи: иначе порядок на графике перепутается