//Заголовки  
  #include "HardwareSerial.h"
  #include <Arduino.h>
  #include "Settings.h"
  #include <BlynkSimpleEsp32.h>

  //**************************************************************************************************************
  // Логика работы ректификационной колонны
  //**************************************************************************************************************
  void save_profile();
  void samovar_start();
  void pause_withdrawal(bool Pause);
  inline String format_float(float v, int d);
  float get_temp_by_pressure(float start_pressure, float start_temp, float current_pressure);
  void set_current_power(float Volt);
  void open_valve(bool Val, bool msg);
  void stopService(void);
  void startService(void);
  void stopService2(void);
  void startService2(void);  
  void reset_sensor_counter(void);
  void set_pump_speed(float pumpspeed, bool continue_process);
  void set_pump_pwm(float duty);
  void set_pump_speed_pid(float temp);
  void set_power(bool On);
  void set_body_temp();
  void set_buzzer(bool fl);
  void start_self_test(void);
  void stop_self_test(void);
  bool check_boiling();
  float get_alcohol(float t);
  void set_boiling();
  bool set_stepper_target(uint16_t spd, uint8_t direction, uint32_t target);
  bool set_stepper2_target(uint16_t spd, uint8_t direction, uint32_t target);
  float get_dist_remaining_time();
  float get_dist_predicted_total_time();
  void check_power_error(); // Проверка ошибок питания
  void SendMsg(const String &m, MESSAGE_TYPE msg_type);
  void setServoAngle(int angle);
  float get_speed_from_rate(float rate, uint16_t s_per_l);
  void next_capacity();
  void DbgMsg(const String& Msg, bool ln);
  String get_program(uint8_t s);
  void run_program(uint8_t num);
  void create_data();
  void MqttSendMsg(const String &Str, const char *chart );
  void set_capacity(uint8_t cap);
  void WaterSratusCheck();
  void WaterTempAlarmPowerReg();
  void WaterPumpReg();// регулируем водяной насос  
  void stop_process(String reason);
  
  void samovar_start() {
    if (Samovar_Mode != SAMOVAR_RECTIFICATION_MODE || !PowerOn) return;

    if (startval == 2) startval = 3;
    else if (ProgramNum >= ProgramLen - 1 && startval != 0)
      startval = 2;

    if (startval == 0) {
    if (SamSetup.UseMQTT) {
      SessionDescription.replace(",", ";");
      MqttSendMsg((String)chipId + "," + SamSetup.TimeZone + "," + SAMOVAR_VERSION + "," + get_program(MAX_PRG) + "," + SessionDescription, "st");
      delay(200);
    }
      startval = 1;
      run_program(0);
      ProgramNum = 0;
      create_data();  //создаем файл с данными
    } else if (startval == 1) {
      ProgramNum++;
      run_program(ProgramNum);
    } else if (startval == 2) {
      run_program(MAX_PRG);
    } else {
      run_program(MAX_PRG);
      reset_sensor_counter();
    }
  }
