diff --git a/DEVICE_BEHAVIOR.md b/DEVICE_BEHAVIOR.md index 48f8830..e939c76 100644 --- a/DEVICE_BEHAVIOR.md +++ b/DEVICE_BEHAVIOR.md @@ -5,35 +5,39 @@ ```mermaid stateDiagram-v2 [*] --> Booting: Включение питания - Booting --> WiFiConnect: Инициализация + Booting --> WiFiConnect: Инициализация периферии WiFiConnect --> MQTTConnect: WiFi OK - WiFiConnect --> WiFiConnect: Retry (30s) + WiFiConnect --> WiFiConnect: Retry (exponential backoff) MQTTConnect --> Connected: MQTT OK - MQTTConnect --> MQTTConnect: Retry (10s) + MQTTConnect --> MQTTConnect: Retry (exponential backoff) Connected --> Publishing: Таймер сбора данных Publishing --> Connected: Данные отправлены - Connected --> ExecutingCmd: Получена команда + Connected --> ExecutingCmd: Получена команда MQTT ExecutingCmd --> Connected: Команда выполнена - Connected --> Disconnected: Потеря связи - Disconnected --> MQTTConnect: Reconnect + Connected --> OTA: Получена cmd/ota + OTA --> [*]: Перезагрузка после обновления - Connected --> [*]: Команда reboot / WDT reset + Connected --> Disconnected: Потеря связи + Disconnected --> MQTTConnect: Reconnect (exponential backoff) + + Connected --> [*]: cmd/reboot или WDT reset ``` --- ## Последовательность запуска -1. **Boot** — инициализация I2C, SPI, OneWire периферии -2. **WiFi** — подключение, получение IP +1. **Boot** — инициализация I2C, SPI, OneWire, MCP23017 +2. **WiFi** — подключение, получение IP (exponential backoff при неудаче) 3. **MQTT connect** — handshake с брокером -4. **Publish `{UID}/status = "connected"`** — уведомление сервера -5. **Получить preferences** — сервер отвечает `{UID}/set/preferences/all` -6. **Применить калибровку** — записать коэффициенты в память -7. **Запуск основного цикла** — периодический сбор и публикация данных +4. **LWT настройка** — `{UID}/availability = "offline"` при разрыве +5. **Publish `{UID}/status = "connected"`** — уведомление сервера +6. **Получить preferences** — сервер отвечает `{UID}/set/preferences/all` +7. **Применить калибровку** — записать коэффициенты в NVS/RAM без перезагрузки +8. **Запуск основного цикла** — периодический сбор и публикация данных --- @@ -41,11 +45,13 @@ stateDiagram-v2 | Датчик | Интервал | |--------|----------| -| pH, EC, Temp | ~10 сек | -| AirTemp, AirHum | ~30 сек | -| RSSI, uptime | ~60 сек | +| pH, EC, wNTC | ~10 сек | +| AirTemp, AirHum, AirPress | ~30 сек | +| RootTemp (DS18B20) | ~15 сек | +| RSSI, CPUTemp, FreeHeap, Vcc | ~60 сек | +| uptime | ~60 сек | | MixerWeight | ~5 сек (если mixer_enabled) | -| readGPIO | при изменении | +| mcp/gpio | при изменении состояния | --- @@ -58,44 +64,64 @@ stateDiagram-v2 2. `ESP.restart()` ### `cmd/gpio/{pin}` -1. Проверить pin ∈ [0, 15] -2. MCP23017 → setPin(pin, state) +- Проверить pin ∈ [0, 15], payload ∈ {"0", "1"} +- MCP23017 → setPin(pin, state) ### `set/pump/{id}/run` -1. Проверить id ∈ [1, 8], time ∈ [1, 60000] -2. Включить помпу -3. Запустить таймер -4. По истечении времени — выключить +- Проверить id ∈ [0, 7] ⚠️ (см. расхождение ниже), time ∈ [1, 60000] +- Включить помпу → таймер → выключить ### `set/pump/{id}/dispense` -1. Проверить id ∈ [1, 8], grams ∈ [0.1, 1000] -2. Рассчитать время через `ml/sec` калибровку помпы -3. Включить → таймер → выключить -4. Обновить счётчик `total_dispensed` +- Проверить id ∈ [0, 7] ⚠️, grams ∈ [0.1, 1000] +- Рассчитать время через ml/sec калибровку помпы +- Включить → таймер → выключить +- Обновить счётчик `total_dispensed` ### `cmd/pump/{id}/stop` -1. Немедленно выключить помпу -2. Сбросить таймер +- Проверить id ∈ [0, 7] ⚠️ +- Немедленно выключить помпу, сбросить таймер ### `set/preferences/all` -1. Разобрать JSON -2. Обновить только пришедшие поля в NVS / RAM -3. Применить новые коэффициенты калибровки без перезагрузки +- Разобрать JSON, обновить только пришедшие поля в NVS / RAM +- Применить новые коэффициенты без перезагрузки + +### `cmd/ota` +- Payload: пустой (использует `preferences.updateUrl`) или URL строка (override) +- Скачать и прошить firmware.bin +- Перезагрузиться + +### `cmd/ota-storage` +- Аналогично `cmd/ota` но для LittleFS (`preferences.updateFsUrl`) + +--- + +## ⚠️ Pump ID расхождение + +| Сторона | Диапазон | Код | +|---------|----------|-----| +| Сервер (API serializer) | 1–8 | `min_value=1, max_value=8` | +| Прошивка (валидация) | 0–7 | `pumpId < 0 \|\| pumpId > 7` | +| Прошивка (итерация mixer) | 1–pumpCount | `for pumpId = 1...pumpCount` | + +**Следствие**: сервер отправляет `set/pump/8/run` → прошивка валидирует `8 > 7` → команда игнорируется. +**Рекомендация**: привести к единому стандарту 1-based (1–8). Исправить валидацию в `mqtt_callbacks.cpp` строки 457, 482, 507 на `pumpId < 1 || pumpId > 8`. --- ## Reconnect логика -- WiFi: авто-реконнект встроенный в ESP-IDF -- MQTT: переподключение каждые 10 секунд -- При восстановлении MQTT: снова публикует `{UID}/status = "connected"` -- Сервер снова отправляет preferences (идемпотентно) +- WiFi: встроенный авто-реконнект ESP-IDF +- MQTT: экспоненциальный backoff (1s → 2s → 4s → ... → max 60s) +- При восстановлении: публикует `{UID}/status = "connected"` → сервер снова шлёт preferences --- -## OTA обновления +## Home Assistant интеграция (отдельный канал) -URL прошивки: `preferences.updateUrl` -По умолчанию: `https://ponics.online/static/wegabox/esp32-local/firmware.bin` - -Триггер обновления: команда (TBD) или автоматически при запуске если новая версия. +Прошивка поддерживает параллельную интеграцию с Home Assistant через MQTT. +Топики (не используются сервером ponics.online): +- `{UID}/availability` — online/offline +- `{UID}/pin/{i}/cmd` — управление GPIO +- `{UID}/pin/{i}/state` — состояние GPIO +- `{UID}/mixer/cmd` — управление миксером +- Autodiscovery топики для HA diff --git a/MQTT.md b/MQTT.md index 68f3ee5..f4194ec 100644 --- a/MQTT.md +++ b/MQTT.md @@ -25,8 +25,8 @@ sequenceDiagram MQTT->>ESP32: apply calibration & config loop Каждые N секунд - ESP32->>MQTT: publish {UID}/data-timescale/EC = "1.85" - ESP32->>MQTT: publish {UID}/data-timescale/ph = "6.2" + ESP32->>MQTT: publish {UID}/data-timescale/wEC = "1.85" + ESP32->>MQTT: publish {UID}/data-timescale/wpH = "6.2" ESP32->>MQTT: publish {UID}/data-timescale/wNTC = "22.5" MQTT->>Django: forward Django->>Redis: update live cache (boxconfig-{token}) @@ -63,21 +63,30 @@ sequenceDiagram **Payload**: строка с числом (например `"6.24"`, `"1.85"`, `"22.5"`) ``` -{UID}/data-timescale/EC → значение EC (мСм/см) -{UID}/data-timescale/ph → значение pH +{UID}/data-timescale/wEC → EC (мСм/см, с температурной компенсацией) +{UID}/data-timescale/wECnt → EC без температурной компенсации +{UID}/data-timescale/wpH → pH {UID}/data-timescale/wNTC → температура воды (°C, откалиброванная) +{UID}/data-timescale/RootTemp → температура субстрата/корней (°C, DS18B20) {UID}/data-timescale/AirTemp → температура воздуха (°C) {UID}/data-timescale/AirHum → влажность воздуха (%) {UID}/data-timescale/AirPress → давление (гПа) -{UID}/data-timescale/RootTemp → температура субстрата/корней (°C, DS18B20) -{UID}/data-timescale/calc_dist → уровень воды (см/%) -{UID}/data-timescale/calc_pr → освещённость (%) +{UID}/data-timescale/wLevel → уровень воды (% или см, HC-SR04) +{UID}/data-timescale/Light → освещённость (%) {UID}/data-timescale/RSSI → уровень WiFi сигнала (dBm) {UID}/data-timescale/uptime → аптайм устройства (мс) -{UID}/data-timescale/MixerWeight → вес миксера (г) -{UID}/data-timescale/readGPIO → битовая маска GPIO MCP23017 (0-65535) +{UID}/data-timescale/CPUTemp → температура CPU ESP32 (°C) +{UID}/data-timescale/FreeHeap → свободная RAM (байт) +{UID}/data-timescale/Vcc → напряжение питания (мВ) +{UID}/data-timescale/MixerWeight → вес миксера (г, HX711) +{UID}/data-timescale/mcp/gpio → битовая маска GPIO MCP23017 (0-65535) +{UID}/data-timescale/CO2 → CO2 (ppm, SCD30) +{UID}/data-timescale/eCO2 → CO2 эквивалент (ppm, CCS811) +{UID}/data-timescale/TVOC → летучие вещества (ppb, CCS811) ``` +> **Маппинг имён на сервере**: сервер дополнительно принимает синонимы `EC`, `ph`, `ec`, `calc_dist` и маппит их в канонические имена через `field_mappings` в `config.py`. + ### `{UID}/status` Payload: строка `"connected"` — устройство подключилось к брокеру. @@ -85,50 +94,59 @@ Payload: строка `"connected"` — устройство подключил ### `{UID}/status/sensors` -Ответ на запрос `{UID}/cmd/status/sensors`. +Ответ на `{UID}/cmd/status/sensors`. Поля зависят от подключённых датчиков. ```json { - "ph": 6.24, - "ec": 1.85, - "temp_cal": 22.5, - "calc_dist": 45.2, + "rootTemp": 21.0, + "waterTemp": 22.5, "airTemp": 24.1, - "airHumidity": 65.0, - "RSSI": -65, - "uptime": 3600000 + "airHumidity": 65, + "pressure": 1013, + "ntcTemp": 22.4, + "EC": 1.85, + "pH": 6.24, + "light": 80.0, + "waterLevel": 45.2, + "weight": 0.0 } ``` ### `{UID}/status/mixer` -Ответ на запрос `{UID}/cmd/status/mixer`. +Ответ на `{UID}/cmd/status/mixer`. ```json { + "running": false, + "phase": "idle", + "currentPump": 0, + "weight": 480.5, "pumps": [ - {"index": 0, "name": "CaNO3", "total_dispensed": 480.5}, - {"index": 1, "name": "KNO3", "total_dispensed": 320.0}, - {"index": 2, "name": "MgSO4", "total_dispensed": 150.0} + {"index": 1, "total_dispensed": 480.5}, + {"index": 2, "total_dispensed": 320.0} ] } ``` +> **Примечание**: поле `name` в элементах `pumps` отсутствует в ответе прошивки. Названия помп берутся из БД сервера. + +### `{UID}/status/full` + +Полный статус устройства (всё включая actuators + config). Ответ на `cmd/status`. + +### `{UID}/status/actuators` + +Состояние реле, GPIO, помп. + ### `{UID}/main` (legacy) -Устаревший формат, поддерживается для обратной совместимости. Новые устройства не используют. +Устаревший формат. Новые устройства не используют. ```json { - "sensors": { - "ph": "6.5", - "ec": "1.8", - "temp_cal": "22.5" - }, - "system": { - "uptime": "3600000", - "RSSI": "-65" - } + "sensors": {"ph": "6.5", "ec": "1.8", "temp_cal": "22.5"}, + "system": {"uptime": "3600000", "RSSI": "-65"} } ``` @@ -142,22 +160,26 @@ Payload: строка `"connected"` — устройство подключил |-------|---------|----------| | `{UID}/cmd/reboot` | `"1"` | Перезагрузка устройства | | `{UID}/cmd/gpio/{pin}` | `"1"` / `"0"` | GPIO HIGH/LOW (MCP23017, pin 0–15) | -| `{UID}/cmd/pump/{id}/stop` | `"1"` | Стоп помпы (id 1–8) | +| `{UID}/cmd/pump/{id}/stop` | `"1"` | Стоп помпы (id 1–7, см. ниже) | | `{UID}/cmd/status` | `"1"` | Запрос полного статуса | | `{UID}/cmd/status/sensors` | `"1"` | Запрос данных датчиков | | `{UID}/cmd/status/actuators` | `"1"` | Запрос статуса актуаторов | | `{UID}/cmd/status/mixer` | `"1"` | Запрос статуса миксера | +| `{UID}/cmd/ota` | `""` или URL строка | OTA обновление прошивки | +| `{UID}/cmd/ota-storage` | `""` или URL строка | OTA обновление LittleFS | ### Управление помпами (`set/pump/`) | Топик | Payload | Ограничения | |-------|---------|------------| -| `{UID}/set/pump/{id}/run` | `"5000"` (мс) | id: 1–8, время: 1–60000 мс | -| `{UID}/set/pump/{id}/dispense` | `"50.5"` (граммы) | id: 1–8, граммы: 0.1–1000 | +| `{UID}/set/pump/{id}/run` | `"5000"` (мс) | id: 1–7 ⚠️, время: 1–60000 мс | +| `{UID}/set/pump/{id}/dispense` | `"50.5"` (граммы) | id: 1–7 ⚠️, граммы: 0.1–1000 | + +> ⚠️ **Pump ID расхождение**: сервер (API) принимает id 1–8, прошивка валидирует `pumpId < 0 || pumpId > 7` — id=8 будет отклонён прошивкой. Нужно либо выровнять API на 1–7, либо исправить прошивку на 0–8. Текущее рабочее значение: **1–7**. ### Настройки (`set/preferences/all`) -Полная конфигурация устройства. Отправляется при подключении или изменении настроек. +Полная конфигурация устройства. Все поля опциональны. ```json { @@ -169,40 +191,83 @@ Payload: строка `"connected"` — устройство подключил "Rx1": 1000, "Rx2": 1000, "Dr": 1.0, + "ec1": 1.41, "ec2": 12.88, + "ec3": 80.0, "ex1": 0.5, "ex2": 0.9, + "ex3": 0.95, + "ea": 0.0, + "eb": 0.0, "kt": 0.019, "ecKorr": 1.0, - "px1": 100, - "px2": 580, - "px3": 1023, - "py1": 0.0, - "py2": 7.0, - "py3": 14.0, + + "px1": 100, "py1": 0.0, + "px2": 580, "py2": 7.0, + "px3": 1023, "py3": 14.0, "pHlKorr": 0.0, - "maxLLevel": 30.0, - "maxLRaw": 100, - "minLLevel": 10.0, - "minLRaw": 500, + + "pR_DAC": 4095, + "pR1": 10000, + "pR_raw_p1": 0, "pR_val_p1": 0, + "pR_raw_p2": 512, "pR_val_p2": 50, + "pR_raw_p3": 1023,"pR_val_p3": 100, + "pR_Rx": 10000, + "pR_T": 25, + "pR_x": 0.5, + "pRType": "linear", + + "maxLLevel": 30.0, "maxLRaw": 100, + "minLLevel": 10.0, "minLRaw": 500, + "hostname": "wega-box-1", "updateUrl": "https://ponics.online/static/wegabox/esp32-local/firmware.bin", + "updateFsUrl": "https://ponics.online/static/wegabox/esp32-local/littlefs.bin", + "ds18b20_enabled": true, "bme280_enabled": true, + "aht10_enabled": false, + "am2320_enabled": false, + "scd30_enabled": false, + "ccs811_enabled": false, + "ads1115_enabled": false, "ec_enabled": true, + "ntc_enabled": true, + "pr_enabled": true, + "hx711_enabled": false, + "hx710b_enabled": false, + "us025_enabled": false, + "us025_reboot_on_error": false, + "vl53l0x_enabled": false, + "vcc_enabled": true, + "cputemp_enabled": true, + "mixer_enabled": false, "mixer_pumpCount": 8, + "mixer_bidirectional": false, + "mixer_dropThreshold": 0.5, + "mixer_preloadMs": 0, + "hx711_calibrationA": 1000.0, "hx711_calibrationB": 500.0, + "hx711_offsetA": 0.0, + "hx711_offsetB": 0.0, + "hx711_updateMs": 500, + "hx711_gpioDout": 4, + "hx711_gpioSck": 5, + "hx711_spsRate": 10, + "ecDoser_enabled": false, "ecDoser_intervalSec": 3600, - "ecDoser_limitEC": 3.0 + "ecDoser_limitEC": 3.0, + "ecDoser_valueA": 10.0, + "ecDoser_valueB": 10.0, + + "dallas_ecSensorIndex": 0 } ``` -**Все поля опциональны** — устройство применяет только те, что пришли. - --- ## Обработка сообщений на сервере @@ -217,7 +282,7 @@ flowchart TD CHECK -->|"data-timescale/{METRIC}"| PROC[MessageProcessor] PROC --> PARSE[Парсинг значения] - PARSE --> MAP[Маппинг имени поля] + PARSE --> MAP[Маппинг имени поля
wEC→ec, wpH→ph, wLevel→calc_dist...] MAP --> BATCH[Batcher
100 метрик / 25 сек] BATCH --> TSDB[(TimescaleDB)] MAP --> CACHE[Redis live cache
boxconfig-{token}]