Инженерный дневник — умная розетка на esp32

Как автоматизировать полное обесточивание материнской платы после прошивок — без Wi‑Fi, без магии, зато с полным разбором что и почему.
Плата | Реле | Пины | Связь с ПК |
ESP32-DevKitC V4 | 3× SRD-05VDC‑SL‑C | GPIO5 · GPIO19 · GPIO34 | USB‑Serial, без сети |
01 Зачем это вообще нужно
Отправная точка — не «сделать умную розетку» ради самой розетки, а конкретная практическая проблема: часть прошивок на материнской плате ПК корректно накатывается только при полном обесточивании после установки — обычного программного выключения недостаточно, БП должен действительно снять напряжение с платы на несколько секунд. Делать это руками каждый раз — то еще удовольствие, особенно если процедуру нужно повторять неоднократно во время отладки или серийного производства. Решение — вынести это на отдельный контроллер, который сможет дождаться сигнала «пора», убедиться, что ОС действительно завершила работу, аппаратно разорвать питание на 10 секунд, восстановить его и нажать кнопку включения на корпусе.
Ниже — не столько готовая инструкция «повтори за мной», сколько разбор конкретных решений и тупиков по пути: почему первая версия схемы не работала, что оказалось не тем, чем казалось, и какой код в итоге получился рабочим.
02 Комплект железа
Компонент | Роль |
ESP32-DevKitC V4 (WROOM-32D) | управляющий контроллер, USB‑Serial к ПК |
3× SRD-05VDC‑SL‑C | два реле — размыкание фазы и ноля; третье — сухой контакт на кнопку питания |
HLK‑PM01 | AC→DC 5 В для самой платы, запитан до реле |
Резисторы 1кОм × 2 | делитель напряжения для датчика на USB VBUS, до диода — оба резистора обязательны |
Сетевой фильтр, 5 розеток | механическая основа, защита от КЗ и варистор |

здесь фокус на логике и на том, почему она именно такая.
03 Силовая часть: почему два реле
Разрывать решили оба провода — L и N, — а не один. Розетки без поляризации (Schuko) не гарантируют, какой контакт при включении в стену окажется фазой: разомкнув только «предполагаемую» фазу, можно с равными шансами оставить настоящую фазу подключённой к нагрузке через второй, не разомкнутый провод. Земля (PE) при этом не размыкается никогда — идет от вилки к выходу напрямую, минуя оба реле.

