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

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

16 KiB
Raw Permalink Blame History

growbox (ESP32-контроллер)

Прошивка для реального ESP32, обслуживающая зону "Гроубокс" платформы 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)

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.