У меня была дача и три платы: датчик температуры в теплице, реле насоса в сарае и дисплей с кнопками в доме. Хотелось, чтобы показания сами стекались в дом, а команда «полей грядки» уходила насосу без моего похода через двор. Я соединил платы напрямую: теплица стучалась HTTP-запросом в дом, дом опрашивал сарай, сарай — теплицу. Три устройства, а связей уже шесть, и все они рвались по очереди.
Каждая плата должна была знать адреса двух остальных. Роутер после перезагрузки раздавал новые IP — я переписывал адреса руками. Теплица теряла сеть в дождь — дом висел в ожидании ответа. Я строил сеть точка-точка, как будто устройств два, хотя зоопарк рос.
Потом я узнал, что для этого случая придумали MQTT — ещё в 1999 году, для мониторинга нефтепровода через спутниковый канал: инженеры IBM боролись с дорогой и ненадёжной связью, а получился протокол, на котором сегодня стоит половина умных домов. Идея та же, что у почты: все пишут на один адрес, все читают из своих ящиков. В этом уроке наводим порядок: брокер вместо прямых связей, топики вместо адресов, уровни QoS вместо надежды на везение — и первый скетч, который публикует показания в виртуального брокера прямо в симуляторе.
Брокер: почтамт для плат
MQTT работает по схеме издатель–подписчик, и в центре стоит брокер — сервер, к которому подключаются все устройства. Плата с датчиком публикует показания, дашборд подписан на них, и при этом они никогда не встречаются напрямую: издатель не знает, кто читает его топик, подписчик не знает, кто пишет. Отключилась теплица — дом даже не заметит: просто в топике перестанут появляться свежие значения. Добавили четвёртую плату — никого уведомлять не нужно: она просто публикует или подписывается.
Адреса сообщений называются топиками — строки через слэш, иерархией как папки: home/temp, home/kitchen/led, dacha/pump/status. Порты у протокола стандартные: незашифрованный MQTT живёт на порту 1883, вариант с TLS — на 8883. Число с условием: в домашней сети за роутером порт 1883 — нормально; как только брокер выходит в интернет, 1883 превращается в открытую дверь, и любой, кто найдёт адрес, прочитает ваши данные.
В подписках есть подстановочные знаки: плюс заменяет один уровень (home/+/temp — температура во всех комнатах), решётка — все вложенные (home/# — вообще всё про дом). Публиковать по маске нельзя, только подписываться: брокер не станет угадывать, куда вы хотели положить сообщение. Топики я строю сверху вниз: dacha/greenhouse/temp, dacha/pump/on — и через год по имени всё ещё вижу, что в нём ездит.
Мелочь, которая пригодится позже: некоторые брокеры открывают ещё и WebSocket-порт, часто 9001, — через него к MQTT подключаются браузерные дашборды, которым обычный TCP-сокет недоступен. Локальным платам он не нужен: они живут на 1883 и не подозревают о существовании соседа.
У брокера есть две функции, которые вы узнаете на вкус позже, но упомянуть сейчас стоит, чтобы не удивляться. Retained-сообщение: при publish его можно пометить как «запомнить», и брокер отдаст последнее значение каждому новому подписчику — дашборд показывает температуру сразу после открытия, не дожидаясь следующей публикации. Last will — завещание: при connect клиент обещает брокеру «если я отвалюсь без прощания — опубликуй за меня dacha/thermo/status со значением offline». Дом так узнаёт об упавшей плате.
#include <PubSubClient.h>// в скетче для ESP32 рядом нужны WiFi.h и подключение к сети WiFiClient net;PubSubClient client(net); void setup() { Serial.begin(9600); client.setServer("192.168.1.50", 1883); // адрес и порт брокера if (client.connect("thermometer-01")) { // уникальное имя клиента client.subscribe("home/led"); // слушаем команды }} void loop() { client.loop(); // обслуживает соединение client.publish("home/temp", "23.5"); // кладём показание в топик delay(3000);}Четыре вызова PubSubClient закрывают девять задач из десяти. setServer() запоминает адрес брокера и порт — 1883 в большинстве случаев. connect() регистрирует клиента под именем-идентификатором: придумайте уникальное, потому что двух клиентов с одним именем брокер не терпит — он считает, что один абонент переподключился, и выкидывает того, кто был раньше. publish() кладёт сообщение в топик, subscribe() оформляет подписку.
Скелет намеренно наивный: он публикует без проверки соединения и без reconnect-логики. В домашнем скетче так делать нельзя — Wi-Fi моргает, брокер перезагружается. Взрослая версия с автоматическим переподключением и client.loop() в millis-цикле — тема следующего урока; здесь важно почувствовать модель: один адрес, publish, подписка — и вся сеть.
Мнение, выстраданное тем самым топиком dacha/pump: имя топика — это API вашего дома, оно живёт дольше скетчей. Потратьте пять минут на схему «кто что публикует» до того, как писать код: переименовывать топики, когда на них уже подписались три дашборда, — то ещё удовольствие.
PubSubClient — не единственная библиотека для ESP32: есть асинхронные клиенты, которые живут на колбэках и не блокируют loop. Но я начинаю всегда с PubSubClient: синхронная модель наглядна, каждый вызов виден в коде глазами, а понимание протокола она даёт быстрее фреймворка с магией. Когда упрётесь в её ограничения — переходить будет легко: топики и QoS не изменятся.
Тупик: сеть, которая знала все адреса наизусть
Вернёмся к моей дачной сети точка-точка, потому что в ней хорошо видно, от чего спасает брокер. Три платы знали IP друг друга. Роутер перезагрузился — DHCP раздал новые адреса, и я полчаса перепрошивал платы, чтобы просто вернуть систему к жизни. Теплица отвалилась в дождь — дом завис на HTTP-запросе с таймаутом. А трафик был смешной: запрос с заголовками весил 200–300 байт ради пяти байт показаний, и опрос каждые пять секунд держал сеть в постоянном тарахтении.
С брокером вся эта боль сжимается до одного адреса: плате не нужны адреса соседей, только IP брокера. HTTP-опрос умер сам собой: MQTT держит постоянное соединение, и сообщение доезжает за миллисекунды после публикации, а не тогда, когда подписчик догадается спросить. До сих пор считаю: если устройству достаточно знать один адрес — это не компромисс, а правильная архитектура.
И на всякий случай: MQTT — не замена HTTP везде. Обновлять прошивку по MQTT — извращение, для файлов есть HTTP; но для коротких событий «температура изменилась», «кнопку нажали» он экономит и трафик, и нервы.
QoS: три уровня «точно доставить»
| QoS | Гарантия | Механизм | Когда я его беру |
|---|---|---|---|
| 0 | никакой: отправил и забыл | пакет может потеряться молча | телеметрия, которая идёт чаще раза в 10 секунд: свежие данные всё равно придут |
| 1 | минимум один раз | подписчик отвечает PUBACK; нет ответа — клиент шлёт заново | команды свету и реле: потерять плохо, а дубликат безвреден |
| 2 | ровно один раз | двойной обмен подтверждениями в обе стороны | счётчики и оплата; дома мне ни разу не пригодился |
Прочитайте последний столбец как памятку. Моё рабочее правило с тремя условиями: QoS 0, если данные повторяются чаще, чем раз в 10 секунд, — повторная доставка устаревшей температуры бессмысленна; QoS 1 — для всего, что щёлкает реле, там пропуск команды заметен сразу, а дубликат безвреден; QoS 2 дома не встретил полезным — двойной handshake удваивает трафик ради случая, которого в квартире не бывает.
Тонкость, о которую спотыкаются: уровень QoS договаривается дважды. Публикатор договаривается с брокером, и подписчик договаривается с брокером — отдельно и независимо. Публикуете с QoS 2, а подписались с QoS 0 — доставите один раз, как договорился подписчик. Это не баг, а следствие схемы «почтамт»: каждая сторона ведёт свои переговоры, и встречаться им не обязательно.
- Уменьшите интервал публикации с 3000 до 1000 мс и посмотрите, как чаще оживает монитор. Затем аргумент против: каждая публикация — пакет в сети и запись у брокера. Имеет ли смысл дёргать его каждую секунду, если температура меняется раз в минуту?
- Добавьте второй топик: опубликуйте в home/humidity значение random(40, 81) — у виртуального брокера появится второй поток данных.
- Смените имя клиента в connect() с deviceUno на dacha-greenhouse и запустите: имя клиента — паспорт устройства, топик — адрес сообщения. Это разные вещи, и путать их нельзя.
- Начертите таблицу топиков для трёх своих будущих устройств: датчик, реле, дисплей. Три столбца — топик, кто публикует, какой QoS. Эта привычка сэкономит часы при добавлении четвёртого устройства.
- MQTT — издатель-подписчик через брокера: устройства не знают адресов друг друга, только адрес брокера
- Топик — иерархическая строка через слэш: home/temp, dacha/pump/status; имя топика — API дома
- Порт 1883 — незашифрованный MQTT, 8883 — TLS; в интернет 1883 выставлять нельзя
- QoS 0 — без гарантии, QoS 1 — минимум один раз (PUBACK), QoS 2 — ровно один раз (двойной handshake)
- В подписках работают маски: + — один уровень, # — все вложенные; публиковать по маске нельзя
- clientId должен быть уникальным: два клиента с одним именем выкидывают друг друга
- 1. Сколько связей нужно на пятеро устройств в сети точка-точка? А в MQTT?
- 2. Какой порт по умолчанию у незашифрованного MQTT?
- 3. Что гарантирует QoS 1?
- 4. Два устройства подключились к брокеру с одинаковым clientId. Что будет?
- 5. Зачем в подписках нужны подстановочные знаки + и #?
Картина собралась: брокер вместо прямых связей, топики вместо адресов, QoS вместо надежды на везение. Осталось применить это к настоящему железу — и в следующем уроке ESP32 подключается к Wi-Fi, поднимает reconnect-логику и публикует температуру с DHT22 каждые 10 секунд. Там же найдём тупик пострашнее дачного: данные, которые обрываются ровно через 15 секунд, если забыть одну строку.
Частые вопросы по теме урока
- Сколько связей нужно на пятеро устройств в сети точка-точка? А в MQTT?
- Прямых связей N*(N-1)/2 — для пяти устройств это десять; с брокером достаточно пяти подключений к одному серверу. 10 против 5: каждое устройство подключается только к брокеру
- Какой порт по умолчанию у незашифрованного MQTT?
- 1883 — стандартный порт MQTT без шифрования; 8883 — тот же протокол внутри TLS и наружу в интернет выставляют именно его. 1883
- Что гарантирует QoS 1?
- При QoS 1 подписчик подтверждает получение (PUBACK), и без подтверждения клиент шлёт заново — отсюда дубликаты при пропаже ответа. Сообщение дойдёт минимум один раз, возможны дубликаты
- Два устройства подключились к брокеру с одинаковым clientId. Что будет?
- Брокер считает одинаковое имя одним абонентом: новое подключение разрывает старое, соединения рвутся по кругу. Брокер будет выкидывать то одного, то другого
- Зачем в подписках нужны подстановочные знаки + и #?
- + заменяет один уровень иерархии, # — все вложенные; это инструмент подписки, публиковать по маске запрещено. Чтобы одним шаблоном подписаться на группу топиков