Перейти к содержимому

Автоматическая теплица. Часть 3. Управление форточками

Добрый день, уважаемые читатели!

Для начала небольшое лирическое отступление…

Очень часто сталкиваешься с мнением: “Да мне вся эта автоматика не нужна – я и сам(а) открою форточки и полью! Мне не сложно вылить 2 / 5 / 10 / 20 леек воды на свои растения. Да и физическим трудом нужно заниматься!”
Наверное, можно подумать, что вся эта куча электроники и исполнительных устройств (на весьма немаленькую сумму, кстати) нужна только для того, чтобы перестать таскать лейки и открывать форточки?
Вовсе нет! Самое главное преимущество автоматики – “отвязать” себя от грядок. Конечно, очень полезно заниматься физическим трудом на свежем воздухе. Но!!! Только когда у вас есть на это достаточно времени! Например – вы пенсионер (но тогда – “силы уже не те”).
А если у вас дача и всю неделю вы в городе на работе???? Я хоть и живу “на даче” круглый год, но на работу ходить приходится, до пенсии еще далековато.
А если хочется в отпуск??? Почему раньше крестьяне редко ездили куда-то в отпуск? Да потому что от хозяйства и огорода не уедешь. Согласен, автоматизировать все не получится, но я постараюсь. И смело можно ехать в отпуск, не переживая, что все высохло и завяло.

Все части цикла “Теплица с зачатками разума на ESP32”

 

Собранный блок управления теплицей, про который я рассказывал в предыдущих постах, уже установлен на постоянное место жительства и успешно работает. Летучий корабль теплица машет крыльями-форточками и пытается взлететь, но пока безуспешно (успешно отработал весь летний сезон – прим. автора).

Блок управления в теплице

Богатый внутренний мир

 

Как я уже упоминал, автоматика теплицы выполняет три функции:

  • управление форточками (проветривание)
  • контроль влажности почвы и управление поливом
  • контроль уровня воды в накопительном баке для полива и поддержание необходимого запаса воды.

По ходу пьесы первоначальные алгоритмы, изначально кажущиеся довольно простыми, пришлось совершенствовать и усложнять, благо обилие подключенных датчиков и исходных данных это позволяет.

Вот этим мне и нравится процесс самостоятельного изготовления прошивки – вы можете создать такую логику, которая максимально удовлетворяет вашим потребностям. С готовыми устройствами почти всегда это не возможно.

 

Чтобы излишне не “раздувать” размер текста, в этой статье я поделюсь с вами полученным на текущий момент алгоритмом управления форточками – вдруг вы захотите создать примерно такую же конструкцию. Совсем не обязательно реализовать его на ESP-IDF, вполне возможно сделать это и на Arduino IDE. В следующих статьях рассмотрим остальные части алгоритма, а конце, возможно, я расскажу как собрать и прошить точно такое – же устройство. К тому времени, надеюсь, все заметные баги в прошивке будут уже выловлены.

 


Управление форточками по температуре

Логика управление форточками довольно проста.

Длительность полной работы моих актуаторов составляет примерно 1 минуту. Этот интервал я разбил на 8 частей (шагов) примерно по 5 секунд с коэффициентом увеличения каждого последующего шага на 1.1. То есть мы имеем 9 положений форточек от “полностью закрыто” (0) до “полностью открыто” (8). Примеры разных положений на фото ниже:

Сразу же я разделил пороговые значение на два разных: порог открытия и порог закрытия (на данный момент это 24°С и 22 °С, все значения настраиваемые). То есть ввел небольшой гистерезис. Сделано это для того, чтобы при граничной температуре не происходило бесполезное махание форточками. Для этой же цели служит настраиваемый “период стабилизации” (5 минут) – если с момента последнего открытия или закрытия форточек прошло меньше времени, система не реагирует. Но при слишком большой температуре (выше 27°С) и слишком низкой (ниже 20°С) эта задержка может только мешать – поэтому при этих пороговых температурах привод открывает или закрывает форточки полностью.