Второй момент — где именно в цепи стоит собственный блок питания ESP32 (HLK‑PM01). Он подключен до обоих силовых реле, то есть получает 220 В напрямую от вилки независимо от их состояния. Если бы это было не так, ESP32 обесточивала бы саму себя в момент разрыва L/N — а ей в этот момент как раз нужно отсчитывать 10 секунд и не терять состояние.
04 Реле, которое не хотело отпускать
Это самая длинная часть истории поскольку именно тут ушло больше всего времени, а промежуточные версии объяснения оказывались частично неверными.
Первый симптом
Тестовый код каждые несколько секунд переключал реле туда‑обратно. Включение отрабатывало четко: реле щелкало, светодиод модуля менял цвет, силовая цепь размыкалась. А вот на «выключение» — сигнал вроде бы уходил в противоположный уровень, но по показаниям амперметра лабораторного блока питания ток проседал лишь частично, а не падал в ноль. Реле оставалось в сработавшем состоянии. Единственный способ заставить его вернуться в исходное — физически отсоединить провод от входа IN.
Это последнее наблюдение и стало ключевой уликой: если отключение провода работает, а «логический ноль» от GPIO — нет. Значит, дело не в самой логике переключения, а в том, что GPIO, будучи подключенным напрямую, не создает того же электрического состояния, что и физически разомкнутая цепь.
Первая версия объяснения: несовпадение уровней
Использованный на тот момент модуль реле питался от 5 В (VCC), а вход IN был подтянут к этому же VCC через резистор и светодиод оптрона — стандартная схема «низкий уровень включает». GPIO ESP32 логически выдает HIGH, но физически это всего 3.3 В, а не 5 В. Между VCC модуля (5 В) и тем, что реально держит GPIO на линии IN (3.3 В), остаётся разница около 1.7 В — а прямое падение на светодиоде оптрона обычно 1.1–1.3 В, то есть этого хватает, чтобы через него продолжал течь небольшой ток. Модулю оказывается достаточно и такого тока, чтобы держать вторичную сторону приоткрытой.
Практический фикс на тот момент — питать VCC самого модуля реле не от 5 В, а от 3.3 В ESP32: тогда GPIO HIGH в точности совпадает с VCC, разница обнуляется, и реле отпускает чисто. Это сработало и было зафиксировано как рабочее решение первой версии схемы.
Смена модулей и уточнение диагноза
Позже реле заменили на модули с полноценной оптронной развязкой (те же SRD-05VDC‑SL‑C, но исполнение с отдельными выводами VCC и JD‑VCC — логическая и силовая стороны запитаны раздельно, соединены перемычкой по умолчанию). Первая мысль была — воспроизвести прежний фикс через эти выводы: снять перемычку, посадить VCC (логику) на 3.3 В, а JD‑VCC (катушку) — на полные 5В.
Дальше выяснилось, что в этом конкретном случае можно проще: причина «недоразмыкания» была не столько в самом факте несовпадения уровней, сколько в паразитном подпоре — через IN на линии GPIO протекал небольшой обратный ток от подтяжки модуля, из‑за чего линия не садилась в чистый ноль, а зависала где‑то между. У новых модулей на входе оптрона стоит токоограничивающий резистор, который держит этот паразитный ток ниже порога срабатывания вторичной стороны — и реле чисто отпускает уже без всякого совпадения уровней VCC и GPIO. В итоге перемычку JD‑VCC/VCC оставили как есть, оба вывода — на 5 В, пин 3.3 В нигде в проекте больше не используется.
Итог трех версий: задача одна («почему реле не отпускает»), а рабочих объяснений было два, и оба на своем этапе подтверждались поведением схемы. Мораль — не полагаться на объяснение до тех пор, пока оно не проверено на реальном стенде.
Побочный эксперимент: Arduino Nano
В какой‑то момент контроллер в проекте временно заменили на Arduino Nano (ATmega328P). Резон был понятный: Wi‑Fi в проекте не использовался с самого начала протокола ARM/DISARM/STATUS, а остальные возможности ESP32 для этой задачи были явно избыточны. Плюс приятный побочный эффект: у Nano GPIO нативно 0/5 В, то есть тот самый зазор в 3.3 В против 5 В, из‑за которого разгорелась вся история выше, на этом контроллере в принципе не мог возникнуть. Пины остались теми же по смыслу, просто в других обозначениях: D5, D6, A0 вместо GPIO5, GPIO19, GPIO34.
Тут же нашлась новая версия старой ошибки — только уже про датчик, а не про реле. Первый вариант делителя на новой плате был собран как один резистор от VBUS до входа АЦП, без второго плеча на GND. Итог предсказуемый — система срабатывала не всегда: иногда цикл честно отрабатывал, иногда ПК просто гас, и дальше ничего не происходило. Добавили второй резистор — стало похоже на настоящий делитель, — но даже после этого исправления надежность на реальном ПК оставалась «переменной»: иногда чисто, иногда тишина.

Ручная симуляция (замыкание A0 на GND прямо в открытом Serial Monitor, без реального выключения ПК) три раза подряд отработала безупречно ‑сама прошивка и делитель были в порядке. Осталось подозрение на связку USB/DTR: у Nano (и клонов на CH340) открытие serial‑порта аппаратно дергает RESET платы, и тестовый скрипт, открывающий порт перед выключением ПК, теоретически мог тихо сбрасывать взвод обратно в IDLE. Доказать это стопроцентно логами не вышло: сама проверка логами требует открыть порт, а открытие порта — это и есть подозреваемый триггер, замкнутый круг.
Вместо того чтобы и дальше гоняться за этим гремлином, решили вернуться на уже проверенную связку — ESP32 с честным делителем 1кОм/1кОм. Один долгосрочный результат эксперимент всё же оставил: скрипт теперь сам перепроверяет взвод через STATUS и повторяет попытку, если что‑то пошло не так — эта защита осталась в коде и на ESP32, независимо от платформы, на которой её обнаружили нужной.
05 Одна нога на два канала
L и N разрывает двухканальный модуль, но управляется он не двумя разными GPIO, а одним: выводы IN1 и IN2 физически соединены перемычкой прямо на плате реле. Причина — не экономия пинов, а гарантия синхронности. Выгода в том, что синхронность гарантирована на аппаратном уровне: даже если в код закрадется ошибка, физически оба канала все равно связаны одним и тем же электрическим состоянием.

