Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
988c574183 | ||
|
|
138caef3ee | ||
|
|
6a7d512b22 | ||
|
|
0456d4e447 | ||
|
|
4fcca2701b | ||
|
|
51c46bf8d6 | ||
|
|
f4150ac1de |
+117
-40
@@ -1,9 +1,9 @@
|
|||||||
# growbox (ESP32-контроллер)
|
# growbox (ESP32-контроллер)
|
||||||
|
|
||||||
Прошивка для реального ESP32, обслуживающая зону "Гроубокс" платформы
|
Прошивка для реального ESP32, обслуживающая зону "Гроубокс" платформы
|
||||||
[home_automatization](../../home_automatization). Одна плата владеет четырьмя
|
[home_automatization](../../home_automatization). Одна плата владеет пятью
|
||||||
устройствами: `sensor-1` (DHT22) и три реле-актуатора — `light-1`, `pump-1`,
|
устройствами: `sensor-1` (DHT22, температура/влажность воздуха), `soil-1`
|
||||||
`fan-1`.
|
(влажность почвы) и три реле-актуатора — `light-1`, `pump-1`, `fan-1`.
|
||||||
|
|
||||||
## Протокол (MQTT, mosquitto из docker-compose платформы, порт 1883)
|
## Протокол (MQTT, mosquitto из docker-compose платформы, порт 1883)
|
||||||
|
|
||||||
@@ -22,51 +22,86 @@
|
|||||||
|
|
||||||
device_id в конфиге должны совпадать с `devices.external_id`, засеянными в
|
device_id в конфиге должны совпадать с `devices.external_id`, засеянными в
|
||||||
Laravel (`DemoGrowboxSeeder` использует `sensor-1`/`fan-1`), а `zone_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 с распаянными проводами, компараторный модуль почвенной влажности
|
||||||
| DHT22 data | GPIO4 |
|
(потенциометр + разъём `A0 D0 GND VCC`, используем только `A0`) и **два
|
||||||
| Реле — light-1 | GPIO16 |
|
одинаковых 2-канальных** релейных модуля (SRD-05VDC-SL-C×2 каждый, общий
|
||||||
| Реле — pump-1 | GPIO17 |
|
разъём `GND IN1 IN2 VCC`) — вместе четыре канала на три реле, один канал
|
||||||
| Реле — fan-1 | GPIO18 |
|
второй платы свободен про запас.
|
||||||
|
|
||||||
Реле-модули по умолчанию считаются активными по LOW (`RELAY_ACTIVE_LOW` в
|
| Назначение | Пин (по умолчанию) | Примечание |
|
||||||
`config.h`) — типично для дешёвых китайских модулей на оптопаре. Если ваши
|
|--------------------------|---------------------|------------|
|
||||||
реле активны по HIGH — поменяйте флаг.
|
| 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 | — | свободный канал, не задействован |
|
||||||
|
|
||||||
DHT22 требует подтягивающий резистор ~10кОм между data и VCC, если он не
|
На этой конкретной 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 DevKitC
|
ESP32 (30 pin)
|
||||||
┌───────────────────────┐
|
┌───────────────────────┐
|
||||||
DHT22 DATA ───────┤ GPIO4 │
|
DHT22 DATA ───────┤ GPIO4 │
|
||||||
(+10к подтяжка │ │
|
(жёлтый провод) │ │
|
||||||
DATA -> 3V3) │ │
|
Почва A0 ─────────┤ GPIO34 │
|
||||||
│ │
|
│ │
|
||||||
Реле LIGHT IN ────┤ GPIO16 │
|
Реле №1 IN1 (light-1) ┤ GPIO16 (на плате: RX2)
|
||||||
Реле PUMP IN ────┤ GPIO17 │
|
Реле №1 IN2 (pump-1) ┤ GPIO17 (на плате: TX2)
|
||||||
Реле FAN IN ────┤ GPIO18 │
|
Реле №2 IN1 (fan-1) ┤ GPIO18
|
||||||
|
Реле №2 IN2 (свободно) ┤ —
|
||||||
│ │
|
│ │
|
||||||
3V3 ────┤ 3V3 │
|
3V3 ────┤ 3V3 │
|
||||||
GND ────┤ GND │
|
GND ────┤ GND │
|
||||||
USB/5V ─┤ VIN │
|
USB/5V ─┤ VIN │
|
||||||
└───────────────────────┘
|
└───────────────────────┘
|
||||||
|
|
||||||
DHT22: VCC -> 3V3 GND -> GND DATA -> GPIO4
|
DHT22 (готовый модуль, 3 провода):
|
||||||
Реле-модуль: VCC -> 5V (VIN) или 3V3 (смотри маркировку модуля)
|
оранжевый(+/VCC) -> 3V3 жёлтый(out/DATA) -> GPIO4 зелёный(-/GND) -> GND
|
||||||
GND -> GND (общий с ESP32)
|
|
||||||
IN -> GPIO16 / GPIO17 / GPIO18
|
Почва (компараторный модуль, разъём 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 — это ОТДЕЛЬНАЯ силовая цепь со своим
|
12V/220V) через винтовые клеммы COM/NO/NC каждого канала — это
|
||||||
источником питания, к GPIO ESP32 она не подключается. К плате идёт
|
ОТДЕЛЬНАЯ силовая цепь со своим источником питания, к GPIO ESP32 она не
|
||||||
только сигнальный IN каждого реле-модуля.
|
подключается. К плате идёт только сигнальный IN каждого канала.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Библиотеки
|
## Библиотеки
|
||||||
@@ -86,9 +121,9 @@ DHT22: VCC -> 3V3 GND -> GND DATA -> GPIO4
|
|||||||
Каждая плата поднимает у себя простую страницу и JSON API на порту 80 —
|
Каждая плата поднимает у себя простую страницу и JSON API на порту 80 —
|
||||||
удобно наблюдать/управлять локально, не дожидаясь Laravel/MQTT:
|
удобно наблюдать/управлять локально, не дожидаясь Laravel/MQTT:
|
||||||
|
|
||||||
- `GET /` — HTML-страница: показания DHT22 + кнопки Вкл/Выкл для
|
- `GET /` — HTML-страница: показания DHT22 и почвы + кнопки Вкл/Выкл для
|
||||||
light-1/pump-1/fan-1.
|
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"?}`, тот же
|
- `POST /api/command` — JSON-тело `{"device_id","action","level"?}`, тот же
|
||||||
формат, что и в MQTT-команде.
|
формат, что и в MQTT-команде.
|
||||||
|
|
||||||
@@ -102,16 +137,56 @@ DHT22: VCC -> 3V3 GND -> GND DATA -> GPIO4
|
|||||||
авторизации — только для локальной сети, как и mosquitto платформы; для
|
авторизации — только для локальной сети, как и mosquitto платформы; для
|
||||||
выхода за пределы LAN сначала нужно добавить хотя бы Basic Auth.
|
выхода за пределы LAN сначала нужно добавить хотя бы Basic Auth.
|
||||||
|
|
||||||
## Настройка и прошивка
|
## Автономная работа без платформы
|
||||||
|
|
||||||
1. `cp config.h.example config.h`, заполнить Wi-Fi SSID/пароль, IP хоста, на
|
Плате не нужен запущенный `docker compose` (`home_automatization`), чтобы
|
||||||
котором поднят `docker compose` платформы (mosquitto слушает `0.0.0.0:1883`
|
показывать датчики и включать/выключать свет/насос/вентилятор — для этого
|
||||||
— доступен по IP машины в локальной сети), `zone_id` и `device_id`.
|
достаточно Wi-Fi и веб-интерфейса выше (`http://<ip-платы>/`). MQTT-брокер
|
||||||
2. Открыть `growbox.ino` в Arduino IDE, выбрать плату (ESP32 Dev Module или
|
нужен только для синхронизации с платформой (телеметрия в ClickHouse,
|
||||||
конкретную модель), порт.
|
команды из Laravel/rule-engine, device shadow в Redis) — если он недоступен,
|
||||||
3. Прошить. Открыть Serial Monitor на 115200 бод — там лог подключения к
|
эта синхронизация просто не происходит, но локальное управление продолжает
|
||||||
Wi-Fi/NTP/MQTT/веб-сервера и каждой публикации телеметрии/команды, а
|
работать.
|
||||||
также IP платы для похода в веб-интерфейс.
|
|
||||||
|
Важная деталь реализации: `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` (если не через IDE)
|
||||||
|
|
||||||
@@ -133,7 +208,9 @@ Arduino IDE (GUI-приложение) это обычно не требуетс
|
|||||||
## Проверка end-to-end
|
## Проверка end-to-end
|
||||||
|
|
||||||
При работающем `docker compose` в `home_automatization` и засеянных
|
При работающем `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;
|
- телеметрия должна доходить до ClickHouse через ingest-service;
|
||||||
- команда через Laravel UI (или напрямую через `grpcurl` в
|
- команда через Laravel UI (или напрямую через `grpcurl` в
|
||||||
|
|||||||
@@ -14,14 +14,23 @@
|
|||||||
// --- Зона и устройства ---
|
// --- Зона и устройства ---
|
||||||
// zone_id — id зоны "Гроубокс" в Postgres (laravel-app, таблица zones).
|
// zone_id — id зоны "Гроубокс" в Postgres (laravel-app, таблица zones).
|
||||||
// device_id должны совпадать с devices.external_id, засеянными в Laravel.
|
// 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 ZONE_ID "1"
|
||||||
#define DEVICE_ID_SENSOR "sensor-1"
|
#define DEVICE_ID_SENSOR "sensor-1"
|
||||||
|
#define DEVICE_ID_SOIL "soil-1"
|
||||||
#define DEVICE_ID_LIGHT "light-1"
|
#define DEVICE_ID_LIGHT "light-1"
|
||||||
#define DEVICE_ID_PUMP "pump-1"
|
#define DEVICE_ID_PUMP "pump-1"
|
||||||
#define DEVICE_ID_FAN "fan-1"
|
#define DEVICE_ID_FAN "fan-1"
|
||||||
|
|
||||||
// --- Пины (ESP32 DevKitC) ---
|
// --- Пины (ESP32 DevKitC) ---
|
||||||
|
// На 30-контактных платах GPIO16/17 часто подписаны на шёлкографии как
|
||||||
|
// RX2/TX2 (пины UART2), а не номерами — это те же GPIO, см. README.md.
|
||||||
#define PIN_DHT22 4
|
#define PIN_DHT22 4
|
||||||
|
#define PIN_SOIL_MOISTURE 34 // ADC1_CH6, input-only — годится для чистого аналогового чтения
|
||||||
#define PIN_RELAY_LIGHT 16
|
#define PIN_RELAY_LIGHT 16
|
||||||
#define PIN_RELAY_PUMP 17
|
#define PIN_RELAY_PUMP 17
|
||||||
#define PIN_RELAY_FAN 18
|
#define PIN_RELAY_FAN 18
|
||||||
@@ -29,6 +38,13 @@
|
|||||||
// Большинство дешёвых реле-модулей активны по LOW (сигнал LOW = реле включено).
|
// Большинство дешёвых реле-модулей активны по LOW (сигнал LOW = реле включено).
|
||||||
#define RELAY_ACTIVE_LOW true
|
#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
|
#define TELEMETRY_INTERVAL_MS 30000UL
|
||||||
|
|
||||||
|
|||||||
+16
-1
@@ -63,11 +63,25 @@ void mqttBegin() {
|
|||||||
mqttClient.setCallback(onMessage);
|
mqttClient.setCallback(onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mqttClient.connect() блокирует на время TCP-таймаута, если брокер
|
||||||
|
// недоступен. Без паузы между попытками это съедало бы почти весь loop() —
|
||||||
|
// а значит и веб-интерфейс, и опрос датчиков/реле, которые от MQTT не
|
||||||
|
// зависят и обязаны работать даже без связи с платформой (см. README,
|
||||||
|
// "Автономная работа без платформы").
|
||||||
|
static unsigned long lastConnectAttempt = 0;
|
||||||
|
static const unsigned long RECONNECT_INTERVAL_MS = 5000;
|
||||||
|
|
||||||
void mqttEnsureConnected() {
|
void mqttEnsureConnected() {
|
||||||
if (mqttClient.connected()) {
|
if (mqttClient.connected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsigned long now = millis();
|
||||||
|
if (now - lastConnectAttempt < RECONNECT_INTERVAL_MS && lastConnectAttempt != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastConnectAttempt = now;
|
||||||
|
|
||||||
Serial.print("[mqtt] connecting to ");
|
Serial.print("[mqtt] connecting to ");
|
||||||
Serial.print(MQTT_BROKER_HOST);
|
Serial.print(MQTT_BROKER_HOST);
|
||||||
if (mqttClient.connect(MQTT_CLIENT_ID)) {
|
if (mqttClient.connect(MQTT_CLIENT_ID)) {
|
||||||
@@ -75,7 +89,8 @@ void mqttEnsureConnected() {
|
|||||||
mqttClient.subscribe("devices/+/commands", 1);
|
mqttClient.subscribe("devices/+/commands", 1);
|
||||||
} else {
|
} else {
|
||||||
Serial.print(" failed, rc=");
|
Serial.print(" failed, rc=");
|
||||||
Serial.println(mqttClient.state());
|
Serial.print(mqttClient.state());
|
||||||
|
Serial.println(" (это ок, если платформа сейчас не поднята — веб-интерфейс и реле работают локально)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
-4
@@ -13,10 +13,25 @@ static bool hasReading = false;
|
|||||||
static float lastTemperature = 0;
|
static float lastTemperature = 0;
|
||||||
static float lastHumidity = 0;
|
static float lastHumidity = 0;
|
||||||
|
|
||||||
|
static bool hasSoilReading = false;
|
||||||
|
static float lastSoilPercent = 0;
|
||||||
|
|
||||||
void telemetryBegin() {
|
void telemetryBegin() {
|
||||||
dht.begin();
|
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() {
|
void telemetryTick() {
|
||||||
unsigned long now = millis();
|
unsigned long now = millis();
|
||||||
if (now - lastPublish < TELEMETRY_INTERVAL_MS && lastPublish != 0) {
|
if (now - lastPublish < TELEMETRY_INTERVAL_MS && lastPublish != 0) {
|
||||||
@@ -32,10 +47,8 @@ void telemetryTick() {
|
|||||||
float humidity = dht.readHumidity();
|
float humidity = dht.readHumidity();
|
||||||
float temperature = dht.readTemperature();
|
float temperature = dht.readTemperature();
|
||||||
if (isnan(humidity) || isnan(temperature)) {
|
if (isnan(humidity) || isnan(temperature)) {
|
||||||
Serial.println("[telemetry] DHT22 read failed, skipping this cycle");
|
Serial.println("[telemetry] DHT22 read failed, skipping this sensor this cycle");
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
lastTemperature = temperature;
|
lastTemperature = temperature;
|
||||||
lastHumidity = humidity;
|
lastHumidity = humidity;
|
||||||
hasReading = true;
|
hasReading = true;
|
||||||
@@ -47,6 +60,18 @@ void telemetryTick() {
|
|||||||
Serial.print(temperature);
|
Serial.print(temperature);
|
||||||
Serial.print(" humidity=");
|
Serial.print(" humidity=");
|
||||||
Serial.println(humidity);
|
Serial.println(humidity);
|
||||||
|
}
|
||||||
|
|
||||||
|
int soilRaw = analogRead(PIN_SOIL_MOISTURE);
|
||||||
|
lastSoilPercent = rawToSoilPercent(soilRaw);
|
||||||
|
hasSoilReading = true;
|
||||||
|
mqttPublishTelemetry(DEVICE_ID_SOIL, "soil_moisture", lastSoilPercent);
|
||||||
|
|
||||||
|
Serial.print("[telemetry] soil_moisture=");
|
||||||
|
Serial.print(lastSoilPercent);
|
||||||
|
Serial.print("% (raw=");
|
||||||
|
Serial.print(soilRaw);
|
||||||
|
Serial.println(")");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool telemetryGetLast(float& temperature, float& humidity) {
|
bool telemetryGetLast(float& temperature, float& humidity) {
|
||||||
@@ -57,3 +82,11 @@ bool telemetryGetLast(float& temperature, float& humidity) {
|
|||||||
humidity = lastHumidity;
|
humidity = lastHumidity;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool telemetryGetLastSoil(float& percent) {
|
||||||
|
if (!hasSoilReading) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
percent = lastSoilPercent;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,3 +11,6 @@ void telemetryTick();
|
|||||||
// показывать данные не дожидаясь следующего цикла публикации).
|
// показывать данные не дожидаясь следующего цикла публикации).
|
||||||
// Возвращает false, если ни одного успешного чтения ещё не было.
|
// Возвращает false, если ни одного успешного чтения ещё не было.
|
||||||
bool telemetryGetLast(float& temperature, float& humidity);
|
bool telemetryGetLast(float& temperature, float& humidity);
|
||||||
|
|
||||||
|
// Последнее показание почвенной влажности в процентах (0-100).
|
||||||
|
bool telemetryGetLastSoil(float& percent);
|
||||||
|
|||||||
+13
-1
@@ -31,9 +31,12 @@ static const char PAGE_HTML[] PROGMEM = R"HTML(<!DOCTYPE html>
|
|||||||
<h1>Гроубокс</h1>
|
<h1>Гроубокс</h1>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="row"><span>Температура</span><span id="temp" class="sensor-value">—</span></div>
|
<div class="row"><span>Температура</span><span id="temp" class="sensor-value">—</span></div>
|
||||||
<div class="row"><span>Влажность</span><span id="hum" class="sensor-value">—</span></div>
|
<div class="row"><span>Влажность воздуха</span><span id="hum" class="sensor-value">—</span></div>
|
||||||
<div class="muted" id="updated">нет данных</div>
|
<div class="muted" id="updated">нет данных</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="row"><span>Влажность почвы</span><span id="soil" class="sensor-value">—</span></div>
|
||||||
|
</div>
|
||||||
<div class="card" id="actuators"></div>
|
<div class="card" id="actuators"></div>
|
||||||
<script>
|
<script>
|
||||||
const DEVICES = [
|
const DEVICES = [
|
||||||
@@ -46,6 +49,7 @@ function render(state) {
|
|||||||
document.getElementById('temp').textContent = state.sensor.has_reading ? state.sensor.temperature.toFixed(1) + ' °C' : '—';
|
document.getElementById('temp').textContent = state.sensor.has_reading ? state.sensor.temperature.toFixed(1) + ' °C' : '—';
|
||||||
document.getElementById('hum').textContent = state.sensor.has_reading ? state.sensor.humidity.toFixed(0) + ' %' : '—';
|
document.getElementById('hum').textContent = state.sensor.has_reading ? state.sensor.humidity.toFixed(0) + ' %' : '—';
|
||||||
document.getElementById('updated').textContent = state.sensor.has_reading ? 'данные с DHT22 получены' : 'нет данных с DHT22';
|
document.getElementById('updated').textContent = state.sensor.has_reading ? 'данные с DHT22 получены' : 'нет данных с DHT22';
|
||||||
|
document.getElementById('soil').textContent = state.soil.has_reading ? state.soil.percent.toFixed(0) + ' %' : '—';
|
||||||
|
|
||||||
const box = document.getElementById('actuators');
|
const box = document.getElementById('actuators');
|
||||||
box.innerHTML = '';
|
box.innerHTML = '';
|
||||||
@@ -100,6 +104,14 @@ static void handleState() {
|
|||||||
sensor["humidity"] = humidity;
|
sensor["humidity"] = humidity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float soilPercent;
|
||||||
|
bool hasSoilReading = telemetryGetLastSoil(soilPercent);
|
||||||
|
JsonObject soil = doc["soil"].to<JsonObject>();
|
||||||
|
soil["has_reading"] = hasSoilReading;
|
||||||
|
if (hasSoilReading) {
|
||||||
|
soil["percent"] = soilPercent;
|
||||||
|
}
|
||||||
|
|
||||||
JsonObject devicesObj = doc["devices"].to<JsonObject>();
|
JsonObject devicesObj = doc["devices"].to<JsonObject>();
|
||||||
devicesGetState(devicesObj);
|
devicesGetState(devicesObj);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user