Своими рукамипрактическая инструкция источник: Фермозавр

Автополив на ESP32: учим помпу поливать одну грядку

Редакция Фермозавра·9 апреля 2026 г.·8 мин чтения

Сначала узнаем, как датчик видит вашу почву, затем отмерим воду и соберём первый цикл полива. Полный скетч, кнопка подтверждения и сообщения в Telegram.

Автополив на ESP32: учим помпу поливать одну грядку

Земля сверху подсохла, а у корней ещё влажно — или наоборот? Удобный первый проект автополива начинается с этого вопроса. Подключим датчик почвы к ESP32, посмотрим на его отсчёты и научим низковольтную помпу поливать одну зону с паузами на распределение воды.

Собираем лабораторный стенд. До переноса на грядку подберём силовую часть, измерим расход и проверим отключение помпы. Команду «выключено» важно сопоставить с реальной остановкой воды; для защиты от отказа программы или реле предусмотрите независимое отключение питания.

Детали для первой зоны

  • Плата на классическом ESP32, например ESP32-DevKitC V4.
  • Ёмкостный аналоговый датчик почвы с выходом, совместимым с входом ESP32. В качестве документированного примера — DFRobot SEN0193. Характеристики безымянного модуля «v1.2» нельзя автоматически считать такими же.
  • Помпа постоянного тока на 12 В, подходящая для выбранных капельниц, высоты подъёма и требуемого расхода.
  • Источник питания помпы, предохранитель, проводка и коммутационный модуль, рассчитанные на её рабочий и пусковой ток. Для реле нужна допустимая нагрузка именно при постоянном токе и работе с двигателем; цифра для резистивной нагрузки на корпусе этого не подтверждает.
  • Модуль управления с входом, совместимым с логикой 3,3 В. Полярность управления и состояние при обесточенном ESP32 проверяют отдельно.
  • Датчик минимального уровня с сухим контактом, цепь разрешения и кнопка ручного подтверждения. Для защиты от отказа программы — независимое отключение питания помпы.
  • Отдельное подходящее питание платы, корпус, клеммы и кабельные вводы. Макетная плата применяется только для сигнальной части сухого стенда.

Эта инструкция ограничена низковольтной помпой. Её ток не пропускают через плату ESP32, макетную плату или сигнальные перемычки. Подавление выбросов при отключении двигателя подбирают по документации помпы и ключа: для однонаправленной индуктивной нагрузки постоянного тока обычно применяют защитный диод, но его параметры и совместимость с электроникой конкретной помпы необходимо проверить.

Соединяем сигналы, затем проверяем выход

ESP32 3V3 → питание совместимого датчика почвы
ESP32 GND → GND датчика
Выход датчика → GPIO34 (ADC1 классического ESP32)
GPIO26 → совместимый вход коммутационного модуля
GPIO25 → последовательно соединённые контакты разрешения → GND
GPIO27 → нормально разомкнутая кнопка подтверждения → GND

Для SEN0193 производитель указывает питание 3,3–5,5 В и аналоговый выход 0–3,0 В. В этой схеме выбран источник 3,3 В. Распиновку датчика и платы сверяют перед подключением; для другой модели ESP32 назначение выводов может отличаться.

Цепь GPIO25 замкнута на GND только при достаточном уровне воды и разрешённой работе. При её размыкании, в том числе обрыве провода, программа блокирует полив. Это слаботочная сигнальная цепь, а не силовое подключение поплавка к двигателю. Замыкание провода на GND такой контроль не обнаружит. Независимое аварийное отключение должно разрывать питание помпы через устройство с подходящими характеристиками.

Пример использует активный низкий уровень: HIGH — выключено, LOW — включено. Внешняя подтяжка входа коммутационного модуля должна удерживать выключенное состояние до запуска прошивки и во время сброса. Номинал и схему выбирают для конкретного модуля; подтяжка не должна подавать 5 В на GPIO. Если модуль работает иначе, измените RELAY_OFF и RELAY_ON и проверьте все состояния без подключённой помпы.

