Датчик протечки под раковиной на даче отработал всю зиму и сдался в марте: отгорел контакт на блоке питания, и плата молча легла. Дашборд показывал зелёный огонёк и ноль протечек — по его данным, всё было отлично. О том, что под раковиной третий день стоит лужа, мне сообщил сосед, приехавший за лопатой. Разбираться поехал я, а не система, и это было обиднее всего.
Проблема формулируется одной строкой: для подписчика тишина неотличима от нормы. MQTT ничего не сообщает, когда клиент исчезает — сеть просто перестаёт доставлять пакеты, и на этом всё. Дашборд показывает то, что пришло в последний раз, и у него нет повода перерисовать экран: никто не сказал ему, что источник умер.
Лекарство встроено в протокол и называется Last Will — завещание. Клиент при подключении оставляет брокеру сообщение про запас: если я исчезну некрасиво, опубликуй это за меня. В этом уроке заводим пару статусов online/offline на отдельном топике, разбираем, когда завещание срабатывает, а когда его отменяют, и почему без retained-флага вся конструкция разваливается на первом же перезапуске дашборда.
Завещание: что брокер опубликует за вас
Механика прячется в пакет CONNECT — тот самый, которым клиент представляется брокеру. Кроме имени клиента в нём можно указать четыре поля завещания: топик, payload, QoS и retain-флаг. Брокер складывает их в память сессии и терпеливо ждёт. Публикация произойдёт, если соединение рухнуло без DISCONNECT — выдернули питание, завис роутер, клиент утонул в бесконечном цикле. А если клиент ушёл красиво, сам вызвал disconnect, — брокер завещание молча уничтожает: уход штатный, помощь не нужна.
Второй нюанс — терпение. Брокер не публикует завещание в ту же секунду, как пропал клиент: спецификация даёт полтора окна keepalive на всякий случай, вдруг клиент опоздал, но жив. При окне в 15 секунд это до 22–23 секунд тишины, при окне 60 секунд — до полутора минут. Отсчёт идёт с последнего любого пакета: телеметрия тоже продлевает жизнь сессии, поэтому активный датчик может вообще не пинговать. Откуда берётся окно и что происходит внутри, разбирается в уроке про keepalive и PingReq — там же о том, почему раздувать окно значит откладывать статус offline.
И главное, ради чего всё затевалось: об отвале узнаёт брокер — единственный, кто видит связь целиком. Сама плата молчать не откажется, она уже молчит, поэтому подписчики получают плохую новость не от датчика, а от сервера. Это переворачивает привычную картину из урока про устройство протокола: обычно источником сообщений служит клиент, а в статус-топике автором offline становится брокер.
#include <WiFi.h>#include <PubSubClient.h> WiFiClient net;PubSubClient client(net); const char* STATUS = "home/dacha/status"; // одна функция на всё подключение: завещание перезаписывается каждым connectbool connectWithWill() { bool ok = client.connect( "dacha-leak-1", STATUS, 1, true, "offline" // топик, QoS, retain и payload завещания ); if (ok) { client.publish(STATUS, "online", true); // retain: статус доживёт до новых подписчиков } return ok;} unsigned long lastLeak = 0; void setup() { Serial.begin(115200); WiFi.begin("HomeNet", "12345678"); while (WiFi.status() != WL_CONNECTED) { delay(300); } client.setServer("192.168.1.50", 1883); connectWithWill();} void loop() { if (!client.connected()) { connectWithWill(); } // reconnect — тоже с завещанием client.loop(); if (millis() - lastLeak >= 2000) { // опрос геркона раз в 2 с lastLeak = millis(); if (digitalRead(4) == LOW) { client.publish("home/dacha/leak", "1"); // событие — сразу, без retain } }}Разбор — по аргументам connect, потому что каждый выбирался не сразу. Пятый аргумент, payload offline, — это и есть завещание. QoS 1 у него, чтобы сообщение доехало хотя бы раз даже по дрожащей сети: статус устройства слишком важен, чтобы терять его на последнем хмеле, поэтому нулевой QoS для завещания я не ставлю никогда. Retain true — вторая половина магии: брокер запомнит offline и отдаст его каждому новому подписчику мгновенно.
Retain-флаг обязан стоять у обеих половинок пары. После каждого успешного connect скетч сам публикует online с retain — и перезапустившийся дашборд или Home Assistant, перечитавший конфиг, получает статус сразу после подписки, не дожидаясь событий. Ровно тот же флаг -r у mosquitto_pub мы трогали, когда ставили брокер в уроке про Mosquitto. Арифметика для сравнения: два retained-сообщения в сутки против рассылки статуса раз в 30 секунд — это 2880 сообщений и постоянный шум в логе, а результат тот же.
Отсюда правило пары, короткое: online всегда руками после connect, offline всегда завещанием, и никто не пишет в статус-топик в промежутке. Как только появляется третий автор — например, watchdog решил честно сообщить offline перед рестартом, — пары рассыпаются: retained-кэш начинает помнить то, чего нет. Одно значение на публикацию, зашитое в топик, — то же правило, что и для телеметрии из урока про публикацию показаний, просто применённое к статусу.
Статус-топик: договор с дашбордом
| Топик | Кто пишет | Payload | Retain |
|---|---|---|---|
| home/dacha/status | клиент: online после connect; брокер: offline по завещанию | online / offline | да, обе половины |
| home/dacha/leak | клиент по событию геркона | 1 / 0 | нет: события не кэшируем |
| home/dacha/leak/avg | клиент раз в 60 секунд | влажность в процентах | нет: устареет за час |
| home/dacha/pump/set | дашборд в плату, команда | 1 / 0 | нет: см. ниже, это ловушка |
Парности ради — почему статус в отдельном топике, а не флагом внутри телеметрии. Подписчики разные: графику нужен поток показаний, а логике доступности — один бит живости. Смешивать их значит заставлять каждый инструмент резать строки, а заодно лишать завещание места: will публикует один payload в один топик, а не правит кусок JSON. Слова online и offline вместо 1/0 — тоже договор: их читают люди в логах, и Home Assistant из коробки понимает их в поле payload_available.
Тупик, который я распутывал дольше, чем стоил бы сам LWT: статус мигал online/offline каждые несколько секунд без видимой причины. Wi-Fi был ровный, брокер живой, а в логе шли пары connect/disconnect с одним именем — две платы с одинаковым clientId воевали за паспорт, как в уроке про clientId и статусы подключения. Брокер отрезал старое соединение, у старого срабатывало завещание, новая плата публиковала online — и по кругу. Диагноз тот же: чинить паспорт устройства, а не лезть в логику статуса.
И последняя деталь, о которой забывают: завещание живёт до следующего connect. Если в setup() вы подключаетесь с will, а в reconnect() — обычным connect без него, после первого же переподключения устройство молча осталось без завещания, и ничего не подскажет об этом. Выносите подключение в одну функцию вроде connectWithWill() из скетча выше и зовите её отовсюду — я держу такое правило с тех пор, как нашёл у себя эти два разных connect.
Отдельная песня — батарейные узлы. ESP32 в глубоком сне не шлёт пингов и не держит соединение, поэтому схема сна обязана быть вежливой: перед тем как уснуть, клиент сам прощается через disconnect — брокер уничтожит завещание и не объявит спящего мёртвым. Проснулся — новый connect, новое завещание, публикация online. Уснул без прощания — через полтора окна дашборд честно покажет offline для устройства, которое мирно дремлет и через десять минут проснётся. Я неделю лечил мигающий статус, прежде чем нашёл в скетче сна без прощания.
Завещание у соединения одно. На плате может жить хоть пять логических сенсоров — статус всё равно общий, на уровне платы: will описывается в CONNECT одним топиком и одним payload. Нужна индивидуальность — заводите отдельных клиентов на сущность, но чаще это лишнее: дашборду достаточно знать, что узел жив. MQTT пятой версии добавил свойство will delay interval — брокер выжидает заданные секунды перед публикацией завещания, и краткое моргание связи перестаёт врать статусом; mosquitto второй версии протокол 5.0 умеет, но мои скетчи на PubSubClient по-прежнему живут на 3.1.1 — просто знайте, что там дальше есть.
И про цену вопроса: статус — самый дешёвый трафик в доме и самый ценный. Пара сообщений в сутки весом в десятки байтов, а решает главное — отличает сломанный прибор от работающего. После истории с лужей под раковиной я вешаю availability на каждое своё устройство, от насоса до почвенного датчика, и проверка дома сводится к одной карточке: кто в сети, кто молчит. Дешевле страховки в протоколе не нашёл.
- Запустите симулятор и дождитесь первой строки hb=1: устройство живо и публикует пульс. Нажмите кнопку и держите пару секунд — светодиод погаснет, а через 5 секунд тишины модель исполнит завещание и напечатает offline.
- Сравните поведение с настоящим брокером: в реальности offline публикует сервер через полтора окна keepalive, здесь — модель. Поменяйте KA на 3000 и 8000 и посмотрите, как растёт задержка статуса с окном.
- В основном скетче урока уберите retain у publish online и представьте подписчика, подключившегося в момент перезапуска дашборда: он не узнает статус, пока не случится событие. Это и есть цена отсутствия retain.
- Добавьте в скетч симулятора второй клиент second-leak с тем же именем dacha-leak-1 и нажмите кнопку дважды подряд: мигающий статус напомнит, почему имена клиентов чинят раньше завещаний.
- Last Will задают в connect: топик, payload, QoS, retain; брокер публикует их сам, когда клиент исчез без DISCONNECT
- Штатный disconnect уничтожает завещание — перед осознанным рестартом публикуйте offline руками
- Брокер терпит полтора окна keepalive: при 15 секундах статус offline приходит через 22–23 секунды тишины
- Пара online/offline живёт на одном статус-топике с retain: online руками после каждого connect, offline — только завещанием
- Завещание перезаписывается каждым connect — держите подключение в одной функции, иначе reconnect оставит устройство без LWT
- 1. Когда брокер публикует завещание клиента?
- 2. Зачем retain-флаг у статуса online/offline?
- 3. Окно keepalive — 15 секунд. Через сколько максимум брокер опубликует завещание молчащего клиента?
- 4. Плата вызвала client.disconnect() и ушла в перезагрузку на 10 секунд. Что видит дашборд со статус-топиком?
- 5. После первого же reconnect устройство перестало сообщать offline при отвалах. В чём вероятная причина?
Частые вопросы по теме урока
- Когда брокер публикует завещание клиента?
- Завещание — страховка некрасивого ухода: обрыв сети, питание, зависание. При вызове disconnect брокер считает уход штатным и уничтожит сообщение. Только когда соединение пропало без DISCONNECT
- Зачем retain-флаг у статуса online/offline?
- Retained-сообщение брокер отдаёт каждому новому подписчику мгновенно: перезапущенный дашборд узнаёт статус, не дожидаясь событий. Чтобы новый подписчик получил статус сразу после подписки
- Окно keepalive — 15 секунд. Через сколько максимум брокер опубликует завещание молчащего клиента?
- Спецификация даёт полтора окна: 15 x 1.5 = 22.5 секунды терпения. Только после этого соединение считается мёртвым и уходит завещание. Около 22–23 секунд
- Плата вызвала client.disconnect() и ушла в перезагрузку на 10 секунд. Что видит дашборд со статус-топиком?
- disconnect отключает завещание, retained-кэш хранит последний online. Честный вариант — опубликовать offline руками перед рестартом. online все 10 секунд: завещание было отменено штатным уходом
- После первого же reconnect устройство перестало сообщать offline при отвалах. В чём вероятная причина?
- Каждый connect задаёт завещание заново. Если в reconnect() обычный connect без will, устройство остаётся без страховки. Лечение — единая функция подключения. В reconnect() стоит connect без will, и он перезаписал подключение из setup()