//-----------Перистальтические насосы
 // Настройки для шагового двигателя 2 насоса в режиме пиво
  void startService(void) { // Запустить перист. насос
    #if (defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 3))
      timerAlarm(timer, stepper.getPeriod(), true, 0);
    #else  // ESP_ARDUINO_VERSION_MAJOR >= 3
      timerAlarmWrite(timer, stepper.getPeriod(), true);
      timerAlarmEnable(timer);
    #endif
    ActualVolumePerHour = get_liquid_rate_by_step(stepper.getSpeed());
  }
  void startService2(void) {  // Запустить перист. насос2
    #if (defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 3))
      timerAlarm(timer2, stepper2.getPeriod(), true, 0);
    #else  // ESP_ARDUINO_VERSION_MAJOR >= 3
      timerAlarmWrite(timer2, stepper2.getPeriod(), true);
      timerAlarmEnable(timer2);
    #endif
    ActualVolumePerHour2 = get_liquid_rate_by_step(stepper2.getSpeed());
  }
  void stopService(void) { // Остановить перист. насос
    #if (defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 3))
      timerWrite(timer, 0);
    #else  // ESP_ARDUINO_VERSION_MAJOR >= 3
      timerAlarmDisable(timer);
    #endif
    ActualVolumePerHour = 0;
  }
  void stopService2(void) { // Остановить перист. насос2
    #if (defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 3))
      timerWrite(timer2, 0);
    #else  // ESP_ARDUINO_VERSION_MAJOR >= 3
      timerAlarmDisable(timer2);
    #endif
    ActualVolumePerHour2 = 0;
  }
  void IRAM_ATTR StepperTicker(void) { // Генерация шагов насосу 
    portENTER_CRITICAL_ISR(&timerMux);
    StepperMoving = stepper.tickManual();
    portEXIT_CRITICAL_ISR(&timerMux);
  }
  void IRAM_ATTR Stepper2Ticker(void) { // Генерация шагов насосу 2
    portENTER_CRITICAL_ISR(&timerMux2);
    Stepper2Moving = stepper2.tickManual();
    portEXIT_CRITICAL_ISR(&timerMux2);
  }
  float get_liquid_volume_by_step(float StepCount) {// Получить объем отбора
    if (Samovar_Mode == SAMOVAR_NBK_MODE) 
      return (SamSetup.NBK_StepperStepMl > 0) ? (float)StepCount / SamSetup.NBK_StepperStepMl : 0;
      else return (SamSetup.StepperStepMl > 0) ? (float)StepCount / SamSetup.StepperStepMl : 0;
  }
  float get_liquid_rate_by_step(int StepperSpeed) {// Получить скорость отбора
    //return round(get_liquid_volume_by_step(StepperSpeed) * 3.6 * 1000) / 1000.00;
    return get_liquid_volume_by_step(StepperSpeed) * 3.6;
  }
  float get_speed_from_rate(float rate, uint16_t s_per_l) {// Получить скорость из расхода
    //ActualVolumePerHour = rate;
    //float v = round(s_per_l * rate * 1000 / 3.6) / 1000.00;
    float v = (s_per_l * rate) / 3.6;
    return (v < 1) ? 1 : v;  // Минимальная скорость 1
  }
  bool set_stepper_target(uint16_t spd, uint8_t direction, uint32_t target) {//spd - скорость в шагах в секунду, direction - прямое или обратное направление, target - количество шагов
    bool result = false;
    CurrentStepperSpeed = spd;
    stopService();
    if (spd > 0) {
      stepper.reverse(direction);
      CurrentStepperSpeed = spd;
      stepper.setMaxSpeed(spd);
      stepper.setCurrent(0);
      stepper.setTarget(target);
      //stepper.setSpeed(spd);
      startService();
    } else {
      stopService();
      stepper.brake();
      stepper.disable();
      stepper.setCurrent(0);
      stepper.setTarget(0);
    }
    return true;
  }
  bool set_stepper2_target(uint16_t spd, uint8_t direction, uint32_t target) {
      CurrentStepper2Speed = spd;
      stopService2();
      if (spd > 0) {
        stepper2.reverse(direction);
        stepper2.setMaxSpeed(spd);
        stepper2.setCurrent(0);
        stepper2.setTarget(target);
        //stepper.setSpeed(spd);
        startService2();
      } else {
        stopService2();
        stepper2.brake();
        stepper2.disable();
        stepper2.setCurrent(0);
        stepper2.setTarget(0);
      }
      return true;
  }
  bool set_stepper2_by_time(uint16_t spd, uint8_t direction, uint16_t time) {//spd - скорость в оборотах в минуту, direction - прямое или обратное направление, time - время включения двигателя в секундах
    return set_stepper2_target(spd * SamSetup.MixerPWM / 60, direction, time * spd * SamSetup.MixerPWM / 60);
  }
  void set_pump_speed(float pumpspeed, bool continue_process) {// Установить скорость насоса
    if (pumpspeed < 1) return;
    if (!(SamovarStatusInt == 10 || SamovarStatusInt == 15 || SamovarStatusInt == 40)) return;
    bool cp = continue_process;
    if (!stepper.getState()) cp = false;
    CurrentStepperSpeed = pumpspeed;
    stopService();
    stepper.setMaxSpeed(pumpspeed);
    stepper.setTarget(stepper.getTarget());
    stepper.reverse(SamSetup.StRev1);
    if (cp) startService();
      if (ActualVolumePerHour == 0) program[ProgramNum].Time = 65535;//Пересчитываем время отбора этой строки программы на текущую скорость
    else
      program[ProgramNum].Time = program[ProgramNum].Volume / ActualVolumePerHour / 1000;
  }
  void set_pump2_speed(float pumpspeed, bool continue_process) {// Установить скорость насоса 2
    if (pumpspeed < 1) return;
    if (!(SamovarStatusInt == 10 || SamovarStatusInt == 15 || SamovarStatusInt == 40)) return;
    stopService2();
    stepper2.setMaxSpeed(pumpspeed);
    stepper2.setTarget(stepper2.getTarget());
    stepper2.reverse(SamSetup.StRev2);
    if (continue_process) startService2();
  }
  
  /**
  * @brief Совершает шаг шаговым двигателем для засыпи хмеля и др. инградиентов
  */
  void HopStepperStep(uint16_t speed, uint16_t steps) {
    stopService();
    stepper.brake();
    stepper.disable();
    stepper.setMaxSpeed(speed); //скорость движения шагового двигателя
    //stepper.setSpeed(200);    //скорость движения шагового двигателя, должна быть равна предыдущей
    TargetStepps = steps;  //16 - множитель на драйвере двигателя. 20 - количество отверстий по целому кругу (если бы они занимали всю окружность)
    stepper.setCurrent(0);
    stepper.setTarget(TargetStepps);
    stepper.reverse(SamSetup.StRev1);
    stepper.enable();
    startService();
  }
