Погодную панель на даче я собрал за вечер и три дня честно любовался, как она показывает 293.15. Не минус, не плюс двадцать — двести девяносто три градуса с четвертью. HTTP-запрос я уже умел делать из урока про HTTP-запросы на ESP32, JSON разбирать — из урока про разбор ответа по WiFi, и каждый этаж по отдельности работал. А в склейке я забыл один параметр в URL: без units=metric OpenWeatherMap присылает температуру в Кельвинах, и мой doc["main"]["temp"] честно выводил абсолютную шкалу. Стыков в этом проекте два, и оба стоили мне ночей — зато теперь на каждом стыке у меня стоит проверка.
Через неделю я словил второй сюрприз: панель показала нули. Запрос уходил, ответ приходил, но разбор сорвался — сервис вернул страницу-заглушку вместо данных, а мою проверку кода HTTP я в тот скетч так и не дописал. Оба случая — не про библиотеку и не про сеть: оба про стыки. Проект — это три этажа, которые вы уже строили по отдельности, соединённые двумя строками кода: String body = http.getString(); и lcd.print(t, 1);. Именно на стыках живёт половина граблей.
В этом уроке склеим всё в одну панель: ключ и URL с параметрами, разбор вложенного main.temp и массива weather, вывод города, температуры и описания на две строки LCD. Проверим цепочку в симуляторе — там она настоящая от WiFi.begin до дисплейной ячейки, — а заодно разберёмся, почему описание погоды на настоящем LCD может превратиться в кракозябры.
Ключ, URL и три параметра
Регистрация на openweathermap.org бесплатна: почта, письмо, ключ вида 32 шестнадцатеричных символов. Ключ вставляется параметром appid в каждый запрос. Первая ловушка ждёт не в коде, а в ожиданиях: новый ключ активируется не мгновенно — после регистрации API до пары часов отвечает кодом 401. Я тогда пересоздал ключ трижды, подозревая опечатку, пока не дочитал письмо до конца. Если 401 приходит на свежий ключ — просто подождите.
Сам запрос выглядит так: api.openweathermap.org/data/2.5/weather?q=Moscow&appid=ВАШ_КЛЮЧ&units=metric&lang=ru. Параметр units=metric переводит шкалу в Цельсии — тот самый забытый мной кусочек. Параметр lang=ru просит описание погоды по-русски: «небольшой снег» вместо «light snow». Город задаётся через q=; если в названии есть пробел — закодируйте его как %20: q=Nizhny%20Novgorod — процент-двадцать тут пробел, закодированный для URL.
Лимиты бесплатного плана: 60 запросов в минуту и около миллиона в месяц — на первый взгляд можно опрашивать погоду хоть каждую секунду. Но данные на стороне сервиса обновляются раз в десять минут, и мой запрос чаще этого добавляет только трафик и риск упереться в лимит. Мой интервал — 10 минут: 144 запроса в сутки при лимите 86 400 в минуту. Запас в шестьсот раз — этого достаточно, чтобы спать спокойно. И держите ключ в константе вверху скетча: переезд панели на другой город — это правка одной строки, а не охота по всему коду.
Структура ответа — матрёшка из синтаксиса JSON: температура живёт во вложенном объекте main — doc["main"]["temp"], описание — первым элементом массива weather — doc["weather"][0]["description"], имя города — в поле name. Каждый уровень вложенности стоит памяти в документе, и его цену мы считали в уроке про JsonDocument седьмой версии. В симуляторе движок отдаёт упрощённое тело с плоскими ключами city, temp, desc — механика чтения та же, а матрёшку оставим плате.
#include <WiFi.h>#include <HTTPClient.h>#include <ArduinoJson.h>#include <LiquidCrystal_I2C.h> const char* WIFI_SSID = "DachaWiFi";const char* WIFI_PASS = "parol";// units=metric — Цельсии; lang=ru — описание по-русски:String url = "http://api.openweathermap.org/data/2.5/weather?q=Moscow" "&appid=VASH_KLYUCH&units=metric&lang=ru"; LiquidCrystal_I2C lcd(0x27, 16, 2);JsonDocument doc;unsigned long lastFetch = 0;const unsigned long PERIOD = 600000UL; // 10 минут: данные сервиса так обновляются void fetchWeather() { HTTPClient http; http.begin(url); int code = http.GET(); if (code != 200) { Serial.print("HTTP kod: "); Serial.println(code); // 401 — ключ не активирован, 429 — перебор лимита http.end(); return; } String body = http.getString(); http.end(); DeserializationError err = deserializeJson(doc, body); if (err) { Serial.print("Razbor sorvan: "); Serial.println(err.c_str()); return; } float t = doc["main"]["temp"]; // вложенный объект main const char* desc = doc["weather"][0]["description"]; // массив weather const char* city = doc["name"]; Serial.print(city); Serial.print(": "); Serial.println(t, 1); lcd.clear(); lcd.setCursor(0, 0); lcd.print(city); lcd.print(" "); lcd.print(t, 1); // один знак после точки: -12.5 занимает 5 ячеек lcd.setCursor(0, 1); lcd.print(desc);} void setup() { Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); } lcd.init(); lcd.backlight(); lcd.print("zagruzka..."); fetchWeather(); lastFetch = millis();} void loop() { if (millis() - lastFetch >= PERIOD) { // таймер без delay — как в уроке lastFetch = millis(); // про millis вместо delay fetchWeather(); }}Цепочка в симуляторе: от WiFi.begin до дисплейной ячейки
В симуляторе эта цепочка работает целиком, кроме самих данных: WiFi.begin подключается с печатью в лог, http.GET возвращает код 200, а getString отдаёт демо-тело {"city":"Moscow","temp":18.5,"desc":"Малооблачно"} — фиксированное, живого интернета у движка нет, и я это оговариваю честно. Разбор — настоящий deserializeJson из строки, как в уроке про первый разбор.
Одна деталь чтения: в скетче ниже поля читаются точечной формой — doc.temp, doc.city, doc.desc. На настоящей плате точка и квадратные скобки равнозначны, но движок книги понимает только точку: скобочная doc["temp"] в чтении даёт ноль. Я проверил это на движке, прежде чем писать урок, — если увидите ноль в чужом sim-скетче, теперь знаете причину. А за страховки вроде оператора | для отсутствующих полей — в уроке про чтение полей.
Кнопка на макетке — «обновить сейчас»: на плате так делают, чтобы не ждать таймера десять минут. Значения при повторных нажатиях не меняются — демо-тело фиксировано, и это честная особенность стенда: на плате каждое обновление приносит свежие показания. Вторая строка LCD с описанием — место, где всплывает кириллица. Движок печатает «Малооблачно» без вопросов, а настоящий LCD1602 живёт с одним из двух ROM: стандартный хранит латиницу и японскую катакану — русский текст превратится в кракозябры; «русские» дисплеи с прошитой кириллицей покажут текст как есть. Практика без сюрпризов: запрашивайте lang=en или транслит в скетче, а русские буквы оставьте дисплеям с русской прошивкой — их легко узнать по фотокарточке товара с «Температура» на экране.
| Что видите | Что сломано | Лечение |
|---|---|---|
| 293.15 вместо 20 | Забыт units=metric — приходит шкала Кельвина | Добавить &units=metric в URL |
| Нули на LCD, в логе 200 | Разбор сорван: HTML-страница вместо данных, код не проверен | Проверять code == 200 до deserializeJson |
| Кракозябры во второй строке | ROM дисплея без кириллицы при lang=ru | lang=en, транслит или дисплей с русской прошивкой |
| Пустой экран с подсветкой | Не тот адрес: не 0x27, а 0x3F | Прогнать I2C-сканер и сверить адрес |
Память: почему панель живёт на ESP32
Полный ответ OpenWeatherMap — шесть-девять килобайт: координаты, видимость, облака, ветер по часам и десяток полей, из которых табло читает три. Плюс getString() держит весь ответ в String, а документ добавляет свои слоты — суммарно разбор занимает заметно больше самого ответа, и двойную память этой связки мы считали в уроке про разбор ответа по WiFi. У Uno с её 2 КБ SRAM шансов нет даже близко: документ не влезет целиком, и NoMemory придёт раньше, чем закроется фигурная скобка. Погодная панель — проект для ESP32: у него сотни килобайт кучи, и десяток под разбор никто не заметит.
В симуляторе документ объявлен как DynamicJsonDocument doc(768) — оформление шестой версии с фиксированной ёмкостью; движок книги понимает и его, и седьмую версию, где класс один и растёт сам. Демо-тело крошечное — три плоских ключа, — поэтому 768 байт здесь с тройным запасом. На плате с настоящим ответом семисот байт не хватит, и в седьмой версии библиотека вырастит документ в куче сама — а вот на шестой придётся закладывать ёмкость по замеру.
Последняя деталь экономного проекта: между опросами плате нечего делать десять минут. Для настольной панели это неважно, а для батарейного узла — это глубокий сон на весь интервал и просыпание по таймеру; WiFi при пересыпании пересобирается с нуля, и это нормально: подключение занимает пару секунд. Я держу весь опрос в одной функции, как в code-блоке выше: однажды переписал её тело на сон — и остальной скетч не изменился ни строкой.
- Порог заморозка. Добавьте в скетч ветку: если t < 0.0 — печатайте в Serial preduprezhdenie=led; на настоящей плате сюда встанет реле или зуммер из урока про реле.
- Город по кнопке. Замените q=Moscow на q=Sochi в URL внутри fetchWeather и запустите: тело то же, но путь запроса в sys-логе меняется — движок печатает его на каждый begin.
- Фаренгейт второй строкой. После lcd.print(t, 1) добавьте lcd.print(" / "); lcd.print(t * 1.8 + 32.0, 0); — перевод шкалы делается после разбора, а не в нём.
- На настоящей плате. Перенесите code-блок из середины урока на ESP32: вложенные doc["main"]["temp"] и doc["weather"][0] сработают там, где движок даёт ноль.
- Проект = три этажа, построенных по отдельности, плюс два стыка: String body = http.getString() и lcd.print(t, 1) — грабли живут на стыках
- URL: q= — город, appid — ключ, units=metric — Цельсии (без него Кельвины, 293.15), lang=ru — русские описания
- Новый ключ OpenWeatherMap активируется до пары часов: 401 сразу после регистрации — ждать, а не пересоздавать ключ
- Данные сервиса обновляются раз в 10 минут — опрос чаще не добавляет точности; мой интервал 10 минут при лимите 60 запросов в минуту
- Температура лежит в doc["main"]["temp"], описание — в doc["weather"][0]["description"]: вложенность и массив читаются цепочкой
- Кириллица на LCD1602 зависит от ROM: стандартный покажет кракозябры при lang=ru — берите lang=en, транслит или дисплей с русской прошивкой
- 1. Скетч показывает температуру 293.15. Чего не хватает?
- 2. Вы зарегистрировались минуту назад, запрос возвращает 401. Что делать?
- 3. Почему в sim-скетче этого урока чтение выглядит как doc.temp, а не doc["temp"]?
- 4. Где в настоящем ответе OpenWeatherMap лежит описание погоды?
- 5. Какой интервал опроса разумный для погодной панели на бесплатном плане?
Панель погоды собрана, и половина её деталей переезжает в следующий проект без переделки: WiFi-подключение, GET-запрос, разбор, вывод — поменяется только URL и то, что мы достаём из документа. Возьмём курсы валют: там выяснится, что float хватает с запасом на показ, но не на арифметику с деньгами, а вместо матрёшки из одного объекта вас ждёт вложенный словарь с десятками котировок. Для табло на настоящем дисплее пригодится и борьба с морганием при обновлении — мы разбирали её в уроке про тик таймера.
Частые вопросы по теме урока
- Скетч показывает температуру 293.15. Чего не хватает?
- Без units=metric OpenWeatherMap присылает Кельвины: 293.15 — это 20 по Цельсию. Ключ при этом мог работать нормально. Параметра units=metric в URL
- Вы зарегистрировались минуту назад, запрос возвращает 401. Что делать?
- 401 на свежем ключе — этап активации, она длится от минут до пары часов. Пересоздание ключа сбрасывает ожидание заново. Подождать: новый ключ активируется до пары часов
- Почему в sim-скетче этого урока чтение выглядит как doc.temp, а не doc["temp"]?
- На плате обе формы равнозначны; движок книги реализует только точечную. Скобочная форма в sim-чтении молча даёт ноль — я проверил это на движке перед написанием урока. Движок книги понимает только точку: скобочное чтение даёт ноль, на плате формы равнозначны
- Где в настоящем ответе OpenWeatherMap лежит описание погоды?
- Сервис кладёт описание в массив weather, первое описание — индекс 0. Температура — в вложенном объекте main, имя города — в name. В первом элементе массива weather: doc["weather"][0]["description"]
- Какой интервал опроса разумный для погодной панели на бесплатном плане?
- Чаще, чем обновляется источник, опрашивать бессмысленно: 10 минут покрывают обновление данных и оставляют запас в сотни раз от лимита. Раз в 10 минут: данные сервиса обновляются раз в 10 минут, лимит 60 запросов в минуту