// заголовки
  #include <asyncHTTPrequest.h>
  #include <ESPping.h>

  #include "Settings.h"
  #include "FS.h"

  extern float nbk_M;
  extern float nbk_Mo;
  extern float nbk_P;
  extern float nbk_Po;
  float get_speed_from_rate(float volume_per_hour, uint16_t s_per_l);
  void set_current_power(float Volt);
  void reset_wifi();
  uint16_t get_stepper_speed(void);
  bool set_stepper_target(uint16_t spd, uint8_t direction, uint32_t target);
  String get_program(uint8_t s);
  String get_beer_program();
  String get_dist_program();
  String get_nbk_program();
  float get_speed_from_rate(float rate, uint16_t s_per_l);
  float get_alcohol(float t);
  void set_mixer(bool On);
  void FS_init(void);
  void save_profile();
  void read_config();
  void WebServerInit(void);
  String get_DSAddressList(String Address);
  void set_pump_speed(float pumpspeed, bool continue_process);
  void start_self_test(void);
  void stop_self_test(void);
  String get_web_file(String fn, get_web_type type);
  void get_web_interface();
  String http_sync_request_get(String url);
  void set_water_temp(float temp);
  void set_power(bool On);
  void set_pump_pwm(float duty);
  void set_pump_speed_pid(float temp);
  void set_power_mode(String Mode);
  void set_capacity(uint8_t cap);
  void web_command(AsyncWebServerRequest *request);
  void handleSave(AsyncWebServerRequest *request);
  void get_data_log(AsyncWebServerRequest *request, String fn);
  void web_program(AsyncWebServerRequest *request);
  void calibrate_command(AsyncWebServerRequest *request);
  String get_DSAddressList(String Address);
  void set_pump_speed(float pumpspeed, bool continue_process);
  void start_self_test(void);
  void stop_self_test(void);
  String get_web_file(String fn, get_web_type type);
  void get_web_interface();
  float fromPower(float value);
  void SendMsg(const String& m, MESSAGE_TYPE msg_type);
  void handleAjaxSetup(AsyncWebServerRequest *request);
  #ifdef USE_LUA
  void start_lua_script();
  void load_lua_script();
  String get_lua_script_list();
  void run_lua_script(String fn);
  String run_lua_string(String lstr);
  #endif

  // filter out specific headers from the incoming request
  AsyncHeaderFilterMiddleware headerFilter;

