commit 6655dd3d7a3004fca3b487fdc7e8e1f78f2e75c3 Author: progl Date: Tue May 5 11:42:57 2026 +0300 init: спецификация протокола WegaBox ↔ ponics.online MQTT топики, REST API, поведение устройства, маппинг датчиков. Mermaid диаграммы: flow подключения, обработка сообщений, FSM устройства. diff --git a/DEVICE_BEHAVIOR.md b/DEVICE_BEHAVIOR.md new file mode 100644 index 0000000..48f8830 --- /dev/null +++ b/DEVICE_BEHAVIOR.md @@ -0,0 +1,101 @@ +# Device Behavior + +## Конечный автомат устройства + +```mermaid +stateDiagram-v2 + [*] --> Booting: Включение питания + Booting --> WiFiConnect: Инициализация + WiFiConnect --> MQTTConnect: WiFi OK + WiFiConnect --> WiFiConnect: Retry (30s) + MQTTConnect --> Connected: MQTT OK + MQTTConnect --> MQTTConnect: Retry (10s) + + Connected --> Publishing: Таймер сбора данных + Publishing --> Connected: Данные отправлены + + Connected --> ExecutingCmd: Получена команда + ExecutingCmd --> Connected: Команда выполнена + + Connected --> Disconnected: Потеря связи + Disconnected --> MQTTConnect: Reconnect + + Connected --> [*]: Команда reboot / WDT reset +``` + +--- + +## Последовательность запуска + +1. **Boot** — инициализация I2C, SPI, OneWire периферии +2. **WiFi** — подключение, получение IP +3. **MQTT connect** — handshake с брокером +4. **Publish `{UID}/status = "connected"`** — уведомление сервера +5. **Получить preferences** — сервер отвечает `{UID}/set/preferences/all` +6. **Применить калибровку** — записать коэффициенты в память +7. **Запуск основного цикла** — периодический сбор и публикация данных + +--- + +## Периодичность публикации данных + +| Датчик | Интервал | +|--------|----------| +| pH, EC, Temp | ~10 сек | +| AirTemp, AirHum | ~30 сек | +| RSSI, uptime | ~60 сек | +| MixerWeight | ~5 сек (если mixer_enabled) | +| readGPIO | при изменении | + +--- + +## Обработка команд + +Все команды **fire-and-forget** — ответ не отправляется. + +### `cmd/reboot` +1. Завершить активные операции помп (стоп) +2. `ESP.restart()` + +### `cmd/gpio/{pin}` +1. Проверить pin ∈ [0, 15] +2. MCP23017 → setPin(pin, state) + +### `set/pump/{id}/run` +1. Проверить id ∈ [1, 8], time ∈ [1, 60000] +2. Включить помпу +3. Запустить таймер +4. По истечении времени — выключить + +### `set/pump/{id}/dispense` +1. Проверить id ∈ [1, 8], grams ∈ [0.1, 1000] +2. Рассчитать время через `ml/sec` калибровку помпы +3. Включить → таймер → выключить +4. Обновить счётчик `total_dispensed` + +### `cmd/pump/{id}/stop` +1. Немедленно выключить помпу +2. Сбросить таймер + +### `set/preferences/all` +1. Разобрать JSON +2. Обновить только пришедшие поля в NVS / RAM +3. Применить новые коэффициенты калибровки без перезагрузки + +--- + +## Reconnect логика + +- WiFi: авто-реконнект встроенный в ESP-IDF +- MQTT: переподключение каждые 10 секунд +- При восстановлении MQTT: снова публикует `{UID}/status = "connected"` +- Сервер снова отправляет preferences (идемпотентно) + +--- + +## OTA обновления + +URL прошивки: `preferences.updateUrl` +По умолчанию: `https://ponics.online/static/wegabox/esp32-local/firmware.bin` + +Триггер обновления: команда (TBD) или автоматически при запуске если новая версия. diff --git a/MQTT.md b/MQTT.md new file mode 100644 index 0000000..68f3ee5 --- /dev/null +++ b/MQTT.md @@ -0,0 +1,226 @@ +# MQTT Protocol + +## Общее + +- **Брокер**: Mosquitto 2.x (порт 1883) +- **Topic prefix**: `{UID}` — UUID токен устройства (из БД `GrowBox.token`) +- **QoS**: 1 для команд, 0 для данных датчиков +- **Subscribe**: сервер подписан на `#` (все топики) + +--- + +## Flow: подключение устройства + +```mermaid +sequenceDiagram + participant ESP32 as WegaBox (ESP32) + participant MQTT as MQTT Broker + participant Django as ponics.online (Django) + participant Redis + + ESP32->>MQTT: publish {UID}/status = "connected" + MQTT->>Django: forward + Django->>Django: push_box_settings(box) + Django->>MQTT: publish {UID}/set/preferences/all = {JSON} + 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/wNTC = "22.5" + MQTT->>Django: forward + Django->>Redis: update live cache (boxconfig-{token}) + Django->>Django: batch → TimescaleDB (каждые 25с или 100 метрик) + end +``` + +--- + +## Flow: команда с веб-интерфейса + +```mermaid +sequenceDiagram + participant User as Браузер + participant Django as ponics.online (Django) + participant MQTT as MQTT Broker + participant ESP32 as WegaBox (ESP32) + + User->>Django: POST /api/box/{id}/pump/run/ {pump_id:1, time_ms:5000} + Django->>MQTT: publish {UID}/set/pump/1/run = "5000" + MQTT->>ESP32: execute pump 1 for 5000ms + Django->>User: 200 {"success": true} + Note over ESP32: Команды fire-and-forget, ответа нет +``` + +--- + +## Топики: устройство → сервер (Subscribe) + +### `{UID}/data-timescale/{METRIC_NAME}` + +Основной поток данных датчиков. Каждый датчик — отдельное сообщение. + +**Payload**: строка с числом (например `"6.24"`, `"1.85"`, `"22.5"`) + +``` +{UID}/data-timescale/EC → значение EC (мСм/см) +{UID}/data-timescale/ph → значение pH +{UID}/data-timescale/wNTC → температура воды (°C, откалиброванная) +{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/RSSI → уровень WiFi сигнала (dBm) +{UID}/data-timescale/uptime → аптайм устройства (мс) +{UID}/data-timescale/MixerWeight → вес миксера (г) +{UID}/data-timescale/readGPIO → битовая маска GPIO MCP23017 (0-65535) +``` + +### `{UID}/status` + +Payload: строка `"connected"` — устройство подключилось к брокеру. +Триггер: сервер отправляет настройки (`set/preferences/all`). + +### `{UID}/status/sensors` + +Ответ на запрос `{UID}/cmd/status/sensors`. + +```json +{ + "ph": 6.24, + "ec": 1.85, + "temp_cal": 22.5, + "calc_dist": 45.2, + "airTemp": 24.1, + "airHumidity": 65.0, + "RSSI": -65, + "uptime": 3600000 +} +``` + +### `{UID}/status/mixer` + +Ответ на запрос `{UID}/cmd/status/mixer`. + +```json +{ + "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} + ] +} +``` + +### `{UID}/main` (legacy) + +Устаревший формат, поддерживается для обратной совместимости. Новые устройства не используют. + +```json +{ + "sensors": { + "ph": "6.5", + "ec": "1.8", + "temp_cal": "22.5" + }, + "system": { + "uptime": "3600000", + "RSSI": "-65" + } +} +``` + +--- + +## Топики: сервер → устройство (Publish) + +### Команды (`cmd/`) + +| Топик | Payload | Описание | +|-------|---------|----------| +| `{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/status` | `"1"` | Запрос полного статуса | +| `{UID}/cmd/status/sensors` | `"1"` | Запрос данных датчиков | +| `{UID}/cmd/status/actuators` | `"1"` | Запрос статуса актуаторов | +| `{UID}/cmd/status/mixer` | `"1"` | Запрос статуса миксера | + +### Управление помпами (`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 | + +### Настройки (`set/preferences/all`) + +Полная конфигурация устройства. Отправляется при подключении или изменении настроек. + +```json +{ + "ntcDAC": 4095, + "ntcB": 3950, + "ntcValKorr": 0.0, + "ntcType": "NTC3950", + "R1": 10000, + "Rx1": 1000, + "Rx2": 1000, + "Dr": 1.0, + "ec1": 1.41, + "ec2": 12.88, + "ex1": 0.5, + "ex2": 0.9, + "kt": 0.019, + "ecKorr": 1.0, + "px1": 100, + "px2": 580, + "px3": 1023, + "py1": 0.0, + "py2": 7.0, + "py3": 14.0, + "pHlKorr": 0.0, + "maxLLevel": 30.0, + "maxLRaw": 100, + "minLLevel": 10.0, + "minLRaw": 500, + "hostname": "wega-box-1", + "updateUrl": "https://ponics.online/static/wegabox/esp32-local/firmware.bin", + "ds18b20_enabled": true, + "bme280_enabled": true, + "ec_enabled": true, + "mixer_enabled": false, + "mixer_pumpCount": 8, + "hx711_calibrationA": 1000.0, + "hx711_calibrationB": 500.0, + "ecDoser_enabled": false, + "ecDoser_intervalSec": 3600, + "ecDoser_limitEC": 3.0 +} +``` + +**Все поля опциональны** — устройство применяет только те, что пришли. + +--- + +## Обработка сообщений на сервере + +```mermaid +flowchart TD + MSG[MQTT сообщение] --> CHECK{Суффикс топика} + CHECK -->|"main"| REDIS[Redis hash-store] + CHECK -->|"status/mixer"| MIXER[Обновить статус миксера] + CHECK -->|"status/sensors"| SENS[Обновить кеш датчиков] + CHECK -->|"status" + "connected"| SYNC[push_box_settings → отправить преференции] + CHECK -->|"data-timescale/{METRIC}"| PROC[MessageProcessor] + + PROC --> PARSE[Парсинг значения] + PARSE --> MAP[Маппинг имени поля] + MAP --> BATCH[Batcher
100 метрик / 25 сек] + BATCH --> TSDB[(TimescaleDB)] + MAP --> CACHE[Redis live cache
boxconfig-{token}] + CACHE --> COND{Проверка условий} + COND -->|Сработало| TG[Telegram уведомление] +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c30e0d --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# ponics-protocol + +Единый источник истины для коммуникации между **ponics.online** (сервер) и **WegaBox** (ESP32 устройство). + +## Репозитории + +| Репо | Описание | +|------|----------| +| [web-calc](https://gitea.progl.su/progl/web-calc) | Django backend + frontend (ponics.online) | +| [wega-box](https://gitea.progl.su/progl/wega-box) | Прошивка ESP32 | +| **ponics-protocol** (этот репо) | Спецификация протокола | + +## Документация + +- [MQTT.md](MQTT.md) — Топики, payload схемы, flow диаграммы +- [REST_API.md](REST_API.md) — HTTP API эндпоинты +- [DEVICE_BEHAVIOR.md](DEVICE_BEHAVIOR.md) — Конечный автомат устройства +- [SENSORS.md](SENSORS.md) — Поля датчиков, маппинг имён + +## Быстрый старт + +``` +Device UID = token устройства (UUID формат) +MQTT Broker = ponics.online:1883 +Аутентификация = username/password (в настройках устройства) +``` + +### Ключевые топики + +``` +{UID}/data-timescale/{METRIC} ← устройство шлёт данные датчиков +{UID}/status ← устройство шлёт "connected" +{UID}/cmd/reboot → сервер перезагружает устройство +{UID}/set/pump/{id}/run → сервер запускает помпу (мс) +{UID}/set/pump/{id}/dispense → сервер наливает граммы +{UID}/set/preferences/all → сервер синхронизирует настройки +``` diff --git a/REST_API.md b/REST_API.md new file mode 100644 index 0000000..7dc8455 --- /dev/null +++ b/REST_API.md @@ -0,0 +1,156 @@ +# REST API + +Base URL: `https://ponics.online/api/box/` + +Аутентификация: сессия Django (cookie) или токен. + +--- + +## Эндпоинты + +### GET `/api/box/` +Список боксов текущего пользователя. + +**Response:** +```json +[ + { + "id": 23, + "name": "Бокс №1", + "token": "550e8400-e29b-41d4-a716-446655440000", + "online": true, + "last_seen": "2026-05-03T08:00:00Z" + } +] +``` + +--- + +### GET `/api/box/{id}/` +Детали бокса с конфигурацией. + +--- + +### GET `/api/box/{id}/live/` +Живые данные датчиков из Redis-кеша. + +**Response:** +```json +{ + "ph": 6.24, + "ec": 1.85, + "tankTemp": 22.5, + "rootTemp": 21.0, + "airTemp": 24.1, + "airHumidity": 65.0, + "waterLevel": 45.2, + "light": 80.0, + "online": true, + "last_seen": "2026-05-03T08:01:23Z" +} +``` + +--- + +### POST `/api/box/{id}/reboot/` +Перезагрузка устройства. + +**Request:** `{}` (пустой body) + +**Response:** +```json +{"success": true, "message": "Команда reboot отправлена", "command": "reboot"} +``` + +--- + +### POST `/api/box/{id}/gpio/` +Управление GPIO (MCP23017). + +**Request:** +```json +{"pin": 5, "state": true} +``` +- `pin`: 0–15 +- `state`: `true` (HIGH) / `false` (LOW) + +**Response:** +```json +{"success": true, "command": "gpio", "pin": 5, "state": true} +``` + +--- + +### POST `/api/box/{id}/pump/run/` +Запуск помпы на заданное время. + +**Request:** +```json +{"pump_id": 1, "time_ms": 5000} +``` +- `pump_id`: 1–8 +- `time_ms`: 1–60000 + +**Response:** +```json +{"success": true, "command": "pump_run", "pump_id": 1, "time_ms": 5000} +``` + +--- + +### POST `/api/box/{id}/pump/dispense/` +Налить определённое количество граммов. + +**Request:** +```json +{"pump_id": 2, "grams": 50.5} +``` +- `pump_id`: 1–8 +- `grams`: 0.1–1000 + +**Response:** +```json +{"success": true, "command": "pump_dispense", "pump_id": 2, "grams": 50.5} +``` + +--- + +### POST `/api/box/{id}/pump/stop/` +Остановить помпу. + +**Request:** +```json +{"pump_id": 1} +``` +- `pump_id`: 1–8 + +**Response:** +```json +{"success": true, "command": "pump_stop", "pump_id": 1} +``` + +--- + +### POST `/api/box/{id}/preferences/` +Обновить настройки и калибровку устройства. + +**Request:** JSON с любыми полями из [MQTT.md → set/preferences/all](MQTT.md#настройки-setpreferencesall) + +**Response:** +```json +{ + "success": true, + "message": "Настройки отправлены (3 полей)", + "fields": ["ecKorr", "pHlKorr", "ntcValKorr"] +} +``` + +--- + +## Коды ошибок + +| HTTP | Описание | +|------|----------| +| 400 | Неверные параметры (pin/pump_id/time вне диапазона) | +| 503 | MQTT недоступен | +| 404 | Бокс не найден / нет доступа | diff --git a/SENSORS.md b/SENSORS.md new file mode 100644 index 0000000..5e340e8 --- /dev/null +++ b/SENSORS.md @@ -0,0 +1,78 @@ +# Sensors & Field Mapping + +## Основные поля (live data) + +Эти поля публикуются в `{UID}/data-timescale/{FIELD}` и доступны через `/api/box/{id}/live/`. + +| API поле | MQTT имена (синонимы) | Единица | Описание | +|----------|----------------------|---------|----------| +| `ph` | `ph`, `wpH`, `pH` | 0–14 | pH раствора (откалиброванный) | +| `ec` | `ec`, `wEC`, `EC` | мСм/см | EC раствора (откалиброванный) | +| `tankTemp` | `temp_cal`, `wNTC`, `waterTemp` | °C | Температура воды (NTC, откалиброванная) | +| `rootTemp` | `rootTemp`, `RootTemp` | °C | Температура субстрата (DS18B20) | +| `airTemp` | `airTemp`, `AirTemp` | °C | Температура воздуха (BME280) | +| `airHumidity` | `airHumidity`, `AirHum` | % | Влажность воздуха (BME280) | +| `waterLevel` | `calc_dist`, `Dist`, `wLevel` | % или см | Уровень воды (HC-SR04) | +| `light` | `calc_pr`, `PR` | % | Освещённость (фоторезистор) | + +## Системные поля + +| MQTT поле | Единица | Описание | +|-----------|---------|----------| +| `RSSI` | dBm | Уровень WiFi сигнала | +| `uptime` | мс | Аптайм устройства | +| `CPUTemp` | °C | Температура процессора ESP32 | +| `freeHeap` | байт | Свободная RAM | +| `MixerWeight` | г | Вес в миксере (HX711) | +| `readGPIO` | 0–65535 | Битовая маска GPIO MCP23017 | + +## Сырые / отладочные поля + +| MQTT поле | Описание | +|-----------|----------| +| `NTC` | Сырое значение АЦП NTC | +| `pHmV` | pH в милливольтах | +| `pHraw` | Сырое значение АЦП pH | +| `ec_no_termo` | EC без температурной компенсации | +| `AirPress` | Атмосферное давление (гПа) | +| `Vcc` | Напряжение питания (мВ) | +| `hall` | Датчик Холла ESP32 | +| `CO2` | CO2 (ppm) | +| `tVOC` | Летучие вещества | +| `PumpA_SUM`, `PumpB_SUM` | Суммарный объём помп | + +## Калибровка + +### pH + +``` +Точки: (px1, py1), (px2, py2), (px3, py3) +ADC → pH = кусочно-линейная интерполяция +Корректировка: pH_final = pH + pHlKorr +``` + +### EC + +``` +Точки калибровки: ec1 (1.41 мСм/см), ec2 (12.88 мСм/см) +Соответствующие ADC значения: ex1, ex2 +Температурная компенсация: EC_25 = EC_T / (1 + kt * (T - 25)) +Корректировка: ecKorr (множитель, default = 1.0) +``` + +### Температура воды (NTC) + +``` +Тип: NTC3950 (ntcType) +B-коэффициент: ntcB = 3950 +R1 = 10000 Ом +Корректировка: ntcValKorr (offset °C, default = 0.0) +``` + +### Уровень воды (HC-SR04) + +``` +maxLLevel → maxLRaw (100% → ADC значение) +minLLevel → minLRaw (0% → ADC значение) +Линейная интерполяция +```