Дабы не реагировать на кратковременные изменения температуры – добавил медианный фильтр по температуре.

Все это позволяет достаточно точно поддерживать заданную температуру (для томатов идеальная температура составляет от 22 до 26°С).

Температура растет – форточки открываются. Теплый воздух уходит или солнце заходит за тучку – закрываются…

Выяснилось, то форточки такого размера под крышей вполне успешно справляются с отводом лишнего тепла, даже при полностью закрытых дверях и окнах снизу. Но только примерно до +25°С снаружи – тогда уж сколько не открывай дверей и окон – ситуация практически не меняется.

Внутри теплицы я применил “специальный тепличный” датчик, про который уже писал на этом сайте. Почти сразу обнаружилось, что в солнечные дни сенсор перегревается, а спрятать его в “дворце хрустальном” просто негде. Что, впрочем, и следовало ожидать – я был в этом уверен до запуска проекта в эксплуатацию. Пришлось сделать отражающий чехол из отрезка трубы – утеплителя для канализационных труб и металлизированного скотча. Экран открыт сверху и снизу, что позволяет воздуху свободно циркулировать. Ситуация заметно улучшилась.

 


Закрытие форточек при сильном ветре

Для того, чтобы сильный ветер не отломал форточки и не унес теплицу в страну Оз, дополнительно я добавил три ограничения по скорости ветра (все значения настраиваются):

  • при скорости ветра 5 м/с устанавливается ограничение на 50%
  • при скорости ветра 6 м/с устанавливается ограничение на 12,5%
  • при скорости ветра выше 7 м/с форточки закрываются полностью

В середине дня форточки закрывались как раз из-за сильных порывов ветра

Данные о скорости ветра получаются с простейшего китайского чашечного анемометра, который установлен на крыше и подключен к моей персональной метеостанции.

Китайский измеритель скорости ветра. Пока на доме, но переедет на теплицу. Извините за качество фото

Закрытие форточек перед дождем

Уже в самом начале “опытной” эксплуатации выяснилось, что неплохо бы экстренно закрывать форточки перед дождем. Просмотрев графики изменений с метеостанции понял, что перед дождем резко падает освещенность и температура снаружи теплицы, хотя внутри теплицы остается еще достаточно тепло. Да и время стабилизации не способствует быстрому закрытию форточек.

Поэтому я добавил дополнительные настраиваемые пороговые значения освещенности и наружной температуры, при которых форточки полностью закрываются, дабы дольше сохранить тепло внутри теплицы. И аналогичные такие же параметры, когда форточки принудительно открываются “на полную” после окончания дождя.

 

Ручное управление форточками

Дабы иметь возможность экстренно закрыть или открыть форточки, предусмотрены две кнопки на передней панели. При нажатии на них форточки полностью открываются или закрываются, но по прошествии периода стабилизации система опять возвращается к автоматическому алгоритму. Однако ручное управление помогает иногда быстрее закрыть форточки в случае приближающейся непогоды.

 


Алгоритмы

Для управления форточками (и шаровым краном) я написал специальную библиотеку – класс, который вы можете найти на моем GitHub. Библиотека reShutter создана для управления простыми электроприводами с возможностью реверса и промежуточными состояниями – кранами, форточками, заслонками и т.д.

Библиотека рассчитана на приводы с обычными коллекторными или беcколлекторными электродвигателями без пошагового управления, поэтому все управление осуществляется на основе временных интервалов (программных таймеров). То есть для открытия или закрытия привода в определенное положение на него просто подается соответствующее напряжение на определенное время, рассчитанное библиотекой. Для осуществления реверса используются два разных GPIO, поэтому вам необходимо использовать мостовую схему для непосредственного управления двигателем.

При работе библиотеки предполагается, что она никак не контролирует текущее физическое положение привода, а отключение электродвигателя в конечных положениях осуществляется с помощью встроенных в привод конечных выключателей. Поэтому при инициализации экземпляра класса привод всегда переводится в положение “полностью закрыто“, а затем отсчитывается положение исходя из этого начального значения. Поэтому перед использованием библиотеки важно максимально точно определить время полного закрытия или открытия привода. Однако вы можете добавить “внешний” контроль положения с помощью каких-либо датчиков самостоятельно.