uint16_t readPinsState() {// Функция для чтения состояния 16 пинов и возврата в виде слова (uint16_t)
  uint16_t pinsState = 0;
  
  // Первый байт (0-7)
  pinsState |= (digitalRead(WHEAD_LEVEL_SENSOR_PIN) ? 1 : 0) << 0;
  pinsState |= (digitalRead(WATERSENSOR_PIN) ? 1 : 0) << 1;
  pinsState |= (digitalRead(BTN_PIN) ? 1 : 0) << 2;
  pinsState |= (digitalRead(ALARM_BTN_PIN) ? 1 : 0) << 3;
  pinsState |= (digitalRead(RELE_CHANNEL1) ? 1 : 0) << 4;
  pinsState |= (digitalRead(RELE_CHANNEL2) ? 1 : 0) << 5;
  pinsState |= (digitalRead(RELE_CHANNEL3) ? 1 : 0) << 6;
  pinsState |= (digitalRead(RELE_CHANNEL4) ? 1 : 0) << 7;
  
  // Второй байт (8-15)
  pinsState |= (digitalRead(LUA_PIN) ? 1 : 0) << 8; 
  #if BOARD == ESP32S3
  pinsState |= (digitalRead(LUA_PIN2) ? 1 : 0) << 9; 
  pinsState |= (digitalRead(LUA_PIN3) ? 1 : 0) << 10;
  pinsState |= (digitalRead(BARDA_LEVEL_DWN) ? 1 : 0) << 11; 
  pinsState |= (digitalRead(BARDA_LEVEL_UP) ? 1 : 0) << 12; 
  #endif
  pinsState |= (PowerOn ? 1 : 0) << 13;
  pinsState |= (PauseOn ? 1 : 0) << 14;
  pinsState |= (SamSetup.UseBBuzzer ? 1 : 0) << 15;
 
  return pinsState;
}
// Вспомогательные функции для работы с JSON
String escapeJsonString(const String& input) {
  String result = input;
  result.replace("\\", "\\\\");
  result.replace("\"", "\\\"");
  result.replace("\n", "\\n");
  result.replace("\r", "\\r");
  result.replace("\t", "\\t");
  return result;
}
bool getJsonIntValue(const String& json, const char* key, int* result) {
  int startPos = json.indexOf(key);
  if (startPos == -1) return false;
  
  startPos += strlen(key);
  
  // Пропускаем пробелы и двоеточие
  while (startPos < json.length() && 
         (json.charAt(startPos) == ' ' || json.charAt(startPos) == ':' || json.charAt(startPos) == '\t')) {
    startPos++;
  }
  
  // Пропускаем открывающую кавычку, если есть
  if (startPos < json.length() && json.charAt(startPos) == '"') {
    startPos++;
  }
  
  int endPos = json.indexOf(',', startPos);
  if (endPos == -1) endPos = json.indexOf('}', startPos);
  
  // Если значение в кавычках, ищем закрывающую кавычку
  if (startPos > 0 && json.charAt(startPos-1) == '"') {
    int quotePos = json.indexOf('"', startPos);
    if (quotePos != -1 && quotePos < endPos) {
      endPos = quotePos;
    }
  }
  
  if (endPos == -1 || endPos <= startPos) return false;
  
  String valueStr = json.substring(startPos, endPos);
  // Убираем возможные кавычки
  valueStr.replace("\"", "");
  valueStr.trim();
  
  if (valueStr.length() == 0) return false;
  
  *result = valueStr.toInt();
  return true;
}
bool getJsonFloatValue(const String& json, const char* key, float* result) {
  int startPos = json.indexOf(key);
  if (startPos == -1) return false;
  
  startPos += strlen(key);
  
  // Пропускаем пробелы и двоеточие
  while (startPos < json.length() && 
         (json.charAt(startPos) == ' ' || json.charAt(startPos) == ':' || json.charAt(startPos) == '\t')) {
    startPos++;
  }
  
  // Пропускаем открывающую кавычку, если есть
  if (startPos < json.length() && json.charAt(startPos) == '"') {
    startPos++;
  }
  
  int endPos = json.indexOf(',', startPos);
  if (endPos == -1) endPos = json.indexOf('}', startPos);
  
  // Если значение в кавычках, ищем закрывающую кавычку
  if (startPos > 0 && json.charAt(startPos-1) == '"') {
    int quotePos = json.indexOf('"', startPos);
    if (quotePos != -1 && quotePos < endPos) {
      endPos = quotePos;
    }
  }
  
  if (endPos == -1 || endPos <= startPos) return false;
  
  String valueStr = json.substring(startPos, endPos);
  // Убираем возможные кавычки
  valueStr.replace("\"", "");
  valueStr.trim();
  
  if (valueStr.length() == 0) return false;
  
  *result = valueStr.toFloat();
  return true;
}
bool getJsonBoolValue(const String& json, const char* key, bool* result) {
  int intValue;
  if (getJsonIntValue(json, key, &intValue)) {
    *result = (intValue != 0);
    return true;
  }
  
  // Пробуем распарсить как строку "true"/"false"
  String stringValue;
  if (getJsonStringValue(json, key, &stringValue)) {
    stringValue.toLowerCase();
    if (stringValue == "true" || stringValue == "1") {
      *result = true;
      return true;
    } else if (stringValue == "false" || stringValue == "0") {
      *result = false;
      return true;
    }
  }
  
  return false;
}
bool getJsonStringValue(const String& json, const char* key, String* result) {
  int startPos = json.indexOf(key);
  if (startPos == -1) return false;
  
  startPos += strlen(key);
  
  // Пропускаем пробелы и двоеточие
  while (startPos < json.length() && 
         (json.charAt(startPos) == ' ' || json.charAt(startPos) == ':' || json.charAt(startPos) == '\t')) {
    startPos++;
  }
  
  if (startPos >= json.length()) return false;
  
  // Проверяем, является ли значение строкой (в кавычках)
  if (json.charAt(startPos) == '"') {
    // Значение в кавычках
    startPos++; // Пропускаем открывающую кавычку
    int endPos = json.indexOf('"', startPos);
    if (endPos == -1) return false;
    
    String value = json.substring(startPos, endPos);
    // Убираем экранирование
    value.replace("\\\"", "\"");
    value.replace("\\\\", "\\");
    value.replace("\\n", "\n");
    value.replace("\\r", "\r");
    value.replace("\\t", "\t");
    
    *result = value;
    return true;
  } else {
    // Числовое значение без кавычек
    int endPos = json.indexOf(',', startPos);
    if (endPos == -1) endPos = json.indexOf('}', startPos);
    
    if (endPos == -1 || endPos <= startPos) return false;
    
    *result = json.substring(startPos, endPos);
    result->trim();
    return true;
  }
}
bool getJsonAddressIndex(const String& json, const char* key, int* result) {
  int startPos = json.indexOf(key);
  if (startPos == -1) return false;
  
  startPos += strlen(key);
  
  // Пропускаем пробелы и двоеточие
  while (startPos < json.length() && 
         (json.charAt(startPos) == ' ' || json.charAt(startPos) == ':' || json.charAt(startPos) == '\t')) {
    startPos++;
  }
  
  // Пропускаем открывающую кавычку, если есть
  if (startPos < json.length() && json.charAt(startPos) == '"') {
    startPos++;
  }
  
  int endPos = json.indexOf(',', startPos);
  if (endPos == -1) endPos = json.indexOf('}', startPos);
  
  // Если значение в кавычках, ищем закрывающую кавычку
  if (startPos > 0 && json.charAt(startPos-1) == '"') {
    int quotePos = json.indexOf('"', startPos);
    if (quotePos != -1 && quotePos < endPos) {
      endPos = quotePos;
    }
  }
  
  if (endPos == -1 || endPos <= startPos) return false;
  
  String valueStr = json.substring(startPos, endPos);
  // Убираем возможные кавычки
  valueStr.replace("\"", "");
  valueStr.trim();
  
  if (valueStr.length() == 0) return false;
  
  *result = valueStr.toInt();
  return true;
}
String get_DSAddressListJson(const String& currentAddress) {
  String json = "{\"values\":[\"\"";
  int selectedIndex = 0;
  
  for (uint8_t i = 0; i < DScnt; i++) {
    String addr = getDSAddress(DSAddr[i]);
    json += ",\"" + escapeJsonString(addr) + "\"";
    
    if (currentAddress == addr) {
      selectedIndex = i + 1;
    }
  }
  
  json += "],\"selected\":" + String(selectedIndex) + "}";
  return json;
}
void parseCSVStringToInt8(const String& input, int8_t* output, uint8_t maxValues) {
    // Инициализируем выходной массив нулями
    for (uint8_t i = 0; i < maxValues; i++) {
        output[i] = 0;
    }
    
    uint8_t index = 0;
    int startPos = 0;
    int commaPos = input.indexOf(',');
    
    while (commaPos != -1 && index < maxValues) {
        String numStr = input.substring(startPos, commaPos);
        numStr.trim();
        
        if (numStr.length() > 0) {
            output[index] = numStr.toInt(); // toInt() вернет int, который преобразуется в int8_t
        }
        
        index++;
        startPos = commaPos + 1;
        commaPos = input.indexOf(',', startPos);
    }
    
    if (index < maxValues) {
        String numStr = input.substring(startPos);
        numStr.trim();
        if (numStr.length() > 0) {
            output[index] = numStr.toInt();
        }
    }
}
void parseCSVStringToUInt16(const String& input, uint16_t* output, uint8_t maxValues) {
    for (uint8_t i = 0; i < maxValues; i++) {
        output[i] = 0;
    }
    
    uint8_t index = 0;
    int startPos = 0;
    int commaPos = input.indexOf(',');
    
    while (commaPos != -1 && index < maxValues) {
        String numStr = input.substring(startPos, commaPos);
        numStr.trim();
        
        if (numStr.length() > 0) {
            output[index] = numStr.toInt();
        }
        
        index++;
        startPos = commaPos + 1;
        commaPos = input.indexOf(',', startPos);
    }
    
    if (index < maxValues) {
        String numStr = input.substring(startPos);
        numStr.trim();
        if (numStr.length() > 0) {
            output[index] = numStr.toInt();
        }
    }
}
void WebServerInit(void) {

  events.onConnect([](AsyncEventSourceClient * client) {
    client->send("hello!", NULL, millis(), 1000);
  });
  server.addHandler(&events);
  server.addHandler(new SPIFFSEditor(SPIFFS));
  server.onNotFound([](AsyncWebServerRequest * request) {
    Serial.printf("NOT_FOUND: ");
    if (request->method() == HTTP_GET)
      Serial.printf("GET");
    else if (request->method() == HTTP_POST)
      Serial.printf("POST");
    else if (request->method() == HTTP_DELETE)
      Serial.printf("DELETE");
    else if (request->method() == HTTP_PUT)
      Serial.printf("PUT");
    else if (request->method() == HTTP_PATCH)
      Serial.printf("PATCH");
    else if (request->method() == HTTP_HEAD)
      Serial.printf("HEAD");
    else if (request->method() == HTTP_OPTIONS)
      Serial.printf("OPTIONS");
    else
      Serial.printf("UNKNOWN");
    Serial.printf(" http://%s%s\n", request->host().c_str(), request->url().c_str());

    if (request->contentLength()) {
      Serial.printf("_CONTENT_TYPE: %s\n", request->contentType().c_str());
      Serial.printf("_CONTENT_LENGTH: %u\n", request->contentLength());
    }

    int headers = request->headers();
    int i;
    for (i = 0; i < headers; i++) {
      const AsyncWebHeader *h = request->getHeader(i);
      Serial.printf("_HEADER[%s]: %s\n", h->name().c_str(), h->value().c_str());
    }

    int params = request->params();
    for (i = 0; i < params; i++) {
      const AsyncWebParameter *p = request->getParam(i);
      if (p->isFile()) {
        Serial.printf("_FILE[%s]: %s, size: %u\n", p->name().c_str(), p->value().c_str(), p->size());
      } else if (p->isPost()) {
        Serial.printf("_POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
      } else {
        Serial.printf("_GET[%s]: %s\n", p->name().c_str(), p->value().c_str());
      }
    }

    request->send(404);
  });
  server.onFileUpload([](AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t *data, size_t len, bool final) {
    if (!index)
      Serial.printf("UploadStart: %s\n", filename.c_str());
    Serial.printf("%s", (const char *)data);
    if (final)
      Serial.printf("UploadEnd: %s (%u)\n", filename.c_str(), index + len);
  });
  server.onRequestBody([](AsyncWebServerRequest * request, uint8_t *data, size_t len, size_t index, size_t total) {
    if (!index)
      Serial.printf("BodyStart: %u\n", total);
    Serial.printf("%s", (const char *)data);
    if (index + len == total)
      Serial.printf("BodyEnd: %u\n", total);
  });
  server.on("/", HTTP_GET | HTTP_POST, [](AsyncWebServerRequest* request) {
    request->redirect("/index.htm");
  });
  server.serveStatic("/alarm.mp3", SPIFFS, "/alarm.mp3");
  server.serveStatic("/resetreason.css", SPIFFS, "/resetreason.css").setCacheControl("max-age=1");
  server.serveStatic("/data_old.csv", SPIFFS, "/data_old.csv").setCacheControl("max-age=1");
  server.serveStatic("/prg.csv", SPIFFS, "/prg.csv").setCacheControl("max-age=1");
  server.serveStatic("/state.csv", SPIFFS, "/state.csv").setCacheControl("max-age=1");
  server.serveStatic("/manual.htm", SPIFFS, "/manual.htm").setCacheControl("max-age=800");
  server.serveStatic("/pong.htm", SPIFFS, "/alarm.mp3");
  server.serveStatic("/program_fruit.txt", SPIFFS, "/program_fruit.txt").setCacheControl("max-age=1");
  server.serveStatic("/program_grain.txt", SPIFFS, "/program_grain.txt").setCacheControl("max-age=1");
  server.serveStatic("/program_shugar.txt", SPIFFS, "/program_shugar.txt").setCacheControl("max-age=1");

  server.on("/program.htm", HTTP_GET, [](AsyncWebServerRequest *request) {
    if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/program.htm.gz")) {
      AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/program.htm.gz", "text/html");
      response->addHeader("Content-Encoding", "gzip");
      response->addHeader("Cache-Control", "max-age=1");
      request->send(response);
    } else {
      request->send(SPIFFS, "/program.htm", "text/html", false);
    }
  });
  server.on("/chart.htm", HTTP_GET, [](AsyncWebServerRequest *request) {
    if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/chart.htm.gz")) {
      AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/chart.htm.gz", "text/html");
      response->addHeader("Content-Encoding", "gzip");
      response->addHeader("Cache-Control", "max-age=1");
      request->send(response);
    } else {
      request->send(SPIFFS, "/chart.htm", "text/html", false);
    }
  });
  server.on("/calibrate.htm", HTTP_GET, [](AsyncWebServerRequest *request) {
    if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/calibrate.htm.gz")) {
      AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/calibrate.htm.gz", "text/html");
      response->addHeader("Content-Encoding", "gzip");
      response->addHeader("Cache-Control", "max-age=1");
      request->send(response);
    } else {
      request->send(SPIFFS, "/calibrate.htm", "text/html", false);
    }
  });
  server.on("/calibrate2.htm", HTTP_GET, [](AsyncWebServerRequest *request) {
    if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/calibrate2.htm.gz")) {
      AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/calibrate2.htm.gz", "text/html");
      response->addHeader("Content-Encoding", "gzip");
      response->addHeader("Cache-Control", "max-age=1");
      request->send(response);
    } else {
      request->send(SPIFFS, "/calibrate2.htm", "text/html", false);
    }
  });
  server.on("/brewxml.htm", HTTP_GET, [](AsyncWebServerRequest *request) {
    if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/brewxml.htm.gz")) {
      AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/brewxml.htm.gz", "text/html");
      response->addHeader("Content-Encoding", "gzip");
      response->addHeader("Cache-Control", "max-age=1");
      request->send(response);
    } else {
      request->send(SPIFFS, "/brewxml.htm", "text/html", false);
    }
  });
  server.on("/setup.htm", HTTP_GET, [](AsyncWebServerRequest *request) {
    if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/setup.htm.gz")) {
      AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/setup.htm.gz", "text/html");
      response->addHeader("Content-Encoding", "gzip");
      response->addHeader("Cache-Control", "max-age=1");
      request->send(response);
    } else {
      if (SPIFFS.exists("/setup.htm")) request->send(SPIFFS, "/setup.htm", "text/html", false); else request->redirect("/technical");
    }
  });
  server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request) {
    if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/style.css.gz")) {
      AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/style.css.gz", "text/css");
      response->addHeader("Content-Encoding", "gzip");
      request->send(response);
    } else {
      request->send(SPIFFS, "/style.css", "text/css");
    }
  });
  server.on("/index.htm", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (Samovar_Mode == SAMOVAR_BEER_MODE) {
      if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/beer.htm.gz")) {
        AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/beer.htm.gz", "text/html");
        response->addHeader("Content-Encoding", "gzip");
        response->addHeader("Cache-Control", "max-age=1");
        request->send(response);
      } else {
        if (SPIFFS.exists("/beer.htm")) request->send(SPIFFS, "/beer.htm", "text/html", false); else request->redirect("/technical");
      }
    } else if (Samovar_Mode == SAMOVAR_DISTILLATION_MODE) {
      if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/distiller.htm.gz")) {
        AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/distiller.htm.gz", "text/html");
        response->addHeader("Content-Encoding", "gzip");
        response->addHeader("Cache-Control", "max-age=1");
        request->send(response);
      } else {
       if (SPIFFS.exists("/distiller.htm")) request->send(SPIFFS, "/distiller.htm", "text/html", false); else request->redirect("/technical");
      }
    } else if (Samovar_Mode == SAMOVAR_BK_MODE) {
      if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/bk.htm.gz")) {
        AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/bk.htm.gz", "text/html");
        response->addHeader("Content-Encoding", "gzip");
        response->addHeader("Cache-Control", "max-age=1");
        request->send(response);
      } else {
        if (SPIFFS.exists("/bk.htm")) request->send(SPIFFS, "/bk.htm", "text/html", false); else request->redirect("/technical");
      }
    } else if (Samovar_Mode == SAMOVAR_NBK_MODE) {
      if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/nbk.htm.gz")) {
        AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/nbk.htm.gz", "text/html");
        response->addHeader("Content-Encoding", "gzip");
        response->addHeader("Cache-Control", "max-age=1");
        request->send(response);
      } else {
        if (SPIFFS.exists("/nbk.htm")) request->send(SPIFFS, "/nbk.htm", "text/html", false); else request->redirect("/technical");
      }
    } else {
      if(request->header("Accept-Encoding").indexOf("gzip") != -1 && SPIFFS.exists("/index.htm.gz")) {
        AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/index.htm.gz", "text/html");
        response->addHeader("Content-Encoding", "gzip");
        response->addHeader("Cache-Control", "max-age=1");
        request->send(response);
      } else {
        if (SPIFFS.exists("/index.htm")) request->send(SPIFFS, "/index.htm", "text/html", false); else request->redirect("/technical");
      }
    }  
  });
  server.on("/rrlog", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(SPIFFS, "/resetreason.css", String());
  });
  server.on("/data.csv", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (fileToAppend && fileToAppend.available())
      fileToAppend.flush();
    request->send(SPIFFS, "/data.csv", String());
  });
 server.on("/ajax", HTTP_GET, [](AsyncWebServerRequest *request) {
  static unsigned long timeout = millis();
  static String jsonstr;
  static String LastIP, IP;
  
  IP = request->client()->remoteIP().toString();
  if ((millis() - timeout >= 3000) || (LastIP == IP) || (jsonstr.length() < 30)) {
    timeout = millis();
    LastIP = IP;
    
    String json = "{";
    
    json += "\"bt\":\"" + format_float(bme_temp, 1) + "\",";
    json += "\"bp\":\"" + format_float(bme_pressure, 1) + "\",";
    json += "\"sp\":\"" + format_float(start_pressure, 1) + "\",";
    json += "\"ST\":\"" + format_float(SteamSensor.avgTemp, 2) + "\",";
    json += "\"PT\":\"" + format_float(PipeSensor.avgTemp, 2) + "\",";
    json += "\"WT\":\"" + format_float(WaterSensor.avgTemp, 2) + "\",";
    json += "\"TT\":\"" + format_float(TankSensor.avgTemp, 2) + "\",";
    json += "\"AT\":\"" + format_float(ACPSensor.avgTemp, 2) + "\",";
    json += "\"AV\":\"" + format_float(ActualVolumePerHour, 2) + "\",";
    
    if (SamSetup.UsePump2 && Samovar_Mode == SAMOVAR_RECTIFICATION_MODE) {
      json += "\"AV1\":\"" + format_float(ActualVolumePerHour2, 2) + "\",";
      json += "\"VA1\":" + String(round(get_liquid_volume_by_step(stepper2.getCurrent()))) + ",";
    }
    
    if (Samovar_Mode == SAMOVAR_NBK_MODE) {
      json += "\"SPd\":\"" + format_float(get_liquid_rate_by_step(CurrentStepperSpeed), 2) + "\",";
    }
    
    json += "\"B\":\"" + String(readPinsState()) + "\",";
    
    if (Samovar_Mode == SAMOVAR_RECTIFICATION_MODE) {
      json += "\"cap\":\"" + String(program[ProgramNum].capacity_num) + "\",";
    }
    
    json += "\"VA\":" + String(round(get_liquid_volume_by_step(stepper.getCurrent()))) + ",";
    json += "\"WP\":" + String(WthdrwlProgress) + ",";
    json += "\"WS\":" + String(startval) + ",";
    json += "\"h\":" + String(ESP.getFreeHeap()) + ",";
    json += "\"r\":" + String(WiFi.RSSI()) + ",";
    json += "\"f\":" + String(total_byte - used_byte) + ",";
    json += "\"s\":\"" + escapeJsonString(NTP.getFormattedTime((unsigned long)(millis() / 1000))) + "\",";
    json += "\"St\":\"" + escapeJsonString(get_Samovar_Status()) + "\",";
    json += "\"L\":\"" + escapeJsonString(Lua_status) + "\",";
    
    // Условные блоки  
    if (Samovar_Mode == SAMOVAR_RECTIFICATION_MODE || 
        Samovar_Mode == SAMOVAR_BEER_MODE || 
        Samovar_Mode == SAMOVAR_DISTILLATION_MODE || 
        Samovar_Mode == SAMOVAR_NBK_MODE) {
      String pt = "";
      if (SamovarStatusInt == 10 || SamovarStatusInt == 15 || (SamovarStatusInt == 2000 && PowerOn)) {
        pt = program[ProgramNum].WType;
      }
      json += "\"PTp\":\"" + escapeJsonString(pt) + "\",";
    }
    
    if (hasMessages()) {
      MESSAGE_TYPE msgType;
      String msg = MsgGet(&msgType);
      json += "\"Msg\":\"" + escapeJsonString(msg) + "\",";
      json += "\"msglvl\":" + String(msgType) + ",";
    }

    if (LogMsg.length() > 0) {
      json += "\"LogMsg\":\"" + escapeJsonString(LogMsg) + "\",";
      LogMsg = "";
    }

    if (SamSetup.PwrType != NO_POVER_REG) {
      json += "\"cpv\":\"" + format_float(current_power_volt, 1) + "\",";
      json += "\"tpv\":\"" + format_float(target_power_volt, 1) + "\",";
      
      String cpm;
      if (current_power_mode == "0") cpm = "ON";
      else if (current_power_mode == "1") cpm = "BOOST";
      else if (current_power_mode == "N") cpm = "OFF";
      else cpm = "NONE";
      
      json += "\"cpm\":\"" + escapeJsonString(cpm) + "\",";
      json += "\"cpp\":" + String(current_power_p) + ",";
    } else {
      json += "\"cpv\":\"0\",";
      json += "\"tpv\":\"0\",";
      json += "\"cpm\":\"NO REG\",";
      json += "\"cpp\":0,";
    }

    if (SamSetup.UseWP) {
      json += "\"wps\":" + String(water_pump_speed) + ",";
    }

    if (SamSetup.UseWS == 1) {
      json += "\"WfR\":\"" + format_float(WFflowRate, 1) + "\",";
      json += "\"WfT\":" + String(WFtotalMilliLitres) + ",";
    }

    #if defined(USE_PRESSURE_XGZ) || defined(USE_PRESSURE_1WIRE) || defined(USE_PRESSURE_MPX)
    if (Samovar_Mode == SAMOVAR_RECTIFICATION_MODE || 
        Samovar_Mode == SAMOVAR_NBK_MODE) {
      json += "\"P\":\"" + format_float(pressure_value, 2) + "\",";
    }
    #endif

    if (Samovar_Mode == SAMOVAR_DISTILLATION_MODE) {
      json += "\"a\":\"" + format_float(get_alcohol(TankSensor.avgTemp), 1) + "\",";
      json += "\"sa\":\"" + format_float(get_steam_alcohol(TankSensor.avgTemp), 1) + "\",";
    }

    if (PowerOn && Samovar_Mode == SAMOVAR_DISTILLATION_MODE) {
      json += "\"TR\":" + String(int(timePredictor.remainingTime)) + ",";
      json += "\"Tt\":" + String(int(timePredictor.predictedTotalTime)) + ",";
    }
    
    // Убираем последнюю запятую
    if (json.endsWith(",")) {
      json.remove(json.length() - 1);
    }
    
    json += "}";
    jsonstr = json;
    
    vTaskDelay(2 / portTICK_PERIOD_MS);
  }
  
  request->send(200, "application/json", jsonstr);
 });
  server.on("/ajax_setup", HTTP_GET, handleAjaxSetup);
 
  server.on("/command", HTTP_GET, [](AsyncWebServerRequest *request) {
    web_command(request);
  });
  server.on("/ajax_index", HTTP_GET, [](AsyncWebServerRequest *request) {
    String json = "{";
    
    json += "\"btn_list\":\"";
    #ifdef USE_LUA
    json += escapeJsonString(get_lua_script_list());
    #else
    json += "";
    #endif
    json += "\",";
    
    switch (SamSetup.PwrType) {
      case KVIC: case KVIC9600: case RMVK: 
        json += "\"pwr_unit\":\"V\","; 
        break;
      case STAB_AVR: case UNI_PROTOCOL: case MODBUS_RTU:
        json += "\"pwr_unit\":\"P\","; 
        break;
      default:
        json += "\"pwr_unit\":\"N\","; 
        break;
    }
    
    json += "\"PipeColor\":\"" + escapeJsonString(String(SamSetup.PipeColor)) + "\",";
    json += "\"SteamColor\":\"" + escapeJsonString(String(SamSetup.SteamColor)) + "\",";
    json += "\"WaterColor\":\"" + escapeJsonString(String(SamSetup.WaterColor)) + "\",";
    json += "\"TankColor\":\"" + escapeJsonString(String(SamSetup.TankColor)) + "\",";
    json += "\"ACPColor\":\"" + escapeJsonString(String(SamSetup.ACPColor)) + "\",";
    json += "\"Descr\":\"" + escapeJsonString(SessionDescription) + "\",";
    
    // Обработка WProgram в зависимости от режима
    if (Samovar_Mode == SAMOVAR_BEER_MODE) {
      json += "\"WProgram\":\"" + escapeJsonString(get_beer_program()) + "\",";
    } else if (Samovar_Mode == SAMOVAR_DISTILLATION_MODE) {
      json += "\"WProgram\":\"" + escapeJsonString(get_dist_program()) + "\",";
    } else if (Samovar_Mode == SAMOVAR_NBK_MODE) {
      json += "\"WProgram\":\"" + escapeJsonString(get_nbk_program()) + "\",";
    } else {
      json += "\"WProgram\":\"" + escapeJsonString(get_program(MAX_PRG)) + "\",";
    }
    
    json += "\"PWM_LV\":" + String(SamSetup.PWM_L_V * 10) + ",";
    json += "\"PWM_V\":" + String(bk_pwm) + ",";
    json += "\"v\":\"" + String(SAMOVAR_VERSION) + "\",";
    json += "\"StepperStep\":" + String(STEPPER_MAX_SPEED) + ",";
    json += "\"StepperStepMl\":" + String(SamSetup.StepperStepMl * 100) + ",";
    
    // Логика скрытия/показа элементов
    json += "\"SteamHide\":" + String((SteamSensor.avgTemp > 0) ? "false" : "true") + ",";
    json += "\"PipeHide\":" + String((PipeSensor.avgTemp > 0) ? "false" : "true") + ",";
    json += "\"WaterHide\":" + String((WaterSensor.avgTemp > 0) ? "false" : "true") + ",";
    json += "\"TankHide\":" + String((TankSensor.avgTemp > 0) ? "false" : "true") + ",";
    json += "\"PressureHide\":" + String((bme_pressure > 0) ? "false" : "true") + ",";
    json += "\"ProgNumHide\":" + String((ProgramNum > 0) ? "false" : "true") + ",";
    
    // Видео URL
    json += "\"videourl\":\"" + escapeJsonString(String(SamSetup.videourl)) + "\",";
    json += "\"showvideo\":\"" + String((strlen(SamSetup.videourl) > 0) ? "inline" : "none") + "\"";
    
    json += "}";
    
    request->send(200, "application/json", json);
  });
  server.on("/ajax_calibrate", HTTP_GET, [](AsyncWebServerRequest *request) {
    String json = "{";
    json += "\"StepperStep\":" + String(STEPPER_MAX_SPEED) + ",";
    json += "\"volume\":\"" + String((Samovar_Mode == SAMOVAR_NBK_MODE) ? "1000" : "100") + "\",";
    json += "\"StepperStepMl\":" + String(SamSetup.StepperStepMl * 100);
    json += "}";
    
    request->send(200, "application/json", json);
  });
    server.on("/program", HTTP_POST, [](AsyncWebServerRequest *request) {
    web_program(request);
    SavePrgToFS();
  });

  server.on("/calibrate", HTTP_GET, [](AsyncWebServerRequest *request) {
    calibrate_command(request);
  });

  server.on("/getlog", HTTP_GET, [](AsyncWebServerRequest *request) {
    get_data_log(request, "data.csv");
  });

  server.on("/getoldlog", HTTP_GET, [](AsyncWebServerRequest *request) {
    get_data_log(request, "data_old.csv");
  });
 #ifdef USE_LUA
  server.on("/lua", HTTP_GET, [](AsyncWebServerRequest *request) {
    start_lua_script();
    request->send(200, "text/html", "OK");
  });
 #endif
  server.on("/save", HTTP_POST, [](AsyncWebServerRequest *request) {
    handleSave(request);
  });
  server.onFileUpload([](AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) {
    if (!index)
      DbgMsg("Upload: " + filename, 1);
    if (final)
      DbgMsg("Upload End: " + filename + String(index + len), 1);
  });
  server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
    if (!index)
      DbgMsg("BodyStart: " + String(total), 1);
    if (index + len == total)
      DbgMsg("BodyEnd: " + String(total),1);
  });
  DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");  // CORS
  headerFilter.filter("If-Modified-Since");
  //  DefaultHeaders::Instance().addHeader("Cache-Control", "no-cache");
  //  DefaultHeaders::Instance().addHeader("Pragma", "no-cache");
  //  DefaultHeaders::Instance().addHeader("Expires", "Thu, 01 Jan 1970 00:00:00 UTC");
  //  DefaultHeaders::Instance().addHeader("Last-Modified", "Mon, 03 Jan 2050 00:00:00 UTC");
  server.begin();

 #ifndef NOT_USE_INTERFACE_UPDATE
  //get_web_interface(); //запуск обновления интерфейса// отключу пока от греха
 #endif
}
void handleAjaxSetup(AsyncWebServerRequest *request) {
  String json = "{";
  
  // Режим работы
  json += "\"mode\":" + String(SamSetup.Mode) + ",";
  
  // Основные параметры
  json += "\"DistTemp\":\"" + format_float(SamSetup.DistTemp, 2) + "\",";
  json += "\"DistTimeF\":" + String(SamSetup.DistTimeF) + ",";
  json += "\"UsePreccureCorrect\":" + String(SamSetup.UsePreccureCorrect ? 1 : 0) + ",";
  json += "\"UseHLS\":" + String(SamSetup.UseHLS ? 1 : 0) + ",";
  json += "\"UseHLS_D\":" + String(SamSetup.UseHLS_D ? 1 : 0) + ",";
  json += "\"useautospeed\":" + String(SamSetup.useautospeed ? 1 : 0) + ",";
  json += "\"autospeed\":" + String(SamSetup.autospeed) + ",";
  json += "\"useautopowerdown\":" + String(SamSetup.useautopowerdown ? 1 : 0) + ",";
  json += "\"MaxPressureValue\":\"" + format_float(SamSetup.MaxPressureValue, 2) + "\",";
  json += "\"UseBuzzer\":" + String(SamSetup.UseBuzzer ? 1 : 0) + ",";
  json += "\"UseBBuzzer\":" + String(SamSetup.UseBBuzzer ? 1 : 0) + ",";
  json += "\"ChangeProgramBuzzer\":" + String(SamSetup.ChangeProgramBuzzer ? 1 : 0) + ",";
  
  // Температурные параметры
  json += "\"SteamColor\":\"" + escapeJsonString(String(SamSetup.SteamColor)) + "\",";
  json += "\"SteamAddr\":" + get_DSAddressListJson(getDSAddress(SteamSensor.Sensor)) + ",";
  json += "\"DeltaSteamTemp\":\"" + format_float(SamSetup.DeltaSteamTemp, 2) + "\",";
  json += "\"SetSteamTemp\":\"" + format_float(SamSetup.SetSteamTemp, 2) + "\",";
  json += "\"SteamDelay\":" + String(SamSetup.SteamDelay) + ",";
  
  json += "\"PipeColor\":\"" + escapeJsonString(String(SamSetup.PipeColor)) + "\",";
  json += "\"PipeAddr\":" + get_DSAddressListJson(getDSAddress(PipeSensor.Sensor)) + ",";
  json += "\"DeltaPipeTemp\":\"" + format_float(SamSetup.DeltaPipeTemp, 2) + "\",";
  json += "\"SetPipeTemp\":\"" + format_float(SamSetup.SetPipeTemp, 2) + "\",";
  json += "\"PipeDelay\":" + String(SamSetup.PipeDelay) + ",";
  
  json += "\"WaterColor\":\"" + escapeJsonString(String(SamSetup.WaterColor)) + "\",";
  json += "\"WaterAddr\":" + get_DSAddressListJson(getDSAddress(WaterSensor.Sensor)) + ",";
  json += "\"DeltaWaterTemp\":\"" + format_float(SamSetup.DeltaWaterTemp, 2) + "\",";
  json += "\"SetWaterTemp\":\"" + format_float(SamSetup.SetWaterTemp, 2) + "\",";
  json += "\"WaterDelay\":" + String(SamSetup.WaterDelay) + ",";
  
  json += "\"TankColor\":\"" + escapeJsonString(String(SamSetup.TankColor)) + "\",";
  json += "\"TankAddr\":" + get_DSAddressListJson(getDSAddress(TankSensor.Sensor)) + ",";
  json += "\"DeltaTankTemp\":\"" + format_float(SamSetup.DeltaTankTemp, 2) + "\",";
  json += "\"SetTankTemp\":\"" + format_float(SamSetup.SetTankTemp, 2) + "\",";
  json += "\"TankDelay\":" + String(SamSetup.TankDelay) + ",";
  
  json += "\"ACPColor\":\"" + escapeJsonString(String(SamSetup.ACPColor)) + "\",";
  json += "\"ACPAddr\":" + get_DSAddressListJson(getDSAddress(ACPSensor.Sensor)) + ",";
  json += "\"DeltaACPTemp\":\"" + format_float(SamSetup.DeltaACPTemp, 2) + "\",";
  json += "\"SetACPTemp\":\"" + format_float(SamSetup.SetACPTemp, 2) + "\",";
  json += "\"ACPDelay\":" + String(SamSetup.ACPDelay) + ",";
  
  // Насос и PID
  json += "\"StepperStepMl\":" + String(SamSetup.StepperStepMl) + ",";
  json += "\"Kp\":\"" + format_float(SamSetup.Kp, 3) + "\",";
  json += "\"Ki\":\"" + format_float(SamSetup.Ki, 3) + "\",";
  json += "\"Kd\":\"" + format_float(SamSetup.Kd, 3) + "\",";
  json += "\"StbVoltage\":\"" + format_float(SamSetup.StbVoltage, 2) + "\",";
  json += "\"BVolt\":\"" + format_float(SamSetup.BVolt, 2) + "\",";
  json += "\"UseST\":" + String(SamSetup.UseST ? 1 : 0) + ",";
  
  // НБК
  json += "\"NbkIn\":\"" + format_float(SamSetup.NbkIn, 2) + "\",";
  json += "\"NbkDelta\":\"" + format_float(SamSetup.NbkDelta, 2) + "\",";
  json += "\"NbkDM\":\"" + format_float(SamSetup.NbkDM, 2) + "\",";
  json += "\"NbkDP\":\"" + format_float(SamSetup.NbkDP, 2) + "\",";
  json += "\"NbkSteamT\":\"" + format_float(SamSetup.NbkSteamT, 2) + "\",";
  json += "\"NbkOwPress\":\"" + format_float(SamSetup.NbkOwPress, 2) + "\",";
  json += "\"NBK_Mult_Pause\":\"" + format_float(SamSetup.NBK_Mult_Pause, 2) + "\",";
  json += "\"NBK_Pump_Limit\":\"" + format_float(SamSetup.NBK_Pump_Limit, 2) + "\",";
  json += "\"Use_NBK_Delta_Pressure\":" + String(SamSetup.Use_NBK_Delta_Pressure ? 1 : 0) + ",";
  json += "\"NBK_StepperStepMl\":" + String(SamSetup.NBK_StepperStepMl) + ",";
  json += "\"Nbk_Tn\":\"" + format_float(SamSetup.Nbk_Tn, 2) + "\",";
  
  // NTC
  for (uint8_t i = 0; i < 5; i++) {
    json += "\"NTC_dT" + String(i + 1) + "\":\"" + format_float(SamSetup.NTC_dT[i], 2) + "\",";
  }
  
  String ADS = String(SamSetup.m_adc[0]);
  for (uint8_t i = 1; i < 151; i++) {
    ADS += ", " + String(SamSetup.m_adc[i]);
  }
  json += "\"m_adc\":\"" + ADS + "\",";
  
  // Прочие
  json += "\"MQTT_Serv\":\"" + escapeJsonString(String(SamSetup.MQTT_Serv)) + "\",";
  json += "\"blynkauth\":\"" + escapeJsonString(String(SamSetup.blynkauth)) + "\",";
  json += "\"tgtoken\":\"" + escapeJsonString(String(SamSetup.tg_token)) + "\",";
  json += "\"tgchatid\":\"" + escapeJsonString(String(SamSetup.tg_chat_id)) + "\",";
  json += "\"videourl\":\"" + escapeJsonString(String(SamSetup.videourl)) + "\",";
  json += "\"TimeZone\":" + String(SamSetup.TimeZone) + ",";
  json += "\"CheckPower\":" + String(SamSetup.CheckPower ? 1 : 0) + ",";
  json += "\"HeaterR\":\"" + format_float(SamSetup.HeaterResistant, 3) + "\",";
  json += "\"LogPeriod\":" + String(SamSetup.LogPeriod) + ",";
  json += "\"UseWS\":" + String(SamSetup.UseWS) + ",";
  
  // Реле
  json += "\"Rele1\":" + String(SamSetup.rele1 ? 1 : 0) + ",";
  json += "\"Rele2\":" + String(SamSetup.rele2 ? 1 : 0) + ",";
  json += "\"Rele3\":" + String(SamSetup.rele3 ? 1 : 0) + ",";
  json += "\"Rele4\":" + String(SamSetup.rele4 ? 1 : 0) + ",";
  
  // Настройки WiFi
  json += "\"SSID\":\"" + escapeJsonString(String(SamSetup.SavedSSID)) + "\",";
  json += "\"PASS\":\"" + escapeJsonString(String(SamSetup.SavedPASS)) + "\",";
  
  // Второй насос
  json += "\"UsePump2\":" + String(SamSetup.UsePump2 ? 1 : 0) + ",";
  json += "\"SpPump2\":\"" + format_float(SamSetup.SpPump2, 2) + "\",";
  
  // Настройки предельных значений
  json += "\"NBK_WPrc\":\"" + format_float(SamSetup.NBK_WPrc, 2) + "\",";
  json += "\"Alrm_Wt_T\":\"" + format_float(SamSetup.Alrm_Wt_T, 2) + "\",";
  json += "\"Max_Wt_T\":\"" + format_float(SamSetup.Max_Wt_T, 2) + "\",";
  json += "\"Max_St_T\":\"" + format_float(SamSetup.Max_St_T, 2) + "\",";
  json += "\"Max_ACP_T\":\"" + format_float(SamSetup.Max_ACP_T, 2) + "\",";
  json += "\"Ch_Pwr_Md_St_T\":\"" + format_float(SamSetup.Ch_Pwr_Md_St_T, 2) + "\",";
  json += "\"Opn_Vlv_Tnk_T\":\"" + format_float(SamSetup.Opn_Vlv_Tnk_T, 2) + "\",";
  json += "\"D_Cls_Vlv\":\"" + format_float(SamSetup.D_Cls_Vlv, 2) + "\",";
  json += "\"Heat_Dt\":\"" + format_float(SamSetup.Heat_Dt, 2) + "\",";
  json += "\"Axlr_Heat_Dt\":\"" + format_float(SamSetup.Axlr_Heat_Dt, 2) + "\",";
  json += "\"Boilng_T\":\"" + format_float(SamSetup.Boilng_T, 2) + "\",";
  json += "\"Trg_W_T\":\"" + format_float(SamSetup.Trg_W_T, 2) + "\",";
  
  json += "\"PWM_L_V\":" + String(SamSetup.PWM_L_V) + ",";
  json += "\"PWM_St_V\":" + String(SamSetup.PWM_St_V) + ",";
  json += "\"PwrType\":" + String(SamSetup.PwrType) + ",";
  json += "\"PwStartPause\":" + String(SamSetup.PwStartPause) + ",";
  json += "\"Ws_Calbr\":" + String(SamSetup.Ws_Calbr) + ",";
  json += "\"BlynkUrl\":\"" + escapeJsonString(String(SamSetup.BlynkUrl)) + "\",";
  
  ADS = String(SamSetup.servoDelta[0]);
  for (uint8_t i = 1; i < 11; i++) {
    ADS += ", " + String(SamSetup.servoDelta[i]);
  }
  json += "\"servoDelta\":\"" + ADS + "\",";
  
  json += "\"Use_BMP180\":" + String(SamSetup.Use_BMP180 ? 1 : 0) + ",";
  json += "\"UseBlynk\":" + String(SamSetup.UseBlynk ? 1 : 0) + ",";
  json += "\"UseBTN\":" + String(SamSetup.UseBTN ? 1 : 0) + ",";
  json += "\"UseA_BTN\":" + String(SamSetup.UseA_BTN ? 1 : 0) + ",";
  json += "\"UseMQTT\":" + String(SamSetup.UseMQTT ? 1 : 0) + ",";
  json += "\"UseDS\":" + String(SamSetup.UseDS ? 1 : 0) + ",";
  json += "\"UseNTC\":" + String(SamSetup.UseNTC ? 1 : 0) + ",";
  json += "\"DUFnpn\":" + String(SamSetup.DUFnpn ? 1 : 0) + ",";
  json += "\"UseTg\":" + String(SamSetup.UseTg ? 1 : 0) + ",";
  json += "\"UseBdT_Aset\":" + String(SamSetup.UseBdT_Aset ? 1 : 0) + ",";
  json += "\"StRev1\":" + String(SamSetup.StRev1 ? 1 : 0) + ",";
  json += "\"StRev2\":" + String(SamSetup.StRev2 ? 1 : 0) + ",";
  json += "\"UseWP\":" + String(SamSetup.UseWP ? 1 : 0) + ",";
  json += "\"UseWV\":" + String(SamSetup.UseWV) + ",";
  
  // Вентилятор
  json += "\"PWM_Cull\":" + String(SamSetup.PWM_Cull ? 1 : 0) + ",";
  json += "\"Cull_N_Min\":" + String(SamSetup.Cull_N_Min) + ",";
  json += "\"Cull_N_Max\":" + String(SamSetup.Cull_N_Max) + ",";
  json += "\"Cull_T_ON\":" + String(SamSetup.Cull_T_ON) + ",";
  json += "\"Cull_T_Max\":" + String(SamSetup.Cull_T_Max) + ",";
  
  json += "\"dbg\":" + String(SamSetup.dbg ? 1 : 0) + ",";
  json += "\"MQTT_User\":\"" + escapeJsonString(String(SamSetup.MQTT_User)) + "\",";
  json += "\"MQTT_Pass\":\"" + escapeJsonString(String(SamSetup.MQTT_Pass)) + "\",";
  json += "\"MQTT_Port\":" + String(SamSetup.MQTT_Port);
  
  json += "}";
  
  request->send(200, "application/json", json);
}
void parseJsonToSamSetup(const String& json) {
  String cleanJson = json;
  // Не убираем пробелы полностью, только лишние
  cleanJson.replace("\n", "");
  cleanJson.replace("\r", "");
  cleanJson.replace("\t", "");
  
  int intValue;
  float floatValue;
  String stringValue;
  int indexValue;
  bool boolValue;
  
  // Вентилятор - используем bool парсинг
  if (getJsonBoolValue(cleanJson, "\"PWM_Cull\"", &boolValue)) SamSetup.PWM_Cull = boolValue ? 1 : 0;
  if (getJsonIntValue(cleanJson, "\"Cull_N_Min\"", &intValue)) SamSetup.Cull_N_Min = intValue;
  if (getJsonIntValue(cleanJson, "\"Cull_N_Max\"", &intValue)) SamSetup.Cull_N_Max = intValue;
  if (getJsonFloatValue(cleanJson, "\"Cull_T_ON\"", &floatValue)) SamSetup.Cull_T_ON = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Cull_T_Max\"", &floatValue)) SamSetup.Cull_T_Max = floatValue;
  
  if (!SamSetup.PWM_Cull) Culler_PWM.write(0);
  
  // Boolean параметры через getJsonBoolValue
  if (getJsonBoolValue(cleanJson, "\"UseWP\"", &boolValue)) SamSetup.UseWP = boolValue ? 1 : 0;
  if (!SamSetup.UseWP) pump_pwm.write(0);
  if (getJsonIntValue(cleanJson, "\"UseWV\"", &intValue)) SamSetup.UseWV = intValue;
  
  // Числовые параметры
  if (getJsonIntValue(cleanJson, "\"SteamDelay\"", &intValue)) SamSetup.SteamDelay = intValue;
  if (getJsonIntValue(cleanJson, "\"PipeDelay\"", &intValue)) SamSetup.PipeDelay = intValue;
  if (getJsonIntValue(cleanJson, "\"WaterDelay\"", &intValue)) SamSetup.WaterDelay = intValue;
  if (getJsonIntValue(cleanJson, "\"TankDelay\"", &intValue)) SamSetup.TankDelay = intValue;
  if (getJsonIntValue(cleanJson, "\"ACPDelay\"", &intValue)) SamSetup.ACPDelay = intValue;
  if (getJsonIntValue(cleanJson, "\"MQTT_Port\"", &intValue)) SamSetup.MQTT_Port = intValue;
  
  // Float параметры
  if (getJsonFloatValue(cleanJson, "\"DeltaSteamTemp\"", &floatValue)) SamSetup.DeltaSteamTemp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"DeltaPipeTemp\"", &floatValue)) SamSetup.DeltaPipeTemp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"DeltaWaterTemp\"", &floatValue)) SamSetup.DeltaWaterTemp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"DeltaTankTemp\"", &floatValue)) SamSetup.DeltaTankTemp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"DeltaACPTemp\"", &floatValue)) SamSetup.DeltaACPTemp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"SetSteamTemp\"", &floatValue)) SamSetup.SetSteamTemp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"SetPipeTemp\"", &floatValue)) SamSetup.SetPipeTemp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"SetWaterTemp\"", &floatValue)) SamSetup.SetWaterTemp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"SetTankTemp\"", &floatValue)) SamSetup.SetTankTemp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"SetACPTemp\"", &floatValue)) SamSetup.SetACPTemp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Kp\"", &floatValue)) SamSetup.Kp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Ki\"", &floatValue)) SamSetup.Ki = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Kd\"", &floatValue)) SamSetup.Kd = floatValue;
  if (getJsonFloatValue(cleanJson, "\"StbVoltage\"", &floatValue)) SamSetup.StbVoltage = floatValue;
  if (getJsonFloatValue(cleanJson, "\"BVolt\"", &floatValue)) SamSetup.BVolt = floatValue;
  if (getJsonFloatValue(cleanJson, "\"MaxPressureValue\"", &floatValue)) SamSetup.MaxPressureValue = floatValue;
  if (getJsonFloatValue(cleanJson, "\"DistTemp\"", &floatValue)) SamSetup.DistTemp = floatValue;
  if (getJsonFloatValue(cleanJson, "\"HeaterR\"", &floatValue)) SamSetup.HeaterResistant = floatValue;
  if (getJsonFloatValue(cleanJson, "\"NbkIn\"", &floatValue)) SamSetup.NbkIn = floatValue;
  if (getJsonFloatValue(cleanJson, "\"NbkDelta\"", &floatValue)) SamSetup.NbkDelta = floatValue;
  if (getJsonFloatValue(cleanJson, "\"NbkDM\"", &floatValue)) SamSetup.NbkDM = floatValue;
  if (getJsonFloatValue(cleanJson, "\"NbkDP\"", &floatValue)) SamSetup.NbkDP = floatValue;
  if (getJsonFloatValue(cleanJson, "\"NbkSteamT\"", &floatValue)) SamSetup.NbkSteamT = floatValue;
  if (getJsonFloatValue(cleanJson, "\"NbkOwPress\"", &floatValue)) SamSetup.NbkOwPress = floatValue;
  if (getJsonFloatValue(cleanJson, "\"SpPump2\"", &floatValue)) SamSetup.SpPump2 = floatValue;
  if (getJsonFloatValue(cleanJson, "\"NBK_Mult_Pause\"", &floatValue)) SamSetup.NBK_Mult_Pause = floatValue;
  if (getJsonFloatValue(cleanJson, "\"NBK_Pump_Limit\"", &floatValue)) SamSetup.NBK_Pump_Limit = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Nbk_Tn\"", &floatValue)) SamSetup.Nbk_Tn = floatValue;
  
  if (getJsonFloatValue(cleanJson, "\"NBK_WPrc\"", &floatValue)) SamSetup.NBK_WPrc = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Alrm_Wt_T\"", &floatValue)) SamSetup.Alrm_Wt_T = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Max_Wt_T\"", &floatValue)) SamSetup.Max_Wt_T = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Max_St_T\"", &floatValue)) SamSetup.Max_St_T = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Max_ACP_T\"", &floatValue)) SamSetup.Max_ACP_T = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Ch_Pwr_Md_St_T\"", &floatValue)) SamSetup.Ch_Pwr_Md_St_T = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Opn_Vlv_Tnk_T\"", &floatValue)) SamSetup.Opn_Vlv_Tnk_T = floatValue;
  if (getJsonFloatValue(cleanJson, "\"D_Cls_Vlv\"", &floatValue)) SamSetup.D_Cls_Vlv = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Heat_Dt\"", &floatValue)) SamSetup.Heat_Dt = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Axlr_Heat_Dt\"", &floatValue)) SamSetup.Axlr_Heat_Dt = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Boilng_T\"", &floatValue)) SamSetup.Boilng_T = floatValue;
  if (getJsonFloatValue(cleanJson, "\"Trg_W_T\"", &floatValue)) SamSetup.Trg_W_T = floatValue;
  
  // Целочисленные параметры
  if (getJsonIntValue(cleanJson, "\"DistTimeF\"", &intValue)) SamSetup.DistTimeF = intValue;
  if (getJsonIntValue(cleanJson, "\"StepperStepMl\"", &intValue)) SamSetup.StepperStepMl = intValue;
  if (getJsonIntValue(cleanJson, "\"TimeZone\"", &intValue)) SamSetup.TimeZone = intValue;
  if (getJsonIntValue(cleanJson, "\"LogPeriod\"", &intValue)) SamSetup.LogPeriod = intValue;
  if (getJsonIntValue(cleanJson, "\"autospeed\"", &intValue)) SamSetup.autospeed = intValue;
  if (getJsonIntValue(cleanJson, "\"NBK_StepperStepMl\"", &intValue)) SamSetup.NBK_StepperStepMl = intValue;
  
  if (getJsonIntValue(cleanJson, "\"PWM_L_V\"", &intValue)) SamSetup.PWM_L_V = intValue;
  if (getJsonIntValue(cleanJson, "\"PWM_St_V\"", &intValue)) SamSetup.PWM_St_V = intValue;
  
  if (getJsonIntValue(cleanJson, "\"PwrType\"", &intValue)) {
    if (SamSetup.PwrType != intValue) {
      SamSetup.PwrType = intValue;
      is_reboot = true;
    }
  }
  
  if (getJsonIntValue(cleanJson, "\"PwStartPause\"", &intValue)) SamSetup.PwStartPause = intValue;
  if (getJsonIntValue(cleanJson, "\"Ws_Calbr\"", &intValue)) SamSetup.Ws_Calbr = intValue;
  
  // Boolean параметры через getJsonBoolValue
  if (getJsonBoolValue(cleanJson, "\"UseHLS\"", &boolValue)) SamSetup.UseHLS = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"UseHLS_D\"", &boolValue)) SamSetup.UseHLS_D = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"UsePreccureCorrect\"", &boolValue)) SamSetup.UsePreccureCorrect = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"useautospeed\"", &boolValue)) SamSetup.useautospeed = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"useautopowerdown\"", &boolValue)) SamSetup.useautopowerdown = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"ChangeProgramBuzzer\"", &boolValue)) SamSetup.ChangeProgramBuzzer = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"UseBuzzer\"", &boolValue)) SamSetup.UseBuzzer = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"UseBBuzzer\"", &boolValue)) SamSetup.UseBBuzzer = boolValue ? 1 : 0;
  if (getJsonIntValue(cleanJson, "\"UseWS\"", &intValue)) SamSetup.UseWS = intValue;
  if (getJsonBoolValue(cleanJson, "\"UseST\"", &boolValue)) SamSetup.UseST = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"CheckPower\"", &boolValue)) SamSetup.CheckPower = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"UsePump2\"", &boolValue)) SamSetup.UsePump2 = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"Use_NBK_Delta_Pressure\"", &boolValue)) SamSetup.Use_NBK_Delta_Pressure = boolValue ? 1 : 0;
  
  if (getJsonBoolValue(cleanJson, "\"Use_BMP180\"", &boolValue)) SamSetup.Use_BMP180 = boolValue ? 1 : 0;
  
  if (getJsonBoolValue(cleanJson, "\"UseBlynk\"", &boolValue)) {
    if (boolValue != SamSetup.UseBlynk) {
      SamSetup.UseBlynk = boolValue ? 1 : 0;
      is_reboot += boolValue;
    }
  }
  
  if (getJsonBoolValue(cleanJson, "\"UseBTN\"", &boolValue)) SamSetup.UseBTN = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"UseA_BTN\"", &boolValue)) SamSetup.UseA_BTN = boolValue ? 1 : 0;
  
  if (getJsonBoolValue(cleanJson, "\"UseMQTT\"", &boolValue)) {
    if (boolValue != SamSetup.UseMQTT) {
      SamSetup.UseMQTT = boolValue ? 1 : 0;
      is_reboot += boolValue;
    }
  }
  
  if (getJsonBoolValue(cleanJson, "\"UseDS\"", &boolValue)) SamSetup.UseDS = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"UseNTC\"", &boolValue)) SamSetup.UseNTC = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"DUFnpn\"", &boolValue)) SamSetup.DUFnpn = boolValue ? 1 : 0;
  
  if (getJsonBoolValue(cleanJson, "\"UseTg\"", &boolValue)) {
    if (SamSetup.UseTg != boolValue) {
      SamSetup.UseTg = boolValue ? 1 : 0;
      is_reboot += boolValue;
    }
  }
  
  if (getJsonBoolValue(cleanJson, "\"UseBdT_Aset\"", &boolValue)) SamSetup.UseBdT_Aset = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"StRev1\"", &boolValue)) SamSetup.StRev1 = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"StRev2\"", &boolValue)) SamSetup.StRev2 = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"dbg\"", &boolValue)) SamSetup.dbg = boolValue ? 1 : 0;
  
  // Строковые параметры
  if (getJsonStringValue(cleanJson, "\"videourl\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.videourl, stringValue.c_str(), sizeof(SamSetup.videourl));
  } else SamSetup.videourl[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"blynkauth\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.blynkauth, stringValue.c_str(), sizeof(SamSetup.blynkauth));
  } else SamSetup.blynkauth[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"tgtoken\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.tg_token, stringValue.c_str(), sizeof(SamSetup.tg_token));
  } else SamSetup.tg_token[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"tgchatid\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.tg_chat_id, stringValue.c_str(), sizeof(SamSetup.tg_chat_id));
  } else SamSetup.tg_chat_id[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"SteamColor\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.SteamColor, stringValue.c_str(), sizeof(SamSetup.SteamColor));
  } else SamSetup.SteamColor[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"PipeColor\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.PipeColor, stringValue.c_str(), sizeof(SamSetup.PipeColor));
  } else SamSetup.PipeColor[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"WaterColor\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.WaterColor, stringValue.c_str(), sizeof(SamSetup.WaterColor));
  } else SamSetup.WaterColor[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"TankColor\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.TankColor, stringValue.c_str(), sizeof(SamSetup.TankColor));
  } else SamSetup.TankColor[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"ACPColor\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.ACPColor, stringValue.c_str(), sizeof(SamSetup.ACPColor));
  } else SamSetup.ACPColor[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"SSID\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.SavedSSID, stringValue.c_str(), sizeof(SamSetup.SavedSSID));
  } else SamSetup.SavedSSID[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"PASS\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.SavedPASS, stringValue.c_str(), sizeof(SamSetup.SavedPASS));
  } else SamSetup.SavedPASS[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"MQTT_Serv\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.MQTT_Serv, stringValue.c_str(), sizeof(SamSetup.MQTT_Serv));
  } else SamSetup.MQTT_Serv[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"MQTT_User\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.MQTT_User, stringValue.c_str(), sizeof(SamSetup.MQTT_User));
  } else SamSetup.MQTT_User[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"MQTT_Pass\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.MQTT_Pass, stringValue.c_str(), sizeof(SamSetup.MQTT_Pass));
  } else SamSetup.MQTT_Pass[0] = '\0';
  
  if (getJsonStringValue(cleanJson, "\"BlynkUrl\"", &stringValue) && stringValue.length() > 0) {
    strlcpy(SamSetup.BlynkUrl, stringValue.c_str(), sizeof(SamSetup.BlynkUrl));
  } else SamSetup.BlynkUrl[0] = '\0';
  
  // Реле
  if (getJsonBoolValue(cleanJson, "\"Rele1\"", &boolValue)) SamSetup.rele1 = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"Rele2\"", &boolValue)) SamSetup.rele2 = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"Rele3\"", &boolValue)) SamSetup.rele3 = boolValue ? 1 : 0;
  if (getJsonBoolValue(cleanJson, "\"Rele4\"", &boolValue)) SamSetup.rele4 = boolValue ? 1 : 0;
  
  // Обработка адресов датчиков
  if (getJsonAddressIndex(cleanJson, "\"SteamAddr\"", &indexValue)) {
    if (indexValue > 0) {
      CopyDSAddress(DSAddr[indexValue-1], SamSetup.SteamAdress);
    } else {
      SamSetup.SteamAdress[0] = 0;
    }
  }
  
  if (getJsonAddressIndex(cleanJson, "\"PipeAddr\"", &indexValue)) {
    if (indexValue > 0) {
      CopyDSAddress(DSAddr[indexValue-1], SamSetup.PipeAdress);
    } else {
      SamSetup.PipeAdress[0] = 0;
    }
  }
  
  if (getJsonAddressIndex(cleanJson, "\"WaterAddr\"", &indexValue)) {
    if (indexValue > 0) {
      CopyDSAddress(DSAddr[indexValue-1], SamSetup.WaterAdress);
    } else {
      SamSetup.WaterAdress[0] = 0;
    }
  }
  
  if (getJsonAddressIndex(cleanJson, "\"TankAddr\"", &indexValue)) {
    if (indexValue > 0) {
      CopyDSAddress(DSAddr[indexValue-1], SamSetup.TankAdress);
    } else {
      SamSetup.TankAdress[0] = 0;
    }
  }
  
  if (getJsonAddressIndex(cleanJson, "\"ACPAddr\"", &indexValue)) {
    if (indexValue > 0) {
      CopyDSAddress(DSAddr[indexValue-1], SamSetup.ACPAdress);
    } else {
      SamSetup.ACPAdress[0] = 0;
    }
  }
  
  // Поправки для термисторов
  for (uint8_t i = 0; i < 5; i++) {
    String key = "\"NTC_dT" + String(i + 1) + "\"";
    if (getJsonFloatValue(cleanJson, key.c_str(), &floatValue)) {
      SamSetup.NTC_dT[i] = floatValue;
    }
  }
  
  // Корректировка для угла поворота сервопривода
  if (getJsonStringValue(cleanJson, "\"servoDelta\"", &stringValue) && stringValue.length() > 0) {
    parseCSVStringToInt8(stringValue, SamSetup.servoDelta, 11);
  }
  
  // ADC характеристика термисторов
  if (getJsonStringValue(cleanJson, "\"m_adc\"", &stringValue) && stringValue.length() > 0) {
    parseCSVStringToUInt16(stringValue, SamSetup.m_adc, 151);
  }
  
  // Обработка режима работы
  if (getJsonIntValue(cleanJson, "\"mode\":", &intValue)) {
    SAMOVAR_MODE newMode = (SAMOVAR_MODE)intValue;
    if (SamSetup.Mode != newMode && newMode >= 0 && newMode < 5) {
      if (SamovarStatusInt != 0) {
        SendMsg(F("Попытка сменить режим при запущенном процессе! Отмена смены режима."), ALARM_MSG);
      } else {
        SamSetup.Mode = newMode;
        Samovar_Mode = newMode;
        Samovar_CR_Mode = newMode;
        program_init();
      }
    }
  }
}
void handleSave(AsyncWebServerRequest *request) {
  if (request->hasParam("plain", true)) {
    jsonstr2 = request->getParam("plain", true)->value();
    DbgMsg("Begin save", 1);
    
    parseJsonToSamSetup(jsonstr2);
    
    DbgMsg("Parsing end.", 1);
    
    ValidateSamSetup();
    UseSamSetup();
    save_profile();
    
    if (SamSetup.DUFnpn) 
      whls.setType(HIGH_PULL);
    else
      whls.setType(LOW_PULL);
      
    DbgMsg("Save end", 1);
    request->send(200, "text/plain", "Settings saved");
  } else {
    request->send(400, "text/plain", "No data received");
  }
  
  vTaskDelay(10 / portTICK_PERIOD_MS);
  if (is_reboot) {
    ESP.restart();
  }
}
void web_command(AsyncWebServerRequest *request) {
  // Защита от повторных команд
  static uint32_t last_command_time = 0;
  if (millis() - last_command_time < 1500) {
    request->send(200, "text/plain", "OK");
    return;
  }
  last_command_time = millis();
  if (request->params() == 1) {
    if (request->hasArg("start") && PowerOn) {
      if (Samovar_Mode == SAMOVAR_BEER_MODE) {
        sam_command_sync = SAMOVAR_BEER_NEXT;
      } else if (Samovar_Mode == SAMOVAR_DISTILLATION_MODE) {
        sam_command_sync = SAMOVAR_DIST_NEXT;
      } else if (Samovar_Mode == SAMOVAR_NBK_MODE) {
        sam_command_sync = SAMOVAR_NBK_NEXT;
      } else {
        sam_command_sync = SAMOVAR_START;
      }
    } else if (request->hasArg("power")) {
      if (Samovar_Mode == SAMOVAR_BEER_MODE) {
        if (!PowerOn) sam_command_sync = SAMOVAR_BEER;
        else
          sam_command_sync = SAMOVAR_POWER;
      } else if (Samovar_Mode == SAMOVAR_DISTILLATION_MODE) {
        if (!PowerOn) sam_command_sync = SAMOVAR_DISTILLATION;
        else
          sam_command_sync = SAMOVAR_POWER;
      } else if (Samovar_Mode == SAMOVAR_BK_MODE) {
        if (!PowerOn) sam_command_sync = SAMOVAR_BK;
        else
          sam_command_sync = SAMOVAR_POWER;
      } else if (Samovar_Mode == SAMOVAR_NBK_MODE) {
        if (!PowerOn) sam_command_sync = RUN_NBK;
        else
          sam_command_sync = SAMOVAR_POWER;
      } else
        sam_command_sync = SAMOVAR_POWER;
    } else if (request->hasArg("setbodytemp")) {
      sam_command_sync = SAMOVAR_SETBODYTEMP;
    } else if (request->hasArg("reset")) {
      sam_command_sync = SAMOVAR_RESET;
    } else if (request->hasArg("reboot")) {
      ESP.restart();
    } else if (request->hasArg("resetwifi")) {
      reset_wifi();
    } else if (request->hasArg("startst")) {
      sam_command_sync = SAMOVAR_SELF_TEST;
    } else if (request->hasArg("rescands")) {
      if (SamSetup.UseDS) scan_ds_adress();
      if (SamSetup.UseNTC) NTC_Init();      
    } else if (request->hasArg("stopst")) {
      stop_self_test();
    } else if (request->hasArg("mixer")) {
      if (request->arg("mixer").toInt() == 1) {
        set_mixer(true);
      } else {
        set_mixer(false);
      }
    } else if (request->hasArg("pnbk") && PowerOn) {
      if (request->arg("pnbk").toInt() == 9000) { //TODO повышаем скорость насоса на один шаг
        set_stepper_target(CurrentStepperSpeed + get_speed_from_rate(float(SamSetup.NbkDP), SamSetup.NBK_StepperStepMl), 0, 2147483640);
        } else if (request->arg("pnbk").toInt() == 8000) { //TODO понижаем скорость насоса на один шаг
          if (CurrentStepperSpeed - get_speed_from_rate(float(SamSetup.NbkDP), SamSetup.NBK_StepperStepMl) <= 0) {
            set_stepper_target(0, 0, 0);
          } else {
            set_stepper_target(CurrentStepperSpeed - get_speed_from_rate(float(SamSetup.NbkDP), SamSetup.NBK_StepperStepMl), 0, 2147483640);
          }
        } else if (request->arg("pnbk").toFloat() >= 0 && request->arg("pnbk").toInt() < 40) { // TODO устанавливаем заказанную скорость насоса
          set_stepper_target(get_speed_from_rate(float(request->arg("pnbk").toFloat()), SamSetup.NBK_StepperStepMl), 0, 2147483640);

        }
      } else if (request->hasArg("nbkopt") && PowerOn) { //TODO устанавливаем текущие М и Р как оптимальные Мо и Ро
        nbk_Mo = nbk_M;
        nbk_Po = nbk_P;
        SendMsg("Установлены оптимальные значения: " + String(fromPower(nbk_Mo),0) + String(PwrSign) + ",  " + String(nbk_Po,1) + " л/ч", WARNING_MSG);
    } else if (request->hasArg("distiller")) {
      if (request->arg("distiller").toInt() == 1) {
        sam_command_sync = SAMOVAR_DISTILLATION;
      } else {
        sam_command_sync = SAMOVAR_POWER;
      }
    } else if (request->hasArg("startbk")) {
      if (request->arg("startbk").toInt() == 1) {
        sam_command_sync = SAMOVAR_BK;
      } else {
        sam_command_sync = SAMOVAR_POWER;
      }
    } else if (request->hasArg("startnbk")) {
      if (request->arg("startnbk").toInt() == 1) {
        sam_command_sync = RUN_NBK;
      } else {
        sam_command_sync = SAMOVAR_POWER;
      }
    } else if (request->hasArg("watert")) {
      set_water_temp(request->arg("watert").toFloat());
    } else if (request->hasArg("pumpspeed")) {
      set_pump_speed(get_speed_from_rate(request->arg("pumpspeed").toFloat(), SamSetup.StepperStepMl), true);
    } else if (request->hasArg("pspd2")) {
      set_pump2_speed(get_speed_from_rate(request->arg("pspd2").toFloat(), SamSetup.StepperStepMl), true);
    } else if (request->hasArg("setcap")) {
      int8_t d_cap = request->arg("setcap").toInt();
      if (program[ProgramNum].capacity_num == 0 && d_cap == -1) d_cap=0;
      if (program[ProgramNum].capacity_num == 10 && d_cap == 1) d_cap=0;
      program[ProgramNum].capacity_num += d_cap;
      set_capacity(program[ProgramNum].capacity_num);
    } else if (request->hasArg("pause")) {
      if (PauseOn) {
        sam_command_sync = SAMOVAR_CONTINUE;
      } else {
        sam_command_sync = SAMOVAR_PAUSE;
      }
    }
    else if (request->hasArg("voltage") && SamSetup.PwrType != NO_POVER_REG) {
      set_current_power(request->arg("voltage").toFloat());
    }
    else if (request->hasArg("servo") && SamSetup.PwrType != NO_POVER_REG) {
      setServoAngle(request->arg("servo").toInt());
    }
 #ifdef USE_LUA
    else if (request->hasArg("lua")) {
      run_lua_script(request->arg("lua"));
    } else if (request->hasArg("luastr")) {
      String lstr = request->arg("luastr");
      lstr.replace("^", " ");
      run_lua_string(lstr);
    }
 #endif
  }
  request->send(200, "text/plain", "OK");
}
void web_program(AsyncWebServerRequest *request) {
  if (request->hasArg("WProgram")) {
    if (Samovar_Mode == SAMOVAR_BEER_MODE) {
      set_beer_program(request->arg("WProgram"));
      request->send(200, "text/plain", get_beer_program());
    } else if (Samovar_Mode == SAMOVAR_DISTILLATION_MODE) {
      set_dist_program(request->arg("WProgram"));
      request->send(200, "text/plain", get_dist_program());
    } else if (Samovar_Mode == SAMOVAR_NBK_MODE) {
      set_nbk_program(request->arg("WProgram"));
      request->send(200, "text/plain", get_nbk_program());
    } else {
      set_program(request->arg("WProgram"));
      request->send(200, "text/plain", get_program(MAX_PRG));
    }
  }
  if (request->hasArg("Descr")) {
    SessionDescription = request->arg("Descr");
    SessionDescription.replace("%", "&#37;");
  }
}
void calibrate_command(AsyncWebServerRequest *request) {
  bool cl = false;
  if (request->params() >= 1) {
    if (request->hasArg("stpstep")) {
      CurrentStepperSpeed = request->arg("stpstep").toInt();
    }
    if (request->hasArg("start") && startval == 0) {
      sam_command_sync = CALIBRATE_START;
    }
    if (request->hasArg("finish") && startval == 100) {
      sam_command_sync = CALIBRATE_STOP;
      cl = true;
    }
  }
  vTaskDelay(5 / portTICK_PERIOD_MS);
  if (cl) {
    int s = (Samovar_Mode == SAMOVAR_NBK_MODE ? SamSetup.NBK_StepperStepMl * 1000 : SamSetup.StepperStepMl * 100); 
    request->send(200, "text/plain", (String)s);
  } else
    request->send(200, "text/plain", "OK");
}
void get_data_log(AsyncWebServerRequest *request, String fn) {
  if (fileToAppend && fileToAppend.available())
    fileToAppend.flush();
  AsyncWebServerResponse *response;
  if (SPIFFS.exists("/" + fn)) {
    response = request->beginResponse(SPIFFS, "/" + fn, String(), true);
  } else {
    response = request->beginResponse(400, "text/plain", "");
  }
  response->addHeader(F("Content-Type"), F("application/octet-stream"));
  response->addHeader(F("Content-Description"), F("File Transfer"));
  response->addHeader(F("Content-Disposition"), "attachment; filename=\"" + fn + "\"");
  response->addHeader(F("Pragma"), F("public"));
  response->addHeader(F("Cache-Control"), F("no-cache"));
  request->send(response);
}
void get_web_interface() {
     SerialMsg("Free space:" + String(SPIFFS.totalBytes()), 1);
  bool ret = Ping.ping("web.samovar-tool.ru", 2);
  if (!ret) {
    SerialMsg(F("Нет покдлючения к интернету. Не удалось проверить обновление интерфейса. Если это первичная установка - необходимо загрузить интерфейс в Самоварыч в соответствии с инструкцией"),1);
    return;
  }
  String version;
  String local_version;
  String s = "";
  version = get_web_file("version.txt", GET_CONTENT);
  if (version == "<ERR>") return;
  SerialMsg("WEB interface version = " + String(version),1);
  File fn = SPIFFS.open("/version.txt", FILE_READ);
  if (fn) {
    local_version = fn.readStringUntil('\n');
    fn.close();
  }
  SerialMsg("Local interface version = " + String(local_version), 1);
  if (version != local_version) {
    s += get_web_file("index.htm", SAVE_FILE_OVERRIDE);
    s += get_web_file("alarm.mp3", SAVE_FILE_OVERRIDE);
    s += get_web_file("style.css", SAVE_FILE_OVERRIDE);
    s += get_web_file("beer.htm", SAVE_FILE_OVERRIDE);
    s += get_web_file("bk.htm", SAVE_FILE_OVERRIDE);
    s += get_web_file("nbk.htm", SAVE_FILE_OVERRIDE);
    s += get_web_file("brewxml.htm", SAVE_FILE_OVERRIDE);
    s += get_web_file("calibrate.htm", SAVE_FILE_OVERRIDE);
    s += get_web_file("chart.htm", SAVE_FILE_OVERRIDE);
    s += get_web_file("distiller.htm", SAVE_FILE_OVERRIDE);
    s += get_web_file("edit.htm", SAVE_FILE_OVERRIDE);
    s += get_web_file("program.htm", SAVE_FILE_OVERRIDE);
    s += get_web_file("setup.htm", SAVE_FILE_OVERRIDE);
    s += get_web_file("beer.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("bk.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("nbk.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("dist.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("init.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("rectificat.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("script.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("btn_rect_button1.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("btn_rect_button2.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("btn_beer_button1.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("btn_beer_button2.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("btn_dist_button1.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("btn_dist_button2.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("btn_bk_button1.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("btn_bk_button2.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("btn_nbk_button1.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("btn_nbk_button2.lua", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("program_fruit.txt", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("program_grain.txt", SAVE_FILE_IF_NOT_EXIST);
    s += get_web_file("program_shugar.txt", SAVE_FILE_IF_NOT_EXIST);
    if (s.length() == 0) {
      s = get_web_file("version.txt", SAVE_FILE_OVERRIDE);
    }
  }
}
String get_web_file(String fn, get_web_type type) {
  if (type == SAVE_FILE_IF_NOT_EXIST && SPIFFS.exists("/" + fn)) {
    SerialMsg("File " + fn + " already exist.",1);
    return "";
  }
  String url = "http://web.samovar-tool.ru/" + String(SAMOVAR_VERSION) + "/" + fn + "?" + micros();
  SerialMsg("url = " + url, 1);
  String s = http_sync_request_get(url);
  if (s == "<ERR>") {
    return s;
  } else {
    if (type == GET_CONTENT) {
      return s;
    } else {
      File wf = SPIFFS.open("/" + fn, FILE_WRITE);
      wf.print(s);
      wf.close();
     // DbgMsg("responseText = " + s,1);
     if (SPIFFS.exists("/" + fn)) SerialMsg("File exists",1); else SerialMsg("File not exists",1);
     SerialMsg("Free space:" + String(SPIFFS.totalBytes()-SPIFFS.usedBytes()),1);
    }
    SerialMsg("Done (L=" + String(s.length()) + ")",1);
  }
  return "";
}
String http_sync_request_get(String url) {
  asyncHTTPrequest request;
  request.setDebug(false);
  request.setTimeout(8); // Таймаут восемь секунд
  request.open("GET", url.c_str());
  unsigned long startTime = millis();
  while (request.readyState() < 1) {
    if (millis() - startTime > 4000) { // Общий таймаут 8 секунд
      DbgMsg("Timeout: readyState never reached.",1);
      return "<ERR>";
    }
    vTaskDelay(25 / portTICK_PERIOD_MS);
  }
  vTaskDelay(150 / portTICK_PERIOD_MS);
  request.send();
  vTaskDelay(150 / portTICK_PERIOD_MS);
  // Таймаут для ожидания завершения запроса (readyState == 4)
  startTime = millis();
  while (request.readyState() != 4) {
    if (millis() - startTime > 2000) { // Таймаут 4 секунд
      DbgMsg("Timeout: request not completed within 4 seconds.",1);
      return "<ERR>";
    }
    vTaskDelay(25 / portTICK_PERIOD_MS);
  }
  vTaskDelay(60 / portTICK_PERIOD_MS);
  if (request.responseHTTPcode() >= 0) {
    if (request.responseHTTPcode() != 200) {
    DbgMsg(F("responseHTTPcode = "),1);
    DbgMsg(String(request.responseHTTPcode()),1);
    DbgMsg("Content " + url + " download error",1);
      return "<ERR>";
    }
    return request.responseText();
  } else {
    DbgMsg(F("responseHTTPcode = "),1);
    DbgMsg(String(request.responseHTTPcode()),1);
    DbgMsg("Content " + url + " download error (2)",1);
    return "<ERR>";
  }  
  return "";
}
String http_sync_request_post(String url, String body, String ContentType) {
  asyncHTTPrequest request;
  request.setDebug(false);
  request.setTimeout(8);  //Таймаут восемь секунд
  request.open("POST", url.c_str());  //URL
  unsigned long startTime = millis();
  while (request.readyState() < 1) {
    if (millis() - startTime > 4000) { // Общий таймаут 8 секунд
      DbgMsg("Timeout: readyState never reached 1",1);
      return "<ERR>";
    }
    vTaskDelay(25 / portTICK_PERIOD_MS);
  }
  vTaskDelay(150 / portTICK_PERIOD_MS);
  request.setReqHeader("Content-Type", getValue(ContentType, ':', 1).c_str());
  request.send(body);
  startTime = millis();
  while (request.readyState() != 4) {
    if (millis() - startTime > 2000) { // Таймаут 4 секунд
      DbgMsg("Timeout: request not completed within 4 seconds",1);
      return "<ERR>";
    }
    vTaskDelay(25 / portTICK_PERIOD_MS);
  }
  vTaskDelay(60 / portTICK_PERIOD_MS);
  if (request.responseHTTPcode() >= 0) {
    return request.responseText();
  } else {
    return "<ERR>";
  }
}
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
  if (type == WS_EVT_CONNECT) {
    Serial.printf("ws[%s][%u] connect\n", server->url(), client->id());
    client->printf("Hello Client %u :)", client->id());
    client->ping();
  } else if (type == WS_EVT_DISCONNECT) {
    Serial.printf("ws[%s][%u] disconnect\n", server->url(), client->id());
  } else if (type == WS_EVT_ERROR) {
    Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data);
  } else if (type == WS_EVT_PONG) {
    Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len) ? (char *)data : "");
  } else if (type == WS_EVT_DATA) {
    AwsFrameInfo *info = (AwsFrameInfo *)arg;
    String msg = "";
    if (info->final && info->index == 0 && info->len == len) {
      //the whole message is in a single frame and we got all of it's data
      Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT) ? "text" : "binary", info->len);

      if (info->opcode == WS_TEXT) {
        for (size_t i = 0; i < info->len; i++) {
          msg += (char)data[i];
        }
      } else {
        char buff[4];
        for (size_t i = 0; i < info->len; i++) {
          sprintf(buff, "%02x ", (uint8_t)data[i]);
          msg += buff;
        }
      }
      Serial.printf("%s\n", msg.c_str());

      if (info->opcode == WS_TEXT)
        client->text(F("I got your text message"));
      else
        client->binary(F("I got your binary message"));
    } else {
      //message is comprised of multiple frames or the frame is split into multiple packets
      if (info->index == 0) {
        if (info->num == 0)
          Serial.printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT) ? "text" : "binary");
        Serial.printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len);
      }

      Serial.printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT) ? "text" : "binary", info->index, info->index + len);

      if (info->opcode == WS_TEXT) {
        for (size_t i = 0; i < len; i++) {
          msg += (char)data[i];
        }
      } else {
        char buff[4];
        for (size_t i = 0; i < len; i++) {
          sprintf(buff, "%02x ", (uint8_t)data[i]);
          msg += buff;
        }
      }
      Serial.printf("%s\n", msg.c_str());

      if ((info->index + len) == info->len) {
        Serial.printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len);
        if (info->final) {
          Serial.printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT) ? "text" : "binary");
          if (info->message_opcode == WS_TEXT)
            client->text(F("I got your text message"));
          else
            client->binary(F("I got your binary message"));
        }
      }
    }
  }
}