06 Откуда ESP32 знает, что ПК выключился
Нужен был сигнал, который отвечает на вопрос «ОС действительно закончила выключаться». Рассматривались три варианта.
Отклоненный вариант: программный heartbeat по USB
Скрипт на ПК мог бы периодически слать в Serial “PING”, а ESP32 — считать «ПК выключился», если пинги перестали приходить. Проблема — момент, когда обрывается heartbeat, наступает намного раньше, чем реальное физическое выключение, — риск повреждения файловой системы.
Первая мысль по USB VBUS — отклонить, и не зря на тот момент
Многие материнские платы держат USB‑порты под напряжением даже в полном выключении (S5). Первой альтернативой рассматривался рельс самого блока питания ATX (+5В/+3.3В), который БП включает и выключает по сигналу PS_ON# от материнской платы и который действительно падает в ноль при полном выключении. Универсальнее — но требует лезть в корпус ПК.
Рабочий вариант: VBUS, но строго до диода
VBUS все‑таки проверили на конкретной связке — этой материнской плате и этом DevKit — и оказалось, что VBUS реально проседает в ноль при штатном выключении. Осталась одна тонкость: на плате ESP32-DevKit между линией VBUS и общей 5 В‑шиной 5 В стоит диод защиты от обратного питания. Делитель напаян на дорожку VBUS прямо на плате DevKit, строго до этого диода — иначе точка смешалась бы с внешним питанием платы и показывала бы «включено» всегда.
Делитель — два резистора по 1кОм, на выходе ~2.5 В при включенном ПК и 0В при выключенном. Пин — GPIO34 (ADC1), который продолжает нормально читаться даже при активном Wi‑Fi.

