К этому уроку на столе всё, из чего собирается взрослый клиент: WiFi и GET-запрос мы гоняли на ESP32 ещё в базовых уроках и подробно в уроке про HTTPClient, разбор ответа держим с урока про deserializeJson, строку на LCD выводили в уроке про первый текст на LCD, а конфиг в EEPROM лежит с прошлого урока. Погода соберёт всё в один скетч — и покажет главную правду сборки: по отдельности куски работают, а вместе начинают требовать порядка. Пять проверок по порядку, две фильтрации и одно правило про порог, о которое я сам споткнулся на первом же запуске.
Мой первый клиент погоды был сборкой из готовых кусков, и он держался ровно до первого обрыва роутера: setup завис в цикле подключения, экран показывал «WiFi...» до выключения питания. Тогда я и выписал для себя порядок, который теперь ставлю во все сетевые скетчи: каждой беде — своя проверка, и все проверки — до первого касания экрана. Пользователь, который смотрит на устройство три секунды, не должен видеть полуфабрикат.
И сразу настрой на проект: он не про погоду. Погодный API здесь просто честный собеседник — живой, с задержками, с полем, которое однажды переименуют. Подставьте вместо него курс валют, состояние склада или данные вашего же узла — скелет останется тем же: подняться, спросить, проверить четыре раза, показать одно число, принять команду с порогом. Итоговый проект — это про порядок сборки, а порядок переживает тему. Каждый элемент цепочки ниже подписан и проверен, так что сборка превращается в сверку, а не в изобретение.
Цепочка: пять проверок по порядку
Порядок в клиенте — цепочка проверок, и каждая снимает один класс беды. Сначала сеть: WiFi.status() в цикле с таймаутом на десять секунд — без таймаута умерший роутер вешает setup навсегда, как было у меня. Потом транспорт: код ответа, 200 значит данные, всё остальное — к экрану с кодом, не разбирая. Потом разбор: DeserializationError в err — сервер отдал HTML вместо JSON, такое бывает при лимитах и капчах. Потом содержимое: containsKey("temp") — API жив, но поле сменило имя или пропало. И только после четырёх зелёных галочек документ касается LCD. Та же схема из урока про JSON по HTTP, собранная теперь с дисплеем и настройками.
Отдельное слово про частоту опроса. Бесплатные погодные API режут частоту: сверх лимита — перекос кодов, временные блокировки, HTML вместо JSON. Раз в минуту — уже много, моё рабочее расписание — раз в десять-пятнадцать минут, а между запросами плата спит или занимается своими делами. Повтор после провала — не мгновенный: пауза в пару минут, чтобы не долбить сервер, который и так отвечает плохо. Уважительный клиент живёт дольше на любом API.
Слово «фильтры» в заголовке проекта работает с двух сторон. На входе — фильтр ArduinoJson: разрешаю ключи temp, city, desc, и парсер пропускает остальное — недельный прогноз, давление, ультрафиолет; документ остаётся маленьким, и на Uno это вопрос выживания, а на ESP32 — вопрос опрятности. На выходе — логический фильтр: по документу я решаю, что показать человеку на экране. Температура и вердикт «norma» или «hladno» по порогу; подробности уходят в Serial тому, кто подключится и спросит. Экран — не свалка данных, а витрина: три секунды взгляда — один вывод.
#include <WiFi.h>#include <HTTPClient.h>#include <ArduinoJson.h>#include <LiquidCrystal_I2C.h>#include <EEPROM.h> const char* WIFI_SSID = "dom";const char* WIFI_PASS = "parol";const char* URL = "http://api.example.ru/weather?city=Moscow"; DynamicJsonDocument doc(768);LiquidCrystal_I2C lcd(0x27, 16, 2);int porog = 20; void setup() { Serial.begin(115200); EEPROM.begin(16); // ESP32: зарезервировать байты во флеше if (EEPROM.read(0) != 0) { porog = EEPROM.read(0) - 50; // порог хранится со смещением +50 } lcd.init(); lcd.backlight(); lcd.setCursor(0, 0); lcd.print("WiFi..."); WiFi.begin(WIFI_SSID, WIFI_PASS); unsigned long t0 = millis(); while (WiFi.status() != WL_CONNECTED && millis() - t0 < 10000) { delay(100); } if (WiFi.status() != WL_CONNECTED) { lcd.print(" net seti"); return; } HTTPClient http; http.begin(URL); int code = http.GET(); if (code != 200) { lcd.setCursor(0, 0); lcd.print("HTTP "); lcd.print(code); http.end(); return; } String body = http.getString(); http.end(); DeserializationError err = deserializeJson(doc, body); if (err) { lcd.setCursor(0, 0); lcd.print("json ne vysel"); return; } if (!doc.containsKey("temp")) { lcd.print("net temp"); return; } float t = doc["temp"]; lcd.clear(); lcd.print(doc["city"].as<const char*>()); lcd.print(" "); lcd.print(t); lcd.setCursor(0, 1); lcd.print(t < porog ? "hladno!" : "norma");} void loop() { if (Serial.available()) { String cmd = Serial.readStringUntil('\n'); cmd.trim(); if (cmd.startsWith("porog ")) { porog = cmd.substring(6).toInt(); EEPROM.update(0, porog + 50); EEPROM.commit(); // ESP32: без commit не сохранится } }}Порог, который переживает выключение
Порог замерзания — не константа: для зелени на подоконнике он один, для рассады другой, и бегать с ноутбуком ради каждого значения я устал ещё на теплице. Поэтому порог живёт в EEPROM по схеме прошлого урока, с одной добавкой: байт не умеет хранить минус, а порог надо уметь ставить хоть в минус десять. Хранение со смещением плюс пятьдесят решает это одним движением: записываем порог плюс пятьдесят, читаем минус пятьдесят — диапазон от минус пятидесяти до плюс двухсот пяти, для температуры с запасом. Приём из арсенала побайтовой раскладки чисел, только без разбора на два байта — один байт и так справляется. Метка тут не нужна: ячейка ноль либо пустая, либо валидное значение порога плюс пятьдесят — третья история раскладкой не предусмотрена, и в этом её сила.
В сим-скетче ниже вся цепочка живая: WiFi поднимается, GET отдаёт код и демо-тело, разбор идёт по полям, LCD рисует город с температурой и вердикт, а порог читается из EEPROM — в симуляторе она живёт между запусками скетча, так что после перезапуска порог прочитается обратно. Чтение полей точечной формой doc.temp: движок книги понимает только точку, на плате скобки doc["temp"] равнозначны — та же оговорка, что и в уроке про синтаксис JSON. В мониторе три команды: porog 15 меняет и сохраняет порог, status печатает текущие значения, вопрос со списком. После смены порога вердикт на второй строке пересчитывается сразу — без перезапуска и перепрошивки.
Почему команды именно в Serial? Потому что это интерфейс, который не требует ни кнопок, ни приложения: подключился по USB — и управляешь. Три команды — минимум, с которым узел конфигурируем: поменять порог, спросить состояние, получить список. Протокол из урока про команды по проводу ложится сюда один в один: строка до терминала, обрезка пробелов, разбор префикса. Грабля одна: readStringUntil ждёт терминатор, и без перевода строки в конце отправки команда висит до таймаута — проверьте настройку монитора, прежде чем винить скетч.
- Поменяйте порог. Печатайте porog 15 в монитор: вердикт на LCD сменится с hladno на norma, а EEPROM.update сохранит значение — перезапустите скетч и найдите строку porog iz pamyati.
- Держите диапазон. Печатайте porog 99: скетч ответит про диапазон и ничего не запишет — проверка ввода стоит дешевле чиненой настройки.
- Ломайте порядок. Печатайте что-нибудь постороннее: ответ «ne ponyal» со списком команд — протокол, который не молчит, экономит часы отладки.
- Стройте витрину. Добавьте в вердикт ветку для жары: t выше тридцати — «zharko» — и посмотрите, как одна функция verdictNaLcd растёт в полноценный фильтр вывода.
Скетч для платы — это сим-версия, доведённая до ума: библиотеки ESP32, таймаут сети, DeserializationError, скобочное чтение и EEPROM.begin с commit. Разница — в деталях, и в них цена. getString() тащит весь ответ в память: на ESP32 с сотнями килобайт это ок, на Uno путь другой — разбор из потока и фильтр, чтобы документ не рос. Экран обновляется только после всех проверок — витрина никогда не показывает полусырые данные. А вердикт вынесен в отдельную функцию: порог меняется по Serial, и экран должен отзываться без переподключения и перезапуска.
Каждый шаг клиента печатает в Serial строку-свидетельство: code=, ok=, porog=, city=. Это десятки байтов и секунд разовой отладки, зато по логу за один запуск видно, где цепочка порвалась, — без отладчика и осциллографа. В рабочем узле печати можно перевести в редкий режим: раз в час или по команде log — но не выкорчёвывайте их совсем: при следующем странном сбое именно эти строчки расскажут, что случилось до того, как вы добрались до платы.
Куда расти дальше: заменить периодический опрос публикациями и подписками — MQTT на ESP32 снимает с клиента роль спрашивающего, и брокер сам присылает новые значения. Либо оставить HTTP, но вынести опрос в собственный таймер по millis, чтобы loop не блокировался на сети. И последнее: первый запуск этого клиента у меня висел на WiFi десять минут без единой строчки в Serial — я вынес begin() подключения до Serial.begin и логировал тишину. Порядок инициализации — тоже фильтр: сначала порт, потом всё остальное, иначе отлаживать нечем.
| Шаг | Проверка | Экран при провале |
|---|---|---|
| WiFi | status == WL_CONNECTED, таймаут 10 с | net seti |
| GET | Код ответа == 200 | HTTP и код: 404, 401, минус один |
| Разбор | err == Ok, затем containsKey(temp) | json ne vysel |
| Вердикт | temp против порога из EEPROM | Порог по умолчанию 20 |
- 1. Почему порог хранят в EEPROM со смещением +50?
- 2. Что обязан вызвать скетч на ESP32 после EEPROM.update()?
- 3. Правильный порядок проверок в клиенте после старта:
- 4. Зачем циклу WiFi.status() нужен таймаут?
- 5. Чтение doc.temp в сим-скетче вместо doc["temp"] — это:
- Цепочка клиента: WiFi с таймаутом, код 200, err, containsKey — и только после всех проверок экран
- Две фильтрации: Filter на входе обрезает лишние поля документа, логический порог на выходе решает, что видит человек
- Порог хранится со смещением +50: байт не умеет минус, а порог замерзания нужен и отрицательным
- ESP32: EEPROM.begin() в setup и EEPROM.commit() после записи — без commit сохранение вымышленное
- На Uno разбор из потока вместо getString(): ответ не дублируется в куче, документ держит фильтр маленьким
Частые вопросы по теме урока
- Почему порог хранят в EEPROM со смещением +50?
- Байт беззнаковый: минус десять в него не кладётся. Смещение плюс пятьдесят растягивает диапазон порога от минус пятидесяти и выше — читается обратным вычитанием. байт не умеет хранить минус, а порог бывает отрицательным
- Что обязан вызвать скетч на ESP32 после EEPROM.update()?
- На ESP32 EEPROM эмулируется во флеше: запись ложится в буфер, и только commit переносит её в постоянную память. Без commit значение живёт до перезагрузки. EEPROM.commit()
- Правильный порядок проверок в клиенте после старта:
- Каждая проверка снимает свой класс беды и стоит до касания экрана: сеть, транспорт, разбор, содержимое. Пользователь не должен видеть полуфабрикат. WiFi с таймаутом, код 200, err, containsKey
- Зачем циклу WiFi.status() нужен таймаут?
- Без таймаута умершая сеть превращает setup в вечный цикл: устройство показывает «WiFi...» до выключения питания. Десять секунд — и понятный экран с вердиктом. чтобы setup не висел вечно при умершем роутере
- Чтение doc.temp в сим-скетче вместо doc["temp"] — это:
- На плате doc.temp и doc["temp"] — одно и то же. Движок книги читает поля точечной формой, и все сим-скетчи раздела честно это оговаривают. движок книги понимает только точку, на плате формы равнозначны