#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <DHT.h>
#include <TimerOne.h>
#include <GyverEncoder.h>
#include <MQ135.h>
#define PWM_FAN_PIN 9 // Пин для управления вентилятором через ШИМ
#define RELAY_IN_PUMP 10 // Пин для реле насоса
#define RELAY_IN_UV 11 // Пин для реле УФ лампы
#define RELAY_IN_HEATER 12 // Пин для реле обогревателя
bool pumpFlag = false; // Флаг состояния насоса (полива)
bool UVFlag = false; // Флаг состояния УФ лампы
bool heaterFlag = false; // Флаг состояния обогревателя
bool relayPUMPState = LOW; // Текущее состояние реле насоса
bool lastPUMPRelayState = LOW; // Предыдущее состояние реле насоса
bool relayUVState = HIGH; // Текущее состояние реле УФ лампы (начальное состояние включено)
bool lastUVRelayState = LOW; // Предыдущее состояние реле УФ лампы
bool relayHEATERState = LOW; // Текущее состояние реле обогревателя
bool lastHEATERRelayState = LOW; // Предыдущее состояние реле обогревателя
#define DHT_PIN 4 // Пин для датчика DHT22
#define MQ135_PIN A0 // Пин для датчика MQ135
#define CALIBRATION_SAMPLE_TIMES (50) // Количество образцов для калибровки MQ135
#define CALIBRATION_SAMPLE_INTERVAL (500) // Интервал между образцами в миллисекундах
#define READ_SAMPLE_INTERVAL (100) // Интервал между чтениями в миллисекундах
#define READ_SAMPLE_TIMES (5) // Количество чтений для вычисления среднего значения
uint16_t valCO2; // Переменная для хранения значения CO2 с датчика MQ135
volatile boolean readingCO2 = false; // Флаг, указывающий на процесс чтения CO2
MQ135 mq135(MQ135_PIN); // Создание объекта MQ135, связанного с указанным пином
#define SOIL_MOISTURE_PIN A1 // Пин для датчика влажности почвы
uint16_t arrMoisture[10]; // Массив для хранения 10 последних значений влажности почвы
uint16_t valMoisture; // Переменная для расчета среднего значения влажности почвы
#define ENCODER_SW 7 // Пин для кнопки энкодера
#define ENCODER_DT 6 // Пин для DT сигнала энкодера
#define ENCODER_CLK 5 // Пин для CLK сигнала энкодера
Encoder encoder(ENCODER_CLK, ENCODER_DT, ENCODER_SW, TYPE2); // Создание объекта энкодера
LiquidCrystal_I2C lcd(0x27, 16, 2); // Создание объекта LCD с I2C адресом 0x27, 16 столбцов и 2 строки
#define ONE_WIRE_BUS 2 // Пин для шины OneWire, используемой для датчиков температуры DS18B20
OneWire oneWire(ONE_WIRE_BUS); // Создание объекта OneWire, связанного с указанным пином
DallasTemperature sensors(&oneWire); // Создание объекта DallasTemperature для работы с датчиками температуры DS18B20
uint16_t valTemperature; // Переменная для хранения значения температуры с датчиков DS18B20
int numberOfDevices; // Количество найденных датчиков DS18B20 на шине OneWire
DeviceAddress tempDeviceAddress; // Переменная для хранения адреса текущего датчика DS18B20
DHT dht(DHT_PIN, DHT22); // Создание объекта DHT для работы с датчиком DHT22, подключенным к указанному пину
// Настройки интерфейса пользователя
volatile int currentPage = 1; // Текущая страница интерфейса
volatile int thresholdValueTemp = 25; // Пороговое значение температуры
volatile int lowerThresholdValueTemp = 10; // Нижняя граница порога температуры
volatile int upperThresholdValueTemp = 40; // Верхняя граница порога температуры
volatile int thresholdValueSoil = 50; // Пороговое значение влажности почвы
volatile int lowerThresholdValueSoil = 5; // Нижняя граница порога влажности почвы
volatile int upperThresholdValueSoil = 95; // Верхняя граница порога влажности почвы
volatile int thresholdValueCO2 = 40; // Пороговое значение уровня CO2
volatile int lowerThresholdValueCO2 = 20; // Нижняя граница порога уровня CO2
volatile int upperThresholdValueCO2 = 60; // Верхняя граница порога уровня CO2
volatile int thresholdValueUV = 12; // Пороговое значение времени работы УФ лампы (в часах)
volatile int lowerThresholdValueUV = 2; // Нижняя граница порога времени работы УФ лампы
volatile int upperThresholdValueUV = 23; // Верхняя граница порога времени работы УФ лампы
volatile int thresholdWateringTime = 15; // Пороговое значение времени полива (в минутах)
volatile int lowerThresholdWateringTime = 5; // Нижняя граница порога времени полива
volatile int upperThresholdWateringTime = 60; // Верхняя граница порога времени полива
volatile int counter = 0; // Счетчик для различных целей
volatile int thresholdTempTime = 30; // Время работы обогревателя в секундах
int timeLastWateringSecs; // Время последнего полива в секундах
int timeLastWateringHours; // Время последнего полива в часах
int timeLastWateringMins; // Время последнего полива в минутах
unsigned long elapsedTimeSinceWatering = 0; // Время, прошедшее с последнего полива (в миллисекундах)
unsigned long relayPUMPCameOn = 0; // Время включения реле насоса (в миллисекундах)
unsigned long relayHEATERCameOn = 0; // Время включения реле обогревателя (в миллисекундах)
unsigned long relayUVCameOn = 0; // Время включения реле УФ лампы (в миллисекундах)
void setup() {
lcd.init(); // Инициализация LCD дисплея
lcd.backlight(); // Включение подсветки дисплея
Timer1.initialize(1000); // Установка таймера на 1000 микросекунд (1 миллисекунда)
Timer1.attachInterrupt(timerIsr); // Запуск таймера и привязка к прерыванию `timerIsr`
pinMode(PWM_FAN_PIN, OUTPUT); // Установка пина для вентилятора в режим вывода
// Устанавливаем пины для реле в режим вывода и задаем начальные состояния
pinMode(RELAY_IN_PUMP, OUTPUT); // Установка пина реле насоса в режим вывода
digitalWrite(RELAY_IN_PUMP, relayPUMPState); // Установка начального состояния реле насоса (LOW)
pinMode(RELAY_IN_UV, OUTPUT); // Установка пина реле УФ лампы в режим вывода
digitalWrite(RELAY_IN_UV, relayUVState); // Установка начального состояния реле УФ лампы (HIGH)
pinMode(RELAY_IN_HEATER, OUTPUT); // Установка пина реле обогревателя в режим вывода
digitalWrite(RELAY_IN_HEATER, relayHEATERState); // Установка начального состояния реле обогревателя (LOW)
pinMode(ENCODER_CLK, INPUT); // Установка пина для CLK сигнала энкодера в режим ввода
pinMode(ENCODER_DT, INPUT); // Установка пина для DT сигнала энкодера в режим ввода
pinMode(ENCODER_SW, INPUT_PULLUP); // Установка пина для кнопки энкодера в режим ввода с подтяжкой к питанию
Serial.begin(9600); // Инициализация последовательного порта со скоростью 9600 бод
dht.begin(); // Инициализация датчика DHT22
sensors.begin(); // Инициализация библиотеки для работы с датчиками DS18B20
numberOfDevices = sensors.getDeviceCount(); // Получение количества устройств на шине OneWire
// Печать информации о найденных устройствах
Serial.print("Locating devices...");
Serial.print("Found ");
Serial.print(numberOfDevices, DEC); // Вывод количества найденных устройств
Serial.println(" devices.");
// Цикл для каждого устройства, вывод адреса устройства
for(int i = 0; i < numberOfDevices; i++) {
// Поиск адреса устройства на шине
if(sensors.getAddress(tempDeviceAddress, i)) {
Serial.print("Found device ");
Serial.print(i, DEC);
Serial.print(" with address: ");
printAddress(tempDeviceAddress); // Вызов функции для печати адреса устройства
Serial.println();
} else {
Serial.print("Found ghost device at ");
Serial.print(i, DEC);
Serial.print(" but could not detect address. Check power and cabling"); // Сообщение об ошибке если адрес не найден
}
}
}
void loop() {
// Обновление меню дисплея
displayUpdate();
// Чтение уровня порога температуры и установка ШИМ для вентилятора
int dutyCycle = map((int)readTemperature(), 0, 100, 50, 255); // Перевод уровня температуры внутри системы в скважность ШИМ сигнала (от 50 до 255)
analogWrite(PWM_FAN_PIN, dutyCycle); // Управление вентилятором
// Управление насосом для полива
if(thresholdValueSoil >= readSoilMoisture()) {
pumpFlag = true; // Включить насос, если влажность почвы ниже порога
} else {
pumpFlag = false; // Выключить насос
}
if(relayPUMPState == HIGH) {
if(millis() - relayPUMPCameOn > thresholdWateringTime * 1000) {
digitalWrite(RELAY_IN_PUMP, LOW); // Выключить насос по истечении времени полива
relayPUMPState = LOW;
Serial.println("Stop Watering");
}
}
// Проверка изменения состояния насоса
if(pumpFlag != lastPUMPRelayState) {
lastPUMPRelayState = pumpFlag;
if((pumpFlag == true) && (relayPUMPState == false)) {
digitalWrite(RELAY_IN_PUMP, HIGH); // Включить насос
relayPUMPState = HIGH;
Serial.println("Watering");
relayPUMPCameOn = millis(); // Запомнить время включения насоса
}
}
elapsedTimeSinceWatering = millis() - relayPUMPCameOn; // Время, прошедшее с последнего полива
// Управление подогревом
if(thresholdValueTemp >= (int)readTemperature()) {
heaterFlag = true; // Включить подогрев, если температура ниже порога
} else {
heaterFlag = false; // Выключить подогрев
}
if(relayHEATERState == HIGH) {
if(millis() - relayHEATERCameOn > thresholdTempTime * 1000) {
digitalWrite(RELAY_IN_HEATER, LOW); // Выключить подогрев по истечении времени
relayHEATERState = LOW;
Serial.println("Stop Heating");
}
}
// Проверка изменения состояния подогрева
if(heaterFlag != lastHEATERRelayState) {
lastHEATERRelayState = heaterFlag;
if((heaterFlag == true) && (relayHEATERState == false)) {
digitalWrite(RELAY_IN_HEATER, HIGH); // Включить подогрев
relayHEATERState = HIGH;
Serial.println("Heating");
relayHEATERCameOn = millis(); // Запомнить время включения подогрева
}
}
// Управление УФ лампой
unsigned long currentMillis = millis();
const unsigned long relayOnDuration = thresholdValueUV * 60UL * 60UL * 1000UL; // Время работы УФ лампы в миллисекундах
const unsigned long relayOffDuration = (24 - thresholdValueUV) * 60UL * 60UL * 1000UL; // Время выключенной УФ лампы в миллисекундах
if (relayUVState) {
if (currentMillis - relayUVCameOn >= relayOnDuration) {
relayUVState = false; // Выключить УФ лампу
digitalWrite(RELAY_IN_UV, LOW);
Serial.println("UV Off");
relayUVCameOn = currentMillis; // Обновить время переключения
}
} else {
if (currentMillis - relayUVCameOn >= relayOffDuration) {
relayUVState = true; // Включить УФ лампу
digitalWrite(RELAY_IN_UV, HIGH);
Serial.println("UV On");
relayUVCameOn = currentMillis; // Обновить время переключения
}
}
}
// Энкодер
void timerIsr() { // Прерывание таймера
encoder.tick(); // Обработка энкодера
readCO2Callback(); // Чтение CO2 также выполняется в прерывании
// Проверка удержания энкодера и поворота вправо
if (encoder.isRightH())
Serial.println("Right holded");
// Проверка удержания энкодера и поворота влево
if (encoder.isLeftH())
Serial.println("Left holded");
// Проверка нажатия кнопки энкодера
if (encoder.isPress()) {
Serial.println("Pressed");
currentPage++; // Переход на следующую страницу
resetCounter(); // Сброс счетчика
// Возврат на первую страницу после последней
if (currentPage > 6) {
currentPage = 1;
}
}
}
int getCounter() {
int result;
noInterrupts(); // Отключение прерываний
result = counter; // Получение текущего значения счетчика
interrupts(); // Включение прерываний
return result; // Возврат значения счетчика
}
void resetCounter() {
noInterrupts(); // Отключение прерываний
counter = 0; // Сброс счетчика
interrupts(); // Включение прерываний
}
// UI
void displayUpdate() {
//lcd.clear(); // Закомментированная строка для очистки экрана
switch (currentPage) {
case 1: // Главная страница
lcd.setCursor(0, 0); // Установка курсора в начало первой строки
lcd.print((int)readTemperature()); // Отображение температуры
lcd.print("C ");
lcd.print("Hum. ");
lcd.print((int)readHumidity()); // Отображение влажности
lcd.print("%");
lcd.setCursor(0, 1); // Установка курсора в начало второй строки
lcd.print((int)readCO2()); // Отображение уровня CO2
lcd.print("ppm ");
lcd.print("Soil ");
lcd.print(readSoilMoisture()); // Отображение влажности почвы
lcd.print("%");
break;
case 2: // Температура
lcd.setCursor(0, 0); // Установка курсора в начало первой строки
lcd.print("Temp treshold ");
lcd.setCursor(0, 1); // Установка курсора в начало второй строки
lcd.print(" << ");
// Проверка поворота энкодера вправо для увеличения порога температуры
if (encoder.isRight()) {
if (thresholdValueTemp < upperThresholdValueTemp) {
thresholdValueTemp++;
}
}
// Проверка поворота энкодера влево для уменьшения порога температуры
if (encoder.isLeft()) {
if (thresholdValueTemp > lowerThresholdValueTemp) {
thresholdValueTemp--;
}
}
lcd.print(thresholdValueTemp); // Отображение порога температуры
lcd.print(" C >> ");
break;
case 3: // Влажность почвы
lcd.setCursor(0, 0); // Установка курсора в начало первой строки
lcd.print("Soil M. treshold ");
lcd.setCursor(0, 1); // Установка курсора в начало второй строки
lcd.print(" << ");
// Проверка поворота энкодера вправо для увеличения порога влажности почвы
if (encoder.isRight()) {
if (thresholdValueSoil < upperThresholdValueSoil) {
thresholdValueSoil++;
}
}
// Проверка поворота энкодера влево для уменьшения порога влажности почвы
if (encoder.isLeft()) {
if (thresholdValueSoil > lowerThresholdValueSoil) {
thresholdValueSoil--;
}
}
lcd.print(thresholdValueSoil); // Отображение порога влажности почвы
lcd.print(" % >> ");
break;
case 4: // Состояние CO2
lcd.setCursor(0, 0); // Установка курсора в начало первой строки
lcd.print("CO2 treshold ");
lcd.setCursor(0, 1); // Установка курсора в начало второй строки
lcd.print(" << ");
// Проверка поворота энкодера вправо для увеличения порога CO2
if (encoder.isRight()) {
if (thresholdValueCO2 < upperThresholdValueCO2) {
thresholdValueCO2++;
}
}
// Проверка поворота энкодера влево для уменьшения порога CO2
if (encoder.isLeft()) {
if (thresholdValueCO2 > lowerThresholdValueCO2) {
thresholdValueCO2--;
}
}
lcd.print(thresholdValueCO2); // Отображение порога CO2
lcd.print(" ppm >> ");
break;
case 5: // Засвет УФ лампы
lcd.setCursor(0, 0); // Установка курсора в начало первой строки
lcd.print("UV timer ");
lcd.setCursor(0, 1); // Установка курсора в начало второй строки
lcd.print(" << ");
// Проверка поворота энкодера вправо для увеличения времени работы УФ лампы
if (encoder.isRight()) {
if (thresholdValueUV < upperThresholdValueUV) {
thresholdValueUV++;
}
}
// Проверка поворота энкодера влево для уменьшения времени работы УФ лампы
if (encoder.isLeft()) {
if (thresholdValueUV > lowerThresholdValueUV) {
thresholdValueUV--;
}
}
lcd.print(thresholdValueUV); // Отображение времени работы УФ лампы
lcd.print(" h >> ");
break;
case 6: // Установка времени работы помпы полива (время полива)
lcd.setCursor(0, 0); // Установка курсора в начало первой строки
lcd.print("Watering time ");
lcd.setCursor(0, 1); // Установка курсора в начало второй строки
lcd.print(" << ");
// Проверка поворота энкодера вправо для увеличения времени полива
if (encoder.isRight()) {
if (thresholdWateringTime < upperThresholdWateringTime) {
thresholdWateringTime++;
}
}
// Проверка поворота энкодера влево для уменьшения времени полива
if (encoder.isLeft()) {
if (thresholdWateringTime > lowerThresholdWateringTime) {
thresholdWateringTime--;
}
}
lcd.print(thresholdWateringTime); // Отображение времени полива
lcd.print(" s >> ");
break;
default:
break;
}
}
// В Wokwi невозможно эмулировать состояние датчика корректным способом с помощью протокола OneWire
// Протестируйте на итоговом устройстве
// Метод автоматически сканирует указанную шину данных, опрашивает один датчик или группу датчиков на шине и вычисляет среднюю температуру
float readTemperature() {
sensors.requestTemperatures(); // Отправляем команду для получения температур
float tempC = 0; // Инициализация переменной для хранения температуры
// Цикл для сканирования каждого датчика на шине
for (int i = 0; i < numberOfDevices; i++) {
// Проверка наличия адреса датчика на шине
if (sensors.getAddress(tempDeviceAddress, i)) {
// Вывод дебаг информации с каждого датчика (закомментировано)
// Serial.print("Temperature for device: ");
// Serial.println(i, DEC);
tempC += sensors.getTempC(tempDeviceAddress); // Добавление температуры текущего датчика к общей сумме
// Serial.print("Temperature C: ");
// Serial.println(tempC);
// Serial.print(" Temp F: ");
// Serial.println(DallasTemperature::toFahrenheit(tempC)); // Преобразование температуры в Фаренгейты (закомментировано)
}
}
// Вычисление средней температуры
float averageTempC = tempC / numberOfDevices; // Деление общей суммы температур на количество датчиков
// Serial.print("Temperature average C: ");
// Serial.println(averageTempC);
return averageTempC; // Возврат средней температуры
}
// Метод для вывода адресов подключенных датчиков 18B20
void printAddress(DeviceAddress deviceAddress) {
for (uint8_t i = 0; i < 8; i++) {
if (deviceAddress[i] < 16) Serial.print("0"); // Добавление ведущего нуля для байтов с значением меньше 16
Serial.print(deviceAddress[i], HEX); // Вывод байта адреса в шестнадцатеричном формате
}
}
// Метод для чтения влажности с датчика DHT
float readHumidity() {
return dht.readHumidity(); // Возвращает текущую влажность
}
// Метод обратного вызова для установки флага чтения CO2
void readCO2Callback() {
readingCO2 = true; // Установка флага для чтения CO2
}
// Метод для чтения значения CO2
float readCO2() {
if (readingCO2) { // Если флаг чтения установлен
float rs = 0; // Переменная для хранения суммы сопротивлений
for (int i = 0; i < CALIBRATION_SAMPLE_TIMES; i++) {
rs += mq135.getResistance(); // Суммирование значений сопротивлений
}
rs = rs / CALIBRATION_SAMPLE_TIMES; // Вычисление среднего значения сопротивления
float ratio = rs / mq135.getRZero(); // Вычисление отношения текущего сопротивления к базовому
float co2 = mq135.getPPM(); // Получение значения CO2 в ppm
readingCO2 = false; // Сброс флага чтения
return co2; // Возврат значения CO2
}
return -1; // Возвращаем -1, если данные еще не готовы
}
// Метод для чтения значения влажности почвы
int readSoilMoisture() {
// Цикл для считывания значений влажности почвы
for (int i = 0; i < 10; i++) {
int tempValMoisture = map(analogRead(SOIL_MOISTURE_PIN), 0, 1024, 0, 100); // Считывание значения и преобразование в процент
// Возможно потребуется калибровка значений на итоговом устройстве
// Для калибровки измените значения "0, 1023", где 0 - минимум, 1023 - максимум, считываемый с аналогового пина датчика влажности почвы
arrMoisture[i] = tempValMoisture; // Сохранение значения в массив
// delay(10); // Закомментированный вызов задержки
}
for (int i = 0; i < 10; i++) {
valMoisture += arrMoisture[i]; // Суммирование значений из массива
}
valMoisture /= 10; // Вычисление среднего значения влажности почвы
return valMoisture; // Возврат среднего значения влажности почвы
}