Отсутствие ретраев webhook — сознательное решение: автономная работа без отправки событий это штатный режим. Пауза мониторинга до 2 минут при старте без сохранённой сети (блокирующий портал WiFiManager в setup()) — принята как есть, с указанием способа лечения, если понадобится. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
15 KiB
Pump Controller v8.2 (OLED)
Контроллер мониторинга насосной станции на Wemos D1 Mini (ESP8266).
Следит за состоянием трёх насосов (два основных + резервный), выдаёт сухие контакты аварии, показывает статус на OLED-дисплее, публикует веб-панель в локальной сети и отправляет события на HTTPS-webhook (n8n).
1. Состав репозитория
| Файл | Назначение |
|---|---|
pump_controller_8_2_OLED_DONE.ino |
Весь прошивочный код (single-file Arduino sketch) |
README.md |
Техническая документация (этот файл) |
MANUAL.md |
Руководство пользователя / монтажника |
2. Аппаратная часть
2.1 Плата
Wemos D1 Mini (ESP8266, 4 МБ Flash). Питание 5 В по microUSB или 3.3 В на пин 3V3.
2.2 Распиновка
| Пин | Имя в коде | Режим | Логика |
|---|---|---|---|
D5 |
IN_PUMP_1 |
INPUT_PULLUP |
LOW = насос 1 работает |
D6 |
IN_PUMP_2 |
INPUT_PULLUP |
LOW = насос 2 работает |
D7 |
IN_PUMP_EMERGENCY |
INPUT_PULLUP |
LOW = резервный насос работает |
D3 |
BTN_WIFI_RESET |
INPUT_PULLUP |
LOW (замыкание на GND) = кнопка нажата |
D8 |
OUT_ALARM_MAIN |
OUTPUT |
HIGH = авария основных насосов |
D0 |
OUT_ALARM_EMERGENCY |
OUTPUT |
HIGH = работает резерв |
D1 |
I2C SDA | — | OLED |
D2 |
I2C SCL | — | OLED |
Замечания по особенностям ESP8266:
D3(GPIO0) иD8(GPIO15) участвуют в выборе режима загрузки.D8должен быть подтянут к GND при старте — не вешайте на него нагрузку, тянущую пин вверх. Кнопку наD3при старте держать нажатой нельзя (плата уйдёт в режим прошивки).D0(GPIO16) не имеет внутреннего pull-up (только pull-down) и не поддерживает прерывания — используется только как выход.
2.3 Дисплей
SSD1306 128×64, I2C, адрес по умолчанию 0x3C (константа OLED_ADDR).
Если экран не найден — в лог уходит сообщение, прошивка продолжает работать
без дисплея (oledOK = false).
2.4 Подключение датчиков
Датчик — сухой контакт (реле пускателя, датчик потока, контакт КМ). Замкнут на GND → насос считается работающим. Внешние резисторы не нужны, используется внутренняя подтяжка.
3. Зависимости
Устанавливаются через Arduino Library Manager:
- Adafruit SSD1306
- Adafruit GFX Library
- WiFiManager (tzapu)
Входят в ESP8266 core: ESP8266WiFi, ESP8266WebServer, ESP8266HTTPClient,
WiFiClientSecure, EEPROM, Wire.
Board: LOLIN(WEMOS) D1 R2 & mini, Upload speed 921600, Flash size 4MB.
4. Логика работы
4.1 Антидребезг
updatePumpState() для каждого входа:
если чтение изменилось -> сбросить таймер lastDebounceTime
если чтение стабильно > 3000 мс -> зафиксировать stableState
debounceDelay = 3000 мс — длинный намеренно: фильтрует пусковые дребезги
пускателя и кратковременные просадки.
4.2 Готовность системы
systemReady становится true через debounceDelay + 500 мс после старта.
До этого момента аварии не формируются и webhook не отправляется — иначе
при включении питания система рапортовала бы ложную аварию.
4.3 Основная авария
Условие: оба основных насоса стоят (!p1 && !p2).
bothStopped -> запуск таймера mainAlarmStartTime
выдержка alarmDelay = 5000 мс
-> OUT_ALARM_MAIN = HIGH
-> webhook {"event":"MAIN PUMPS","status":"ALARM"} (однократно)
любой насос запустился
-> OUT_ALARM_MAIN = LOW
-> webhook {"event":"MAIN PUMPS","status":"OK"} (однократно)
Суммарная задержка от факта остановки до аварии: 3 с (дребезг) + 5 с
(выдержка) ≈ 8 секунд.
4.4 Резервный насос
Реакция без дополнительной выдержки, сразу после стабилизации входа:
pE = работает -> OUT_ALARM_EMERGENCY = HIGH, webhook EMERGENCY PUMP / STARTED
pE = стоит -> OUT_ALARM_EMERGENCY = LOW, webhook EMERGENCY PUMP / STOPPED
Флаги mainAlarmSent / emergencyActiveSent гарантируют отправку ровно
одного webhook на каждый переход состояния.
5. Webhook
POST на webhook_url, Content-Type: application/json:
{"event":"MAIN PUMPS","status":"ALARM","device":"Wemos_D1_Pump"}
event |
status |
|---|---|
MAIN PUMPS |
ALARM / OK |
EMERGENCY PUMP |
STARTED / STOPPED |
TLS-соединение поднимается через WiFiClientSecure с setInsecure() —
сертификат сервера не проверяется. Достаточно для отправки в доверенную
локальную/корпоративную инфраструктуру, но не защищает от MITM.
5.1 Бюджет отправки
Отправка остаётся синхронной — BearSSL выполняет TLS-хендшейк блокирующе, и
полностью асинхронного HTTPS на ESP8266 без хрупких сторонних библиотек нет.
Вместо этого блокировка ограничена по времени бюджетом
WEBHOOK_BUDGET_MS = 3000 мс, который расходуется по этапам:
- Разбор URL (
parseWebhookHost) — извлекает имя хоста, мгновенно. - DNS —
WiFi.hostByName(host, ip, WEBHOOK_DNS_BUDGET_MS), лимит 800 мс. - TCP + TLS + чтение ответа —
http.setTimeout(остаток бюджета).
Шаг 2 существует именно ради потолка: WiFiClientSecureCtx::connect(name, port)
внутри вызывает WiFi.hostByName() без таймаута и на мёртвом DNS подвешивает
loop() примерно на 10 секунд. Предварительный резолв с лимитом кладёт адрес в
кэш lwIP, после чего внутренний резолв возвращается мгновенно. Соединение
по-прежнему устанавливается по имени, поэтому SNI и заголовок Host не ломаются
(важно, если webhook живёт за реверс-прокси).
Шаг 3 опирается на то, что HTTPClient::connect() вызывает
_client->setTimeout(_tcpTimeout) до _client->connect(), а хендшейк
BearSSL ограничен тем же _timeout. Чтение ответа ограничено отдельно —
собственным циклом HTTPClient по _tcpTimeout.
Проверять бюджет между фазами внутри HTTPClient нельзя, поэтому потолок не
строго 3000 мс. Но провал любой фазы обрывает цепочку, так что лимиты не
складываются. Реальные худшие случаи:
| Сценарий | Задержка |
|---|---|
| Успешная отправка | 300–1500 мс |
| DNS не отвечает | ~800 мс |
| IP не отвечает (чёрная дыра) | ~800 мс + остаток бюджета |
| TLS не поднимается | то же |
| Хендшейк прошёл, ответа нет | ~1500 мс + остаток бюджета ≈ 3.5 с |
До введения бюджета те же сценарии давали 10–15 секунд.
Фактическое время каждой отправки пишется в лог: [HTTP] Код: 200 (412 мс).
6. Хранение настроек (EEPROM)
Эмулируемая EEPROM, 256 байт.
| Адрес | Размер | Содержимое |
|---|---|---|
0 |
2 | Magic 0xA55A — признак инициализации |
2 |
220 | Webhook URL, \0-терминированный |
При первом старте (magic не совпал) записывается значение константы
webhook_url_default — она намеренно пустая, чтобы секретный токен не попадал
в исходник и в git. Пока URL не задан через /set_webhook, sendPostWebhook()
сразу выходит и пишет в лог [HTTP] Webhook URL не задан.
Учётные данные Wi-Fi хранит WiFiManager в своей области флеша, не в этой EEPROM.
7. Веб-интерфейс
Сервер на порту 80.
| Маршрут | Метод | Описание |
|---|---|---|
/ |
GET |
HTML-панель мониторинга |
/status |
GET |
JSON {"p1":0,"p2":0,"pE":0} |
/reset_wifi |
POST |
Стереть настройки Wi-Fi и перезагрузиться |
/set_webhook |
POST |
Параметр url (≤220 симв.), сохранить в EEPROM |
Панель опрашивает /status каждые 2 секунды и перезагружает страницу при
изменении состояния. Шрифты подгружаются с Google Fonts — при отсутствии
интернета у клиента интерфейс отрисуется системным моноширинным шрифтом.
Аутентификации нет: любой в той же сети может сбросить Wi-Fi и подменить webhook. Контроллер рассчитан на изолированный технологический сегмент.
8. OLED
Обновление раз в секунду (DISPLAY_INTERVAL).
PUMP CONTROLLER v8
────────────────────────
PUMP 1 : RUNNING
PUMP 2 : STOPPED
BACKUP : STANDBY
────────────────────────
192.168.1.42
STATUS: OK
Строка статуса:
| Условие | Текст |
|---|---|
!systemReady |
INIT... |
| оба основных стоят | !! ALARM: NO PUMPS !! (инверсия, мигание 500 мс) |
| работает резерв | WARN: BACKUP RUNNING |
| иначе | STATUS: OK |
9. Кнопка сброса Wi-Fi
Пин D3, удержание BTN_HOLD_MS = 3000 мс. Во время удержания на дисплее
рисуется прогресс-бар с обратным отсчётом. По достижении порога —
wm.resetSettings() и ESP.restart(). Отпускание до порога отменяет операцию.
10. Известные ограничения
setInsecure()— TLS без проверки сертификата.- Веб-интерфейс и
/reset_wifiне защищены паролем. - Webhook отправляется синхронно и блокирует цикл, но не дольше бюджета
(см. 5.1). Выходы аварии от этого не зависят:
digitalWrite()выполняется до отправки, поэтому реле и сирена не ждут сервер. Подвисает только обновление OLED, опрос кнопки и веб-панель. - События, произошедшие без Wi-Fi или при неудачной отправке, не буферизуются
и не повторяются — webhook теряется, флаги
mainAlarmSent/emergencyActiveSentвыставляются независимо от результата POST. Ретраи и очередь сознательно не реализованы: контроллер рассчитан на работу и без отправки событий, автономность — штатный режим, а не отказ. - Пауза мониторинга при старте без сохранённой сети.
wm.autoConnect()поднимает портал настройки и блокируетsetup()в цикле ожидания до истеченияsetConfigPortalTimeout(120). Покаsetup()не завершён,loop()не выполняется: дребезг не считается, аварии не формируются, выходы не переключаются (они остаются в безопасномLOW, выставленном до Wi-Fi). В установке, где сеть не настраивается никогда, эти 2 минуты повторяются при каждом включении питания. Принято как есть; при необходимости лечитсяwm.setConfigPortalBlocking(false)+wm.process()вloop()— с оговоркой, что в неблокирующем режимеsetConfigPortalTimeoutне применяется и гасить портал придётся своим таймером. - Логика аварии — «оба стоят»: остановка одного насоса штатной ситуацией не считается и никак не сигнализируется.
- Webhook URL хранится в EEPROM в открытом виде и отдаётся веб-панелью всем, кто может её открыть. Если URL содержит секретный токен — доступ к панели равносилен доступу к токену.
11. Сборка
arduino-cli compile --fqbn esp8266:esp8266:d1_mini pump_controller_8_2_OLED_DONE.ino
arduino-cli upload -p COM3 --fqbn esp8266:esp8266:d1_mini pump_controller_8_2_OLED_DONE.ino
Монитор порта: 115200 бод.