Теплица на даче ждала команд от узла на веранде: тот раз в минуту отправлял по радиомодулю строку вида {"temp":18.5,"humi":64}, а тепличный скетч её разбирал — по-самодельному. Я искал в строке позицию слова temp через indexOf(), отступал шесть символов и вырезал число до запятой. Всё лето схема работала, и я успел забыть о ней как о детали, которая вообще может сломаться.
В ноябре ночью ударил заморозок: датчик на веранде показал минус три с половиной, пакет выглядел как {"temp":-3.5,"humi":88}, а мой вырезатель цифр пропускал всё до первой цифры — минус он просто не считал значимым. Теплица увидела плюс три с половиной, грелка по условию «включаться ниже пяти» молчала, и к утру базилик стоял серый. Устройство честно сообщало правду, а врвал мой разбор: он читал текст по позициям, а не структуру по смыслу.
Полчаса я чинил indexOf() на случай минуса, потом полчаса — на случай, когда температура станет последним полем и после неё придёт фигурная скобка вместо запятой. На третьем случае я остановился и переписал приём на ArduinoJson: строка, один вызов deserializeJson, значения по имени. Скетч стал короче на пятнадцать строк, а главное — перестал зависеть от порядка полей и знака числа.
В этом уроке разберём разбор по шагам: откуда берётся строка, как вызвать deserializeJson и что он возвращает, какие бывают коды ошибок и почему при провале документ молчит, а не ругается. Соберём рабочий разбор для настоящей платы, а потом прогоним ту же механику в симуляторе — там есть свои упрощения, о которых скажу заранее.
Три шага: библиотека, документ, вызов
Шаг первый — подключить библиотеку: #include <ArduinoJson.h> в начале скетча. Если библиотеки ещё нет, она ставится из Менеджера библиотек за минуту — мы это делали в уроке про библиотеки в IDE. Шаг второй — завести документ: JsonDocument doc; глобально, чтобы жил между вызовами. В седьмой версии библиотеки ёмкость указывать не надо, документ растёт сам; в старых скетчах вы встретите StaticJsonDocument<256> — то же самое с ручным лимитом, как мы разбирали в первом уроке раздела.
Шаг третий — сам разбор: deserializeJson(doc, source). Первый аргумент — документ, второй — источник. Им может быть готовая строка String, массив символов char, а ещё — поток Stream целиком: deserializeJson(doc, Serial) заставит библиотеку самой дождаться и прочитать данные из порта. Для урока берём строку: так видно вход и выход целиком, а приём пакета по кускам — отдельная история про Serial.available и чтение порта.
Возвращает deserializeJson значение типа DeserializationError. Идиома проверки выглядит непривычно для Си-глаза: if (err) — ошибка, else — успех. Тип устроен так, что любое значение кроме Ok приводится к истине. Я первую неделю писал if (err == Ok), это тоже работает, но короче — и быстрее замечаешь пропущенную проверку, когда ошибку вынес в отдельную переменную и смотришь её один раз.
После успешного разбора документ полон: doc["temp"] при чтении в float даёт число, doc["city"] в const char* — строку. Чтению полей посвящён отдельный урок, здесь хватит простого присваивания в переменную. И держите в голове цену: пакет в 34 символа при 9600 бод идёт около 34 миллисекунд, а сам разбор занимает доли миллисекунды — медленная часть всегда канал, а не парсер.
#include <ArduinoJson.h> JsonDocument doc; // v7; в v6 писали StaticJsonDocument<256> void setup() { Serial.begin(9600); // Строка, как будто её прислали соседним узлом или сервером: String packet = "{\"from\":\"node2\",\"temp\":-3.5,\"humi\":88}"; DeserializationError err = deserializeJson(doc, packet); if (err) { // Любая ошибка кроме Ok: документ пуст, данные не годятся Serial.print("Razbor sorvan: "); Serial.println(err.c_str()); // имя ошибки человекочитаемо return; // дальше без данных не идём } float t = doc["temp"]; // число int h = doc["humi"]; // целое const char* from = doc["from"]; // строка Serial.print("Ot: "); Serial.println(from); Serial.print("Temp: "); Serial.println(t); // минус теперь не теряется Serial.print("Humi: "); Serial.println(h); if (t < 5.0) { Serial.println("Grelka: vkl."); } else { Serial.println("Grelka: vykl."); }} void loop() {}Код ошибки — это не формальность
InvalidInput значит, что источник вообще не JSON. Самый частый сценарий в моей практике: HTTP-запрос вернул не данные, а HTML-страницу с ошибкой 502 — вы парсите заголовки и теги, библиотека честно отказывает. Второй сценарий: мусор в начале приёма — пара нулевых байтов, пока радиомодуль просыпается. Библиотека видит не фигурную скобку — и всё, InvalidInput.
IncompleteInput — источник похож на JSON, но оборван: не закрыта скобка или кавычка. Так бывает, когда пакет длиннее приёмного буфера (у аппаратного Serial на Uno буфер 64 байта — телеметрия из двадцати полей в него не влезает), или когда вы начали разбор до конца приёма. Лечится сбором строки до полного пакета — приёмы мы разбирали в уроке про чтение строк по ограничителю, — и разбор уже собранного.
NoMemory — документу не хватило места под данные. В седьмой версии, где документ растёт сам, встречается редко и говорит о действительно крупном ответе или о том, что память уже съедена соседями. В шестой версии с заданной ёмкостью это была ошибка номер один: StaticJsonDocument<128> против ответа на 300 байт. Ориентир тот же, что в первом уроке: ёмкость в полтора-два раза больше размера ответа, а на Uno с её 2 КБ SRAM — сначала замер, потом выбор.
Главное свойство всех трёх ошибок: они не взрываются. При провале deserializeJson документ остаётся пустым, и каждое последующее чтение doc["temp"] молча вернёт ноль или пустую строку. Никаких исключений, никаких сообщений — станция просто покажет ноль градусов, как моя в июле. Единственная страховка — проверка err до первого обращения к документу; я ставлю её рефлекторно, даже в одноразовых скетчах для стола.
И ещё одна привычка, вынесенная из тех июльских ночей: одно место проверки на весь скетч. Приём и разбор я заворачиваю в одну функцию, которая вернула успех и печатает имя ошибки в лог — и больше нигде в программе deserializeJson не встречается. Когда проверка размазана по трём местам, одно из них рано или поздно забывается; когда она одна — код ошибки либо стоит в журнале, либо разбор гарантированно удался. Для узлов, которые живут без присмотра, журнал с именами ошибок стоит часом отладки: InvalidInput в пять утра говорит о мусоре на линии громче любого нуля на экране.
Живая валидация JSON, инспектор полей и скетч с doc["ключ"], который растёт из вашего текста.
Подгружаем интерактив…
Разбор в симуляторе
Движок симулятора понимает deserializeJson из строки: документ наполняется, проверки ключей работают, документ печатается. Две упрощённости я оговариваю честно. Первая: функция ошибки здесь представлена числом — единица при успехе, ноль при провале, поэтому в скетче проверка выглядит как if (ok == 1); на настоящей плате то же место занимает DeserializationError и if (err). Смысл проверки — «разбор удался?» — один и тот же.
Вторая: поимённое чтение doc["temp"] симулятор не эмулирует. Поэтому поля разобранного пакета мы показываем печатью документа: serializeJson выдаёт компактный JSON, и каждое поле с его значением видно глазами — это ровно те данные, которые на плате вы бы разложили по переменным. На скетче ниже проверьте глазом: минус три с половиной дошло до документа без потерь — то, на чём сломался мой indexOf().
Механика проверки кода разбора и содержания пакета в симуляторе настоящая: измените пакет — и ветка провала отработает так же, как на плате.
- Добавить поле. Вставьте в пакет пару "wind":4.2, запустите и найдите её в печати документа; строка est_veter= сменится с 0 на 1 — containsKey заметил новый ключ.
- Убрать поле. Вырежьте "humi":88 и его запятую: проверка est_humi вернёт 0, а в печати документа пара исчезнет — разбор проходит по тому, что реально пришло.
- Сменить знак. Поставьте temp равным 14.0 без минуса и сравните печати: минус в JSON — обычный символ, парсер не путает его с параметром.
- Оборвать пакет. Удалите последнюю фигурную скобку в строке на настоящей плате: deserializeJson вернёт IncompleteInput, и ветка ошибки напечатает имя кода.
- На настоящей плате. Перенесите скетч из симулятора на Uno, замените int ok на DeserializationError err и добавьте чтение float t = doc["temp"]; значения совпадут с печатью документа.
- deserializeJson(doc, source) разбирает строку, char-массив или поток Stream; после разбора значения достаются по имени из документа
- Возврат — DeserializationError: идиома if (err) ловит любой провал; в симуляторе та же проверка выглядит как ok == 1
- InvalidInput — источник не JSON (HTML-страница, мусор в буфере); IncompleteInput — пакет оборван; NoMemory — не хватило ёмкости документа
- При ошибке документ пуст, а чтения молча дают ноль и пустоту — громкого падения не будет, поэтому проверка кода разбора обязательна
- Пакет в 34 символа при 9600 бод идёт около 34 мс — узкое место всегда канал, разбор занимает доли миллисекунды
- В симуляторе deserializeJson работает из строки; поля показывает печать serializeJson, поимённое чтение не эмулируется
- 1. Пакет при приёме оборвался: не закрыта фигурная скобка. Какой код вернёт deserializeJson на плате?
- 2. Что будет с документом, если deserializeJson завершился ошибкой, а вы всё равно читаете doc["temp"]?
- 3. Сервер вернул вместо данных HTML-страницу с ошибкой 502. Что вернёт deserializeJson?
- 4. Как в симуляторе выглядит проверка успеха разбора?
- 5. Пакет в 34 символа идёт при 9600 бод примерно 34 мс. Сколько времени занимает сам разбор этой строки?
Разбор — половина дороги. Обратное направление не менее полезно: ваш узел сам становится источником JSON для сервера, брокера или соседней платы, и собирать строку конкатенацией мы уже научились не делать. В следующем уроке — serializeJson: печать документа в Serial, сетевой клиент и заранее выделенный буфер, замер длины до печати и то, кто ставит бэкслеши, когда в значении встречается кавычка. Домашнее задание с платой: подключите к Serial-приёму второй узел или HC-05 из урока про Bluetooth, примите настоящий пакет и разберите его с проверкой ошибки — с живым каналом все четыре кода из урока перестают быть теорией.
Частые вопросы по теме урока
- Пакет при приёме оборвался: не закрыта фигурная скобка. Какой код вернёт deserializeJson на плате?
- Оборванный источник даёт IncompleteInput: библиотека увидела начало JSON, но не дождалась конца. InvalidInput — когда JSON нет вовсе. IncompleteInput
- Что будет с документом, если deserializeJson завершился ошибкой, а вы всё равно читаете doc["temp"]?
- Проваленный разбор оставляет документ пустым, и каждое чтение молча даёт ноль или пустоту. Отсюда правило: сначала проверка err, потом чтения. Чтение вернёт ноль или пустую строку — молча
- Сервер вернул вместо данных HTML-страницу с ошибкой 502. Что вернёт deserializeJson?
- HTML не начинается с структуры JSON, поэтому библиотека отвечает InvalidInput. Проверяйте код HTTP до разбора тела ответа. InvalidInput — источник не JSON
- Как в симуляторе выглядит проверка успеха разбора?
- В движке deserializeJson представлен числом: 1 — успех, 0 — провал. На настоящей плате то же место занимает DeserializationError с if (err). if (ok == 1) — функция возвращает единицу при успехе
- Пакет в 34 символа идёт при 9600 бод примерно 34 мс. Сколько времени занимает сам разбор этой строки?
- Разбор строки в 30–40 символов стоит доли миллисекунды даже на Uno. Задержки приносит передача: по миллисекунде на символ при 9600 бод. Доли миллисекунды — узкое место всегда канал