Пять дней прогноза для дачного узла на ESP32 я забирал одним запросом: сервис отдавал около семи килобайт, сорок записей list, и температура в каждой записи лежала не на верхнем этаже, а внутри вложенного объекта temp. В мониторе вместо ожидаемых минус восьми градусов печаталось 0.00 — без единой жалобы со стороны разбора. Я трижды перепроверял ключи, пока не посчитал этажи: doc["list"][3]["temp"]["day"] — четыре пары скобок, и каждую пару я обязан пройти по очереди. Промах на любой из них не роняет скетч и не срывает разбор — он подставляет ноль, и устройство молча живёт с выдуманной погодой.
Механику цепочек мы уже разбирали в уроке про вложенные объекты и в уроке про список объектов из API; здесь цель другая — собрать полный маршрут по живому ответу сервиса погоды, где массив, объект и снова массив чередуются через этаж. Заодно закрою типовую граблю инструкций про OpenWeather: почти каждая из них показывает doc["main"]["temp"] для текущей погоды и молчит о том, что в прогнозе та же температура спрятана на два этажа глубже — и ключ там уже не temp наверху, а day внутри вложенного объекта. Смешать эти два формата — типовое начало вечера отладки: скетч с текущей погоды переносят на прогноз, и половина маршрутов перестаёт сходиться.
План такой: анатомия многоэтажного ответа со счётом слотов, итерация по списку записей с фильтром по дате, затем симулятор — с честной оговоркой, какой этаж он видит, а какой остаётся механикой настоящей платы.
Анатомия многоэтажного ответа
Этажи бывают трёх сортов: объект внутри объекта, массив внутри объекта и объект внутри массива. Нотация у всех одна — каждая пара квадратных скобок или каждое имя после точки опускает вас на один уровень вниз, и читать цепочку надо слева направо, как маршрут на маршрутке: пересадки по порядку, без прыжков через остановку. В ответе сервиса погоды объект main держит измерения текущего часа, массив weather — текстовое описание неба, а в прогнозе те же измерения лежат в списке list, где каждая запись — самостоятельный объект со своим temp и своим weather. Ноль в конце цепочки почти всегда значит, что вы сошли не на тот этаж или перепутали лестницу. Я вычерчиваю маршрут на бумаге до кода: скобочный путь на три этажа проще вычеркнуть глазами, чем ловить в мониторе по нулям.
Массив weather стоит отдельного абзаца: сервис почти всегда кладёт туда один элемент, но формат — именно массив, рассчитанный на несколько явлений разом, скажем дождь с грозой одновременно. Отсюда классический промах: обращение doc["weather"]["main"] без индекса компилируется, выполняется и возвращает ноль — вы просите у массива поле, которого у него нет и быть не может. Правильная лестница — doc["weather"][0]["main"], и индекс 0 здесь не перестраховка, а честное согласие с форматом. Проверять это согласие нужно один раз при смене API, а не в каждом цикле. В моём журнале промахов половина была именно тут: индекс выбрасывают, когда элемент виден один, и формат перестают читать как формат.
Глубина стоит памяти: каждый этаж добавляет слоты в пул документа, и прогнозный ответ на сорок записей с тремя уровнями вложенности просит заметно больше, чем плоский пакет с теми же значениями. Как считать эти байты и сколько просить у платы — раскладывали в уроке про расчёт ёмкости; здесь добавлю практическое следствие: если вам нужны только дневные температуры, берите фильтром один этаж temp и не тащите весь list целиком — механика фильтров была в уроке про разбор нужных полей. Фильтр срежет и память, и время разбора: парсер не станет раскладывать записи, которые вы всё равно выбросите.
#include <ArduinoJson.h> // По HTTP строку отдаёт client.getString(); для стенда пакет задан константой.// Каждая запись list — объект с вложенным объектом temp и массивом weather. JsonDocument doc; // v6: DynamicJsonDocument<1536> doc; const char* PAKET = "{\"cnt\":2,\"list\":[" "{\"dt_txt\":\"2025-01-20 12:00:00\",\"weather\":[{\"main\":\"Clear\"}],\"temp\":{\"day\":-3.5}}," "{\"dt_txt\":\"2025-01-21 12:00:00\",\"weather\":[{\"main\":\"Snow\"}],\"temp\":{\"day\":-8.1}}" "]}"; void setup() { Serial.begin(9600); pinMode(12, OUTPUT); DeserializationError err = deserializeJson(doc, PAKET); if (err) { Serial.print("razbor sorvan: "); Serial.println(err.c_str()); return; } int cnt = doc["cnt"]; // число записей вместо константы for (int i = 0; i < cnt; i++) { float den = doc["list"][i]["temp"]["day"]; // три этажа вниз const char* nebo = doc["list"][i]["weather"][0]["main"]; // массив внутри записи const char* kogda = doc["list"][i]["dt_txt"]; Serial.print(kogda); Serial.print(" den="); Serial.print(den, 1); Serial.print(" nebo="); Serial.println(nebo); if (den < -7.0) { digitalWrite(12, HIGH); // морозный день — отметка светодиодом delay(200); digitalWrite(12, LOW); } }} void loop() {}Итерация по списку записей
Число записей сервис держит в ключе cnt наверху, но доверять константе я не привык: запрос с параметром count=3 вернёт три записи, и цикл с жёсткой сороковкой уедет в пустоту, читая нули с несуществующих этажей. Честный способ — size() на самом массиве: doc["list"].size() возвращает текущую длину, и цикл подстраивается сам, хоть сервис режет прогноз, хоть разворачивает до сорока. Внутри каждая запись — объект, и к её этажам обращение то же, что и к одиночному ответу: doc["list"][i]["temp"]["day"] за дневной температурой, doc["list"][i]["weather"][0]["main"] за описанием неба. Имена этажей внутри записи совпадают с именами одиночного ответа — привычка переносится без переучивания.
Записи идут с шагом три часа, и для будильника «одеть рассаду» мне нужна была только полуденная: поле dt_txt хранит строку с датой и часом, из неё достают подстроку с полуднем и сравнивают — механика строковых сравнений та же, что при разборе команд из монитора в уроке про приём по ограничителю. Альтернатива — считать индексы: полдень сидит на позициях 4, 12, 20 при шаге в три часа, и цикл просто прыгает через восемь записей. Способ дешевле сравнений, но хрупче: сервис сменит шаг прогноза — и прыжки уедут мимо полудня. Универсальный компромисс — сравнение по подстроке, а шаг оставляю для грубой прикидки индексов.
Чем длиннее цепочка, тем дороже отладка, поэтому я печатаю промежуточные этажи: сначала doc["list"][i]["temp"] целиком через serializeJson, потом конкретное поле. Две строки печати экономят вечер — сразу видно, на каком этаже вы сошли с лестницы. Для массивов внутри массивов, скажем почасовой разбивки внутри дня, механика та же: её раскладывали в уроке про массивы массивов. Приём один и тот же: спуск по этажам, замер size(), цикл по индексу.
| Маршрут | Что лежит | Типовая грабля |
|---|---|---|
| doc["main"]["temp"] | текущая температура, дробное число | целочисленная переменная режет дробь |
| doc["weather"][0]["main"] | короткое описание неба: Clear, Rain, Snow | обращение без [0] — у массива нет полей |
| doc["list"][i]["temp"]["day"] | дневная температура i-й записи прогноза | перепутать day с max при отладке порога |
| doc["list"].size() | число записей: до 40 по умолчанию | константа в цикле ломается при count=3 |
Симулятор: верхний этаж честно
Движок книги эмулирует разбор, чтение верхних ключей и доступ к массиву чисел по индексу — doc.t[i] работает; цепочки на три этажа вроде doc.list[i].temp.day он не опускает и вернёт ноль, что мы честно проверили заранее. Поэтому прогнозный пакет в симуляторе сплющен: город и влажность наверху, температуры — массивом t на верхнем этаже. Логика урока при этом цела: обходим записи по индексу, ищем минимум и максимум, вердикт отдаёт LED на D12. Пакет при этом полный: город, влажность, четыре температуры — сплющен формат, а не содержание.
Смотрите на вывод монитора как на трассировку лестницы: razbor=1 — разбор прошёл, дальше по строке на каждый день. На настоящей плате вместо плоского массива работает цепочка из code-блока выше — она проверена на Uno с седьмой версией библиотеки и на ESP32 с шестой. Если движок и плата разойдутся в цифре, приоритет у платы: симулятор — тренажёр маршрута, а не замена железа.
- Добавить день. Допишите значение в массив t и поправьте счётчик цикла с четырёх на пять: минимум и максимум пересчитаются, вердикт LED сменится без правки логики.
- Уронить этаж. Замените doc.t[i] на doc.tc[i]: razbor останется единицей, а значения станут нулями — так выглядит промах по этажу в чистом виде.
- Сместить порог. Поставьте проверку tmin < 5.0: вердикт перевернётся на пакете с тёплыми днями — порог живёт в одном месте, маршрут чтения в другом.
- На настоящей плате. Перенесите code-блок на Uno или ESP32, подставьте свой ключ API и сравните трёхэтажную цепочку с плоским пакетом из симулятора: разница только в нотации.
- Каждая пара скобок в цепочке — один этаж вниз: doc["list"][3]["temp"]["day"] читают слева направо, как маршрут с пересадками
- weather — массив даже с одним элементом: обращение без [0] компилируется, но молча даёт ноль
- Число записей берут size() с массива list: константа 40 ломается при параметре count в запросе
- Промах по этажу не срывает deserializeJson: разбор вернёт Ok, а значения подставятся нулями
- Каждый этаж стоит слотов в пуле: из глубокого ответа берите фильтром только нужный уровень
- 1. Почему doc["weather"]["main"] возвращает ноль при живом разборе?
- 2. Сколько записей обойти в цикле по прогнозу?
- 3. Разбор вернул Ok, а температуры печатаются нулями. Где искать?
- 4. Пакет с тремя этажами вложенности против плоского с теми же значениями:
- 5. Движок симулятора на цепочке doc.list[i].temp.day вернёт:
Следующий шаг после спуска по этажам — не доверять пройденному без проверки: пакет мог прийти синтаксически чистым, но без нужного ключа или с текстом вместо числа, и молчаливый ноль вернётся с чёрного хода. Как стоит заслон из containsKey, is и as — третий урок: валидация входящего JSON. Домашнее задание: возьмите свой сервис с вложенным ответом — прогноз, телеметрия, расписания — и выпишите на бумаге лестницу до каждого нужного поля с числом этажей; половина нулей в мониторе исчезнет уже от одной такой таблицы. Число этажей в таблице заодно подскажет, стоит ли просить фильтр: чем меньше лестниц, тем дешевле разбор на восьмибитной плате.
Частые вопросы по теме урока
- Почему doc["weather"]["main"] возвращает ноль при живом разборе?
- weather — массив объектов: у самого массива полей нет, обращение без индекса промахивается мимо элемента и тихо подставляет ноль. Правильная лестница — doc["weather"][0]["main"]. weather — массив, а полю нужен индекс [0]
- Сколько записей обойти в цикле по прогнозу?
- size() возвращает фактическое число записей: сервис может отдать и три, и сорок — цикл подстроится. Константа 40 при запросе count=3 уедет в пустоту за нулями. doc["list"].size() — текущую длину массива
- Разбор вернул Ok, а температуры печатаются нулями. Где искать?
- Ok говорит о синтаксисе: скобки сошлись. Нули при живом разборе — промах по этажу цепочки или регистр ключа: Temp и temp — разные ключи. Печатайте промежуточные этажи: первый ноль укажет, где вы сошли с лестницы. В маршруте: промах по этажу или регистру ключа
- Пакет с тремя этажами вложенности против плоского с теми же значениями:
- Каждый этаж добавляет слоты в пул, и глубокий ответ стоит заметно больше плоского. Если нужны один-два поля — берите их фильтром и не тащите весь list. Экономия видна и в measureJson, и в ёмкости пула. вложенный просит больше слотов пула документа
- Движок симулятора на цепочке doc.list[i].temp.day вернёт:
- Движок видит верхний этаж: ключи и массивы чисел по индексу. Цепочки на три этажа остаются механикой настоящей платы — потому пакет в симуляторе и сплющен. ноль: трёхэтажные цепочки не эмулируются