POST-запрос телеметрии падал на ровном месте: сервер требовал Content-Length до отправки тела, я собирал документ в String и вызывал length() — вроде бы всё сходилось. Но узел на Uno с Ethernet-шилдом через час работы начал фрагментировать кучу: String на каждом цикле то рос, то переезжал, между ними оставались дырки, и в один не самый прекрасный момент сборка тела из четырёх склеек не нашла свободного блока. Переезд на char-буфер фиксированного размера закрыл вопрос за вечер и заодно заставил посчитать байты честно — до отправки, а не постфактум. С тех пор расчёт размера ведёт библиотека, а не калькулятор: документ живёт своей жизнью, и угадывать длину — занятие проигрышное.
Печать документа — это два вопроса: сколько байт займёт печать и куда её положить. Библиотека отвечает на оба: measureJson() считает длину будущей печати, serializeJson() кладёт её в монитор, String или char-массив и возвращает, сколько записала. Замер мы трогали в уроке про оценку памяти, печать в монитор — в уроке про свою сборку JSON; здесь собираю пару в рабочий конвейер для HTTP и UART. Пара стоит одной функции: посчитал — записал — сверил — отправил, и каждый шаг в мониторе виден отдельно.
Разберём три вещи: расчёт размера с нуль-терминатором, тихую обрезку при тесном буфере и выбор между char-массивом и String — с числами и границами применимости, а не с лозунгами. Числа ниже сняты с Uno в руках, а не из чужих статей.
Сколько байт просит документ
measureJson(doc) возвращает длину печати без завершающего нуля: для пакета из трёх телеметрических полей это порядка 40-60 символов, и ровно столько съест канал. Буфер под печать просите на один байт больше — нуль-терминатор нужен, если после печати вы передаёте буфер строковым функциям вроде Serial.print(buf) или strlen(). Формула, которую я пишу уже без вспоминания: размер буфера — measureJson(doc) + 1, а Content-Length для HTTP — сам measureJson(doc) без единицы. Разница в один байт между этими числами — источник половины тесных буферов у новичков: печать влезла, ноль — нет.
Длина зависит от значений, и это ловит всех: число 2.5 занимает три символа, целочисленное 2 — один; статус «ok» против «reconnect_away» — разница в девять байт на каждом пакете. Прикидку по образцу из статьи я делаю с запасом в полтора раза, но финальный расчёт веду на живом документе: собрал — померил — отправил. Место под буфер выделяю один раз на максимальный вариант пакета, а не под средний: где считать ёмкость с запасом на состав пакета — мы разбирали в уроке про NoMemory и ёмкость. Память под максимум стоит нулевых усилий: константа вместо расчёта на каждом цикле.
Про pretty-режим держите в голове отдельную полку: serializeJsonPretty вставляет пробелы и переводы строк, тот же пакет раздувается с 46 символов до 180-200, и для канала это чистый налог без всякой пользы. Красивую печать я оставляю мониторам и отладке, каналам — минифицированный вид. Для ESP32 с широкой трубой налог незаметен; на GSM-связке через GPRS он превращается в секунды на каждом пакете.
#include <ArduinoJson.h> // Тело POST-запроса: буфер объявлен заранее, размер посчитан measureJson.// Тот же buf уходит в client.print(buf) после Content-Length. JsonDocument doc; // v6: DynamicJsonDocument<256> doc;char buf[160]; void sobrat() { doc.clear(); doc["up"] = millis() / 1000; doc["t"] = analogRead(A0) / 10.24; // условная температура 0-99 doc["st"] = "ok";} void otpravka() { int mera = measureJson(doc); // длина печати без нуля int n = serializeJson(doc, buf, sizeof(buf)); Serial.print("mera="); Serial.print(mera); Serial.print(" zapisano="); Serial.println(n); if (n < mera) { Serial.println("BUF MAL: telo obrezano, otpravka otlozhena"); return; } Serial.print("telo: "); Serial.println(buf); // В HTTP перед телом шлём Content-Length: mera (без нуль-терминатора).} void setup() { Serial.begin(9600); pinMode(A0, INPUT);} void loop() { sobrat(); otpravka(); delay(3000);}char-массив против String
String удобен ровно до той минуты, когда скетч начинает собирать тело на каждый цикл: каждая склейка просит у кучи блок побольше, старый блок возвращается в общую кучу, и через пару часов работы память превращается в решето. Это поведение мы раскладывали по косточкам в уроке про строки и копии. char-массив ведёт себя противоположно: размер известен на компиляции, блок не двигается, после выхода из функции стек освобождается сам, без всякого сборщика мусора и без дырок. Стек к тому же быстрее кучи: запись в готовый массив — это копирование, а не поиск блока подходящего размера.
Границы у меня такие: тело до пары сотен байт — буфер на стеке внутри функции отправки; тело больше или живёт между циклами — глобальный статический массив с честным sizeof в вызове serializeJson. На ESP32 куча в сотни килобайт и String терпим для редких сборок, но Content-Length всё равно считают measureJson-ом: длина тела в запросе — это длина печати, а не длины служебных байтов строкового объекта. Для канала UART арифметика та же, что и у приёма: устройство приёмного буфера раскладывали в уроке про буфер UART. Правило одно на оба канала: длина печати считается до отправки, а не подглядывается из длины объекта.
Отдельный случай — когда строку из документа надо сохранить надолго, скажем имя узла до следующего цикла: копия делается strdup-ом в отдельный блок, и про это у нас целый урок про strdup. Писать char* указатель прямо на внутренности документа — классический способ прочитать мусор после doc.clear(). Копия живёт своей жизнью, документ пересобирается хоть каждый цикл — связности нет, и утечки нет.
| Вызов | Что делает | Когда беру |
|---|---|---|
| serializeJson(doc, Serial) | печатает в монитор, возвращает число байт | отладка документа на стенде |
| serializeJson(doc, buf, sizeof(buf)) | пишет в char-буфер, возвращает записанное | HTTP-тело и пакеты в UART |
| serializeJson(doc, myString) | дописывает в String, память в куче | редкие сборки на ESP32 |
| measureJson(doc) | длина печати без нуль-терминатора | расчёт Content-Length и размера буфера |
Симулятор: буфер по расчёту
Движок книги умеет measureJson и печать serializeJson в Serial, но запись в char-массив не эмулирует — и притворяться я не стану. Конвейер в симуляторе выглядит так: документ собирается заново на каждом витке loop, считается need = measureJson(doc) + 1, вердикт против константы BUF = 96 печатается словами, а готовое тело уходит в монитор ровно в том виде, в каком его ждал бы сервер. LED на D12 горит, пока пакет влезает, и гаснет в момент переполнения — телеметрия состояния буфера без всякого экрана. Заметьте, что документ пересобирается с нуля каждый виток: doc.clear() перед заполнением — не церемония, а защита от прилипших полей.
Крутаните потенциометр на A0: трёхзначное число меняется, и вместе с ним длина печати прыгает на символ-два. Порог влезания при этом не двигается — он упирается в BUF, а не в удачу. На настоящей плате ту же роль играет code-блок выше: там возврат serializeJson сверяется с measureJson, и обрезанное тело не отправляется вовсе. Разница в одну-две буквы между витками — норма: длина печати живая, и буфер просите под верхнюю планку, а не под среднее.
- Уронить буфер. Опустите BUF до 24: вердикт перевернётся, LED погаснет, а печать тела исчезнет из монитора — так выглядит переполнение до отправки, а не после.
- Толстый пакет. Добавьте поле с длинной строкой вроде примечания на 40 символов: need перепрыгнет порог, и расчёт заставит думать о размере, а не надеяться.
- Посчитать налог pretty. Рядом с need напечатайте pretty-вариант через serializeJsonPretty в Serial: разница в длине — цена пробелов и переводов строк.
- На настоящей плате. Прошейте code-блок на Uno, поднимите buf до 160 байт и следите в мониторе за парами mera/zapisano: равенство — конвейер здоров.
- Буфер под печать — measureJson(doc) + 1: единица уходит нуль-терминатору, а Content-Length — сам measureJson без единицы
- serializeJson возвращает записанное число байт: меньше measureJson — тело обрезано тихо, без всякого флага
- Длина зависит от значений: считать по живому документу, прикидку по образцу делать с запасом в полтора раза
- До пары сотен байт — буфер на стеке, больше или надолго — глобальный массив; String на Uno дробит кучу склейками
- serializeJsonPretty — налог канала: тот же пакет в три-четыре раза длиннее за счёт пробелов и переводов строк
- 1. Печать документа занимает 86 символов. Сколько просить под буфер?
- 2. serializeJson вернул 60, measureJson — 86. Что произошло?
- 3. Почему String плох для сборки тела на каждый цикл на Uno?
- 4. Content-Length для HTTP-запроса с телом-документом берут как:
- 5. serializeJsonPretty против минифицированной печати:
Расчёт размера и тесный буфер — половина конвейера; вторая половина начинается, когда вместо вашего пакета приходит чужой: без ключа, с текстом вместо числа, со значением вне диапазона. Разбор такой пакет примет со счастьем — и молчаливый ноль вернётся уже на применении значений. Как ставить заслон из containsKey, is и as — финальный урок цикла: валидация входящего JSON. Домашнее задание: в своём телеметрическом скетче замените String на char-буфер с расчётом через measureJson и посмотрите свободное ОЗУ до и после часа работы — разница обычно говорит сама за себя. Если разница заметная, конвейер с char-буфером уже окупился: память перестала зависеть от числа отправок.
Частые вопросы по теме урока
- Печать документа занимает 86 символов. Сколько просить под буфер?
- measureJson считает печать без завершающего нуля. Буфер — 87: единица уходит нуль-терминатору, без которого Serial.print(buf) и strlen уедут за конец массива. Обратная сторона: Content-Length в запросе — 86, без единицы. 87 — плюс байт на нуль-терминатор
- serializeJson вернул 60, measureJson — 86. Что произошло?
- Возврат serializeJson — сколько байт реально записано. Число меньше measureJson — тихая обрезка: в буфере JSON без закрывающей скобки, отправлять его нельзя. Получатель увидит незакрытый документ и откажет по разбору. Буфер тесен: записано 60 байт, хвост обрезан
- Почему String плох для сборки тела на каждый цикл на Uno?
- Каждая склейка просит блок побольше, старый возвращается в кучу, между блоками остаются дырки. char-массив фиксированного размера не двигается и стеком освобождается сам. На ESP32 сотни килобайт прощают, на Uno — нет. Склейки двигают блоки и дробят кучу: после часа работы — решето
- Content-Length для HTTP-запроса с телом-документом берут как:
- Сервер ждёт длину самого тела: это длина печати, measureJson без единицы. Единица нужна только буферу — под нуль-терминатор, который по каналу не уезжает. Размер всего буфера в заголовок не пишут. measureJson(doc) — длину печати без нуля
- serializeJsonPretty против минифицированной печати:
- Пробелы и переводы строк раздувают пакет с 46 символов до 180-200 на том же составе полей. Красивую печать оставляют мониторам, каналам — минифицированный вид: pretty в моих скетчах живёт за отладочным флагом. короче на десять процентов