//-----------Вода, насос и клапаны
  void SetWatherValve2(bool state) {// Управление дополнительным клапаном воды
    if (!SamSetup.UseWP) { //Если не задействован водяной насос
      if (state) pump_pwm.write(((SamSetup.UseWV-1))? 0 : 1023);//вкл
      else pump_pwm.write((!(SamSetup.UseWV-1))? 0 : 1023);//откл
    }
    
  }
//-----------Мешалка
  /**
  * @brief Управляет состоянием мешалки и насоса в зависимости от этапа программы и времени.
  */
  void check_mixer_state() {
    if (program[ProgramNum].capacity_num > 0) {
      //обрабатываем время включения и управляем мешалкой и насосом

      if (alarm_c_min > 0 && alarm_c_min <= millis()) {
        //завершили паузу мешалки
        alarm_c_min = 0;
        alarm_c_low_min = 0;
        set_mixer_state(OFF);
      }

      if ((alarm_c_low_min > 0) && (alarm_c_low_min <= millis())) {
        //выключаем мешалку, если alarm_c_min > millis()
        alarm_c_low_min = 0;
        if (alarm_c_min > 0)
          set_mixer_state(OFF);
      }

      if (alarm_c_low_min == 0 && alarm_c_min == 0) {
        //включаем мешалку
        alarm_c_low_min = millis() + program[ProgramNum].Volume * 1000;
        if (program[ProgramNum].Power > 0) alarm_c_min = alarm_c_low_min + program[ProgramNum].Power * 1000;
        currentstepcnt++;
        bool dir = false;
        if (currentstepcnt % 2 == 0 && program[ProgramNum].Speed < 0) dir = true;
        set_mixer_state(ON, dir);
      }

    } else {
      if (mixer_status) {
        //если мешалка или насос работают, их нужно выключить, так как в этой строке программы они не нужны
        set_mixer_state(OFF);
      }
    }
  }
  void SetMixerOnBTS7960(bool on, bool dir, uint16_t pwm){
    if (on) {
      digitalWrite(STEPPER2_EN, dir ); // Устанавливаем направление
      digitalWrite(STEPPER2_DIR, !dir); 
      pump_pwm.write(pwm); // Устанавливаем PWM ШИМ на входах драйвера L_EN и R_EN, это скорость
    } else {
      digitalWrite(STEPPER2_EN, !dir ); // Устанавливаем одинаковый потенциал на выходе к мотору
      digitalWrite(STEPPER2_DIR, !dir); 
      pump_pwm.write(0);  // Устанавливаем логический 0 на входах драйвера L_EN и R_EN, значит выходы M+ и M- перейдут в состояние высокого импеданса и мотор будет электрически отключён.
    }
  }
  /**
  * @brief Включает или выключает мешалку и насос, а также управляет направлением вращения.
  * @param state true — включить, false — выключить
  * @param dir true — реверс, false — прямое вращение
  */
  void set_mixer_state(bool state, bool dir) {
    mixer_status = state;
    //DbgMsg("State = " + String(state) + "; DIR = " + String(dir) + "; alarm_c_min = " + String(alarm_c_min) + "; alarm_c_low_min = " + String(alarm_c_low_min),1);
    if (state) {//включаем мешалку
      
      if (BitIsSet(program[ProgramNum].capacity_num, 0)) {
        switch (SamSetup.MixerMode) { // 0 - не использовать, 1 - rele2 + dir2, 2 - Stepper2, 3 - BTS7960 на пинах stepper2
          case 1: { digitalWrite(RELE_CHANNEL2, SamSetup.rele2);//включаем реле 2,
                    digitalWrite(STEPPER2_EN, 1);//и пин en2 для альтернативы,
                    digitalWrite(STEPPER2_DIR, dir); // с dir шд2 можно взять направление (вариант 1)
                    break;
                  }
          case 2: {// включаем шаговик 2 с направлением dir (вариант 2)
                    int tm = abs(program[ProgramNum].Volume);
                    if (tm == 0) tm = 10;
                    bool b = set_stepper2_by_time(20, dir, tm);
                    break;
                  }
          case 3: { // включаем драйвер  мотора BTS7960
                    SetMixerOnBTS7960(true, dir, SamSetup.MixerPWM);
                    break;
                  }
        }
      }
      if (BitIsSet(program[ProgramNum].capacity_num, 1)) {
        if (SamSetup.UseWP) pump_pwm.write(1023);//включаем SSD реле, вода
      }
    } else {//выключаем мешалку,
        switch (SamSetup.MixerMode) { // 0 - не использовать, 1 - rele2 + dir2, 2 - Stepper2, 3 - BTS7960 на пинах stepper2
        case 1: digitalWrite(RELE_CHANNEL2, !SamSetup.rele2);//отключаем реле 2, (вариант 1)
                digitalWrite(STEPPER2_EN, 0);//и пин en2 для альтернативы,
                break;
        case 2: // отключаем шаговик 2 (вариант 2)
                set_stepper2_target(0, SamSetup.StRev2, 0);
                break;
        case 3:  // отключаем драйвер  мотора BTS7960
                SetMixerOnBTS7960(false, 0, 0);
                break;
      }
      if (SamSetup.UseWP) pump_pwm.write(0); //выключаем SSD реле - вода
    }
  }

