//*************Объявления
  #include <Arduino.h>
  #include <esp_wifi.h>

  #if defined(ARDUINO_ESP32S3_DEV)
  #else
  #include "esp32/rom/rtc.h"
  #include <driver/dac.h>
  #endif

  #include <driver/touch_sensor.h>
  #include <esp32-hal-cpu.h>
  #include <esp_heap_caps.h>
  #include <esp_heap_caps_init.h>
  #include "esp_log.h"
  #include <Wire.h>
  #include <OneWire.h>
  #include <DallasTemperature.h>
  #include <Adafruit_Sensor.h>
  #include <WiFi.h>
  #include <WiFiUdp.h>
  #include <AsyncTCP.h>
  #include <ESPAsyncWebServer.h>
  #include <ESPmDNS.h>
  #include <Update.h>
  //#include <ESPping.h>
  #include <GyverButton.h>
  #include <GyverPID.h>
  #include <PID_v1.h>
  #include <PID_AutoTune_v0.h>

  #include "Settings.h"

  #ifndef __SAMOVAR_DEBUG
  #define ARDUINOTRACE_ENABLE 0  // Disable all traces
  #endif
  #include <ArduinoTrace.h>

  #ifdef __SAMOVAR_NOT_USE_WDT
  #include "soc/rtc_wdt.h"
  #include <esp_task_wdt.h>
  #endif

  #include "lua.h"

  #include <NTPClient.h>
  WiFiUDP ntpUDP;
  NTPClient NTP(ntpUDP);
  #include <Adafruit_BMP085_U.h>
  #include <XGZP6897D.h>
  XGZP6897D pressure_sensor(32);
  #include <ArduinoOTA.h>
  #include <BlynkSimpleEsp32.h>
  #include <simple_queue.h>
 
  #include <UrlEncode.h>

  #include "pumppwm.h"
  #include "rect.h"
  #include "distiller.h"
  #include "beer.h"
  #include "BK.h"
  #include "nbk.h"
  #include "SPIFFSEditor.h"
  #include "debug.h"
  void taskButton(void *pvParameters);
  SemaphoreHandle_t btnSemaphore;

  hw_timer_t *timer = NULL;
  hw_timer_t *timer2 = NULL;  
  portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
  portMUX_TYPE timerMux2 = portMUX_INITIALIZER_UNLOCKED;

  //flag for saving data
  void PowerStatus_Loop(void *parameter);
  void WebServerInit(void);
  void calibrate();
  void set_body_temp();
  void distiller_finish();
  void beer_finish();
  void bk_finish();
  void nbk_finish();
  void verbose_print_reset_reason();
  void set_alarm();
  float get_steam_alcohol(float t);
  float get_alcohol(float t);
  void startService(void);
  String http_sync_request_get(String url);
  void FS_init(void);
  void get_web_interface();
  void get_current_power();
  void InitPower();
    void setupTechnicalWebHandlers(); //PreSetup
    void handleTechnicalPage(AsyncWebServerRequest *request);
    void handleWifiSave(AsyncWebServerRequest *request);
    void handleFormatFS(AsyncWebServerRequest *request);
    void handleFileUpload(AsyncWebServerRequest *request);
    void handleDeleteFile(AsyncWebServerRequest *request);
  static void cleanupNVS();
  void run_program(uint8_t num);
  void pump_calibrate(int stpspeed);
  void reset_sensor_counter();
  String get_program(uint8_t s);
  void create_data();
  void setup_bugtrace(); //записываем в SPIFFS bugtrace
    void NTC_Init();
    void NTC_Refresh(float correctT);
    String get_Samovar_Status();
    void set_power(bool On);
    String append_data();
    void CopyDSAddress(const uint8_t* DevSAddress, uint8_t* DevTAddress);
    void set_beer_program(String WProgram);
    void set_program(String WProgram);
    void set_dist_program(String WProgram);
    void set_nbk_program(String WProgram);
    String getDSAddress(DeviceAddress deviceAddress);
    void setupOpenLog(void);
    void createFile(char* fileName);
    void set_capacity(uint8_t cap);
    void init_pump_pwm(uint8_t pin, int freq);
    void set_pump_pwm(float duty);
    float getBeerCurrentTemp();
    void DbgMsg(const String& Msg, bool ln);
    void connectToMqtt();
    void IRAM_ATTR StepperTicker(void);
    void IRAM_ATTR Stepper2Ticker(void);
    void IRAM_ATTR WFpulseCounter();
    void IRAM_ATTR isrWHLS_TICK();
    void sensor_init(void);
    void printTaskMemoryUsage();
    void StatusUDPStabilizer();
    void UDP_Tick();
    void UDPStab_Stop();
    float get_dist_remaining_time();
    float get_dist_predicted_total_time();
//----------------------------------------------------------------------------------------------------------
//---------- Кольцевой буфер сообщений 
  SimpleStringQueue msg_q(4, 512);

  const uint8_t PSRAM_BUFFER_SIZE = 10;
  #if BOARD == ESP32S3
  const uint8_t RAM_BUFFER_SIZE = 6;
  #else
  const uint8_t RAM_BUFFER_SIZE = 4;
  #endif

  SimpleQueue* msgQueue = nullptr;

  void initMsgSystem() {
      msgMutex = xSemaphoreCreateMutex();
      
      if (xSemaphoreTake(msgMutex, portMAX_DELAY) == pdTRUE) {
          // Проверяем доступность PSRAM
          if (psramFound()) {
              Serial.println("PSRAM found - creating large buffer (" + String(PSRAM_BUFFER_SIZE) + " messages)");
              
              // Выделяем память в PSRAM
              void* psramBuffer = ps_malloc(PSRAM_BUFFER_SIZE * sizeof(Message));
              
              if (psramBuffer != nullptr) {
                  msgQueue = new SimpleQueue(PSRAM_BUFFER_SIZE, psramBuffer, PSRAM_BUFFER_SIZE * sizeof(Message));
                  Serial.println("PSRAM buffer allocated successfully");
              } else {
                  Serial.println("Failed to allocate PSRAM, falling back to RAM");
                  msgQueue = new SimpleQueue(RAM_BUFFER_SIZE);
              }
          } else {
              Serial.println("No PSRAM - creating small buffer (" + String(RAM_BUFFER_SIZE) + " messages)");
              msgQueue = new SimpleQueue(RAM_BUFFER_SIZE);
          }
          
          // Проверяем успешность инициализации очереди
          if (msgQueue != nullptr && !msgQueue->isInitialized()) {
              Serial.println("ERROR: Queue initialization failed!");
              delete msgQueue;
              msgQueue = nullptr;
          }
          
          xSemaphoreGive(msgMutex);
      }
  }

  void SendMsg(const String& ms, MESSAGE_TYPE msg_type) {
      String m = ms, MsgPl;
      if (ms=="") return;
      if (SamSetup.UseMQTT) {
          MsgPl = m;
          MsgPl.replace(",", ";");
          MqttSendMsg(MsgPl + "," + String(msg_type), "msg");
      }
      
      if (SamSetup.UseTg) {
          switch (msg_type) {
              case ALARM_MSG: MsgPl = F("*Тревога!*\n"); break;
              case WARNING_MSG: MsgPl = F("*Предупреждение!*\n"); break;
              case NOTIFY_MSG: MsgPl = ""; break;
              default: MsgPl = "";
          }
          MsgPl += " Самоварыч - " + m;

          if (xSemaphoreTake(xMsgSemaphore, (TickType_t)(50 / portTICK_RATE_MS)) == pdTRUE) {
              msg_q.push(MsgPl.c_str());
              xSemaphoreGive(xMsgSemaphore);
          }
      }
      
      if (msgQueue == nullptr) return;
      
      if (xSemaphoreTake(msgMutex, 500 / portTICK_RATE_MS) == pdTRUE) {
          Message newMsg;
          m = String(NTP.getFormattedTime()) + " " + ms;
          strncpy(newMsg.text, m.c_str(), 300);
          newMsg.text[300] = '\0';
          newMsg.type = (uint8_t)msg_type; // Приводим к uint8_t
          msgQueue->push(&newMsg);
          xSemaphoreGive(msgMutex);
      }
  }

  String MsgGet(MESSAGE_TYPE* msg_type, bool clear = true) {
      String result = "";
      if (msgQueue == nullptr) {
          if (msg_type != nullptr) {
              *msg_type = NONE_MSG;
          }
          return result;
      }
      
      if (xSemaphoreTake(msgMutex, 50 / portTICK_RATE_MS) == pdTRUE) {
          Message retrievedMsg;
          bool messageFound = false;
          
          if (clear) {
              messageFound = msgQueue->pop(&retrievedMsg);
          } else {
              messageFound = msgQueue->peek(&retrievedMsg);
          }
          
          if (messageFound) {
              result = String(retrievedMsg.text);
              if (msg_type != nullptr) {
                  *msg_type = (MESSAGE_TYPE)retrievedMsg.type; // Приводим обратно к MESSAGE_TYPE
              }
          } else {
              if (msg_type != nullptr) {
                  *msg_type = NONE_MSG;
              }
          }
          xSemaphoreGive(msgMutex);
      } else {
          if (msg_type != nullptr) {
              *msg_type = NONE_MSG;
          }
      }
      return result;
  }

  uint16_t getMessageCount() {
      if (msgQueue == nullptr) return 0;
      return msgQueue->getCount();
  }

  bool hasMessages() {
      if (msgQueue == nullptr) return false;
      return !msgQueue->isEmpty();
  }

  uint16_t getBufferCapacity() {
      if (msgQueue == nullptr) return 0;
      return msgQueue->getCapacity();
  }

  String getBufferInfo() {
      if (msgQueue == nullptr) return "Buffer not initialized";
      
      String info = "Buffer: ";
      info += String(getMessageCount());
      info += "/";
      info += String(getBufferCapacity());
      info += " messages, ";
      
      if (psramFound() && getBufferCapacity() == PSRAM_BUFFER_SIZE) {
          info += "PSRAM";
      } else {
          info += "RAM";
      }
      
      return info;
  }
