Для поездки на дачу мне нужен был вердикт «годится ли день для покраски забора» на три дня вперёд: температура в диапазоне и без дождя. Первая версия скетча ходила в сервис три раза — по запросу на день, с разными датами в адресе. Ответы приходили, но не в одну секунду: «завтра» из первого запроса к третьему уже отставал, и вердикт собирался из трёх разных версий прогноза. Дважды за месяц он противоречил сам себе — «сегодня красить нельзя, завтра можно» — при свежих данных, говоривших обратное.
Правильная форма у сервиса была всегда: один ответ, внутри — массив d из трёх объектов, у каждого дня своя температура, влажность и метка дождя. Один запрос — один срез прогноза, все дни из одной секунды времени. Осталось научиться раскладывать массив объектов по переменным — третий шаг после вложенных объектов и рядов значений, и самый частый в реальных API: от прогноза погоды до списка котировок.
Разберём устройство массива объектов и почему сервисы шлют списки именно так, как выглядит обход на плате — range-for с раскладкой полей элемента, — где ломается разбор при смене формата сервером и как вся механика выглядит в симуляторе: там одно честное упрощение, о котором скажу заранее.
Список объектов: у каждого элемента свои поля
{"d":[{"t":18,"h":70,"rain":0},{"t":26,"h":52,"rain":1},{"t":21,"h":60,"rain":0}]} — массив d из трёх объектов. Индекс выбирает день: doc["d"][0] — первый, doc["d"][2] — последний. Внутри элемента — обычные пары «ключ: значение», и адрес поля собирается из двух шагов: doc["d"][1]["t"] — температура второго дня. Структура та же, что в уроке про вложенные объекты, только карманов много и они идут в ряду. Нумерация с нуля — как у рядов значений из прошлого урока: [0] — сегодня, [1] — завтра, [2] — послезавтра; порядок сервис фиксирует, но проверять дату внутри элемента на плате полезнее, чем верить в порядок.
Почему API шлют списки объектами, а не значениями: у элемента свой набор полей, и набор вправе отличаться. Сервер добавит к каждому дню поле облачности cloud — старый скетч его просто не прочитает, разбор не заметит; массив значений такой гибкости не даёт. Классический пример — прогноз OpenWeatherMap: список list из сорока объектов, по одному на каждые три часа, у каждого свои время dt, температура main.temp и описание weather. Один запрос — пять дней с шагом в три часа. У других сервисов встречается и обратный ход: поле rain появляется у каждого дня только тогда, когда дождь в прогнозе есть, — и это довод в пользу запасных значений из следующего абзаца.
Раскладка по переменным — навык, ради которого затевается обход: int t = day["t"]; bool rain = day["rain"] | false; — поле может отсутствовать, запасное значение через |, правила — в уроке про чтение полей. Переменные итерации локальны и перезаписываются на каждом шаге: итоги — самый тёплый день, число дождливых — копите в переменные, объявленные до цикла, иначе к концу обхода останется только последний элемент. Мой вердикт копил лучший день в пару bestDay и bestT, объявленных до цикла: печать шла из них, а не из последней итерации.
Один запрос против трёх
Согласованность среза — первое преимущество одного ответа. Прогноз на дачу собирается в один вердикт из трёх дней; если каждый день приходит своим запросом в свою секунду, вердикт смешивает версии прогноза, сделанные в разное время. Мой случай с противоречием «сегодня нельзя, завтра можно» — ровно это: третий ответ был свежее первого, и сопоставлять их — работа скетча, которой в нём быть не должно.
Трафик и лимиты — второе. Один ответ с тремя днями весит около восьмидесяти байт — замер в симуляторе ниже печатает точное число; три отдельных ответа — трижды по пятьдесят-шестьдесят плюс трижды служебные заголовки HTTP, которые тяжелее тела, — их анатомию мы разбирали в уроке про GET-запрос из скетча. Лимиты у популярных погодных сервисов измеряются десятками запросов в минуту на ключ, и тройной расход — это минус две трети запаса прочности при том же наборе данных.
Есть и третье, менее очевидное: один ответ проще держать в кэше. Приняли, разобрали, отдали вердикт — и в течение часа данные живут в документе, повторные запросы не нужны. Три запроса кэшировать втрое сложнее: три срока годности, три повтора при сбое. Для батарейного узла, просыпающегося раз в десять минут, разница между одним и тремя запросами — это разница между секундой и тремя на радио, а радио на батарейке — самая дорогая строка бюджета.
#include <ArduinoJson.h> JsonDocument doc; void setup() { Serial.begin(9600); // Один запрос — три дня прогноза как массив объектов String resp = "{\"d\":[{\"t\":18,\"h\":70,\"rain\":0},{\"t\":26,\"h\":52,\"rain\":1},{\"t\":21,\"h\":60,\"rain\":0}]}"; if (deserializeJson(doc, resp)) return; JsonArray days = doc["d"]; Serial.print("Dney: "); Serial.println(days.size()); for (JsonObject day : days) { // каждый элемент — объект int t = day["t"]; // раскладка полей в переменные int h = day["h"]; bool rain = day["rain"] | false; // поля может не быть — запасное Serial.print("t="); Serial.print(t); Serial.print(" vlazh="); Serial.println(h); if (rain) { Serial.println("Dozhd: den ne goditsya"); } else { if (t >= 18 && t <= 26) { Serial.println("Den goditsya dlya pokraski"); } else { Serial.println("Ne v diapazone"); } } }} void loop() {}Три дня в симуляторе
Что в движке настоящее: разбор полного ответа с массивом объектов, проверка doc.containsKey("d"), печать структуры serializeJsonPretty и замер measureJson. Чего нет: индексации объектов внутри массива — doc.d[0] и цепочки doc.d[0].t вернут ноль. Поэтому раскладка по дням в скетче ниже сделана отдельными документами: каждый пакет дня подаётся своему deserializeJson с doc.clear() между шагами, поля читаются точечной формой. clear() — не церемония, а страховка: забытый перед разбором очисткой смешает поля соседних дней в одном документе, и вердикт первого дня прилипнет ко второму. На плате это один документ и range-for — механика чтения полей та же, меняется только источник.
На что смотреть в выводе: день с 26 и меткой дождя получает отказ по дождю, день с 18 проходит по нижней границе диапазона, день с 21 — посредине. Вердикт собирается из полей элемента — то, ради чего затевался массив объектов. Порядок веток тоже часть вердикта: дождь проверяется раньше диапазона — отказ с причиной нагляднее отказа вообще. Поставьте дождь третьему дню или сдвиньте границы — вердикты пересоберутся; добавьте четвёртый объект в строку ответа — печать структуры покажет четыре дня, а цикл останется на трёх: та самая граница из урока про итерацию, только теперь по дням.
Собранный вердикт — готовая строка для журнала, экрана или брокера: в уроке про MQTT на ESP32 такие строки уходят в топик одной публикацией. А на Uno с её двумя килобайтами список из трёх компактных дней весит десятки байт и влезает свободно — считать цену документа measureJson полезно до того, как сервис добавит пятое поле каждому дню.
Ёмкости в скетче — две: 768 под полный ответ и 256 под пакет одного дня. Числа не с потолка: полный ответ в симуляторе занимает 82 байта, пакет дня — 24, и в полтора-два раза больше — как раз 256 и 768. На плате ориентир тот же, замер той же measureJson, и правило прежнее: запас на вырост, потому что дни имеют свойство обрастать полями.
- Дождь третьему дню. В двух местах — в общем ответе и в пакете дня 2 — поставьте "rain":1: вердикт третьего дня сменится на «дождь». Вердикт собирается из полей, а не из порядка дней.
- Сдвинуть диапазон. Замените условие t >= 18 && t <= 26 на t >= 20: день с 18 вылетит из годных. Данные и условие разделены — правится одна строка.
- Четвёртый день. Допишите четвёртый объект в строку общего ответа и отдельный пакет для него: печать структуры покажет четыре элемента, цикл останется на трёх, пока не поправите границу. На плате границу даёт days.size().
- На настоящей плате. Соберите range-for из урока: for (JsonObject day : doc["d"].as<JsonArray>()) — и сверьте вердикты с симулятором: раскладка та же, источник один.
| Формат списка от сервера | Как выглядит | Когда встречаю |
|---|---|---|
| массив объектов | [{"t":18},{"t":26}] | прогнозы, письма, узлы сети: у элементов свои поля, набор может отличаться |
| массив значений | [54,62,71] | ряды измерений, история датчиков: по одному числу на элемент |
| плоские ключи с номером | {"t0":18,"t1":26} | часть самодельных API: индекс зашит в имя ключа, обходится константами |
| словарь по id | {"node2":{"t":21}} | случайный доступ по имени узла без перебора всего списка |
- Массив объектов — список элементов, у каждого свой набор полей; адрес поля — два шага: индекс, потом ключ
- Один запрос со списком лучше трёх по дням: один срез времени и втрое меньший расход лимита API
- Раскладка: int t = day["t"]; внутри итерации; итоги копятся в переменные до цикла — иначе останется последний день
- Поле может отсутствовать у отдельного элемента: day["rain"] | false подставит запасное значение
- Смена формата сервером молча даёт нули: проверяйте поле первого элемента isNull, печатайте структуру, чините адреса
- В симуляторе полный ответ, containsKey, pretty-печать настоящие; индексация объектов массива — нет: дни разбираются отдельными документами
- 1. Чем массив объектов отличается от массива значений?
- 2. Как выглядит адрес температуры второго дня в массиве d?
- 3. Зачем метка дождя читается как day["rain"] | false?
- 4. Почему три отдельных запроса по дням хуже одного с массивом?
- 5. Почему в симуляторе каждый день разбирается отдельным документом?
Список объектов — самый частый формат API, и теперь он раскладывается по переменным за один цикл: индекс выбирает день, поля складываются в переменные, вердикт собирается из данных. Сам вердикт — тоже данные: для машины его честнее отдавать числом или булевым полем ok, чем строкой на русском — строки в логах для людей, поля в документах для машин. Дальше по разделу — фильтры разбора, которые из прогноза на сорок объектов достают только нужные поля и экономят память Uno. А если вердикт надо куда-то отдать, соберите ответный документ — приёмы записи из урока про serializeJson ложатся на него без изменений.
Частые вопросы по теме урока
- Чем массив объектов отличается от массива значений?
- Элемент массива объектов несёт пары ключ-значение, и набор полей вправе отличаться от соседа: сервер добавит поле — старый разбор его просто не прочитает, а разбор не заметит. Тем, что каждый элемент — объект со своим набором полей
- Как выглядит адрес температуры второго дня в массиве d?
- Нумерация с нуля: второй день — индекс 1, поле — за ним. doc["d"][2] — уже третий день; перепутанный индекс даёт вердикт не тому дню. doc["d"][1]["t"]
- Зачем метка дождя читается как day["rain"] | false?
- Оператор | подставляет запасное значение, когда ключа нет или тип несовместим. Для метки дождя запасное — false: день без поля считается сухим, а не зависшим. Поле может отсутствовать — тогда возьмётся запасное false
- Почему три отдельных запроса по дням хуже одного с массивом?
- Один ответ — один срез времени. Три запроса — три версии прогноза в одном вердикте плюс тройной расход лимита и заголовков HTTP. Ответы приходят в разные моменты — вердикт собирается из разных версий прогноза, и лимит API тает втрое быстрее
- Почему в симуляторе каждый день разбирается отдельным документом?
- Полный ответ и печать структуры симулятор тянет, а доступ внутрь объектов массива — нет. Пакет дня подаётся отдельному документу; на плате это один doc и range-for, раскладка полей при этом настоящая в обоих мирах. Индексация объектов внутри массива движком не эмулируется; механика чтения полей при этом та же