Счётчик воды в котельной раз в минуту отправлял пакет показаний в домашний сервер, а я раз в неделю лениво просматривал журнал. В один понедельник в журнале начались нули — и тянулись три дня. Я перебрал всё подряд: заменю датчик, проверю питание, перешью скетч — а менять было нечего: данные не приходили вовсе. Причина нашлась случайно, когда я ради интереса добавил в скетч одну строку — печать кода разбора. За следующий вечер журнал показал три разных кода: пустой ответ, когда отваливался Wi-Fi; HTML-страницу вместо данных, когда сервер обновлялся; обрезанный пакет на радиоканале. Три разные поломки три дня маскировались под один симптом — нули в логе.
До того вечера моя проверка разбора выглядела как if (err) Serial.println("paket plohoj"); — честно, но бесполезно: я знал, что разбор сорвался, и не знал, чем именно меня кормит канал. Код разбора — это диагноз, а не формальность: EmptyInput и InvalidInput лечатся на стороне сети, IncompleteInput — на стороне приёма, NoMemory — на стороне памяти, и перепутать их — значит чинить не то.
В этом уроке разберём все шесть кодов, которые возвращает DeserializationError, свяжем каждый с типовой причиной и первым действием, соберем разбор с полноценной диагностикой для настоящей платы и посмотрим, что из этого умеет показывать симулятор: там невалидную строку в парсер не скормишь — движок честно падает, — поэтому осмотр строки сделаем руками.
Шесть кодов: кто есть кто
deserializeJson возвращает тип DeserializationError, и у него шесть значений: Ok — разбор удался, EmptyInput — источник пустой, InvalidInput — источник не JSON, IncompleteInput — JSON начался, но оборвался, NoMemory — не хватило памяти, TooDeep — вложенность глубже допустимой. Каждый код печатается человекочитаемо: err.c_str() отдаёт строку "Ok", "EmptyInput", "InvalidInput" и так далее — готовую для журнала. Базовую тройку кодов мы уже трогали в уроке про первый разбор, здесь доходим до конца и главное — раскладываем по причинам.
EmptyInput — строка пустая или состоит из одних пробелов. Так выглядит ответ, когда сеть умерла до запроса: WiFi-клиент вернул пустой String, модуль ещё просыпается, сервер закрыл соединение молча. Лечится на стороне связи: проверять статус соединения и код HTTP до разбора — мы это делали в уроке про JSON по HTTP.
InvalidInput — источник начинается не с JSON: HTML-страница с кодом 502 во время деплоя, мусорные нулевые байты в начале приёма от радиомодуля, чужой протокол. Библиотека видит не фигурную скобку — и сразу отказывает. Мусор в начале приёма — отдельная глава: радиомодули при включении выплёскивают случайные байты, и пара нулей перед фигурной скобкой превращает целый пакет в InvalidInput. Лечится чисткой: читать и выбрасывать всё до первой скобки ещё до разбора.
IncompleteInput — самый коварный на Uno: пакет влез наполовину, потому что приёмный буфер аппаратного Serial держит всего 64 байта — мы считали его устройство в уроке про буфер UART; либо канал оборвал передачу на середине, либо вы позвали deserializeJson до конца приёма. Телеметрия из двадцати полей на 300 байт через 64-байтный буфер проходит тремя кусками — и разбор между кусками даёт ровно этот код.
NoMemory в седьмой версии говорит о кончившейся куче, в шестой — о тесной ёмкости документа; TooDeep срабатывает, когда вложенность уходит глубже десяти уровней — лимит по умолчанию задаёт константа ARDUINOJSON_DEFAULT_NESTING_LIMIT, её можно поднять, но честнее спросить себя, зачем парсер должен переваривать матрёшку в десять этажей. После сорванного разбора документ остаётся пустым, и каждое чтение молча даёт ноль или пустоту — как ловится этот тихий ноль, показано в уроке про чтение полей.
| Код | Типовая причина | Первое действие |
|---|---|---|
| EmptyInput | Пустой ответ: сеть отвалилась, клиент вернул пустую строку | Проверить соединение и код HTTP до разбора |
| InvalidInput | Источник не JSON: HTML-страница 502, мусор в начале приёма | Напечатать первые 40–60 символов пакета |
| IncompleteInput | Пакет оборван: буфер Serial на 64 байта, обрыв канала | Копить пакет до конца приёма, потом разбирать |
| NoMemory | В v7 кончилась куча, в v6 — ёмкость документа | Замерить размер пакета, разгрузить память соседей |
| TooDeep | Вложенность глубже десяти уровней | Сгладить структуру или поднять лимит константой |
Привычка, которая экономит вечера: одна точка разбора на весь скетч. Приём и разбор живут в одной функции — она возвращает успех или печатает диагноз, — и нигде больше в программе deserializeJson не встречается. Когда проверки размазаны по трём местам, одно из них рано или поздно забывается; когда одна — код либо стоит в журнале, либо разбор гарантированно удался. Поля записи держите короткими и постоянными: kod=, dlina=, nachalo= — через месяц глаз находит нужное в журнале за секунду.
Ретраи — с паузой. Пустой ответ при мёртвой сети не лечится спамом: повторять запрос раз в миллисекунду — значит молотить по мёртвому каналу и забивать журнал. Пауза между попытками — секунды, а не миллисекунды, и держать её лучше на millis(), как в уроке про millis вместо delay, — без блокирующего delay() на полминуты. А приём пакета по кускам до конца — отдельная работа с портом: приёмы показаны в уроке про Serial.available и чтение порта, и разбирать стоит только собранное.
И симметричное действие: при успехе печатайте одну строку ok=1 с длиной пакета. Журнал, где успех — одна строка, а провал — диагноз, читается глазами через месяц: не надо искать нули среди тишины, все поломки видны по кодам и длинам. Я оставил в котельном узле счётчик провалов и печать начала пакета навсегда — за полгода журнал дважды указал мне на чужие поломки: смену DNS роутера и плановый апгрейд сервера.
Пример чтения такого журнала: строка kod=IncompleteInput dlina=57 в 03:14 и тишина до утра — канал рвался ночью на полуслове; строка kod=InvalidInput dlina=41 с началом <html> в полдень — сервер подменил данные на страницу. Ни одна не требует лезть на объект: повод ясен, время видно, длина подсказывает, сколько успело дойти. Диагностика окупается ровно в такие моменты — не когда всё работает, а когда работает не так.
#include <ArduinoJson.h> JsonDocument doc;int fails = 0; // сколько разборов сорвалось за жизнь узла // Разбор с диагнозом: при провале печатает код, длину и начало пакетаbool razobrat(const String& pkt) { DeserializationError err = deserializeJson(doc, pkt); if (err == Ok) { return true; } fails++; Serial.print("sorvan #"); Serial.print(fails); Serial.print(" kod="); Serial.print(err.c_str()); // EmptyInput / InvalidInput / ... Serial.print(" dlina="); Serial.println(pkt.length()); Serial.print("nachalo: "); Serial.println(pkt.substring(0, 48)); // чем именно кормили парсер return false;} void setup() { Serial.begin(9600); if (razobrat("{\"temp\":21.4,\"humi\":58}")) { Serial.print("temp="); Serial.println((float)doc["temp"]); } // Так выглядит вечер деплоя сервера: данные подменила страница razobrat("<html><body>502 Bad Gateway</body></html>"); // А так — пакет, обрезанный на середине радиоканалом razobrat("{\"temp\":21.4,\"humi\"");} void loop() {}Диагностика в симуляторе: строку проверяем руками
Честная оговорка про симулятор: движок роняет скетч при попытке разобрать невалидный JSON — это его способ сказать «так нельзя», и он прав: на плате за это отвечает библиотека, а не scanf-надежда. Поэтому в сим-скетче ниже парсеру скармливается только годная строка, а осмотр пакетов — пустой, начинающийся не со скобки, оборванный — сделан руками: длина, первый символ, последний символ.
На настоящей плате эти ручные проверки не нужны: deserializeJson быстрее и точнее любого осмотра. Но навык полезен с другой стороны — 80% диагноза видно по первым символам пакета ещё до разбора, и когда журнал показывает kod=InvalidInput, вы первым делом смотрите на начало строки. Механика в симуляторе настоящая: три пакета — HTML-подмена, обрыв и цельный JSON — проходят один и тот же осмотр, как будто их принес канал.
- Подменить виновника. Замените строку html на пустую "" — осмотр встанет в ветку EmptyInput и до проверки скобок дело не дойдёт: порядок проверок имеет значение.
- Починить обрыв. Допишите в obryv недостающую ":58} — пакет пройдёт осмотр и дойдёт до разбора вместе с остальными.
- Сдвинуть приём. Поменяйте в good значение temp на -3.5 — осмотр пропускает минус спокойно: он внутри структуры, а не на границе строки.
- На настоящей плате. Перенесите функцию razobrat из code-блока в скетч с приёмом по Serial: три кода за один вечер — обычное дело для домашнего канала.
- DeserializationError имеет шесть значений: Ok, EmptyInput, InvalidInput, IncompleteInput, NoMemory, TooDeep — err.c_str() печатает имя кода для журнала
- EmptyInput — пустой ответ при мёртвой сети; InvalidInput — источник не JSON: HTML-страница, мусор в начале приёма
- IncompleteInput — пакет оборван: приёмный буфер Serial на Uno всего 64 байта; NoMemory — кончилась куча или ёмкость; TooDeep — глубже десяти уровней
- После сорванного разбора документ пуст, чтения молча дают ноль — проверка кода стоит перед каждым чтением
- Диагностика — это код плюс начало пакета плюс счётчик провалов; печать держите в 40–60 символах, чтобы не подвешивать loop
- 1. Во время деплоя сервер вернул HTML-страницу с кодом 502 вместо JSON. Какой код вернёт deserializeJson?
- 2. Приёмный буфер Serial на Uno — 64 байта, пакет телеметрии — 90. Вы позвали deserializeJson, не дождавшись конца приёма. Какой код ожидать?
- 3. Что напечатает Serial.println(err.c_str()), если источник разбора — пустая строка?
- 4. Разбор сорвался, ошибку вы проигнорировали. Что окажется в doc["temp"] при чтении в float?
- 5. Какая вложенность по умолчанию вызывает код TooDeep?
Код разбора говорит, что сломалось, — но есть поломка, которая не громкая, а тихая: пакет не влез. Не влез в буфер, не влез в документ, не влез в канал — и всё это считается байтами до того, как узел встал в поле. Диагностика из этого урока — не разовая операция, а постоянный слой скетча: код, длина, начало пакета и счётчик провалов живут в функции разбора столько, сколько живёт узел. В следующем уроке берём линейку: measureJson считает длину JSON до печати, а мы раскладываем байты по документу, буферу и плате — до потолка, а не до первого внезапного NoMemory.
Частые вопросы по теме урока
- Во время деплоя сервер вернул HTML-страницу с кодом 502 вместо JSON. Какой код вернёт deserializeJson?
- HTML не начинается с фигурной скобки JSON, поэтому библиотека отвечает InvalidInput. Первое действие — напечатать начало пакета и проверить код HTTP до разбора. InvalidInput — источник не JSON
- Приёмный буфер Serial на Uno — 64 байта, пакет телеметрии — 90. Вы позвали deserializeJson, не дождавшись конца приёма. Какой код ожидать?
- Библиотека видит начало JSON, но не дождалась закрывающей скобки — это IncompleteInput. Лечится коплением пакета до конца приёма. IncompleteInput — JSON начался, но оборвался
- Что напечатает Serial.println(err.c_str()), если источник разбора — пустая строка?
- Пустой источник или строка из одних пробелов даёт EmptyInput, и c_str() печатает именно это имя — готовое для журнала. "EmptyInput"
- Разбор сорвался, ошибку вы проигнорировали. Что окажется в doc["temp"] при чтении в float?
- deserializeJson сбрасывает документ перед разбором и при провале оставляет его пустым: чтение молча вернёт ноль. Поэтому код проверяют до первого чтения — а печать значения сразу после разбора тоже диагностика: ноль при живом датчике — симптом, а не температура. Ноль — документ пуст после провала
- Какая вложенность по умолчанию вызывает код TooDeep?
- Лимит по умолчанию — десять уровней, его держит константа ARDUINOJSON_DEFAULT_NESTING_LIMIT. Честнее сгладить структуру, чем поднимать лимит. Десять уровней