Первый погодный узел я собрал за вечер: ESP32, экран, запрос к OpenWeather раз в десять минут. Разбор писал по памяти от учебных примеров — float t = doc["temp"]; — и на экране честно встал ноль при восемнадцати градусах за окном. Час я потратил на сеть, ключ и адрес, пока не напечатал ответ целиком и не увидел: температуры в корне нет. Она этажом ниже, в группе main, а описание погоды вообще в массиве — weather[0].
Настоящий ответ OpenWeather устроен не как учебный. В корне лежат плоские поля — name, cod, timezone, — рядом группа main с температурой, влажностью и давлением, массив weather с описанием и группой wind со скоростью ветра. Учебные заглушки сглаживают ответ в один этаж, и на них привыкаешь к адресу doc["temp"], которого в живом ответе не существует. Разбор полного погодного клиента я уже показывал в уроке про OpenWeather; здесь другой срез — адресация по настоящей структуре и предохранители, которые в учебных примерах молчат.
План: два этажа и один массив, float и округление до десятой, поле cod и молчаливый ноль при промахе, фильтр нужных полей вместо двух килобайт в ОЗУ. Сам запрос по HTTP с WiFi и GET разобран в материале про JSON по HTTP на ESP32 — здесь начинается там, где тот закончил: у нас на руках тело ответа, и его надо разобрать без потерь.
Два этажа и один массив
Температура живёт по адресу doc["main"]["temp"]: первый индекс возвращает группу main, второй читает поле внутри неё. Точечная форма doc.main.temp равнозначна, но в чужих скетчах чаще встречаются скобки — по ним сразу видно, что ключ может быть и с дефисом, где точка не сработает. Влажность — сосед по группе: doc["main"]["humidity"], ощущение как будто — doc["main"]["feels_like"]. Каждое имя внутри своей группы короткое, а полные адреса получаются длинными — так и задумано: группа собирает смысл, и читать её адреса можно вслух без путаницы.
weather — массив, хотя в живом ответе элемент там один. Сервис оставил место под несколько описаний, и первое берётся нулевым индексом: doc["weather"][0]["description"]. Пропущенный индекс — doc["weather"]["description"] — самая частая опечатка этого ответа: библиотека вернёт null, печать покажет пустоту, и виноватым окажется сервис. Ветер живёт третьим этажом — doc["wind"]["speed"] — и по той же схеме читается без сюрпризов.
Числа приходят float-ом с двумя знаками: temp = 18.53 при units=metric. Serial.print(t) напечатает обе цифры после точки, но для экрана и графика шага в одну десятую хватает, а длинные хвосты вроде 18.529999 иногда прорываются из-за двоичного представления дробей. Округление стоит копейки: (int)(t * 10.0) / 10.0 перед записью в переменную или прямо в печати Serial.print(t, 1). Я округляю до записи в документ — тогда и журнал, и печать показывают одно и то же число, а не две версии одной температуры.
#include <WiFi.h>#include <HTTPClient.h>#include <ArduinoJson.h> const char* ssid = "DachaNet";const char* pass = "parol";// units=metric — Цельсий, lang=ru — русские описания, appid — ваш ключString url = "http://api.openweathermap.org/data/2.5/weather?q=Moscow" "&appid=VASH_KLUCH&units=metric&lang=ru"; void setup() { Serial.begin(115200); WiFi.begin(ssid, pass); while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); } Serial.println();} void loop() { HTTPClient http; http.begin(url); int code = http.GET(); if (code != 200) { Serial.print("HTTP code="); Serial.println(code); http.end(); delay(600000UL); // raz v desyat minut return; } String body = http.getString(); http.end(); JsonDocument doc; // v6: StaticJsonDocument<768> doc; // Фильтр: из ~2 КБ ответа в пул едут только нужные поля. // JsonDocument filter; // filter["name"] = true; // filter["cod"] = true; // filter["main"]["temp"] = true; // filter["weather"][0]["description"] = true; // DeserializationError err = deserializeJson(doc, body, DeserializationOption::Filter(filter)); DeserializationError err = deserializeJson(doc, body); if (err) { Serial.print("razbor sorvan: "); Serial.println(err.c_str()); delay(600000UL); return; } // Предохранитель: промах приезжает cod "404" и message без main int cod = doc["cod"] | 0; // cod бывает числом и строкой — оба приводятся if (cod != 200) { Serial.print("zapros ne proshel: cod="); Serial.println(cod); Serial.println((const char*)(doc["message"] | "?")); delay(600000UL); return; } if (!doc["main"].containsKey("temp")) { Serial.println("main bez temperatury: pechatayu telo i smotryu glazami"); delay(600000UL); return; } float t = doc["main"]["temp"]; // 18.53 — float const char* opisanie = doc["weather"][0]["description"]; // массив, элемент нулевой Serial.print("t = "); Serial.print(t, 1); // округление до 0,1 силами печати Serial.print(" C, "); Serial.println(opisanie); delay(600000UL);}Разбор короткий. Три адреса после предохранителей — и всё: имя города мне не нужно, узел и так знает, где стоит. err.c_str() в монитор — та же привычка из сетевых уроков: обрезанное тело даст IncompleteInput, страница логина вместо JSON — InvalidInput, и имя кода отличает одно от другого без гаданий. Таблицу кодов я собирал в уроке про диагностику разбора; она одна и та же для любого сервиса.
cod — служебный код в корне, и у него есть сюрприз, о котором в документации сервиса написано мелким шрифтом: коды успеха приезжают числом, а коды промахов — строкой. cod 200 и cod "404" в одном и том же поле. Чтение в int — doc["cod"] | 0 — переживает оба написания: библиотека приводит строку "404" к числу четыреста четыре. Сравнение строк с числом через == или проверка только containsKey("cod") к этому не готовы — и предохранитель либо срабатывает всегда, либо никогда. Разбор типов значений и ловушек приведения — в уроке про is и as.
Параметры адреса экономят и байты, и нервы: units=metric даёт Цельсий вместо Кельвинов, lang=ru — русские описания вместо broken clouds. Без ключа appid сервис вернёт cod 401 — и предохранитель из скетча поймает это так же спокойно, как опечатку в городе. Я держу адрес целиком в конфиге на карте — из урока про config.json на SD: смена города без перепрошивки стоит одной правки в блокноте.
Частота опросов — отдельная статья бюджета. На бесплатном тарифе OpenWeather считает звонки: шестьдесят в минуту и миллион в месяц на ключ. Опрос раз в десять минут — сто сорок четыре вызова в сутки и четыре с лишним тысячи в месяц, запас больше двухсоткратный; раз в минуту — восемьдесят шесть тысяч в месяц, тоже влезает, но без резерва на второй узел с тем же ключом. Опрос держу на millis-планировщике без delay: loop между запросами жив и крутит экран. Кэшировать ответ смысла нет: погода меняется медленнее, чем устаревает лимит.
Промах и лишние байты
Опечатка в имени города выглядит обиднее всего: HTTP вернёт 200, тело придёт, разбор пройдёт — но внутри будет {"cod":"404","message":"city not found"} и ни следа группы main. doc["main"]["temp"] на таком ответе молча вернёт ноль: экран покажет ровный нуль, и вы час будете лечить датчик, которого нет. Предохранитель из скетча закрывает это в две строки: cod, приведённый к числу, сверяется с 200, а вторая страховка — doc["main"].containsKey("temp") — ловит сервис, который однажды переименует группу.
Второй вопрос — объём. Полный ответ текущей погоды для моего города весит около двух килобайт: координаты, восход, системный блок с кодами страны — всё это узел получает и выбрасывает. Фильтр десериализации режет состав ещё на входе: в пуле оказываются только перечисленные поля, и документ на пять полей занимает десятки байт вместо сотен. Механика фильтра с вложенными списками разобрана в уроке про фильтры, границы применимости на больших ответах считал отдельно в материале про фильтры и большие ответы. На ESP32 фильтр — вопрос чистоты, на Uno — вопрос жизни скетча.
Тело ответа в String — отдельная статья расходов: два килобайта лежат в ОЗУ копией ещё до разбора. Полное решение — deserializeJson прямо из потока сети, без промежуточной строки, — показал соседний раздел ESP32 в уроке про JSON по HTTP; фильтр туда же передаётся третьим аргументом и работает на потоке так же честно. На Uno с её двумя килобайтами связка «поток плюс фильтр» — не оптимизация, а условие работы.
Отладка промаха стоит одной печати: serializeJson(doc, Serial) сразу после разбора показывает документ целиком — видно и cod со значением, и отсутствие main, и то, что message на месте. Печатать тело сырой строкой тоже полезно, но разобранная печать короче: экранирование и длинные поля отсекаются, остаётся состав. На моей памяти пара печатей — тело и документ — закрывала почти все «погода не приходит» за минуту; исключение оказалось ключом с кириллической буквой в имени города, и message честно об этом сказал.
| Адрес в ответе | Что лежит | Примечание |
|---|---|---|
| name, cod, timezone | корень: имя города, служебный код, сдвиг времени | cod 200 у успеха, "404" строкой у промаха — приводить к числу |
| main.temp / main.humidity | 18.53 float и 71 целое | единицы задаёт units=metric; без него — Кельвины |
| weather[0].description | «облачно с прояснениями» | массив с одним элементом в живом ответе; индекс обязателен |
| wind.speed | 4.2 м/с | третий этаж; узкому прогнозу он обычно не нужен |
Честная оговорка про симулятор: HTTP-заглушка движка отдаёт плоский дубликат {"city":"Moscow","temp":18.5,"desc":"Малооблачно"} — настоящая структура main и weather в неё не влезла. Поэтому при старте симулятор печатает эталон вложенного ответа — тот самый состав, который разбирает код-блок, — а чтение по этажам doc["main"]["temp"] движок не эмулирует: на плате его делает код из первого блока. Всё остальное настоящее: cod-предохранитель, округление, LED.
Что попробовать. Кнопка D2 — новый запрос: в логе пролетают HTTP-код и разобранные поля. Влепите в монитор тело промаха {"cod":"404","message":"city not found"} — предохранитель сработает, LED погаснет, message уедет в лог. Шлите и свои плоские пакеты с отрицательной температурой: LED горит при морозе — логика решения, а не заглушка.
- 1. Температура в настоящем ответе OpenWeather лежит…
- 2. Имя города с опечаткой. Как выглядит тело ответа?
- 3. temp пришла как 18.53. Что напечатает Serial.print(t, 1)?
- 4. cod у промаха — строка "404", у успеха — число 200. Какой приём переживает оба?
- 5. Полный ответ — около 2 КБ, нужны пять полей. Что включаю в deserializeJson?
- Настоящие адреса OpenWeather: doc["main"]["temp"] и doc["weather"][0]["description"] — массив с одним элементом в живом ответе
- temp приезжает float (18.53); округление до 0,1 — (int)(t * 10.0) / 10.0 до записи или Serial.print(t, 1)
- Промах приезжает при HTTP 200: cod "404", message есть, main отсутствует — чтение температуры молча даёт ноль
- cod бывает числом и строкой: чтение в int через | переживает оба, сравнение строк — нет
- Фильтр десериализации оставляет в пуле только нужные поля: ~2 КБ ответа против десятков байт документа
Частые вопросы по теме урока
- Температура в настоящем ответе OpenWeather лежит…
- main — группа в корне, temp — поле внутри неё. Учебные заглушки сглаживают ответ в один этаж — на них и вырастает привычка к несуществующему адресу. В doc["main"]["temp"] — этажом ниже корня
- Имя города с опечаткой. Как выглядит тело ответа?
- Промах приезжает при HTTP 200: служебные поля и ни следа main. Чтение doc["main"]["temp"] молча даёт ноль — отсюда предохранитель на cod. cod "404", message "city not found", группы main нет вовсе
- temp пришла как 18.53. Что напечатает Serial.print(t, 1)?
- Второй аргумент печати — число знаков после точки. Шаг в одну десятую для погодного экрана честен, а хвосты двоичных дробей в лог не едут. 18.5
- cod у промаха — строка "404", у успеха — число 200. Какой приём переживает оба?
- Библиотека приводит строку из цифр к числу при чтении в int. Сравнение строк с числом и проверки без чтения значения на таком ответе ломаются. Чтение в int: doc["cod"] | 0 приводит строку "404" к числу
- Полный ответ — около 2 КБ, нужны пять полей. Что включаю в deserializeJson?
- Вложенный фильтр оставляет в пуле только перечисленные поля: десятки байт вместо сотен. На потоке он работает так же — третьим аргументом. DeserializationOption::Filter: состав режется ещё на входе