//-----------Дисплеи
  
  void reset_lcd() {
    if ( xSemaphoreTake( xI2CSemaphore, ( TickType_t ) (50 / portTICK_RATE_MS)) == pdTRUE) {
      if (SamSetup.disp == 1) {//#ifdef USE_DISP_LC
      LQ.begin(20, 4);
      LQ.init();
      LQ.clear();
      LQ.setCursor(0,0);LQ.print("Tp:        dT:");
      LQ.setCursor(0,1);LQ.print("Tc:       TCA:");
      LQ.setCursor(0,2);LQ.print("Tk:        Tw:");
      LQ.setCursor(0,3);LQ.print("Pmp         W:");
      } //#endif
      if (SamSetup.disp == 2) {//#ifdef USE_DISPLAY
      LD.printString_6x8("Tп:      dT:       °C", 1, 2);
      LD.printString_6x8("Tц:      TCA:      °C", 1, 3);
      LD.printString_6x8("Tк:      Tв:       °C", 1, 4);
      LD.printString_6x8("Pmp       W:       Вт", 1, 5);
      if (use_pressure_sensor) LD.printString_6x8("P:        ", 61, 6);
      StIP = WiFi.localIP().toString();
      LD.printString_6x8((StIP + "    ").c_str(), 1, 7); 
      LD.printString_6x8("R    ", 1+16*6, 7);
      LD.printString_6x8("          ", 1, 6);
      LD.printString_6x8((WiFi.SSID()).c_str(), 1, 6);
      } //#endif
      xSemaphoreGive(xI2CSemaphore);
    }
  }
  void DisplayInit() {
    if (SamSetup.disp == 2) {
    LD.init();
    for (uint8_t i = 0; i < 8; i++) LD.printString_6x8(F("                     "), 1, i); //очистка дисплея  
    LD.printString_6x8(F("   -=САМОВАРЫЧ=-"), 1, 0);
    }
    if (SamSetup.disp == 1) {
    LQ.init();
    LQ.begin(20, 4);
    LQ.backlight();
    LQ.clear();
    LQ.setCursor(0,0);LQ.print(F("   -=SAMOVARICH=-"));
    }
  }
//-----------Преобразования переменных, строк, пересчеты
  String getValue(const String& data, char separator, int index) {//Получить подстроку через разделитель
    int found = 0;
    int strIndex[] = { 0, -1 };
    int maxIndex = data.length() - 1;

    for (int i = 0; i <= maxIndex && found <= index; i++) {
      if (data.charAt(i) == separator || i == maxIndex) {
        ++found;
        strIndex[0] = strIndex[1] + 1;
        strIndex[1] = (i == maxIndex) ? i + 1 : i;
      }
    }
    return found > index ? data.substring(strIndex[0], strIndex[1]) : "";
  }
  void FormatTimeString(int hours, int minutes, String &output) {// функция для форматирования времени
    static String h, m;
    if (hours < 10) h = "0" + String(hours);
    else h = String(hours);
    if (minutes < 10) m = "0" + String(minutes);
    else m = String(minutes);
    output = h + ":" + m;
  }
  String StrToCharLen(String S, uint8_t targetLen) {//Подгонка строки к нужной длине в символах с учетом двухбайтных русских символов
    String result = "";
    uint8_t charCount = 0;
    uint8_t i = 0;
    uint8_t len = S.length();
    
    // Добавляем символы пока не достигнем нужной длины
    while (i < len && charCount < targetLen) {
      if (bitRead(S.charAt(i), 7)) { // Русский символ
        if (i + 1 < len) {
          result += S.charAt(i);
          result += S.charAt(i + 1);
          i += 2;
        } else {
          break;
        }
      } else { // ASCII символ
        result += S.charAt(i);
        i += 1;
      }
      charCount++;
    }
    while (charCount < targetLen) {
      result += " ";
      charCount++;
    }
    return result.c_str();
  }
  float get_steam_alcohol(float t) {
    if (!boil_started) return 100;

    float r;
    float t1;
    float s;
    float k;
    uint8_t t0;

    t1 = t;
    t = get_temp_by_pressure(0, t, bme_pressure);

    if (t >= 99 && t < 99.84) {
      s = 11.21;
      k = -13;
      t0 = 99;
    } else if (t >= 98 && t < 99) {
      s = 20.744;
      k = -9.84;
      t0 = 98;
    } else if (t >= 97 && t < 98) {
      s = 29.936;
      k = -9;
      t0 = 97;
    } else if (t >= 96 && t < 97) {
      s = 39.781;
      k = -9.6;
      t0 = 96;
    } else if (t >= 95 && t < 96) {
      s = 44.628;
      k = -4.847;
      t0 = 95;
    } else if (t >= 94 && t < 95) {
      s = 49.2775;
      k = -4.65;
      t0 = 94;
    } else if (t >= 93 && t < 94) {
      s = 53.76;
      k = -4.483;
      t0 = 93;
    } else if (t >= 92 && t < 93) {
      s = 57.539;
      k = -3.778;
      t0 = 92;
    } else if (t >= 91 && t < 92) {
      s = 61.22;
      k = -3.682;
      t0 = 91;
    } else if (t >= 90 && t < 91) {
      s = 66.4633;
      k = -5.244;
      t0 = 90;
    } else if (t >= 89 && t < 90) {
      s = 69.334;
      k = -2.87;
      t0 = 89;
    } else if (t >= 88 && t < 89) {
      s = 70.82;
      k = -1.4857;
      t0 = 88;
    } else if (t >= 87 && t < 88) {
      s = 72.42;
      k = -1.6;
      t0 = 87;
    } else if (t >= 86 && t < 87) {
      s = 75.03;
      k = -2.66;
      t0 = 86;
    } else if (t >= 85 && t < 86) {
      s = 77.21;
      k = -2.2;
      t0 = 85;
    } else if (t >= 84 && t < 85) {
      s = 79.88;
      k = -2.67;
      t0 = 84;
    } else if (t >= 83 && t < 84) {
      s = 81.08;
      k = -1.2;
      t0 = 83;
    } else {
      s = 82;
      k = -1;
      t0 = 82;
    }

    if (t > 100) {
      r = 0;
    } else if (t > 99.84) {
      r = get_alcohol(t1);
    } else {
      r = s + k * (t - t0);
    }
    if (r < 0) r = 0;
    return r;
  }
  float get_alcohol(float t) {
    if (!boil_started) return 100;
    t = get_temp_by_pressure(0, t, bme_pressure);
    float r;
    float k;
    k = (t - 89) / 6.49;

    r = 17.26 - k * (18.32 - k * (7.81 - k * (1.77 - k * (4.81 - k * (2.95 + k * (1.43 - k * (0.8 + 0.05 * k)))))));  // формула Макеода для вычисления крепости
    if (r < 0) r = 0;
    r = float(round(r * 10)) / 10;  // округляем до одного знака после запятой
    return r;

    //reverse
    //t[град]=85,37 - 3,75 * Ti + 1,48 * Ti ^ 2 - 0,32 * Ti ^ 3 +0,41 * Ti ^ 4 - 0,92 * Ti ^ 5 +0,32 * Ti ^ 6 + 0,1 * Ti ^ 7 - 0,05 * Ti ^ 8
    //Где  Ti = ( К%об - 43,15 ) / 30,18
    //более точно
    // Темп=105.47* крепость^-0.065 - применима для растворов крепче 2%мас.
  }
