Мой первый скетч с MQTT компилировался с первого раза, загружался, печатал в Serial бодрые строки — и брокер не видел ни одного сообщения. Час я менял адрес брокера, перезапускал Mosquitto, перепрошивал плату. Ошибка была в одном слове: я писал client.publish(...) и не смотрел, что он вернул. А возвращает publish значение bool: единица — пакет ушёл, ноль — пакет не ушёл, и никакой другой индикации нет. Ноль мой скетч печатал исправно, час подряд, прямо мне в лицо.
С тех пор у меня правило: каждый вызов, который может не сработать, — connect, publish, subscribe — проверяется на месте. Диагноз потом занял секунды: в логе брокера не было ни одного подключения с именем моей платы, а publish молча возвращал ноль. Один print после каждого вызова — и время поиска сжимается с часа до минуты; эта привычка стоит одной строки кода. PubSubClient — библиотека честная: она молчит, когда ей что-то не нравится, но всё говорит в кодах возврата и в client.state(), просто читать её надо уметь.
Весь этот урок я делаю на ESP32, но на Uno с Ethernet-шилдом W5500 всё то же самое: меняется только сетевой класс под обёрткой. Пара слов о том, что у нас под ногами: брокер из прошлого урока уже слушает 1883-й порт на 0.0.0.0, плата уже умеет подключаться к Wi-Fi — я беру за основу каркас из урока про подключение ESP32 к роутеру. Дальше только MQTT. План урока: ставим библиотеку, собираем минимальный рабочий скетч — connect, publish, loop — и учимся диагностировать отказ за минуту, а не за час.
Установка: три минуты в Library Manager
Библиотека ставится штатно: Tools — Manage Libraries — в поиске PubSubClient — ставим ту, что от Nick O'Leary, на момент моих последних проектов версия 2.8. Она тянет за собой примеры — File — Examples — PubSubClient, — и самый полезный из них называется mqtt_esp8266: не пугайтесь имени, на ESP32 он работает с той же логикой, там меняется только заголовок Wi-Fi. Установка из ZIP с GitHub ничем не лучше, а обновляться через менеджер удобнее — установка библиотек в один клик была отдельным уроком, там и вспомните, если менеджер ругнётся.
Одно знание, которое стоит получить до скетча: PubSubClient не умеет в сеть сам. Ему нужен уже готовый клиентский класс — WiFiClient на ESP32/ESP8266, EthernetClient на шилдах W5100/W5500. Поэтому в скетче всегда пара: сетевой клиент и обёрнутый в него PubSubClient. Именно эта пара — причина, почему библиотека жива больше десяти лет: она не привязана к радиомодулю, и под неё подходит всё, что умеет TCP, включая экзотику вроде SIM800 через TinyGsm.
И тонкость для перфекционистов: PubSubClient разговаривает на протоколе 3.1.1, а не на 5.0 — свежие фичи вроде reason-кодов и properties ему недоступны. Для домашней телеметрии это без разницы: брокеры совместимы со всеми версиями, и мой Mosquitto из прошлого урока принимает 3.1.1 не моргнув.
После установки загляните в примеры — File, Examples, PubSubClient. Кроме mqtt_esp8266 там живут mqtt_reconnect_nonblocking с неблокирующим переподключением и mqtt_publish_in_callback с разбором входящих. Листать их полезно как справочник: автор писал примеры под себя, и в них видны все четыре-пять приёмов, которыми я пользуюсь до сих пор. Версию менеджер показывает прямо в карточке; за последней я не гонюсь — 2.8 стабильна годами.
#include <WiFi.h>#include <PubSubClient.h> const char* WIFI_SSID = "HomeNet";const char* WIFI_PASS = "supernet123";const char* MQTT_HOST = "192.168.1.50"; WiFiClient net;PubSubClient client(net);unsigned long lastPub = 0; void setup() { Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() != WL_CONNECTED) { delay(300); } Serial.print("Wi-Fi ok, IP: "); Serial.println(WiFi.localIP()); client.setServer(MQTT_HOST, 1883); if (client.connect("first-sketch")) { Serial.println("MQTT ok"); } else { Serial.print("MQTT fail, state="); Serial.println(client.state()); }} void loop() { client.loop(); unsigned long now = millis(); if (now - lastPub >= 4000) { lastPub = now; if (client.connected()) { int h = 40 + random(0, 20); String payload = String(h); int ok = client.publish("home/humidity", payload); if (ok) { Serial.print("pub=home/humidity/0 msg="); Serial.println(payload); } else { Serial.println("publish вернул 0: пакет не ушёл"); } } }}Разбор по строчкам, потому что здесь каждая строка — чьё-то набитое ребро. setServer() вызывается один раз в setup: адрес и порт брокера. connect() принимает один обязательный аргумент — имя клиента, оно обязано быть уникальным на весь брокер: два устройства с одним clientId начнут выкидывать друг друга, и соединение будет жить секунду. У меня имена честные: first-sketch для урока, esp32-balcony для балкона, dacha-pump для насоса.
client.loop() — сердце библиотеки, и вызывать его нужно в каждом обороте loop без пауз. Внутри него две работы: уходит keepalive-пинг (окно по умолчанию 15 секунд — при длинных delay брокер отрежет соединение, об этом был подробный разбор в уроке про публикацию с ESP32), и разбираются входящие пакеты — то, ради чего живут подписки. Забытый loop — вторая по популярности причина «сначала работает, потом молчит».
Публикация в примере проверена с двух сторон: перед publish смотрим client.connected() — жива ли связь, — а после publish смотрим код возврата: ушёл ли пакет. Число с условием: publish возвращает 0 не только при мёртвой связи, но и при переполнении буфера — сообщение длиннее 256 байт по умолчанию молча не уходит; длинные JSON отправляются после client.setBufferSize(512) в setup.
Что публиковать и в каком виде — вопрос, на который я отвечаю правилом: числа строками, единицы в имени топика, никаких составных сообщений там, где хватает одного числа. 45 в home/humidity читается любой системой, а конструкция вида «h=45;p=60» придётся парсить руками и на плате-подписчике, и в дашборде. Если единицы важны — они едут в топике: home/temp/c в цельсиях, home/temp/f в фаренгейтах. Разница между «45» и «45.0» для брокера — просто два разных текста; подписчик на Arduino превращает строку обратно в число через toInt() или atof(), и для целых значений это работает ровно.
client.state(): когда брокер молчит
Если connect вернул ноль, внятного текста ошибки не будет — будет число из client.state(). Таблица ниже — моя шпаргалка, которую я перенёс на стикер к монитору. Логика чтения простая: отрицательные числа — проблемы транспорта, сети; положительные — проблемы логики протокола, и брокер честно назвал причину отказом в CONNACK.
Разберу диагностику как сценарий. Печатаем state после неудачного connect: минус два — сеть есть, а соединения нет, идём в конфиг брокера смотреть listener; минус четыре — адрес или порт, пингуем малину; четвёрка — пароль; пятёрка — анонимов не пускают, а мы их и не представили. Три минуты по этому дереву — и причина найдена. Без кода состояния те же три минуты превращаются в три часа блужданий: брокер молчит одинаково на все ошибки. Держите таблицу под рукой первые две недели — потом коды запомнятся сами.
| state | Значение | Что я проверяю первым |
|---|---|---|
| -4 | connection timeout | адрес брокера и порт; отвечает ли малина по пингу |
| -2 | connect failed | строки listener 1883 0.0.0.0 в конфиге Mosquitto |
| -1 | connection lost | не падал ли брокер; systemctl status mosquitto |
| 0 | connected | это успех — но проверяйте и после него client.loop() |
| 2 | bad client id | clientId не пустой и не длиннее 23 символов |
| 4 | bad credentials | логин и пароль совпадают с mosquitto_passwd |
| 5 | not authorized | allow_anonymous false, а пароля нет — смотрим конфиг |
- Сократите интервал с 4000 до 1000 мс и посчитайте публикации в минуту: 60 сообщений — это уже трафик, о котором стоит думать, если плата на батарейке с GSM.
- Поменяйте clientId на first-sketch-2 и запустите ещё раз: с точки зрения брокера это уже другое устройство. А теперь представьте два реальных скетча с одним именем — они начнут воевать.
- Добавьте после успешного connect вызов client.subscribe на топик home/light — подписка в этом симуляторе не получит сообщений, но в логе брокера появится факт подписки.
- Выпишите на стикер три кода state, которые встречаются чаще всего: -2, 4 и 5. Следующий раз, когда брокер молчит, вы прочитаете причину за десять секунд.
- PubSubClient от Nick O'Leary, версия 2.8; ставится через Library Manager; протокол 3.1.1
- Пара классов: WiFiClient (или EthernetClient) заворачивается в PubSubClient
- Каждый вызов проверяется: connect возвращает bool, publish возвращает bool, при отказе печатаем client.state()
- client.loop() — в каждом обороте: keepalive 15 секунд и входящие пакеты живут внутри него
- clientId уникален на весь брокер; пустой или длиннее 23 символов — отказ с кодом 2
- Буфер пакета 256 байт по умолчанию; для длинных сообщений — setBufferSize в setup
- 1. Что возвращает client.publish() при неудачной отправке?
- 2. Зачем в каждом loop() вызывать client.loop()?
- 3. connect вернул false, client.state() равен 4. Что это значит?
- 4. Два устройства подключились с одним clientId. Что произойдёт?
- 5. Длинный JSON в publish молча не уходит. Что делать?
- 6. Какой класс нужен PubSubClient для работы с сетью на ESP32?
Скелет готов и проверен: connect с диагностикой, publish с кодом возврата, loop без пауз. Дальше в разделе всё наращивается на этот каркас: retained-сообщения и last will, чтобы дом узнавал о статусе плат мгновенно, порт 8883 с TLS, когда брокер выйдет наружу, и callback с подписками, когда команды пойдут в обратную сторону. Если захочется разобрать каркас до костей — в уроке про публикацию с ESP32 этот же скетч вырастает во взрослого: с reconnect-логикой и millis-таймерами из урока про millis вместо delay. А если брокера у вас ещё нет — вернитесь на шаг назад, к установке Mosquitto: без собственной доски объявлений проверять скетч будет не на что. И проверка на владение: не глядя в код, назовите три кода state из таблицы — если выходит, скетч вы уже чувствуете, а не просто переписали.
Частые вопросы по теме урока
- Что возвращает client.publish() при неудачной отправке?
- publish возвращает bool: 1 — пакет ушёл, 0 — нет. Библиотека молчалива, поэтому код возврата — единственная честная индикация. Ноль: пакет не ушёл
- Зачем в каждом loop() вызывать client.loop()?
- Внутри client.loop() живёт весь обмен: пинг протокола (окно 15 секунд) и обработка подписок. Без него соединение умирает, а команды не приходят. Там уходят keepalive-пинги и разбираются входящие пакеты
- connect вернул false, client.state() равен 4. Что это значит?
- Код 4 — bad credentials: брокер отказал из-за логина и пароля. Сверяем их с тем, что создавали в mosquitto_passwd. Неверные логин или пароль
- Два устройства подключились с одним clientId. Что произойдёт?
- Имя клиента — паспорт: новое подключение с тем же именем разрывает старое. Соединения начнут бесконечно сменять друг друга. Брокер выкинет более старого клиента
- Длинный JSON в publish молча не уходит. Что делать?
- Буфер пакета по умолчанию 256 байт. setBufferSize(512) до первого connect решает проблему, либо ужимайте сообщение и делите по топикам. Разбить на два топика или вызвать setBufferSize(512) в setup
- Какой класс нужен PubSubClient для работы с сетью на ESP32?
- Библиотека не умеет в сеть сама: ей отдают готовый TCP-клиент — WiFiClient на ESP32/ESP8266 или EthernetClient на шилдах. WiFiClient