Датчик котельной я регистрировал в Home Assistant через discovery: плата сама публикует конфиг-документ в служебный топик, и в доме появляется карточка. Я собрал конфиг со всеми приличиями — имя, класс устройства, топик состояния, единица, шаблон разбора, уникальный идентификатор — получилось 380 байтов. Вызвал publish, а он вернул 0. Сеть была жива: пинг до малины шёл, соединение стояло, телеметрия уходила как ни в чём не бывало. Полчаса я перезапускал скетч и смотрел в лог брокера — а в логе пусто: пакет даже не родился. Имя датчика в карточке я вспомнил только через неделю: discovery уезжает при старте платы, пока её не перезапустишь — ничего не появится, и тишина растянулась бы ещё дальше.
Причина сидела в константе, о которой никто не предупреждает: буфер PubSubClient по умолчанию 256 байтов. Мой конфиг в него не влез, и библиотека молча отказалась отправлять — без записи в лог брокера, без ругани в Serial, просто ноль из publish. Тот самый случай, когда тишина дороже громкой поломки: телеметрия рядом работала, и на её фоне пропажу одного пакета не видно неделями. Проверка кода возврата publish с печатью в Serial — та привычка из уроков про журнал, которой я пренебрёг в тот вечер ради скорости.
В этом уроке разбираем, из чего складывается размер пакета, как поднять буфер через setBufferSize и чем вы за это платите RAM — потому что на Uno цена вопроса вырастает до половины всей памяти платы.
Почему не влезает: анатомия пакета
У PubSubClient один буфер на всё: и на отправку, и на приём. Размер задан константой MQTT_MAX_PACKET_SIZE в PubSubClient.h — те самые 256 байтов. В буфер должен лечь пакет целиком: два-четыре байта фиксированного заголовка, затем длина топика, затем payload. Телеметрия вида dacha/temp = 23.5 занимает около 45 байтов и живёт в дефолте спокойно — тот самый payload, который мы публиковали в прошлых уроках, лежит в буфере вшестеро ниже потолка. Discovery-конфиг из шести полей — уже 300–500, а с блоком описания устройства и все 600. Граница простая: payload до двухсот байтов при коротком топике — дефолт живёт; от двухсот пятидесяти и выше — уже рулетка, потому что топик и заголовок съедают остаток.
Отсюда два симптома, которые я собрал на своих граблях. Первый — publish возвращает 0 при живом соединении: пакет не собирается, сеть даже не дёргается. Второй коварнее — входящая сторона: подписались на топик, куда брокер кладёт длинный JSON, а в callback приходят обрезанные байты. Буфер режет молча, и разбор такого обрубка честно провалится; проверка payload — из урока про subscribe и callback — хотя бы покажет, что пришло не то. Симптом общий для обеих сторон: ноль из publish или пустой документ из разбора при полностью живой связи — в ту секунду, когда смотришь на сеть, виновника там нет.
Каркас подключения при этом роли не играет: буфер не зависит ни от clientId, ни от порта, ни от брокера — это целиком настройка библиотеки на плате, и каркас из урока про первый скетч остаётся прежним. Замер до отправки у меня стал привычкой: measureJson(doc) из ArduinoJson печатает точную длину будущего payload, я прибавляю к ней топик с запасом в пять байтов — и нижняя граница для лимита готова ещё до всякого поднятого буфера.
#include <PubSubClient.h>#include <ArduinoJson.h> WiFiClient net;PubSubClient client(net);DynamicJsonDocument doc(768); const char* DISCOVERY = "homeassistant/sensor/kotel_temp/config"; void setup() { Serial.begin(115200); // ... Wi-Fi и client.setServer() как обычно ... client.setBufferSize(512); // ДО первого connect: 256 не хватит doc["name"] = "Kotel temperature"; doc["dev_cla"] = "temperature"; doc["stat_t"] = "dacha/kotel/state"; doc["unit_of_meas"] = "C"; doc["val_tpl"] = "{{ value_json.t }}"; doc["uniq_id"] = "kotel_temp_1"; char buf[512]; unsigned int n = serializeJson(doc, buf); Serial.print("payload n="); Serial.println(n); // ~180: замер до отправки int ok = client.publish(DISCOVERY, buf); Serial.print("publish ok="); Serial.println(ok); // 0 при переполнении буфера} void loop() {}Поднять лимит можно двумя путями, и я выбираю между ними по ситуации. Первый — client.setBufferSize(512) в setup(): размер становится переменной во время работы, менять легко, но под новый размер библиотека берёт память в куче. Второй — на этапе компиляции: #define MQTT_MAX_PACKET_SIZE 512 до подключения PubSubClient.h — буфер выйдет статическим, без кучи, но любое изменение размера требует перепрошивки. В PlatformIO тот же флаг пишется в platformio.ini строкой build_flags = -D MQTT_MAX_PACKET_SIZE=512: один раз настроил, и все прошивки проекта собираются с одинаковым лимитом. Помните, что буфер общий: поднимая его для исходящего discovery, вы заодно разрешаете и такие же длинные входящие — это не всегда плохо, но всегда стоит места. Для моих домашних проектов первый путь дешевле, для прошивок, что живут годами без перекомпиляции, — второй.
Числа для выбора значения. 512 закрывает типовой discovery-конфиг с запасом: пакет из 380 байтов payload, топика в 35 символов и заголовка — около 420. 1024 нужны, когда в конфиг едет блок device с несколькими полями или сообщение собрано из нескольких снимков; такие длинные payload я в MQTT вообще не гоняю, но если гоняете — считайте сначала measureJson, который замеряет длину документа до отправки; разбор этой функции и её соседей — в уроке про оценку памяти документа.
И вторая половина цены: буфер один на приём и передачу, растёт он для обоих направлений сразу. Держать 1024, когда максимум сообщений — 60-байтовая телеметрия, значит резервировать килобайт под случай, который не наступит: на Uno это половина всей SRAM, на ESP32 — незаметная мелочь.
Цена: RAM под разным капотом
| Плата | SRAM для приложения | Мой потолок буфера | Почему так |
|---|---|---|---|
| Uno / Nano (ATmega328) | 2 КБ | 256, максимум 512 | 512 — четверть ОЗУ; вместе с документом ArduinoJson память кончается на глазах |
| ESP8266 | 40–50 КБ живых | 512–1024 | WiFi-стек отъедает половину чипа; 1024 — уже под контролем |
| ESP32 | около 320 КБ | 1024 и выше | кратный запас есть; буфер теряется на фоне WiFi-стека в 150+ КБ |
Арифметика по худшему случаю. На Uno: буфер 512, документ ArduinoJson на 768, String на сборку, два Serial-буфера по 64, стек — и из двух килобайтов свободным остаётся меньше трети, ещё и без буферов сетевого шилда, которые живут в той же памяти. Я доводил такой скетч до того, что куча трещала уже на WiFi.begin; как выглядит нехватка RAM изнутри — разобрано в уроке про PROGMEM и память. На ESP32 те же слагаемые теряются в трёхстах двадцати килобайтах, и я поднимаю буфер не глядя.
Порог, с которого я начинаю беспокоиться на Uno, простой: буфер плюс документ уходит за килобайт — тогда я переписываю payload, а не поднимаю лимит. Отсюда и мои рекомендации по платам. На Uno с Ethernet-шилдом discovery-конфиги я не публикую вовсе: конфиг пишется один раз руками, а плате оставляю короткую телеметрию в дефолтных 256. На ESP8266 ставлю 512 и ужимаю документ короткими ключами. На ESP32 — 1024 по умолчанию для любого проекта с Home Assistant, ещё и потому, что документ ArduinoJson там тоже собирается без придирок к ёмкости.
Ужимать payload стоит в любом случае: ключи stat_t вместо state_topic, t вместо temperature, единицы и лишние поля — на нет. Пакет в 180 байтов влезает в дефолт, и вопрос буфера отпадает сам — это дешевле любого поднятого лимита, особенно на плате, где каждый килобайт на счету. Ниже — счётчик байтов, который я держу в голове перед каждым длинным сообщением: собери документ, замерь длину, прибавь топик и заголовок — и решение о буфере станет арифметикой, а не надеждой.
- При старте симулятор считает базовый конфиг: он влезает в 256. Пошлите more и посмотрите, как блок описания устройства переворачивает вердикт для 256, но не для 512 — это и есть цена полей, о которых вы забываете при проектировании.
- Пошлите 1024 и сравните запас с 512: резерв под случай, который не наступит, — тоже ресурс. Прикиньте, какой лимит нужен вашему конфигу и почему.
- Укоротите значения в скетче — Kotel вместо Kotel temperature, dacha/kotel/state вместо длиннее — и перезапустите: measureJson покажет экономию построчно.
- В коде урока для платы поставьте setBufferSize(1024) вместо 512 и запустите на Uno в симуляторе нельзя — прикиньте на бумаге сумму: буфер плюс документ 768 плюс стек. Вывод запишите рядом со скетчем.
- Буфер PubSubClient один на отправку и приём: в него должен лечь пакет целиком — заголовок, топик, payload
- Дефолт 256 байтов: длинный discovery-JSON не уходит, publish молча возвращает 0, брокер в логе ничего не видит
- setBufferSize(512) — до первого connect; альтернатива — MQTT_MAX_PACKET_SIZE на этапе компиляции, без кучи
- Цена — RAM: Uno с двумя килобайтами живёт в 256, ESP8266 — 512, ESP32 с 320 КБ позволяет 1024 не глядя
- Сначала measureJson, потом решение о буфере; короткие ключи экономят байты дешевле, чем поднятый лимит
- 1. publish с payload на 380 байтов вернул 0 при живом соединении. Первая версия?
- 2. Где вызывать client.setBufferSize(512)?
- 3. Во сколько раз SRAM под приложение у ESP32 больше, чем у Uno?
- 4. Что должно поместиться в буфер, чтобы публикация ушла?
- 5. На Uno понадобился длинный discovery-конфиг. Что выбираю?
Частые вопросы по теме урока
- publish с payload на 380 байтов вернул 0 при живом соединении. Первая версия?
- Библиотека собирает пакет в свой буфер и молча отказывается, если он не помещается: сеть не трогается, лог брокера пуст, телеметрия рядом продолжает уходить. Проверка кода возврата publish здесь — единственный сигнал, и потому она обязательна. Пакет не влез в буфер 256 байтов
- Где вызывать client.setBufferSize(512)?
- Размер подбирается под соединение: вызов до connect действует сразу, после — только со следующего подключения. Одного раза в setup достаточно, повторять перед каждым publish не нужно — библиотека помнит размер между вызовами. В setup() до первого connect
- Во сколько раз SRAM под приложение у ESP32 больше, чем у Uno?
- У ATmega328 — 2 КБ SRAM, у ESP32 приложению доступно порядка 320 КБ. Отсюда разница в смелости с буферами и документами ArduinoJson. Примерно в 160 раз: 320 КБ против 2 КБ
- Что должно поместиться в буфер, чтобы публикация ушла?
- Буфер один на пакет: два-четыре байта заголовка, длина топика и тело сообщения. Короткий payload при длинном топике тоже может не влезть — считайте пакет целиком, а не только payload. Заголовок, топик и payload целиком
- На Uno понадобился длинный discovery-конфиг. Что выбираю?
- 2048 на Uno — вся SRAM против одного буфера, и куча ляжет раньше буфера; брокер части не склеивает, это три разных сообщения, каждое со своим топиком. Сокращённый конфиг и умеренный лимит — единственная дорога. Резать payload короткими ключами и держать буфер не выше 512 с полной проверкой RAM