Телеметрию дачного узла я складывал в char-буфер перед отправкой в GSM-модуль: buf[64] — «посчитал на глаз». Пакет {"device":"teplica1","temp":21.4,"humi":58} весит 43 байта, запас в треть — на столе всё уходило красиво. Через месяц я добавил в пакет давление и время работы: "press":748 и "up":3600 — пакет стал 65 байт, а вместе с ноль-терминатором понадобилось 66: serializeJson в переполненный буфер вернул ноль, я ноль не проверил и стал отправлять буфер как есть. В канал ушли старые байты от прошлого пакета, сервер получал кашу. Диагноз поставила одна функция, которую мне стоило позвать ещё в первый день, — measureJson.
Считать байты до печати — та же гигиена, что проверять код разбора: один вызов, ноль побочных эффектов, а на выходе точный размер будущего пакета. На Uno с её 2 КБ SRAM это ещё и вопрос выживания: документ, буфер печати, приёмный буфер Serial на 64 байта и чужие библиотеки делят одну и ту же память, и у кого-то из них всегда кончается первой.
В этом уроке освоим measureJson по трём задачам: размер буфера под serializeJson с честным запасом, вес пакета на канале с пересчётом на сутки и бюджет памяти платы — сколько можно брать под документ, чтобы не упереться в потолок. Все числа ниже не «примерно на глаз», а замеренные: симулятор печатает настоящую длину, и вы проверите каждую цифру руками.
Буфер под печать: формула с запасом
measureJson(doc) возвращает size_t — число байтов компактного JSON, ровно столько, сколько запишет serializeJson(doc, Serial) или serializeJson(doc, buf, sizeof(buf)). Пустой документ весит 2 байта — пара фигурных скобок. Функция ничего не печатает и не строит строку: чистый подсчёт, который стоит микросекунды. Первый замер мы делали ещё в уроке про JsonDocument седьмой версии, когда смотрели, как пул растёт от поля к полю; теперь пускаем ту же функцию в дело.
Формула буфера: measureJson(doc) плюс один байт под ноль-терминатор — serializeJson в char-буфер дописывает \0 в конец, как любой честный C-строчный приёмник. И плюс запас 20–30% под поля «на вырост»: серверы добавляют поля без спроса — мы это наблюдали в прошлом уроке на InvalidInput, — а пакет из 43 байта после двух добавленных полей стал 65. Телеметрии из трёх полей хватает buf[64]; если же печать идёт не в буфер, а в Serial или сетевой клиент, буфер не нужен вовсе — приёмник принимает байты потоком, и measureJson остаётся только для журнала и трафика.
Число measureJson проверяется руками и без библиотеки — так вы поймёте, откуда берутся байты. Пакет {"temp":21.4}: две фигурные скобки — 2, ключ temp в кавычках — 6, двоеточие — 1, значение 21.4 — 4 символа; итого 13 — ровно из таблицы ниже. Строки дороже чисел: кавычки появляются и вокруг значения; булево пишет само слово true или false; каждая следующая пара добавляет запятую перед собой. Такая арифметика на салфетке ставит буфер за минуту, а measureJson потом подтверждает до байта.
Число measureJson совпадает с возвращаемым значением serializeJson — байтами записанными: печать отдала 30 из 43 — значит, приёмник-поток оборвался на середине, и сверка цифр до и после печати ловит усечение. И замер это двусторонний: measureJson считает только компактную печать, для pretty-версий числа умножаются в уме на два-три.
Про pretty-печать отдельная оговорка: serializeJsonPretty расставляет отступы на каждый уровень вложенности, и размер уезжает в два-три раза от компактного: тот же плоский пакет в 13 байт в pretty-печати занимает уже под тридцать — переводы строк у каждой скобки и по два пробела на уровень. Точной функции замера для pretty нет, поэтому в журнале оценка делится на два до печати. Для канала всегда компактная форма: каждый пробел на GSM-тарифе платит деньгами, а на батарейном узле — миллиамперами.
| Пакет | measureJson | Комментарий |
|---|---|---|
| Пустой документ | 2 байта | Две фигурные скобки — минимальный вес |
| {"temp":21.4} | 13 байт | Одно поле: ключ в кавычках, двоеточие, число, скобки |
| {"node":"kotelnaya","temp":21.4,"ok":true} | 42 байта | Три поля разных типов: строка, дробь, булево |
| {"sens":[18,20,22],"meta":{"id":"bme280","ok":true}} | 52 байта | Массив и вложенный объект: скобки и запятые добавляют вес |
#include <ArduinoJson.h> JsonDocument doc;char buf[64]; // телеметрия в 43 байта плюс запас на новые поля void setup() { Serial.begin(9600);} void loop() { doc.clear(); doc["device"] = "teplica1"; doc["temp"] = 21.4; doc["humi"] = 58; size_t need = measureJson(doc); // считаем ДО печати if (need + 1 > sizeof(buf)) { // +1 байт под ноль-терминатор Serial.print("ne vlazaetsya: "); Serial.println(need); // диагноз вместо молчаливой порчи } else { serializeJson(doc, buf, sizeof(buf)); Serial.print("razmer="); Serial.println(need); Serial.println(buf); } // На ESP32 сюда же просится бюджет кучи: // Serial.println(ESP.getFreeHeap()); delay(60000);}Потолок платы: сколько можно брать
Теперь бюджет. У Arduino Uno 2 КБ SRAM — мы считали это в первом уроке раздела: документ на 512 байт — четверть всей памяти, плюс приёмный буфер Serial на 64 байта, плюс ваш буфер печати, плюс стек и переменные скетча — и локальные String из функций живут в той же половине счётчика. Моё правило с условием: если в проекте нет прожорливых библиотек, документ плюс буфер держите в пределах половины SRAM — до килобайта; если влез Ethernet, SD-карта или SoftSerial — каждый из них приносит свои буферы, и потолок наступает быстрее. Статические тексты выносите во флеш через PROGMEM — в уроке про PROGMEM и память это разобрано по байтам.
На ESP32 с 520 КБ ОЗУ документы на 2–4 КБ никто не заметит — но Wi-Fi-стек сам забирает порядка 50 КБ кучи, а батарейный узел платит не памятью, а эфиром. Здесь measureJson работает калькулятором трафика: пакет в 42 байта раз в минуту — это 42 × 1440 = 60480 байт в сутки, около 60 КБ; нарядная pretty-версия того же пакета умножит счёт на два-три. И трафик — не единственная цена: каждый лишний байт — это время GSM-модуля в активном режиме, а значит и расход батареи. Для NB-IoT с пошаговой оплатой и для GSM с лимитом разница — деньги, и считать их до развёртывания дешевле, чем после.
Свободную память на Uno меряют классическим приёмом с указателем стека: функция freeRam() возвращает разницу между вершиной стека и концом статических переменных — десяток строк, которые есть в каждом втором Uno-проекте. Замер интересен минимумом за сутки: если минимум ползёт вниз день ото дня — где-то течёт куча, и лучше узнать это в среду, чем в момент, когда узел замолчал. На ESP32 ту же роль играет ESP.getFreeHeap(): после старта Wi-Fi остаётся порядка 200–250 КБ из 520, и документ на 4 КБ в этом бюджете уже решение, а не деталь.
Замер полезно вести в динамике: печатайте размер по таймеру на millis() раз в секунду во время отладки — увидите, как документ дышит по фазам скетча. А для пакетов, которые живут в HTTP, вес пакета равен весу тела запроса — мы это считали при отправке телеметрии в уроке про HTTP POST.
Симулятор покажет замер целиком. Документ один, пакеты три — плоский из одного поля, телеметрия из трёх и пакет с массивом и вложенным объектом — разбираются по очереди, и после каждого measureJson печатает настоящую длину: движок считает те же байты, что библиотека на плате, поэтому числа из таблицы выше вы проверите прямо в браузере, и этот же пакет на Uno даст те же байты — печать компактного JSON не зависит от платформы. Финальный шаг — калькулятор трафика: байты телеметрии умножаются на 1440 минут суток, и абстрактная «лёгкая телеметрия» превращается в килобайты, за которые батарейный узел платит каждый день.
- Добавить поле. Вставьте в третий пакет пару "press":748 — размер вырастет ровно на длину ключа, значения и три символа пунктуации: кавычки ключа, двоеточие и запятая.
- Ужать буфер. Поменяйте buf[64] на buf[40] в code-блоке и проследите мысленно: need + 1 упрётся в потолок — ветка ne vlazaetsya напечатает честное число вместо молчаливой порчи буфера.
- Плоский против вложенного. Сложите число из sens-массива в отдельные поля того же документа и сравните measureJson: массив экономит пунктуацию, вложенность — тратит.
- На настоящей плате. На Uno добавьте замер свободной SRAM классическим приёмом с указателем стека, на ESP32 — ESP.getFreeHeap(): цифры рядом с measureJson покажут весь бюджет.
- measureJson(doc) возвращает байты компактного JSON до печати; пустой документ весит 2 байта — две фигурные скобки
- Формула буфера: measureJson + 1 байт под ноль-терминатор + запас 20–30% на поля, которые сервер добавит без спроса
- serializeJson в переполненный буфер возвращает ноль — проверяйте результат, иначе в канал уходят старые байты
- Pretty-печать уводит размер в два-три раза от компактной: в канал — компактную, с отступами — только в журнал
- Uno: документ плюс буфер — до половины 2 КБ SRAM; ESP32: память не проблема, но 60 КБ в сутки трафика — считаются
- 1. Что вернёт measureJson для только что созданного пустого JsonDocument?
- 2. serializeJson(doc, buf, sizeof(buf)) вернул 0. Что это значит?
- 3. Пакет весит 42 байта, узел отправляет раз в минуту. Сколько байт уйдёт в канал за сутки?
- 4. Буфер под serializeJson для пакета в 41 байт. Какой размер верен?
- 5. Что занимает больше байт при печати: массив [18,20,22] или три вложенных объекта с теми же числами по одному?
Размер посчитали — остался последний вопрос про байты: чьи они. После разбора строки в документе значения лежат не там, где кажется: библиотека экономит память и не всегда копирует текст, а значит, у каждой извлечённой строки есть владелец и срок жизни. Замер из этого урока оставьте в скетчах навсегда — строка razmer= рядом с печатью пакета однажды ответит на вопрос «кто добавил поле» раньше, чем вы вспомните, что меняли что-то на сервере. А кто владеет строкой после разбора, когда нужны копии и что происходит с указателем, когда буфер приёма перезаписался следующим пакетом, — финальный урок этой тройки.
Частые вопросы по теме урока
- Что вернёт measureJson для только что созданного пустого JsonDocument?
- Пустой документ печатается как {}, это 2 байта — ровно столько же measureJson и насчитает; и в v6 с заданной ёмкостью число это считало компактную печать, не ёмкость. 2 — пара фигурных скобок
- serializeJson(doc, buf, sizeof(buf)) вернул 0. Что это значит?
- Ноль при печати в буфер — признак переполнения. Диагноз ставит measureJson до печати: нужен буфер размером не меньше этой цифры плюс один байт. Документ не влез в буфер
- Пакет весит 42 байта, узел отправляет раз в минуту. Сколько байт уйдёт в канал за сутки?
- 1440 минут в сутках: 42 × 1440 = 60480 байт, около 60 КБ. Pretty-версия того же пакета умножит счёт в два-три раза. Около 60 КБ
- Буфер под serializeJson для пакета в 41 байт. Какой размер верен?
- Минимум — 42 байта, но пакет растёт: поля добавляются без спроса, поэтому берут measureJson + 1 и запас 20–30%. От 64 с запасом — например 64–96 байт
- Что занимает больше байт при печати: массив [18,20,22] или три вложенных объекта с теми же числами по одному?
- Массив пишет числа через запятую в одних скобках; вложенным объектам нужны свои ключи, скобки и двоеточия. На вес указывает measureJson в симуляторе. Массив — меньше скобок и запятых