Мёрж feature/web-ui: веб-интерфейс и распиновка в README
This commit is contained in:
+75
-2
@@ -40,13 +40,68 @@ Laravel (`DemoGrowboxSeeder` использует `sensor-1`/`fan-1`), а `zone_
|
|||||||
DHT22 требует подтягивающий резистор ~10кОм между data и VCC, если он не
|
DHT22 требует подтягивающий резистор ~10кОм между data и VCC, если он не
|
||||||
встроен в конкретный модуль.
|
встроен в конкретный модуль.
|
||||||
|
|
||||||
## Библиотеки (Arduino Library Manager)
|
### Схема подключения
|
||||||
|
|
||||||
|
```
|
||||||
|
ESP32 DevKitC
|
||||||
|
┌───────────────────────┐
|
||||||
|
DHT22 DATA ───────┤ GPIO4 │
|
||||||
|
(+10к подтяжка │ │
|
||||||
|
DATA -> 3V3) │ │
|
||||||
|
│ │
|
||||||
|
Реле LIGHT IN ────┤ GPIO16 │
|
||||||
|
Реле PUMP IN ────┤ GPIO17 │
|
||||||
|
Реле FAN IN ────┤ GPIO18 │
|
||||||
|
│ │
|
||||||
|
3V3 ────┤ 3V3 │
|
||||||
|
GND ────┤ GND │
|
||||||
|
USB/5V ─┤ VIN │
|
||||||
|
└───────────────────────┘
|
||||||
|
|
||||||
|
DHT22: VCC -> 3V3 GND -> GND DATA -> GPIO4
|
||||||
|
Реле-модуль: VCC -> 5V (VIN) или 3V3 (смотри маркировку модуля)
|
||||||
|
GND -> GND (общий с ESP32)
|
||||||
|
IN -> GPIO16 / GPIO17 / GPIO18
|
||||||
|
|
||||||
|
⚠️ Реле коммутируют цепь самой нагрузки (свет/насос/вентилятор, обычно
|
||||||
|
12V/220V) через контакты COM/NO/NC — это ОТДЕЛЬНАЯ силовая цепь со своим
|
||||||
|
источником питания, к GPIO ESP32 она не подключается. К плате идёт
|
||||||
|
только сигнальный IN каждого реле-модуля.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Библиотеки
|
||||||
|
|
||||||
|
Через Arduino Library Manager (Sketch → Include Library → Manage Libraries):
|
||||||
|
|
||||||
- `PubSubClient` (Nick O'Leary) — MQTT-клиент
|
- `PubSubClient` (Nick O'Leary) — MQTT-клиент
|
||||||
- `ArduinoJson` (Benoit Blanchon, v7) — сериализация/парсинг JSON
|
- `ArduinoJson` (Benoit Blanchon, v7) — сериализация/парсинг JSON
|
||||||
- `DHT sensor library` (Adafruit) + зависимость `Adafruit Unified Sensor`
|
- `DHT sensor library` (Adafruit) + зависимость `Adafruit Unified Sensor`
|
||||||
- Board support: `esp32` (Espressif Systems) через Boards Manager
|
- Board support: `esp32` (Espressif Systems) через Boards Manager
|
||||||
|
|
||||||
|
`WebServer` (веб-интерфейс) и `WiFi` идут в комплекте с board support
|
||||||
|
`esp32` — отдельно ставить не нужно.
|
||||||
|
|
||||||
|
## Веб-интерфейс
|
||||||
|
|
||||||
|
Каждая плата поднимает у себя простую страницу и JSON API на порту 80 —
|
||||||
|
удобно наблюдать/управлять локально, не дожидаясь Laravel/MQTT:
|
||||||
|
|
||||||
|
- `GET /` — HTML-страница: показания DHT22 + кнопки Вкл/Выкл для
|
||||||
|
light-1/pump-1/fan-1.
|
||||||
|
- `GET /api/state` — JSON `{"sensor":{"has_reading","temperature","humidity"},"devices":{"light-1":{"power"},...}}`.
|
||||||
|
- `POST /api/command` — JSON-тело `{"device_id","action","level"?}`, тот же
|
||||||
|
формат, что и в MQTT-команде.
|
||||||
|
|
||||||
|
Команда с веб-UI применяется через тот же код и публикует тот же ack в
|
||||||
|
`devices/{id}/ack`, что и команда из device-control-service — device shadow
|
||||||
|
платформы (Redis) не расходится с реальным состоянием железа независимо от
|
||||||
|
того, кто его переключил.
|
||||||
|
|
||||||
|
Включается/выключается флагом `ENABLE_WEB_UI` в `config.h` (`1`/`0`) — при
|
||||||
|
`0` HTTP-сервер не поднимается вообще, порт 80 не слушается. У сервера нет
|
||||||
|
авторизации — только для локальной сети, как и mosquitto платформы; для
|
||||||
|
выхода за пределы LAN сначала нужно добавить хотя бы Basic Auth.
|
||||||
|
|
||||||
## Настройка и прошивка
|
## Настройка и прошивка
|
||||||
|
|
||||||
1. `cp config.h.example config.h`, заполнить Wi-Fi SSID/пароль, IP хоста, на
|
1. `cp config.h.example config.h`, заполнить Wi-Fi SSID/пароль, IP хоста, на
|
||||||
@@ -55,7 +110,25 @@ DHT22 требует подтягивающий резистор ~10кОм ме
|
|||||||
2. Открыть `growbox.ino` в Arduino IDE, выбрать плату (ESP32 Dev Module или
|
2. Открыть `growbox.ino` в Arduino IDE, выбрать плату (ESP32 Dev Module или
|
||||||
конкретную модель), порт.
|
конкретную модель), порт.
|
||||||
3. Прошить. Открыть Serial Monitor на 115200 бод — там лог подключения к
|
3. Прошить. Открыть Serial Monitor на 115200 бод — там лог подключения к
|
||||||
Wi-Fi/NTP/MQTT и каждой публикации телеметрии/команды.
|
Wi-Fi/NTP/MQTT/веб-сервера и каждой публикации телеметрии/команды, а
|
||||||
|
также IP платы для похода в веб-интерфейс.
|
||||||
|
|
||||||
|
### Сборка через `arduino-cli` (если не через IDE)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
arduino-cli core install esp32:esp32
|
||||||
|
arduino-cli lib install "PubSubClient" "ArduinoJson" "DHT sensor library"
|
||||||
|
arduino-cli compile --fqbn esp32:esp32:esp32 growbox
|
||||||
|
arduino-cli upload --fqbn esp32:esp32:esp32 -p /dev/cu.usbserial-XXXX growbox
|
||||||
|
```
|
||||||
|
|
||||||
|
На macOS `arduino-cli` по умолчанию ставит библиотеки в
|
||||||
|
`~/Documents/Arduino/libraries` — если этот каталог защищён sandbox'ом
|
||||||
|
(Operation not permitted при установке библиотек, встречалось в среде этого
|
||||||
|
агента), перенаправьте sketchbook в доступное место:
|
||||||
|
`arduino-cli config set directories.user ~/путь/без/ограничений`. Через саму
|
||||||
|
Arduino IDE (GUI-приложение) это обычно не требуется — она сама запрашивает
|
||||||
|
нужные разрешения у macOS.
|
||||||
|
|
||||||
## Проверка end-to-end
|
## Проверка end-to-end
|
||||||
|
|
||||||
|
|||||||
@@ -31,3 +31,9 @@
|
|||||||
|
|
||||||
// --- Телеметрия ---
|
// --- Телеметрия ---
|
||||||
#define TELEMETRY_INTERVAL_MS 30000UL
|
#define TELEMETRY_INTERVAL_MS 30000UL
|
||||||
|
|
||||||
|
// --- Веб-интерфейс ---
|
||||||
|
// 1 — поднимать HTTP-сервер на порту 80 (страница + /api/state, /api/command).
|
||||||
|
// 0 — полностью выключить (сервер не поднимается вообще, порт не слушается).
|
||||||
|
// Без авторизации — только для локальной сети, см. README.md.
|
||||||
|
#define ENABLE_WEB_UI 1
|
||||||
|
|||||||
@@ -70,3 +70,10 @@ bool devicesApplyCommand(const String& deviceId, const String& action, double le
|
|||||||
Serial.println(actuator->on ? "on" : "off");
|
Serial.println(actuator->on ? "on" : "off");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void devicesGetState(JsonObject& out) {
|
||||||
|
for (size_t i = 0; i < actuatorCount; i++) {
|
||||||
|
JsonObject deviceState = out[actuators[i].deviceId].to<JsonObject>();
|
||||||
|
deviceState["power"] = actuators[i].on ? "on" : "off";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,3 +12,8 @@ void devicesBegin();
|
|||||||
// применённым состоянием для последующей публикации в devices/{id}/ack.
|
// применённым состоянием для последующей публикации в devices/{id}/ack.
|
||||||
// Возвращает false, если deviceId этой плате не принадлежит.
|
// Возвращает false, если deviceId этой плате не принадлежит.
|
||||||
bool devicesApplyCommand(const String& deviceId, const String& action, double level, JsonDocument& outState);
|
bool devicesApplyCommand(const String& deviceId, const String& action, double level, JsonDocument& outState);
|
||||||
|
|
||||||
|
// Заполняет out текущим состоянием всех актуаторов этой платы:
|
||||||
|
// {"light-1":{"power":"on"},"pump-1":{"power":"off"},"fan-1":{"power":"off"}}.
|
||||||
|
// Нужно веб-интерфейсу, чтобы показать состояние без ожидания следующей команды.
|
||||||
|
void devicesGetState(JsonObject& out);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "net.h"
|
#include "net.h"
|
||||||
#include "telemetry.h"
|
#include "telemetry.h"
|
||||||
#include "time_sync.h"
|
#include "time_sync.h"
|
||||||
|
#include "web_ui.h"
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
@@ -18,6 +19,9 @@ void setup() {
|
|||||||
timeSyncBegin();
|
timeSyncBegin();
|
||||||
mqttBegin();
|
mqttBegin();
|
||||||
telemetryBegin();
|
telemetryBegin();
|
||||||
|
#if ENABLE_WEB_UI
|
||||||
|
webUiBegin();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
@@ -25,4 +29,7 @@ void loop() {
|
|||||||
mqttEnsureConnected();
|
mqttEnsureConnected();
|
||||||
mqttLoop();
|
mqttLoop();
|
||||||
telemetryTick();
|
telemetryTick();
|
||||||
|
#if ENABLE_WEB_UI
|
||||||
|
webUiHandle();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ static String deviceIdFromCommandTopic(const String& topic) {
|
|||||||
return topic.substring(8, topic.length() - 9); // strip "devices/" and "/commands"
|
return topic.substring(8, topic.length() - 9); // strip "devices/" and "/commands"
|
||||||
}
|
}
|
||||||
|
|
||||||
static void publishAck(const String& deviceId, JsonDocument& state) {
|
void mqttPublishAck(const String& deviceId, JsonDocument& state) {
|
||||||
JsonDocument ackDoc;
|
JsonDocument ackDoc;
|
||||||
ackDoc["state"] = state.as<JsonObject>();
|
ackDoc["state"] = state.as<JsonObject>();
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ static void onMessage(char* topic, byte* payload, unsigned int length) {
|
|||||||
|
|
||||||
JsonDocument state;
|
JsonDocument state;
|
||||||
if (devicesApplyCommand(deviceId, action, level, state)) {
|
if (devicesApplyCommand(deviceId, action, level, state)) {
|
||||||
publishAck(deviceId, state);
|
mqttPublishAck(deviceId, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
// Подключение к mosquitto, подписка на devices/+/commands, диспетчеризация
|
// Подключение к mosquitto, подписка на devices/+/commands, диспетчеризация
|
||||||
// команд в devices.cpp и публикация ack. См. протокол в README.md.
|
// команд в devices.cpp и публикация ack. См. протокол в README.md.
|
||||||
@@ -10,3 +11,10 @@ void mqttLoop();
|
|||||||
|
|
||||||
// Публикует одно показание телеметрии в devices/{deviceId}/telemetry.
|
// Публикует одно показание телеметрии в devices/{deviceId}/telemetry.
|
||||||
void mqttPublishTelemetry(const String& deviceId, const String& sensorType, double value);
|
void mqttPublishTelemetry(const String& deviceId, const String& sensorType, double value);
|
||||||
|
|
||||||
|
// Публикует ack {"state": ...} для deviceId. Используется и обработчиком
|
||||||
|
// MQTT-команд, и веб-интерфейсом — команда, отданная локально через веб-UI,
|
||||||
|
// должна так же обновлять device shadow платформы (Redis), как и команда
|
||||||
|
// из device-control-service, иначе состояние в UI Laravel разойдётся с
|
||||||
|
// реальным железом.
|
||||||
|
void mqttPublishAck(const String& deviceId, JsonDocument& state);
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
static DHT dht(PIN_DHT22, DHT22);
|
static DHT dht(PIN_DHT22, DHT22);
|
||||||
static unsigned long lastPublish = 0;
|
static unsigned long lastPublish = 0;
|
||||||
|
|
||||||
|
static bool hasReading = false;
|
||||||
|
static float lastTemperature = 0;
|
||||||
|
static float lastHumidity = 0;
|
||||||
|
|
||||||
void telemetryBegin() {
|
void telemetryBegin() {
|
||||||
dht.begin();
|
dht.begin();
|
||||||
}
|
}
|
||||||
@@ -32,6 +36,10 @@ void telemetryTick() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastTemperature = temperature;
|
||||||
|
lastHumidity = humidity;
|
||||||
|
hasReading = true;
|
||||||
|
|
||||||
mqttPublishTelemetry(DEVICE_ID_SENSOR, "temperature", temperature);
|
mqttPublishTelemetry(DEVICE_ID_SENSOR, "temperature", temperature);
|
||||||
mqttPublishTelemetry(DEVICE_ID_SENSOR, "humidity", humidity);
|
mqttPublishTelemetry(DEVICE_ID_SENSOR, "humidity", humidity);
|
||||||
|
|
||||||
@@ -40,3 +48,12 @@ void telemetryTick() {
|
|||||||
Serial.print(" humidity=");
|
Serial.print(" humidity=");
|
||||||
Serial.println(humidity);
|
Serial.println(humidity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool telemetryGetLast(float& temperature, float& humidity) {
|
||||||
|
if (!hasReading) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
temperature = lastTemperature;
|
||||||
|
humidity = lastHumidity;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,3 +6,8 @@ void telemetryBegin();
|
|||||||
// Вызывать на каждой итерации loop(): сама решает, пора ли публиковать
|
// Вызывать на каждой итерации loop(): сама решает, пора ли публиковать
|
||||||
// очередное показание (см. TELEMETRY_INTERVAL_MS в config.h).
|
// очередное показание (см. TELEMETRY_INTERVAL_MS в config.h).
|
||||||
void telemetryTick();
|
void telemetryTick();
|
||||||
|
|
||||||
|
// Последнее успешно прочитанное показание DHT22 (для веб-интерфейса —
|
||||||
|
// показывать данные не дожидаясь следующего цикла публикации).
|
||||||
|
// Возвращает false, если ни одного успешного чтения ещё не было.
|
||||||
|
bool telemetryGetLast(float& temperature, float& humidity);
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
#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" 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';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Локальный веб-интерфейс на самой плате: страница со статусом и кнопками
|
||||||
|
// управления + JSON API (GET /api/state, POST /api/command). Включается
|
||||||
|
// флагом ENABLE_WEB_UI в config.h (0, чтобы полностью выключить — тогда
|
||||||
|
// webUiBegin()/webUiHandle() не вызываются вовсе, HTTP-сервер не поднимается).
|
||||||
|
void webUiBegin();
|
||||||
|
void webUiHandle();
|
||||||
Reference in New Issue
Block a user