Температура на экране дачной метеостанции показывала ровно ноль. Не минус, не пустую клетку — честный ноль посреди тёплого июля. Станция на ESP32 раз в десять минут ходила в погодный API, а разбор ответа я написал по-рабочему: нашёл в тексте позицию слова temp через indexOf(), отступил от двоеточия, вырезал число до запятой. Неделю это работало — пока сервер не поменял порядок полей в ответе.
Вечер я потратил на новые indexOf(), ещё один — на случай, когда температура приходит строкой вместо числа, и ещё полчаса на осознание: я чиню не ошибку, а собственную конструкцию. Утром поставил библиотеку ArduinoJson — и двадцатью минутами позже разбор переживал бы и перестановку полей, и лишние пробелы, и вложенный объект с описанием сенсора. Разница не в аккуратности рук, а в подходе: indexOf() сравнивает текст по позициям, библиотека читает структуру.
В этом уроке разберём, что такое JSON и почему его опасно разбирать вручную, как устроен JsonDocument и сколько он стоит в байтах на плате с 2 КБ ОЗУ, как выглядит deserializeJson на настоящей плате — и почему в симуляторе мы имитируем разбор присвоением, не теряя самой механики чтения. К концу урока чужой API-ответ перестанет быть для вас текстом и станет документом, из которого значения достаются по имени.
Что такое JSON и почему руками его не читают
JSON (JavaScript Object Notation) — текстовый формат обмена данными. Объект пишется в фигурных скобках, внутри — пары «ключ: значение», разделённые запятыми. Ключ — всегда строка в двойных кавычках. Значение — число, строка, true или false, массив в квадратных скобках или снова объект, и так вглубь, сколько нужно. Получается матрёшка: прогноз погоды на неделю — это один объект, внутри которого массив из семи объектов с датой, температурой и кодом осадков.
Формат победил конкурентов по трём причинам: он короче XML — никаких закрывающих тегов, он читается человеком почти без боли и он парсится везде — от JavaScript в браузере до C++ на микроконтроллере за несколько сотен рублей. Погодные API, курсы валют, телеграм-боты, умный дом — почти всё публичное общение машин идёт в JSON; XML в документации к датчикам я не встречал уже лет четыре.
Проблема не в формате, а в его гибкости. Поля могут идти в любом порядке — сервер вправе переставить их без предупреждения. Пробелы и переводы строк необязательны: один и тот же ответ приходит то в одну строку, то с красивыми отступами. Числа приходят то целыми, то дробными, а внутри строк прячутся экранированные кавычки. Парсер все эти вольности переживает, потому что читает структуру: вот объект, вот ключ, вот значение. Мой indexOf() пережить их не мог, потому что читал текст: позиция 112, четыре символа, до запятой.
Где вы встретите JSON на практике: тело HTTP-ответа от погодного API, строка в MQTT-топике, файл конфигурации на SD-карте, поток от соседнего узла по Serial. Один и тот же формат — значит один и тот же навык: научились читать ответ погоды, прочитаете и конфиг, и телеметрию чужого устройства. Мы в этом разделе делаем упор на сетевые ответы, но механика разбора у всех источников одна.
{ "city": "Moscow", "temp": 23.5, "humi": 57, "wind": 3.4, "forecast": [18, 20, 22], "sensor": { "id": "bme280", "ok": true }}JsonDocument: контейнер, который платит байтами
ArduinoJson не разбирает JSON «по месту», в исходной строке. Библиотека копирует данные в специальную структуру — JsonDocument. Это область ОЗУ, где значения лежат в удобном для доступа виде: числа — как числа, строки — с указателями. doc["temp"] после разбора — это готовое число 23.5, а не поиск по тексту. Разобрали один раз — дальше обращаетесь к значениям как к обычным переменным, хоть из десяти мест программы.
В седьмой версии библиотеки класс один — JsonDocument, и память под него растёт по мере надобности. В шестой версии их было два: StaticJsonDocument<512> с ёмкостью в угловых скобках и DynamicJsonDocument(512) с ёмкостью в конструкторе. Старые туториалы пестрят обоими вариантами — идея у них общая: у документа есть лимит байтов, и его надо выбрать заранее. Увидели в чужом скетче StaticJsonDocument — знайте, что это то же самое, только с ручным лимитом.
А теперь арифметика с условиями. У Arduino Uno — 2 КБ SRAM, и документ на 512 байт съедает четверть всей памяти платы, притом что Serial держит свои 64-байтные буферы, а остальные библиотеки — свои. На ESP32 с 520 КБ вопрос не стоит: 8–16 КБ под документ никто не заметит. Моё правило такое: закладывать ёмкость в полтора-два раза больше размера ответа — но на Uno, где каждый байт на счету, я сначала замеряю реальный размер ответа и только потом решаю, сколько памяти отдаю. И помните про запас на вырост: если API добавит в ответ прогноз на неделю, документ распухнет в два-три раза, и на Uno это будет уже вопрос жизни скетча, а не вопрос стиля.
Как документ растёт внутри, полезно представлять хотя бы в общих чертах. Библиотека кладёт каждую пару «ключ — значение» в отдельный слот; строковые значения хранит отдельно, числа — прямо в слоте. Отсюда следствие, которое экономит нервы: двадцать коротких ключей дешевле десяти длинных строк, а массив из десяти чисел занимает меньше места, чем десять отдельных объектов. На ESP32 это академично, на Uno — разница между «работает» и «упало с NoMemory в полдень».
deserializeJson: разбор на плате и его имитация в симуляторе
За превращение строки в документ отвечает deserializeJson(doc, payload): первый аргумент — документ, второй — источник: объект String, массив символов или поток Stream. Возвращает функция DeserializationError: при успехе это Ok, иначе InvalidInput — источник вообще не JSON, NoMemory — не хватило ёмкости документа, IncompleteInput — ответ оборвался на полпути. Проверять ошибку — не занудство, а выживание: если разбор сорвался, документ остаётся пустым, и каждый doc["ключ"] молча вернёт ноль. Станция покажет 0 градусов и ни словом не обмолвится, что что-то пошло не так — ровно как моя в июле.
Одна ловушка для реальной платы: источник данных должен жить, пока идёт разбор. Если вы читаете ответ в локальный String и парсите его в той же функции — всё хорошо; если собираете буфер по кусочкам от Serial, запускайте deserializeJson только когда приём закончен полностью. Разорванный на полпути ответ даст IncompleteInput, а не мусор в документе: библиотека честно откажется, а не станет угадывать.
Чтение устроено как доступ по имени: float t = doc["temp"]; достаёт число, const char* city = doc["city"]; — строку. Ключа нет — реальная библиотека вернёт null-прокси: при приведении к числу это ноль, к строке — пустота. Есть и явные страховки: doc["temp"] | 0.0 подставит запасное значение, а doc.containsKey("temp") честно ответит, существует ли ключ. В симуляторе этой книги страховок нет — там мы просто читаем doc[ключ], значит, следим за ключами сами. Механика обращения при этом идентична до последнего символа, различается только источник данных.
Сеть и настоящий парсер в симуляторе не эмулируются, поэтому шаг deserializeJson мы имитируем честным присвоением: кладём значения в документ руками — как будто разбор уже отработал. Всё остальное — чтение по ключам, сравнения, serializeJson в Serial — в симуляторе настоящее. Под симулятором — четыре эксперимента: они закрепляют ровно те операции, что вы видели в коде.
- Новый ключ. Допишите
doc["press"] = 748;рядом с остальными и запустите: ключ сам встанет в документ и появится в печати serializeJson. - Порог срабатывания. Поменяйте условие: если humi больше 70 — сообщение «сыро». Положите в
doc["humi"]значение 72 и проверьте, какая ветка выполнится. - Живой документ. После первого serializeJson измените
doc["temp"] = 30.1;и напечатайте документ ещё раз: перезапись ключа меняет значение, а не заводит дубликат. - На настоящей плате. Скетч из симулятора заработает и на Uno, если присвоения заменить на
deserializeJson(doc, payload)со строкой ответа — проверьте чтениеdoc["temp"]на живом API из первого кода урока.
- JSON — текстовый формат: объекты в фигурных скобках, пары «ключ: значение», значения — числа, строки, true/false, массивы и вложенные объекты
- Разбирать JSON вручную (indexOf, вырезание по позициям) ненадёжно: порядок полей и пробелы сервер меняет без предупреждения
- ArduinoJson копирует данные из строки в JsonDocument в ОЗУ; в v7 класс один, в v6 были Static/Dynamic с заданной ёмкостью
- Uno имеет 2 КБ SRAM: документ на 512 байт — четверть памяти; закладывайте ёмкость в 1,5–2 раза больше размера ответа
- deserializeJson возвращает DeserializationError: Ok, InvalidInput, NoMemory, IncompleteInput; без проверки ошибка проходит молча — чтение вернёт нули
- В симуляторе разбор имитируется присвоением doc[ключ] = значение; чтение и serializeJson работают по-настоящему
- 1. Что вернёт doc["temp"] после неудачного deserializeJson, если ошибку не проверить?
- 2. Сколько SRAM у Arduino Uno и какую долю занимает JsonDocument на 512 байт?
- 3. Что означает DeserializationError с именем NoMemory?
- 4. Почему в симуляторе значения кладут в документ присвоением?
Разобрать чужой ответ вы теперь можете: документ, чтение по имени, проверка ошибки — этого хватает для любого погодного API и почти любого IoT-сервиса. В следующем уроке развернём поток: соберём свой JSON из показаний датчиков, напечатаем его двумя способами и выясним, почему String-конкатенация для этого не годится — с механикой, а не на словах. Попутно посчитаем, сколько байт стоит красивая печать с отступами.
Частые вопросы по теме урока
- Что вернёт doc["temp"] после неудачного deserializeJson, если ошибку не проверить?
- Сорванный разбор оставляет документ пустым: чтение по ключу молча даёт ноль (число) или пустоту (строку). Поэтому ошибку проверяют. Ноль или пустую строку — молча
- Сколько SRAM у Arduino Uno и какую долю занимает JsonDocument на 512 байт?
- У Uno 2 КБ SRAM, и 512 байт — четверть. Плюс свои буферы держат Serial и остальные библиотеки. 2 КБ; примерно четверть
- Что означает DeserializationError с именем NoMemory?
- NoMemory — документ меньше данных; InvalidInput — источник не JSON, IncompleteInput — ответ оборвался. Ёмкости документа не хватило под данные
- Почему в симуляторе значения кладут в документ присвоением?
- Сеть и парсер не эмулируются, поэтому разбор имитируется присвоением; чтение doc[ключ] и serializeJson при этом работают как на плате. deserializeJson в симуляторе не эмулируется; механика чтения при этом та же