Соединение земли ESP32 и силового модуля зависит от его схемы, особенно при гальванической развязке. Нельзя считать любую плату с оптопарой автоматически изолированной. Для ESP32-DevKitC V4 используйте один способ питания платы: USB, 5V/GND или 3V3/GND; производитель запрещает их одновременное подключение.

Знакомим датчик с вашей почвой

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

  1. Установите датчик в почву или субстрат так, как он будет работать в зоне полива. Исключите пустоты и прямое попадание струи на место измерения.
  2. Сопоставьте отсчёты с состоянием почвы, при котором по вашему регламенту нужен полив, и с состоянием после достаточного увлажнения и распределения воды.
  3. Установите пороги DRY и WET. В данном примере сухой почве соответствует больший отсчёт, поэтому DRY должен быть больше WET. При противоположном направлении сигнала нужно изменить условия сравнения.
  4. Отдельно измерьте шум и нормальный диапазон. VALID_MIN и VALID_MAX должны выявлять явно недостоверные значения, не отсекая рабочие показания.
  5. Проверьте фактическую подачу воды за ограниченное время, паузу на впитывание и поведение после нескольких циклов. Порог у одного датчика не описывает влажность всей грядки.

Стартовые числа в коде: DRY=2100, WET=1700, допустимые отсчёты 100–4000. Замените их результатами своей калибровки. Программа усредняет десять измерений; выход любого отсчёта за допустимые границы вызывает блокировку. Проверку показаний дополните осмотром почвы — постоянный правдоподобный сигнал может скрывать неисправность.

Один цикл: проверить, полить, дать воде распределиться

После включения контроллер ждёт вас. Проверьте воду и показания, нажмите подтверждение — начнётся пауза 30 минут. Такая же пауза следует за обычной остановкой. Это удобный момент понаблюдать, как влага распределяется возле датчика, прежде чем начнётся следующий цикл.

Достигнут WET — помпа останавливается. Если прошли 30 секунд, исчезло разрешение или испортились показания, выход выключается и ошибка остаётся зафиксированной. Сначала разберитесь с причиной, затем подтвердите продолжение кнопкой. Временной лимит подбирайте по измеренному расходу: одинаковые секунды у разных помп дают разный объём.

Показания читаются раз в секунду. Пока помпа работает, HTTP-запросы и ручное переподключение Wi-Fi отложены: сообщение в Telegram подождёт окончания цикла. Независимый ограничитель времени и отключение питания обеспечат отдельный рубеж защиты там, где опасны перелив и сухой ход.

Подключаем Telegram и загружаем скетч

Для сборки использованы Arduino-ESP32 3.3.12, плата ESP32 Dev Module и ArduinoJson 7.4.3. Создайте собственного бота через BotFather, отправьте ему /start и получите chat.id из getUpdates. Заполните SSID, PASSWORD, BOT_TOKEN и CHAT_ID. Токен храните как пароль; при утечке отзовите его через BotFather.

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

#include <Arduino.h>
#include <WiFi.h>
#include <NetworkClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <time.h>