Почему нельзя сэкономить и обойтись одним резистором
Без резистора на GND делить попросту не с чем: входное сопротивление АЦП — единицы‑десятки МОм, ток через единственный резистор исчезающе мал, и на входе окажется практически весь VBUS как есть. Именно эта экономия и стала первопричиной нестабильности при эксперименте с Nano (см. врезку в предыдущем разделе).
Даже если МК питается на те же «около 5 В» — это не одна и та же шина: VBUS приходит от ПК, питание платы — от отдельного источника, и они не обязаны совпадать в моменте. С делителем 1кОм/1кОм это перестает быть вопросом: на входе всегда ~2.5 В, с большим запасом.
Дебаунс: код не реагирует на первое же показание ниже порога, а требует, чтобы напряжение непрерывно оставалось низким минимум 2 секунды подряд.
07 ARM, DISARM, STATUS — протокол по USB
Постоянный опрос делителя без «разрешения» запускал бы весь цикл при любом обычном выключении компьютера. Значит, нужен отдельный сигнал «начни следить», который приходит только перед той самой прошивкой, что требует полного обесточивания.
Кандидат «использовать Wi‑Fi и HTTP‑запрос» был осознанно отклонен — узел существует именно на случай восстановления после сбоя, и заставлять его зависеть от роутера и сети — плохая идея.
Выбор пал на USB‑Serial — тот же п орт, что уже используется для прошивки и монитора:
ARM — взвести ожидание; ESP32 начинает следить за делителем. Отвечает OK armed либо BUSY.
DISARM — снять взвод вручную, не дожидаясь пятиминутного тайм‑аута.
STATUS — текущее состояние конечного автомата и мгновенное напряжение на делителе.
Питание самой ESP32 через USB по‑прежнему не идёт — плата запитана от отдельного источника через собственный 5V‑пин. Наличие диода, защищающего линию VBUS от обратного питания, стоит физически прозвонить — и именно до него нужно подключать делитель.
08 Код: конечный автомат
Вся логика — неблокирующий конечный автомат на пяти состояниях (IDLE → ARMED → CUTTING → RESTORING → PRESSING_BTN → IDLE), без единого delay() внутри loop().
main.cpp — назначение пинов
const int RELAY_LN_PIN = 5; // GPIO5 — общее управление реле L и N разом
const int RELAY_BTN_PIN = 19; // GPIO19 — реле, сухой контакт на PWR_SW
const int VOLT_SENSE_PIN = 34; // GPIO34, ADC1 — делитель 1к/1к с USB VBUS
main.cpp — пересчёт АЦП в милливольты
// на ESP32 есть готовая калиброванная функция esp32-arduino-core
int readMilliVolts(int pin) {
return analogReadMilliVolts(pin);
}
Обёртка тонкая, но не случайная: когда код на время портировался на Arduino Nano, analogReadMilliVolts() пришлось заменить на ручной пересчёт для AVR — readMilliVolts() как раз и появилась, чтобы остальной код не зависел от платформы. После возврата на ESP32 обёртку решили оставить — минимальная цена за портируемость на будущее.
main.cpp — тайминги цикла
const int POWER_OFF_THRESHOLD_MV = 1000; // ниже — рельс обесточен
const unsigned long DEBOUNCE_MS = 2000; // непрерывно "в нуле" столько мс
const unsigned long CUTOFF_MS = 10000; // сколько держим L/N разомкнутыми
const unsigned long PSU_SETTLE_MS = 2000; // пауза перед нажатием кнопки
const unsigned long BUTTON_PULSE_MS = 300; // длительность "нажатия" кнопки
const unsigned long ARM_TIMEOUT_MS = 5UL 60UL 1000UL; // автоснятие взвода
main.cpp — обработка ARM / DISARM
void handleArmCommand() {
if (state == State::IDLE) {
lowSince = 0;
enterState(State::ARMED);
Serial.println("OK armed — жду выключения платы");
} else {
Serial.print("BUSY state=");
Serial.println(stateName());
}
}
DISARM сознательно разрешён только из IDLE и ARMED — но не из CUTTING, RESTORING или PRESSING_BTN. Отменять уже начатое физическое действие — источник куда более странных состояний, чем просто отказ выполнить команду не вовремя.
09 Скрипт для сквозного теста
Чтобы не гонять весь цикл руками, есть Python‑скрипт: посылает ARM, дожидается подтверждения, затем — обратный отсчёт и настоящий poweroff ОС. После истории с Nano скрипт заодно научили не доверять первому же «armed» на слово.
test_power_cycle.py — взвод с повтором и контрольной проверкой
for attempt in range(1, max_attempts + 1):
ser.write(b"DISARM\n")
readresponse(ser, timeout, ("OK", "ERR"))
time.sleep(disarm_settle)
ser.write(b"ARM\n")
line = readresponse(ser, timeout, ("OK armed", "BUSY", "ERR"))
if line and line.startswith("OK armed") and confirmarmed(ser, timeout):
return True # STATUS подтвердил STATE=armed
time.sleep(retry_delay) # не подтвердилось — пробуем ещё раз
return False # попытки исчерпаны, выключение не запускаем
Все — в одном открытом соединении: переоткрытие порта между попытками само по себе дернуло бы DTR и сбросило бы плату. DISARM перед каждым ARM гарантирует чистый старт, а сверка через STATUS не дает поверить простому эху команды. Если все попытки исчерпаны, скрипт вообще не идет выключать компьютер.
Отдельный флаг dry‑run подтверждает только Serial‑рукопожатие и не выключает машину по‑настоящему — а в конце сам шлет DISARM, чтобы не оставлять плату висящей в ARMED до пятиминутного тайм‑аута.
10 Сборка и меры безопасности
Силовая часть — это 220 В. Собирать и прозванивать схему — только при устройстве, отключенном от сети. Первое включение — через УЗО или автомат, кратковременно и под наблюдением.
На макетке — сначала со светодиодами вместо реле, силовую часть отдельно от логики.
Проверять не «код компилируется», а физическое поведение: отпускает ли реле полностью от GPIO HIGH.
Делитель на VBUS — всегда оба резистора (на VBUS и на GND), никогда не один резистор в разрыв.
Общая земля обязательна между ESP32, делителем и материнской платой ПК.
Низковольтную и высоковольтную проводку — физически разносить, не пускать одним пучком.
Перед первым реальным циклом — проверить STATUS по Serial и убедиться, что показания делителя совпадают с реальным состоянием ПК.
11 Что в итоге и чего это стоило
Готовая система работает так: скрипт на ПК шлет ARM перед нужной прошивкой → ESP32 следит за USB VBUS через делитель, напаянный до диода → как только ОС действительно завершила выключение, размыкает L/N на 10 секунд → замыкает обратно → выжидает стабилизацию БП → на 300 мс замыкает контакты кнопки питания. Вся логика — на одной ноге для двух реле, без Wi‑Fi, без стороннего сервера, с диагностическими командами прямо в Serial.

Самое дорогое по времени в этом проекте — не написание кода, а несколько циклов ошибочных или неполных объяснений вокруг одних и тех же узлов: сначала реле, потом датчик напряжения — и даже смена самого контроллера на Nano не срезала путь, а добавила ровно такой же урок на новом железе. Каждый раз схема «работала» после очередного фикса, и каждый раз более внимательная проверка показывала, что дело было немного не в том, о чём подумали сначала. Хороший повод для памятки: если поведение исправилось, а объяснение так и не проверено экспериментально до конца, стоит отнестись к этому объяснению с долей сомнения.
Понравился материал и хотите решать подобные задачи на практике в команде «Гравитон»? Присылайте резюме на почту: k.zhdanova@3l.ru
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.