Затвор ворот во дворе раз в десяток нажатий молчал, и первая мысль была дежурной: поднять QoS с нуля до максимума — надёжнее же. Открыл документацию PubSubClient и упёрся в смешное: у publish() вообще нет параметра QoS. Ни нулевого, ни второго — библиотека шлёт всегда с нулевым уровнем, и это не недоработка, а осознанный выбор автора. Разбираюсь здесь, что теряет и что экономит плата на каждом уровне, почему двойка на слабой плате — балласт и где проходит граница, за которой нулевого действительно мало. Арифметика уровней в общих чертах уже была в уроке про QoS — я не повторяю её, а считаю цену в пакетах и байтах RAM.
Интуиция «надёжнее значит выше уровень» подводит в обе стороны. HTTP-клиентов мы привыкли обвешивать ретраями руками: таймаут, повтор, пауза с ростом — и в MQTT по привычке тянем максимальную гарантию. Только каждый уровень — не флажок в настройке, а живой обмен пакетами, который ест эфир, RAM и миллисекунды цикла. На настольном сервере это незаметно, на плате с радиомодулем за доллар — заметно сразу.
Три уровня — три цены
QoS 0 — «максимум один раз»: один пакет PUBLISH и всё. Потерялся — никто не заметит: ни подтверждения, ни повтора. Звучит страшно, пока не вспомнишь, под чем едет MQTT: TCP сам ретранслирует сегменты внутри живого соединения, поэтому нулевой уровень теряет сообщения в основном на обрывах — роутер перезагрузился, Wi-Fi отвалился, стык с LTE дрогнул. Телеметрия, которая идёт каждые десять секунд, такое прощает: следующее показание исправит график без всяких гарантий доставки. Потерянное значение живёт в графике дыркой шириной в один период — я называю это надёжностью повтором, и для показаний её хватает.
QoS 1 — «минимум один раз»: отправитель ждёт PUBACK на свой packet-id и, не дождавшись, шлёт пакет заново с пометкой dup. Доставка гарантирована, единственность — нет: подтверждение могло потеряться уже после доставки, и подписчик получит сообщение дважды. Цена: два пакета на сообщение в лучшем случае, память о неотправленном на отправителе и терпимость к дубликатам на получателе. Практическое следствие: обработчик команды обязан переживать дубликат, и это дешевле, чем кажется, — показываю после скетча.
QoS 2 — «ровно один раз»: четырёхтактный обмен, который давит и потери, и дубликаты:
PUBLISH (id=42) -> # публикация <- PUBREC (id=42) # брокер получилPUBREL (id=42) -> # издатель подтвердил готовность <- PUBCOMP (id=42) # сделка закрыта # QoS 1 - половина этого: PUBLISH (id=42) -> ... <- PUBACK (id=42)# QoS 0 - только PUBLISH, без id и без ответаЧетыре пакета вместо одного, состояние транзакции на обеих сторонах, повторные такты при любой заминке. Придуман уровень для случаев, где дубликат дороже потери: оплата, учёт, накопительные команды. В домашнем MQTT я не встретил ни одного топика, где повторное срабатывание стоило бы денег: счётчики шлют накопленные итоги и дубликат итога безвреден, реле ждут абсолютных команд on/off, дубликат которых тоже безвреден. Мой вывод, готовый к спору: в квартире QoS 2 не нужен никогда, а более спорная половина вывода в том, что и QoS 1 нужен реже, чем кажется. Я включаю его там, где потеря команды видна глазом и жалуется человек: ворота, замок, насос. Всё остальное чинится повтором, retain и здравым смыслом. Двойка на слабой плате плоха ещё и памятью: клиент обязан держать транзакцию до PUBCOMP, а очереди-confirmations на ESP8266 с его 80 КБ кучи — роскошь, за которую платит фрагментацией.
Что на самом деле умеет PubSubClient
Факт, который определил мой выбор раз и навсегда: publish() в PubSubClient шлёт только QoS 0 — параметра в API не существует; subscribe() умеет нулевой и первый; второй недостижим ни в какую сторону. Для ESP32 и Arduino это значит, что вся надёжность исходящих строится поверх библиотеки: retained-статус вместо гарантии доставки, повторная публикация после reconnect, подтверждение на уровне приложения. Как реле получает команду по подписке QoS 1 и приложение гасит дубликаты, разобрано в уроке про реле с подтверждением. Согласование уровней напомню одной строкой: брокер отдаёт подписчику минимум из двух обещаний — публикуете с двойкой, а он подписался с нулём, прилетит нулевой.
Простота PubSubClient — это одновременно его плата и его бонус: клиент не держит очередь исходящих с packet-id, потому что исходящих с подтверждением у него нет, и RAM на это не тратится. Клиенты с полноценным QoS 1/2 исходящих — AsyncMqttClient, стек ESP-IDF — платят за гарантии памятью, колбэками и очередями ретрансмиссий. Буфер входящих пакетов и его пресловутые 256/512 байт — отдельная история, разобранная в уроке про буфер 512.
Сетка, которую применяю без размышлений. QoS 0 — всё, что повторяется: телеметрия чаще раза в 10 секунд, пульсы, статусы с retain, логи. QoS 1 — то, что должно доехать при живом соединении: команды реле, и подписка на команды в PubSubClient ставится на первый уровень ровно поэтому. QoS 2 — не использую: единственное место, где я его видел вживую, — платный шлюз оператора, а не дом. Если сообщение обязано дождаться офлайн-клиента, это решается не уровнем QoS, а retained-сообщением и восстановлением после рестарта — механика в уроке про рестарт, и тот же принцип прикрывает команды сценам: сценарий описывает целевое состояние, а не цепочку переключений, а про паузы между попытками reconnect — в уроке про backoff. Свежую проверку здоровья я собираю дешево: узел публикует счётчик uptime раз в минуту, панель краснеет, когда счётчик замер, — у тишины нет пакета, который можно было бы гарантировать.
Теперь цена в байтах на проводе. К MQTT-пакету транспорт добавляет свои шубы: 20 байт IPv4, 20 байт TCP, поверх Ethernet — ещё 18 с преамбулой и кадром. Телеметрический пакет QoS 0 на 26 байт выезжает в сегмент примерно на 84 байта; подтверждение QoS 1 — ещё один такой сегмент в ответ, и на вялой сети это десятки миллисекунд ожидания на каждое сообщение. QoS 2 умножает всё на четыре и добавляет состояние транзакции в куче. И не байтами едиными: каждый лишний пакет — проход loop() через обработку входящих, а на слабой плате при потоке сообщений именно этот бюджет цикла и кончается. Суточная арифметика узла с пульсом: 1440 сообщений QoS 0 — сотня с небольшим килобайт в эфире, те же сообщения с QoS 2 — под полмегабайта и ворох лишних пробуждений радио.
Отсюда пропорция, которую я вывел на своём брокере: переход QoS 0 → QoS 1 — примерно двойная цена одного сообщения в эфире плюс одна RTT задержки; QoS 1 → QoS 2 — ещё двойная сверху. Платите её только там, где дубликат или потеря ломают логику, а не график. И не путайте уровни с постоянной сессией: доставка офлайн-клиенту — механизм брокера, а не QoS, и помеченный retain итог спасает офлайн лучше любого уровня.
#include <WiFi.h>#include <PubSubClient.h> const char* T_DATA = "home/pump/data";const char* T_CMD = "home/pump/cmd";const int RELAY = 26; WiFiClient net;PubSubClient client(net);unsigned long lastPub = 0;bool pumpOn = false; void onMessage(char* topic, byte* payload, unsigned int len) { String cmd = ""; for (unsigned int i = 0; i < len; i++) cmd = cmd + (char)payload[i]; // абсолютные команды: дубликат QoS 1 не щёлкает реле второй раз if (cmd == "on" && !pumpOn) { pumpOn = true; digitalWrite(RELAY, HIGH); } if (cmd == "off" && pumpOn) { pumpOn = false; digitalWrite(RELAY, LOW); }} void setup() { Serial.begin(115200); pinMode(RELAY, OUTPUT); WiFi.begin("HomeNet", "12345678"); while (WiFi.status() != WL_CONNECTED) delay(300); client.setServer("192.168.1.50", 1883); client.setCallback(onMessage); if (client.connect("pump-01")) { client.subscribe(T_CMD, 1); // QoS 1: команды не теряются при живой связи }} void loop() { client.loop(); unsigned long ms = millis(); if (ms - lastPub >= 5000) { lastPub = ms; if (pumpOn) { client.publish(T_DATA, "1"); // QoS 0: повторится через 5 с } else { client.publish(T_DATA, "0"); } }}| Уровень | Пакетов на сообщение | Что при потере | Где беру |
|---|---|---|---|
| QoS 0 | 1 | потеря молча, узнаем по пропуску в графике | телеметрия чаще раза в 10 с, пульсы, retained-статусы |
| QoS 1 | 2 плюс повторы | ретрансмиссия и возможный дубликат | команды реле и подписки на них |
| QoS 2 | 4 плюс повторы | ничего: ровно один раз | в доме не встретил: платные шлюзы и учёт с деньгами |
- 1. Почему потеря пакета QoS 0 внутри живого соединения — редкость?
- 2. Сколько пакетов минимум уносит одно сообщение QoS 2?
- 3. Какой QoS у исходящих сообщений в PubSubClient?
- 4. Издатель шлёт с QoS 2, подписчик взял топик с QoS 0. Что прилетит подписчику?
- 5. Команда реле по QoS 1 пришла дважды. Какой обработчик делает дубликат безвредным?
- QoS 0 — один пакет и молчание при потере: под ним TCP, поэтому в живом соединении теряется редко
- QoS 1 — PUBACK и ретрансмиссия с dup: минимум один раз, дубликаты возможны
- QoS 2 — четыре пакета и транзакция до PUBCOMP: ровно один раз, дома я его не включаю
- PubSubClient публикует только с QoS 0 и подписывается с 0/1: надёжность строится retain и подтверждением приложения
- Команды — абсолютные: дубликат «on» безвреден, дубликат «toggle» ломает логику
Частые вопросы по теме урока
- Почему потеря пакета QoS 0 внутри живого соединения — редкость?
- MQTT едет поверх TCP: пока соединение живо, ретрансмиссии делает транспорт. Нулевой уровень теряет сообщения в основном на обрывах, а не на шумах эфира; при явном разрыве брокер узнаёт мгновенно и тут же раздаёт волю. Под MQTT стоит TCP, который сам ретранслирует сегменты
- Сколько пакетов минимум уносит одно сообщение QoS 2?
- PUBLISH, PUBREC, PUBREL, PUBCOMP — четыре такта, плюс повторы при любой заминке. Против одного пакета QoS 0 это четырёхкратная цена эфира и состояния, а на вялом радио обмен легко растягивается на секунды. 4
- Какой QoS у исходящих сообщений в PubSubClient?
- publish() в PubSubClient не имеет параметра уровня: исходящие всегда нулевые. Подписка умеет QoS 0 и 1, второй недостижим ни в какую сторону. Захотите выше — придётся менять библиотеку, и вместе с ней появится память под очереди подтверждений. Всегда QoS 0: параметра в API нет
- Издатель шлёт с QoS 2, подписчик взял топик с QoS 0. Что прилетит подписчику?
- Каждая сторона договаривается с брокером отдельно, а доставляется минимум из двух уровней. Публиковать с двойкой в надежде протащить гарантию через слабого подписчика бесполезно — ставьте уровень там, где читают. QoS 0: брокер отдаёт минимум из двух обещаний
- Команда реле по QoS 1 пришла дважды. Какой обработчик делает дубликат безвредным?
- Дубликат безвреден, когда команда описывает целевое состояние, а не переход. Второй «on» на уже включённом реле не меняет ничего — это и есть идемпотентность; тот же приём спасает кнопки от дребезга. Абсолютные on/off и действие только при смене состояния