Тепличный датчик на ESP32 раз в десять минут ходил в погодный API, и разбор ответа я собрал по-рабочему, как умел: нашёл в теле слово temp через indexOf(), отступил шесть символов и вырезал четыре до запятой. Неделю на табло было ровно то, что нужно. Потом ударили заморозки, температура ушла в минус — и минус занял один символ, которого моя вырезка не ждала. Табло показывало 2.5 с хвостом из кавычек, хотя за окном было -12.5. Час я искал виновника в датчике, а сидел он в одной строке substring.
Дело не в кривых руках, а в подходе: indexOf() читает текст по позициям, а позиция — вещь ненадёжная. Знак числа, лишний пробел, переставленные сервером поля — любая мелочь сдвигает вырезку на символ. Строка с сервера не текст для резать, а документ со структурой: город, температура, описание — каждый со своим именем. Библиотека ArduinoJson читает структуру, а не позиции, и минус ей безразличен.
Здесь собираем полную связку: GET-запрос из урока про HTTP-запросы плюс честный разбор через deserializeJson. Разберём, сколько памяти съедает тело ответа вместе с документом, когда вместо getString() брать поток, и что делать с кодами разбора. Синтаксис JSON и устройство JsonDocument по косточкам разбирались в разделе про ArduinoJson — здесь ставим их на сетевые рельсы.
Строка с сервера: что в ней лежит
Демо-ответ, который ходит по этой странице, выглядит так: {"city":"Moscow","temp":18.5,"desc":"Малооблачно"}. Сорок пять байт, три ключа, два типа значений: city и desc — строки в кавычках, temp — число без кавычек. Ничего лишнего, и именно такой размер ответа я считаю здоровым для железного проекта: один запрос — одна строка, всё видно глазами в мониторе порта.
Гибкость формата — то, что ломает ручные вырезки. Сервер вправе переставить поля без предупреждения, добавить wind между temp и desc, отдать температуру строкой "18.5" вместо числа 18.5 — разбор переживёт всё это, потому что читает имена, а не позиции. Правила синтаксиса и четыре типа значений я подробно разбирал в уроке про синтаксис JSON — там же про то, почему одинарные кавычки невалидны.
Про память сразу с числами. У документа есть ёмкость, и моё правило — закладывать в полтора-два раза больше длины ответа: для 45 байт берём 128, с запасом на новые поля — 256. На ESP32 с его 520 КБ RAM это не вопрос вовсе, я ставлю 768 и не думаю. Отличия версий библиотеки и установка через менеджер — в материале про установку ArduinoJson 7; в старых туториалах вы встретите StaticJsonDocument<256> — то же самое с ручным лимитом.
Полный скетч: GET и разбор одной связкой
#include <WiFi.h>#include <HTTPClient.h>#include <ArduinoJson.h> const char* ssid = "DachaNet";const char* password = "superParol123";String url = "http://api.example.com/v1/weather"; void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Сеть есть: http://"); Serial.println(WiFi.localIP());} void loop() { HTTPClient http; http.begin(url); http.addHeader("User-Agent", "ESP32-Meteo/1.0"); int code = http.GET(); if (code == 200) { String body = http.getString(); // тело ответа как String DynamicJsonDocument doc(768); // ёмкость с запасом к длине ответа DeserializationError err = deserializeJson(doc, body); if (err) { Serial.print("Разбор сорвался: "); Serial.println(err.c_str()); // имя кода прямо в лог } else { const char* city = doc["city"]; float temp = doc["temp"]; const char* desc = doc["desc"]; Serial.print("city="); Serial.println(city); Serial.print("temp="); Serial.println(temp, 1); Serial.print("desc="); Serial.println(desc); } } else { Serial.print("Запрос не прошёл, код="); Serial.println(code); } http.end(); // освободили соединение delay(10000); // не чаще раза в 10 секунд}Разбор по косточкам. Три include — WiFi для сети, HTTPClient для запроса, ArduinoJson для разбора; последняя ставится из Менеджера библиотек за минуту. addHeader() встаёт между begin() и GET(): в живых API заголовок User-Agent иногда проверяют, и клиент без имени часть сервисов молча отсекает. Сам запрос возвращает код — 200 означает, что тело можно забирать.
Теперь честная арифметика памяти. getString() кладёт ответ в String — для 45 байт это мелочь. Но вместе с документом на 768 байт тело лежит в RAM дважды: копия строкой и копия в разобранном виде. На ESP32 сумма незаметна, а вот на Uno с её 2 КБ уже тесно. У библиотеки есть ход лучше: deserializeJson(doc, http.getStream()) — парсер читает из потока сети сам, без промежуточного String, и память тратится один раз. Для ответов в килобайт и больше я беру только этот вариант — String такого размера успевает наделать дыр в куче за сутки опросов. И про терпение: если сервер задумался, выручает http.setConnectTimeout(5000) — через пять секунд GET вернёт -1, и loop хотя бы не встанет навсегда; без этой строчки я однажды ждал ответа целую минуту.
Идиома if (err) выглядит непривычно: тип DeserializationError устроен так, что любое значение кроме Ok приводится к истине. Без этой проверки срыв разбора проходит молча: документ остаётся пустым, и каждое чтение doc["temp"] вежливо вернёт ноль — табло покажет 0.0 и ни словом не обмолвится. Моё правило: err.c_str() в Serial сразу после разбора, даже в одноразовом скетче для стола. Имя кода в логе — InvalidInput, NoMemory или IncompleteInput — экономит час гадания, а приёмы ведения такого лога одни и те же для любого железа, я собирал их в уроке про отладку через Serial.
| Код разбора | Что случилось | Как выглядит | Что делать |
|---|---|---|---|
| Ok | Разбор удался, документ полный | if (err) не срабатывает | Читать поля по именам |
| InvalidInput | В теле не JSON: HTML-страница, пустота, мусор | err.c_str() в Serial | Проверить адрес и что код был 200 |
| NoMemory | Документ мал для этого ответа | err.c_str() в Serial | Увеличить ёмкость в 1.5-2 раза |
| IncompleteInput | Ответ оборвался на полпути | err.c_str() в Serial | Забирать тело целиком до разбора |
Разбор в симуляторе
Сеть в симуляторе эмулируется честно: WiFi.begin подключает плату за секунду-полторы, GET возвращает 200, а getString отдаёт демо-тело с городом, температурой и описанием. Все шаги живого скетча на месте — от статусного цикла до вердикта по температуре, меняется только то, что сервер тут учебный.
Одна оговорка про движок: чтение полей он понимает в точечной форме — doc.temp, doc.desc. В библиотеке скобки и точка равнозначны: doc["temp"] и doc.temp на плате делают одно и то же, пишите как удобнее. Проверку ошибки движок отдаёт числом: 1 — разбор удался, 0 — провал; на плате то же место занимает DeserializationError с if (err). Смысл проверки один и тот же, а в code-блоке выше остались именно скобки — как на живой плате.
В логе виден весь путь данных: тело пришло строкой, разбор вернул 1, значения легли по именам, и решение про проветривание плата приняла сама — без единого indexOf(). Если убрать проверку ok, поведение скетча не изменится ровно до первого кривого ответа: движок вернёт 0, документ останется пустым, а чтения дадут нули — та самая тихая ловушка из таблицы кодов. Проверьте и вторую сторону: временно подмените адрес на несуществующий — на живой плате GET вернёт -1, разбор даже не начнётся, и станет ясно, что дело в сети, а не в JSON.
Один запуск — мало. Перезапустите симулятор два-три раза и посмотрите на тайминги: подключение занимает пару секунд, ответ приходит с задержкой полсекунды — это нормальный ритм для живого API, а не подвисание. По этому ритму на реальной плате вы отличите медленный сервер от умершего соединения за первые же сутки наблюдений.
- Прочитайте отсутствующий ключ: добавьте строку с doc.wind и посмотрите, что печатается. Ноль без единой жалобы — на плате такую ловушку закрывают через doc.containsKey("wind") перед чтением.
- Напечатайте документ обратно: serializeJson(doc, Serial) сразу после разбора. Сравните с телом ответа — поля на месте, порядок мог сохраниться, а мог и нет: библиотека вправе хранить как ей удобно.
- Заведите счётчик опросов: int n = 0; и n = n + 1; после http.end(), печать n= вместе с кодом. За сутки вы узнаете свой реальный трафик к API — бесплатные тарифы считают именно такие запросы.
- Уберите delay(8000) и замените его millis-планировщиком из урока про millis вместо delay — опрос раз в 10 секунд, а loop между запросами жив и занимается проветриванием.
- Тело ответа — документ со структурой: значения достаются по именам doc["ключ"], порядок полей и знак числа не ломают разбор
- Связка целиком: begin(url) → addHeader → GET → getString → deserializeJson(doc, body) → чтение полей → http.end()
- Проверяйте результат разбора: сорвавшийся deserializeJson молчит, чтения дают нули; err.c_str() печатает имя кода
- Ёмкость документа — в 1.5-2 раза больше ответа; getString хранит тело дважды, для килобайтных ответов берите deserializeJson(doc, http.getStream())
- Планировщик на millis вместо delay держит loop живым между опросами
- 1. Сервер вернул 200, но в теле HTML-страница вместо JSON. Что вернёт deserializeJson?
- 2. Ответ API занимает 300 байт. Какую ёмкость документа взять?
- 3. temp показывает 0, код ответа 200, в теле temp есть. Первое действие?
- 4. Зачем deserializeJson(doc, http.getStream()) вместо getString()?
- 5. Что делает err.c_str() после сорвавшегося разбора?
Теперь плата умеет не только спрашивать, но и понимать ответ: строка с сервера превращается в числа и решения, а минус в температуре больше никого не пугает. Тепличное табло после переезда на deserializeJson пережило и перестановку полей, и первый минус, и добавление ветра в ответ сервера — к разбору я больше не возвращался ни разу.
Дальше разворачиваем роли: в следующем уроке плата сама станет сервером — библиотека WebServer.h, маршруты server.on и первая HTML-страница, которая живёт прямо в скетче, а браузер ходит к плате сам. А если данные нужны не вам, а другой технике, тот же приём отдачи JSON пригодится в MQTT на ESP32 — там плата публикует показания брокеру, и разбирает чужие ответы он уже без нас.
Частые вопросы по теме урока
- Сервер вернул 200, но в теле HTML-страница вместо JSON. Что вернёт deserializeJson?
- Библиотека смотрит на структуру тела, а не на код запроса. HTML начинается с тега, а не с фигурной скобки — разбор честно вернёт InvalidInput, документ останется пустым. InvalidInput: в теле нет JSON-структуры
- Ответ API занимает 300 байт. Какую ёмкость документа взять?
- Каждый ключ и значение занимают слоты сверх длины строки, поэтому ёмкость равная длине почти гарантирует NoMemory. Запас в 1.5-2 раза — моё рабочее правило. 512-768 байт — с запасом в полтора-два раза
- temp показывает 0, код ответа 200, в теле temp есть. Первое действие?
- Тихий ноль — следствие сорвавшегося разбора или ключа в другом регистре: Temp и temp для JSON разные. Печать документа показывает всё сразу. Проверить регистр ключа и напечатать serializeJson(doc, Serial)
- Зачем deserializeJson(doc, http.getStream()) вместо getString()?
- getString кладёт тело в String, а разбор копирует его ещё раз в документ. Поток читается парсером напрямую — для больших ответов экономия существенная. Ответ не копируется в String — память тратится один раз
- Что делает err.c_str() после сорвавшегося разбора?
- c_str() отдаёт имя кода как строку для печати. Одна строка в логе говорит о причине провала громче, чем ноль на экране. Печатает имя кода: InvalidInput, NoMemory или IncompleteInput