//*************Объявления
  #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

  #ifdef USE_LUA
  #include "lua.h"
  #endif

  #include <NTPClient.h>
  WiFiUDP ntpUDP;
  NTPClient NTP(ntpUDP);

  #include <Adafruit_BMP085_U.h>

  #ifdef USE_PRESSURE_XGZ
  #include <XGZP6897D.h>
  XGZP6897D pressure_sensor(USE_PRESSURE_XGZ);
  #endif
  #ifdef USE_UPDATE_OTA
  #include <ArduinoOTA.h>
  #endif
  #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 triggerPowerStatus(void *parameter);
  void WebServerInit(void);
  void calibrate();
  void set_body_temp();
  void distiller_finish();
  void beer_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 handleCoreDumpOnBoot(); // Дампы ядра в SPIFFS в режиме отладки
    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();

// Макросы для удобства
  //#define STACK_CHECK(monitor, location) monitor.check(location)
  //#define STACK_CHECK_FUNC(monitor) monitor.check(__FUNCTION__)
  // Глобальные экземпляры
  //AdvancedStackMonitor NetMonitor("NetMonitor", 20);
  //AdvancedStackMonitor i2cMonitor("I2C_Task", 10);
//----------------------------------------------------------------------------------------------------------
//---------- Кольцевой буфер сообщений 
  SimpleStringQueue msg_q(4, 512); // Вместо cppQueue msg_q(512, 4, FIFO);

  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) {
      #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
      #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);
    }
  }
  