// Функция для управления отбором**************************************************************************
void withdrawal(void) {//Определяем, что необходимо сменить режим работы
  //По завершению паузы
  if (program_Pause) {
    if (millis() >= t_min) {
      t_min = 0;
      samovar_start();
    }
    return;
  }
  //По достижению шаговика цели
 if (SamSetup.UsePump2 && program[ProgramNum].WType == "H"){ // учтем возможный отбор над ЦП
  CurrentStepps = stepper2.getCurrent();
  CurrentStepperSpeed = stepper2.getSpeed();
  } else {
  CurrentStepps = stepper.getCurrent();  
  CurrentStepperSpeed = stepper.getSpeed();
  }
  if (TargetStepps <= CurrentStepps && TargetStepps != 0 && (startval == 1 || startval == 2)) { // если все шаги выполнены
    samovar_start();                                                                        // переход на след программу
 }
  //По превышению температуры в программе
  if (program[ProgramNum].Temp != 0) {
    if (program[ProgramNum].Temp > 0 && program[ProgramNum].Temp < 20) {
      if (SteamSensor.avgTemp > (program[ProgramNum].Temp + SteamSensor.StartProgTemp)) {
        samovar_start();
      }
    } else {
      if (SteamSensor.avgTemp > program[ProgramNum].Temp) {
        samovar_start();
      }
    }
  } 
  //стартовая температура отбора тела с учетом корректировки от давления или без
  float c_temp = get_temp_by_pressure(SteamSensor.Start_Pressure, SteamSensor.BodyTemp, bme_pressure);
  //ПАУЗА ПО ПАРу Возвращаем колонну в стабильное состояние, если работает программа отбора тела и температура пара вышла за пределы или корректируем пределы
  if ((program[ProgramNum].WType == "B" || program[ProgramNum].WType == "C") && (SteamSensor.avgTemp >= c_temp + SteamSensor.SetTemp) && SteamSensor.BodyTemp > 0) {
    if (SamSetup.UseBdT_Aset) {
      //Если строка программы - предзахлеб и после есть еще две строки с отбором тела, то корректируем Т тела
      if (program[ProgramNum].WType == "C" && (ProgramNum < ProgramLen - 3) && (program[ProgramNum + 1].WType == "B" || 
      program[ProgramNum + 1].WType == "C" || program[ProgramNum + 1].WType == "P") && (program[ProgramNum + 2].WType == "B" || program[ProgramNum + 2].WType == "C")) {
        set_body_temp();
      }
      //Если это первая строка с телом - корректируем Т тела
      else if ((ProgramNum > 0) && (program[ProgramNum].WType == "B" || program[ProgramNum].WType == "C") && program[ProgramNum - 1].WType == "H") {
        set_body_temp();
      }
    }
      //ставим отбор на паузу, если еще не стоит, и задаем время ожидания
    if (!PauseOn && !program_Wait) {
      program_Wait_Type = "(пар)";
      //Если в настройках задан параметр - снижать скорость отбора - снижаем, и напряжение тоже
      if (SamSetup.useautospeed && setautospeed) {
        setautospeed = false;
        int baseSpeed = (int)CurrentStepperSpeed;
        if (baseSpeed < 1) baseSpeed = (int)abs((int)stepper.getSpeed());
        int dec = (int)round((float)baseSpeed / 100.0f * (float)SamSetup.autospeed);
        int newSpeed = baseSpeed - dec;
        if (newSpeed < 1) newSpeed = 1;
        set_pump_speed((float)newSpeed, false);
        if (SamSetup.PwrType != NO_POVER_REG) {
          if (program[ProgramNum].WType == "B" && SamSetup.useautopowerdown) {
            if (SamSetup.PwrType >= STAB_AVR)
            set_current_power(target_power_volt - target_power_volt / 100 * 3);
            else
            set_current_power(target_power_volt - 3 * PwrFactor);
          }
        }
        vTaskDelay(10 / portTICK_PERIOD_MS);
      }
      program_Wait = true;
      pause_withdrawal(true);
      t_min = millis() + SteamSensor.Delay * 1000;
      set_buzzer(true);
      SendMsg(("Пауза по Т пара"), WARNING_MSG);
    }
    // если время вышло, еще раз пытаемся дождаться
    if (millis() >= t_min && SteamSensor.avgTemp >= c_temp + SteamSensor.SetTemp) {
      t_min = millis() + SteamSensor.Delay * 1000;
    }
   } else if ((program[ProgramNum].WType == "B" || program[ProgramNum].WType == "C") && SteamSensor.avgTemp < SteamSensor.BodyTemp + SteamSensor.SetTemp && millis() >= t_min && t_min > 0 && program_Wait) {
    //продолжаем отбор
    SendMsg(("Продолжаем отбор после автоматической паузы"), NOTIFY_MSG);
    setautospeed = true;
    t_min = 0;
    program_Wait = false;
    pause_withdrawal(false);
  }
  c_temp = get_temp_by_pressure(SteamSensor.Start_Pressure, PipeSensor.BodyTemp, bme_pressure);
  //ПАУЗА ПО ЦАРГе Возвращаем колонну в стабильное состояние, если работает программа отбора тела и температура в колонне вышла за пределы или корректируем пределы
  if ((program[ProgramNum].WType == "B" || program[ProgramNum].WType == "C") && (PipeSensor.avgTemp >= c_temp + PipeSensor.SetTemp) && PipeSensor.BodyTemp > 0) {
      if (SamSetup.UseBdT_Aset) {
          if (program[ProgramNum].WType == "C" && (ProgramNum < ProgramLen - 3) && (program[ProgramNum + 1].WType == "B" || program[ProgramNum + 1].WType == "C" || 
              program[ProgramNum + 1].WType == "P") && (program[ProgramNum + 2].WType == "B" || program[ProgramNum + 2].WType == "C")) {
            set_body_temp();
          }
          //Если это первая строка с телом - корректируем Т тела
          else if ((ProgramNum > 0) && (program[ProgramNum].WType == "B" || program[ProgramNum].WType == "C") && program[ProgramNum - 1].WType == "H") {
            set_body_temp();
          }
      }
      //ставим отбор на паузу, если еще не стоит, и задаем время ожидания
      if (!PauseOn && !program_Wait) {
        program_Wait_Type = "(царга)";
        //Если в настройках задан параметр - снижать скорость отбора - снижаем, и напряжение тоже
        if (SamSetup.useautospeed && setautospeed) {
          setautospeed = false;
          int baseSpeed = (int)CurrentStepperSpeed;
          if (baseSpeed < 1) baseSpeed = (int)abs((int)stepper.getSpeed());
          int dec = (int)round((float)baseSpeed / 100.0f * (float)SamSetup.autospeed);
          int newSpeed = baseSpeed - dec;
          if (newSpeed < 1) newSpeed = 1;
          set_pump_speed((float)newSpeed, false);
          if (SamSetup.PwrType != NO_POVER_REG) {
            if (program[ProgramNum].WType == "B" && SamSetup.useautopowerdown) {
              if (SamSetup.PwrType >= STAB_AVR)
              set_current_power(target_power_volt - target_power_volt / 100 * 3);
              else
              set_current_power(target_power_volt - 3 * PwrFactor);
            }
          }
          vTaskDelay(10 / portTICK_PERIOD_MS);
        }
        program_Wait = true;
        pause_withdrawal(true);
        t_min = millis() + PipeSensor.Delay * 1000;
        set_buzzer(true);
        SendMsg(("Пауза по Т царги"), WARNING_MSG);
      }
      // если время вышло, еще раз пытаемся дождаться
      if (millis() >= t_min && PipeSensor.avgTemp >= c_temp + PipeSensor.SetTemp) {
        t_min = millis() + PipeSensor.Delay * 1000;
      }
    } else if ((program[ProgramNum].WType == "B" || program[ProgramNum].WType == "C") && PipeSensor.avgTemp < PipeSensor.BodyTemp + PipeSensor.SetTemp && millis() >= t_min && t_min > 0 && program_Wait) {
      SendMsg(("Продолжаем отбор после автоматической паузы"), NOTIFY_MSG);//продолжаем отбор
      setautospeed = true;
      t_min = 0;
      program_Wait = false;
      pause_withdrawal(false);
  }
  vTaskDelay(10 / portTICK_PERIOD_MS);
}
void pump_calibrate(int stpspeed) {// Калибровка насоса
  if (startval != 0 && startval != 100) {
    return;
  }
  if (stpspeed == 0) {
    startval = 0;
    //Сохраняем полученное значение калибровки
    if (Samovar_Mode == SAMOVAR_NBK_MODE) {
    SamSetup.NBK_StepperStepMl = round((float)stepper.getCurrent() / 1000);}
    else {SamSetup.StepperStepMl = round((float)stepper.getCurrent() / 100);}
    set_stepper_target(0,0,0);
    //save_profile();
  } else {
    startval = 100;
    //крутим двигатель, пока не остановят
    if (!stepper.getState()) stepper.setCurrent(0);
    stepper.setMaxSpeed(stpspeed);
    stepper.setTarget(999999999);
    startService();
  }
}
void pause_withdrawal(bool Pause) {//Обработка паузы
  if (Samovar_Mode != SAMOVAR_RECTIFICATION_MODE) return;
  if (!stepper.getState() && !stepper2.getState() && !PauseOn) return;
  PauseOn = Pause;
  if (Pause) {//При паузе
    if (SamSetup.UsePump2 && (program[ProgramNum].WType == "H" || program[ProgramNum].WType == "B" || program[ProgramNum].WType == "C")){//Тормозим второй насос в режимах отбора тела, предзахлеба и голов
        TargetStepps2 = stepper2.getTarget();
        CurrentStepps2 = stepper2.getCurrent();
        CurrentStepper2Speed = stepper2.getSpeed();
        stopService2();
        stepper2.brake();
        stepper2.disable();
        if (program[ProgramNum].WType == "H") {
          CurrentStepperSpeed = stepper2.getSpeed();
          return;
      }
   } 
    TargetStepps = stepper.getTarget();
    CurrentStepps = stepper.getCurrent();
    CurrentStepperSpeed = stepper.getSpeed();
    //if (CurrrentStepperSpeed < 1) CurrrentStepperSpeed = (uint16_t)max(1, (int)abs((int)stepper.getSpeed()));
    stopService();//Первый насос тормозим во всех режимах
    stepper.brake();
    stepper.disable();
  } else {//При не паузе
    if (SamSetup.UsePump2 && (program[ProgramNum].WType == "H" || program[ProgramNum].WType == "B" || program[ProgramNum].WType == "C")){
      stepper2.setMaxSpeed(CurrentStepper2Speed); //Стартуем второй насос в режимах отбора голов и тела
      stepper2.setCurrent(CurrentStepps2);
      stepper2.setTarget(TargetStepps2);
      startService2();
      if (program[ProgramNum].WType == "H") {
        CurrentStepperSpeed = stepper2.getSpeed();
        return;
      }
    }
    stepper.setMaxSpeed(CurrentStepperSpeed); // Первый насос стартует всегда
    stepper.setCurrent(CurrentStepps);
    stepper.setTarget(TargetStepps);
    startService();
  }
}
void set_program(String WProgram) {// Установить программу
  //  WProgram.trim();
  //  if (WProgram.length() == 0) return;
  char c[500] = {0};
  WProgram.toCharArray(c, 500);
  char *pair = strtok(c, ";");
  int i = 0;
  while (pair != NULL && i < MAX_PRG) {
    program[i].WType = pair;
    pair = strtok(NULL, ";");
    program[i].Volume = atoi(pair);
    pair = strtok(NULL, ";");
    program[i].Speed = atof(pair);
    pair = strtok(NULL, ";");
    program[i].capacity_num = atoi(pair);
    pair = strtok(NULL, ";");
    program[i].Temp = atof(pair);
    pair = strtok(NULL, "\n");
    program[i].Power = atof(pair);
    if (program[i].WType == "P") {
      program[i].Time = program[i].Volume / 60 / (float)60;
    } else {
      program[i].Time = program[i].Volume / program[i].Speed / 1000;
    }
    i++;
    ProgramLen = i;
    pair = strtok(NULL, ";");
    if ((!pair || pair == NULL || pair[0] == 13) && i < MAX_PRG) {
      program[i].WType = "";
      break;
    }
  }
}
String get_program(uint8_t s) {// Получить программу
  String Str = "";
  Str.reserve(512);
  uint8_t k = MAX_PRG;
  if (s == MAX_PRG) {
    s = 0;
  } else {
    k = s + 1;
  }
  for (uint8_t i = s; i < k; i++) {
    if (program[i].WType.length() == 0) {
      return Str;
    } else {
      Str += program[i].WType + ";";
      Str += (String)program[i].Volume + ";";
      Str += (String)program[i].Speed + ";";
      Str += (String)program[i].capacity_num + ";";
      Str += (String)program[i].Temp + ";";
      Str += (String)program[i].Power + "\n";
    }
  }
  return Str;
}
void run_program(uint8_t num) {// Запустить программу
  t_min = 0;
  program_Pause = false;
  program_Wait = false;
  PauseOn = false;
  pause_withdrawal(false);
  SteamSensor.StartProgTemp = SteamSensor.avgTemp;//запоминаем текущие значения температур
  PipeSensor.StartProgTemp = PipeSensor.avgTemp;
  WaterSensor.StartProgTemp = WaterSensor.avgTemp;
  TankSensor.StartProgTemp = TankSensor.avgTemp;
  String p_s;// Наполняем строку для сообщения в WEB интерфейс
    set_stepper2_target(0,0,0);// тормозим 2 насос при смене программы
  if (num == MAX_PRG) {  //если num = MAX_PRG значит мы достигли финала (или отбор сброшен принудительно), завершаем отбор
    ProgramNum = 0;
    set_stepper_target(0,0,0);
    set_capacity(0);
    if (fileToAppend) {
      fileToAppend.close();
    }
    stop_process("Выполнение программы завершено.");
  } else {// Номер программы в допустимых пределах
    if (SamSetup.PwrType != NO_POVER_REG) {//Используется регулятор мощности
      int tt = (SamSetup.PwrType >= STAB_AVR) ? 400 : 40;
      if (abs(program[num].Power) > tt && program[num].Power > 0) {
        set_current_power(program[num].Power); // Команда регулятору
      } else if (program[num].Power != 0) {
        set_current_power(target_power_volt + program[num].Power);
      }
      vTaskDelay(500 / portTICK_PERIOD_MS);
      if (program[num].WType == "C" && alarm_c_low_min == 0) alarm_c_low_min = millis();
    }
    p_s = "Программа: старт строки  №" + (String)(num + 1);
    // В режимах отбора голов, тела, предзахлёба
    if (program[num].WType == "H" || program[num].WType == "B" || program[num].WType == "T" || program[num].WType == "C") {
      if (program[num].WType == "H" || program[num].WType == "T") {
        SteamSensor.BodyTemp = 0;
        PipeSensor.BodyTemp = 0;
        WaterSensor.BodyTemp = 0;
        TankSensor.BodyTemp = 0;
      }
      p_s += ", отбор в ёмкость " + (String)program[num].capacity_num;
      //устанавливаем параметры для текущей программы отбора
      set_capacity(program[num].capacity_num);
      if (SamSetup.UsePump2 && program[num].WType == "H"){ // отбор над ЦП в режиме головы
        stepper2.setMaxSpeed(get_speed_from_rate(program[num].Speed, SamSetup.StepperStepMl));
        TargetStepps = program[num].Volume * SamSetup.StepperStepMl;
        stepper2.setCurrent(0);
        stepper2.setTarget(TargetStepps);
        startService2();
      } else {//Отбор голов первым насосом
        stepper.setMaxSpeed(get_speed_from_rate(program[num].Speed, SamSetup.StepperStepMl));
        TargetStepps = program[num].Volume * SamSetup.StepperStepMl;
        stepper.setCurrent(0);
        stepper.setTarget(TargetStepps);
        startService();
      }
      if (SamSetup.UsePump2 && (program[num].WType == "B" || program[num].WType == "C")){  // отбор голов во время отбора тела или предзахлёба со скоростью SpPump2
        //float v = round(SamSetup.StepperStepMl * SamSetup.SpPump2 * 1000 / 3.6) / 1000.00;
        float v = round(SamSetup.StepperStepMl * SamSetup.SpPump2 / 3.6);
        if (v < 1) v = 1;
        stepper2.setMaxSpeed(v);
        stepper2.setCurrent(0);
        stepper2.setTarget(2147483640);
        startService2();
      }
      if ((program[num].WType == "B" || program[num].WType == "C") && program[num].Temp > 0) {
        SteamSensor.BodyTemp = program[num].Temp; //Если в программе задана явно температура устанавливаем
      }
      if ((program[num].WType == "B" || program[num].WType == "C") && SteamSensor.BodyTemp == 0) {
        set_body_temp(); //Если у первой программы отбора тела не задана температура, при которой начинать отбор, считаем, что она равна текущей
      }
    } else if (program[num].WType == "P") { //В режиме паузы
      //Сбрасываем Т тела, так как при изменении напряжения на регуляторе изменяется Т в царге.
      SteamSensor.BodyTemp = 0;
      PipeSensor.BodyTemp = 0;
      WaterSensor.BodyTemp = 0;
      TankSensor.BodyTemp = 0;
      //устанавливаем параметры ожидания для программы паузы. Время в секундах задано в program[num].Volume
      p_s += ", пауза " + (String)program[num].Volume + " сек.";
      t_min = millis() + program[num].Volume * 1000;
      program_Pause = true;
      stopService(); 
      set_stepper2_target(0,0,0);
      stepper.setMaxSpeed(0); 
      //stepper.setSpeed(-1);
      stepper.brake(); 
      stepper.disable(); 
      stepper.setCurrent(0); 
      stepper.setTarget(0); 
    }
  }
  if (SamSetup.ChangeProgramBuzzer) {
    set_buzzer(true);
    SendMsg(p_s, ALARM_MSG);//Видимо способ включить сигнализацию в браузере при смене программы
  } else {
    SendMsg(p_s, NOTIFY_MSG);
  }
  if (SamSetup.UsePump2 && program[num].WType == "H") TargetStepps = stepper2.getTarget(); else TargetStepps = stepper.getTarget();
}
float get_temp_by_pressure(float start_pressure, float start_temp, float current_pressure) {//функция корректировки температуры кипения спирта в зависимости от давления
  if (start_temp == 0) return 0;
  if (current_pressure < 10) return start_temp;

  //скорректированная температура кипения спирта при текущем давлении
  float c_temp;

  if (SamSetup.UsePreccureCorrect) {
    //идеальная температура кипения спирта при текущем давлении
    float i_temp;
    //температурная дельта
    float d_temp;

    i_temp = current_pressure * 0.038 + 49.27;

    if (start_pressure == 0) {
      d_temp = start_temp - 78.15;
    } else {
      d_temp = start_temp - start_pressure * 0.038 - 49.27;  //учитываем поправку на погрешность измерения датчиков
    }
    c_temp = i_temp + d_temp;  // получаем текущую температуру кипения при переданном давлении с учетом поправки
  } else {
    //Используем сохраненную температуру отбора тела без корректировки
    c_temp = start_temp;
  }

  return c_temp;
}
void set_body_temp() {// Установить температуру тела
  if (program[ProgramNum].WType == "B" || program[ProgramNum].WType == "C" || program[ProgramNum].WType == "P") {
    SteamSensor.Start_Pressure = bme_pressure;
    SteamSensor.BodyTemp = SteamSensor.avgTemp;
    PipeSensor.BodyTemp = PipeSensor.avgTemp;
    WaterSensor.BodyTemp = WaterSensor.avgTemp;
    TankSensor.BodyTemp = TankSensor.avgTemp;
    SendMsg("Новые Т тела: пар = " + String(SteamSensor.BodyTemp) + ", царга = " + String(PipeSensor.BodyTemp), WARNING_MSG);
  } else {
    SendMsg(("Не возможно установить Т тела."), WARNING_MSG);
  }
}
void check_alarm_rect() {
  static bool close_valve_message_sent = false;

  if (SamSetup.PwrType != NO_POVER_REG) {  //управляем разгонным тэном
    if (SamovarStatusInt == 50 && TankSensor.avgTemp <= SamSetup.Opn_Vlv_Tnk_T && PowerOn) {
      if (!acceleration_heater) { //включаем разгонный тэн
        digitalWrite(RELE_CHANNEL4, SamSetup.rele4);
        acceleration_heater = true;
      }
    } else {
      if (acceleration_heater) { //выключаем разгонный тэн
        digitalWrite(RELE_CHANNEL4, !SamSetup.rele4);
        acceleration_heater = false;
      }
    }
  }
  if (SamSetup.UseHLS && PowerOn) { //Если используется датчик уровня флегмы в голове
    whls.tick();
    if (whls.isHolded() && alarm_h_min == 0) {
      whls.resetStates();
      if (program[ProgramNum].WType != "C") {
        set_buzzer(true);
        SendMsg(("Сработал датчик захлёба!"), ALARM_MSG);
        if (SamSetup.PwrType != NO_POVER_REG) {
          alarm_c_min = 0;
          alarm_c_low_min = 0;
          prev_target_power_volt = 0;
        }
      } else {
        if (SamSetup.PwrType != NO_POVER_REG) {
          //запускаем счетчик - TIME_C минут, нужен для возврата заданного напряжения
          alarm_c_min = millis() + 1000 * 60 * TIME_C / 5;
          //счетчик для повышения напряжения сбрасываем
          alarm_c_low_min = 0;
          if (prev_target_power_volt == 0) prev_target_power_volt = target_power_volt;
        }
      }
      if (SamSetup.PwrType != NO_POVER_REG) {
        SendMsg((String)PwrMSG_str + " снижаем c " + (String)target_power_volt, NOTIFY_MSG);
          if (SamSetup.PwrType >= STAB_AVR) {
            set_current_power(target_power_volt - target_power_volt / 100 * 2);
          } else {
            set_current_power(target_power_volt - 1 * PwrFactor);
          }  
      }
      //Если уже реагировали - надо подождать 40 секунд, так как процесс инерционный
      alarm_h_min = millis() + 1000 * 40;
    }

    if (alarm_h_min > 0 && alarm_h_min <= millis()) {
      whls.resetStates();
      alarm_h_min = 0;
    }
    if (SamSetup.PwrType != NO_POVER_REG) {
      //Если программа - предзахлеб, и сброс напряжения был больше TIME_C минут назад, то возвращаем напряжение к последнему сохраненному - 0.5
      if (alarm_c_min > 0 && alarm_c_min <= millis()) {
        if (program[ProgramNum].WType == "C") {
          if (prev_target_power_volt == 0) {
            if (SamSetup.PwrType >= STAB_AVR)
            prev_target_power_volt = target_power_volt + target_power_volt / 100 * 4;
            else
            prev_target_power_volt = target_power_volt + 2 * PwrFactor;
          }
          if (SamSetup.PwrType >= STAB_AVR)
          set_current_power(prev_target_power_volt - target_power_volt / 100 * 3);
          else
          set_current_power(prev_target_power_volt - 1 * PwrFactor);
          SendMsg((String)PwrMSG_str + " повышаем до " + (String)target_power_volt, NOTIFY_MSG);
          prev_target_power_volt = 0;
          //запускаем счетчик - TIME_C минут, нужен для повышения текущего напряжения чтобы поймать предзахлеб
          alarm_c_low_min = millis() + 1000 * 60 * TIME_C;
        }
        alarm_c_min = 0;
      }
      //Если программа предзахлеб и давно не было срабатывания датчика - повышаем напряжение
      if (program[ProgramNum].WType == "C") {
        if (alarm_c_low_min > 0 && alarm_c_low_min <= millis()) {
          if (SamSetup.PwrType >= STAB_AVR)
          set_current_power(target_power_volt + target_power_volt / 100 * 1);
          else
          set_current_power(target_power_volt + 0.5 * PwrFactor);
          alarm_c_low_min = millis() + 1000 * 60 * TIME_C;
        } else if (alarm_c_low_min == 0 && alarm_c_min == 0) {
          alarm_c_low_min = millis() + 1000 * 60 * TIME_C;
        }
      } else alarm_c_low_min = 0;
    }
  }
  if (SamSetup.PwrType != NO_POVER_REG) {
    check_power_error();
  }
  if (!valve_status) {// призыв открыть/закрыть воду либо автоматом клапан по выпару из ТСА
    if (ACPSensor.avgTemp >= SamSetup.Max_ACP_T - 5) {
      set_buzzer(true);
      open_valve(true, true);
    } else if (TankSensor.avgTemp >= SamSetup.Opn_Vlv_Tnk_T && PowerOn) {
      set_buzzer(true);
      open_valve(true, true);
    }
  }
  if (!PowerOn && !is_self_test && valve_status && WaterSensor.avgTemp <= SamSetup.SetWaterTemp - SamSetup.D_Cls_Vlv && 
      ACPSensor.avgTemp <= SamSetup.Max_ACP_T - 10) {
    if (!close_valve_message_sent) {
      open_valve(false, true);
      set_buzzer(true);
      close_valve_message_sent = true;
    }
    if (SamSetup.UseWP) {
        if (pump_started) set_pump_pwm(0);
    }
  } 
  if (valve_status && close_valve_message_sent)  {
    close_valve_message_sent = false;
  }
   
  //Проверяем, что температурные параметры не вышли за предельные значения
  if ((SteamSensor.avgTemp >= SamSetup.Max_St_T || WaterSensor.avgTemp >= SamSetup.Max_Wt_T || TankSensor.avgTemp >= SamSetup.DistTemp || ACPSensor.avgTemp >= SamSetup.Max_ACP_T) && PowerOn) {
    //Если с температурой проблемы - выключаем нагрев, пусть оператор разбирается
    delay(1000);  //Пауза на всякий случай, чтобы прошли все другие команды
    set_buzzer(true);
    set_power(false);
    String s = "";
    if (SteamSensor.avgTemp >= SamSetup.Max_St_T) s = s + " Пара";
    else if (WaterSensor.avgTemp >= SamSetup.Max_Wt_T)
      s = s + " Воды";
    else if (ACPSensor.avgTemp >= SamSetup.Max_ACP_T)
      s = s + " ТСА";

    if (TankSensor.avgTemp >= SamSetup.DistTemp) {
      //Если температура в кубе превысила заданную, штатно завершаем ректификацию.
      SendMsg(("Лимит максимальной температуры куба. Программа завершена."), NOTIFY_MSG);
    } else
      SendMsg("Аварийное отключение! Превышена максимальная температура" + s, ALARM_MSG);
  }

  WaterSratusCheck();       //Проверим, что вода подается
  WaterPumpReg();           // регулируем водяной насос 
  WaterTempAlarmPowerReg(); // При перегреве воды снижаем мощность

  if (SamovarStatusInt == 50 && SteamSensor.avgTemp >= SamSetup.Ch_Pwr_Md_St_T) { //Окончание разгона
    if (SamSetup.UseWP) {//Сбросим счетчик насоса охлаждения, что приведет к увеличению потока воды. Дальше уже будет штатно работать PID
      wp_count = -5;
    }
    SamovarStatusInt = 51;  //достигли заданной температуры на разгоне, переходим на рабочий режим, устанавливаем заданную температуру, зовем оператора
    SendMsg("Разгон завершён. Стабилизация/работа на себя.", NOTIFY_MSG);
    set_buzzer(true);
    if (SamSetup.PwrType != NO_POVER_REG) {
      set_current_power(program[0].Power);
    } else {
    current_power_mode = POWER_WORK_MODE;
    digitalWrite(RELE_CHANNEL4, !SamSetup.rele4);
    }
  }
  if (SamovarStatusInt == 51 && !boil_started) {
    set_boiling();
    if (boil_started) {
      SendMsg("Спиртуозность " + format_float(alcohol_s, 1), WARNING_MSG);
    }
  }

  //Разгон и стабилизация завершены - шесть минут температура пара не меняется больше, чем на 0.1 градус:
  //https://alcodistillers.ru/forum/viewtopic.php?id=137 - указано 3 замера раз в три минуты.
  if ((SamovarStatusInt == 51 || SamovarStatusInt == 52) && SteamSensor.avgTemp > SamSetup.Ch_Pwr_Md_St_T) {
    float d = SteamSensor.avgTemp - SteamSensor.PrevTemp;
    d = abs(d);
    if (d < 0.1) {
      acceleration_temp += 1;
      if (acceleration_temp == 60 * 6) {
        SamovarStatusInt = 52;
        set_buzzer(true);
        SendMsg(("Стабилизация завершена, колонна работает стабильно."), NOTIFY_MSG);
        //Здесь начиная с 6.26 у Алексея автостарт голов если включено смачивание насадки
        //if (wetting_autostart && startval == 0) {
        //  wetting_autostart = false;
        //menu_samovar_start();  // Автостарт голов после стабилизации
        //}
        if (SamSetup.r_ASt) sam_command_sync = SAMOVAR_START; //Включаем отбор после стабилизации, простой автостарт
      }
    } else {
      acceleration_temp = 0;
      SteamSensor.PrevTemp = SteamSensor.avgTemp;
    }
  }
 if (SamSetup.UseWV && !SamSetup.UseWP) { //если клапан воды и не насос
  if (WaterSensor.avgTemp >= SamSetup.SetWaterTemp + 1) {
    digitalWrite(WATER_PUMP_PIN, SamSetup.UseWV - 1);
  } else if (WaterSensor.avgTemp <= SamSetup.SetWaterTemp - 1) {
    digitalWrite(WATER_PUMP_PIN, !(SamSetup.UseWV-1));
  }
 }
}