Библиотека реализована в виде нескольких классов, которые поддерживают работу как с встроенными GPIO микроконтроллера, так и с расширителями GPIO типа PCF8574 и аналогичных.

  • class rGpioShutter предназначен для работы с встроенными GPIO
  • class rIoExpShutter предназначен для работы через расширители GPIO

Вы можете объявить несколько отдельных экземпляров для управления различными приводами в одном и том же проекте. Дополнительную справочную информацию об использовании данной библиотеки вы можете почерпнуть из файла reShutter.h

С использованием этого класса управление форточками из прикладной задачи выглядит довольно просто. Вся логика вычисления положений форточек и непосредственного управления приводами скрыта внутри класса:

bool shuttersCheckTimeStab()
{
  return (time(nullptr) - lcShutters.getLastChange()) >= shuttersTimeStab;
}

void shuttersWaitBusy()
{
  while (lcShutters.isBusy()) {
    vTaskDelay(pdMS_TO_TICKS(1000));
  };
}

void shuttersOpen(int8_t steps)
{
  if (!lcShutters.isFullOpen() && !lcShutters.isBusy() && shuttersCheckTimeStab()) {
    lcShutters.Change(steps, true);
  };
}

void shuttersClose()
{
  if (!lcShutters.isFullClose() && !lcShutters.isBusy() && shuttersCheckTimeStab()) {
    lcShutters.Change(-1, true);
  };
}

void shuttersOpenFull(bool checkTimeStab)
{
  if (!lcShutters.isFullOpen() && !lcShutters.isBusy() && (!checkTimeStab || shuttersCheckTimeStab())) {
    lcShutters.OpenFull(true);
  };
}

void shuttersCloseFull(bool checkTimeStab)
{
  if (!lcShutters.isFullClose() && (!checkTimeStab || shuttersCheckTimeStab())) {
    lcShutters.CloseFull(true, true);
  };
}

Для начала определим необходимые переменные-параметры. В переменных заданы значения по умолчанию, но в runtime их можно изменять и реальные значения отличаются от указанных.

// Интервал суток работы форточек
static uint32_t shuttersTimespan = 5002100U;

typedef enum {
  SHUTTERS_OFF      = 0,  // Отключено
  SHUTTERS_FORCED   = 1,  // Включено принудительно
  SHUTTERS_INTERNAL = 2,  // Управление по датчику воздуха
  SHUTTERS_OUTDOOR  = 3,  // Управление по датчику улицы
  SHUTTERS_AUX      = 4,  // Управление по дополнительному датчику 
  SHUTTERS_MIXED    = 5   // Управление по датчику воздуха и дополнительному датчику одновременно
} shutters_mode_t;
static shutters_mode_t shuttersMode = SHUTTERS_INTERNAL;

// Уведомления в telegram
static notify_type_t shuttersNotify = NOTIFY_OFF;
static notify_type_t shuttersNotifyManual = NOTIFY_SOUND;

// Пороговая скорость ветра, при которой принудительно закрываются форточки
static float shuttersWindSpeed1             = 4.0;
static uint8_t shuttersWindMax1             = 4;
static float shuttersWindSpeed2             = 5.0;
static uint8_t shuttersWindMax2             = 2;
static float shuttersWindSpeed3             = 6.0;
static uint8_t shuttersWindMax3             = 0;
// Уровень освещенности, при котором принудительно закрываются или открываются форточки
static uint8_t shuttersLightInternal        = 0;
static float shuttersLightForcedOpen        = 30000.0;
static float shuttersLightForcedClose       = 3000.0;
// Температура, при которой открываются форточки на 1 шаг
static float shuttersTempIndoorOpen         = 25.0;
static float shuttersTempIndoorClose        = 22.0;
static float shuttersTempIndoorOpenForced   = 28.0;
static float shuttersTempIndoorCloseForced  = 20.0;
// Температура на улице, при которой закрываются или открываются форточки в режиме SHUTTERS_OUTDOOR
static float shuttersTempOutdoorOpen        = 25.0;
static float shuttersTempOutdoorClose       = 20.0;
// Температура на улице, при которой принудительно закрываются или открываются форточки в режиме SHUTTERS_OUTDOOR
static float shuttersTempOutForcedOpen      = 25.0;
static float shuttersTempOutForcedClose     = 12.0;
// Коэффициенты для режима SHUTTERS_MIXED (в сумме они должны давать 1)
static float shuttersContrFactorInternal    = 0.45;
static float shuttersContrFactorAux         = 0.55;
// Интервал стабилизации температуры в секундах
static uint16_t shuttersTimeStab            = 5*60;