//-----------Общая логика
  void setup() {
    Serial.begin(115200);
    vTaskDelay(50 / portTICK_PERIOD_MS);
    esp_log_level_set("i2c.master", ESP_LOG_NONE);
    esp_log_level_set("gpio", ESP_LOG_VERBOSE);
    #ifdef __SAMOVAR_NOT_USE_WDT
    esp_task_wdt_init(1, false);
    esp_task_wdt_init(2, false);
    rtc_wdt_protect_off();
    rtc_wdt_disable();
    disableCore0WDT();
    disableCore1WDT();
    #endif
    esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);// Запрет на сон
    xMsgSemaphore = xSemaphoreCreateBinaryStatic(&xMsgSemaphoreBuffer); //семафор для сообщений
    xSemaphoreGive(xMsgSemaphore);
    xI2CSemaphore = xSemaphoreCreateBinaryStatic(&xI2CSemaphoreBuffer); //семафор для шины I2C
    xSemaphoreGive(xI2CSemaphore);
    xSemaphoreAVR = xSemaphoreCreateBinaryStatic(&xSemaphoreBufferAVR); //семафор для Регуляторов мощности
    xSemaphoreGive(xSemaphoreAVR);
    xSemaphoreSerial = xSemaphoreCreateBinaryStatic(&xSemaphoreBufferSerial); //семафор для отладочных сообщений
    xSemaphoreGive(xSemaphoreSerial);
    msgMutex = xSemaphoreCreateMutex(); //Семафор для WEB мессаг
    heap_caps_enable_nonos_stack_heaps();
    
    Serial.println();
    Serial.println("Start...");
    if (SamSetup.dbg) esp_log_level_set("*", ESP_LOG_VERBOSE);
    Serial.printf("ESP32 Chip model = %s Rev %d\n", ESP.getChipModel(), ESP.getChipRevision());
    Serial.print("Chip ID: ");  Serial.println(chipId);
    if (psramFound()) DbgMsg("PSRAM доступна!",1); else DbgMsg("PSRAM не обнаружена!",1);
    setup_bugtrace(); // Если был reset запишет bugtrace в файл bugtrace.txt
    
    #if defined(ARDUINO_ESP32S3_DEV)
    #else
    touch_pad_intr_disable();
    #endif
    Wire.begin(LCD_SDA, LCD_SCL);


    DbgMsg("Init file system...",1);
    FS_init();  // Включаем работу с файловой системой  
    bool wifiAP = false;
    if (SamSetup.UseBTN) {//Если при старте нажата кнопка - Самоварыч запустится в режиме AP
      btn.tick();  // отработка нажатия
      if (btn.isPress()) {
        wifiAP = true;
        vTaskDelay(2000);
      }
    }
    DbgMsg("Allocate timers...",1);
      // Настройте предделитель на 80, четверть ESP32 — это частота 80 МГц
      // 80000000 / 80 = 1000000 tics / seconde
    #if (defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 3))
      timer = timerBegin(1000000);
      timerAttachInterrupt(timer, &StepperTicker);
      timer2 = timerBegin(1000000);
      timerAttachInterrupt(timer2, &Stepper2Ticker);
    #else  // ESP_ARDUINO_VERSION_MAJOR >= 3
      timer = timerBegin(2, 80, true);
      timerAttachInterrupt(timer, &StepperTicker, true);
      timer2 = timerBegin(0, 80, true);
      timerAttachInterrupt(timer2, &Stepper2Ticker, true);
    #endif
      ESP32PWM::allocateTimer(0);
      ESP32PWM::allocateTimer(1);
      ESP32PWM::allocateTimer(2);
      //ESP32PWM::allocateTimer(3);

    DbgMsg("Read config... ",1);
    read_config(); //Читаем сохраненную конфигурацию из NVS, там же инициализация шим насоса воды

    DisplayInit(); //Запуск дисплея

    DbgMsg("Init Culler", 1);
    if (SamSetup.PWM_Cull)
    Culler_PWM.attachPin(CULL_PIN, 2, 1, 50, 10);  // channel 2, timer 110- разрядность 0-1023, 50-частота

    DbgMsg("Init Servo", 1);
    servo_pwm.attachPin(SERVO_PIN, 0, 0, 50, 14);  // pin, channel, timer, freq, bits
    set_capacity(0);

    if (SamSetup.dbg) { // отладочная инфа на шим
      printDetailedPWMInfo();
      ESP32PWM::printTimerAllocation();
    }
    DbgMsg("Init WiFi... ",1);

    InitWiFi(wifiAP);

    DbgMsg("Init power stabilizer...",1);
    InitPower(); //Инициализация портов, единиц измерения и множителя инкремента    
    DbgMsg("Sensors init... ",1);
    sensor_init(); //инициализация датчиков

    stepper.disable();
    stepper2.disable();
    //Запускаем таск для обработки нажатия кнопки 
    xTaskCreatePinnedToCore(
      taskButton,       /* Function to implement the task */
      "taskButton",     /* Name of the task */
      1750,             /* Stack size in words */
      NULL,             /* Task input parameter */
      1,                /* Priority of the task */
      &SysTickerButton, /* Task handle. */
      1);               /* Core where the task should run */

    pinMode(LUA_PIN, INPUT);
    #if BOARD == ESP32S3
    pinMode(LUA_PIN2, INPUT);
    pinMode(LUA_PIN3, INPUT);
    pinMode(BARDA_LEVEL_UP, INPUT);
    pinMode(BARDA_LEVEL_DWN, INPUT);
    pinMode(WATERSENSOR_PIN, INPUT);
    #endif
    //Инициализируем ноги для реле  
    pinMode(RELE_CHANNEL1, OUTPUT);  digitalWrite(RELE_CHANNEL1, !SamSetup.rele1);
    pinMode(RELE_CHANNEL2, OUTPUT);  digitalWrite(RELE_CHANNEL2, !SamSetup.rele2);
    pinMode(RELE_CHANNEL3, OUTPUT);  digitalWrite(RELE_CHANNEL3, !SamSetup.rele3);
    pinMode(RELE_CHANNEL4, OUTPUT);  digitalWrite(RELE_CHANNEL4, !SamSetup.rele4);
    pinMode(BZZ_PIN, OUTPUT);  digitalWrite(BZZ_PIN, LOW);  //Инициализируем ногу для пищалки

    DbgMsg("init Msg System... ",1);
    initMsgSystem();  //Кольцевой буфер на 10 сообщений в PSRAM, либо на 6/4 в RAM
    DbgMsg(getBufferInfo(), 1);// Вывод информации о буфере
    SerialMsg(F("Samovar started"),1);
    for (uint8_t i = 0; i < 17; i = i + 8) { chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; }

    if (!MDNS.begin(host)) {  //http://samovarich.local
      SerialMsg(F("Error setting up MDNS responder!"),1);
    } else {
      DbgMsg(F("mDNS responder started"),1);
    }
    DbgMsg("Init Blynk...",1);
    InitBlynk();
    DbgMsg("Init Telegram...",1);
    InitTelegram();
    DbgMsg("Init OTA...",1);
    InitOTA();
    alarm_event = false;
    startService();//,,,,,Зачем здесь запускать насос?
    startService2();
    samovar_reset();
    DbgMsg("WebServer Init...",1);
    WebServerInit();
    setupTechnicalWebHandlers();// объявление техстраницы
    Samovar_CR_Mode = Samovar_Mode;
  
    set_power_mode(POWER_SLEEP_MODE);// если регулятор вдруг работает, гасим его (на случай внезапного ресета Самовара)

    if (SamSetup.WSerial) {
      WebSerial.begin(&server);
      WebSerial.onMessage(recvMsg);
    }

    attachInterrupt(WATERSENSOR_PIN, WFpulseCounter, FALLING); //вешаем прерывание на изменения датчика потока воды

    //Задаем параметры для сенсора уровня флегмы
    if (SamSetup.DUFnpn)  whls.setType(HIGH_PULL); else  whls.setType(LOW_PULL);
    whls.setDebounce(50);  //игнорируем дребезг
    whls.setTickMode(MANUAL);
    whls.setTimeout(WHLS_ALARM_TIME * 1000);  //время, через которое сработает тревога по уровню флегмы
    attachInterrupt(WHEAD_LEVEL_SENSOR_PIN, isrWHLS_TICK, CHANGE);  //вешаем прерывание на изменение датчика уровня флегмы

    #if BOARD == ESP32S3 //Датчики уровня барды
    BLU_s.setType(LOW_PULL);
    BLU_s.setDebounce(50);  //игнорируем дребезг
    BLU_s.setTickMode(MANUAL);
    BLU_s.setTimeout(1000);  //время, через которое сработает, 1 сек
    BLD_s.setType(LOW_PULL);
    BLD_s.setDebounce(50);  //игнорируем дребезг
    BLD_s.setTickMode(MANUAL);
    BLD_s.setTimeout(1000);  //время, через которое сработает, 1 сек
    #endif

    DbgMsg("init Mqtt...",1);
    if (WiFi.status() == WL_CONNECTED) {
      initMqtt();
    }

    DbgMsg("init NTP...",1);
    NTP.setTimeOffset(SamSetup.TimeZone * 3600);
    NTP.setUpdateInterval(1800000);//30 min
    NTP.begin(); 
    delay(100);
    NTP.update();

    if (SamSetup.LUA) {
    DbgMsg("init LUA...",1);
    lua_init();
    }

    SerialMsg("Samovar ready.",1);

    used_byte = SPIFFS.usedBytes();
    ReadPrgFromFS(); // Читаем сохраненную программу 
    verbose_print_reset_reason();
    //DbgMsg("Размер SamSetup: " + String(sizeof(SamSetup)),1);
    #ifdef USE_DISP_LC
    delay(3000);//Задержка для прочтения юзером IP на Liquid Cristal
    #endif
    reset_lcd();// выводим заголовки на дисплей

      //Запускаем таск считывания параметров регулятора
    xTaskCreatePinnedToCore(
      PowerStatus_Loop, /* Function to implement the task */
      "PowerStatus_Loop",  /* Name of the task */
      3000,               /* Stack size in words */
      NULL,               /* Task input parameter */
      1,                  /* Priority of the task */
      &PowerStatusTask,   /* Task handle. */
      0);                 /* Core where the task should run */
    //Запускаем таск для чтения датчиков и различных проверок
    xTaskCreatePinnedToCore(
      Sensors_Loop, /* Function to implement the task */
      "Sensors_Loop",      /* Name of the task */
      5000,             /* Stack size in words */
      NULL,             /* Task input parameter */
      1,                /* Priority of the task */
      &SysTickerTask1,  /* Task handle. */
      0);               /* Core where the task should run */ //И здесь 0 ядро!!!
    //Запускаем таск для сетевых задач
    xTaskCreatePinnedToCore(
      NET_Loop,  /* Function to implement the task */
      "NET_Loop", /* Name of the task */
      6000,             /* Stack size in words *///6000
      NULL,             /* Task input parameter */
      1,                /* Priority of the task */
      &GetClockTask1,   /* Task handle. */
      1);               /* Core where the task should run */
      if (SamSetup.dbg) printTaskMemoryUsage();//Выводим у кого сколько стека свободно
  }
  void stop_process(String reason) {// Остановка любого процесса с общим набором действий
    SamovarStatusInt = 0;
    set_power(false);
    reset_sensor_counter();
    SendMsg(reason, NOTIFY_MSG);
  }
  void CalculateProgressAndTime() {//  функция расчета прогресса и времени
    static float wp;
    static int hi;
    if (Samovar_Mode == SAMOVAR_BEER_MODE) { // Режим ПИВА - расчет по времени
      if (program[ProgramNum].Time > 0 && begintime > 0) {
        unsigned long elapsed = millis() - begintime;
        float elapsedMinutes = elapsed / 60000.0;
        wp = elapsedMinutes / program[ProgramNum].Time;
      } else {
        wp = 0;
      }    
      WthdrwlProgress = wp * 100;
      WthdrwTime = program[ProgramNum].Time * (1 - wp);

    // Режим РЕКТИФИКАЦИИ - расчет по шагам или времени
    } else if (Samovar_Mode == SAMOVAR_RECTIFICATION_MODE && (TargetStepps > 0 || program[ProgramNum].WType == "P")) {
      if (program[ProgramNum].WType == "P") {
        if (t_min > millis()) { // Для паузы - расчет по времени
          unsigned long remaining = t_min - millis();
          WthdrwTime = remaining / 60000.0; // в минутах
          if (WthdrwTime > program[ProgramNum].Time) WthdrwTime = program[ProgramNum].Time;
          wp = 1 - (WthdrwTime / program[ProgramNum].Time);
        } else {
          WthdrwTime = 0;
          wp = 1;
        }
      } else {
        if (TargetStepps > 0) { // Для отбора - расчет по шагам с учетом реальной скорости
          wp = (float)CurrentStepps / (float)TargetStepps;
          if (CurrentStepperSpeed > 0 && CurrentStepps < TargetStepps) { // РАСЧЕТ ВРЕМЕНИ С УЧЕТОМ СКОРОСТИ НАСОСА
            int remainingSteps = TargetStepps - CurrentStepps;
            WthdrwTime = (float)remainingSteps / ((float)CurrentStepperSpeed*3600); // Время в минутах = оставшиеся шаги / скорость (шагов в минуту)
          } else {
            WthdrwTime = program[ProgramNum].Time * (1 - wp);
          }
        } else {
          wp = 0;
          WthdrwTime = program[ProgramNum].Time;
        }
      }
      WthdrwlProgress = wp * 100;
    } else {
        WthdrwlProgress = 0;
        WthdrwTime = 0;
        WthdrwTimeAll = 0;
        WthdrwTimeS = "";
        WthdrwTimeAllS = "";
    }
    if (Samovar_Mode == SAMOVAR_BEER_MODE || Samovar_Mode == SAMOVAR_RECTIFICATION_MODE) {// Расчет общего времени для всех режимов
      WthdrwTimeAll = WthdrwTime;
      for (uint8_t i = ProgramNum + 1; i < ProgramLen; i++) {
        WthdrwTimeAll += program[i].Time;
      }
      hi = (int)WthdrwTime; // Форматирование времени текущей строки
      FormatTimeString(hi, (int)((WthdrwTime - hi) * 60), WthdrwTimeS);
      hi = (int)WthdrwTimeAll; // Форматирование общего времени
      FormatTimeString(hi, (int)((WthdrwTimeAll - hi) * 60), WthdrwTimeAllS);
    }
  }
  void WaterSratusCheck(){//Проверим, что вода подается
    if (SamSetup.UseWS) {
      if ( PowerOn && SamSetup.UseWS && WFAlarmCount > WF_ALARM_COUNT) {
        set_buzzer(true);
        //Если с водой проблемы - выключаем нагрев, пусть оператор разбирается
        sam_command_sync = SAMOVAR_POWER;
        SendMsg(("Аварийное отключение! Прекращена подача воды."), ALARM_MSG);
      }
    }
  }
  void WaterTempAlarmPowerReg() {//Снижение мощности при перегреве воды
        //сбросим паузу события безопасности
    if (alarm_t_min > 0 && alarm_t_min <= millis()) alarm_t_min = 0;
    if ((WaterSensor.avgTemp >= SamSetup.Alrm_Wt_T - 5) && PowerOn && alarm_t_min == 0) {
      set_buzzer(true);
      //Если уже реагировали - надо подождать 30 секунд, так как процесс инерционный
      SendMsg(("Критическая температура воды!"), WARNING_MSG);
      if (SamSetup.PwrType != NO_POVER_REG) {
          if (WaterSensor.avgTemp >= SamSetup.Alrm_Wt_T) {
            set_buzzer(true);
            SendMsg("Критическая температура воды! Ошибка подачи воды. " + (String)PwrMSG_str + " снижаем с " + (String)target_power_volt, ALARM_MSG);
            //Попробуем снизить напряжение регулятора на 5 вольт, чтобы исключить перегрев колонны.
            if (SamSetup.PwrType >= STAB_AVR)
            set_current_power(target_power_volt - target_power_volt / 100 * 8);
            else
            set_current_power(target_power_volt - 5 * PwrFactor);
          }
      }
      alarm_t_min = millis() + 1000 * 30;
    }
  }
  void WaterPumpReg() {// регулируем водяной насос      
    if (SamSetup.UseWP) {
    //Устанавливаем ШИМ для насоса в зависимости от температуры воды
      if (valve_status) { // если вода включена
        // Если Т в ТСА больше предела и Т в ТСА больше Т воды (?) - крутим водяной насос усерднее, будто Т воды выше на 3 гр.
        if (ACPSensor.avgTemp > SamSetup.SetACPTemp && ACPSensor.avgTemp > WaterSensor.avgTemp) set_pump_speed_pid(SamSetup.SetWaterTemp + 3);
        else
          set_pump_speed_pid(WaterSensor.avgTemp); // иначе крутим как обычно
      }
    }
  }
   void CheckTWatherPumpOFF() { //Отключение насоса по снижению Т воды
    // Если нагрев выключен и это не самотестирование и вода включена и Т воды на 20 и более гр. ниже уставки
    if (!PowerOn && !is_self_test && valve_status && WaterSensor.avgTemp <= SamSetup.Trg_W_T - SamSetup.D_Cls_Vlv) {
      open_valve(false, true); //призыв закрыть воду либо закрытие клапана
      if (SamSetup.UseWP) {
          if (pump_started) set_pump_pwm(0); // стоп водяной насос
      }
    }
  }
  void set_alarm() {// Установить аварию
    // выключаем питание, выключаем воду, взводим флаг аварии
    if (PowerOn) {
      sam_command_sync = SAMOVAR_POWER;
    }
    set_power(false);
    alarm_event = true;
    open_valve(false, true);
    set_stepper_target(0, 0, 0);  
    set_stepper2_target(0, 0, 0);
    if (SamSetup.UseWP) {
      set_pump_pwm(0);
    }
    SendMsg(("Аварийное отключение!"), ALARM_MSG);
  }
  void set_buzzer(bool fl) {// вкл/выкл пищалки
    if (fl && SamSetup.UseBuzzer) { // Активируем пищалку
      buzzer_active = true;
      buzzer_beep_count = 0;
      buzzer_state = false;
      buzzer_next_time = millis(); // Начинаем сразу
    } else {                       // Деактивируем пищалку
      buzzer_active = false;
      buzzer_beep_count = 0;
      buzzer_state = false;
      digitalWrite(BZZ_PIN, LOW);
    }
  }
  void process_buzzer() {// Обработка пищалки в основном цикле
    if (!buzzer_active) {
      return;
    }
    unsigned long current_time = millis();
    if (current_time >= buzzer_next_time) {
      if (buzzer_state) {// Пищалка включена, выключаем её
        digitalWrite(BZZ_PIN, LOW);
        buzzer_state = false;
        buzzer_beep_count++;
        if (buzzer_beep_count >= 5) {// Проверяем, нужно ли продолжать пищать
          buzzer_active = false;
          buzzer_beep_count = 0;
        } else {// Планируем следующее включение через 600 мс
          buzzer_next_time = current_time + 600;
        }
      } else {// Пищалка выключена, включаем её
        digitalWrite(BZZ_PIN, HIGH);
        buzzer_state = true;
        buzzer_next_time = current_time + 400;// Планируем выключение через 400 мс
        }
      }
  }  
  void set_boiling() {
    //Учитываем задержку измерения Т кипения
    if (!boil_started) {
      //началось кипение, запоминаем Т кипения
      boil_started = true;
      boil_temp = TankSensor.avgTemp;
      alcohol_s = get_alcohol(TankSensor.avgTemp);
      if (SamSetup.UseWP) {
          wp_count = -10;
      }
    }
  }
  bool check_boiling() {
    if (boil_started || !PowerOn || !valve_status || TankSensor.avgTemp < 70) {
      return false;
    }
    //учтем задержку в 60 секунд до начала процесса определения кипения, чтобы датчик температуры воды успел остыть, если он нагрелся
    if (b_t_time_delay == 0 || (b_t_time_delay + 60 * 1000 > millis())) {
      if (b_t_time_delay == 0) {
        b_t_time_delay = millis();
      }
      return false;
    }
    //Если минимальная температура воды охлаждения больше текущей, то запоминаем её
    if (d_s_temp_prev > WaterSensor.avgTemp || d_s_temp_prev == 0) {
      d_s_temp_prev = WaterSensor.avgTemp;
    }
    //Определяем, что началось кипение - вода охлаждения начала нагреваться
    if (WaterSensor.avgTemp - d_s_temp_prev > 8 || SteamSensor.avgTemp > SamSetup.Ch_Pwr_Md_St_T) {
      set_boiling();
    }
    //Если температура воды охлаждения близка к заданной, то кипение началось
    if (abs(WaterSensor.avgTemp - SamSetup.SetWaterTemp) < 3 && WaterSensor.avgTemp - d_s_temp_prev > 2) {
      set_boiling();
    }
    //Проверяем, что температура в кубе не менялась более 0.1 градуса в течение 50 секунд, если менялась, то кипение не началось
    if (TankSensor.avgTemp - b_t_temp_prev > 0.1) {
      b_t_temp_prev = TankSensor.avgTemp;
      b_t_time_min = millis();
    } else if ((millis() - b_t_time_min) > 50 * 1000 && b_t_time_min > 0) {
      set_boiling();
    }
    if (boil_started) {
      SendMsg("Началось кипение в кубе! Спиртуозность " + format_float(alcohol_s, 1), WARNING_MSG);
    }
    return boil_started;
  }
  void start_self_test(void) {
    is_self_test = true;
    SendMsg(("Запуск самотестирования."), NOTIFY_MSG);
    open_valve(true, true);
    if (SamSetup.UseWP) {
      //включаем насос воды
      set_pump_pwm((SamSetup.PWM_St_V + 20) * 10);
    }
    //включаем шаговый двигатель
    stopService();
    #ifdef USE_STEPPER_ACCELERATION
      // В самотестировании отключаем плавный разгон/торможение, чтобы мотор сразу крутился
      // с заданной скоростью (даже если в целом акселерация включена).
    stepper.setAcceleration(0);
    #endif
    TargetStepps = 100 * SamSetup.StepperStepMl;
    set_stepper_target(get_speed_from_rate(1, Samovar_Mode == SAMOVAR_NBK_MODE ? SamSetup.NBK_StepperStepMl : SamSetup.StepperStepMl),0,TargetStepps);
    set_stepper2_target(get_speed_from_rate(1, Samovar_Mode == SAMOVAR_NBK_MODE ? SamSetup.NBK_StepperStepMl : SamSetup.StepperStepMl),0,TargetStepps);
    //включаем сервопривод
    set_capacity(1);
    while (capacity_num != 0 && capacity_num < 5) {
      vTaskDelay(3000 / portTICK_PERIOD_MS);
      next_capacity();
    }
    vTaskDelay(15000 / portTICK_PERIOD_MS);
    stop_self_test();
    SendMsg(("Самотестирование закончено."), NOTIFY_MSG);
  }
  void stop_self_test(void) {
    if (SamSetup.UseWP) {
      //выключаем насос воды
      set_pump_pwm(0);
    }
    open_valve(false, true);
    set_capacity(0);
    stopService();
    #ifdef USE_STEPPER_ACCELERATION
    // Возвращаем ускорение библиотеки по умолчанию, чтобы остальные режимы работали штатно
    stepper.setAcceleration(200);
    #endif
    set_stepper2_target(0,0,0);
    is_self_test = false;
    reset_sensor_counter();
  }
  void calibrate() {
    if (startval > 0 && startval != 100) return;
    int stpspeed = stepper.getSpeed();
    if (startval == 100) {
      stpspeed = stpspeed + stpspeed / 10;
      pump_calibrate(stpspeed);
      return;
    }
    if (StepperMoving) {
      stpspeed = 0;
    } else {
      startval = 100;
      stpspeed = STEPPER_MAX_SPEED;
    }
    pump_calibrate(stpspeed);
  }
  void samovar_reset() {
    reset_sensor_counter();
    sam_command_sync = SAMOVAR_NONE;
  }
  String get_Samovar_Status() {// Получить статус Самовара
    if (!PowerOn) {
      SamovarStatus = F("Выключено");
      SamovarStatusInt = 0;
    } else if (PowerOn && startval == 1 && !PauseOn && !program_Wait) {
      SamovarStatus = "П-" + String(ProgramNum + 1);
      SamovarStatusInt = 10;
    } else if (PowerOn && startval == 1 && program_Wait) {
      int s = 0;
      if (t_min > (millis() + 10)) {
        s = (t_min - millis()) / 1000;
      }
      SamovarStatus = "П-" + String(ProgramNum + 1) + " Пауза " + program_Wait_Type + ". Продолж.-> " + (String)s + " сек.";
      SamovarStatusInt = 15;
    } else if (PowerOn && startval == 2) {
      SamovarStatus = F("Программа завершена");
      SamovarStatusInt = 20;
    } else if (PowerOn && startval == 100) {
      SamovarStatus = F("Калибровка");
      SamovarStatusInt = 30;
    } else if (PauseOn) {
      SamovarStatus = F("Пауза");
      SamovarStatusInt = 40;
    } else if (PowerOn && startval == 0 && !stepper.getState()) {
      if (SamovarStatusInt != 51 && SamovarStatusInt != 52) {
        SamovarStatus = F("Разгон колонны");
        SamovarStatusInt = 50;
      } else if (SamovarStatusInt == 51) {
        SamovarStatus = F("Стабилизация");
      } else if (SamovarStatusInt == 52) {
        SamovarStatus = F("Работа на себя");
      }
    } else if (SamovarStatusInt == 1000) {
      SamovarStatus = F("Режим дистилляции");
      //SamovarStatus += "Ост.:" + String(get_dist_remaining_time(), 1) + " мин";
      //SamovarStatus += "; Общее:" + String(get_dist_predicted_total_time(), 1) + " мин";
    } else if (SamovarStatusInt == 3000) {
      SamovarStatus = F("Режим БК");
    } else if (SamovarStatusInt == 4000) {
      if (startval == 4001) {
        SamovarStatus = "П-" + String(ProgramNum + 1) + "; ";
        if (program[ProgramNum].WType == "H") {
          SamovarStatus = SamovarStatus + "Прогрев";
        } else if (program[ProgramNum].WType == "S") {
          SamovarStatus = SamovarStatus + "Настройка";
        } else if (program[ProgramNum].WType == "O") {
          SamovarStatus = SamovarStatus + "Оптимизация";
        } else if (program[ProgramNum].WType == "W") {
          SamovarStatus = SamovarStatus + "Работа";
        }
      }
    } else if (SamovarStatusInt == 2000) {
      #ifdef SAM_BEER_PRG
      SamovarStatus = "Прг №" + String(ProgramNum + 1) + "; ";
      #else
      SamovarStatus = "";
      #endif
      if (startval == 2001 && program[ProgramNum].WType == "M") {
        float currentTemp = getBeerCurrentTemp();
        SamovarStatus = SamovarStatus + "Прогрев до t солода";
        if (currentTemp < program[ProgramNum].Temp - 0.5) {
          SamovarStatus += "; Текущая Т: " + String(currentTemp) + "°";
        }
      } else if (startval == 2002 && program[ProgramNum].WType == "M") {
        SamovarStatus = SamovarStatus + "Ожидание солода";
      } else if (program[ProgramNum].WType == "P") {
        if (begintime == 0) {
          float currentTemp = getBeerCurrentTemp();
          SamovarStatus = SamovarStatus + "Пауза " + String(program[ProgramNum].Temp) + "°; Разогрев";
          if (currentTemp < program[ProgramNum].Temp - 0.5) {
            SamovarStatus += "; Текущая Т: " + String(currentTemp) + "°";
          }
        } else {
          SamovarStatus = SamovarStatus + "Пауза " + String(program[ProgramNum].Temp) + "°";
        }
      } else if (program[ProgramNum].WType == "C") {
        float currentTemp = getBeerCurrentTemp();
        SamovarStatus = SamovarStatus + "Охлаждение до " + String(program[ProgramNum].Temp);
        if (currentTemp > program[ProgramNum].Temp + 0.5) {
          SamovarStatus += "; Т: " + String(currentTemp) + "°";
        }
      } else if (program[ProgramNum].WType == "W") {
        SamovarStatus = SamovarStatus + "Ожидание кн.След.пр.";
      } else if (program[ProgramNum].WType == "A") {
        SamovarStatus = SamovarStatus + "Автокалибровка";
      } else if (program[ProgramNum].WType == "B") {
        if (begintime == 0) {
          SamovarStatus = SamovarStatus + "Кипячение - нагрев";
        } else {
          SamovarStatus = SamovarStatus + "Кипячение " + String(program[ProgramNum].Time) + " мин";
        }
      } else if (program[ProgramNum].WType == "F") {
        if (PowerOn) {
          float currentTemp = getBeerCurrentTemp();
          SamovarStatus = SamovarStatus + "Ферментация; Поддерж. Т=" + String(program[ProgramNum].Temp) + "°";
          SamovarStatus += "; Тек: " + String(currentTemp) + "°";
          if (heater_state) {
            SamovarStatus += " (Нагрев)";
          } else {
            SamovarStatus += " (Термостатирование)";
          }
        } else {
          SamovarStatus = SamovarStatus + "Ферментация (остановлена)";
        }
      } else if (program[ProgramNum].WType == "L") {
      SamovarStatus = SamovarStatus + "Выполнение Lua скрипта";
      }

      if (PowerOn && (program[ProgramNum].WType == "P" || program[ProgramNum].WType == "B") && begintime > 0) {
        float progress = ((float)(millis() - begintime) / (program[ProgramNum].Time * 60 * 1000)) * 100;
        if (progress > 100) progress = 100;
        SamovarStatus += "; Прогресс: " + String(progress, 1) + "%";
      }
    }

    if (SamovarStatusInt == 10 || SamovarStatusInt == 15 || (SamovarStatusInt == 2000 && PowerOn)) {
      SamovarStatus += " Ост:" + WthdrwTimeS + "|" + WthdrwTimeAllS;
    }
    if (SteamSensor.BodyTemp > 0) {
      SamovarStatus += ";Т тела пар:" + format_float(get_temp_by_pressure(SteamSensor.Start_Pressure, SteamSensor.BodyTemp, bme_pressure), 3) + ";Т тела царга:" + format_float(get_temp_by_pressure(SteamSensor.Start_Pressure, PipeSensor.BodyTemp, bme_pressure), 3);
    }

    return SamovarStatus;
  }
  void open_valve(bool Val, bool msg = true) {
    if (Val && !alarm_event) {
      valve_status = true;
      if (msg) {
        SendMsg(("Откройте подачу воды!"), WARNING_MSG);
      } else {
        SendMsg(("Открыт клапан воды охлаждения!"), NOTIFY_MSG);
      }
      digitalWrite(RELE_CHANNEL3, SamSetup.rele3);
    } else {
      valve_status = false;
      if (msg) {
        SendMsg(("Закройте подачу воды!"), WARNING_MSG);
      } else {
        SendMsg(("Закрыт клапан воды охлаждения!"), NOTIFY_MSG);
      }
      digitalWrite(RELE_CHANNEL3, !SamSetup.rele3);
    }
  }