//-----------Узел разбора (сервопривод)
  void setServoAngle(int angle) {// Функция для установки угла сервопривода   
      int minPulse = 510; // Для 14 бит, 50Hz
      int maxPulse = 2450;
      int pulseWidth = map(angle, 0, 180, minPulse, maxPulse);
      // расчет для 14 бит: duty = (pulse_width * 2^14) / (20000), где 20000 = 1000000/50 (период в микросекундах)
      int duty = (pulseWidth * 16384) / 20000;
      if (SamSetup.dbg) Serial.println("Angle: " + String(angle) + " -> Pulse: " + String(pulseWidth) + "us -> Duty: " + String(duty) + "/16384");
      servo_pwm.write(duty);
  }
  void set_capacity(uint8_t cap) {// Установить емкость
    capacity_num = cap;
    int p = ((int)cap * SERVO_ANGLE) / (int)CAPACITY_NUM + SamSetup.servoDelta[cap];
    setServoAngle(p);
  }
  void next_capacity(void) {// Переход к следующей емкости
    set_capacity(capacity_num + 1);
  }
//-----------Кнопки
  void InitButtons() {
    btn.setType(LOW_PULL);
    btn.setTickMode(MANUAL);
    btn.setDebounce(30); 
    alarm_btn.setType(HIGH_PULL);
    alarm_btn.setTickMode(MANUAL);
    alarm_btn.setDebounce(30);
    
    //Задаем параметры для сенсора уровня флегмы
    whls.setDebounce(50);  //игнорируем дребезг
    whls.setTickMode(MANUAL);
    whls.setTimeout(WHLS_ALARM_TIME * 1000);  //время, через которое сработает тревога по уровню флегмы

    #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  
  }
  void ButtonsTick() {
    static uint32_t lastCheckTime = 0;
    uint32_t currentTime = millis();
    if (currentTime - lastCheckTime >= 20) { // Опрос каждые 20мс (50Hz)
        lastCheckTime = currentTime;
        if (SamSetup.UseBTN) btn.tick();        
        if (SamSetup.UseA_BTN) alarm_btn.tick();
        if (SamSetup.HLS) whls.tick();
        #if BOARD == ESP32S3
        BLU_s.tick();
        BLD_s.tick();
        #endif
      if (SamSetup.UseA_BTN && alarm_btn.isPress()) set_alarm();
      
      if (SamSetup.UseBTN) {
        //обработка нажатий кнопки и разное поведение в зависимости от режима работы
        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);
          } else if (Samovar_Mode == SAMOVAR_CHEESE_MODE) {
            //если сыр включаем или двигаем программу
            if (!PowerOn) {
              sam_command_sync = SAMOVAR_CHEESE;
            } else
              run_cheese_program(ProgramNum + 1);
          }
        }
      }
    }
  }
//-----------Датчик расхода воды
  void IRAM_ATTR WFpulseCounter() { // Счетчик расхода воды
    WFpulseCount++;
  }
