#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <SoftwareSerial.h>
#include <DS3231.h>
#define SOIL_MOISTURE_PIN A0 // YL-69 аналоговый выход (A0 -> Arduino A0)
#define MHZ19_TX_PIN 3 // MH-Z19 TX пин
#define MHZ19_RX_PIN 2 // MH-Z19 RX пин
#define RELAY_1_PIN 8 // Реле канал 1 (Лампа)
#define RELAY_2_PIN 10 // Реле канал 2 (Шторы)
#define RELAY_3_PIN 11 // Реле канал 3 (Насос)
#define RELAY_4_PIN 12 // Реле канал 4 (Вентилятор)
// Определения для RGB светодиода
#define LED_COM 7
#define LED_R 9
#define LED_G 6
#define LED_B 5
// Определения для реле
#define LED_RELAY RELAY_1_PIN
#define CURTAINS_RELAY RELAY_2_PIN
#define PUMP_RELAY RELAY_3_PIN
#define FAN_RELAY RELAY_4_PIN
// Настройка датчиков
#define BME280_I2C_ADDR 0x76 // Адрес BME280 (может быть 0x76 или 0x77)
#define LCD_I2C_ADDR 0x27 // Адрес LCD дисплея (обычно 0x27 или 0x3F)
#define DISPLAY_INTERVAL 5000
#define TIME_UPDATE_INTERVAL 1000
// Инициализация объектов
SoftwareSerial mhz19Serial(MHZ19_TX_PIN, MHZ19_RX_PIN); // TX, RX
LiquidCrystal_I2C lcd(LCD_I2C_ADDR, 20, 4); // 20x4 LCD дисплей
Adafruit_BME280 bme; // BME280 датчик
DS3231 rtc; // DS3231 часы реального времени
enum DisplayMode {
TIME_DATE,
SOIL_MOISTURE,
AIR_DATA,
CO2_LEVEL,
DEVICES_STATE
};
DisplayMode currentDisplayMode = SOIL_MOISTURE;
// Целевые значения
int targetTemperature = 25;
int tempTolerance = 3;
int targetSoilMoisture = 50;
int soilTolerance = 5;
int targetCO2Level = 800;
int co2Tolerance = 200;
// Настройки времени
int curtainsOpenHour = 8;
int curtainsOpenMinute = 0;
int curtainsCloseHour = 20;
int curtainsCloseMinute = 0;
int lampOnHour = 8;
int lampOnMinute = 30;
int lampOffHour = 21;
int lampOffMinute = 0;
// Флаги состояния
bool isTimeSet = false;
bool ledState = false;
bool curtainsState = false;
bool pumpState = false;
bool fanState = false;
// Калибровка датчика почвы
int drySoilValue = 1000;
int wetSoilValue = 200;
// Временные переменные
unsigned long lastDisplayChange = 0;
unsigned long lastTimeUpdate = 0;
unsigned long lastCO2Reading = 0;
unsigned long co2ReadInterval = 5000; // Интервал чтения CO2 (5 секунд)
// Буфер для CO2
byte cmd[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
unsigned char response[9];
int co2Level = 0;
void setup() {
Serial.begin(9600);
Wire.begin();
// Инициализация LCD дисплея
lcd.init();
lcd.backlight();
// Инициализация BME280
if (!bme.begin(BME280_I2C_ADDR)) {
Serial.println(F("BME280 not found!"));
lcd.setCursor(0, 0);
lcd.print(F("BME280 not found!"));
}
// Инициализация MH-Z19
mhz19Serial.begin(9600);
// Настройка пинов
pinMode(RELAY_1_PIN, OUTPUT);
pinMode(RELAY_2_PIN, OUTPUT);
pinMode(RELAY_3_PIN, OUTPUT);
pinMode(RELAY_4_PIN, OUTPUT);
// Настройка пинов RGB светодиода
pinMode(LED_COM, OUTPUT);
pinMode(LED_R, OUTPUT);
pinMode(LED_G, OUTPUT);
pinMode(LED_B, OUTPUT);
// Установка реле в выключенное состояние (HIGH для выключения)
digitalWrite(RELAY_1_PIN, HIGH);
digitalWrite(RELAY_2_PIN, HIGH);
digitalWrite(RELAY_3_PIN, HIGH);
digitalWrite(RELAY_4_PIN, HIGH);
// Установка RGB светодиода
digitalWrite(LED_COM, HIGH); // Общий анод
digitalWrite(LED_R, HIGH); // Выключен (для общего анода HIGH = выкл)
digitalWrite(LED_G, HIGH);
digitalWrite(LED_B, HIGH);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Fitodomik System"));
lcd.setCursor(0, 1);
lcd.print(F("Initializing..."));
delay(1000);
// Запрос времени с компьютера
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Waiting for"));
lcd.setCursor(0, 1);
lcd.print(F("setup..."));
Serial.println(F("READY"));
displaySoilMoistureData();
}
void loop() {
processSerialCommands();
updateTime();
readCO2();
controlDevices();
updateCO2LED();
// Автоматическое переключение экранов
if (millis() - lastDisplayChange >= DISPLAY_INTERVAL) {
lastDisplayChange = millis();
switchToNextDisplay();
}
}
void updateTime() {
if (isTimeSet) {
if (millis() - lastTimeUpdate >= TIME_UPDATE_INTERVAL) {
lastTimeUpdate = millis();
if (currentDisplayMode == TIME_DATE) {
displayTimeDate();
}
}
}
}
void processSerialCommands() {
if (Serial.available() > 0) {
String command = Serial.readStringUntil('\n');
command.trim();
command.toUpperCase();
// Команды для настройки времени
if (command.startsWith("TIME:")) {
setTimeFromString(command.substring(5));
isTimeSet = true;
Serial.println(F("TIME_OK"));
}
// Команды для настройки параметров
else if (command.startsWith("SET:TEMP:")) {
targetTemperature = command.substring(9).toInt();
Serial.println(F("TEMP_OK"));
}
else if (command.startsWith("SET:TEMP_TOL:")) {
tempTolerance = command.substring(13).toInt();
Serial.println(F("TEMP_TOL_OK"));
}
else if (command.startsWith("SET:SOIL:")) {
targetSoilMoisture = command.substring(9).toInt();
Serial.println(F("SOIL_OK"));
}
else if (command.startsWith("SET:SOIL_TOL:")) {
soilTolerance = command.substring(13).toInt();
Serial.println(F("SOIL_TOL_OK"));
}
else if (command.startsWith("SET:CO2:")) {
targetCO2Level = command.substring(8).toInt();
Serial.println(F("CO2_OK"));
}
else if (command.startsWith("SET:CO2_TOL:")) {
co2Tolerance = command.substring(12).toInt();
Serial.println(F("CO2_TOL_OK"));
}
// Команды для настройки времени устройств
else if (command.startsWith("SET:CURT_OPEN:")) {
int h = command.substring(14, 16).toInt();
int m = command.substring(17, 19).toInt();
curtainsOpenHour = h; curtainsOpenMinute = m;
Serial.println(F("CURT_OPEN_OK"));
}
else if (command.startsWith("SET:CURT_CLOSE:")) {
int h = command.substring(15, 17).toInt();
int m = command.substring(18, 20).toInt();
curtainsCloseHour = h; curtainsCloseMinute = m;
Serial.println(F("CURT_CLOSE_OK"));
}
else if (command.startsWith("SET:LAMP_ON:")) {
int h = command.substring(12, 14).toInt();
int m = command.substring(15, 17).toInt();
lampOnHour = h; lampOnMinute = m;
Serial.println(F("LAMP_ON_OK"));
}
else if (command.startsWith("SET:LAMP_OFF:")) {
int h = command.substring(13, 15).toInt();
int m = command.substring(16, 18).toInt();
lampOffHour = h; lampOffMinute = m;
Serial.println(F("LAMP_OFF_OK"));
}
// Команды для прямого управления устройствами
else if (command.startsWith("LED:")) {
int state = command.substring(4).toInt();
ledState = state == 1;
digitalWrite(LED_RELAY, ledState ? LOW : HIGH);
Serial.print(F("LED:"));
Serial.println(ledState ? F("1") : F("0"));
}
else if (command.startsWith("CURTAINS:")) {
int state = command.substring(9).toInt();
curtainsState = state == 1;
digitalWrite(CURTAINS_RELAY, curtainsState ? LOW : HIGH);
Serial.print(F("CURTAINS:"));
Serial.println(curtainsState ? F("1") : F("0"));
}
else if (command.startsWith("RELAY3:")) {
int state = command.substring(7).toInt();
pumpState = state == 1;
digitalWrite(PUMP_RELAY, pumpState ? LOW : HIGH);
Serial.print(F("PUMP:"));
Serial.println(pumpState ? F("1") : F("0"));
}
else if (command.startsWith("RELAY4:")) {
int state = command.substring(7).toInt();
fanState = state == 1;
digitalWrite(FAN_RELAY, fanState ? LOW : HIGH);
Serial.print(F("FAN:"));
Serial.println(fanState ? F("1") : F("0"));
}
// Команды для запроса данных
else if (command == "GET_SENSORS" || command == "SENSORS" || command == "READ") {
sendSensorData();
}
else if (command == "GET_DEVICES") {
sendDeviceStates();
}
}
}
void setTimeFromString(String timeData) {
int yearVal = timeData.substring(0, 4).toInt();
int monthVal = timeData.substring(5, 7).toInt();
int dayVal = timeData.substring(8, 10).toInt();
int hourVal = timeData.substring(11, 13).toInt();
int minuteVal = timeData.substring(14, 16).toInt();
int secondVal = timeData.substring(17, 19).toInt();
rtc.setYear(yearVal - 2000); // DS3231 ожидает год в формате 0-99
rtc.setMonth(monthVal);
rtc.setDate(dayVal);
rtc.setHour(hourVal);
rtc.setMinute(minuteVal);
rtc.setSecond(secondVal);
isTimeSet = true;
Serial.print(F("Time set to: "));
Serial.println(timeData);
if (currentDisplayMode == TIME_DATE) {
displayTimeDate();
}
}
void switchToNextDisplay() {
switch (currentDisplayMode) {
case TIME_DATE:
currentDisplayMode = SOIL_MOISTURE;
displaySoilMoistureData();
break;
case SOIL_MOISTURE:
currentDisplayMode = AIR_DATA;
displayBMEData();
break;
case AIR_DATA:
currentDisplayMode = CO2_LEVEL;
displayCO2Data();
break;
case CO2_LEVEL:
currentDisplayMode = DEVICES_STATE;
displayDevicesState();
break;
case DEVICES_STATE:
currentDisplayMode = SOIL_MOISTURE;
displaySoilMoistureData();
break;
}
}
void displayTimeDate() {
if (!isTimeSet) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Waiting for time"));
lcd.setCursor(0, 1);
lcd.print(F("synchronization"));
return;
}
int yearVal = 2000 + rtc.getYear();
bool century;
int monthVal = rtc.getMonth(century);
int dayVal = rtc.getDate();
bool h12, PM_time;
int hourVal = rtc.getHour(h12, PM_time);
int minuteVal = rtc.getMinute();
int secondVal = rtc.getSecond();
lcd.clear();
lcd.setCursor(4, 0);
if (hourVal < 10) lcd.print("0");
lcd.print(hourVal);
lcd.print(":");
if (minuteVal < 10) lcd.print("0");
lcd.print(minuteVal);
lcd.print(":");
if (secondVal < 10) lcd.print("0");
lcd.print(secondVal);
lcd.setCursor(2, 1);
lcd.print(yearVal);
lcd.print("-");
if (monthVal < 10) lcd.print("0");
lcd.print(monthVal);
lcd.print("-");
if (dayVal < 10) lcd.print("0");
lcd.print(dayVal);
}
void displaySoilMoistureData() {
int soilMoistureRaw = analogRead(SOIL_MOISTURE_PIN);
int soilMoisturePercent = map(soilMoistureRaw, drySoilValue, wetSoilValue, 0, 100);
soilMoisturePercent = constrain(soilMoisturePercent, 0, 100);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Soil moisture:"));
lcd.setCursor(0, 1);
lcd.print(soilMoisturePercent);
lcd.print("%");
lcd.setCursor(0, 2);
lcd.print(F("Target: "));
lcd.print(targetSoilMoisture);
lcd.print(F("% \xB1"));
lcd.print(soilTolerance);
lcd.print(F("%"));
}
void displayBMEData() {
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0F; // гПа
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Temp: "));
lcd.print(temperature, 1);
lcd.print("\xDF""C");
lcd.setCursor(0, 1);
lcd.print(F("Humidity: "));
lcd.print(humidity, 1);
lcd.print("%");
lcd.setCursor(0, 2);
lcd.print(F("Pressure: "));
lcd.print(pressure, 1);
lcd.print(F("hPa"));
}
// Функция для управления RGB светодиодом в зависимости от уровня CO2
void updateCO2LED() {
// Выключаем все цвета
digitalWrite(LED_R, HIGH);
digitalWrite(LED_G, HIGH);
digitalWrite(LED_B, HIGH);
if (co2Level < 100) {
// Датчик не отвечает или ошибка - мигаем синим
static unsigned long lastToggle = 0;
static bool blinkState = false;
if (millis() - lastToggle >= 500) { // Мигаем каждые 500 мс
lastToggle = millis();
blinkState = !blinkState;
digitalWrite(LED_B, blinkState ? LOW : HIGH); // Для общего анода LOW = вкл
}
} else if (co2Level <= 800) {
// Нормальный уровень CO2 (100-800 ppm) - зеленый
digitalWrite(LED_G, LOW); // Включаем зеленый
} else if (co2Level <= 1200) {
// Повышенный уровень CO2 (800-1200 ppm) - желтый (зеленый + красный)
digitalWrite(LED_R, LOW);
digitalWrite(LED_G, LOW);
} else {
// Высокий уровень CO2 (>1200 ppm) - красный
digitalWrite(LED_R, LOW);
}
}
void readCO2() {
if (millis() - lastCO2Reading >= co2ReadInterval) {
lastCO2Reading = millis();
mhz19Serial.write(cmd, 9);
// Ждем ответа от датчика
memset(response, 0, 9);
int i = 0;
unsigned long startTime = millis();
while (mhz19Serial.available() == 0 && millis() - startTime < 1000) {
delay(10);
}
i = 0;
startTime = millis();
while (mhz19Serial.available() && i < 9 && millis() - startTime < 1000) {
response[i++] = mhz19Serial.read();
}
// Проверяем формат ответа
if (i > 2 && response[0] == 0xFF && response[1] == 0x86) {
co2Level = 256 * (int)response[2] + (int)response[3];
// Проверка на разумные значения CO2
if (co2Level < 100 || co2Level > 5000) {
co2Level = 0; // Значение при ошибке - будет мигать синим
}
// Обновляем RGB светодиод
updateCO2LED();
if (currentDisplayMode == CO2_LEVEL) {
displayCO2Data();
}
} else {
// Если нет ответа от датчика
co2Level = 0;
updateCO2LED();
}
}
}
void displayCO2Data() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("CO2 level:"));
lcd.setCursor(0, 1);
lcd.print(co2Level);
lcd.print(F(" ppm"));
// Отображение целевого значения
lcd.setCursor(0, 2);
lcd.print(F("Target: "));
lcd.print(targetCO2Level);
lcd.print(F(" \xB1"));
lcd.print(co2Tolerance);
// Отображение статуса CO2
lcd.setCursor(0, 3);
lcd.print(F("Status: "));
if (co2Level < 100) {
lcd.print(F("ERROR"));
} else if (co2Level <= 800) {
lcd.print(F("NORMAL"));
} else if (co2Level <= 1200) {
lcd.print(F("ELEVATED"));
} else {
lcd.print(F("HIGH"));
}
}
void displayDevicesState() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Lamp: "));
lcd.print(ledState ? F("ON") : F("OFF"));
lcd.setCursor(0, 1);
lcd.print(F("Curtains: "));
lcd.print(curtainsState ? F("OPEN") : F("CLOSED"));
lcd.setCursor(0, 2);
lcd.print(F("Pump: "));
lcd.print(pumpState ? F("ON") : F("OFF"));
lcd.setCursor(0, 3);
lcd.print(F("Fan: "));
lcd.print(fanState ? F("ON") : F("OFF"));
// Отображение состояния CO2 LED
lcd.setCursor(10, 3);
lcd.print(F("CO2:"));
if (co2Level < 100) {
lcd.print(F("ERROR"));
} else if (co2Level <= 800) {
lcd.print(F("NORM"));
} else if (co2Level <= 1200) {
lcd.print(F("ELEV"));
} else {
lcd.print(F("HIGH"));
}
}
void controlDevices() {
// Контроль влажности почвы и насоса
int soilMoistureRaw = analogRead(SOIL_MOISTURE_PIN);
int soilMoisturePercent = map(soilMoistureRaw, drySoilValue, wetSoilValue, 0, 100);
soilMoisturePercent = constrain(soilMoisturePercent, 0, 100);
if (soilMoisturePercent < (targetSoilMoisture - soilTolerance)) {
if (!pumpState) {
pumpState = true;
digitalWrite(PUMP_RELAY, LOW); // LOW активирует реле
}
} else if (soilMoisturePercent >= targetSoilMoisture) {
if (pumpState) {
pumpState = false;
digitalWrite(PUMP_RELAY, HIGH); // HIGH деактивирует реле
}
}
// Контроль температуры и вентилятора
float temperature = bme.readTemperature();
if (!isnan(temperature)) {
if (temperature > (targetTemperature + tempTolerance)) {
if (!fanState) {
fanState = true;
digitalWrite(FAN_RELAY, LOW); // LOW активирует реле
}
} else if (temperature <= targetTemperature) {
if (fanState) {
fanState = false;
digitalWrite(FAN_RELAY, HIGH); // HIGH деактивирует реле
}
}
}
// Управление шторами и освещением по времени
if (isTimeSet) {
bool h12, PM_time;
int currentHour = rtc.getHour(h12, PM_time);
int currentMinute = rtc.getMinute();
int currentTimeInMinutes = currentHour * 60 + currentMinute;
// Управление шторами
int curtainsOpenTimeInMinutes = curtainsOpenHour * 60 + curtainsOpenMinute;
int curtainsCloseTimeInMinutes = curtainsCloseHour * 60 + curtainsCloseMinute;
bool shouldCurtainsBeOpen;
if (curtainsOpenTimeInMinutes < curtainsCloseTimeInMinutes) {
shouldCurtainsBeOpen = (currentTimeInMinutes >= curtainsOpenTimeInMinutes &&
currentTimeInMinutes < curtainsCloseTimeInMinutes);
} else {
shouldCurtainsBeOpen = (currentTimeInMinutes >= curtainsOpenTimeInMinutes ||
currentTimeInMinutes < curtainsCloseTimeInMinutes);
}
if (shouldCurtainsBeOpen != curtainsState) {
curtainsState = shouldCurtainsBeOpen;
digitalWrite(CURTAINS_RELAY, curtainsState ? LOW : HIGH); // LOW активирует реле
}
// Управление лампой
int lampOnTimeInMinutes = lampOnHour * 60 + lampOnMinute;
int lampOffTimeInMinutes = lampOffHour * 60 + lampOffMinute;
bool shouldLampBeOn;
if (lampOnTimeInMinutes < lampOffTimeInMinutes) {
shouldLampBeOn = (currentTimeInMinutes >= lampOnTimeInMinutes &&
currentTimeInMinutes < lampOffTimeInMinutes);
} else {
shouldLampBeOn = (currentTimeInMinutes >= lampOnTimeInMinutes ||
currentTimeInMinutes < lampOffTimeInMinutes);
}
if (shouldLampBeOn != ledState) {
ledState = shouldLampBeOn;
digitalWrite(LED_RELAY, ledState ? LOW : HIGH); // LOW активирует реле
}
}
}
// Отправка данных с датчиков в формате, понятном для приложения
void sendSensorData() {
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0F; // гПа
int soilMoistureRaw = analogRead(SOIL_MOISTURE_PIN);
int soilMoisturePercent = map(soilMoistureRaw, drySoilValue, wetSoilValue, 0, 100);
soilMoisturePercent = constrain(soilMoisturePercent, 0, 100);
// Формат: SENSORS:температура:влажность:влажность_почвы:освещенность:co2:давление
Serial.print(F("SENSORS:"));
Serial.print(temperature);
Serial.print(F(":"));
Serial.print(humidity);
Serial.print(F(":"));
Serial.print(soilMoisturePercent);
Serial.print(F(":"));
Serial.print(F("500")); // Заглушка для освещенности, так как нет датчика света
Serial.print(F(":"));
Serial.print(co2Level);
Serial.print(F(":"));
Serial.println(pressure);
}
// Отправка состояния устройств
void sendDeviceStates() {
// Формат: DEVICES:лампа:шторы:насос:вентилятор
Serial.print(F("DEVICES:"));
Serial.print(ledState ? F("1") : F("0"));
Serial.print(F(":"));
Serial.print(curtainsState ? F("1") : F("0"));
Serial.print(F(":"));
Serial.print(pumpState ? F("1") : F("0"));
Serial.print(F(":"));
Serial.println(fanState ? F("1") : F("0"));
}