constexpr char SSID[]="CHANGE_ME", PASSWORD[]="CHANGE_ME";
constexpr char BOT_TOKEN[]="CHANGE_ME", CHAT_ID[]="CHANGE_ME";
constexpr int SENSOR_PIN=34, RELAY_PIN=26, PERMIT_PIN=25, RESET_PIN=27;
constexpr int RELAY_OFF=HIGH, RELAY_ON=LOW; // Проверить на отключённой помпе.
constexpr int DRY=2100, WET=1700, VALID_MIN=100, VALID_MAX=4000;
constexpr uint32_t MAX_RUN_MS=30000, PAUSE_MS=1800000, READ_MS=1000;
constexpr uint32_t REPORT_MS=3600000, SEND_MS=60000;
struct Controller {
  bool on=false, fault=true, valid=false;
  int raw=0;
  uint32_t started=0, stopped=0;
  const char* reason="Проверка после запуска";
  void stop(uint32_t now) { if(on) {on=false;stopped=now;} }
  void fail(uint32_t now,const char* why) {stop(now);fault=true;reason=why;}
  void tick(uint32_t now,bool permit,bool reset) {
    if(!permit) fail(now,"Нет разрешения: бак/аварийная цепь");
    else if(!valid) fail(now,"Недостоверный сигнал датчика");
    else if(on && uint32_t(now-started)>=MAX_RUN_MS) fail(now,"Лимит времени: проверить полив");
    else if(fault) {
      if(reset) {fault=false;stopped=now;reason="Разрешено, выдерживается пауза";}
    } else if(on && raw<=WET) {stop(now);reason="Достигнут порог остановки";}
    else if(!on && raw>=DRY && uint32_t(now-stopped)>=PAUSE_MS) {
      on=true;started=now;reason="Начат цикл полива";
    }
  }
} control;
constexpr size_t QUEUE_SIZE=16;
String events[QUEUE_SIZE];size_t head=0,count=0;
uint32_t dropped=0,readAt=0,reportAt=0,sentAt=0,reconnectAt=0;
bool readOnce=false,previousReset=false,sendAttempted=false;
uint32_t sendDelayMs=SEND_MS;
void logEvent(const String& message) {
  Serial.println(message);
  if(count==QUEUE_SIZE){head=(head+1)%QUEUE_SIZE;--count;++dropped;}
  events[(head+count)%QUEUE_SIZE]=message;++count;
}
String snapshot(uint32_t now) {
  return "Через "+String(now/1000)+" с после запуска: ADC="+String(control.raw)
    +", команда помпе="+(control.on?"ВКЛ":"ВЫКЛ")+", блокировка="+(control.fault?"ДА":"НЕТ")
    +". "+control.reason;
}
void readSoil() {
  uint32_t sum=0;bool ok=true;
  for(int i=0;i<10;i++) {
    int v=analogRead(SENSOR_PIN);sum+=v;
    if(v<VALID_MIN || v>VALID_MAX) ok=false;
    delay(2);
  }
  control.raw=sum/10;control.valid=ok;
}
bool sendTelegram(const String& text) {
  if(control.on || WiFi.status()!=WL_CONNECTED || time(nullptr)<1735689600) return false;
  if(String(BOT_TOKEN)=="CHANGE_ME" || String(CHAT_ID)=="CHANGE_ME") return false;
  NetworkClientSecure client;client.useBuiltinCACertBundle();client.setHandshakeTimeout(5);
  HTTPClient http;http.setConnectTimeout(3000);http.setTimeout(5000);
  if(!http.begin(client,String("https://api.telegram.org/bot")+BOT_TOKEN+"/sendMessage"))return false;
  JsonDocument request;request["chat_id"]=CHAT_ID;request["text"]=text;
  String payload;serializeJson(request,payload);http.addHeader("Content-Type","application/json");
  int code=http.POST(payload);String response=http.getString();http.end();
  JsonDocument result;bool parsed=!deserializeJson(result,response);
  sendDelayMs=SEND_MS;
  if(code==429 && parsed){uint32_t s=result["parameters"]["retry_after"]|60U;if(s<60)s=60;if(s>86400)s=86400;sendDelayMs=s*1000U;}
  bool ok=code==200 && parsed && result["ok"].is<bool>() && result["ok"].as<bool>();
  Serial.printf("Telegram HTTP=%d accepted=%d\n",code,ok);return ok;
}
void setup() {
  // Внешняя подтяжка входа реле должна удерживать OFF до запуска программы.
  digitalWrite(RELAY_PIN,RELAY_OFF);pinMode(RELAY_PIN,OUTPUT);
  pinMode(PERMIT_PIN,INPUT_PULLUP);pinMode(RESET_PIN,INPUT_PULLUP);
  Serial.begin(115200);analogReadResolution(12);analogSetPinAttenuation(SENSOR_PIN,ADC_11db);
  control.stopped=millis();
  WiFi.mode(WIFI_STA);WiFi.setAutoReconnect(true);WiFi.begin(SSID,PASSWORD);
  configTime(0,0,"pool.ntp.org","time.cloudflare.com");
  logEvent("Запуск: помпа выключена. Проверьте стенд и нажмите подтверждение.");
}
void loop() {
  uint32_t now=millis();
  if(!readOnce || uint32_t(now-readAt)>=READ_MS){readAt=now;readSoil();readOnce=true;}
  now=millis();
  bool beforeOn=control.on,beforeFault=control.fault;
  const char* beforeReason=control.reason;
  bool reset=digitalRead(RESET_PIN)==LOW;
  control.tick(now,digitalRead(PERMIT_PIN)==LOW,reset && !previousReset);
  previousReset=reset;
  digitalWrite(RELAY_PIN,control.on?RELAY_ON:RELAY_OFF);
  if(beforeOn!=control.on || beforeFault!=control.fault || beforeReason!=control.reason)logEvent(snapshot(now));
  // Пока помпа включена, никаких HTTP-запросов и ручного переподключения Wi-Fi.
  if(control.on) return;
  if(WiFi.status()!=WL_CONNECTED && uint32_t(now-reconnectAt)>=30000){reconnectAt=now;WiFi.reconnect();}
  if(uint32_t(now-reportAt)>=REPORT_MS){reportAt=now;logEvent("Отчёт. "+snapshot(now));}
  if((sendAttempted && uint32_t(now-sentAt)<sendDelayMs) || WiFi.status()!=WL_CONNECTED)return;
  if(!count && !dropped)return;
  sendAttempted=true;sentAt=now;
  if(dropped){if(sendTelegram("Очередь: пропущено событий "+String(dropped)+". "+snapshot(now)))dropped=0;}
  else if(sendTelegram(events[head])){events[head]="";head=(head+1)%QUEUE_SIZE;--count;}
}

