Files
home_automatization_control…/growbox/web_ui.cpp
cacto 6a7d512b22 Датчик почвенной влажности soil-1 + второй релейный модуль под fan-1
По фото: у нас два одинаковых 2-канальных релейных модуля (не один) —
второй закрывает fan-1 по-настоящему (GPIO18), больше не "пока не куплено".

Добавлен soil-1: компараторный модуль почвенной влажности, читаем A0 через
ADC (GPIO34, input-only), публикуем sensor_type=soil_moisture в процентах.
Raw->% калибруется через SOIL_RAW_DRY/SOIL_RAW_WET в config.h — заводские
значения приблизительные, точную калибровку раньше сделать было нельзя (не
было физического доступа к датчику), нужно измерить на месте.

soil-1 в Postgres/Laravel платформы ещё не заведён (не трогаем
home_automatization) — телеметрия будет идти в ClickHouse, но в Laravel UI
устройство не появится до отдельной регистрации device_type/devices-записи.

Веб-интерфейс и /api/state дополнены полем "soil".
2026-08-18 12:57:49 +05:00

161 lines
5.3 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "web_ui.h"
#include <ArduinoJson.h>
#include <WebServer.h>
#include "devices.h"
#include "mqtt_link.h"
#include "telemetry.h"
static WebServer server(80);
static const char PAGE_HTML[] PROGMEM = R"HTML(<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Гроубокс</title>
<style>
body { font-family: -apple-system, sans-serif; max-width: 420px; margin: 2rem auto; padding: 0 1rem; background:#0f172a; color:#e2e8f0; }
h1 { font-size: 1.25rem; }
.card { background:#1e293b; border-radius: 12px; padding: 1rem; margin-bottom: 1rem; }
.row { display:flex; justify-content:space-between; align-items:center; margin: .5rem 0; }
button { border:0; border-radius:8px; padding:.5rem 1.1rem; font-size:1rem; cursor:pointer; }
.on { background:#22c55e; color:#052e16; }
.off { background:#334155; color:#e2e8f0; }
.sensor-value { font-size:1.4rem; font-weight:600; }
.muted { color:#94a3b8; font-size:.85rem; }
</style>
</head>
<body>
<h1>Гроубокс</h1>
<div class="card">
<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="muted" id="updated">нет данных</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>
<script>
const DEVICES = [
{ id: 'light-1', label: 'Свет' },
{ id: 'pump-1', label: 'Насос' },
{ id: 'fan-1', label: 'Вентилятор' },
];
function render(state) {
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('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');
box.innerHTML = '';
DEVICES.forEach(function (d) {
const on = state.devices[d.id] && state.devices[d.id].power === 'on';
const row = document.createElement('div');
row.className = 'row';
const label = document.createElement('span');
label.textContent = d.label;
const btn = document.createElement('button');
btn.className = on ? 'on' : 'off';
btn.textContent = on ? 'Вкл' : 'Выкл';
btn.onclick = function () { sendCommand(d.id, on ? 'turn_off' : 'turn_on'); };
row.appendChild(label);
row.appendChild(btn);
box.appendChild(row);
});
}
function sendCommand(deviceId, action) {
fetch('/api/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_id: deviceId, action: action }),
}).then(refresh);
}
function refresh() {
fetch('/api/state').then(function (r) { return r.json(); }).then(render);
}
refresh();
setInterval(refresh, 3000);
</script>
</body>
</html>
)HTML";
static void handleRoot() {
server.send_P(200, "text/html", PAGE_HTML);
}
static void handleState() {
JsonDocument doc;
float temperature, humidity;
bool hasReading = telemetryGetLast(temperature, humidity);
JsonObject sensor = doc["sensor"].to<JsonObject>();
sensor["has_reading"] = hasReading;
if (hasReading) {
sensor["temperature"] = temperature;
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>();
devicesGetState(devicesObj);
String body;
serializeJson(doc, body);
server.send(200, "application/json", body);
}
static void handleCommand() {
JsonDocument cmd;
DeserializationError parseErr = deserializeJson(cmd, server.arg("plain"));
if (parseErr) {
server.send(400, "application/json", "{\"error\":\"invalid json\"}");
return;
}
String deviceId = cmd["device_id"] | "";
String action = cmd["action"] | "";
double level = cmd["level"] | 0.0;
JsonDocument state;
// Тот же путь применения команды и публикации ack, что и у MQTT-команд
// (см. mqtt_link.cpp) — device shadow платформы остаётся консистентным
// независимо от того, кто отдал команду: Laravel/rule-engine или веб-UI.
if (!devicesApplyCommand(deviceId, action, level, state)) {
server.send(404, "application/json", "{\"error\":\"unknown device or action\"}");
return;
}
mqttPublishAck(deviceId, state);
String body;
serializeJson(state, body);
server.send(200, "application/json", body);
}
void webUiBegin() {
server.on("/", HTTP_GET, handleRoot);
server.on("/api/state", HTTP_GET, handleState);
server.on("/api/command", HTTP_POST, handleCommand);
server.begin();
Serial.println("[web] UI started on port 80");
}
void webUiHandle() {
server.handleClient();
}