Узел телеметрии на даче должен был раз в минуту отправлять на сервер строку вида {"temp":21.4,"humi":58}. Задача на десять минут — так я думал, пока собирал её на String: temp, двоеточие, значение, запятая. Первая версия ушла на сервер и вернулась с ошибкой 400: я забыл кавычки вокруг ключей. Вторая — с кавычками — падала раз в сутки: строка росла, String перевыделял память в куче, и после сотен таких перевыделений на Uno с её 2 КБ наступала фрагментация — свободные байты в ОЗУ есть, но не одним куском, и очередное выделение возвращает ноль.
Дальше — как по сценарию: я чинил запятые между полями, потом хвостовую запятую после последнего поля, которую мой сервер принимал в штыки, потом хвост «21.40» у чисел. Три вечера. Потом заменил всё это на четыре строки с ArduinoJson: doc["temp"] = t;, doc["humi"] = h;, serializeJson(doc, Serial); — и с тех пор эта часть скетча не беспокоила ни разу.
В этом уроке соберём свой JSON из показаний датчиков, напечатаем его компактно и нарядно, разберём по косточкам, почему склейка String для этой работы не годится — не «не рекомендуется», а именно не годится, с механикой, — и соберём в симуляторе телеметрию, которая каждые две секунды печатает свежий JSON.
Почему не String-конкатенация
Формально склейка работает: строка плюс число, плюс ещё строка — и JSON готов. Падает она по трём причинам, и все три инженерные, а не вкусовые. Первая — пунктуация и экранирование. Кавычки вокруг ключей и строковых значений надо не забыть, запятые ставить между парами, но не после последней, а кавычки внутри строк экранировать бэкслешем. Каждый новый формат ответа — новый шанс ошибиться, и ошибка видна не в IDE, а на сервере, в ответе 400, через час полёта.
Вторая причина — числа. У String нет понятия «напечатать число как JSON-число»: дробное значение 21.4 превращается в текст «21.40» с хвостом, который часть серверов сочтёт строкой, а не числом, и отвергнет. Отключать хвост придётся вручную, ещё один кусок кода, который надо писать, отлаживать и потом никогда не трогать.
Третья и главная причина — память. String хранит данные в куче, и каждое += при нехватке места вызывает перевыделение: скопировать буфер на новое место, старое освободить. На ESP32 с сотнями килобайт кучи это терпимо; на Uno с 2 КБ SRAM начинается фрагментация: после сотен перевыделений свободные байты есть, но разорваны на лоскуты, и выделение даже небольшого куска падает. Скетч работает час и умирает — я через это прошёл, о чём честно написал выше. Мнение с условием: я не против String вообще — для печатей в Serial он прекрасен; но если строка собирается в цикле скетча, живущего месяцами, я пишу её через ArduinoJson или в заранее выделенный char-буфер. Условие простое: строка живёт дольше секунды — ей не место в динамической склейке.
Проверьте себя: если в скетче есть строка вида s = s плюс что-то внутри loop, и жить скетчу неделями — у вас уже стоит мина. Она не рванёт на столе, где скетч поработал десять минут, и не рванёт на второй день. Она рванёт через две недели, когда вы будете в отъезде, и телеметрия тихо перестанет идти. Удалённый узел чинится дорогой ценой, поэтому я отношу отказ от склейки к правилам гигиены, а не к оптимизации.
doc[ключ] = значение: документ как контейнер
Запись в документ зеркальна чтению из прошлого урока: doc["temp"] = 21.4; кладёт число, doc["device"] = "teplica1"; — строку, doc["ok"] = true; — булево. Типы ArduinoJson различает сам: при сериализации число напечатается без кавычек, строка — с ними, булево — словом true или false. Записали ключ — он существует; записали снова — значение перезаписалось, дублей в документе не появляется.
Порядок полей в JSON не важен ни одному нормальному парсеру — но ArduinoJson всё же кладёт их в документ в порядке записи и не перетасовывает: собранный вами {"temp":21.4,"humi":58} уйдёт на сервер ровно в этом виде. Если поле в печати приходит пустым — проверяйте, что присвоение выполняется до serializeJson, а не после: сериализация фиксирует документ в тот момент, в который вы её вызвали, а не «в конце программы».
Вложенные объекты и массивы на реальной плате создаются в той же манере: doc["dht"]["temp"] = t; — библиотека сама заведёт промежуточный объект. В симуляторе ограничимся плоскими ключами: эмуляция упрощённая, а телеметрии из четырёх-пяти скаляров хватает с головой. Вложенность, фильтры и массивы — темы отдельных уроков этого раздела, там им и место.
Есть и приятный побочный эффект: собранный документ можно менять без пересборки. Положили телеметрию, напечатали — потом поправили один ключ на аварийное значение и напечатали снова: структура та же, значение новое. Для отладки это удобно — сервер получает один и тот же формат, а вы в тестах гоняете граничные числа, не трогая код формирования.
#include <ArduinoJson.h> JsonDocument doc; void setup() { Serial.begin(9600);} void loop() { float t = 21.4; // здесь будет чтение датчика int h = 58; doc["device"] = "teplica1"; doc["temp"] = t; doc["humi"] = h; serializeJson(doc, Serial); // компактно: {"device":"teplica1","temp":21.4,"humi":58} Serial.println(); // serializeJsonPretty(doc, Serial); // отладочная печать с отступами delay(5000);}serializeJson и serializeJsonPretty: печать и цена байта
serializeJson(doc, Serial) пишет документ в Serial в компактной форме: без пробелов и переводов строк. Второй аргумент — куда печатать, и это не обязательно Serial: на реальной плате туда встаёт Wi-Fi-клиент, файл на SD-карте — любой объект, умеющий Print. Библиотека сама соберёт строку целиком, без единого перевыделения памяти со стороны вашего кода — ровно то, чего не давала String-склейка.
serializeJsonPretty(doc, Serial) — та же запись с отступами и переводами строк. Человеку приятно, каналу дорого: каждый уровень вложенности обрастает пробелами, и тот же документ распухает. Ориентир из практики: компактная телеметрия из четырёх полей занимает 55–65 байт, нарядная — 150–200. При 9600 бод Serial печатает порядка 960 байт в секунду, значит компактная версия уходит за 60–70 мс, нарядная — за пятую часть секунды. Для отладки — пожалуйста; в брокер или на сервер — компактная.
Полезен и вариант с char-буфером: serializeJson умеет печатать не только в Print-приёмник, но и в заранее выделенный массив — тогда размер ответа известен заранее, и куча не участвует вовсе. Компактная телеметрия из четырёх полей укладывается в 64–80 байт с запасом, вот ориентир на размер буфера. В симуляторе этот вариант не эмулируется; на плате он выручает, когда динамическая память в проекте и вовсе под запретом.
И не путайте цену печати с ценой отправки: serializeJson в Serial стоит только времени печати, а что вы дальше делаете со строкой — ваш выбор и ваш трафик. Телеметрия в 60 байт раз в минуту — это около 86 КБ в сутки; на GSM-тарифе с пошаговой оплатой уже интересно, а нарядный JSON в 200 байт — втрое дороже. Байты на канале считаются не потому, что всё плохо, а потому что их можно не платить.
В симуляторе поддерживается компактная serializeJson — pretty остаётся угощением для Serial-монитора на настоящей плате. Собираем дашборд в консоль: документ глобальный, значения обновляются по таймеру на millis() без delay, и каждые две секунды в Serial уходит свежий JSON с перезаписанными ключами.
- Хвост истории. Уберите строку
doc["up"] = millis() / 1000;и запустите: документ станет короче на несколько байт — каждое поле стоит места, и это видно в печати. - Перезапись. Поменяйте строку устройства на teplica2 и убедитесь: ключ перезаписался, а не задвоился — вторая запись в тот же ключ изменяет значение.
- Порядок полей. Переставьте присвоения местами — humi первым — и посмотрите на печать: поля уходят в том порядке, в каком вы их записали.
- На настоящей плате. Раскомментируйте serializeJsonPretty рядом с компактной версией и сравните длину обеих печатей: разница — цена отступов, которую платит канал.
- Собирать JSON конкатенацией String не надо: экранирование кавычек, запятые между парами и realloc в куче на 2 КБ Uno — три источника падений
- Запись зеркальна чтению: doc[ключ] = значение; повторная запись перезаписывает поле, дублей не появляется
- Типы ArduinoJson различает сам: число печатается без кавычек, строка — с ними, булево — словом true/false
- serializeJson(doc, Serial) печатает компактный JSON; второй аргумент — любой Print-приёмник: Serial, Wi-Fi-клиент, файл на SD
- serializeJsonPretty добавляет отступы: телеметрия из четырёх полей растёт с 55–65 байт до 150–200
- Обновление по millis() без delay: документ живёт глобально, ключи перезаписываются, свежий JSON уходит каждые N мс
- 1. Какая из причин не собирать JSON конкатенацией String — главная на Uno?
- 2. Что напечатает serializeJson после doc["ok"] = true;?
- 3. Чем serializeJsonPretty отличается от serializeJson?
- 4. Что произойдёт, если записать doc["temp"] дважды с разными значениями?
Дальше по разделу — чтение с фильтрами, вложенные объекты и массивы, момент, когда документ на 2 КБ Uno становится тесен и пора переезжать на ESP32. А сразу за ArduinoJson в курсе стоит MQTT: собранный здесь JSON — ровно то, что публикуется в брокер строкой. Домашнее задание с настоящей платой: подключите DHT22, соберите документ из температуры и влажности и печатайте его в Serial раз в пять секунд — это готовый скелет телеметрии, который переживёт и смену датчика, и смену сервера.
Частые вопросы по теме урока
- Какая из причин не собирать JSON конкатенацией String — главная на Uno?
- Склейка в живущем месяцами скетче изнашивает кучу: свободные байты остаются, но разорваны на лоскуты, и выделение падает. Каждое += может вызвать realloc в куче — на 2 КБ SRAM это путь к фрагментации
- Что напечатает serializeJson после doc["ok"] = true;?
- Библиотека различает типы: булево печатается словом true, ключ — в кавычках, число — без кавычек. "ok": true
- Чем serializeJsonPretty отличается от serializeJson?
- Pretty — тот же документ, но с отступами и переводами строк: удобно для отладки, дорого для канала. Печатает документ с отступами — за счёт дополнительных байт
- Что произойдёт, если записать doc["temp"] дважды с разными значениями?
- Ключ в документе один: повторное присвоение меняет значение, дублей не заводится. Останется последнее значение — ключ перезаписан