События уходят в Telegram после остановки помпы. В памяти ждут последние 16 событий; при переполнении программа считает пропуски. HTTP 200 и ok=true подтверждают приём API. После неопределённого ответа возможно повторное сообщение; для истории после перезапуска понадобится постоянное хранилище.

Между попытками отправки проходит хотя бы минута, при ответе 429 учитывается retry_after. Раз в час при выключенной помпе создаётся отчёт. В нём — время после запуска и команда выходу. Во время ожидания сетевого ответа кнопка и следующий запуск могут задержаться, поэтому проверяйте подтверждение на самом контроллере.

Опыты с водой в ёмкости

  1. Без помпы проверьте выход при включении питания, сбросе и перезагрузке. Реле не должно кратковременно включаться. Проверьте также исчезновение питания ESP32 при сохранённом питании модуля.
  2. Проверьте блокировку после запуска, подтверждение кнопкой и выдержку паузы. Для ускоренного теста можно временно уменьшить PAUSE_MS, затем вернуть выбранное рабочее значение.
  3. Имитируйте сухую и влажную почву, обрыв цепи разрешения и явно недостоверные показания датчика. Убедитесь, что неисправность нельзя сбросить до восстановления условий.
  4. С помпой на контролируемом стенде измерьте подачу воды. Проверьте остановку по порогу и по времени; после временного лимита автоматического повторного запуска быть не должно.
  5. Отключите интернет во время цикла. Проверьте остановку, сохранение события до ответа API и последующую отправку после восстановления связи.
  6. Отдельно испытайте независимое отключение питания помпы. Проверьте, не продолжает ли вода идти самотёком после снятия питания.

Первый полный цикл проведите под наблюдением, с водой в ёмкости. Сопоставьте отсчёты, продолжительность и собранный объём. Затем повторите с почвой: интереснее всего увидеть, как показание меняется уже после выключения помпы. Это поможет подобрать паузу и место датчика.

Следующая грядка

Когда одна зона работает понятно, можно проектировать несколько. Каждой понадобятся свой датчик и состояние, а общей помпе — согласование с клапанами и ограничение одновременной нагрузки. Начните расширение с рисунка: какая зона получает воду и что происходит, когда предыдущая закончила полив.

Первичные источники: датчик DFRobot SEN0193, ADC Arduino-ESP32, питание ESP32-DevKitC V4, защита контактов при индуктивной нагрузке, Telegram Bot API.

Источник: Фермозавр

💬 Комментарии

Чтобы оставить комментарий, войдите или зарегистрируйтесь

Загрузка комментариев...

Похожие статьи