Files
Arduino/pump_controller_8_2_OLED_DONE
oskarvitaliiandClaude Opus 5 4c760e943f Задокументировать два принятых ограничения
Отсутствие ретраев webhook — сознательное решение: автономная работа
без отправки событий это штатный режим.

Пауза мониторинга до 2 минут при старте без сохранённой сети
(блокирующий портал WiFiManager в setup()) — принята как есть,
с указанием способа лечения, если понадобится.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:59:57 +07:00
..

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 мс, который расходуется по этапам:

  1. Разбор URL (parseWebhookHost) — извлекает имя хоста, мгновенно.
  2. DNSWiFi.hostByName(host, ip, WEBHOOK_DNS_BUDGET_MS), лимит 800 мс.
  3. 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 мс. Но провал любой фазы обрывает цепочку, так что лимиты не складываются. Реальные худшие случаи:

Сценарий Задержка
Успешная отправка 3001500 мс
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 бод.