//-----------Преобразования переменных, строк, пересчеты
  String getValue(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);
    #ifdef USE_DISPLAY
    LD.init();
    for (uint8_t i = 0; i < 8; i++) LD.printString_6x8(F("                    "), 1, i); //очистка дисплея  
    #endif
    #ifdef USE_DISP_LC
    LQ.init();
    LQ.begin(20, 4);
    LQ.backlight();
    LQ.clear();
    LQ.setCursor(0,0);LQ.print(F("   -=SAMOVARICH=-"));
    #endif

    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, там же инициализация шим насоса воды
        if(SPIFFS.exists("/s_log.txt")) { 
          File file = SPIFFS.open("/s_log.txt"); 
          if (file.size()>10000) {
            file.close();
          SPIFFS.remove("/s_log.txt"); 
          } else file.close();
            }
    DbgMsg("Save Coredump if reset...",1);
    if (SamSetup.dbg)   handleCoreDumpOnBoot(); //Сохраняем предыдущий дамп ресета

    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 power stabilizer...",1);
    InitPower(); //Инициализация портов, единиц измерения и множителя инкремента

    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("Sensors init... ",1);
    sensor_init(); //инициализация датчиков
    DbgMsg("Init WiFi... ",1);
    InitWiFi(wifiAP);
    stepper.disable();
    stepper2.disable();
    //Запускаем таск для обработки нажатия кнопки 
    xTaskCreatePinnedToCore(
      taskButton,       /* Function to implement the task */
      "taskButton",     /* Name of the task */
      1250,             /* 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);
    #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;
  
    //На всякий случай пошлем команду выключения питания на UART
    set_power_mode(POWER_SLEEP_MODE);// если регулятор вдруг работает, гасим его (на случай внезапного ресета Самовара)

    #ifdef USE_WEB_SERIAL
    WebSerial.begin(&server);
    WebSerial.onMessage(recvMsg);
    #endif

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

    //Задаем параметры для сенсора уровня флегмы
    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);  //вешаем прерывание на изменение датчика уровня флегмы

    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();

    #ifdef USE_LUA
    DbgMsg("init LUA...",1);
    lua_init();
    #endif

    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(
      triggerPowerStatus, /* Function to implement the task */
      "PowerStatusTask",  /* Name of the task */
      2000,               /* Stack size in words */
      NULL,               /* Task input parameter */
      1,                  /* Priority of the task */
      &PowerStatusTask,   /* Task handle. */
      0);                 /* Core where the task should run */
    //Запускаем таск для получения температур и различных проверок
    xTaskCreatePinnedToCore(
      triggerSysTicker, /* Function to implement the task */
      "SysTicker",      /* 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(
      triggerGetClock,  /* Function to implement the task */
      "GetClockTicker", /* Name of the task */
      6000,             /* Stack size in words */
      NULL,             /* Task input parameter */
      1,                /* Priority of the task */
      &GetClockTask1,   /* Task handle. */
      1);               /* Core where the task should run */
      if (SamSetup.dbg) printTaskMemoryUsage();//Выводим у кого сколько стека свободно
  }
  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 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) {
      if (BuzzerTask == NULL) {
        BuzzerTaskFl = true;
        //Запускаем таск для пищалки
        xTaskCreatePinnedToCore(
          triggerBuzzerTask, /* Function to implement the task */
          "BuzzerTask",      /* Name of the task */
          800,               /* Stack size in words */
          NULL,              /* Task input parameter */
          0,                 /* Priority of the task */
          &BuzzerTask,       /* Task handle. */
          1);                /* Core where the task should run */
      }
    } else {
      if (BuzzerTask != NULL && !BuzzerTaskFl) {
        vTaskDelete(BuzzerTask);
        BuzzerTask = NULL;
        digitalWrite(BZZ_PIN, LOW);
      }
    }
  }
  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();
    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(); 
    set_stepper2_target(0,0,0);
    is_self_test = false;
    reset_sensor_counter();
  }
  void ValidateSamSetup() {
    if (SamSetup.HeaterResistant < 1) SamSetup.HeaterResistant = 15;
    if (SamSetup.LogPeriod < 5) SamSetup.LogPeriod = 10;
    if (SamSetup.autospeed >= 100) SamSetup.autospeed = 10;
    SamSetup.Cull_N_Min = constrain(SamSetup.Cull_N_Min, 0, 100);             // минимальные обороты, %
    SamSetup.Cull_N_Max = constrain(SamSetup.Cull_N_Max, 10, 100);             // максимальные обороты, %
    SamSetup.Cull_T_ON = constrain(SamSetup.Cull_T_ON, 10, 50);                // Т включения
    SamSetup.Cull_T_Max = constrain(SamSetup.Cull_T_Max, 20, 50);               // T максимальных оборотов
  }
  void UseSamSetup() {
    heaterPID.SetTunings(SamSetup.Kp, SamSetup.Ki, SamSetup.Kd);
    SteamSensor.SetTemp = SamSetup.SetSteamTemp;
    PipeSensor.SetTemp = SamSetup.SetPipeTemp;
    WaterSensor.SetTemp = SamSetup.SetWaterTemp;
    TankSensor.SetTemp = SamSetup.SetTankTemp;
    ACPSensor.SetTemp = SamSetup.SetACPTemp;
    SteamSensor.Delay = SamSetup.SteamDelay;
    PipeSensor.Delay = SamSetup.PipeDelay;
    WaterSensor.Delay = SamSetup.WaterDelay;
    TankSensor.Delay = SamSetup.TankDelay;
    ACPSensor.Delay = SamSetup.ACPDelay;

    CopyDSAddress(SamSetup.SteamAdress, SteamSensor.Sensor);
    CopyDSAddress(SamSetup.PipeAdress, PipeSensor.Sensor);
    CopyDSAddress(SamSetup.WaterAdress, WaterSensor.Sensor);
    CopyDSAddress(SamSetup.TankAdress, TankSensor.Sensor);
    CopyDSAddress(SamSetup.ACPAdress, ACPSensor.Sensor);
    SteamSensor.ErrCount = -110;
    PipeSensor.ErrCount = -110;
    WaterSensor.ErrCount = -110;
    TankSensor.ErrCount = -110;
    ACPSensor.ErrCount = -110;
    
    MaxPower = SamSetup.HeaterResistant > 1 ? (230 * 230 / SamSetup.HeaterResistant) : 3000; // Номинальная мощность ТЭН-а

    #if BOARD == DEVKIT  
    if ((CULL_PIN==4) && (SamSetup.UseWP || SamSetup.UseWV)) Culler_PWM.detachPin(CULL_PIN);// В случае DEVKIT и использования регулировки насоса воды 4-й пин уже занят.
    else if (SamSetup.PWM_Cull) Culler_PWM.attachPin(CULL_PIN, 2, 1, 50, 10);  // channel 2, timer 110- разрядность 0-1023, 50-частота
    #endif  
    
    if (SamSetup.UseWP) {
      DbgMsg("Init WP",1);
      init_pump_pwm(WATER_PUMP_PIN, PUMP_PWM_FREQ);
      set_pump_pwm(0);
    } else if (SamSetup.UseWV) {// в случае клапана воды
      pump_pwm.detachPin(WATER_PUMP_PIN);
      pinMode(WATER_PUMP_PIN, OUTPUT);
      digitalWrite(WATER_PUMP_PIN, !(SamSetup.UseWV-1));
    }
    NTP.setTimeOffset(SamSetup.TimeZone * 3600);
  }
  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) {
      calibrate_text_ptr = (char *)"Stop";
      stpspeed = 0;
    } else {
      startval = 100;
      calibrate_text_ptr = (char *)"Start";
      stpspeed = STEPPER_MAX_SPEED;
    }
    pump_calibrate(stpspeed);
  }
  void samovar_reset() {
    char str[20] = "Stoped             ";
    memcpy(str, startval_text_val, 20);
    power_text_ptr = (char *)"ON";
    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("Режим дистилляции");
    } 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") {
        float currentTemp = getBeerCurrentTemp();
        SamovarStatus = SamovarStatus + "Ферментация T->" + String(program[ProgramNum].Temp) + "°";
        SamovarStatus += "; Т: " + String(currentTemp) + "°";
      } else if (!PowerOn) {
        SamovarStatus = "Выключено";
      }

      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 triggerGetClock(void *parameter) {//Запускаем таск для получения точного времени из интернет
    String qMsg;
    uint8_t tcntST = 0;
    //STACK_CHECK(NetMonitor, "Начало.");
    //STACK_CHECK(NetMonitor, "Заголовки.");
    while (true) {
      static unsigned long timeout = millis();
        if (millis() - timeout >= 20000) { 
          if (WiFi.status() == WL_CONNECTED) {
            } else {
              WiFi.disconnect();
              WiFi.reconnect();
              DbgMsg("WiFi.reconnect...", 1);
            }
          timeout = millis();
          }
        NTP.update();//Обновляем время
        vTaskDelay(50 / portTICK_PERIOD_MS);
      //STACK_CHECK(NetMonitor, "WiFi reconnect.");

        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 (SamSetup.UseMQTT) {
              MqttSendMsg(s, "log");  
              }
            }
          }
        }
        vTaskDelay(5 / portTICK_PERIOD_MS);
      //STACK_CHECK(NetMonitor, "MQTT send.");
    if (SamSetup.UseTg) {
        if (WiFi.status() == WL_CONNECTED && SamSetup.tg_token[0] != 0 && SamSetup.tg_chat_id[0] != 0 && Ping.ping("212.237.16.93", 1)) {
            if (!msg_q.isEmpty()) {
                vTaskDelay(5 / portTICK_PERIOD_MS);
                if (xSemaphoreTake(xMsgSemaphore, (TickType_t)(50 / portTICK_RATE_MS)) == pdTRUE) {
                    char c[512];
                    if (msg_q.pop(c)) {
                        qMsg = urlEncode(c);
                        http_sync_request_get(String("http://212.237.16.93/bot") + SamSetup.tg_token + "/sendMessage?chat_id=" + SamSetup.tg_chat_id + "&text=" + qMsg);
                    }
                    xSemaphoreGive(xMsgSemaphore);
                }
            }
        } else if (SamSetup.tg_chat_id[0] != 0) {
            SerialMsg(F("Проблема с подключением к интернету."), WARNING_MSG);
        }
        vTaskDelay(5 / portTICK_PERIOD_MS);
    }
      //STACK_CHECK(NetMonitor, "TG send.");
      if (SamSetup.UseBlynk) {
          if (!Blynk.connected() && WiFi.status() == WL_CONNECTED && SamSetup.blynkauth[0] != 0) {
            Blynk.connect(BLYNK_TIMEOUT_MS);
            vTaskDelay(5 / portTICK_PERIOD_MS);
          }
      }
      //STACK_CHECK(NetMonitor, "Blynk reconnect.");
      if (SamSetup.UseMQTT) {
          {
            if (!mqttClient.connected() && WiFi.status() == WL_CONNECTED) {
              connectToMqtt();
            }
          }
      }
      //STACK_CHECK(NetMonitor, "MQTT reconnect.");
        vTaskDelay(1000 / portTICK_PERIOD_MS); //засыпаем на секунду
    }
  }
  void triggerSysTicker(void *parameter) {//Запускаем таск для получения температур и различных проверок
    uint8_t CurMinST = 0;
    uint8_t OldMinST = 0;
    uint8_t tcntST = 0;
    unsigned long oldTime = 0;  // Предыдущее время в милисекундах
    //STACK_CHECK(i2cMonitor, "");
    //STACK_CHECK(i2cMonitor, "Вход в таск.");
    while (true) {
      //STACK_CHECK(i2cMonitor, "Вход в цикл.");
      CurMinST = (millis() / 1000);
      #if defined(USE_PRESSURE_XGZ) || defined(USE_PRESSURE_MPX) || defined(USE_PRESSURE_1WIRE)
          //Проверим, что давление не вышло за пределы, если вышло - авария
          if (SamSetup.MaxPressureValue > 0 && pressure_value >= SamSetup.MaxPressureValue) {
            SendMsg("Превышено предельное давление!", ALARM_MSG);
            set_alarm();
          }
          //STACK_CHECK(i2cMonitor, "Проверка предельного давления");
      #endif
      #ifdef USE_DISPLAY//   раз в 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 = Crt.substring(6) + "   " + NTP.getUptimeString();
          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 (PwrFactor == 20) 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);
        //STACK_CHECK(i2cMonitor, "Вывод на дисплей");  
      }
      
      #endif
      #ifdef USE_DISP_LC
            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 (PwrFactor == 20) 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);
        }
      #endif
    
      if (OldMinST != CurMinST) { // раз в секунду запрашиваем значения давления, напряжения и датчика потока
        
        BME_getvalue(false);// Чтение датчика АД        I2C
        //STACK_CHECK(i2cMonitor, "Чтение BMP180");
        vTaskDelay(50 / portTICK_PERIOD_MS);
        #if !defined(USE_PRESSURE_XGZ) //               I2C
              vTaskDelay(50 / portTICK_PERIOD_MS);
        #else
              vTaskDelay(40 / portTICK_PERIOD_MS);
              pressure_sensor_get();       
        #endif
        //STACK_CHECK(i2cMonitor, "Чтение ДД");
        vTaskDelay(50 / portTICK_PERIOD_MS);

        #ifdef USE_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();
              }
              //STACK_CHECK(i2cMonitor, "Обработка LUA");
        #endif
        vTaskDelay(50 / portTICK_PERIOD_MS);
        if (SamSetup.PwrType != NO_POVER_REG) { // Быстрая, простое присваивание
              get_current_power();
              //STACK_CHECK(i2cMonitor, "Запрос текущей мощности.");
        }
        DS_getvalue();                              // 1-Ware + I2C, 
                                                    // есть еще I2C Stepper, его оставим, поскольку пользуемся редко и уж очень он в пиво интегрирован
                                                    // а вот I2C расширители портов здесь зло, своих портов в достатке, убираем
        ////STACK_CHECK(i2cMonitor, "Чтение DS18B20 и NTC.");

        //проверка параметров работы колонны на критичность и аварийное выключение нагрева, в случае необходимости
        if (Samovar_Mode == SAMOVAR_RECTIFICATION_MODE) {
          check_alarm();
        } 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;
        }
        //STACK_CHECK(i2cMonitor, "Аварийные проверки");
        vTaskDelay(5 / portTICK_PERIOD_MS);
        CalculateProgressAndTime();//Считаем прогресс для текущей строки программы и время до конца завершения строки и всего отбора
        
        //STACK_CHECK(i2cMonitor, "Расчет прогресса строки программы");

        vTaskDelay(5 / portTICK_PERIOD_MS);

        if (SamSetup.UseWS) {
              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);
              //STACK_CHECK(i2cMonitor, "Датчик протока воды");
        }

        //Проверяем, что температурные датчики считывают температуру без проблем, если есть проблемы - пишем оператору
        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);
        }
        //STACK_CHECK(i2cMonitor, "Проверка темп. датчиков.");
        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

    #ifdef USE_UPDATE_OTA
    ArduinoOTA.handle();
    #endif

    if (SamSetup.UseBlynk) {
      if (Blynk.connected()) {
        Blynk.run();
      }
    }
    ::ws.cleanupClients();

    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) {
        static unsigned long tout = millis();
        if (millis() - tout >= 300000) { //раз в 5 min отчитываемся в Serial о состоянии HEAP и стеков выполняемых задач
              detailedHeapAnalysis();
              printTaskMemoryUsage();
              tout = millis();
        }
      }
    set_buzzer(false);
    vTaskDelay(5 / portTICK_PERIOD_MS);
  } //loop
  void triggerBuzzerTask(void *parameter) {
    TickType_t beep = 400 / portTICK_RATE_MS;
    TickType_t silent = 600 / portTICK_RATE_MS;
    int tick_buzz = 0;

    while (true) {
      if (BuzzerTaskFl) {
        digitalWrite(BZZ_PIN, HIGH);
        vTaskDelay(beep / portTICK_PERIOD_MS);
        digitalWrite(BZZ_PIN, LOW);
        tick_buzz++;
        if (tick_buzz > 5) BuzzerTaskFl = false;
      }
      vTaskDelay(silent / portTICK_PERIOD_MS);
    }
  }