//-----------Датчик кислотности
  void ReadAcid() {
    #define SAMPLES 30
    #define TRIM    5
    static bool first_run=true;
    int buf[SAMPLES];
    for (int i = 0; i < SAMPLES; i++) {
        buf[i] = analogRead(LUA_PIN);
        delay(2);
    }
    // сортировка по возрастанию
    for (int i = 0; i < SAMPLES - 1; i++)
        for (int j = i + 1; j < SAMPLES; j++)
            if (buf[i] > buf[j]) { int t = buf[i]; buf[i] = buf[j]; buf[j] = t; }
    // среднее по средним значениям
    long sum = 0;
    for (int i = TRIM; i < SAMPLES - TRIM; i++) sum += buf[i];
    ALUA = sum / 20;
    if (first_run) {
      MAcid = SamSetup.d_Acid + SamSetup.k_Acid * ALUA;
      first_run = false;
    }
    else MAcid = (SamSetup.d_Acid + SamSetup.k_Acid * ALUA)*(100-SamSetup.AcidPrc)/100 + MAcid*SamSetup.AcidPrc/100;
    //Serial.println(String(MAcid,2));
  }
//-----------Модуль чтения датчиков NTC (термисторов)
  #define ADS_Trg 30100  // * Предел для определения подключенных термисторов, в исходнике был 32750, у меня, как оказалось, он должен быть меньше, если оставить \

  #define ADS_I2CADDR_1 (0x48)
  #define ADS_I2CADDR_2 (0x49)
  #define ADS1115_REG_CONFIG_MUX_SINGLE_0 (0x4000)
  #define ADS1115_REG_CONFIG_MUX_SINGLE_1 (0x5000)
  #define ADS1115_REG_CONFIG_MUX_SINGLE_2 (0x6000)
  #define ADS1115_REG_CONFIG_MUX_SINGLE_3 (0x7000)
  #define ADS1115_REG_POINTER_CONVERT (0x00)
  #define ADS1115_REG_POINTER_CONFIG (0x01)

  uint8_t ntcEn[9];                                                 // Массив флагов наличия подключенных датчиков

  static void writeRegister(uint8_t i2cAddress, uint8_t reg, uint16_t value) {  // Запись регистров АЦП
    Wire.beginTransmission(i2cAddress);
    Wire.write((uint8_t)reg);
    Wire.write((uint8_t)(value >> 8));
    Wire.write((uint8_t)(value & 0xFF));
    Wire.endTransmission();
  }
  static uint16_t ReadRegister(uint8_t i2cAddress, uint8_t reg) {              // Чтение регистров АЦП
    Wire.beginTransmission(i2cAddress);
    Wire.write((uint8_t)reg);
    Wire.endTransmission();
    Wire.requestFrom(i2cAddress, (uint8_t)2);
    return ((Wire.read() << 8) | Wire.read());
  }
  uint16_t readADS(uint8_t channel, uint8_t cnt) {                              // Чтение усредненных из количества cnt значений канала АЦП № channel

    if (channel > 7 || cnt == 0) return 0;
    uint8_t ads_i2cAddress = 0;
    uint16_t config = 0b1000010111100011;
    switch (channel) {
      case (0):
        config |= ADS1115_REG_CONFIG_MUX_SINGLE_0;
        ads_i2cAddress = ADS_I2CADDR_1;
        break;
      case (1):
        config |= ADS1115_REG_CONFIG_MUX_SINGLE_1;
        ads_i2cAddress = ADS_I2CADDR_1;
        break;
      case (2):
        config |= ADS1115_REG_CONFIG_MUX_SINGLE_2;
        ads_i2cAddress = ADS_I2CADDR_1;
        break;
      case (3):
        config |= ADS1115_REG_CONFIG_MUX_SINGLE_3;
        ads_i2cAddress = ADS_I2CADDR_1;
        break;
      case (4):
        config |= ADS1115_REG_CONFIG_MUX_SINGLE_0;
        ads_i2cAddress = ADS_I2CADDR_2;
        break;
      case (5):
        config |= ADS1115_REG_CONFIG_MUX_SINGLE_1;
        ads_i2cAddress = ADS_I2CADDR_2;
        break;
      case (6):
        config |= ADS1115_REG_CONFIG_MUX_SINGLE_2;
        ads_i2cAddress = ADS_I2CADDR_2;
        break;
      case (7):
        config |= ADS1115_REG_CONFIG_MUX_SINGLE_3;
        ads_i2cAddress = ADS_I2CADDR_2;
        break;
    }
    uint32_t summ = 0;
    if (xSemaphoreTake(xI2CSemaphore, (TickType_t)(200 / portTICK_RATE_MS)) == pdTRUE) {  
      delay(5);
      for (uint8_t i = 0; i < cnt; i++) {
        writeRegister(ads_i2cAddress, ADS1115_REG_POINTER_CONFIG, config);  // Write config register to the ADC
        delayMicroseconds(1200);
        summ += ReadRegister(ads_i2cAddress, ADS1115_REG_POINTER_CONVERT);
      }
      xSemaphoreGive(xI2CSemaphore);
      return (uint16_t)(summ / cnt);
    } else {
      DbgMsg("No semafor for NTC read.",1);
      return 32751; //если не удалось взять семафор возвращаем ошибку
    }
  }
  int32_t computeTemp_15bit(uint16_t adcRez) {                                  // Перевод значения из АЦП в температуру
    if (adcRez > SamSetup.m_adc[0]) return -55000;
    else if (adcRez < SamSetup.m_adc[150]) return 125000;
    else {
      for (int32_t i = 0; i < 150; i++) {
        if (adcRez <= SamSetup.m_adc[i] && adcRez > SamSetup.m_adc[i + 1]) {
          int32_t ra = (uint32_t)(SamSetup.m_adc[i] - adcRez) * 1000 / (SamSetup.m_adc[i] - SamSetup.m_adc[i + 1]);
          return ((i - 25) * 1000 + ra);
        }
      }
    }
    return -11;
  }
  void NTC_Init() {
      uint8_t error1, error2;
        if (xSemaphoreTake(xI2CSemaphore, (TickType_t)(50 / portTICK_RATE_MS)) == pdTRUE) {  
        Wire.beginTransmission(ADS_I2CADDR_1);
        error1 = Wire.endTransmission();
        Wire.beginTransmission(ADS_I2CADDR_2);
        error2 = Wire.endTransmission();
        xSemaphoreGive(xI2CSemaphore);
      }
    if (error1==0) {
      ntcEn[1] = (readADS(0, 1) < ADS_Trg);// Определение подключенных термисторов
      ntcEn[2] = (readADS(1, 1) < ADS_Trg);
      ntcEn[3] = (readADS(2, 1) < ADS_Trg);
      ntcEn[4] = (readADS(3, 1) < ADS_Trg);      
    } else {
     for (uint8_t i=1; i<5; i++) ntcEn[i] = 0; 
     SerialMsg(F("Not found ADS1"),1);
    }
    
    if (error2==0) {
      ntcEn[5] = (readADS(4, 1) < ADS_Trg);
    } else {
      ntcEn[5] = 0; 
      SerialMsg(F("Not found ADS2"),1);
    }
       
      uint8_t n=0;
      for (uint8_t i=1; i<6; i++) n+= ntcEn[i];
      if (SamSetup.disp == 2) LD.printString_6x8(("/" + String(n) + "N").c_str(), 9*6+1, 7);
      if (SamSetup.disp == 1) {LQ.setCursor(14,3);LQ.print("/" + String(n) + "N");}
      Serial.println("Found NTC:" + String(n));
  }
  float NTC_Read(uint8_t n) {
    float Temp=-127;
    if (ntcEn[n]) Temp = (float)computeTemp_15bit(readADS(n - 1, 16)) / 1000;//16 чтений
    return Temp;
  }
  void NTC_Refresh(float correctTs, float correctTp, float correctTt) {
    float Temp;
      if (ntcEn[1]) {
          Temp = NTC_Read(1);
          if (Temp > -10) {
        SteamSensor.avgTemp = Temp + correctTs + SamSetup.NTC_dT[0] ;
        SteamSensor.PrevTemp = SteamSensor.avgTemp;
        SteamSensor.ErrCount = 0;
      } else {
        if (SteamSensor.PrevTemp > 0) SteamSensor.ErrCount++;
      }
    }
      if (ntcEn[2]) {
          Temp = NTC_Read(2);
          if (Temp > -10) {
        PipeSensor.avgTemp = Temp + correctTp + SamSetup.NTC_dT[1];
        PipeSensor.PrevTemp = PipeSensor.avgTemp;
        PipeSensor.ErrCount = 0;
      } else {
        if (PipeSensor.PrevTemp > 0) PipeSensor.ErrCount++;
      }
    }
      if (ntcEn[3]) {
          Temp = NTC_Read(3);
          if (Temp > -10) { 
        TankSensor.avgTemp = Temp + correctTt + SamSetup.NTC_dT[2];
        TankSensor.PrevTemp = TankSensor.avgTemp;
        TankSensor.ErrCount = 0;
      } else {
        if (TankSensor.PrevTemp > 0) TankSensor.ErrCount++;
      }
    }
      if (ntcEn[4]) {
          Temp = NTC_Read(4);
          if (Temp > -10) {
      WaterSensor.avgTemp = Temp + SamSetup.NTC_dT[3];
      WaterSensor.PrevTemp = WaterSensor.avgTemp;
      WaterSensor.ErrCount = 0;
    } else {
      if (WaterSensor.PrevTemp > 0) WaterSensor.ErrCount++;
      }
    }
      if (ntcEn[5]) {
          Temp = NTC_Read(5);
          if (Temp > -10) { 
        ACPSensor.avgTemp = Temp + SamSetup.NTC_dT[4];
        ACPSensor.PrevTemp = ACPSensor.avgTemp;
        ACPSensor.ErrCount = 0;
      } else {
        if (ACPSensor.PrevTemp > 0) ACPSensor.ErrCount++;
      }
    }
  }

