JXCT Soil Sensor 7-in-1 v3.4.9 (June 2025)
Professional IoT soil monitoring system with ESP32, Modbus RTU, MQTT, and advanced compensation algorithms
Загрузка...
Поиск...
Не найдено
routes_config.cpp
См. документацию.
1
6
12#include "../wifi_manager.h"
13#include <ArduinoJson.h>
15
16extern WebServer webServer;
18
19// Объявления внешних функций
20extern String navHtml();
21extern void loadConfig();
22extern void saveConfig();
23
24// --- API v1 helpers ---
25static void sendConfigExportJson();
26
28{
29 // Красивая страница интервалов и фильтров (оригинальный дизайн)
30 webServer.on(
31 "/intervals", HTTP_GET,
32 []()
33 {
34 logWebRequest("GET", "/intervals", webServer.client().remoteIP().toString());
35
37 {
38 webServer.send(200, "text/html; charset=utf-8", generateApModeUnavailablePage("Интервалы", UI_ICON_INTERVALS));
39 return;
40 }
41
42 String html = generatePageHeader("Интервалы и фильтры", UI_ICON_INTERVALS);
43 html += navHtml();
44 html += "<h1>" UI_ICON_INTERVALS " Настройка интервалов и фильтров</h1>";
45 html += "<form action='/save_intervals' method='post'>";
46
47 html += "<div class='section'><h2>📊 Интервалы опроса и публикации</h2>";
48 html += "<div class='form-group'><label for='sensor_interval'>Интервал опроса датчика (сек):</label>";
49 html += "<input type='number' id='sensor_interval' name='sensor_interval' min='1' max='300' value='" +
50 String(config.sensorReadInterval / 1000) + "' required>";
51 html += "<div class='help'>1-300 сек. Текущее: " + String(config.sensorReadInterval / 1000) +
52 " сек (по умолчанию: 1 сек)</div></div>";
53
54 html += "<div class='form-group'><label for='mqtt_interval'>Интервал MQTT публикации (мин):</label>";
55 html += "<input type='number' id='mqtt_interval' name='mqtt_interval' min='1' max='60' value='" +
56 String(config.mqttPublishInterval / 60000) + "' required>";
57 html += "<div class='help'>1-60 мин. Текущее: " + String(config.mqttPublishInterval / 60000) +
58 " мин</div></div>";
59
60 html += "<div class='form-group'><label for='ts_interval'>Интервал ThingSpeak (мин):</label>";
61 html += "<input type='number' id='ts_interval' name='ts_interval' min='5' max='120' value='" +
62 String(config.thingSpeakInterval / 60000) + "' required>";
63 html += "<div class='help'>5-120 мин. Текущее: " + String(config.thingSpeakInterval / 60000) +
64 " мин</div></div>";
65
66 html +=
67 "<div class='form-group'><label for='web_interval'>Интервал обновления веб-интерфейса (сек):</label>";
68 html += "<input type='number' id='web_interval' name='web_interval' min='5' max='60' value='" +
69 String(config.webUpdateInterval / 1000) + "' required>";
70 html += "<div class='help'>5-60 сек. Текущее: " + String(config.webUpdateInterval / 1000) +
71 " сек</div></div></div>";
72
73 html += "<div class='section'><h2>🎯 Пороги дельта-фильтра</h2>";
74 html += "<div class='form-group'><label for='delta_temp'>Порог температуры (°C):</label>";
75 html += "<input type='number' id='delta_temp' name='delta_temp' min='0.1' max='5.0' step='0.1' value='" +
76 String(config.deltaTemperature) + "' required>";
77 html += "<div class='help'>0.1-5.0°C. Публикация при изменении более чем на это значение</div></div>";
78
79 html += "<div class='form-group'><label for='delta_hum'>Порог влажности (%):</label>";
80 html += "<input type='number' id='delta_hum' name='delta_hum' min='0.5' max='10.0' step='0.5' value='" +
81 String(config.deltaHumidity) + "' required>";
82 html += "<div class='help'>0.5-10.0%. Публикация при изменении более чем на это значение</div></div>";
83
84 html += "<div class='form-group'><label for='delta_ph'>Порог pH:</label>";
85 html += "<input type='number' id='delta_ph' name='delta_ph' min='0.01' max='1.0' step='0.01' value='" +
86 String(config.deltaPh) + "' required>";
87 html += "<div class='help'>0.01-1.0. Публикация при изменении более чем на это значение</div></div>";
88
89 html += "<div class='form-group'><label for='delta_ec'>Порог EC (µS/cm):</label>";
90 html += "<input type='number' id='delta_ec' name='delta_ec' min='10' max='500' value='" +
91 String((int)config.deltaEc) + "' required>";
92 html += "<div class='help'>10-500 µS/cm. Публикация при изменении более чем на это значение</div></div>";
93
94 html += "<div class='form-group'><label for='delta_npk'>Порог NPK (mg/kg):</label>";
95 html += "<input type='number' id='delta_npk' name='delta_npk' min='1' max='50' value='" +
96 String((int)config.deltaNpk) + "' required>";
97 html +=
98 "<div class='help'>1-50 mg/kg. Публикация при изменении более чем на это значение</div></div></div>";
99
100 html += "<div class='section'><h2>📈 Скользящее среднее</h2>";
101 html += "<div class='form-group'><label for='avg_window'>Размер окна усреднения:</label>";
102 html += "<input type='number' id='avg_window' name='avg_window' min='5' max='15' value='" +
103 String(config.movingAverageWindow) + "' required>";
104 html += "<div class='help'>5-15 измерений. Больше = плавнее, но медленнее реакция</div></div>";
105
106 html += "<div class='form-group'><label for='force_cycles'>Принудительная публикация (циклов):</label>";
107 html += "<input type='number' id='force_cycles' name='force_cycles' min='5' max='50' value='" +
108 String(config.forcePublishCycles) + "' required>";
109 html += "<div class='help'>5-50 циклов. Публикация каждые N циклов даже без изменений</div></div>";
110
111 // Новые настройки алгоритма и фильтра выбросов
112 html += "<div class='form-group'><label for='filter_algo'>Алгоритм обработки данных:</label>";
113 html += "<select id='filter_algo' name='filter_algo' required>";
114 html += "<option value='0'" + String(config.filterAlgorithm == 0 ? " selected" : "") +
115 ">Среднее арифметическое</option>";
116 html += "<option value='1'" + String(config.filterAlgorithm == 1 ? " selected" : "") +
117 ">Медианное значение</option>";
118 html += "</select>";
119 html += "<div class='help'>Среднее - быстрее, медиана - устойчивее к выбросам</div></div>";
120
121 html += "<div class='form-group'><label for='outlier_filter'>Фильтр выбросов >2σ:</label>";
122 html += "<select id='outlier_filter' name='outlier_filter' required>";
123 html += "<option value='0'" + String(config.outlierFilterEnabled == 0 ? " selected" : "") +
124 ">Отключен</option>";
125 html +=
126 "<option value='1'" + String(config.outlierFilterEnabled == 1 ? " selected" : "") + ">Включен</option>";
127 html += "</select>";
128 html +=
129 "<div class='help'>Автоматически отбрасывает измерения, отклоняющиеся более чем на 2 "
130 "сигма</div></div></div>";
131
132 html += generateButton(ButtonType::PRIMARY, UI_ICON_SAVE, "Сохранить настройки", "");
133 html +=
134 generateButton(ButtonType::SECONDARY, UI_ICON_RESET, "Сбросить к умолчанию (1 сек + мин. фильтрация)",
135 "location.href='/reset_intervals'");
136 html += "</form>";
137 html += generatePageFooter();
138
139 webServer.send(200, "text/html; charset=utf-8", html);
140 });
141
142 // Обработчик сохранения настроек интервалов
143 webServer.on("/save_intervals", HTTP_POST,
144 []()
145 {
146 logWebRequest("POST", "/save_intervals", webServer.client().remoteIP().toString());
147
149 {
150 webServer.send(403, "text/plain", "Недоступно в режиме точки доступа");
151 return;
152 }
153
154 // Сохраняем интервалы (с конвертацией в миллисекунды)
155 config.sensorReadInterval = webServer.arg("sensor_interval").toInt() * 1000; // сек -> мс
156 config.mqttPublishInterval = webServer.arg("mqtt_interval").toInt() * 60000; // мин -> мс
157 config.thingSpeakInterval = webServer.arg("ts_interval").toInt() * 60000; // мин -> мс
158 config.webUpdateInterval = webServer.arg("web_interval").toInt() * 1000; // сек -> мс
159
160 // Сохраняем пороги дельта-фильтра
161 config.deltaTemperature = webServer.arg("delta_temp").toFloat();
162 config.deltaHumidity = webServer.arg("delta_hum").toFloat();
163 config.deltaPh = webServer.arg("delta_ph").toFloat();
164 config.deltaEc = webServer.arg("delta_ec").toFloat();
165 config.deltaNpk = webServer.arg("delta_npk").toFloat();
166
167 // Сохраняем настройки скользящего среднего
168 config.movingAverageWindow = webServer.arg("avg_window").toInt();
169 config.forcePublishCycles = webServer.arg("force_cycles").toInt();
170
171 // Сохраняем новые настройки алгоритма и фильтра выбросов
172 config.filterAlgorithm = webServer.arg("filter_algo").toInt();
173 config.outlierFilterEnabled = webServer.arg("outlier_filter").toInt();
174
175 // Сохраняем в NVS
176 saveConfig();
177
178 String html =
179 "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta http-equiv='refresh' "
180 "content='3;url=/intervals'>";
181 html += "<title>" UI_ICON_SUCCESS " Настройки сохранены</title>";
182 html += "<style>" + String(getUnifiedCSS()) + "</style></head><body><div class='container'>";
183 html += "<h1>" UI_ICON_SUCCESS " Настройки интервалов сохранены!</h1>";
184 html += "<div class='msg msg-success'>" UI_ICON_SUCCESS " Новые настройки вступили в силу</div>";
185 html += "<p><strong>Текущие интервалы:</strong><br>";
186 html += "📊 Датчик: " + String(config.sensorReadInterval / 1000) + " сек<br>";
187 html += "📡 MQTT: " + String(config.mqttPublishInterval / 60000) + " мин<br>";
188 html += "📈 ThingSpeak: " + String(config.thingSpeakInterval / 60000) + " мин</p>";
189 html += "<p><em>Возврат к настройкам через 3 секунды...</em></p>";
190 html += "</div>" + String(getToastHTML()) + "</body></html>";
191 webServer.send(200, "text/html; charset=utf-8", html);
192 });
193
194 // Сброс интервалов к умолчанию
195 webServer.on("/reset_intervals", HTTP_GET,
196 []()
197 {
198 logWebRequest("GET", "/reset_intervals", webServer.client().remoteIP().toString());
199
201 {
202 webServer.send(403, "text/plain", "Недоступно в режиме точки доступа");
203 return;
204 }
205
206 // Сбрасываем к умолчанию (МИНИМАЛЬНАЯ ФИЛЬТРАЦИЯ + ЧАСТЫЙ MQTT)
207 config.sensorReadInterval = SENSOR_READ_INTERVAL;
208 config.mqttPublishInterval = MQTT_PUBLISH_INTERVAL;
209 config.thingSpeakInterval = THINGSPEAK_INTERVAL;
210 config.webUpdateInterval = WEB_UPDATE_INTERVAL;
211 config.deltaTemperature = DELTA_TEMPERATURE; // 0.1°C
212 config.deltaHumidity = DELTA_HUMIDITY; // 0.5%
213 config.deltaPh = DELTA_PH; // 0.01 pH
214 config.deltaEc = DELTA_EC; // 10 µS/cm
215 config.deltaNpk = DELTA_NPK; // 1 mg/kg
216 config.movingAverageWindow = 5; // минимальное окно
217 config.forcePublishCycles = FORCE_PUBLISH_CYCLES; // каждые 5 циклов
218 config.filterAlgorithm = 0; // среднее
219 config.outlierFilterEnabled = 0; // отключен
220
221 saveConfig();
222
223 String html =
224 "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta http-equiv='refresh' "
225 "content='2;url=/intervals'>";
226 html += "<title>" UI_ICON_RESET " Сброс настроек</title>";
227 html += "<style>" + String(getUnifiedCSS()) + "</style></head><body><div class='container'>";
228 html += "<h1>" UI_ICON_RESET " Настройки сброшены</h1>";
229 html += "<div class='msg msg-success'>" UI_ICON_SUCCESS
230 " Настройки интервалов возвращены к значениям по умолчанию</div>";
231 html += "<p><em>Возврат к настройкам через 2 секунды...</em></p>";
232 html += "</div>" + String(getToastHTML()) + "</body></html>";
233 webServer.send(200, "text/html; charset=utf-8", html);
234 });
235
236 // Страница управления конфигурацией
237 webServer.on("/config_manager", HTTP_GET,
238 []()
239 {
240 logWebRequest("GET", "/config_manager", webServer.client().remoteIP().toString());
241
243 {
244 String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>" UI_ICON_FOLDER
245 " Конфигурация</title>";
246 html += "<style>" + String(getUnifiedCSS()) + "</style></head><body><div class='container'>";
247 html += "<h1>" UI_ICON_FOLDER " Конфигурация</h1>";
248 html += "<div class='msg msg-error'>" UI_ICON_ERROR
249 " Недоступно в режиме точки доступа</div></div></body></html>";
250 webServer.send(200, "text/html; charset=utf-8", html);
251 return;
252 }
253
254 String html =
255 "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' "
256 "content='width=device-width, initial-scale=1.0'>";
257 html += "<title>" UI_ICON_FOLDER " Управление конфигурацией JXCT</title>";
258 html += "<style>" + String(getUnifiedCSS()) + "</style></head><body><div class='container'>";
259 html += navHtml();
260 html += "<h1>" UI_ICON_FOLDER " Управление конфигурацией</h1>";
261
262 if (webServer.hasArg("import_ok")) {
263 html += "<div class='msg msg-success'>✅ Конфигурация успешно импортирована и сохранена</div>";
264 }
265
266 html += "<div class='section'>";
267 html += "<h2>📤 Экспорт конфигурации</h2>";
268 html += "<p>Скачайте текущую конфигурацию в формате JSON (пароли заменены на заглушки):</p>";
269 html += generateButton(ButtonType::PRIMARY, "📥", "Скачать конфигурацию",
270 "location.href='/api/v1/config/export'");
271 html += "</div>";
272
273 html += "<div class='section'>";
274 html += "<h2>📥 Импорт конфигурации</h2>";
275 html += "<p>Загрузите файл конфигурации для восстановления настроек:</p>";
276 html += "<form action='/api/config/import' method='post' enctype='multipart/form-data'>";
277 html += "<input type='file' name='config' accept='.json' required>";
278 html += generateButton(ButtonType::SECONDARY, "📤", "Загрузить конфигурацию", "");
279 html += "</form>";
280 html += "</div>";
281
282 html += "</div>" + String(getToastHTML()) + "</body></html>";
283 webServer.send(200, "text/html; charset=utf-8", html);
284 });
285
286 // API v1 конфигурация
288
289 // Импорт конфигурации через multipart/form-data (файл JSON)
290 static String importedJson;
291 webServer.on(
292 "/api/config/import", HTTP_POST,
293 // Финальный обработчик после загрузки
294 []()
295 {
296 logWebRequest("POST", "/api/config/import", webServer.client().remoteIP().toString());
297
299 {
300 webServer.send(403, "application/json", "{\"error\":\"Недоступно в режиме AP\"}");
301 importedJson = "";
302 return;
303 }
304
305 // Парсим накопленный JSON
306 StaticJsonDocument<2048> doc;
307 DeserializationError err = deserializeJson(doc, importedJson);
308 if (err)
309 {
310 String resp = String("{\"error\":\"Ошибка JSON: ") + err.c_str() + "\"}";
311 webServer.send(400, "application/json", resp);
312 importedJson = "";
313 return;
314 }
315
316 // --- Применяем конфигурацию --- (минимальный набор, расширяйте по необходимости)
317 if (doc.containsKey("wifi"))
318 {
319 JsonObject wifi = doc["wifi"];
320 strlcpy(config.ssid, wifi["ssid"].as<const char*>(), sizeof(config.ssid));
321 strlcpy(config.password, wifi["password"].as<const char*>(), sizeof(config.password));
322 }
323
324 if (doc.containsKey("mqtt"))
325 {
326 JsonObject mqtt = doc["mqtt"];
327 config.flags.mqttEnabled = mqtt["enabled"].as<bool>();
328 strlcpy(config.mqttServer, mqtt["server"].as<const char*>(), sizeof(config.mqttServer));
329 config.mqttPort = mqtt["port"].as<int>();
330 strlcpy(config.mqttUser, mqtt["user"].as<const char*>(), sizeof(config.mqttUser));
331 strlcpy(config.mqttPassword, mqtt["password"].as<const char*>(), sizeof(config.mqttPassword));
332 }
333
334 // TODO: обработать intervals, filters, device и др.
335
336 saveConfig();
337 importedJson = "";
338
339 // Отправляем 303 Redirect, чтобы браузер вернулся к менеджеру конфигурации
340 webServer.sendHeader("Location", "/config_manager?import_ok=1", true);
341 webServer.send(303, "text/plain", "Redirect");
342 },
343 // uploadHandler: накапливаем файл
344 []()
345 {
346 HTTPUpload& up = webServer.upload();
347 if (up.status == UPLOAD_FILE_START)
348 {
349 importedJson = "";
350 }
351 else if (up.status == UPLOAD_FILE_WRITE)
352 {
353 importedJson += String((const char*)up.buf, up.currentSize);
354 }
355 else if (up.status == UPLOAD_FILE_END)
356 {
357 // ничего, финальное действие в основном хендлере
358 }
359 });
360
361 logDebug("Маршруты конфигурации настроены: /intervals, /config_manager, /api/v1/config/export");
362}
363
364// ---------------------------------------------------------------------------
365// API v1: /api/v1/config/export
366// ---------------------------------------------------------------------------
368{
369 logWebRequest("GET", webServer.uri(), webServer.client().remoteIP().toString());
370
372 {
373 webServer.send(403, "application/json", "{\"error\":\"Недоступно в режиме точки доступа\"}");
374 return;
375 }
376
377 StaticJsonDocument<1024> root;
378
379 // MQTT
380 JsonObject mqtt = root.createNestedObject("mqtt");
381 mqtt["enabled"] = (bool)config.flags.mqttEnabled;
382 mqtt["server"] = "YOUR_MQTT_SERVER_HERE";
383 mqtt["port"] = config.mqttPort;
384 mqtt["user"] = "YOUR_MQTT_USER_HERE";
385 mqtt["password"] = "YOUR_MQTT_PASSWORD_HERE";
386
387 // ThingSpeak
388 JsonObject ts = root.createNestedObject("thingspeak");
389 ts["enabled"] = (bool)config.flags.thingSpeakEnabled;
390 ts["channel_id"] = "YOUR_CHANNEL_ID_HERE";
391 ts["api_key"] = "YOUR_API_KEY_HERE";
392
393 // Intervals
394 JsonObject intervals = root.createNestedObject("intervals");
395 intervals["sensor_read"] = config.sensorReadInterval;
396 intervals["mqtt_publish"] = config.mqttPublishInterval;
397 intervals["thingspeak"] = config.thingSpeakInterval;
398 intervals["web_update"] = config.webUpdateInterval;
399
400 // Filters
401 JsonObject filters = root.createNestedObject("filters");
402 filters["delta_temperature"] = config.deltaTemperature;
403 filters["delta_humidity"] = config.deltaHumidity;
404 filters["delta_ph"] = config.deltaPh;
405 filters["delta_ec"] = config.deltaEc;
406 filters["delta_npk"] = config.deltaNpk;
407 filters["moving_average_window"] = config.movingAverageWindow;
408 filters["force_publish_cycles"] = config.forcePublishCycles;
409 filters["filter_algorithm"] = config.filterAlgorithm;
410 filters["outlier_filter_enabled"] = config.outlierFilterEnabled;
411
412 // Device flags
413 JsonObject device = root.createNestedObject("device");
414 device["use_real_sensor"] = (bool)config.flags.useRealSensor;
415 device["hass_enabled"] = (bool)config.flags.hassEnabled;
416
417 root["export_timestamp"] = millis();
418
419 String json;
420 serializeJson(root, json);
421
422 webServer.sendHeader("Content-Disposition", "attachment; filename=\"jxct_config_" + String(millis()) + ".json\"");
423 webServer.send(200, "application/json", json);
424}
Config config
Определения config.cpp:34
void logWebRequest(const String &method, const String &uri, const String &clientIP)
Логирование веб-запросов
Определения error_handlers.cpp:152
#define API_CONFIG_EXPORT
#define WEB_UPDATE_INTERVAL
Определения jxct_config_vars.h:12
#define MQTT_PUBLISH_INTERVAL
Определения jxct_config_vars.h:10
#define DELTA_EC
Определения jxct_config_vars.h:30
#define FORCE_PUBLISH_CYCLES
Определения jxct_config_vars.h:32
#define SENSOR_READ_INTERVAL
Определения jxct_config_vars.h:9
#define DELTA_NPK
Определения jxct_config_vars.h:31
#define DELTA_PH
Определения jxct_config_vars.h:29
#define DELTA_HUMIDITY
Определения jxct_config_vars.h:28
#define DELTA_TEMPERATURE
Определения jxct_config_vars.h:27
#define THINGSPEAK_INTERVAL
Определения jxct_config_vars.h:11
const char * getUnifiedCSS()
Определения jxct_ui_system.cpp:4
String generateButton(ButtonType type, const char *icon, const char *text, const char *action)
Определения jxct_ui_system.cpp:286
const char * getToastHTML()
Определения jxct_ui_system.cpp:320
#define UI_ICON_INTERVALS
Определения jxct_ui_system.h:47
#define UI_ICON_ERROR
Определения jxct_ui_system.h:53
#define UI_ICON_SUCCESS
Определения jxct_ui_system.h:52
#define UI_ICON_FOLDER
Определения jxct_ui_system.h:57
#define UI_ICON_RESET
Определения jxct_ui_system.h:43
#define UI_ICON_SAVE
Определения jxct_ui_system.h:42
@ SECONDARY
Определения jxct_ui_system.h:66
@ PRIMARY
Определения jxct_ui_system.h:65
void logDebug(const char *format,...)
Определения logger.cpp:112
Система логгирования с красивым форматированием
WebServer webServer
static void sendConfigExportJson()
Определения routes_config.cpp:367
void setupConfigRoutes()
Настройка маршрутов конфигурации (/intervals, /config_manager, /api/config/*)
Определения routes_config.cpp:27
void saveConfig()
Определения config.cpp:128
String navHtml()
Определения wifi_manager.cpp:82
void loadConfig()
Определения config.cpp:39
WiFiMode currentWiFiMode
Определения wifi_manager.cpp:26
String generateApModeUnavailablePage(const String &title, const String &icon)
Генерация страницы "Недоступно в AP режиме".
Определения web_templates.cpp:175
String generatePageHeader(const String &title, const String &icon)
Генерация заголовка HTML страницы
Определения web_templates.cpp:8
String generatePageFooter()
Генерация футера HTML страницы
Определения web_templates.cpp:19
WiFiMode
Определения wifi_manager.h:12
@ AP
Определения wifi_manager.h:13