mqttClient.connect() блокирует на время TCP-таймаута, если брокер недоступен. Без паузы между попытками это происходило на каждой итерации loop() — веб-интерфейс и опрос датчиков/реле (не зависят от MQTT и должны работать даже без платформы) подвисали. Теперь переподключение пробуется не чаще раза в 5 секунд. В README добавлен раздел "Автономная работа без платформы" — что именно работает локально без docker compose, а что требует брокера.
219 lines
16 KiB
Markdown
219 lines
16 KiB
Markdown
# 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.
|