//-----------Чтение DS18B20 и ДД
  #include <Arduino.h>
  #include <DallasTemperature.h>
  #include "Settings.h"
  #include "pumppwm.h"
  #include <Adafruit_BMP085_U.h>
    Adafruit_BMP085_Unified bme;  // I2C

    //**************************************************************************************************************
    // Функции для работы с сенсорами
    //**************************************************************************************************************

    void printAddress(DeviceAddress deviceAddress);

  //***************************************************************************************************************
  // считываем параметры с датчика BME680
  //***************************************************************************************************************
  void BMEInit() {

  }
  void BME_getvalue(bool fl) {
    if (!bmefound) {
      bme_temp = -1;
      bme_pressure = -1;
      return;
    }
    if (SamSetup.Use_BMP180) {
      if (xSemaphoreTake(xI2CSemaphore, (TickType_t)(50 / portTICK_RATE_MS)) == pdTRUE) {
        vTaskDelay(50 / portTICK_PERIOD_MS);
          sensors_event_t event;
          bme.getEvent(&event);
          if (event.pressure) {
            if (SamSetup.BMPFltr) {
              static float pr = event.pressure;//простецкий фильтр
              pr = pr * SamSetup.BMPFltr / 100 + event.pressure * (100 - SamSetup.BMPFltr) / 100;
              bme_pressure = pr * 0.75; 
            } else bme_pressure = event.pressure * 0.75;
            float temp;
            bme.getTemperature(&temp);
            bme_temp = temp;
          }
        xSemaphoreGive(xI2CSemaphore);  
      }
    }
  }

  //***************************************************************************************************************
  // считываем параметры с датчика XGZP6897D
  //***************************************************************************************************************
  void pressure_sensor_get() {
    if (!use_pressure_sensor) {
      pressure_value = -1;
      return;
    }
    float t;
    if (SamSetup.PressureSensor==1) {
      if (xSemaphoreTake(xI2CSemaphore, (TickType_t)(30 / portTICK_RATE_MS)) == pdTRUE) {
        pressure_sensor.readSensor(t, pressure_value);
        xSemaphoreGive(xI2CSemaphore);
      }
      pressure_value = pressure_value / 133.32; //переводим паскали в мм. рт. столба
      pressure_value = (old_pressure_value + pressure_value) / 2; //усредняем давление с предыдущим
      old_pressure_value = pressure_value;
    } else {
    pressure_value = -1;
    }
  }

  //***************************************************************************************************************
  // считываем температуры с датчиков DS18B20
  //***************************************************************************************************************
  void DS_getvalue(void) {

    float ss, ps, ws, ts, acp;
    float correctTs = 0,  correctTp = 0, correctTt = 0;

    //Считаем корректировку температуры от атмосферного давления
    if (SamSetup.UsePreccureCorrect && bme_pressure > 0 && PowerOn) {
      correctTs = (760 - bme_pressure) * 0.037;
      if (SamSetup.UseTPrCor && pressure_value > 0 && pressure_value < 50) {
        correctTt = (760 - bme_pressure - pressure_value) * 0.037;
        correctTp = (760 - bme_pressure - pressure_value / 3) * 0.037;
      } else {
        correctTt = correctTs; correctTp = correctTs;
      }
    } else {
      correctTs = 0;
      correctTt = 0;
      correctTp = 0;
    }
    if (SamSetup.UseDS) {
      ss = correctTs + sensors.getTempC(SteamSensor.Sensor);  // считываем температуру с датчика 0
      ps = correctTp + sensors.getTempC(PipeSensor.Sensor);   // считываем температуру с датчика 1
      ws = sensors.getTempC(WaterSensor.Sensor);  // считываем температуру с датчика 2
      ts = correctTt + sensors.getTempC(TankSensor.Sensor);   // считываем температуру с датчика 3
      acp = sensors.getTempC(ACPSensor.Sensor);   // считываем температуру с датчика 4
    }
    if (SamSetup.PressureSensor==2) {
      float pv;
      uint8_t p_addr[8] = PRESSURE_1WIRE;
      pv = sensors.getTempC(p_addr);   // считываем давление с расширителя по 1Wire
      if (pv > -120) {
        pressure_value = pv;
        use_pressure_sensor = true;
      } else {
        //pressure_value = -1;
        //use_pressure_sensor = false;
      }
    }
    if (SamSetup.UseDS) {
      sensors.requestTemperatures();

      if (ss > -10) {
        SteamSensor.avgTemp = ss + SamSetup.DeltaSteamTemp;
        SteamSensor.PrevTemp = SteamSensor.avgTemp;
        SteamSensor.ErrCount = 0;
      } else {
        if (SteamSensor.PrevTemp > 0) SteamSensor.ErrCount++;
      }
      if (ps > -10) {
        PipeSensor.avgTemp = ps + SamSetup.DeltaPipeTemp;
        PipeSensor.PrevTemp = PipeSensor.avgTemp;
        PipeSensor.ErrCount = 0;
      } else {
        if (PipeSensor.PrevTemp > 0) PipeSensor.ErrCount++;
      }
      if (ws > -10) {
        WaterSensor.avgTemp = ws + SamSetup.DeltaWaterTemp;
        WaterSensor.PrevTemp = WaterSensor.avgTemp;
        WaterSensor.ErrCount = 0;
      } else {
        if (WaterSensor.PrevTemp > 0) WaterSensor.ErrCount++;
      }
      if (ts > -10) {
        TankSensor.avgTemp = ts + SamSetup.DeltaTankTemp;
        TankSensor.PrevTemp = TankSensor.avgTemp;
        TankSensor.ErrCount = 0;
      } else {
        if (TankSensor.PrevTemp > 0) TankSensor.ErrCount++;
      }
      //DbgMsg("ACP t = " + String(ACPSensor.avgTemp));
      if (acp > -10) {
        ACPSensor.avgTemp = acp + SamSetup.DeltaACPTemp;
        ACPSensor.PrevTemp = ACPSensor.avgTemp;
        ACPSensor.ErrCount = 0;
      } else {
        if (ACPSensor.PrevTemp > 0) ACPSensor.ErrCount++;
      } 
    }
    if (SamSetup.UseNTC) {
      NTC_Refresh(correctTs, correctTp, correctTt);
    }
  }

  void scan_ds_adress() {
    sensors.begin();  // стартуем датчики температуры

    uint8_t dc = 0;

    while (sensors.getAddress(DSAddr[dc], dc)) {
      sensors.setResolution(DSAddr[dc], 12);  // устанавливаем разрешение для датчика
      dc++;
      if (dc > 5) break;
    }

    DScnt = dc;
    if (SamSetup.disp == 2) LD.printString_6x8(("F:" + String(dc) + " DS18B20     ").c_str(), 1, 7);
    if (SamSetup.disp == 1) { LQ.setCursor(0,3);LQ.print("Found:" + String(dc) + " DS18B20     "); }
    // определяем устройства на шине
    if (SamSetup.dbg) {
    if (xSemaphoreTake(xSemaphoreSerial, (TickType_t)((200) / portTICK_RATE_MS)) == pdTRUE) {
    Serial.println("Locating DS18B20..."); 
    Serial.println("Found " + String(DScnt) + " devices.");
    Serial.print("1 Sensor Address: ");  // пишем адрес датчика 0
    printAddress(DSAddr[0]);
    Serial.print("2 Sensor Address: ");  // пишем адрес датчика 1
    printAddress(DSAddr[1]);
    Serial.print("3 Sensor Address: ");  // пишем адрес датчика 2
    printAddress(DSAddr[2]);
    Serial.print("4 Sensor Address: ");  // пишем адрес датчика 3
    printAddress(DSAddr[3]);
    Serial.print("5 Sensor Address: ");  // пишем адрес датчика 4
    printAddress(DSAddr[4]);
    Serial.print("6 Sensor Address: ");  // пишем адрес датчика 5
    printAddress(DSAddr[5]);
    xSemaphoreGive(xSemaphoreSerial);
    }}
    sensors.setWaitForConversion(false);  // работаем в асинхронном режиме
    sensors.requestTemperatures();
    if (SamSetup.dbg) {
    if (xSemaphoreTake(xSemaphoreSerial, (TickType_t)((200) / portTICK_RATE_MS)) == pdTRUE) {
    Serial.print("1 Sensor Resolution: ");  // пишем разрешение для датчика 0
    Serial.println(sensors.getResolution(DSAddr[0]));
    Serial.print("2 Sensor Resolution: ");  // пишем разрешение для датчика 1
    Serial.println(sensors.getResolution(DSAddr[1]));
    Serial.print("3 Sensor Resolution: ");  // пишем разрешение для датчика 2
    Serial.println(sensors.getResolution(DSAddr[2]));
    Serial.print("4 Sensor Resolution: ");  // пишем разрешение для датчика 3
    Serial.println(sensors.getResolution(DSAddr[3]));
    Serial.print("5 Sensor Resolution: ");  // пишем разрешение для датчика 3
    Serial.println(sensors.getResolution(DSAddr[4]));
    xSemaphoreGive(xSemaphoreSerial);
    }}
  }
  void program_init(void) {
    if (Samovar_Mode == SAMOVAR_BEER_MODE || Samovar_Mode == SAMOVAR_SUVID_MODE) {
      set_beer_program("M;45;0;1^-1^2^2;0\nP;45;1;0^-1^2^3;0\nP;60;1;0^-1^2^3;0\nW;0;0;0^-1^2^3;0\nB;0;1;0^-1^2^3;0\nC;30;0;0^-1^2^3;0\n");
    } else if (Samovar_Mode == SAMOVAR_CHEESE_MODE) {
      set_cheese_program("M;45;0;1^-1^2^2;0\nP;45;1;0^-1^2^3;0\nP;60;1;0^-1^2^3;0\nW;0;0;0^-1^2^3;0\nB;0;1;0^-1^2^3;0\nC;30;0;0^-1^2^3;0\n");
    } else if (Samovar_Mode == SAMOVAR_DISTILLATION_MODE) {
      set_dist_program("A;80.00;1;0\nS;0.50;2;0\nS;0.30;3;0\n");
    } else if (Samovar_Mode == SAMOVAR_NBK_MODE) {
      String NBK_Def_P = SamSetup.PwrType >= STAB_AVR ? "H;1;0\nS;10;2000\nO;0;0\nW;0;0\n" : "H;1;0\nS;10;167\nO;0;0\nW;0;0\n";
      set_nbk_program(NBK_Def_P);
    } else {
      set_program("H;300;0.1;0;0;1000\nB;5000;0.8;1;0;900\nB;5000;0.8;2;0;900\n");
    }
  }

  void sensor_init(void) {
    delay(200);
    if (!bme.begin(BMP085_MODE_STANDARD)) {
    DbgMsg("BMP180 not found",1);
      bmefound = false;
    } else BMEInit();

    if (SamSetup.UseDS) scan_ds_adress();
    if (SamSetup.UseNTC) NTC_Init();//опрос термисторов
  
    stepper.setMaxSpeed(STEPPER_MAX_SPEED);  // установка макс. скорости в шагах/сек
    stepper.autoPower(true); //Драйвер выключится по достижении позиции
  
    stepper2.setMaxSpeed(STEPPER_MAX_SPEED);
    stepper2.autoPower(true);

    stepper.reverse(SamSetup.StRev1);
    stepper2.reverse(SamSetup.StRev2);
    
      //Настраиваем датчик потока
      pinMode(WATERSENSOR_PIN, INPUT);

    program_init();//Инициализация программы для режима

    use_pressure_sensor = false;
    if (SamSetup.PressureSensor==1) {
      DbgMsg("Init pressure sensor XGZ...",1);
      pressure_sensor.setK(SamSetup.XGZ_K);
      if (!pressure_sensor.begin())  // initialize and check the device
      {
        use_pressure_sensor = false;
        DbgMsg("Device not responding.",1);
      } else use_pressure_sensor = true;

    }

    if (SamSetup.PressureSensor==2) use_pressure_sensor = true;


    DbgMsg("Init heaterPID...",1);
      heaterPID.SetSampleTime(1000);
      heaterPID.SetOutputLimits(0, 100);
      heaterPID.SetTunings(SamSetup.Kp, SamSetup.Ki, SamSetup.Kd);
  }

  //Обнуляем все счетчики
  void reset_sensor_counter(void) {
    sam_command_sync = SAMOVAR_NONE;
    stopService();
    stepper.setMaxSpeed(0); 
    //stepper.setSpeed(0);
    stepper.brake();
    stepper.disable();
    stepper.setCurrent(0);
    stepper.setTarget(0); 
    stopService2();
    stepper2.setMaxSpeed(0);
    stepper2.brake();
    stepper2.disable();
    stepper2.setCurrent(0);
    stepper2.setTarget(0);
    set_capacity(0);
    alarm_h_min = 0;
    alarm_t_min = 0;
    t_min = 0;
    b_t_temp_prev = 0;
    b_t_time_min = 0;
    b_t_time_delay = 0;
    alarm_c_min = 0;
    alarm_c_low_min = 0;
    prev_target_power_volt = 0;
    d_s_time_min = 0;
    d_s_temp_finish = 0;

    current_power_volt = 0.0;
    target_power_volt = 0.0;
    pressure_value = 0.0;
    old_pressure_value = 0.0;

    ProgramNum = 0;
    SteamSensor.BodyTemp = 0;
    PipeSensor.BodyTemp = 0;
    WaterSensor.BodyTemp = 0;
    TankSensor.BodyTemp = 0;
    ACPSensor.BodyTemp = 0;
    SteamSensor.PrevTemp = 0;
    PipeSensor.PrevTemp = 0;
    WaterSensor.PrevTemp = 0;
    TankSensor.PrevTemp = 0;
    ACPSensor.PrevTemp = 0;


    ActualVolumePerHour = 0;
    ActualVolumePerHour2 = 0;
    SamovarStatusInt = 0;
    startval = 0;
    PauseOn = false;
    program_Wait = false;
    SteamSensor.Start_Pressure = 0;
    WthdrwlProgress = 0;
    TargetStepps = 0;

    begintime = 0;

    d_s_temp_prev = 0;
    is_self_test = false;

    if (fileToAppend) {
      fileToAppend.close();
    }

    //if (bme_pressure < 100) BME_getvalue(false);
    start_pressure = bme_pressure;

    boil_started = false;
    boil_temp = 0;
    alcohol_s = 0;
    b_t_time_delay = 0;

    if (xSemaphore != NULL) xSemaphoreGive(xSemaphore);
    if (xSemaphoreAVR != NULL) xSemaphoreGive(xSemaphoreAVR);

    current_power_mode = "N";
    set_power(false);
    sam_command_sync = SAMOVAR_NONE;
    get_Samovar_Status();
    bk_pwm = SamSetup.PWM_L_V * 40;
    power_err_cnt = 0;
    if (SamSetup.LUA) {
      Lua_status = "";
    }
  }

  inline String format_float(float v, int d) {
    char outstr[15];
    dtostrf(v, 1, d, outstr);
    return outstr;
  }

  void printAddress(DeviceAddress deviceAddress) { // функция печати адреса DS18B20
    Serial.println(getDSAddress(deviceAddress));
  }

  String getDSAddress(DeviceAddress deviceAddress) {
    String dsaddr;
    for (uint8_t j = 0; j < 8; j++) {
      if (deviceAddress[j] < 16) dsaddr += "0";
      dsaddr += String(deviceAddress[j], HEX);
    }
    return dsaddr;
  }

  String get_DSAddressList(String Address) {
    String s = "<option value='-1'>NONE</option>";
    String dsaddr = "";
    for (uint8_t i = 0; i != DScnt; i++) {
      dsaddr = getDSAddress(DSAddr[i]);
      s += "<option value='" + String(i) + "'";
      if (Address == dsaddr) s = s + " " + "selected";
      s = s + ">" + dsaddr + "</option>";
    }
    return s;
  }

  void CopyDSAddress(const uint8_t* DevSAddress, uint8_t* DevTAddress) {
    for (uint8_t dsj = 0; dsj < 8; dsj++) {
      DevTAddress[dsj] = DevSAddress[dsj];
    }
  }
