Files
home_automatization_control…/growbox/README.md
T
cacto 988c574183 Не блокировать loop() бесконечными попытками подключения к MQTT
mqttClient.connect() блокирует на время TCP-таймаута, если брокер
недоступен. Без паузы между попытками это происходило на каждой итерации
loop() — веб-интерфейс и опрос датчиков/реле (не зависят от MQTT и должны
работать даже без платформы) подвисали. Теперь переподключение пробуется не
чаще раза в 5 секунд.

В README добавлен раздел "Автономная работа без платформы" — что именно
работает локально без docker compose, а что требует брокера.
2026-08-18 13:09:15 +05:00

219 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# growbox (ESP32-контроллер)
Прошивка для реального ESP32, обслуживающая зону "Гроубокс" платформы
[home_automatization](../../home_automatization). Одна плата владеет пятью
устройствами: `sensor-1` (DHT22, температура/влажность воздуха), `soil-1`
(влажность почвы) и три реле-актуатора — `light-1`, `pump-1`, `fan-1`.
## Протокол (MQTT, mosquitto из docker-compose платформы, порт 1883)
- `devices/{device_id}/telemetry` — публикует контроллер, JSON
`{"device_id","zone_id","sensor_type","value","timestamp"}`, timestamp —
RFC3339 UTC. `sensor-1` шлёт по одному сообщению на `temperature` и
`humidity` раз в `TELEMETRY_INTERVAL_MS`.
- `devices/{device_id}/commands` — подписывается контроллер (wildcard
`devices/+/commands`, фильтрует по своим 3 актуаторам), JSON
`{"action":"turn_on"|"turn_off"|"set_level","level"?}`.
- `devices/{device_id}/ack` — публикует контроллер после применения команды,
JSON `{"state":{"power":"on"|"off","level"?}}`.
Реального диммирования нет (обычные реле), поэтому `set_level` трактуется как
порог: `level > 0` включает реле, `level <= 0` выключает.
device_id в конфиге должны совпадать с `devices.external_id`, засеянными в
Laravel (`DemoGrowboxSeeder` использует `sensor-1`/`fan-1`), а `zone_id`с
`zones.id` зоны "Гроубокс" в Postgres. `soil-1` — новое устройство, в
Laravel/Postgres пока не заведено (нужен `device_type` и строка в `devices`
— это в этом репозитории не делаем); до тех пор его телеметрия просто ни к
чему не привязана на стороне платформы, ingest-service это не блокирует
(zone_id едет прямо в payload, в Postgres за ним никто не ходит).
## Железо
По фото реального комплекта: 30-контактная ESP32 DevKit, готовый модуль
DHT22 с распаянными проводами, компараторный модуль почвенной влажности
(потенциометр + разъём `A0 D0 GND VCC`, используем только `A0`) и **два
одинаковых 2-канальных** релейных модуля (SRD-05VDC-SL-C×2 каждый, общий
разъём `GND IN1 IN2 VCC`) — вместе четыре канала на три реле, один канал
второй платы свободен про запас.
| Назначение | Пин (по умолчанию) | Примечание |
|--------------------------|---------------------|------------|
| DHT22 DATA | GPIO4 | провод жёлтый на нашем модуле |
| Почва, A0 | GPIO34 | ADC1_CH6, input-only; нужна калибровка `SOIL_RAW_DRY`/`SOIL_RAW_WET` |
| Реле №1, IN1 — light-1 | GPIO16 | **на шёлкографии платы подписан `RX2`**, не «16» |
| Реле №1, IN2 — pump-1 | GPIO17 | **на шёлкографии платы подписан `TX2`**, не «17» |
| Реле №2, IN1 — fan-1 | GPIO18 | второй такой же модуль |
| Реле №2, IN2 | — | свободный канал, не задействован |
На этой конкретной 30-пиновой плате GPIO16/17 не подписаны номерами — они
выведены как пины UART2 и промаркированы `RX2`/`TX2`. Физически это те же
GPIO, `config.h`/прошивку менять не нужно, важно только не перепутать их при
пайке/монтаже на плате.
Оба релейных модуля (2-канальные, SRD-05VDC-SL-C) активны по LOW —
`RELAY_ACTIVE_LOW` в `config.h` уже стоит в `true`. Перемычка `JD-VCC` на
каждом модуле по умолчанию установлена (питание катушек реле — от той же
VCC, без гальванической развязки) — для этого проекта этого достаточно,
снимать её незачем.
DHT22 требует подтягивающий резистор ~10кОм между DATA и VCC — на нашем
модуле, похоже, уже встроен (компактная плата-переходник с 3 проводами), но
если показания не читаются — проверьте отдельным резистором.
Модуль почвенной влажности — сырое ADC-значение на `A0` **уменьшается** с
ростом влажности (суше — выше сопротивление датчика — выше напряжение).
Заводские `SOIL_RAW_DRY`/`SOIL_RAW_WET` в `config.h.example` — грубая оценка,
не калибровка: замерьте raw-значение на воздухе и после погружения щупа в
воду (печатается в Serial-логе `telemetryTick()`) и подставьте свои числа,
иначе проценты будут врать.
### Схема подключения
```
ESP32 (30 pin)
┌───────────────────────┐
DHT22 DATA ───────┤ GPIO4 │
(жёлтый провод) │ │
Почва A0 ─────────┤ GPIO34 │
│ │
Реле №1 IN1 (light-1) ┤ GPIO16 (на плате: RX2)
Реле №1 IN2 (pump-1) ┤ GPIO17 (на плате: TX2)
Реле №2 IN1 (fan-1) ┤ GPIO18
Реле №2 IN2 (свободно) ┤ —
│ │
3V3 ────┤ 3V3 │
GND ────┤ GND │
USB/5V ─┤ VIN │
└───────────────────────┘
DHT22 (готовый модуль, 3 провода):
оранжевый(+/VCC) -> 3V3 жёлтый(out/DATA) -> GPIO4 зелёный(-/GND) -> GND
Почва (компараторный модуль, разъём A0 D0 GND VCC — используем A0):
VCC -> 3V3 GND -> GND A0 -> GPIO34
Реле, два одинаковых 2-канальных модуля (разъём GND IN1 IN2 VCC):
VCC -> VIN (5V) GND -> GND (общий с ESP32)
Модуль №1: IN1 -> GPIO16/RX2 (light-1) IN2 -> GPIO17/TX2 (pump-1)
Модуль №2: IN1 -> GPIO18 (fan-1) IN2 -> свободен
⚠️ Реле коммутируют цепь самой нагрузки (свет/насос/вентилятор, обычно
12V/220V) через винтовые клеммы COM/NO/NC каждого канала — это
ОТДЕЛЬНАЯ силовая цепь со своим источником питания, к GPIO ESP32 она не
подключается. К плате идёт только сигнальный IN каждого канала.
```
## Библиотеки
Через Arduino Library Manager (Sketch → Include Library → Manage Libraries):
- `PubSubClient` (Nick O'Leary) — MQTT-клиент
- `ArduinoJson` (Benoit Blanchon, v7) — сериализация/парсинг JSON
- `DHT sensor library` (Adafruit) + зависимость `Adafruit Unified Sensor`
- Board support: `esp32` (Espressif Systems) через Boards Manager
`WebServer` (веб-интерфейс) и `WiFi` идут в комплекте с board support
`esp32` — отдельно ставить не нужно.
## Веб-интерфейс
Каждая плата поднимает у себя простую страницу и JSON API на порту 80 —
удобно наблюдать/управлять локально, не дожидаясь Laravel/MQTT:
- `GET /` — HTML-страница: показания DHT22 и почвы + кнопки Вкл/Выкл для
light-1/pump-1/fan-1.
- `GET /api/state` — JSON `{"sensor":{"has_reading","temperature","humidity"},"soil":{"has_reading","percent"},"devices":{"light-1":{"power"},...}}`.
- `POST /api/command` — JSON-тело `{"device_id","action","level"?}`, тот же
формат, что и в MQTT-команде.
Команда с веб-UI применяется через тот же код и публикует тот же ack в
`devices/{id}/ack`, что и команда из device-control-service — device shadow
платформы (Redis) не расходится с реальным состоянием железа независимо от
того, кто его переключил.
Включается/выключается флагом `ENABLE_WEB_UI` в `config.h` (`1`/`0`) — при
`0` HTTP-сервер не поднимается вообще, порт 80 не слушается. У сервера нет
авторизации — только для локальной сети, как и mosquitto платформы; для
выхода за пределы LAN сначала нужно добавить хотя бы Basic Auth.
## Автономная работа без платформы
Плате не нужен запущенный `docker compose` (`home_automatization`), чтобы
показывать датчики и включать/выключать свет/насос/вентилятор — для этого
достаточно Wi-Fi и веб-интерфейса выше (`http://<ip-платы>/`). MQTT-брокер
нужен только для синхронизации с платформой (телеметрия в ClickHouse,
команды из Laravel/rule-engine, device shadow в Redis) — если он недоступен,
эта синхронизация просто не происходит, но локальное управление продолжает
работать.
Важная деталь реализации: `mqttEnsureConnected()` пробует переподключиться к
брокеру не чаще раза в 5 секунд (`RECONNECT_INTERVAL_MS` в `mqtt_link.cpp`)
— без этой паузы блокирующий `mqttClient.connect()` при недоступном брокере
съедал бы почти весь `loop()` на каждой итерации, и веб-интерфейс с опросом
датчиков подвисали бы. Serial-лог в этом случае пишет `failed, rc=...` раз в
5 секунд — это ожидаемо, если платформа сейчас не поднята, а не поломка.
## Настройка и прошивка через Arduino IDE
1. **Поддержка ESP32.** Arduino IDE → Settings → «Additional boards manager
URLs» → добавить:
`https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json`
2. **Ядро.** Tools → Board → Boards Manager → найти `esp32` → установить
пакет `esp32 by Espressif Systems`.
3. **Библиотеки.** Tools → Manage Libraries → поставить по одной:
`PubSubClient`, `ArduinoJson` (v7), `DHT sensor library` (IDE предложит
доустановить зависимость `Adafruit Unified Sensor` — согласиться).
4. **Скетч.** File → Open → `controllers/growbox/growbox.ino` — остальные
`.h`/`.cpp` подхватятся вкладками автоматически.
5. **Конфиг.** `cp config.h.example config.h`, заполнить Wi-Fi SSID/пароль,
IP хоста с поднятым `docker compose` платформы (mosquitto слушает
`0.0.0.0:1883` — доступен по IP машины в локальной сети), `zone_id` и
`device_id`. Если IDE уже была открыта — переоткрыть скетч.
6. **Плата и порт.** Подключить ESP32 по USB (кабель именно с передачей
данных). Tools → Board → esp32 → `ESP32 Dev Module` (или точная модель).
Tools → Port → появившийся `/dev/cu.usbserial-*` /
`/dev/cu.SLAB_USBtoUART`.
7. **Заливка.** Кнопка Upload. Часть плат просит зажать кнопку `BOOT` на
плате до появления точек соединения в логе.
8. **Проверка.** Serial Monitor, 115200 бод — лог подключения к
Wi-Fi/NTP/MQTT/веб-сервера, публикации телеметрии/команд и IP платы для
похода в веб-интерфейс.
**Если не заливается:**
- порта нет в списке — нужен драйвер USB-UART моста платы (CP2102 обычно
ставится сам на современной macOS, CH340 — вручную);
- `Failed to connect to ESP32` / `No serial data received` — зажать `BOOT`
на плате на время заливки;
- заливка обрывается или очень долгая — Tools → Upload Speed → понизить с
921600 до 115200.
### Сборка через `arduino-cli` (если не через IDE)
```bash
arduino-cli core install esp32:esp32
arduino-cli lib install "PubSubClient" "ArduinoJson" "DHT sensor library"
arduino-cli compile --fqbn esp32:esp32:esp32 growbox
arduino-cli upload --fqbn esp32:esp32:esp32 -p /dev/cu.usbserial-XXXX growbox
```
На macOS `arduino-cli` по умолчанию ставит библиотеки в
`~/Documents/Arduino/libraries` — если этот каталог защищён sandbox'ом
(Operation not permitted при установке библиотек, встречалось в среде этого
агента), перенаправьте sketchbook в доступное место:
`arduino-cli config set directories.user ~/путь/без/ограничений`. Через саму
Arduino IDE (GUI-приложение) это обычно не требуется — она сама запрашивает
нужные разрешения у macOS.
## Проверка end-to-end
При работающем `docker compose` в `home_automatization` и засеянных
Postgres-данных (зона + `sensor-1`/`light-1`/`pump-1`/`fan-1`; `soil-1` пока
не засеян — его телеметрию ClickHouse примет, но в Laravel UI устройство не
появится, пока не завести `devices`-запись):
- телеметрия должна доходить до ClickHouse через ingest-service;
- команда через Laravel UI (или напрямую через `grpcurl` в
device-control-service) должна прийти на плату и вызвать щелчок реле, а
затем ack — обновить `reported_state`/`last_seen` в Redis device shadow.