21#define RESET_BUTTON_PIN 0
22#define WIFI_RECONNECT_INTERVAL 30000
71 unsigned long now = millis();
84 String html =
"<div class='nav'>";
88 html +=
"<a href='/readings'>" UI_ICON_DATA " Показания</a>";
91 html +=
"<a href='/config_manager'>" UI_ICON_FOLDER " Конфигурация</a>";
92 html +=
"<a href='/updates'>🚀 Обновления</a>";
107 WiFi.disconnect(
true);
114 logDebug(
"Password: %s", strlen(
config.password) > 0 ?
"задан" :
"не задан");
116 if (strlen(
config.ssid) > 0 && strlen(
config.password) > 0)
118 logWiFi(
"Переход в режим STA (клиент)");
123 logWiFi(
"Переход в режим AP (точка доступа)");
138 static unsigned long lastStaRetry = 0;
139 if (WiFi.softAPgetStationNum() == 0 &&
143 lastStaRetry = millis();
144 logWiFi(
"AP режим: пробуем снова подключиться к WiFi \"%s\"",
config.ssid);
149 if (WiFi.softAPgetStationNum() > 0)
160 static unsigned long lastReconnectAttempt = 0;
161 static int reconnectAttempts = 0;
162 const int MAX_RECONNECT_ATTEMPTS = 3;
164 if (WiFi.status() != WL_CONNECTED)
171 if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS)
173 logWarn(
"Потеряно соединение с WiFi, попытка переподключения %d из %d",
174 reconnectAttempts + 1, MAX_RECONNECT_ATTEMPTS);
176 WiFi.disconnect(
true);
180 lastReconnectAttempt = millis();
185 logError(
"Не удалось восстановить соединение после %d попыток, переход в AP",
186 MAX_RECONNECT_ATTEMPTS);
188 reconnectAttempts = 0;
197 reconnectAttempts = 0;
199 logSuccess(
"Подключено к WiFi, IP: %s", WiFi.localIP().toString().c_str());
209 WiFi.macAddress(mac);
211 snprintf(buf,
sizeof(buf),
"jxct-%02X%02X%02X", mac[3], mac[4], mac[5]);
212 for (
int i = 0; buf[i]; ++i) buf[i] = tolower(buf[i]);
223 dnsServer.start(53,
"*", WiFi.softAPIP());
226 logWiFi(
"Режим точки доступа запущен");
228 logSystem(
"IP адрес: %s", WiFi.softAPIP().toString().c_str());
234 WiFi.disconnect(
true);
239 WiFi.setHostname(hostname.c_str());
241 if (strlen(
config.ssid) > 0)
243 logWiFi(
"Подключение к WiFi...");
248 unsigned long startTime = millis();
250 while (WiFi.status() != WL_CONNECTED &&
262 logWarn(
"Обнаружено длительное нажатие кнопки во время подключения");
268 if (WiFi.status() == WL_CONNECTED)
273 logSystem(
"IP адрес: %s", WiFi.localIP().toString().c_str());
274 logSystem(
"MAC адрес: %s", WiFi.macAddress().c_str());
275 logSystem(
"Hostname: %s", hostname.c_str());
286 unsigned long ntpStart = millis();
287 while (!
timeClient->forceUpdate() && millis() - ntpStart < 5000)
298 logError(
"Не удалось подключиться к WiFi после %d попыток", attempts);
304 logWarn(
"SSID не задан, переход в AP");
311 static unsigned long pressStart = 0;
312 static bool wasPressed =
false;
314 if (isPressed && !wasPressed)
316 pressStart = millis();
320 else if (!isPressed && wasPressed)
326 else if (isPressed && wasPressed)
328 if (millis() - pressStart >= 5000)
338 logWarn(
"Перезагрузка ESP32...");
345 String html =
"<!DOCTYPE html><html><head><meta charset='UTF-8'>";
347 html +=
"<style>" + String(
getUnifiedCSS()) +
"</style></head><body><div class='container'>";
350 html +=
"<div class='section'><h2>WiFi</h2><ul>";
354 html +=
"<li>SSID: " + String(
config.ssid) +
"</li>";
355 html +=
"<li>IP: " + WiFi.localIP().toString() +
"</li>";
356 html +=
"<li>RSSI: " + String(WiFi.RSSI()) +
" dBm</li>";
358 html +=
"</ul></div>";
359 html +=
"<div class='section'><h2>Система</h2><ul>";
361 html +=
"<li>Время работы: " + String(millis() / 1000) +
" сек</li>";
362 html +=
"<li>Свободная память: " + String(ESP.getFreeHeap()) +
" байт</li>";
363 html +=
"</ul></div>";
364 html +=
"</div>" + String(
getToastHTML()) +
"</body></html>";
365 webServer.send(200,
"text/html; charset=utf-8", html);
370 logInfo(
"🏗️ Настройка модульного веб-сервера v2.4.5...");
390 logSystem(
"✅ Активные модули: main, data, config, service, ota, error_handlers");
391 logSystem(
"📋 Полный набор маршрутов готов к использованию");
397 "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, "
398 "initial-scale=1.0'>";
400 html +=
"<style>" + String(
getUnifiedCSS()) +
"</style></head><body><div class='container'>";
403 html +=
"<form action='/save' method='post'>";
404 html +=
"<div class='section'><h2>WiFi настройки</h2>";
405 html +=
"<div class='form-group'><label for='ssid'>SSID:</label><input type='text' id='ssid' name='ssid' value='" +
406 String(
config.ssid) +
"' required></div>";
408 "<div class='form-group'><label for='password'>Пароль:</label><input type='password' id='password' "
409 "name='password' value='" +
410 String(
config.password) +
"' required></div></div>";
415 String mqttChecked =
config.flags.mqttEnabled ?
" checked" :
"";
416 html +=
"<div class='section'><h2>MQTT настройки</h2>";
418 "<div class='form-group'><label for='mqtt_enabled'>Включить MQTT:</label><input type='checkbox' "
419 "id='mqtt_enabled' name='mqtt_enabled'" +
420 mqttChecked +
"></div>";
422 "<div class='form-group'><label for='mqtt_server'>MQTT сервер:</label><input type='text' id='mqtt_server' "
423 "name='mqtt_server' value='" +
424 String(
config.mqttServer) +
"'" + (
config.flags.mqttEnabled ?
" required" :
"") +
"></div>";
426 "<div class='form-group'><label for='mqtt_port'>MQTT порт:</label><input type='text' id='mqtt_port' "
427 "name='mqtt_port' value='" +
428 String(
config.mqttPort) +
"'></div>";
430 "<div class='form-group'><label for='mqtt_user'>MQTT пользователь:</label><input type='text' "
431 "id='mqtt_user' name='mqtt_user' value='" +
432 String(
config.mqttUser) +
"'></div>";
434 "<div class='form-group'><label for='mqtt_password'>MQTT пароль:</label><input type='password' "
435 "id='mqtt_password' name='mqtt_password' value='" +
436 String(
config.mqttPassword) +
"'></div>";
437 String hassChecked =
config.flags.hassEnabled ?
" checked" :
"";
439 "<div class='form-group'><label for='hass_enabled'>Интеграция с Home Assistant:</label><input "
440 "type='checkbox' id='hass_enabled' name='hass_enabled'" +
441 hassChecked +
"></div></div>";
442 String tsChecked =
config.flags.thingSpeakEnabled ?
" checked" :
"";
443 html +=
"<div class='section'><h2>ThingSpeak настройки</h2>";
445 "<div class='form-group'><label for='ts_enabled'>Включить ThingSpeak:</label><input type='checkbox' "
446 "id='ts_enabled' name='ts_enabled'" +
447 tsChecked +
"></div>";
449 "<div class='form-group'><label for='ts_api_key'>API ключ:</label><input type='text' id='ts_api_key' "
450 "name='ts_api_key' value='" +
451 String(
config.thingSpeakApiKey) +
"'" + (
config.flags.thingSpeakEnabled ?
" required" :
"") +
"></div>";
453 "<div class='form-group'><label for='ts_channel_id'>Channel ID:</label><input type='text' "
454 "id='ts_channel_id' name='ts_channel_id' value='" +
455 String(
config.thingSpeakChannelId) +
"'></div>";
457 "<div style='color:#888;font-size:13px'>💡 Интервал публикации настраивается в разделе <a "
458 "href='/intervals' style='color:#4CAF50'>Интервалы</a></div></div>";
459 String realSensorChecked =
config.flags.useRealSensor ?
" checked" :
"";
460 html +=
"<div class='section'><h2>Датчик</h2>";
462 "<div class='form-group'><label for='real_sensor'>Реальный датчик:</label><input type='checkbox' "
463 "id='real_sensor' name='real_sensor'" +
464 realSensorChecked +
"></div>";
467 html +=
"<div class='section'><h2>⚙️ Компенсация датчиков</h2>";
468 String calibChecked =
config.flags.calibrationEnabled ?
" checked" :
"";
469 html +=
"<div class='form-group'><label for='cal_enabled'>Включить компенсацию:</label><input type='checkbox' id='cal_enabled' name='cal_enabled'" + calibChecked +
"></div>";
470 html +=
"<div class='form-group'><label for='irrig_th'>Порог ∆влажности (%):</label><input type='number' step='0.1' id='irrig_th' name='irrig_th' value='" + String(
config.irrigationSpikeThreshold,1) +
"'></div>";
471 html +=
"<div class='form-group'><label for='irrig_hold'>Удержание (мин):</label><input type='number' id='irrig_hold' name='irrig_hold' value='" + String(
config.irrigationHoldMinutes) +
"'></div>";
475 html +=
"<div class='section'><h2>🌱 Агрорекомендации</h2>";
477 html +=
"<div class='form-group'><label for='latitude'>Широта:</label><input type='number' step='0.0001' id='latitude' name='latitude' value='" + String(
config.latitude,4) +
"'></div>";
478 html +=
"<div class='form-group'><label for='longitude'>Долгота:</label><input type='number' step='0.0001' id='longitude' name='longitude' value='" + String(
config.longitude,4) +
"'></div>";
480 html +=
"<div class='form-group'><label for='crop'>Культура:</label><select id='crop' name='crop'>";
481 html += String(
"<option value='none'") + (strcmp(
config.cropId,
"none")==0?
" selected":
"") +
">нет</option>";
482 html += String(
"<option value='tomato'") + (strcmp(
config.cropId,
"tomato")==0?
" selected":
"") +
">Томат</option>";
483 html += String(
"<option value='cucumber'") + (strcmp(
config.cropId,
"cucumber")==0?
" selected":
"") +
">Огурец</option>";
484 html += String(
"<option value='pepper'") + (strcmp(
config.cropId,
"pepper")==0?
" selected":
"") +
">Перец</option>";
485 html += String(
"<option value='lettuce'") + (strcmp(
config.cropId,
"lettuce")==0?
" selected":
"") +
">Салат</option>";
486 html += String(
"<option value='strawberry'") + (strcmp(
config.cropId,
"strawberry")==0?
" selected":
"") +
">Клубника</option>";
487 html += String(
"<option value='apple'") + (strcmp(
config.cropId,
"apple")==0?
" selected":
"") +
">Яблоня</option>";
488 html += String(
"<option value='pear'") + (strcmp(
config.cropId,
"pear")==0?
" selected":
"") +
">Груша</option>";
489 html += String(
"<option value='cherry'") + (strcmp(
config.cropId,
"cherry")==0?
" selected":
"") +
">Вишня/Черешня</option>";
490 html += String(
"<option value='raspberry'") + (strcmp(
config.cropId,
"raspberry")==0?
" selected":
"") +
">Малина</option>";
491 html += String(
"<option value='currant'") + (strcmp(
config.cropId,
"currant")==0?
" selected":
"") +
">Смородина</option>";
492 html += String(
"<option value='blueberry'") + (strcmp(
config.cropId,
"blueberry")==0?
" selected":
"") +
">Голубика</option>";
493 html += String(
"<option value='lawn'") + (strcmp(
config.cropId,
"lawn")==0?
" selected":
"") +
">Газон</option>";
494 html += String(
"<option value='grape'") + (strcmp(
config.cropId,
"grape")==0?
" selected":
"") +
">Виноград</option>";
495 html += String(
"<option value='conifer'") + (strcmp(
config.cropId,
"conifer")==0?
" selected":
"") +
">Хвойные деревья</option>";
496 html +=
"</select></div>";
498 String selectedEnvOutdoor =
config.environmentType == 0 ?
" selected" :
"";
499 String selectedEnvGreenhouse =
config.environmentType == 1 ?
" selected" :
"";
500 String selectedEnvIndoor =
config.environmentType == 2 ?
" selected" :
"";
501 html +=
"<div class='form-group'><label for='env_type'>Среда:</label><select id='env_type' name='env_type'>";
502 html += String(
"<option value='0'") + selectedEnvOutdoor +
">Открытый грунт</option>";
503 html += String(
"<option value='1'") + selectedEnvGreenhouse +
">Теплица</option>";
504 html += String(
"<option value='2'") + selectedEnvIndoor +
">Комнатная</option></select></div>";
507 String seasonalChecked =
config.flags.seasonalAdjustEnabled ?
" checked" :
"";
508 html +=
"<div class='form-group'><label for='season_adj'>Учитывать сезонность:</label><input type='checkbox' id='season_adj' name='season_adj'" + seasonalChecked +
"></div>";
511 const char* selectedSand =
config.soilProfile == 0 ?
" selected" :
"";
512 const char* selectedLoam =
config.soilProfile == 1 ?
" selected" :
"";
513 const char* selectedPeat =
config.soilProfile == 2 ?
" selected" :
"";
514 const char* selectedClay =
config.soilProfile == 3 ?
" selected" :
"";
515 const char* selectedSandPeat =
config.soilProfile == 4 ?
" selected" :
"";
516 html +=
"<div class='form-group'><label for='soil_profile_sel'>Профиль почвы:</label><select id='soil_profile_sel' name='soil_profile_sel'>";
517 html += String(
"<option value='0'") + selectedSand +
">Песок</option>";
518 html += String(
"<option value='1'") + selectedLoam +
">Суглинок</option>";
519 html += String(
"<option value='2'") + selectedPeat +
">Торф</option>";
520 html += String(
"<option value='3'") + selectedClay +
">Глина</option>";
521 html += String(
"<option value='4'") + selectedSandPeat +
">Песчано-торфяной</option>";
522 html +=
"</select></div>";
527 html +=
"<div class='section'><h2>NTP</h2>";
529 "<div class='form-group'><label for='ntp_server'>NTP сервер:</label><input type='text' id='ntp_server' "
530 "name='ntp_server' value='" +
531 String(
config.ntpServer) +
"' required></div>";
533 "<div class='form-group'><label for='ntp_interval'>Интервал обновления NTP (мс):</label><input "
534 "type='number' id='ntp_interval' name='ntp_interval' min='10000' max='86400000' value='" +
535 String(
config.ntpUpdateInterval) +
"'></div></div>";
543 html +=
"document.getElementById('mqtt_enabled').addEventListener('change', function() {";
544 html +=
" document.getElementById('mqtt_server').required = this.checked;";
546 html +=
"document.getElementById('ts_enabled').addEventListener('change', function() {";
547 html +=
" document.getElementById('ts_api_key').required = this.checked;";
552 html +=
"</div>" + String(
getToastHTML()) +
"</body></html>";
553 webServer.send(200,
"text/html; charset=utf-8", html);
void setupErrorHandlers()
Настройка обработчиков ошибок (404, 500, и т.
#define WIFI_RETRY_DELAY_MS
Централизованные константы системы JXCT.
constexpr const char * JXCT_WIFI_AP_PASS
constexpr int DEFAULT_WEB_SERVER_PORT
constexpr unsigned long WIFI_CONNECTION_TIMEOUT
constexpr int WIFI_CONNECTION_ATTEMPTS
const char * getUnifiedCSS()
String generateButton(ButtonType type, const char *icon, const char *text, const char *action)
const char * getToastHTML()
#define UI_ICON_INTERVALS
void logDebug(const char *format,...)
void logWarn(const char *format,...)
void logPrintHeader(const char *title, const char *color)
void logPrintSeparator(const char *symbol, int length)
void logSuccess(const char *format,...)
void logError(const char *format,...)
void logSystem(const char *format,...)
void logWiFi(const char *format,...)
void logInfo(const char *format,...)
Система логгирования с красивым форматированием
void setupConfigRoutes()
Настройка маршрутов конфигурации (/intervals, /config_manager, /api/config/*)
void setupDataRoutes()
Настройка маршрутов данных датчика (/readings, /sensor_json, /api/sensor)
void setupMainRoutes()
Настройка основных маршрутов (/, /save, /status)
void setupOtaRoutes()
Настройка маршрутов OTA (/updates, /api/ota/*, /ota/*)
void setupServiceRoutes()
Настройка сервисных маршрутов (/health, /service_status, /reset, /reboot, /ota)
static const char DEVICE_SW_VERSION[]
void handleStatus()
Обработчик статуса (уже существует в wifi_manager.cpp)
void setLedBlink(unsigned long interval)
unsigned long ledBlinkInterval
void handleRoot()
Обработчик главной страницы (уже существует в wifi_manager.cpp)
#define WIFI_RECONNECT_INTERVAL
unsigned long ledLastToggle