Настройки через веб, состояние реле в UI, распиновка под реле active-LOW
Распиновка. Пина D0, который использовался под выход «Резерв», в доступном наборе нет. Кнопка сброса Wi-Fi удалена как избыточная: при пропавшей сети wm.autoConnect() сам поднимает портал. Освободившийся D3 закрыл дефицит. Выходы переехали на D3 и D4 и стали инверсными. Причина: для реле active-LOW безопасное состояние — пин в HIGH, а D3/D4 подтянуты к HIGH внешними резисторами платы и держат этот уровень всю загрузку. D8, наоборот, подтянут к LOW и щёлкал бы реле при каждом включении питания. В setup() digitalWrite() идёт до pinMode(), иначе защёлка выхода даёт короткий LOW. Инверсия живёт только в setRelay(); логическое состояние дублируется в relayMainOn/relayEmergOn, чтобы панель показывала смысл, а не уровень пина. Настройки. device, выдержка аварии и выдержка резерва вынесены в /set_config и EEPROM. Раскладка EEPROM версионирована: magic 0xA55B, старый 0xA55A распознаётся и переносится, поэтому прошитые приборы не теряют webhook. Значения по умолчанию воспроизводят прежнее поведение (5 с и 0 с). Для резерва добавлена выдержка, которой раньше не было, симметрично основной аварии. Обе выдержки гасят реле и вебхук одновременно. device_name фильтруется до [A-Za-z0-9_-] вместо экранирования: такой набор безопасен и в JSON, и в HTML. Пустое или неверное поле формы означает «не менять», поэтому частичное заполнение не сбрасывает остальное. Состояние реле выведено в /status и в панель. Проверено: xtensa-lx106-elf-g++ -fsyntax-only -Wall -Wextra против ядра 3.1.2 — предупреждений в скетче нет (попутно убран неиспользуемый isEmergency); валидация настроек прогнана на 22 граничных случаях; раскладка EEPROM проверена на перекрытия. На железе не проверялось. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
| `pump_controller_8_2_OLED_DONE.ino` | Весь прошивочный код (single-file Arduino sketch) |
|
||||
| `README.md` | Техническая документация (этот файл) |
|
||||
| `MANUAL.md` | Руководство пользователя / монтажника |
|
||||
| `docs/specs/` | Согласованные проектные решения по крупным изменениям |
|
||||
|
||||
---
|
||||
|
||||
@@ -26,32 +27,70 @@ Wemos D1 Mini (ESP8266, 4 МБ Flash). Питание 5 В по microUSB или
|
||||
|
||||
### 2.2 Распиновка
|
||||
|
||||
Выходы рассчитаны на релейные модули **active-LOW** (`LOW` на входе = реле
|
||||
включено).
|
||||
|
||||
| Пин | Имя в коде | Режим | Логика |
|
||||
|---|---|---|---|
|
||||
| `D1` | I2C SDA | — | OLED |
|
||||
| `D2` | I2C SCL | — | OLED |
|
||||
| `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 |
|
||||
| `D3` | `OUT_ALARM_MAIN` | `OUTPUT` | `LOW` = реле «Авария» включено |
|
||||
| `D4` | `OUT_ALARM_EMERGENCY` | `OUTPUT` | `LOW` = реле «Резерв» включено |
|
||||
| `D8`, `D0`, `A0` | — | — | не используются |
|
||||
|
||||
Замечания по особенностям ESP8266:
|
||||
### 2.3 Почему выходы именно на D3 и D4
|
||||
|
||||
* `D3` (GPIO0) и `D8` (GPIO15) участвуют в выборе режима загрузки. `D8` должен
|
||||
быть подтянут к GND при старте — не вешайте на него нагрузку, тянущую пин вверх.
|
||||
Кнопку на `D3` при старте держать нажатой нельзя (плата уйдёт в режим прошивки).
|
||||
* `D0` (GPIO16) не имеет внутреннего pull-up (только pull-down) и не поддерживает
|
||||
прерывания — используется только как выход.
|
||||
Для реле active-LOW безопасное состояние — пин в `HIGH`. Значит выход обязан
|
||||
сидеть на пине, который держит `HIGH` **всю загрузку**, пока `setup()` ещё не
|
||||
выполнился, иначе реле щёлкнет ложной аварией при каждом включении питания.
|
||||
|
||||
### 2.3 Дисплей
|
||||
| Пин | GPIO | Уровень при загрузке | Реле active-LOW при загрузке |
|
||||
|---|---|---|---|
|
||||
| `D3` | 0 | подтянут `HIGH` (10к) | выключено ✓ |
|
||||
| `D4` | 2 | подтянут `HIGH` (10к) | выключено ✓ |
|
||||
| `D8` | 15 | подтянут **`LOW`** (10к) | **включено** ✗ |
|
||||
| `D1`,`D2`,`D5`,`D6`,`D7` | 5,4,14,12,13 | не определён до `pinMode` | неопределённо |
|
||||
|
||||
Для реле active-**HIGH** вывод был бы обратным: `D8` — лучший пин, `D3`/`D4` —
|
||||
худшие. Логика выходов и выбор пинов связаны жёстко; менять одно без другого
|
||||
нельзя.
|
||||
|
||||
В `setup()` порядок операций тоже важен: `digitalWrite(pin, HIGH)` вызывается
|
||||
**до** `pinMode(pin, OUTPUT)`, иначе защёлка выхода может кратковременно выдать
|
||||
`LOW`.
|
||||
|
||||
Инверсия уровня локализована в одной функции — единственном месте, где она
|
||||
существует:
|
||||
|
||||
```cpp
|
||||
void setRelay(int pin, bool on) { digitalWrite(pin, on ? LOW : HIGH); }
|
||||
```
|
||||
|
||||
### 2.4 Прочие особенности ESP8266
|
||||
|
||||
* `D4` (GPIO2) — встроенный светодиод платы, тоже активен от `LOW`. Реле
|
||||
«Резерв» получает бесплатную индикацию на плате.
|
||||
* `D3` (GPIO0) — пин выбора режима загрузки. Схема автосброса USB дёргает его
|
||||
вниз при заливке скетча, поэтому реле «Авария» щёлкает при каждой прошивке.
|
||||
Косметика, но пугает.
|
||||
* `A0` — **только аналоговый вход** (0–3.2 В, без внутренней подтяжки). Ни
|
||||
цифровым выходом, ни входом с подтяжкой быть не может, поэтому в бюджет
|
||||
цифровых пинов не входит.
|
||||
* Схема **не fail-safe**: авария включает реле, поэтому обесточенный контроллер
|
||||
сигнала не даёт. Если нужна отказобезопасность — снимайте нагрузку с
|
||||
нормально замкнутого (NC) контакта реле, тогда пропажа питания читается как
|
||||
авария.
|
||||
|
||||
### 2.5 Дисплей
|
||||
|
||||
SSD1306 128×64, I2C, адрес по умолчанию `0x3C` (константа `OLED_ADDR`).
|
||||
Если экран не найден — в лог уходит сообщение, прошивка продолжает работать
|
||||
без дисплея (`oledOK = false`).
|
||||
|
||||
### 2.4 Подключение датчиков
|
||||
### 2.6 Подключение датчиков
|
||||
|
||||
Датчик — сухой контакт (реле пускателя, датчик потока, контакт КМ).
|
||||
Замкнут на GND → насос считается работающим. Внешние резисторы не нужны,
|
||||
@@ -100,27 +139,40 @@ Board: **LOLIN(WEMOS) D1 R2 & mini**, Upload speed 921600, Flash size 4MB.
|
||||
|
||||
```
|
||||
bothStopped -> запуск таймера mainAlarmStartTime
|
||||
выдержка alarmDelay = 5000 мс
|
||||
-> OUT_ALARM_MAIN = HIGH
|
||||
выдержка alarmDelaySec (настраивается, по умолчанию 5 с)
|
||||
-> реле «Авария» включено
|
||||
-> webhook {"event":"MAIN PUMPS","status":"ALARM"} (однократно)
|
||||
|
||||
любой насос запустился
|
||||
-> OUT_ALARM_MAIN = LOW
|
||||
-> реле «Авария» выключено
|
||||
-> webhook {"event":"MAIN PUMPS","status":"OK"} (однократно)
|
||||
```
|
||||
|
||||
Суммарная задержка от факта остановки до аварии: `3 с` (дребезг) + `5 с`
|
||||
(выдержка) ≈ **8 секунд**.
|
||||
Суммарная задержка от факта остановки до аварии: `3 с` (дребезг, константа
|
||||
`debounceDelay`) + `alarmDelaySec`. При значениях по умолчанию ≈ **8 секунд**.
|
||||
|
||||
### 4.4 Резервный насос
|
||||
|
||||
Реакция без дополнительной выдержки, сразу после стабилизации входа:
|
||||
Логика симметрична основной аварии — та же схема «ожидание, затем реакция»:
|
||||
|
||||
```
|
||||
pE = работает -> OUT_ALARM_EMERGENCY = HIGH, webhook EMERGENCY PUMP / STARTED
|
||||
pE = стоит -> OUT_ALARM_EMERGENCY = LOW, webhook EMERGENCY PUMP / STOPPED
|
||||
pE = работает -> запуск таймера emergStartTime
|
||||
выдержка emergDelaySec (настраивается, по умолчанию 0 с)
|
||||
-> реле «Резерв» включено
|
||||
-> webhook EMERGENCY PUMP / STARTED (однократно)
|
||||
|
||||
pE = стоит -> реле «Резерв» выключено
|
||||
-> webhook EMERGENCY PUMP / STOPPED (однократно)
|
||||
```
|
||||
|
||||
При `emergDelaySec = 0` условие `millis() - emergStartTime >= 0` выполняется на
|
||||
следующей же итерации `loop()`, то есть реакция мгновенная.
|
||||
|
||||
### 4.5 Однократность и общая выдержка
|
||||
|
||||
Обе выдержки гасят **и реле, и webhook одновременно** — это одна настройка на
|
||||
канал, а не отдельные таймеры для реле и уведомления.
|
||||
|
||||
Флаги `mainAlarmSent` / `emergencyActiveSent` гарантируют отправку ровно
|
||||
одного webhook на каждый переход состояния.
|
||||
|
||||
@@ -139,6 +191,11 @@ pE = стоит -> OUT_ALARM_EMERGENCY = LOW, webhook EMERGENCY PUMP / STOPP
|
||||
| `MAIN PUMPS` | `ALARM` / `OK` |
|
||||
| `EMERGENCY PUMP` | `STARTED` / `STOPPED` |
|
||||
|
||||
Поле `device` берётся из настройки `device_name` (задаётся через веб-панель,
|
||||
по умолчанию `Wemos_D1_Pump`). Значение отфильтровано при вводе до
|
||||
`[A-Za-z0-9_-]`, поэтому экранирование в JSON не требуется — сломать тело
|
||||
запроса ему нечем.
|
||||
|
||||
TLS-соединение поднимается через `WiFiClientSecure` с `setInsecure()` —
|
||||
сертификат сервера **не проверяется**. Достаточно для отправки в доверенную
|
||||
локальную/корпоративную инфраструктуру, но не защищает от MITM.
|
||||
@@ -186,19 +243,41 @@ BearSSL ограничен тем же `_timeout`. Чтение ответа о
|
||||
|
||||
## 6. Хранение настроек (EEPROM)
|
||||
|
||||
Эмулируемая EEPROM, 256 байт.
|
||||
Эмулируемая EEPROM, 512 байт. Занято 258.
|
||||
|
||||
| Адрес | Размер | Содержимое |
|
||||
|---|---|---|
|
||||
| `0` | 2 | Magic `0xA55A` — признак инициализации |
|
||||
| `2` | 220 | Webhook URL, `\0`-терминированный |
|
||||
| Адрес | Размер | Содержимое | По умолчанию |
|
||||
|---|---|---|---|
|
||||
| `0` | 2 | Magic `0xA55B` — версия раскладки 2 | — |
|
||||
| `2` | 220 | `webhook_url`, `\0`-терминированный | пусто |
|
||||
| `222` | 31 | `device_name` | `Wemos_D1_Pump` |
|
||||
| `254` | 2 | `alarm_delay_sec` (`uint16`) | `5` |
|
||||
| `256` | 2 | `emerg_delay_sec` (`uint16`) | `0` |
|
||||
|
||||
При первом старте (magic не совпал) записывается значение константы
|
||||
`webhook_url_default` — она намеренно пустая, чтобы секретный токен не попадал
|
||||
в исходник и в git. Пока URL не задан через `/set_webhook`, `sendPostWebhook()`
|
||||
сразу выходит и пишет в лог `[HTTP] Webhook URL не задан`.
|
||||
Значения по умолчанию воспроизводят поведение прошивки до появления настроек.
|
||||
|
||||
Учётные данные Wi-Fi хранит WiFiManager в своей области флеша, не в этой EEPROM.
|
||||
### 6.1 Версионирование и миграция
|
||||
|
||||
Magic одновременно служит номером версии раскладки:
|
||||
|
||||
| Прочитанный magic | Действие |
|
||||
|---|---|
|
||||
| `0xA55B` | читаются все поля |
|
||||
| `0xA55A` | **миграция v1 → v2**: `webhook_url` сохраняется, новые поля получают значения по умолчанию, magic перезаписывается |
|
||||
| иное | первый старт, все поля по умолчанию |
|
||||
|
||||
Миграция нужна, чтобы уже прошитые приборы не потеряли настроенный webhook при
|
||||
обновлении прошивки.
|
||||
|
||||
При чтении v2 значения дополнительно проверяются: пустое `device_name` и
|
||||
выдержка больше `DELAY_MAX_SEC` заменяются значениями по умолчанию — иначе мусор
|
||||
во флеше дал бы заведомо неверную выдержку.
|
||||
|
||||
Пустой `webhook_url` — легальное состояние. Пока URL не задан,
|
||||
`sendPostWebhook()` сразу выходит и пишет в лог
|
||||
`[HTTP] Webhook URL не задан — отправка пропущена`.
|
||||
|
||||
Учётные данные Wi-Fi хранит WiFiManager в своей области флеша, не в этой EEPROM,
|
||||
поэтому сброс Wi-Fi настройки из этой таблицы не затрагивает.
|
||||
|
||||
---
|
||||
|
||||
@@ -209,16 +288,45 @@ BearSSL ограничен тем же `_timeout`. Чтение ответа о
|
||||
| Маршрут | Метод | Описание |
|
||||
|---|---|---|
|
||||
| `/` | `GET` | HTML-панель мониторинга |
|
||||
| `/status` | `GET` | JSON `{"p1":0,"p2":0,"pE":0}` |
|
||||
| `/status` | `GET` | JSON `{"p1":0,"p2":0,"pE":0,"rMain":0,"rEmg":0}` |
|
||||
| `/reset_wifi` | `POST` | Стереть настройки Wi-Fi и перезагрузиться |
|
||||
| `/set_webhook` | `POST` | Параметр `url` (≤220 симв.), сохранить в EEPROM |
|
||||
| `/set_config` | `POST` | Параметры `url`, `device`, `alarm_delay`, `emerg_delay` |
|
||||
|
||||
Панель опрашивает `/status` каждые 2 секунды и перезагружает страницу при
|
||||
изменении состояния. Шрифты подгружаются с Google Fonts — при отсутствии
|
||||
интернета у клиента интерфейс отрисуется системным моноширинным шрифтом.
|
||||
### 7.1 `/set_config`
|
||||
|
||||
Аутентификации нет: любой в той же сети может сбросить Wi-Fi и подменить
|
||||
webhook. Контроллер рассчитан на изолированный технологический сегмент.
|
||||
Одна форма на все настройки. **Пустое или неверное поле означает «не менять»** —
|
||||
так частично заполненная форма не обнуляет остальные параметры. Запись в EEPROM
|
||||
выполняется только если что-то реально изменилось.
|
||||
|
||||
| Параметр | Валидация | При отказе |
|
||||
|---|---|---|
|
||||
| `url` | 1…220 символов | поле не меняется |
|
||||
| `device` | фильтр до `[A-Za-z0-9_-]`, ≤31 симв.; пусто после фильтрации → отказ | поле не меняется |
|
||||
| `alarm_delay` | строка целиком из цифр, 0…3600 | поле не меняется |
|
||||
| `emerg_delay` | строка целиком из цифр, 0…3600 | поле не меняется |
|
||||
|
||||
Фильтрация `device` вместо экранирования выбрана осознанно: разрешённый набор
|
||||
символов безопасен и в теле JSON, и в HTML, поэтому экранирование не нужно
|
||||
нигде.
|
||||
|
||||
### 7.2 Состояние реле
|
||||
|
||||
`rMain` и `rEmg` в `/status` — это **логическое** состояние реле из переменных
|
||||
`relayMainOn` / `relayEmergOn`, а не `digitalRead()` с пина. Так сделано
|
||||
намеренно: выходы инверсные, и чтение уровня с пина показало бы на панели
|
||||
обратное действительности.
|
||||
|
||||
### 7.3 Автообновление
|
||||
|
||||
Панель опрашивает `/status` каждые 2 секунды и перезагружает страницу при любом
|
||||
изменении JSON. Поскольку состояние реле теперь входит в ответ, страница
|
||||
обновляется и при срабатывании реле. Шрифты подгружаются с Google Fonts — при
|
||||
отсутствии интернета у клиента интерфейс отрисуется системным моноширинным
|
||||
шрифтом.
|
||||
|
||||
Аутентификации нет: любой в той же сети может сбросить Wi-Fi, подменить webhook
|
||||
и изменить выдержки. Контроллер рассчитан на изолированный технологический
|
||||
сегмент.
|
||||
|
||||
---
|
||||
|
||||
@@ -246,13 +354,27 @@ STATUS: OK
|
||||
| работает резерв | `WARN: BACKUP RUNNING` |
|
||||
| иначе | `STATUS: OK` |
|
||||
|
||||
Состояние реле на экран не выводится — свободной строки в макете нет. Реле видны
|
||||
только в веб-панели и, для «Резерва», по встроенному светодиоду платы на `D4`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Кнопка сброса Wi-Fi
|
||||
## 9. Сброс настроек Wi-Fi
|
||||
|
||||
Пин `D3`, удержание `BTN_HOLD_MS = 3000 мс`. Во время удержания на дисплее
|
||||
рисуется прогресс-бар с обратным отсчётом. По достижении порога —
|
||||
`wm.resetSettings()` и `ESP.restart()`. Отпускание до порога отменяет операцию.
|
||||
Физической кнопки сброса нет — она удалена осознанно. При сохранённых учётных
|
||||
данных и пропавшей сети `wm.autoConnect()` не подключается и **сам** поднимает
|
||||
портал `Pump_Control_Set`, поэтому для этого сценария кнопка не нужна.
|
||||
|
||||
Остаётся два способа:
|
||||
|
||||
* кнопка **«Сбросить Wi-Fi»** на веб-панели (`POST /reset_wifi`) — работает, пока
|
||||
прибор в сети и панель доступна;
|
||||
* выключить роутер и перезагрузить прибор — `autoConnect()` не найдёт сеть и
|
||||
откроет портал.
|
||||
|
||||
Второй способ нужен для случая «сеть есть, подключение успешно, но прибор надо
|
||||
перевести в другую сеть»: простая перезагрузка портал не откроет, так как
|
||||
подключение проходит успешно.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user