//-----------Основные потоки
  void NET_Loop(void *parameter) {//Запускаем таск для сетевых задач
    String qMsg;
    uint8_t tcntST = 0;
    while (true) {

      static unsigned long timeout = millis();
      if (millis() - timeout >= 10000) { //раз в 10 секунд попытки переподключиться к WiFi
        if (WiFi.status() != WL_CONNECTED) {
            WiFi.disconnect();
            WiFi.reconnect();
            DbgMsg("WiFi.reconnect...", 1);
          }
        timeout = millis();
      }

      static unsigned long timeout2 = millis();
      if (millis() >= timeout2 && WiFi.status() == WL_CONNECTED) {          //раз в секунду
        timeout2 = millis() + 900;

        NTP.update();//Обновляем время
        vTaskDelay(50 / portTICK_PERIOD_MS);

        vTaskDelay(50 / portTICK_PERIOD_MS);
        if (SamSetup.UseBlynk) { //обработка Blynk
          if (Blynk.connected()) {
            Blynk.run();
          } else if (SamSetup.blynkauth[0] != 0) Blynk.connect(BLYNK_TIMEOUT_MS);
        }
        vTaskDelay(50 / portTICK_PERIOD_MS);

        if (SamSetup.UseMQTT) {//Восстановление соединения с MQTT
          if (!mqttClient.connected()){
            connectToMqtt();
          }
        }
        if (startval != 0) {
          tcntST++;
          if (tcntST >= SamSetup.LogPeriod) {
            tcntST = 0;
            static String s; 
            s = append_data();  //Записываем данные в память ESP32;//                        Запись в SPIFFS и формирование строки, долго!!!
            if (s.length() > 0) {
              s += ",";
              s += format_float(ACPSensor.avgTemp, 3);
              s += ",";
              s += format_float(ActualVolumePerHour, 3);
              s += ",";
              s += (String)current_power_volt;
              s += ",";
              s += format_float(WFflowRate, 2);
              s += ",";
              s += format_float(get_alcohol(TankSensor.avgTemp), 2);
              s += ",";
              s += format_float(get_steam_alcohol(TankSensor.avgTemp), 2);
              s += ",";
              s += format_float(pressure_value, 2);
              if (WiFi.status() == WL_CONNECTED && SamSetup.UseMQTT) {
              if (mqttClient.connected()) MqttSendMsg(s, "log");  
              }
            }
          }
        }
          vTaskDelay(5 / portTICK_PERIOD_MS);
        if (SamSetup.UseTg) {// Отправка в Телеграм
          if (SamSetup.tg_token[0] != 0 && SamSetup.tg_chat_id[0] != 0) {
            if (!msg_q.isEmpty()) {
              if (xSemaphoreTake(xMsgSemaphore, (TickType_t)(50 / portTICK_RATE_MS)) == pdTRUE) {
                char c[512];
                if (msg_q.pop(c)) {
                  qMsg = urlEncode(c);
                  //bool ping_1 = Ping.ping("212.237.16.93", 1);
                  if (//ping_1 &&
                    http_sync_request_get("/bot" + String(SamSetup.tg_token) + "/sendMessage?chat_id=" + SamSetup.tg_chat_id + "&text=" + qMsg, "212.237.16.93") != "<ERR>") {
                  } else SerialMsg(F("Сбой отправки сообщения в Телеграм."), WARNING_MSG);
                }
                xSemaphoreGive(xMsgSemaphore);
              }
            }
          } 
            vTaskDelay(5 / portTICK_PERIOD_MS);
        }
        UDP_Tick();//Отработка команд стабилизатору UDP через его эндпоинты, чтение статуса из UDP broadcast
      }
    vTaskDelay(300 / portTICK_PERIOD_MS); //засыпаем
    }
  }
  void Sensors_Loop(void *parameter) {//Запускаем таск для получения температур и различных проверок
    uint32_t CurMinST = 0;// Секунды для цикла проверки датчиков
    uint32_t OldMinST = 0;
    unsigned long oldTime = 0;  // Предыдущее время в милисекундах
    while (true) {
      CurMinST = (millis() / 1000);
      if (SamSetup.PressureSensor) {
          //Проверим, что давление не вышло за пределы, если вышло - авария
          if (SamSetup.MaxPressureValue > 0 && pressure_value >= SamSetup.MaxPressureValue) {
            SendMsg("Превышено предельное давление!", ALARM_MSG);
            set_alarm();
          }
      }
      if (SamSetup.disp == 2) {//   раз в 2 сек.               I2C -- несмотря на семафор лучше вызывать последовательно
        static unsigned long timeout = millis(), timeout3 = millis();
        if (millis() - timeout >= 2000) {     //раз в 2 сек обновляем дисплей
          if (millis() - timeout3 >= 120000) { //раз в 2 минуты перерисуем заголовки на случай их порчи
            reset_lcd();
            timeout3 = millis();
            }
          Crt = NTP.getFormattedDate();
          StrCrt = NTP.getFormattedTime() + "     " + NTP.getFormattedTime((unsigned long)(millis() / 1000));
          if (xSemaphoreTake(xI2CSemaphore, (TickType_t)(200 / portTICK_RATE_MS)) == pdTRUE) {
            if (WiFi.getMode() == WIFI_STA)  LD.printString_6x8(String(WiFi.RSSI()).c_str(), 109, 7); 
            LD.printString_6x8(StrCrt.c_str(), 1, 0); // время текущее/работы
            static int16_t SamStIntOld = -1;
            static uint8_t ProgramNumOld = 250;
            if (SamovarStatusInt !=SamStIntOld || ProgramNumOld != ProgramNum || program[ProgramNum].WType == "H" || 
            program[ProgramNum].WType == "B" || program[ProgramNum].WType == "P" || program[ProgramNum].WType == "T") {
            String St = get_Samovar_Status();
            LD.printString_6x8(StrToCharLen(St, 19).c_str(), 1, 1); // статус
            SamStIntOld = SamovarStatusInt;
            ProgramNumOld = ProgramNum;
            }
            LD.printString_6x8(format_float(SteamSensor.avgTemp,2).c_str(), 19, 2);
            LD.printString_6x8(format_float(PipeSensor.avgTemp,2).c_str(), 19, 3);
            LD.printString_6x8(format_float(TankSensor.avgTemp,2).c_str(), 19, 4);
            LD.printString_6x8((format_float(PipeSensor.avgTemp - SteamSensor.avgTemp, 2)).c_str(), 80, 2);
            LD.printString_6x8(format_float(ACPSensor.avgTemp,2).c_str(), 80, 3);
            LD.printString_6x8(format_float(WaterSensor.avgTemp,2).c_str(), 80, 4);
            LD.printString_6x8(StrToCharLen(format_float(ActualVolumePerHour, 2),5).c_str(), 19, 5);//скорость насоса
            if (current_power_mode == POWER_SPEED_MODE) LD.printString_6x8(F("BOOST "), 80, 5);
            else if (current_power_mode == 0) LD.printString_6x8(F(" OFF "), 80, 5);
            else if (SamSetup.PwrType >= STAB_AVR) LD.printString_6x8(StrToCharLen(format_float(target_power_volt, 0),6).c_str(), 80, 5);//задание мощности
            else LD.printString_6x8(StrToCharLen(format_float(target_power_volt, 1),6).c_str(), 80, 5);
            if (use_pressure_sensor) LD.printString_6x8(format_float(pressure_value, 2).c_str(), 80, 6);//давление          
          timeout = millis();
          }
          xSemaphoreGive(xI2CSemaphore);
        }
      
      }
      if (SamSetup.disp == 1) {
            static unsigned long timeout = millis(), timeout2 = millis();
        if (millis() - timeout >= 2000) {     //раз в 2 сек обновляем дисплей
          if (millis() - timeout2 >= 120000) { //раз в 2 минуты переинициализируем его на случай рассинхрона
            reset_lcd();
            timeout2 = millis();
            }
          if (xSemaphoreTake(xI2CSemaphore, (TickType_t)(200 / portTICK_RATE_MS)) == pdTRUE) {
            LQ.setCursor(3,0); LQ.print(format_float(SteamSensor.avgTemp,2));
            LQ.setCursor(14,0); LQ.print(format_float(PipeSensor.avgTemp - SteamSensor.avgTemp, 2));
            LQ.setCursor(3,1); LQ.print(format_float(PipeSensor.avgTemp,2));
            LQ.setCursor(14,1); LQ.print(format_float(ACPSensor.avgTemp,2));
            LQ.setCursor(3,2); LQ.print(format_float(TankSensor.avgTemp,2));
            LQ.setCursor(14,2); LQ.print(format_float(WaterSensor.avgTemp,2));
            LQ.setCursor(3,3); LQ.print(StrToCharLen(format_float(ActualVolumePerHour, 2),5));
            LQ.setCursor(14,3);
            if (current_power_mode == POWER_SPEED_MODE) LQ.print(F("BOOST "));
            else if (current_power_mode == 0) LQ.print(F(" OFF ")); 
            else if (SamSetup.PwrType >= STAB_AVR) LQ.print(StrToCharLen(format_float(target_power_volt, 0),5));
            else LQ.print(StrToCharLen(format_float(target_power_volt, 1),5));
                  timeout = millis();
          }
          xSemaphoreGive(xI2CSemaphore);
        }
      }
    
      if (OldMinST != CurMinST) { // раз в секунду запрашиваем значения давления, напряжения и датчика потока
        
        BME_getvalue(false);// Чтение датчика АД        I2C
        vTaskDelay(50 / portTICK_PERIOD_MS);
        if (SamSetup.PressureSensor) {//               I2C
              vTaskDelay(40 / portTICK_PERIOD_MS);
              pressure_sensor_get();  
        } else {
              vTaskDelay(50 / portTICK_PERIOD_MS);     
        }
        vTaskDelay(50 / portTICK_PERIOD_MS);

        if (SamSetup.LUA) { // Только ставит флаг для таска
              //если установлена переменная btn_script, запускаем
              if (btn_script.length() > 0) {
                static String sr;
                if (show_lua_script) {
                  WriteConsoleLog(F("--BEGIN LUA SCRIPT--"));
                  WriteConsoleLog(btn_script);
                  WriteConsoleLog(F("--END LUA SCRIPT--"));
                }
                sr = lua.Lua_dostring(&btn_script);
                sr.trim();
                if (sr.length() > 0) WriteConsoleLog("ERR in BTN_SCRIPT " + sr);
                btn_script = "";
              }

              //если установлена переменная запуска в цикле lua_script, запускаем
              if (loop_lua_fl) {
                start_lua_script();
              }
        }
        vTaskDelay(50 / portTICK_PERIOD_MS);
        if (SamSetup.PwrType != NO_POVER_REG) { // Быстрая, простое присваивание
              get_current_power();
        }
        DS_getvalue();                              // 1-Ware + I2C, 

        //проверка параметров работы колонны на критичность и аварийное выключение нагрева, в случае необходимости
        if (Samovar_Mode == SAMOVAR_RECTIFICATION_MODE) {
          check_alarm_rect();
        } else if (Samovar_Mode == SAMOVAR_DISTILLATION_MODE) {
          check_alarm_distiller();
        } else if (Samovar_Mode == SAMOVAR_BK_MODE) {
          check_alarm_bk();
        } else if (Samovar_Mode == SAMOVAR_NBK_MODE) {
          check_alarm_nbk();
        } else if (Samovar_Mode == SAMOVAR_BEER_MODE) {
          check_alarm_beer();
          WFpulseCount = 100;
        }
        vTaskDelay(5 / portTICK_PERIOD_MS);
        CalculateProgressAndTime();//Считаем прогресс для текущей строки программы и время до конца завершения строки и всего отбора
        
        vTaskDelay(5 / portTICK_PERIOD_MS);

        if (SamSetup.UseWS == 1) {
              if (WFpulseCount < 3) WFpulseCount = 0;
              WFflowRate = ((1000.0 / (millis() - oldTime)) * WFpulseCount) / SamSetup.Ws_Calbr;
              WFflowMilliLitres = WFflowRate * 100 / 6;
              WFtotalMilliLitres += WFflowMilliLitres;
              if (TankSensor.avgTemp > (SamSetup.Opn_Vlv_Tnk_T + 2) && PowerOn && WFpulseCount == 0) {
                WFAlarmCount++;
              } else {
                WFAlarmCount = 0;
              }
              WFpulseCount = 0;
              oldTime = millis();
              vTaskDelay(5 / portTICK_PERIOD_MS);
        }

        if (SamSetup.UseWS == 2) {
              if (TankSensor.avgTemp > (SamSetup.Opn_Vlv_Tnk_T + 2) && PowerOn && !digitalRead(WATERSENSOR_PIN)) {
                WFAlarmCount++;
              } else {
                WFAlarmCount = 0;
              }
        }

        //Проверяем, что температурные датчики считывают температуру без проблем, если есть проблемы - пишем оператору
        if (SteamSensor.ErrCount > 10) {
          SteamSensor.ErrCount = -110;
          SendMsg(("Ошибка датчика температуры пара!"), ALARM_MSG);
        }
        if (PipeSensor.ErrCount > 10) {
          PipeSensor.ErrCount = -110;
          SendMsg(("Ошибка датчика температуры царги!"), ALARM_MSG);
        }
        if (WaterSensor.ErrCount > 10) {
          WaterSensor.ErrCount = -110;
          SendMsg(("Ошибка датчика температуры воды!"), ALARM_MSG);
        }
        if (TankSensor.ErrCount > 10) {
          TankSensor.ErrCount = -110;
          SendMsg(("Ошибка датчика температуры куба!"), ALARM_MSG);
        }
        if (ACPSensor.ErrCount > 10) {
          ACPSensor.ErrCount = -110;
          SendMsg(("Ошибка датчика температуры в ТСА!"), ALARM_MSG);
        }
        OldMinST = CurMinST;
      }
      vTaskDelay(50 / portTICK_PERIOD_MS);
    }
  }
  void loop() {
    // Проверка переполнения стека
    if (uxTaskGetStackHighWaterMark(NULL) < 325) {
      SendMsg("Стек переполнился. Перезагрузка", ALARM_MSG);
      vTaskDelay(5000);
      ESP.restart();
    }

    #ifdef USE_STEPPER_ACCELERATION
      portENTER_CRITICAL_ISR(&timerMux);
      portENTER_CRITICAL_ISR(&timerMux2);
      #if (defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 3))
      timerAlarm(timer, stepper.getPeriod(), true, 0);
      timerAlarm(timer2, stepper2.getPeriod(), true, 0);
      #else  // ESP_ARDUINO_VERSION_MAJOR >= 3
      timerAlarmWrite(timer, stepper.getPeriod(), true);
      timerAlarmWrite(timer2, stepper2.getPeriod(), true);
      #endif
      portEXIT_CRITICAL_ISR(&timerMux);
      portEXIT_CRITICAL_ISR(&timerMux2);
    #endif  //USE_STEPPER_ACCELERATION
    if (SamSetup.OTA) ArduinoOTA.handle(); //обработка OTA

    ::ws.cleanupClients();//перебирает все подключённые WebSocket-клиенты и удаляет те, которые больше не активны или не подключены

    if (SamSetup.UseA_BTN) {
      alarm_btn.tick();  // отработка нажатия аварийной кнопки
      if (alarm_btn.isPress()) {
        set_alarm();
      }
    }
    if (SamSetup.UseBTN) {
      //обработка нажатий кнопки и разное поведение в зависимости от режима работы
      btn.tick();
      if (btn.isPress()) {
        if (Samovar_Mode == SAMOVAR_RECTIFICATION_MODE) {
          //если выключен - включаем
          if (!PowerOn) {
            set_power(true);
          } else if (startval == 0 && SamovarStatusInt < 1000) {
            //если включен и программа отбора не работает - запускаем программу
            samovar_start();
          } else if (startval != 0 && !program_Pause && SamovarStatusInt < 1000) {
            //если выполняется программа, и программа - не пауза, ставим на паузу или снимаем с паузы
            pause_withdrawal(!PauseOn);
          } else if (startval != 0 && program_Pause && SamovarStatusInt < 1000) {
            //если выполняется программа, и программа - пауза, переходим к следующей программе
            samovar_start();
          }
          //Выход из режима калибровки - нажатие на кнопку.
          if (startval == 100) {
            startval = 0;
            calibrate();
          }
        } else if (Samovar_Mode == SAMOVAR_DISTILLATION_MODE) {
          //если дистилляция включаем или выключаем
          if (!PowerOn) {
            sam_command_sync = SAMOVAR_DISTILLATION;
          } else
            distiller_finish();
        } else if (Samovar_Mode == SAMOVAR_BK_MODE) {
          //если дистилляция включаем или выключаем
          if (!PowerOn) {
            sam_command_sync = SAMOVAR_BK;
          } else
            bk_finish();
        } else if (Samovar_Mode == SAMOVAR_NBK_MODE) {
          //если НБК включаем или выключаем
          if (!PowerOn) {
            sam_command_sync = RUN_NBK;
          } else
            nbk_finish();
        } else if (Samovar_Mode == SAMOVAR_BEER_MODE) {
          //если пиво включаем или двигаем программу
          if (!PowerOn) {
            sam_command_sync = SAMOVAR_BEER;
          } else
            run_beer_program(ProgramNum + 1);
        }
      }
    }
    if (sam_command_sync != SAMOVAR_NONE) {
      switch (sam_command_sync) {
        case SAMOVAR_START:
          Samovar_Mode = SAMOVAR_RECTIFICATION_MODE;
          samovar_start();
          break;
        case SAMOVAR_POWER:
          if (SamovarStatusInt == 1000) distiller_finish();
          else if (SamovarStatusInt == 2000)
            beer_finish();
          else if (SamovarStatusInt == 3000)
            bk_finish();
          else if (SamovarStatusInt == 4000)
            nbk_finish();
          else
            set_power(!PowerOn);
          if (PowerOn && Samovar_Mode == SAMOVAR_RECTIFICATION_MODE) {
            SamovarStatusInt = 50;
          }
          break;
        case SAMOVAR_RESET:
          samovar_reset();
          break;
        case CALIBRATE_START:
          pump_calibrate(CurrentStepperSpeed);
          break;
        case CALIBRATE_STOP:
          pump_calibrate(0);
          break;
        case SAMOVAR_PAUSE:
          pause_withdrawal(true);
          break;
        case SAMOVAR_CONTINUE:
          pause_withdrawal(false);
          t_min = 0;
          program_Wait = false;
          break;
        case SAMOVAR_SETBODYTEMP:
          set_body_temp();
          break;
        case SAMOVAR_DISTILLATION:
          Samovar_Mode = SAMOVAR_DISTILLATION_MODE;
          SamovarStatusInt = 1000;
          startval = 1000;
          break;
        case SAMOVAR_BEER:
          Samovar_Mode = SAMOVAR_BEER_MODE;
          SamovarStatusInt = 2000;
          startval = 2000;
          break;
        case SAMOVAR_BEER_NEXT:
          run_beer_program(ProgramNum + 1);
          break;
        case SAMOVAR_DIST_NEXT:
          run_dist_program(ProgramNum + 1);
          break;
        case SAMOVAR_BK:
          Samovar_Mode = SAMOVAR_BK_MODE;
          SamovarStatusInt = 3000;
          startval = 3000;
          break;
        case RUN_NBK:
          Samovar_Mode = SAMOVAR_NBK_MODE;
          SamovarStatusInt = 4000;
          startval = 4000;
          break;
        case SAMOVAR_NBK_NEXT:
          run_nbk_program(ProgramNum + 1);
          break;
        case SAMOVAR_SELF_TEST:
          start_self_test();
          break;
        case SAMOVAR_NONE:
          break;
      }
      if (sam_command_sync != SAMOVAR_RESET) {
        sam_command_sync = SAMOVAR_NONE;
      }
    }
    if (SamovarStatusInt > 0 && SamovarStatusInt < 1000) {// Обработка алгоритмов самогоноварения
        withdrawal();  //функция расчета отбора
      } else if (SamovarStatusInt == 1000) {
        distiller_proc();  //функция для проведения дистилляции
      } else if (SamovarStatusInt == 3000) {
        bk_proc();  //функция для работы с БК
      } else if (SamovarStatusInt == 4000) {
        nbk_proc();  //функция для работы с НБК
      } else if (SamovarStatusInt == 2000 && startval == 2000) {
        beer_proc();  //функция для проведения затирания
    }
    static unsigned long timecull = millis();  //раз в 10 сек обновляем вентилятор
    if (SamSetup.PWM_Cull) {
      if (millis() - timecull >= 10000) {
        if (bme_temp > 0 && SamSetup.Cull_T_ON < SamSetup.Cull_T_Max && SamSetup.Cull_N_Min < SamSetup.Cull_N_Max) {
          if (bme_temp > SamSetup.Cull_T_ON) {
            uint16_t N = map(bme_temp, SamSetup.Cull_T_ON, SamSetup.Cull_T_Max, SamSetup.Cull_N_Min, SamSetup.Cull_N_Max);
            Culler_PWM.write(round(N*10.23));
          } else Culler_PWM.write(0);
        }
        timecull = millis();
      }
    }
    if (SamSetup.dbg) {//раз в 5 min отчитываемся в Serial о состоянии HEAP и стеков выполняемых задач
      static unsigned long tout = millis();
      if (millis() - tout >= 300000) { 
            detailedHeapAnalysis();
            printTaskMemoryUsage();
            tout = millis();
      }
    }
    process_buzzer();
    vTaskDelay(5 / portTICK_PERIOD_MS);
  } //loop