Алгоритм управления форточками по температуре внутри теплицы:

void shuttersTempControl(float temp, float temp_open, float temp_close, float forced_open, float forced_close)
{
  // Учитываем расписание (если задано) и силу ветра
  if (checkTimespanNowEx(shuttersTimespan, true) && shuttersWindControl() && !isnan(temp)) {
    // Принудительное закрытие или открытие по наружной температуре и освещенности
    if (shuttersOutdoorLightForcedControl()) {
      // Если температура превысила пороговую форсированного открытия, открываем полностью
      if ((temp >= forced_open)) {
        shuttersOpenFull(true);
      // Если температура превысила пороговую открытия, открываем на 1 шаг
      } else if ((temp >= temp_open)) {
        shuttersOpen(1);
      // Если температура снизилась до пороговой закрытия, закрываем на 1 шаг
      } else if ((temp <= temp_close)) {
        shuttersClose();
      // Если температура снизилась до пороговой форсированного закрытия, закрываем полностью
      } else if ((temp <= forced_close)) {
        shuttersCloseFull(false);
      };
    };
  } else {
    shuttersCloseFull(false);
  };
}

Добавляем функцию ограничений по скорости ветра:

// Принудительное закрытие по датчику скорости ветра
bool shuttersWindControl()
{
  // Получаем скорость ветра
  float windSpeed = sensorsGetOutdoorWindSpeed();
  if (!isnan(windSpeed)) {
    // Скорость ветра превышает порог 3
    if (windSpeed >= shuttersWindSpeed3) {
      lcShutters.setMaxLimit(shuttersWindMax3, true);
      return shuttersWindMax3 > 0;
    }
    // Скорость ветра превышает порог 2, но ниже порога 3
    else if (windSpeed >= shuttersWindSpeed2) {
      lcShutters.setMaxLimit(shuttersWindMax2, true);
      return shuttersWindMax2 > 0;
    }
    // Скорость ветра превышает порог 1, но ниже порога 2
    else if (windSpeed >= shuttersWindSpeed1) {
      lcShutters.setMaxLimit(shuttersWindMax1, true);
      return shuttersWindMax1 > 0;
    }
    // Скорость ветра ниже порога полного открытия
    else {
      lcShutters.clearMaxLimit(true);
      return true;
    };
  };
  return true;
}

Добавляем принудительное закрытие по  освещенности:

// Принудительное закрытие или открытие по уровню освещенности
bool shuttersOutdoorLightForcedControl()
{
  float outLight = sensorsGetOutdoorLight();
  if (!isnan(outLight)) {
    if (outLight >= shuttersLightForcedOpen) {
      shuttersOpenFull(true);
      return false;
    } else if (outLight <= shuttersLightForcedClose) {
      shuttersCloseFull(true);
      return false;
    };
  };
  // Разрешаем регулировку по температуре внутри
  return true;
}

Для ручного управления с панели управления предусмотрен следующий код:

// Ручное управление с кнопочной панели
void shuttersManualModeOpen()
{
  if (lcShutters.isBusy()) {
    #if CONFIG_TELEGRAM_ENABLE
      if (shuttersNotifyManual != NOTIFY_OFF) {
        tgSend(CONFIG_SHUTTERS_NOTIFY_KIND, CONFIG_SHUTTERS_NOTIFY_PRIORITY, shuttersNotifyManual == NOTIFY_SOUND, CONFIG_TELEGRAM_DEVICE, 
          "❌ <b>Не удалось активировать ручной режим управления форточками</b>, привод занят");
      };
    #endif // CONFIG_TELEGRAM_ENABLE
  } else {
    xEventGroupSetBits(_sensorsFlags, MANUAL_SHUTTERS);
    #if CONFIG_TELEGRAM_ENABLE
      if (shuttersNotifyManual != NOTIFY_OFF) {
        tgSend(CONFIG_SHUTTERS_NOTIFY_KIND, CONFIG_SHUTTERS_NOTIFY_PRIORITY, shuttersNotifyManual == NOTIFY_SOUND, CONFIG_TELEGRAM_DEVICE, 
          "✋ <b>Активирован ручной режим</b> управления форточками, форточки <b>открыты</b>");
      };
    #endif // CONFIG_TELEGRAM_ENABLE
    if (!lcShutters.isFullOpen())  lcShutters.OpenFull(true);
  };
}

void shuttersManualModeClose()
{
  xEventGroupSetBits(_sensorsFlags, MANUAL_SHUTTERS);
  #if CONFIG_TELEGRAM_ENABLE
    if (shuttersNotifyManual != NOTIFY_OFF) {
      tgSend(CONFIG_SHUTTERS_NOTIFY_KIND, CONFIG_SHUTTERS_NOTIFY_PRIORITY, shuttersNotifyManual == NOTIFY_SOUND, CONFIG_TELEGRAM_DEVICE, 
        "✋ <b>Активирован ручной режим</b> управления форточками, форточки <b>закрыты</b>");
    };
  #endif // CONFIG_TELEGRAM_ENABLE
  if (!lcShutters.isFullClose()) {
    lcShutters.Break();
    lcShutters.CloseFull(true, true);
  };
}

void shuttersManualModeDisable()
{
  if ((xEventGroupGetBits(_sensorsFlags) & MANUAL_SHUTTERS) > 0) {
    xEventGroupClearBits(_sensorsFlags, MANUAL_SHUTTERS);
    #if CONFIG_TELEGRAM_ENABLE
      if (shuttersNotifyManual != NOTIFY_OFF) {
        tgSend(CONFIG_SHUTTERS_NOTIFY_KIND, CONFIG_SHUTTERS_NOTIFY_PRIORITY, shuttersNotifyManual == NOTIFY_SOUND, CONFIG_TELEGRAM_DEVICE, 
          "✳️ Ручной режим управления форточками <b>отключен</b>");
      };
    #endif // CONFIG_TELEGRAM_ENABLE
  };
}

void shuttersManualModeReset()
{
  time_t now = time(nullptr);
  struct tm ti; 
  localtime_r(&now, &ti);
  int16_t  t0 = ti.tm_hour * 100 + ti.tm_min;
  uint16_t t1 = shuttersTimespan / 10000;
  uint16_t t2 = shuttersTimespan % 10000;
  if (t1 > 2359) t1 = 0;
  if (t2 > 2359) t2 = 0;
  if ((t0 == t1) || (t0 == t2)) {
    shuttersManualModeDisable();
  };
}

 

Итоговый код, который вызывается из цикла задачи:

void shuttersControl(EventBits_t bits) 
{
  // Ручное управление с кнопочной панели
  if (bits & BTN_SHUTTERS_OPEN) {
    shuttersManualModeOpen();
  } else if (bits & BTN_SHUTTERS_CLOSE) {
    shuttersManualModeClose();
  } else if (bits & BTN_SHUTTERS_AUTO) {
    shuttersManualModeDisable();
  } 
  // Автоматическое управление
  else {
    if ((xEventGroupGetBits(_sensorsFlags) & MANUAL_SHUTTERS) > 0) {
      // Ручное управление - учитывается только скорость ветра
      shuttersWindControl();
    } else {
      switch (shuttersMode) {
        // Принудительное открытие и закрытие по расписанию
        case SHUTTERS_FORCED:
          shuttersForcedControl();
          break;

        // По внутреннему датчику, если он неисправен по дополнительному, если и он неисправен - по внешней температуре
        case SHUTTERS_INTERNAL:
          if (sensorInternal1.getStatus() == SENSOR_STATUS_OK) {
            shuttersTempControl(sensorsGetInternalTemp(), 
              shuttersTempIndoorOpen, shuttersTempIndoorClose, 
              shuttersTempIndoorOpenForced, shuttersTempIndoorCloseForced);
          } else if (sensorAux.getStatus() == SENSOR_STATUS_OK) {
            shuttersTempControl(sensorsGetAuxTemp(), 
              shuttersTempIndoorOpen, shuttersTempIndoorClose, 
              shuttersTempIndoorOpenForced, shuttersTempIndoorCloseForced);
          } else {
            shuttersTempControl(sensorsGetOutdoorTemp(), 
              shuttersTempOutdoorOpen, shuttersTempOutdoorClose, 
              shuttersTempOutForcedOpen, shuttersTempOutForcedClose);
          } 
          break;

        // Только по уличному датчику
        case SHUTTERS_OUTDOOR:
          shuttersTempControl(sensorsGetOutdoorTemp(), 
            shuttersTempOutdoorOpen, shuttersTempOutdoorClose,
            shuttersTempOutForcedOpen, shuttersTempOutForcedClose);
          break;

        // По дополнительному датчику, если он неисправен - по внешней температуре
        case SHUTTERS_AUX:
          if (sensorAux.getStatus() == SENSOR_STATUS_OK) {
            shuttersTempControl(sensorsGetAuxTemp(), 
              shuttersTempIndoorOpen, shuttersTempIndoorClose, 
              shuttersTempIndoorOpenForced, shuttersTempIndoorCloseForced);
          } else {
            shuttersTempControl(sensorsGetOutdoorTemp(), 
              shuttersTempOutdoorOpen, shuttersTempOutdoorClose, 
              shuttersTempOutForcedOpen, shuttersTempOutForcedClose);
          };
          break;

        // По двум датчикам (с коэффициентами), если оба неисправны - по внешней температуре
        case SHUTTERS_MIXED:
          if ((sensorInternal1.getStatus() == SENSOR_STATUS_OK) || (sensorAux.getStatus() == SENSOR_STATUS_OK)) {
            shuttersTempControl(sensorsGetMixedTemp(), 
              shuttersTempIndoorOpen, shuttersTempIndoorClose, 
              shuttersTempIndoorOpenForced, shuttersTempIndoorCloseForced);
          } else {
            shuttersTempControl(sensorsGetOutdoorTemp(), 
              shuttersTempOutdoorOpen, shuttersTempOutdoorClose, 
              shuttersTempOutForcedOpen, shuttersTempOutForcedClose);
          };
          break;

        // Все всегда захлобучено
        default:
          shuttersCloseFull(false);
          break;
      };
    };
  };
}

 


Управление и топики

Всё это хозяйство генерирует целую кучу топиков на MQTT-брокере, большинство из которых публикуются в виде JSON-пакетов (кроме секции настроек config/confirm), содержащих целую кучу информации.

Топиков настолько много, что приходится публиковать их пакетами, распределив по нескольким циклам задачи.

На смартфоне это выглядит так:

Настройки и данные с датчиков на 18:00

После корректной настройки все работает в автоматическом режиме и постоянного контроля или ручного управления не требует.

 


Первая радуга в этом году

В следующих статьях я расскажу о том, как реализовано управление поливом и наполнением емкости.

Все части цикла “Теплица с зачатками разума на ESP32”

 


На этом пока всё, до встречи на сайте и на dzen-канале!

💠 Полный архив статей вы найдете здесь


Пожалуйста, оцените статью:
[ 5 из 5, всего 2 оценок ]

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *