Хочу представить вашему вниманию контроллер управления насосами в зависимости от датчиков влажности.

Программа написана на C++ с использованием фреймворка Arduino.

Но никаких дополнительных библиотек типа Thread для реализации кода без блокировок(delay).

Динамическое добавление насосов в класс контроллера. Для добавления насоса в контроллер надо добавить строку:

pumpController.addPump(SoilSensor(A0, 800, 100000), 2, 2000);  // Насос 1

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

Важно отметить:

Программа использует объектно-ориентированное программирование с классами SoilSensor, PumpController, ProcessStats и Pump, используются классы, конструкторы и другие объектно-ориентированные возможности.

Программа предназначена для выполнения на микроконтроллерах Arduino или совместимых платформах.

  • Arduino-специфичные элементы:

    • Функции pinMode()digitalWrite()analogRead()

    • Константы HIGHLOW

    • Функции времени millis()micros()

    • Объект Serial для работы с последовательным портом

    • Стандартные функции Arduino setup() и loop()

// Инвертированные значения HIGH и LOW
const int MYHIGH = LOW;
const int MYLOW = HIGH;

// Буфер для форматированной строки
char buffer[100];

// Структура для статистики выполнения
struct ProcessStats {
  unsigned long callCount = 0;
  unsigned long totalTime = 0;
  unsigned long minTime = 0xFFFFFFFF; // Максимальное значение unsigned long
  unsigned long maxTime = 0;
  
  void update(unsigned long executionTime) {
    callCount++;
    totalTime += executionTime;
    if (executionTime < minTime) minTime = executionTime;
    if (executionTime > maxTime) maxTime = executionTime;
  }
  
  unsigned long getAverageTime() const {
    return callCount > 0 ? totalTime / callCount : 0;
  }
  
  String getStatsString() const {
    return "(" + String(callCount) + 
           ") " + String(minTime) + 
           "/" + String(maxTime) + 
           "/" + String(getAverageTime());
  }
};

// Глобальная статистика для всех насосов
ProcessStats globalProcessStats;

// Класс для датчика влажности
class SoilSensor {
private:
  int soilPin;              // Пин датчика влажности
  int dryThreshold;         // Порог сухости
  unsigned long checkInterval; // Интервал проверки
  unsigned long lastCheckTime; // Время последней проверки

public:
  // Конструктор
  SoilSensor(int soilPin, int dryThreshold, unsigned long checkInterval)
    : soilPin(soilPin), dryThreshold(dryThreshold), checkInterval(checkInterval), lastCheckTime(0) {}

  // Метод для проверки, пришло ли время для следующей проверки
  bool isCheckTime() {
    return millis() - lastCheckTime >= checkInterval;
  }

  // Метод для чтения влажности
  int readMoisture() {
    // Обновляем время последней проверки
    lastCheckTime = millis();
    return analogRead(soilPin);
  }

  // Метод для проверки необходимости полива
  bool needsWatering(int moisture) {
    return moisture >= dryThreshold;
  }

  // Метод для получения информации о датчике
  String getInfo(int moisture, int getPumpPin) {
    return String(millis() /1000) + " сек. - Датчик " + String(soilPin) + 
           "(p="+ String(getPumpPin)+"): влажность=" + String(moisture) + 
           "(" + String(dryThreshold) +
           ")" + String(moisture - dryThreshold) +
           " | " + globalProcessStats.getStatsString();
  }

  // Геттеры
  int getSoilPin() const { return soilPin; }
  int getDryThreshold() const { return dryThreshold; }
  unsigned long getCheckInterval() const { return checkInterval; }
  unsigned long getLastCheckTime() const { return lastCheckTime; }
};

// Класс для управления насосом
class Pump {
private:
  SoilSensor sensor;        // Объект датчика (копия)
  int pumpPin;              // Пин реле насоса
  unsigned long pumpDuration; // Длительность работы насоса
  bool isPumping;           // Флаг активности насоса
  unsigned long pumpStartTime; // Время начала работы насоса

public:
  // Конструктор принимает временный объект SoilSensor
  Pump(SoilSensor sensor, int pumpPin, unsigned long pumpDuration)
    : sensor(sensor), pumpPin(pumpPin), pumpDuration(pumpDuration), 
      isPumping(false), pumpStartTime(0) {}

  // Метод для настройки пина насоса
  void setupPin() {
    pinMode(pumpPin, OUTPUT);
  }

  // Метод для проверки, истекло ли время работы насоса
  bool isPumpTimeExpired() {
    return millis() - pumpStartTime >= pumpDuration;
  }

  // Метод для включения насоса
  void startPumping() {
    digitalWrite(pumpPin, MYHIGH);
    isPumping = true;
    pumpStartTime = millis();
    Serial.println("Насос на пине " + String(pumpPin) + " включен s=" + String(getSensor().getSoilPin()) + "");
  }

  // Метод для выключения насоса
  void stopPumping() {
    digitalWrite(pumpPin, MYLOW);
    isPumping = false;
    Serial.println("Насос на пине " + String(pumpPin) + " выключен");
  }

  // Основной метод обработки насоса
  void process() {
    unsigned long startTime = micros(); // Засекаем время начала
    
    // Проверяем, не активен ли насос в данный момент
    if (isPumping) {
      // Если насос работает, проверяем, не истекло ли время работы
      if (isPumpTimeExpired()) {
        stopPumping();
      }
      
      unsigned long endTime = micros(); // Засекаем время окончания
      globalProcessStats.update(endTime - startTime); // Обновляем статистику
      return; // Прерываем выполнение, если насос активен
    }
    
    // Проверяем, пришло ли время для следующей проверки датчика
    if (sensor.isCheckTime()) {
      int moisture = sensor.readMoisture();
      
      // Выводим информацию в монитор порта
      Serial.println(sensor.getInfo(moisture, getPumpPin()));

      // Проверяем, слишком ли сухая почва
      if (sensor.needsWatering(moisture)) {
        startPumping();
      }
    }
    
    unsigned long endTime = micros(); // Засекаем время окончания
    globalProcessStats.update(endTime - startTime); // Обновляем статистику
  }

  // Геттеры
  bool getIsPumping() const { return isPumping; }
  unsigned long getPumpStartTime() const { return pumpStartTime; }
  unsigned long getPumpDuration() const { return pumpDuration; }
  SoilSensor getSensor() const { return sensor; }
  int getPumpPin() const { return pumpPin; }
};

// Класс контроллера насосов
class PumpController {
private:
  Pump** pumps;           // Массив указателей на насосы
  int pumpCount;          // Количество насосов
  int maxPumps;           // Максимальное количество насосов

public:
  // Конструктор
  PumpController(int maxPumps = 10) : pumpCount(0), maxPumps(maxPumps) {
    pumps = new Pump*[maxPumps];
    for (int i = 0; i < maxPumps; i++) {
      pumps[i] = nullptr;
    }
  }

  // Деструктор для очистки памяти
  ~PumpController() {
    for (int i = 0; i < pumpCount; i++) {
      delete pumps[i];
    }
    delete[] pumps;
  }

  // Метод для добавления насоса
  bool addPump(SoilSensor sensor, int pumpPin, unsigned long pumpDuration) {
    if (pumpCount >= maxPumps) {
      Serial.println("Ошибка: достигнуто максимальное количество насосов");
      return false;
    }
    
    pumps[pumpCount] = new Pump(sensor, pumpPin, pumpDuration);
    pumpCount++;
    return true;
  }

  // Метод инициализации для раздела setup
  void setup() {
    for (int i = 0; i < pumpCount; i++) {
      pumps[i]->setupPin();
      pumps[i]->stopPumping();
    }
    Serial.println("Система автоматического полива инициализирована");
    Serial.println("Количество насосов: " + String(pumpCount));
  }

  // Метод process для loop
  void process() {
    for (int i = 0; i < pumpCount; i++) {
      pumps[i]->process();
    }
  }

  // Геттеры
  int getPumpCount() const { return pumpCount; }
  int getMaxPumps() const { return maxPumps; }
};

// Создание контроллера насосов
PumpController pumpController(10); // Максимум 10 насосов

void setup() {
  // Инициализация последовательного порта
  Serial.begin(9600);
  
  // Добавление насосов в контроллер
  pumpController.addPump(SoilSensor(A0, 800, 100000), 2, 2000);  // Насос 1
  pumpController.addPump(SoilSensor(A1, 275, 16000), 3, 2000);   // Насос 2
  pumpController.addPump(SoilSensor(A2, 600, 700000), 4, 2000);  // Насос 3
  pumpController.addPump(SoilSensor(A3, 700, 7000000), 5, 2000);  // Насос 4
  
  // Инициализация контроллера
  pumpController.setup();
}

void loop() {
  // Обработка всех насосов через контроллер
  pumpController.process();
}
Это насос с поплавком
Это насос с поплавком
это сенсор Capacitive Soil Moisture Sensor v2
это сенсор Capacitive Soil Moisture Sensor v2
как выглядит в собранная схема
как выглядит в собранная схема

Спасибо за внимание! Ожидаю комментарии и рекомендации.

Комментарии (15)


  1. randomsimplenumber
    02.09.2025 19:07

    const int MYHIGH = LOW;

    Нейминг суровое ;)

    Не то чтобы это сильно мешало.. но специальная константа для ровно одного места, да с таким странным именем..

    Логику и реализацию лучше бы разделить. Вдруг кому то на Raspberry pi захочеться портировать ;)

    Воще выделение памяти в контроллерах дурной тон. Что произойдет, если памяти для add Pump не хватит?


    1. doctor_kulibin Автор
      02.09.2025 19:07

      Нейминг да) у меня ардуино выключает релюхи в режиме HIGH, а хотелось бы включение с таким названием.
      Над разделением логики и реализации надо ещё соображать что и где разделять. А какие будут предложения?
      А вот как проверять оставшуюся свободную память на ардуино пока не знаю. Но я специально так сделал чтобы было удобнее добавлять насосы.


      1. m039
        02.09.2025 19:07

        Как альтернатива можно использовать вместо HIGH/LOW - ON/OFF.


    1. doctor_kulibin Автор
      02.09.2025 19:07

      проверил использование памяти: при создании 10 насосов будет использована половина памяти ардумино уно, т.е. из 2К остаётся только 1К.


      1. randomsimplenumber
        02.09.2025 19:07

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


        1. doctor_kulibin Автор
          02.09.2025 19:07

          В новой версии будет отображаться оставшаяся память RAM.


  1. xSVPx
    02.09.2025 19:07

    Вы правда планируете использовать сенсор так, как изображено на предпоследнем фото? Насколько его хватит, как думаете ?

    В таких проектах основная сложность монтаж и обработка аварий...


    1. sim2q
      02.09.2025 19:07

      Вы правда планируете использовать сенсор так, как изображено на предпоследнем фото?

      Если он не ёмкостный, то питание подавать только на время опроса. Это конечно должно быть предусмотрено в схеме.


      1. LeonidPr
        02.09.2025 19:07

        Capacitive Soil Moisture Sensor v2

        Судя по слову Capacitive, он емкостный


        1. doctor_kulibin Автор
          02.09.2025 19:07

          Именно так


      1. doctor_kulibin Автор
        02.09.2025 19:07

        Согласен. Какой-нибудь MOSFET использовать. В следующей версии скорректирую.


  1. theult
    02.09.2025 19:07

    За статью и старания большое спасибо! Надо дальше расти, прикручивать взрослые вещи на контроль ошибок (как мы мониторим наличие/отсутствие/неисправность датчика, как мы отслеживаем исправность насоса, наличие воды. Вотчдог не повредит)

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


    1. doctor_kulibin Автор
      02.09.2025 19:07

      В лак закатал края платы сенсора. Планирую добавить управление питанием датчиков. А вот как мониторить неисправности(датчика\насоса) пока сложно представляю. Долив воды планирую сделать с поплавковым клапаном.


  1. aspirinne
    02.09.2025 19:07

    Классная статья!
    А можете расписать список покупок? Что входит помимо сенсора и ардуины?


    1. doctor_kulibin Автор
      02.09.2025 19:07

      Я покупал набор с Али. Там ещё насосы и блок реле: https://a.aliexpress.com/_oDFKljH