Конфигурация поливочного узла отправлялась на сервер при каждом включении: имя устройства, режим, пороги. Строку я собирал вручную — ключи, двоеточия, запятые — и однажды назвал теплицу «Teplica "A"», с кавычками, потому что на корпусе она была подписана маркером именно так. Кавычки внутри строкового значения JSON обязаны экранироваться бэкслешем, я об этом знал — вставил слеши руками, сервер принял конфиг, и я поставил галочку.
Через месяц на втором узле в имени оказалась запись со слешем в комментарии, а на третьем скетч-логировщик отправлял текст события, в котором попадался перенос строки. Каждый раз — ручные слеши, каждый раз — новый случай, который я не предусмотрел. Финальным аккордом был буфер: я отводил под конфиг 128 байт, конфиг вырос до 140, хвост молча отрезался, и сервер получал обрезанный JSON с ошибкой 400 — без единого сообщения со стороны скетча.
Когда я переписал отправку на serializeJson, исчезли сразу три заботы: экранирование (библиотека ставит слеши сама и не забывает ни про один случай), длина (она известна до и после печати) и пунктуация (запятые и кавычки перестали быть моей проблемой). Сборкой телеметрии через документ мы уже занимались в уроке про свою телеметрию — теперь добираемся до деталей: приёмники печати, замер длины, цена байта и экранирование.
План урока: как устроен вызов serializeJson и что он возвращает, куда можно печатать результат кроме Serial, зачем нужен measureJson, и почему экранирование — аргумент, который закрывает спор о ручной сборке окончательно.
serializeJson: документ превращается в текст
Механика зеркальна разбору из прошлого урока: doc["ключ"] = значение кладёт данные в документ, serializeJson(doc, output) выписывает их оттуда текстом. Типы библиотека различает сама: число напечатается без кавычек, строка — с кавычками, булево — словом true или false. Порядок полей в JSON не важен ни одному парсеру, но ArduinoJson кладёт их в порядке записи — документ получается предсказуемым, и в логах его удобно читать глазами.
Возвращаемое значение — число записанных байт. Им часто пренебрегают, зря: это готовая диагностика. Напечатали в Serial и записали в лог — видно, сколько ушло на канал; напечатали в буфер — сразу видно, влезло ли. Удобно и то, что документ можно печатать повторно: поменили один ключ — напечатали снова, структура сохранится, значение обновится. Мы пользовались этим в телеметрии, когда обновляли показания по таймеру без пересборки документа с нуля.
Второй аргумент — приёмник, и это не обязательно Serial. Любой объект, умеющий Print, годится: сетевой клиент, файл на SD-карте, дисплей. Выбор приёмника — это выбор цены: куда и как дорого уйдут байты. Разберём три основных варианта и одну полезную функцию замера.
Два свойства вызова, которые экономят нервы. Первое: печать не расходует документ — serializeJson можно звать хоть десять раз подряд, значения останутся на месте; сравните с ручной склейкой, где каждая сборка заново конструирует строку. Второе: между сборками разных конфигураций звените doc.clear() — я один раз забыл, и старые поля от тепличного узла ушли на сервер вместе с новыми полями поливального; сервер их молча принял, и неделю мы обсуждали, откуда в конфиге взялся чужой режим vent. И ещё: serializeJson ничего не проверяет в смысле логики — пустой документ напечатается как пара фигурных скобок, поэтому заполнение должно случиться до печати, а не «когда-нибудь потом».
#include <ArduinoJson.h> JsonDocument doc;char buf[192]; // под конфиг с запасом ~30% void buildConfig(int mode) { doc.clear(); doc["device"] = "Teplica \"A\""; // кавычки внутри значения doc["mode"] = "auto"; doc["tmin"] = 18; doc["tmax"] = 27;} void setup() { Serial.begin(9600); buildConfig(0); // Замер до печати: влезет ли в буфер int need = measureJson(doc); Serial.print("nuzhno bait: "); Serial.println(need); // Печать в буфер возвращает фактически записанные байты int n = serializeJson(doc, buf); Serial.print("zapisano bait: "); Serial.println(n); Serial.println(buf);} void loop() {}Куда печатать: Serial, буфер, сеть
Serial — приёмник по умолчанию и лучший друг отладки: напечатали — увидели. Помните только про скорость: при 9600 бод каждый байт стоит около миллисекунды, конфиг в 62 байта уходит за 62–65 мс, и если печать сидит в середине цикла управления, эти миллисекунды из него вычитаются. Для логов в Serial — пожалуйста, в горячем цикле — осмотрительно.
char-буфер — мой выбор для отправки. Размер известен заранее, куча не участвует вовсе, а значит нет и фрагментации, о которой мы говорили в уроке про String-склейку. Порядок работы такой: measureJson дал длину — сверили с размером буфера — serializeJson записал и вернул фактическое число байтов — отправили буфер клиенту. Запас я держу процентов тридцать: конфиг имеет свойство обрастать полями, и буфер впритык — бомба замедленного действия, как мой случай со 128 байтами.
Сетевой приёмник — Wi-Fi-клиент или MQTT-библиотека — печатает документ прямо в канал, без промежуточной строки: serializeJson(doc, client). Это рецепты из документации ArduinoJson для связки с MQTT-клиентом, и они экономят и память, и один лишний проход по данным. Для ESP32-проектов, где мы делали HTTP-запросы и публиковали данные в MQTT, это самый короткий путь от показаний датчика до брокера.
Файл на SD-карте — тот же Print-приёмник, и это удобный способ хранить конфигурацию: загрузили из файла, разобрали, изменили один порог — записали обратно той же serializeJson. Мы хранили конфиги на карте в уроке про CSV и конфиги на SD; JSON-вариант отличается только содержимым файла. А для HTTP-POST measureJson закрывает ещё одну задачу: сервер хочет знать длину тела заранее, в заголовке Content-Length, — а у вас она уже посчитана, осталось подставить число в строку заголовка.
Одно ограничение надо знать заранее: вывести выборочные поля нельзя — сериализуется документ целиком. Нужен частичный JSON (например, только тревожные поля) — соберите под него второй документ и печатайте его. Так делают в проектах с двумя аудиториями у данных: полный документ — в архив, короткий — в канал тревог.
Экранирование: бэкслеши ставит библиотека
В JSON экранированию подлежат кавычка, сам бэкслеш и несколько служебных символов: перенос строки, возврат каретки, табуляция. Ручная сборка требует помнить весь список и встречать каждый случай в своих данных — я и познакомился с ним по очереди: кавычки, потом слеш, потом перенос строки. Библиотека проходит по строке и ставит слеши сама, для всех случаев сразу, включая те, о которых вы подумаете только после первого сервера со строгим парсером.
Проверяется это за минуту в симуляторе: положите в документ значение с кавычками и переносом строки — и посмотрите на печать. Слеши в выводе стоят ровно там, где требует стандарт, а исходная строка в скетче осталась человекочитаемой. На настоящей плате результат будет тем же: сериализация — чистая функция от документа. Кириллица и прочий UTF-8 пишутся как есть, без юникод-подмены — разберётся тот, кто читает документ дальше. И обратите внимание на цену: каждый экранированный символ удваивается — перенос строки занимает два байта вместо одного, кавычка тоже. Замер measureJson это учитывает сам, но буфер планируйте с учётом худшего случая: значение из сплошных переносов строк распухнет вдвое.
И последнее про объём. Компактный serializeJson пишет без пробелов; нарядная serializeJsonPretty добавляет отступы, и конфиг из четырёх полей толстеет с 60 байт до полутора сотен. Для канала с оплатой за трафик или батарейного узла с радиомодулем компактная форма — не вкусовщина, а прямая экономия; pretty оставьте Serial-монитору на отладке.
| Приёмник | Когда беру | Цена и риск |
|---|---|---|
| Serial | отладка, логи, взгляд глазами | около 1 мс на байт при 9600 бод: 62 байта — 62–65 мс из цикла |
| char-буфер | отправка на сервер, размер известен, куча не участвует | буфер впритык обрезает хвост молча — держите запас 30% и сверяйте числа |
| сетевой клиент | HTTP, MQTT, печать прямо в канал без промежуточной строки | нужен клиент с методом Print; ошибка сети проверяется на его стороне |
| String | быстрый прототип в setup() | перевыделения в куче на Uno — путь к фрагментации в долгом скетче |
- serializeJson(doc, output) печатает документ в любой Print-приёмник и возвращает число записанных байт — готовая диагностика объёма
- measureJson(doc) даёт длину будущего текста до печати: сверили с буфером — потом печатали
- char-буфер с запасом 30% — мой выбор для отправки: размер известен, куча не участвует, фрагментации нет
- Экранирование кавычек, слешей и переносов строки библиотека делает сама — ручная сборка проигрывает этот пункт всегда
- Вывести выборочные поля нельзя: сериализуется весь документ; частичный JSON — это второй, отдельный документ
- Компактная печать экономит канал: конфиг из четырёх полей — 60 байт против полутора сотен у pretty-версии
- 1. Что возвращает serializeJson при печати в char-буфер?
- 2. В строковом значении документа оказалась двойная кавычка. Что попадёт в печатный JSON?
- 3. Зачем нужен measureJson, если serializeJson и так возвращает число байтов?
- 4. Нужно отправить в канал тревог только два поля из восьми. Как правильно?
- 5. Конфиг в 62 байта печатается в Serial при 9600 бод. Сколько времени это занимает?
Писать JSON вы теперь умеете в три приёмника и с честным замером длины. Осталась третья опора раздела — грамотное чтение: что возвращает doc["ключ"] для разных типов, как оператор | подставляет запасное значение, когда ключа нет, и чем isNull отличается от containsKey. Именно эти приёмы отличают скетч, который молча показывает ноль, от скетча, который говорит «данных не было». Домашнее задание: возьмите телеметрию из первого урока раздела, добавьте к каждому числу запасное значение через | и проверьте на настоящей плате, как скетч ведёт себя на пакете без половины полей.
Частые вопросы по теме урока
- Что возвращает serializeJson при печати в char-буфер?
- Возвращаемое значение — фактический объём записи. Сравнение его с размером буфера ловит обрезание хвоста до того, как заметит сервер. Число фактически записанных байтов
- В строковом значении документа оказалась двойная кавычка. Что попадёт в печатный JSON?
- Кавычка, слеш и служебные символы экранируются автоматически. В ручной сборке это был источник ошибок 400 — здесь он закрыт. Бэкслеш перед кавычкой — библиотека экранирует сама
- Зачем нужен measureJson, если serializeJson и так возвращает число байтов?
- Замер до печати позволяет решить, печатать ли вообще и куда: в буфер на 192 байта конфиг на 200 не влезет, и лучше узнать это заранее. Он даёт длину до печати — можно проверить буфер заранее
- Нужно отправить в канал тревог только два поля из восьми. Как правильно?
- Частичной сериализации нет — печатается документ целиком. Для короткого пакета тревог заводят второй документ; вырезание по позициям возвращает нас к граблям первого урока. Так сделать нельзя: сериализуется всегда весь документ
- Конфиг в 62 байта печатается в Serial при 9600 бод. Сколько времени это занимает?
- При 9600 бод каждый байт стоит около миллисекунды, поэтому 62 байта — 62–65 мс. Для лога приемлемо, для горячего цикла управления — заметно. Около 62–65 мс — примерно миллисекунда на байт