К этому уроку на столе всё, из чего складывается настоящая батарейная метеостанция: подключение к WiFi с ожиданием статуса, HTTP-публикация, страница из скетча, сон по таймеру и память, пережидающая сны в RTC-домене. Куски по отдельности работают — проект заставляет их работать вместе, и первое, что выясняется при сборке, — вместе они жить не хотят. Это и есть главный экзамен финального урока: не новый материал, а трение между уже изученными кусками.
Конфликт фундаментальный: веб-серверу нужно ядро, WiFi и секунды бодрствования; сну нужно ровно обратное. Плата, которая вечно слушает порт 80, — это плата, живущая от банки сутки. Решение не в выборе одного режима, а в расписании из трёх состояний, и все кнопки этого расписания раздел уже раздал.
Железо финальной сборки я держу минимальным и обжитым: DHT22 на GPIO 4, кнопка на GPIO 33, банка 18650 через TP4056 с защитой. Провода короче полуметра, датчик вынесен из корпуса на метровом хвосте: DHT22, прижатый к плате, настойчиво показывал мне плюс три градуса к улице — стабилизатор греет соседа. Новых деталей в схеме нет, и проект именно этим хорош: он собирается из уже понятного. Но правило остаётся: всё, что проверяется на столе с USB-питанием, проверяется до пересадки на банку — первый запуск проекта на батарейке не должен быть первым запуском скетча вообще.
Три режима одной платы
Режим первый, основной, — сон-отправка: таймер будит плату раз в десять минут, DHT отдаёт показания, плата шлёт данные на сервер POST-ом, как в уроке про HTTP POST телеметрию, и уходит обратно. Средний ток такого цикла мы посчитали на прошлой странице — около 2 мА, два месяца от банки. Это режим по умолчанию; из него плату выводит только будильник. Причину пробуждения скетч спрашивает первой строкой — от ответа зависит вся развилка ниже. Этот if — самый важный оператор проекта.
Режим второй — веб-сервер по кнопке. Кнопка на GPIO 33 будит плату по ext0 (пин обязан жить в RTC-домене; почему 12-й брать нельзя — отдельный рассказ в уроке про кнопку и причины пробуждения), и на 45 секунд плата становится обычным устройством с браузерной страницей: открыли телефон, увидели показания, страница сама обновляется через meta refresh раз в 5 секунд. Через 45 секунд сервер умирает, плата спит. Сорок пять секунд я выбрал не с потолка: три-четыре обновления страницы с запасом на медленный телефон — и меньше двух миллиампер-часов за заход.
Считаем цену привычки «захожу на страницу когда хочу». 45 секунд по 120 мА — это 1,5 мА·ч за заход; десять заходов в день — 15 мА·ч против 50 мА·ч суточного бюджета цикла. Даже частые походы в браузер дешевле, чем укороченный период сна. Дороги не секунды бодрствования, а радио, и знать это — разница между гаджетом, который живёт месяц, и тем, что дожил до весны. Эти два слагаемых — 2 мА цикла и 1,5 мА·ч за заход — и есть весь бюджет проекта, остальное детали.
| Кусок проекта | Урок, где разобран | Что берём |
|---|---|---|
| Подключение и статус | wifi-begin-podklyuchenie-router | WiFi.begin, ожидание WL_CONNECTED с таймаутом |
| Публикация на сервер | esp32-http-post-telemetriya | GET и POST с данными в параметрах |
| Страница в браузере | esp32-webserver-html-iz-skecha | WebServer.h, обработчик, meta refresh |
| Сон и память между снами | esp32-glubokij-son-tajmer-byudzhet | enable_timer_wakeup, ULL, RTC_DATA_ATTR |
// Режим 1: таймер будит раз в 10 минут -> датчик -> отправка -> сон.// Режим 2: кнопка на GPIO 33 будит ext0 -> 45 секунд веб-сервера.#include <WiFi.h>#include <WebServer.h>#include <HTTPClient.h>#include <DHT.h>#include "esp_sleep.h" DHT dht(4, DHT22);WebServer server(80);RTC_DATA_ATTR int cikl = 0;float t = 0;float h = 0; void page() { String html = "<html><head>"; html += "<meta charset=\"utf-8\">"; html += "<meta http-equiv=\"refresh\" content=\"5\">"; html += "</head><body><h1>Balkon meteo</h1><p>"; html += "t="; html += t; html += " C, h="; html += h; html += " %, cikl="; html += cikl; html += "</p></body></html>"; server.send(200, "text/html", html);} void setup() { Serial.begin(115200); delay(300); cikl = cikl + 1; // 1. датчик: сеть ему не нужна t = dht.readTemperature(); h = dht.readHumidity(); // 2. сеть с таймаутом WiFi.begin("Dom", "parol123"); unsigned long t0 = millis(); while (WiFi.status() != WL_CONNECTED && millis() - t0 < 8000) { delay(100); } // 3. что нас разбудило? if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_EXT0) { // кнопочный заход: живём сервером ровно 45 секунд server.on("/", page); server.begin(); Serial.print("web-server: ip="); Serial.println(WiFi.localIP()); unsigned long konec = millis() + 45000UL; unsigned long tRead = 0; while (millis() < konec) { server.handleClient(); if (millis() - tRead >= 5000) { // свежие показания без delay tRead = millis(); t = dht.readTemperature(); h = dht.readHumidity(); } } } else if (WiFi.status() == WL_CONNECTED) { // плановое пробуждение: отправили и спим дальше HTTPClient http; String url = "http://192.168.1.10:8080/pogoda?c="; url += cikl; url += "&t="; url += t; url += "&h="; url += h; http.begin(url); Serial.print("otvet="); Serial.println(http.GET()); http.end(); } else { Serial.println("seti net: spim bez otpravki"); } // 4. оба будильника и сон WiFi.mode(WIFI_OFF); Serial.println("usnul na 10 minut"); delay(100); esp_sleep_enable_ext0_wakeup(GPIO_NUM_33, 1); // кнопка на RTC-пине esp_sleep_enable_timer_wakeup(600ULL * 1000000ULL); esp_deep_sleep_start();} void loop() { } // сюда плата не доходитПричину пробуждения читаем первой: ESP_SLEEP_WAKEUP_EXT0 означает кнопку, всё остальное — плановый цикл от таймера. Кнопочный заход живёт 45 секунд, внутри крутится server.handleClient() и обновление показаний раз в 5 секунд — без единого delay, тот же millis-планировщик из продвинутого раздела в миниатюре. Страница дёргает meta refresh и показывает свежее, пока заход не кончился. Долгие заходы я пробовал: час жизни сервера стоит 2 мА·ч, и это уже заметная строка в дневном бюджете.
Страничка честно примитивна: показания и счётчик снов. Украшательства я вынес — в уроке про веб-сервер с HTML из скетча есть и стили, и параметры, и автообновление; проекту хватает примитива, потому что задача страницы — показать t и h за две секунды с телефона. После 45 секунд — WIFI_OFF и оба будильника сразу: enable_ext0 и enable_timer мирно сосуществуют, сработает тот, что первым прозвенит, а таблицу причин с cause=1 и cause=3 я вычитывал в уроке про кнопку.
Пара тонкостей, которые вскрылись на живой плате. handleClient() не любит, когда браузер качает страницу медленно: если таймаут захода истечёт посреди отдачи, соединение оборвётся — не страшно, браузер покажет старое, а следующее обновление придёт с новой попытки. После перезагрузки от вилки причины бывают только таймерными — кнопку никто не нажимал. И главное: всё, что сервер показывает, он берёт из переменных, заполненных до его запуска, — поэтому датчик читается первым делом, ещё до WiFi. Сервер лишь витрина: данные готовит планировщик.
Куда я докручивал этот же проект после первого месяца. Время на странице: без синхронизации она покажет uptime ядра, а adjust из урока про NTP и configTime даёт странице настоящие часы. Протокол: HTTP с параметрами работает, но для дома с тремя датчиками я переехал на MQTT — публикация показаний с ESP32 на брокер разобрана в разделе про MQTT, и держится такая связь заметно ровнее. И то и другое встроилось без переделки цикла: меняется содержимое отправки, не сам скелет.
История: тот же цикл пишется в CSV на SD-карту — логгер со сном и батарейкой собирали в разделе про SD. И последнее моё правило для этого проекта: счётчик cikl из RTC-памяти печатается в каждой отправке. Через полгода, когда всплывёт вопрос «а плата вообще жила все эти месяцы?», ответят цифры, а не воспоминания. Счётчик ни разу не соврал за полгода — в отличие от меня, вспоминавшего, были ли в мае дожди.
Планировщик в симуляторе
Симулятор не поднимает ни WebServer, ни deep sleep — зато честно показывает ритм, на котором всё держится: опрос DHT раз в 5 секунд, публикация раз в 30 секунд, кнопка на D2 — принудительная отправка вне расписания. Ни одного delay в loop: только millis, две метки и два счётчика. Это ровно тот скелет, который на живой плате обрастает сном и страницей из скетча выше. Счётчики в логе считают сами: за минуту симуляции опросов шестнадцать, публикаций две — пропорция та же, что на плате.
Что попробовать, пока лог идёт. Нажмите кнопку дважды с интервалом меньше тридцати секунд: счётчик pub покажет две отправки с prichina=knopka, и расписание сдвинется дважды — принудительные команды не ломают планировщик, а переносят его слоты, за это я их и люблю. Ритм светодиода подскажет лишнее: вспышка равна публикации, а не опросу — опрос бесшумный, от него в логе только строки dht n=.
- 1. Почему веб-сервер не может крутиться, пока плата спит глубоким сном?
- 2. Кнопочный заход в веб-сервер длится 45 секунд при 120 мА. Сколько это съедает от банки?
- 3. Страница на плате показывает t и h. Как браузер увидит свежие показания, не перезагружая скетч?
- 4. Датчик питается от GPIO, который выключается на время сна. Первое чтение после подачи питания:
- 5. Публикация по таймеру на millis. Что сделать с меткой markPub сразу после отправки?
- Три режима: сон-отправка по таймеру, веб-сервер на 45 секунд по кнопке, тишина между ними
- Причину пробуждения читайте первой: ESP_SLEEP_WAKEUP_EXT0 — кнопка, остальное — плановый цикл
- Планировщик на millis без delay: опрос DHT раз в 5 с, публикация раз в 30 с, кнопка — вне очереди
- Страница обновляет себя сама: meta refresh раз в 5 секунд, скетч лишь держит свежие переменные
- Куда дальше: NTP-время на странице, MQTT вместо HTTP, CSV-лог на SD-карте
Частые вопросы по теме урока
- Почему веб-сервер не может крутиться, пока плата спит глубоким сном?
- Deep sleep отключает ядро и всю RAM кроме RTC-домена. Сервер — код на живом ядре, поэтому его сессии делают короткими: 45 секунд по кнопке и снова сон. Во сне гаснет ядро: некому вызывать server.handleClient()
- Кнопочный заход в веб-сервер длится 45 секунд при 120 мА. Сколько это съедает от банки?
- 120 мА × 45 с = 1,5 мА·ч. Цикл с отправкой раз в десять минут тратит около 50 мА·ч в сутки — даже десять заходов в день дешевле, чем укороченный период сна. Около 1,5 мА·ч — копейки против суточного бюджета цикла
- Страница на плате показывает t и h. Как браузер увидит свежие показания, не перезагружая скетч?
- Строка meta http-equiv=refresh content=5 в заголовке страницы заставляет браузер перезапрашивать её каждые 5 секунд, пока скетч обновляет переменные из DHT. meta refresh раз в 5 секунд: браузер сам перезапрашивает страницу
- Датчик питается от GPIO, который выключается на время сна. Первое чтение после подачи питания:
- После подачи питания DHT22 пару секунд прогревается. Холостой readTemperature() в никуда плюс пауза решают: второй вызов уже честный. Дежурное питание от 3.3 В грабли снимает. Мусор: нужен прогрев около 2 секунд и один холостой опрос
- Публикация по таймеру на millis. Что сделать с меткой markPub сразу после отправки?
- millis() не обнуляют: следующий слот считается от метки. markPub = millis() держит ритм ровно в 30 секунд даже после принудительной отправки по кнопке. markPub = millis(): иначе следующий интервал посчитается от старого нуля