Датчик в гараже отвалился от роутера в четверг, а я узнал в воскресенье — панель честно показывала «online» все три дня. Смотрите сами: статус online с retain живёт у брокера, плата молчит, и никто ничего не меняет. Панель не отличает мёртвую плату от работающей, ей же просто раздают последнее сохранённое слово. Пинговать каждый датчик из панели вручную — работа сисадмина на полставки, а датчиков у меня двадцать.
Протокол MQTT придумал для этого Last Will — завещание. При подключении плата кладёт в CONNECT-пакет заготовку: топик, payload и retain-флаг. С этого момента за жизнью платы следит брокер: если она перестала отвечать пингами и молчит дольше полутора keepalive-интервалов, брокер сам публикует завещание — home/garage/status = "offline", retain. Панель узнаёт о смерти в тот же момент, что и брокер, без единой строчки кода на самой плате. Ниже — подключение с волей на ESP-плате и разбор, почему штатный disconnect завещание сжигает.
Завещание регистрируется в CONNECT
Воля — не отдельная команда, а поля внутри первого пакета подключения: willTopic, willQos, willRetain и willMessage. В PubSubClient они занимают седьмой-десятый аргументы connect(): client.connect("garage-01", "", "", T_STATUS, 0, true, "offline"). Брокер запоминает заготовку и дальше измеряет тишину: клиент обязан раз в keepalive-интервал слать PINGREQ — setKeepAlive(15) даёт пятнадцать секунд. Полтора интервала тишины — двадцать две с половиной секунды — и брокер считает соединение мёртвым: закрывает TCP и публикует завещание.
Retain у воли — та же история, что у статуса: без него offline увидят только те, кто был подписан в момент смерти, а панель, включившаяся позже, получит старое retained-«online» и снова будет врать. Ставьте willRetain в true — тогда offline ляжет в таблицу и перехлестнёт прежний статус. Обратная сторона: когда плата вернётся, она обязана опубликовать online с retain сама — брокер сам ничего не перепишет. Про то, как ping держит соединение, у нас есть разбор keepalive, а про то, как плата переживает обрывы, — машина состояний связи.
#include <WiFi.h>#include <PubSubClient.h> WiFiClient net;PubSubClient client(net); const int PIN_LED = 2; // горит — связь естьconst char* T_STATUS = "home/garage/status";const char* T_CMD = "home/garage/reboot/set"; void setup() { Serial.begin(9600); pinMode(PIN_LED, OUTPUT); WiFi.begin("HomeNet", "12345678"); while (WiFi.status() != WL_CONNECTED) delay(300); client.setServer("192.168.1.50", 1883); client.setKeepAlive(15); // брокер ждёт молчание ~22 с Serial.println("wifi ok, подключаюсь с завещанием");} void loop() { if (!client.connected()) { // id, user, pass, willTopic, willQos, willRetain, willMessage bool ok = client.connect("garage-01", "", "", T_STATUS, 0, true, "offline"); if (ok) { client.subscribe(T_CMD); client.publish(T_STATUS, "online", true); // статус с retain перезапишет offline Serial.println("на связи: завещание принято, статус online"); } else { Serial.print("брокер недоступен, код "); Serial.println(client.state()); delay(2000); } return; } client.loop(); digitalWrite(PIN_LED, (millis() / 500) % 2 == 0 ? HIGH : LOW);}Седьмой аргумент — топик завещания, девятый — retain, десятый — payload. Логика симметричная, и в этом весь фокус: offline в топик кладёт брокер, online — сама плата, а топик один и тот же. Пока плата жива, каждое переподключение перезаписывает retained-значение на online; пропала связь дольше двадцати двух секунд — брокер переписал на offline; вернулась — плата выложила online обратно. Панель читает один топик и всегда видит правду, а плата про «умерла/ожила» вообще не думает: у неё в скетче только подключение и client.loop().
client.state() в коде отказа — числа, по которым ловят причину: -4 нет ответа, -3 TCP не принял, -2 не удалось отправить, 5 не принят доступ. В логах гаража у меня почти всегда -2: роутер жив, а точка доступа уже далеко. Если коды сыплются пачками — смотрите питание и антенну, а про retry-цикл и паузы у нас есть урок про reconnect с backoff.
| Как умерла связь | Кто объявит | Сколько ждать |
|---|---|---|
| Питание выдернули, плата сгорела | брокер по keepalive | ~1,5 × keepalive, при 15 с — около 22 с |
| WiFi пропал, плата жива и не знает | брокер по keepalive | те же ~22 с после последнего пинга |
| Плата сама вызвала disconnect() | никто: воля сгорает | мгновенно, но offline не публикуется |
| Плата зависла намертво, TCP жив | никто, пока TCP держится | самый коварный случай: воли не будет |
- 1. Кто публикует offline в топик статуса, когда плата замолчала?
- 2. Плата вызвала client.disconnect() перед перезагрузкой. Увидит ли панель offline?
- Завещание — поля CONNECT-пакета: connect(id, user, pass, willTopic, willQos, willRetain, willMessage)
- Публикует воля брокером по тишине: 1,5 × keepAlive, при 15 с — около 22 секунд
- Ставьте willRetain true и топик тот же, что у статуса: offline перезапишет online для всех будущих подписчиков
- Штатный disconnect() сжигает завещание; после возврата плата обязана сама выложить online с retain
Частые вопросы по теме урока
- Кто публикует offline в топик статуса, когда плата замолчала?
- Завещание зарегистрировано в CONNECT, и следит за пингами брокер: полтора keepalive тишины — и он закрывает соединение и публикует волю сам. Брокер, когда тишина превысила 1,5 × keepalive
- Плата вызвала client.disconnect() перед перезагрузкой. Увидит ли панель offline?
- Disconnect — это честное прощание: брокер стирает волю и ничего не публикует. Поздравляю, вы вручную повторили случай «панель врёт три дня». Нет: чистый уход сжигает завещание, retained висит старым