diff --git a/growbox/README.md b/growbox/README.md index 3db8ad6..e994cb5 100644 --- a/growbox/README.md +++ b/growbox/README.md @@ -1,9 +1,9 @@ # growbox (ESP32-контроллер) Прошивка для реального ESP32, обслуживающая зону "Гроубокс" платформы -[home_automatization](../../home_automatization). Одна плата владеет четырьмя -устройствами: `sensor-1` (DHT22) и три реле-актуатора — `light-1`, `pump-1`, -`fan-1`. +[home_automatization](../../home_automatization). Одна плата владеет пятью +устройствами: `sensor-1` (DHT22, температура/влажность воздуха), `soil-1` +(влажность почвы) и три реле-актуатора — `light-1`, `pump-1`, `fan-1`. ## Протокол (MQTT, mosquitto из docker-compose платформы, порт 1883) @@ -22,39 +22,52 @@ device_id в конфиге должны совпадать с `devices.external_id`, засеянными в Laravel (`DemoGrowboxSeeder` использует `sensor-1`/`fan-1`), а `zone_id` — с -`zones.id` зоны "Гроубокс" в Postgres. +`zones.id` зоны "Гроубокс" в Postgres. `soil-1` — новое устройство, в +Laravel/Postgres пока не заведено (нужен `device_type` и строка в `devices` +— это в этом репозитории не делаем); до тех пор его телеметрия просто ни к +чему не привязана на стороне платформы, ingest-service это не блокирует +(zone_id едет прямо в payload, в Postgres за ним никто не ходит). ## Железо По фото реального комплекта: 30-контактная ESP32 DevKit, готовый модуль -DHT22 с распаянными проводами и **один 2-канальный** релейный модуль -(SRD-05VDC-SL-C×2, общий разъём `GND IN1 IN2 VCC`) — закрывает sensor-1, -light-1, pump-1. Для fan-1 нужен ещё один канал реле (третий модуль или -4-канальная плата) — в прошивке/config.h он уже заложен на GPIO18, физически -пока не подключён. +DHT22 с распаянными проводами, компараторный модуль почвенной влажности +(потенциометр + разъём `A0 D0 GND VCC`, используем только `A0`) и **два +одинаковых 2-канальных** релейных модуля (SRD-05VDC-SL-C×2 каждый, общий +разъём `GND IN1 IN2 VCC`) — вместе четыре канала на три реле, один канал +второй платы свободен про запас. -| Назначение | Пин (по умолчанию) | Примечание | -|---------------------|---------------------|------------| -| DHT22 DATA | GPIO4 | провод жёлтый на нашем модуле | -| Реле IN1 — light-1 | GPIO16 | **на шёлкографии платы подписан `RX2`**, не «16» | -| Реле IN2 — pump-1 | GPIO17 | **на шёлкографии платы подписан `TX2`**, не «17» | -| Реле IN — fan-1 | GPIO18 | нужен третий канал реле, пока не подключено | +| Назначение | Пин (по умолчанию) | Примечание | +|--------------------------|---------------------|------------| +| 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 — +Оба релейных модуля (2-канальные, SRD-05VDC-SL-C) активны по LOW — `RELAY_ACTIVE_LOW` в `config.h` уже стоит в `true`. Перемычка `JD-VCC` на -модуле по умолчанию установлена (питание катушек реле — от той же VCC, без -гальванической развязки) — для этого проекта этого достаточно, снимать её -незачем. +каждом модуле по умолчанию установлена (питание катушек реле — от той же +VCC, без гальванической развязки) — для этого проекта этого достаточно, +снимать её незачем. DHT22 требует подтягивающий резистор ~10кОм между DATA и VCC — на нашем модуле, похоже, уже встроен (компактная плата-переходник с 3 проводами), но если показания не читаются — проверьте отдельным резистором. +Модуль почвенной влажности — сырое ADC-значение на `A0` **уменьшается** с +ростом влажности (суше — выше сопротивление датчика — выше напряжение). +Заводские `SOIL_RAW_DRY`/`SOIL_RAW_WET` в `config.h.example` — грубая оценка, +не калибровка: замерьте raw-значение на воздухе и после погружения щупа в +воду (печатается в Serial-логе `telemetryTick()`) и подставьте свои числа, +иначе проценты будут врать. + ### Схема подключения ``` @@ -62,10 +75,12 @@ DHT22 требует подтягивающий резистор ~10кОм ме ┌───────────────────────┐ DHT22 DATA ───────┤ GPIO4 │ (жёлтый провод) │ │ + Почва A0 ─────────┤ GPIO34 │ │ │ - Реле IN1 (light-1)─┤ GPIO16 (на плате: RX2) - Реле IN2 (pump-1) ─┤ GPIO17 (на плате: TX2) - Реле IN (fan-1) ──┤ GPIO18 ← пока не подключено + Реле №1 IN1 (light-1) ┤ GPIO16 (на плате: RX2) + Реле №1 IN2 (pump-1) ┤ GPIO17 (на плате: TX2) + Реле №2 IN1 (fan-1) ┤ GPIO18 + Реле №2 IN2 (свободно) ┤ — │ │ 3V3 ────┤ 3V3 │ GND ────┤ GND │ @@ -75,9 +90,13 @@ DHT22 требует подтягивающий резистор ~10кОм ме DHT22 (готовый модуль, 3 провода): оранжевый(+/VCC) -> 3V3 жёлтый(out/DATA) -> GPIO4 зелёный(-/GND) -> GND -Реле, 2-канальный модуль (разъём GND IN1 IN2 VCC): +Почва (компараторный модуль, разъём A0 D0 GND VCC — используем A0): + VCC -> 3V3 GND -> GND A0 -> GPIO34 + +Реле, два одинаковых 2-канальных модуля (разъём GND IN1 IN2 VCC): VCC -> VIN (5V) GND -> GND (общий с ESP32) - IN1 -> GPIO16/RX2 (light-1) IN2 -> GPIO17/TX2 (pump-1) + Модуль №1: IN1 -> GPIO16/RX2 (light-1) IN2 -> GPIO17/TX2 (pump-1) + Модуль №2: IN1 -> GPIO18 (fan-1) IN2 -> свободен ⚠️ Реле коммутируют цепь самой нагрузки (свет/насос/вентилятор, обычно 12V/220V) через винтовые клеммы COM/NO/NC каждого канала — это @@ -102,9 +121,9 @@ DHT22 (готовый модуль, 3 провода): Каждая плата поднимает у себя простую страницу и JSON API на порту 80 — удобно наблюдать/управлять локально, не дожидаясь Laravel/MQTT: -- `GET /` — HTML-страница: показания DHT22 + кнопки Вкл/Выкл для +- `GET /` — HTML-страница: показания DHT22 и почвы + кнопки Вкл/Выкл для light-1/pump-1/fan-1. -- `GET /api/state` — JSON `{"sensor":{"has_reading","temperature","humidity"},"devices":{"light-1":{"power"},...}}`. +- `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-команде. @@ -172,7 +191,9 @@ Arduino IDE (GUI-приложение) это обычно не требуетс ## Проверка end-to-end При работающем `docker compose` в `home_automatization` и засеянных -Postgres-данных (зона + `sensor-1`/`light-1`/`pump-1`/`fan-1`): +Postgres-данных (зона + `sensor-1`/`light-1`/`pump-1`/`fan-1`; `soil-1` пока +не засеян — его телеметрию ClickHouse примет, но в Laravel UI устройство не +появится, пока не завести `devices`-запись): - телеметрия должна доходить до ClickHouse через ingest-service; - команда через Laravel UI (или напрямую через `grpcurl` в diff --git a/growbox/config.h.example b/growbox/config.h.example index b59967d..0837a09 100644 --- a/growbox/config.h.example +++ b/growbox/config.h.example @@ -14,8 +14,14 @@ // --- Зона и устройства --- // zone_id — id зоны "Гроубокс" в Postgres (laravel-app, таблица zones). // device_id должны совпадать с devices.external_id, засеянными в Laravel. +// soil-1 — новое устройство, в Laravel/Postgres платформы ещё не заведено +// (device_type "sensor_soil_moisture" + строка в devices нужно добавить +// отдельно, это в this репозитории не трогаем) — до тех пор его телеметрия +// просто не будет ни к чему привязана на стороне платформы, ingest-service +// это переживёт (zone_id едет прямо в payload). #define ZONE_ID "1" #define DEVICE_ID_SENSOR "sensor-1" +#define DEVICE_ID_SOIL "soil-1" #define DEVICE_ID_LIGHT "light-1" #define DEVICE_ID_PUMP "pump-1" #define DEVICE_ID_FAN "fan-1" @@ -23,14 +29,22 @@ // --- Пины (ESP32 DevKitC) --- // На 30-контактных платах GPIO16/17 часто подписаны на шёлкографии как // RX2/TX2 (пины UART2), а не номерами — это те же GPIO, см. README.md. -#define PIN_DHT22 4 -#define PIN_RELAY_LIGHT 16 -#define PIN_RELAY_PUMP 17 -#define PIN_RELAY_FAN 18 +#define PIN_DHT22 4 +#define PIN_SOIL_MOISTURE 34 // ADC1_CH6, input-only — годится для чистого аналогового чтения +#define PIN_RELAY_LIGHT 16 +#define PIN_RELAY_PUMP 17 +#define PIN_RELAY_FAN 18 // Большинство дешёвых реле-модулей активны по LOW (сигнал LOW = реле включено). #define RELAY_ACTIVE_LOW true +// Калибровка компараторного модуля почвенной влажности (analogRead 0-4095): +// значение на воздухе (сухо) и в воде (максимально мокро). Заводские числа +// ниже — грубая оценка, откалибровать по месту и подставить свои — иначе +// проценты будут врать. Смотреть Serial-лог telemetryTick() (печатает raw). +#define SOIL_RAW_DRY 3200 +#define SOIL_RAW_WET 1300 + // --- Телеметрия --- #define TELEMETRY_INTERVAL_MS 30000UL diff --git a/growbox/telemetry.cpp b/growbox/telemetry.cpp index ba41a5c..a8a3435 100644 --- a/growbox/telemetry.cpp +++ b/growbox/telemetry.cpp @@ -13,10 +13,25 @@ static bool hasReading = false; static float lastTemperature = 0; static float lastHumidity = 0; +static bool hasSoilReading = false; +static float lastSoilPercent = 0; + void telemetryBegin() { dht.begin(); } +// Компараторный модуль почвенной влажности отдаёт на A0 сырое ADC-значение, +// которое УМЕНЬШАЕТСЯ с ростом влажности (суше — выше сопротивление датчика +// — выше напряжение на A0). SOIL_RAW_DRY/SOIL_RAW_WET в config.h — заводские +// значения-плейсхолдеры, откалибровать по месту (замерить raw на воздухе и +// в воде) через Serial-лог этой функции. +static float rawToSoilPercent(int raw) { + long percent = map(raw, SOIL_RAW_DRY, SOIL_RAW_WET, 0, 100); + if (percent < 0) percent = 0; + if (percent > 100) percent = 100; + return (float)percent; +} + void telemetryTick() { unsigned long now = millis(); if (now - lastPublish < TELEMETRY_INTERVAL_MS && lastPublish != 0) { @@ -32,21 +47,31 @@ void telemetryTick() { float humidity = dht.readHumidity(); float temperature = dht.readTemperature(); if (isnan(humidity) || isnan(temperature)) { - Serial.println("[telemetry] DHT22 read failed, skipping this cycle"); - return; + Serial.println("[telemetry] DHT22 read failed, skipping this sensor this cycle"); + } else { + lastTemperature = temperature; + lastHumidity = humidity; + hasReading = true; + + mqttPublishTelemetry(DEVICE_ID_SENSOR, "temperature", temperature); + mqttPublishTelemetry(DEVICE_ID_SENSOR, "humidity", humidity); + + Serial.print("[telemetry] temperature="); + Serial.print(temperature); + Serial.print(" humidity="); + Serial.println(humidity); } - lastTemperature = temperature; - lastHumidity = humidity; - hasReading = true; + int soilRaw = analogRead(PIN_SOIL_MOISTURE); + lastSoilPercent = rawToSoilPercent(soilRaw); + hasSoilReading = true; + mqttPublishTelemetry(DEVICE_ID_SOIL, "soil_moisture", lastSoilPercent); - mqttPublishTelemetry(DEVICE_ID_SENSOR, "temperature", temperature); - mqttPublishTelemetry(DEVICE_ID_SENSOR, "humidity", humidity); - - Serial.print("[telemetry] temperature="); - Serial.print(temperature); - Serial.print(" humidity="); - Serial.println(humidity); + Serial.print("[telemetry] soil_moisture="); + Serial.print(lastSoilPercent); + Serial.print("% (raw="); + Serial.print(soilRaw); + Serial.println(")"); } bool telemetryGetLast(float& temperature, float& humidity) { @@ -57,3 +82,11 @@ bool telemetryGetLast(float& temperature, float& humidity) { humidity = lastHumidity; return true; } + +bool telemetryGetLastSoil(float& percent) { + if (!hasSoilReading) { + return false; + } + percent = lastSoilPercent; + return true; +} diff --git a/growbox/telemetry.h b/growbox/telemetry.h index eac163c..c7e4ec1 100644 --- a/growbox/telemetry.h +++ b/growbox/telemetry.h @@ -11,3 +11,6 @@ void telemetryTick(); // показывать данные не дожидаясь следующего цикла публикации). // Возвращает false, если ни одного успешного чтения ещё не было. bool telemetryGetLast(float& temperature, float& humidity); + +// Последнее показание почвенной влажности в процентах (0-100). +bool telemetryGetLastSoil(float& percent); diff --git a/growbox/web_ui.cpp b/growbox/web_ui.cpp index 3e8418b..aad119e 100644 --- a/growbox/web_ui.cpp +++ b/growbox/web_ui.cpp @@ -31,9 +31,12 @@ static const char PAGE_HTML[] PROGMEM = R"HTML(