Телеметрию теплицы я собирал присвоениями в StaticJsonDocument<192>: шесть полей, узел на Nano, всё помещалось с запасом — так я думал два месяца. В июне добавил в пакет восьмое поле с уровнем воды, присвоение прошло без предупреждений, а сервер начал получать пакет без последней пары. Присвоения doc["k"] = v кода не возвращают: при нехватке места размещение молча срывается, и единственным симптомом был обрезанный пакет в журнале сервера. Три дня я проверял датчик воды, а не документ.
Код NoMemory из урока про коды разбора здесь стоял бы честно — если бы пакет собирался deserializeJson. При сборке присвоениями кода нет, есть тихий пропуск; при разборе код есть, но он говорит лишь, что места не хватило, а не сколько и на чём. Вопрос, который стоит разобрать до следующего тихого пропуска: сколько памяти просить под документ и как это посчитать, не угадывая. Расчёт у приёма простой: десять минут с калькулятором против трёх дней поиска обрубка пакета в чужом журнале.
Считаем по слотам и строкам, сверяем печать measureJson из урока про размер документа, смотрим, что из расчёта живёт в седьмой версии, где ёмкость не задаётся вовсе, и где брать точную цифру, если салфетка не устраивает.
Арифметика документа: слоты плюс строки
Документ — не строка, и это главная ловушка расчёта. Пакет {"temp":21.4} печатается на 13 байт, а места занимает больше: каждый ключ, каждое значение, каждый вложенный объект и массив живут собственным слотом, и на 32-битных платах слот стоит порядка 16 байт, на восьмибитных AVR — примерно вдвое меньше. Сверху добавляются строки: скопированное значение "teplica1" занимает свою длину плюс ноль-терминатор. Печать и ёмкость — разные величины, и путать их — верный способ получить NoMemory на пакете, который «заведомо влезает». Замечание для строк: они бьют по ёмкости сильнее всего — каждое слово в пакете платит своей длиной плюс терминатором, поэтому длинные названия устройств и названий узлов выносят в цифровые идентификаторы.
Разберём живой пакет: {"dev":"t1","t":[21.4,20.8,19.9],"ok":true}. Слоты: три пары ключ-значение — шесть, вложенный массив — один, три его значения — три; всего десять. Строка "t1" — три байта с терминатором. На ESP32 выходит 160 плюс 3 — порядка 163 байта при печати на 43; на AVR — около 83. Документ весомее печати вчетверо, и это не расточительство библиотеки, а цена быстрых чтений: по слоту на значение парсер отвечает за микросекунды.
Отсюда два инструмента. Грубый — правило из первого урока раздела: ёмкость в полтора-два раза больше длины ответа; на больших пакетах оно закрывает вопрос, потому что слоты и строки на объёме усредняются. Точный — официальный онлайн-ассистент ArduinoJson: кладёте образец ответа, он считает ёмкость по составу. Оба исходят из одного: ёмкость равна длине входа ровно в одном случае — никогда; задокументированный промах с форумов — задать ёмкость по длине строки и ждать, что структуре ничего не надо. Обратная задача считается тем же способом: телеметрия из шести пар с одним строковым именем — тринадцать слотов и строка, порядка 211 байта на ESP32 и 107 на AVR; накинете вложенный массив на три показания — плюс 64 байта на ESP32 и 32 на AVR.
В седьмой версии ёмкость из декларации исчезла, арифметика — нет: теперь она считает бюджет кучи. Десять слотов и строка — по-прежнему те же байты, просто спрос с них идёт к malloc-у по ходу роста. На ESP32 сотни килобайт делают вопрос теоретическим; на Uno с двумя килобайтами бюджет общий с буферами Serial, String и чужими библиотеками — тот же расчёт, другой адресат. Пятнадцать слотов телеметрии — четверть килобайта на ESP32 и вдвое меньше на AVR: цифры маленькие, пока рядом не встают буферы GSM-модуля и Wi-Fi-стека. Статические тексты при жёстком бюджете выносите во флеш, как в уроке про PROGMEM, — каждый вынесенный байт возвращается в кучу.
Диагностика по шагам. Разбор сорвался с NoMemory — уменьшите документ фильтром из урока про большие ответы, пересчитайте состав по слотам или возьмите ассистента. Присвоение молча потеряло поле — у документа седьмой версии есть флаг overflowed(): после сборки он вернёт true, если хотя бы одно размещение сорвалось. В шестой версии тот же эффект добирали иначе: measureJson перед отправкой показывал пакет, в котором недостающая пара видна глазом — обрубок на 43-байтном пакете замечаешь сразу. Третья поломка с тем же симптомом — буфер печати: serializeJson в char-массив при переполнении возвращает ноль, и в канал уходят старые байты от прошлого пакета; печать measureJson до отправки закрывает и этот случай.
#include <ArduinoJson.h> // Телеметрия собирается присвоениями. Присвоения кода не возвращают:// при нехватке места размещение молча срывается — ловим флагом overflowed()// и меряем печать measureJson до отправки. JsonDocument doc; // v6: StaticJsonDocument<192> doc; void sobrat(float t, int h, int press) { doc["node"] = "teplica1"; doc["t"] = t; doc["h"] = h; doc["press"] = press; if (doc.overflowed()) { // хоть одно размещение сорвалось Serial.println("dokument tesen: pole ne vlezlo"); doc.clear(); return; } Serial.print("razmer="); Serial.println(measureJson(doc)); // вес пакета до отправки serializeJson(doc, Serial); Serial.println();} void setup() { Serial.begin(9600); sobrat(21.4, 58, 748);} void loop() {}Прикидка в симуляторе
Пул в симуляторе не виден — договоримся об этом сразу. Числа слотов в скетче ниже посчитаны руками по правилу из текста: ключ, значение, вложенный массив — по слоту; а вот печать measureJson — настоящая: движок считает те же байты, что и библиотека на плате, поэтому столбец печати из таблицы вы проверите прямо в браузере. Рядом печатается прикидка слоты×16 плюс строки — то, что я просил бы у кучи на ESP32; для AVR умножайте на восемь.
LED на D12 загорается на самом большом пакете: оценка перевалила за сотню, хотя печать — 43 байта. Именно этот разрыв между «пакет маленький» и «документ весомый» ловит расчёт до запуска, а не после первой перезагрузки узла на объекте. Уменьшите массив в четвёртом пакете до двух значений и поправьте константу слотов на девять: прикидка опустится до 147 — одно лишнее показание стоило шестнадцать байт, и теперь это видно в цифрах.
И про соседей по куче: документ в SRAM не один. Приёмный буфер Serial — 64 байта, буфер печати под serializeJson — размер печати плюс единица, String-поля скетча — своя доля, у сетевого модуля — свои буферы. Ёмкость, выбранная вплотную к расчёту, оставляет ноль на фрагментацию — и malloc на изрезанной куче отвечает отказом даже при формально свободных байтах. Мой запас — четверть кучи на Uno: не научный закон, а эмпирика узлов, которые живут без перезагрузок по полгода.
| Пакет | Слотов | Печать, байт | Прикидка на ESP32 |
|---|---|---|---|
| {"temp":21.4} | 2 | 13 | 32 байта |
| {"dev":"t1","temp":21.4,"ok":true} | 6 | 34 | 99 байт |
| {"t":[21.4,20.8,19.9]} | 5 | 22 | 80 байт |
| {"dev":"t1","t":[21.4,20.8,19.9],"ok":true} | 10 | 43 | 163 байта |
- Печать и ёмкость — разные величины: слот на 32-битных платах стоит порядка 16 байт, на AVR примерно вдвое меньше; строки копируются длиной плюс терминатор
- Считайте слоты по составу: пара ключ-значение — два, вложенный массив — один плюс его значения; прикидку сверяйте печатью measureJson
- Ёмкость по длине входа — задокументированный промах: структуре нужны свои байты, поэтому правило раздела — полтора-два размера ответа
- Присвоения doc["k"] = v при нехватке места срываются молча — флаг overflowed() показывает это после сборки; код NoMemory остаётся за deserializeJson
- Точную ёмкость по живому образцу ответа даёт официальный онлайн-ассистент; добавляйте запас на поля, которых в образце ещё нет
- 1. Пакет печатается на 43 байта. Сколько места занимает разобранный документ на ESP32 по прикидке слотов?
- 2. Присвоение doc["press"] = 748 молча не сработало. Какой инструмент седьмой версии это покажет?
- 3. Ответ сервиса — 1500 байт. Какая стартовая ёмкость по правилу раздела не сорвёт разбор в шестой версии?
- 4. Разбор сорвался с NoMemory на Uno. Что из перечисленного НЕ решает проблему?
- 5. Что даёт официальный онлайн-ассистент ArduinoJson?
Три урока этой серии закрыли бюджет с двух сторон: приём — чанками, фильтрами и потоком; документ — расчётом слотов и строк. Свести всё в один работающий узел удобно по готовому образцу: в итоговом погодном клиенте раздела запрос, разбор и печать размера собраны в один скетч, который живёт в поле месяцами. Домашнее задание: посчитайте слоты своего телеметричного пакета, сверьте печать через measureJson в симуляторе этого урока — и проверьте честно, сколько байт вы до сих пор просили у платы на глаз.
Частые вопросы по теме урока
- Пакет печатается на 43 байта. Сколько места занимает разобранный документ на ESP32 по прикидке слотов?
- Десять слотов по шестнадцать байт плюс строка — порядка 163 байта. Печать — только байты текста; документ содержит ещё структуру из слотов для быстрых чтений по ключу. Около 163 байта: слоты и строки дороже печати
- Присвоение doc["press"] = 748 молча не сработало. Какой инструмент седьмой версии это покажет?
- Присвоения кода не возвращают: при неудаче размещение пропускается, и документ помечается флагом overflowed(). Код NoMemory принадлежит deserializeJson, а не присваиваниям. Флаг overflowed() после сборки
- Ответ сервиса — 1500 байт. Какая стартовая ёмкость по правилу раздела не сорвёт разбор в шестой версии?
- Ёмкость по длине входа — классический промах: структуре нужны слоты. Правило раздела — полтора-два размера ответа; точную цифру даст ассистент по образцу пакета. От 2250 — полтора-два размера ответа
- Разбор сорвался с NoMemory на Uno. Что из перечисленного НЕ решает проблему?
- Скорость порта не имеет отношения к памяти: не хватало байтов, а не времени. Фильтр, пересчёт и разгрузка кучи — три рабочих направления из диагностики. Поднять скорость Serial до 115200
- Что даёт официальный онлайн-ассистент ArduinoJson?
- Ассистент считает ёмкость по составу вставленного образца. Скармливайте живой ответ сервиса, а не желаемый, и добавляйте запас на поля, которых в образце ещё нет, — они приедут без предупреждения. Ёмкость документа по образцу конкретного ответа