Панель в доме вторые сутки рисовала зелёный индикатор «теплица online», хотя плата в теплице остыла ещё в субботу — перегорел блок питания. Никто не соврал намеренно: панель показывает то, что лежит у брокера, а брокер не читает мысли. В MQTT у смерти устройства есть протокол: клиент при подключении оставляет брокеру завещание — Last Will — и брокер произносит его за плату, когда та перестаёт отвечать. Здесь разбираю механику: кто и когда раздаёт волю, что именно прилетает подписчику и зачем завещанию retain-флаг — без него панель, открытая через час после аварии, видит бодрое «online» из прошлого. Как вписать волю в параметры connect() и привязать её к светодиоду, показано в уроке про Last Will; я не пересказываю его второй раз, а спускаюсь на уровень ниже — внутрь брокера.
Три состояния узла держите в голове как три разные вещи. Честное «online» — плата жива и подтверждает это пульсом и retained-записью. Честное «offline» — брокер раздал завещание, панель покраснела. Нечестное — «online», который лежит в архиве брокера после смерти платы и выдаёт себя свежим каждому новому подписчику. Весь этот урок — про то, как убрать третий случай, и почему без willRetain он не убирается никогда: механика раздачи воли и память брокера о последнем значении разнесены по разным углам протокола.
Кто произносит завещание за вас
Завещание — обычный топик и payload, которые клиент записывает в пакете CONNECT: «если я отвалюсь без прощания, опубликуй за меня home/greenhouse/status со значением offline». Сам клиент волю никогда не шлёт — она лежит у брокера в памяти подключения и ждёт часа. Час наступает при обрыве без прощания: питание выдернули, Wi-Fi пропал, роутер перезагрузился. А вот чистый client.disconnect() работает наоборот — пакет DISCONNECT брокер читает как вежливое прощание и аннулирует волю. Отсюда практический вывод для плат, которые уходят в сон сами: перед disconnect публикуйте offline руками, завещание за вас никто не произнесёт.
Как быстро брокер замечает смерть, решает keepalive. PubSubClient ставит 15 секунд по умолчанию, брокер прощает полторы величины: при дефолте брокер признаёт плату умершей примерно через 22 секунды тишины. Механику пинга PINGREQ и цену длинного интервала я разбирать не буду — это сделано в уроке про keepalive, здесь важна другая цифра: между физической смертью платы и её юридической смертью у брокера живёт окно в двадцать с лишним секунд. К этому окну мы вернёмся — оно кусается.
Одна строка про настройку, чтобы вы не теряли вечер: setKeepAlive() в PubSubClient можно звать только до connect(), на подключённом клиенте новое значение молча игнорируется. Я ставлю его сразу после setServer, до всяких подключений, — тогда порядок вызовов не зависит от версии библиотеки. Пока соединение живо, воля числится за clientid: переподключение той же платы перезаписывает её, а разрыв прошлого подключения должен бы её разослать — на этой тонкости ловятся те, кто гоняет тестовые платы с одним именем. И помните: воля уходит и когда брокер сам отключает клиента — за конфликт имён или переполнение очереди, так что красная панель не всегда означает севшую плату.
Что именно получит подписчик
Подписчику прилетает самый обычный PUBLISH: топик воли, ваш payload, стандартная обвязка. В пакете нет ни флага «это завещание», ни имени умершего — подписчик не отличит волю от живого сообщения и не должен: вся драма разыграна на стороне брокера. Отсюда два правила оформления. Первое: payload воли обязан быть однозначным — «offline», а не пустая строка и не «0», который график температуры честно нарисует нулём градусов. Второе: под статус выделите собственный топик рядом с температурой — home/greenhouse/status против home/greenhouse/temp, — чтобы панели подписывались на здоровье узла отдельно и не путали его с показаниями. Схему дом-комната-назначение я раскладывал в уроке про структуру топиков. У воли есть и собственный QoS — пятый аргумент connect(), и ноль там обычно честная сделка: подписчики статуса живут в той же сети и читают всё вживую. Первый уровень имеет смысл, когда статус обязан доехать до панели через злой LTE-стык, где подтверждения теряются чаще самих сообщений. Запомните простое: воля едет к подписчикам как обычный publish — какие гарантии заложили при connect, такие и получите.
#include <WiFi.h>#include <PubSubClient.h> const char* T_STATUS = "home/greenhouse/status";const char* T_TEMP = "home/greenhouse/temp"; WiFiClient net;PubSubClient client(net);unsigned long lastPub = 0; float readTemp() { // тут ваш датчик: dht.readTemperature() return 22.4;} void setup() { Serial.begin(115200); WiFi.begin("HomeNet", "12345678"); while (WiFi.status() != WL_CONNECTED) delay(300); client.setServer("192.168.1.50", 1883); client.setKeepAlive(15); // только до connect // clientid, user, pass, willTopic, willQoS, willRetain, willMessage client.connect("greenhouse-01", "", "", T_STATUS, 0, true, "offline"); if (client.connected()) { client.publish(T_STATUS, "online", true); // retain: брокер запомнит }} void loop() { client.loop(); unsigned long ms = millis(); if (ms - lastPub >= 10000) { lastPub = ms; char buf[12]; snprintf(buf, sizeof(buf), "%.1f", readTemp()); client.publish(T_TEMP, buf); }}Retain-флаг: память брокера
Retain превращает брокера из почтамта в архив: publish с retain=1 просит брокера запомнить последнее сообщение топика и отдавать его каждому новому подписчику сразу в подтверждении подписки, не дожидаясь следующей публикации. Хранится одно значение на топик, не история: свежий retain затирает старый. Как этим заводить показания и что это даёт дашборду после рестарта, рассказано в уроке про retained-статус — мне же интересна связка с завещанием. Воля тоже может быть retained: шестой аргумент connect() — willRetain — просит брокера не только разослать «offline» текущим подписчикам, но и оставить его лежать на топике.
Разница заметна не в момент смерти, а позже. Без willRetain панель, открытая через час после аварии, подписывается и получает тишину: воля разлетелась тем, кто был подключён тогда, а для новых брокер ничего не хранит. С willRetain = 1 тот же подписчик вытягивает «offline» в первой же секунде — индикатор честно красный, хоть питание сгорело час назад. Для любого индикатора связи эта связка — не украшение, а основа; готовый трёхцветный узел собран в уроке про offline-индикатор. Отдельный бонус: retained-воля переживает перезапуск брокера, если тот хранит retain на диске, — Mosquitto с включённым persistence делает именно это.
$ mosquitto_sub -h 192.168.1.50 -t "home/greenhouse/#" -vhome/greenhouse/temp 22.4home/greenhouse/temp 22.5home/greenhouse/status offline # воля: руками её никто не публиковалhome/greenhouse/status online # плата перезагрузилась и уложила свой retainТеперь обещанное окно незнания. Смерть физическая и юридическая разнесены на полторы величины keepalive: пока брокер досчитывает окно, он уверен, что клиент жив, retained «online» лежит на месте, воля не разослана. Панель, подключившаяся в этот промежуток, вытягивает старое «online» и рисует зелёный при уже остывшей плате. При дефолтных 15 секундах окно — до 22 секунд, терпимо. У меня был батареечный узел с keepalive 300: там окно растягивалось на семь с половиной минут, и зелёная точка врала ровно столько после каждого сбоя питания.
Лечится на стороне панели, а не протокола. Публикуйте вместе со статусом метку времени — unixtime с NTP или хотя бы uptime платы — и научите панель считать сообщение старше пары минут подозрительным, каким бы бодрым оно ни выглядело. Вторая скоба: держите keepalive в разумных 15-60 секундах, пока узел не на батарейке, — окно сжимается пропорционально. Личное правило, к которому я пришёл через тот батареечный узел: я не верю «online», которого нет в последних двух минутах. Метку времени беру с NTP раз в сутки и храню в RTC-модуле — пара копеек железа, и панель перестаёт зависеть от честности брокера.
| Сценарий | Что получит подписчик | Когда |
|---|---|---|
| Обрыв питания или Wi-Fi без прощания | воля на willTopic всем, кто подписан | через 1,5 x keepalive тишины: ~22 с при дефолте 15 |
| Тот же обрыв, willRetain = 1 | то же, плюс offline остаётся у брокера | панель, открытая позже, видит смерть сразу — и после перезапуска брокера с persistence тоже |
| Плата ушла в сон через disconnect() | ничего: воля аннулирована вежливым прощанием | offline публикуйте руками перед сном |
| Панель открылась в окне до воли | retained «online» из прошлого | лечится меткой времени в payload |
- 1. Кто физически отправляет Last Will в топик подписчикам?
- 2. Keepalive поставлен 15 секунд. Через сколько тишины брокер раздаст волю?
- 3. Что меняет willRetain = 1?
- 4. Плата перед сном зовёт client.disconnect() и гаснет. Уйдёт ли воля?
- 5. Второй клиент подключился с тем же clientid. Что увидит панель?
- Воля лежит у брокера с момента CONNECT; сам клиент её никогда не отправляет
- Раздача — при обрыве без прощания или после 1,5 x keepalive тишины (~22 с при дефолте 15); чистый disconnect аннулирует волю
- willRetain = 1 оставляет offline у брокера: панель, открытая позже, видит смерть сразу
- Статус — на отдельном топике /status с однозначным payload: «offline», а не «0»
- Окно до 1,5 keepalive панель может врать online — лечится меткой времени или коротким keepalive
Частые вопросы по теме урока
- Кто физически отправляет Last Will в топик подписчикам?
- Воля записывается в CONNECT и лежит у брокера. Отправить её сама плата не может — она как раз умерла. Брокер рассылает волю при обрыве без прощания или после 1,5 x keepalive тишины. Брокер, когда решит, что клиент умер
- Keepalive поставлен 15 секунд. Через сколько тишины брокер раздаст волю?
- Брокер прощает полторы величины keepalive: 15 x 1,5 = 22,5 секунды. Мгновенно он узнаёт только о явном разрыве TCP; обрыв питания мёртвого сокета прячется в этом окне. Примерно через 22 секунды
- Что меняет willRetain = 1?
- willRetain просит брокера хранить «offline» как retained-значение топика. Панель, открытая через час после аварии, вытягивает его в первой секунде вместо тишины. offline останется у брокера и уйдёт всем новым подписчикам
- Плата перед сном зовёт client.disconnect() и гаснет. Уйдёт ли воля?
- Брокер читает DISCONNECT как штатное завершение: завещание вычищается. Батареечным узлам нужен ручной publish offline перед disconnect — воля их сон не прикроет. Нет: DISCONNECT — вежливое прощание, воля аннулируется
- Второй клиент подключился с тем же clientid. Что увидит панель?
- Коллизия имён выкидывает старое подключение, а это разрыв без прощания: брокер честно рассылает его завещание. Статус мигает offline на полностью живом устройстве. offline при живой плате: брокер разорвал старую пару и раздал её волю