Дачный термометр на ESP32 отработал полгода без единого сбоя — а потом молчал трое суток. На столе в городе та же плата запускалась с первого раза, датчик дышал, скетч был прежний. Причиной оказался плановый перезапуск роутера провайдером в ночь на субботу: роутер поднялся через минуту, а плата так и осталась офлайн — connection после обрыва сам не восстановился, потому что моё «подключение» жило целиком в setup() и запускалось один раз за питание.
Это классическая дыра первого WiFi-проекта, и в этом уроке мы её закрываем. Разберём, что происходит с платой в момент обрыва и какие статусы она видит. Соберём автомат реконнекта на millis() — без единого delay(), чтобы реле, кнопки и датчики продолжали работать, пока связь поднимается. Обсудим паузы между попытками и перезагрузку как последнюю страховку. Сетевой код, который переживает ночной перезапуск роутера, отличается от настольного ровно этим уроком.
Что видит плата в момент обрыва
Когда роутер гаснет, плата не замечает этого мгновенно: ассоциация держится, пока перестают приходить пакеты. Через десять-тридцать секунд ядро WiFi фиксирует потерю, и статус уезжает из WL_CONNECTED в WL_CONNECTION_LOST, а затем в WL_DISCONNECTED. Дальше развилка. В библиотеке есть WiFi.setAutoReconnect(true) — я ставлю его в каждом скетче как бесплатную страховку. Но полагаться только на него я не берусь: в разных версиях ядра автоматика срабатывает по-разному, и именно после «долгого» роутера, который поднимается больше минуты, плата иногда остаётся в подвешенном состоянии. Свой поллинг-автомат ниже я держу потому, что его поведение предсказуемо и легко читается в логе.
Почему нельзя просто завернуть ожидание в while с delay() — тот же паттерн, что в первом подключении? Потому что он живёт в setup() и блокирует всё. В сетевом термометре при обрыве связи он заморозил бы и печать температуры, и управление нагревателем — на неопределённый срок. Реконнект должен жить в loop() рядом с остальной логикой, а не вместо неё; приёмы неблокирующих таймеров мы собирали в уроке про millis() вместо delay(), здесь они ложатся один в один.
И противоположная ошибка — стрелять WiFi.begin() в каждом проходе loop(). Проходов — тысячи в секунду, и роутер получает сотни запросов аутентификации в секунду; об этой истории у меня отдельный note ниже. Между попытками обязана быть пауза — от пяти секунд, лучше с ростом: пять, десять, двадцать, до потолка в пять минут. Роутер в это время живёт свою жизнь: поднимает DHCP, договаривается с провайдером, раздаёт адреса — дайте ему эти полминуты, а не долбите аутентификациями.
Автомат из трёх состояний
#include <WiFi.h> const char* ssid = "DachaNet";const char* password = "superParol123"; int state = 0; // 0 — связь есть, 1 — потеря, 2 — ждём попыткиunsigned long lastCheck = 0; // таймер периодической проверкиunsigned long retryAt = 0; // время следующей попытки begin()int failures = 0; // счётчик неудач подряд void setup() { Serial.begin(115200); WiFi.setAutoReconnect(true); // страховка ядра, не замена автомату WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) delay(300); // первый выход в сеть Serial.print("ip="); Serial.println(WiFi.localIP());} void loop() { unsigned long now = millis(); if (now - lastCheck >= 5000) { // статус смотрим раз в 5 секунд lastCheck = now; if (state == 0 && WiFi.status() != WL_CONNECTED) { state = 1; retryAt = now + 5000; // пауза: дать роутеру подняться Serial.println("EVENT: connection lost"); } else if (state == 2 && WiFi.status() == WL_CONNECTED) { state = 0; failures = 0; Serial.print("ip="); Serial.println(WiFi.localIP()); // связь восстановлена } } if (state == 1 && now >= retryAt) { state = 2; Serial.println("ACTION: WiFi.begin"); WiFi.begin(ssid, password); // повторная попытка } if (state == 2 && now - lastCheck >= 2000) { // попытка висит дольше пары секунд — считаем неудачей и растим паузу failures++; if (failures >= 10) { Serial.println("ACTION: restart"); // последняя страховка ESP.restart(); } state = 1; retryAt = now + 5000UL * (failures + 1); // 10, 15, 20 секунд... с ростом } // ниже живёт остальной скетч: датчики, реле, кнопки — без блокировок}Логика — классический конечный автомат, как в уроке про светофор, только состояния здесь сетевые. Состояние 0 — связь есть: раз в пять секунд тихо проверяем статус. Увидели не-3 — перешли в состояние 1 и назначили попытку через пять секунд: эта пауза не лень, а вежливость к роутеру, которому нужно время подняться. В состоянии 1 дожидаемся retryAt и зовём WiFi.begin() — переход в состояние 2. В состоянии 2 ждём WL_CONNECTED; получилось — возвращаемся в 0 и печатаем адрес, снова полученный по DHCP. Попытка затянулась — считаем её неудачей и удлиняем паузу: роутер, который не поднялся за десять секунд, скорее всего ещё дышит над прошивкой.
Счётчик failures — это признание, что не все обрывы лечатся реконнектом. Десять неудач подряд обычно значат не «роутер перезагружается», а «сети больше нет в таком виде»: роутер умер окончательно, сменился пароль, сгорел блок питания. Дальше держать плату в цикле попыток бессмысленно; ESP.restart() перезагружает её начисто, скетч проходит setup() с первого подключения, и с этого момента у вас чистый лист. Для проектов на батарейке у этого же тупика есть второй выход — глубокий сон с пробуждением по таймеру; тайминги и подводные камни перезагрузок мы трогали в уроке про watchdog и сон.
Продвинутый вариант того же — события: WiFi.onEvent() регистрирует колбэк ядра, и тот зовёт begin в момент обрыва без всякого поллинга. Реакция быстрее на секунды, кода меньше. Но для первого сетевого проекта я сознательно остаюсь на поллинге: он пишется в двадцать строк, которые вы понимаете на сто процентов, а обработчики событий — это отдельный язык колбэков, где легко спрятать баг от самого себя. Мигрируйте на события, когда автомат станет скучным.
Отдельно про первый старт. Автомат в loop() начинает работать после того, как setup() вышел по WL_CONNECTED; если сети нет уже при подаче питания, while в setup() будет крутиться вечно — для термометра это сознательный выбор: без сети ему всё равно нечем заняться. Но для устройства на удалённом объекте вечный цикл — риск пропустить остальную инициализацию, поэтому я ставлю таймаут: запоминаю millis() перед циклом и, если прошло шестьдесят секунд без результата, выхожу дальше по скетчу с флагом «сети нет» — пусть хотя бы локальные датчики и экран работают, а автомат в loop() дособирает сеть, когда она появится. Универсального ответа нет — есть выбор между «без сети всё бессмысленно» и «без сети жить тоже можно», и вы принимаете его для каждого проекта заново.
И ещё одна причина обрыва, которую автомат не вылечит: сменённый пароль. Роутер после прошивки внезапно отказывает плате в сопряжении — статус будет WL_CONNECT_FAILED или вечный WL_DISCONNECTED, и никакие повторные begin() не помогут, потому что плате нечего в ответ сказать роутеру. Пароль придётся менять перепрошивкой; чтобы таких операций было меньше, credentials держат в энергонезависимой памяти и меняют через страницу настройки, а не паяльником и кабелем.
Миграция на события — дело времени, но журнал и поллинг остаются со мной и после: они отвечают на вопрос, который колбэки не показывают — как часто обрывы случаются и в какие часы. Мой дачный термометр печатает пары t= и status= в Serial на каждой проверке; за месяц накопился рисунок: сеть падала каждую ночь в три часа, и виноват оказался не роутер, а ночной профиль провайдерского оборудования. Без журнала я бы искал проблему в плате; с журналом виновник нашёлся за неделю. Поэтому в автомат из этого урока журнал встроен с самого начала: каждая смена состояния печатает строку с секундой, на которой она случилась.
| Стратегия | Как выглядит | Когда её беру |
|---|---|---|
| while + delay в setup() | Блокирует всё до WL_CONNECTED | Только первый выход в сеть при старте |
| Поллинг WiFi.status() по millis() | Автомат из трёх состояний в loop() | По умолчанию для всех моих проектов |
| WiFi.onEvent() | Колбэк ядра сам зовёт begin при обрыве | Когда реакция нужна в пределах секунды |
| ESP.restart() по счётчику | Десять неудач подряд — перезагрузка | Финальная страховка поверх автомата |
- 1. Почему проверку WiFi.status() не совмещают с delay(5000) в основном цикле?
- 2. Что разумно сделать после десяти неудачных попыток реконнекта подряд?
- 3. Какой статус говорит, что связь потеряна уже после удачного подключения?
- 4. Почему нельзя вызывать WiFi.begin() в каждом проходе loop() без паузы?
- 5. WiFi.setAutoReconnect(true) — это...
- Автомат из трёх состояний на millis(): связь, пауза после потери, повторный WiFi.begin()
- Пауза между попытками — от пяти секунд с ростом: reconnect-флуд вешает бюджетные роутеры
- setAutoReconnect(true) ставлю всегда, но как страховку, а не как гарантию
- Десять неудач подряд — ESP.restart(): когда автомат упёрся в стену, начинайте заново
- Пока идёт реконнект, loop() живёт: датчики, реле и кнопки обрыв не замечают
Теперь ваш сетевой скетч переживает ночной перезапуск роутера так же буднично, как пережил бы мигание света. Дальше два маршрута. Первый — платёжеспособная связь с сервером: в уроке про MQTT на ESP32 реконнект понадобится дважды, для сети и для брокера, и полученный автомат ляжет там второй раз. Второй — данные: в уроке про HTTP-запросы плата сама ходит в интернет за погодой и курсами, и первый её вопрос перед запросом будет ровно ваш новый навык — «какой у меня сейчас WiFi.status()?». А если в логах полезли странные коды, которые не свелись в таблицу, — откройте урок про отладку через Serial: приёмы там общие для любого железа.
Частые вопросы по теме урока
- Почему проверку WiFi.status() не совмещают с delay(5000) в основном цикле?
- delay() замораживает весь loop(), а реконнект обязан соседствовать с остальной логикой. Поэтому таймеры строят на millis(), а не на delay. delay блокирует loop: кнопки, реле и датчики замрут на эти секунды
- Что разумно сделать после десяти неудачных попыток реконнекта подряд?
- Десять неудач подряд обычно значат, что сети нет в прежнем виде. Перезагрузка возвращает скетч к чистому старту с первым подключением. Перезагрузить плату через ESP.restart()
- Какой статус говорит, что связь потеряна уже после удачного подключения?
- WL_CONNECTION_LOST — обрыв установленной связи; затем статус обычно уходит в WL_DISCONNECTED, и автомат начинает реконнект. Остальные коды относятся к сканированию и паузам между попытками. WL_CONNECTION_LOST
- Почему нельзя вызывать WiFi.begin() в каждом проходе loop() без паузы?
- Тысячи вызовов в секунду — это флуд аутентификациями. Между попытками нужна пауза от пяти секунд, лучше с ростом до минут — тот же принцип, по которому пишут повторные попытки в любых сетевых протоколах. Роутер заваливается запросами аутентификации: журнал забивается, бюджетные модели на время перестают отвечать
- WiFi.setAutoReconnect(true) — это...
- Автоматика ядра срабатывает не во всех сценариях обрыва — особенно после долгого перезапуска роутера. Свой поллинг-автомат предсказуемее. страховка ядра, которую я ставлю всегда, но не полагаюсь только на неё