ESP8266:Примеры/Смарт-система для орошения растений на базе ESP8266

Материал из Онлайн справочника
Перейти к навигацииПерейти к поиску

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Черновик


Смарт-система для орошения растений на базе ESP8266[1]

Эта статья рассказывает, как при помощи модуля ESP8266 и сервиса EasyIoT Cloud собрать автоматическую систему для орошения растений. Эта система позволит удаленно управлять мотором водяной помпы и, следовательно, влажностью почвы. Благодаря этому проекту вы можете со спокойной душой отправиться в отпуск и поливать домашние растения, просто нажимая на кнопки в смартфоне или даже переключив систему в автоматический режим.

Введение

Функции, которыми будет оснащена наша система орошения растений:

  • Удаленное управление водяной помпой
  • Автоматическое орошение, если влажность почвы понизится до определенного уровня
  • Автоматический/ручной режим
  • Отображение данных о влажности почвы и статусе водяной помпы
  • Запись логов о влажности почвы

Орошение можно запустить прямо из браузерного интерфейса, и в этом случае помпа начнет качать воду в течение 10 секунд. Кроме того, орошение можно выставить в автоматический режим. В этом случае вы задаете в браузерном интерфейсе пороговое значение для влажности почвы, и если значение упадет ниже заданного порога, через система на 10 секунд включит водяную помпу. Через 10 минут система снова проверит влажность почвы и, если потребуется, снова включит помпу.

[Видео – https://www.youtube.com/watch?v=NArYvZaukw8]

Пользовательский интерфейс EasyIoT Cloud:

Данные о влажности почвы:

Необходимые компоненты

  • Один модуль ESP8266
  • Одна водяная помпа
  • Один датчик влажности почвы
  • Один понижающий преобразователь напряжения на базе LM2596
  • Один 12-вольтовый источник питания
  • Один транзистор 2N2222 (типа NPN)
  • Одна макетная плата MB102
  • Один резистор на 5 кОм

Настройка EasyIoT Cloud

Зарегистрируйтесь в сервисе EasyIoT Cloud. Зайдя в сервис, создайте новый модуль для системы орошения. О том, как выставить новый тип модуля (в нашем случае – ZMT_IRRIGATOR), читайте в этом руководстве.

Программа

Программа написана в IDE Arduino. Она использует библиотеку «esp-mqtt» (это враппер-библиотека MQTT для ESP8266, позволяющая коммуницировать с сервисом EasyIoT Cloud и управлять его настройками). Программу можно скачать с GitHub.

В программе нужно поменять следующие строчки (они отвечают за SSID, пароль к SSID, а также за логин и пароль к EasyIoT Cloud):

#define AP_SSID "xxx"
#define AP_PASSWORD "xxx"
#define EIOTCLOUD_USERNAME "xxx"
#define EIOTCLOUD_PASSWORD "xxx"

Затем загрузите программу на плату NodeMCU. Вместо платы NodeMCU можно воспользоваться и другим модулем ESP8266, но в этом случае вам понадобятся дополнительное питание и адаптер USB-Serial от FTDI.

#include <ESP8266WiFi.h>
#include <MQTT.h>
#include <EEPROM.h>

#define AP_SSID     "xxx"
#define AP_PASSWORD "xxx"  

#define EIOTCLOUD_USERNAME "xxx"
#define EIOTCLOUD_PASSWORD "xxx"

// создаем объект MQTT:
#define EIOT_CLOUD_ADDRESS "cloud.iot-playground.com"



#define PIN_PUMP         BUILTIN_LED //D0  // для встроенного 
                                           // светодиода на NodeMCU
#define PIN_BUTTON       D3  // для кнопки «Flash» на NodeMCU
#define PIN_HUM_ANALOG   A0  // для измерения влажности


#define MAX_ANALOG_VAL         956
#define MIN_ANALOG_VAL         250

#define IRRIGATION_TIME        10   // время орошения (в секундах)
#define IRRIGATION_PAUSE_TIME  300  // пауза между орошениями 
                                    // (в секундах); только для 
                                    // автоматического орошения

// значения для статуса оросителя:
typedef enum {
  s_idle             = 0,  // простой оросителя
  s_irrigation_start = 1,  // запуск орошения
  s_irrigation       = 2,  // орошение
  s_irrigation_stop  = 3,  // остановка орошения
} e_state;


#define CONFIG_START 0
#define CONFIG_VERSION "v01"

struct StoreStruct {
  // это чтобы убедиться, что это ваши настройки:
  char version[4];
  // переменные для ваших настроек:
  uint moduleId;  // ID модуля
} storage = {
  CONFIG_VERSION,
  // дефолтный модуль «0»
  0,
};

#define PARAM_HUMIDITY_TRESHOLD   "/Sensor.Parameter1"
#define PARAM_MANUAL_AUTO_MODE    "/Sensor.Parameter2"
#define PARAM_PUMP_ON             "/Sensor.Parameter3"
#define PARAM_HUMIDITY            "/Sensor.Parameter4"


#define MS_IN_SEC  1000 // 1S  


MQTT myMqtt("", EIOT_CLOUD_ADDRESS, 1883);

// переменные:
int state;
bool stepOk = false;
int soilHumidityThreshold;
bool autoMode;
String valueStr("");
String topic("");
boolean result;
int lastAnalogReading;
bool autoModeOld;
int soilHumidityThresholdOld;
unsigned long startTime;
int soilHum;
int irrigatorCounter;

void setup() {
  state = s_idle;
  pinMode(PIN_PUMP, OUTPUT); 
  pinMode(PIN_BUTTON, INPUT);

  autoMode = false;
  stepOk = false;
  soilHumidityThresholdOld = -1;
  startTime = millis();
  soilHum = -1;
  
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);  
  WiFi.begin(AP_SSID, AP_PASSWORD);

  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");  //  "Подключение к "
  Serial.println(AP_SSID);
    
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  };

  Serial.println("WiFi connected");  //  "Подключились к WiFi"
  Serial.println("Connecting to MQTT server");  
             //  "Подключение к MQTT-серверу"


  EEPROM.begin(512);
  loadConfig();


  // задаем ID клиента;
  // генерируем название клиента на основе MAC-адреса 
  // и последних 8 бит счетчика микросекунд:
  String clientName;
  //clientName += "esp8266-";
  uint8_t mac[6];
  WiFi.macAddress(mac);
  clientName += macToStr(mac);
  clientName += "-";
  clientName += String(micros() & 0xff, 16);
  myMqtt.setClientId((char*) clientName.c_str());

  Serial.print("MQTT client id:");  //  "ID MQTT-клиента"
  Serial.println(clientName);

  // функции обратного вызова:
  myMqtt.onConnected(myConnectedCb);
  myMqtt.onDisconnected(myDisconnectedCb);
  myMqtt.onPublished(myPublishedCb);
  myMqtt.onData(myDataCb);
  
  //////Serial.println("connect mqtt...");
                   //  "Подключение к MQTT..."
  myMqtt.setUserPwd(EIOTCLOUD_USERNAME, EIOTCLOUD_PASSWORD);  
  myMqtt.connect();

  delay(500);
  
  Serial.print("ModuleId: ");
  Serial.println(storage.moduleId);


  // создаем модуль, если нужно:
  if (storage.moduleId == 0)
  {
    // создаем модуль:
    Serial.println("create module: /NewModule");
               //  "Создание модуля: /NewModule"

    storage.moduleId = myMqtt.NewModule();

    if (storage.moduleId == 0)
    {
      Serial.println("Module NOT created. Check module limit");
                 //  "Модуль не создан. Проверьте лимит модулей"

      while(1)
        delay(100);
    }

   // задаем тип модуля:
    Serial.println("Set module type");  //  "Установка типа модуля"    
    myMqtt.SetModuleType(storage.moduleId, "ZMT_IRRIGATOR");

    // создаем параметр Sensor.Parameter1;
    // это пороговое значение для влажности почвы:
    Serial.println("new parameter: /"+String(storage.moduleId)+ PARAM_HUMIDITY_TRESHOLD);    
    myMqtt.NewModuleParameter(storage.moduleId, PARAM_HUMIDITY_TRESHOLD);

    // устанавливаем флаг «IsCommand»:
    Serial.println("set isCommand: /"+String(storage.moduleId)+ PARAM_HUMIDITY_TRESHOLD);    
    myMqtt.SetParameterIsCommand(storage.moduleId, PARAM_HUMIDITY_TRESHOLD, true);


    // создаем параметр Sensor.Parameter2;
    // «0» – ручной режим; «1» – автоматический режим:
    Serial.println("new parameter: /"+String(storage.moduleId)+ PARAM_MANUAL_AUTO_MODE);    
    myMqtt.NewModuleParameter(storage.moduleId, PARAM_MANUAL_AUTO_MODE);

    // устанавливаем флаг «IsCommand»:
    Serial.println("set isCommand: /"+String(storage.moduleId)+ PARAM_MANUAL_AUTO_MODE);    
    myMqtt.SetParameterIsCommand(storage.moduleId, PARAM_MANUAL_AUTO_MODE, true);

    
    // создаем параметр Sensor.Parameter3;
    // он отвечает за включение/выключение водяной помпы:
    Serial.println("new parameter: /"+String(storage.moduleId)+ PARAM_PUMP_ON);    
    myMqtt.NewModuleParameter(storage.moduleId, PARAM_PUMP_ON);


    // устанавливаем флаг «IsCommand»:
    Serial.println("set isCommand: /"+String(storage.moduleId)+ PARAM_PUMP_ON);    
    myMqtt.SetParameterIsCommand(storage.moduleId, PARAM_PUMP_ON, true);


    // создаем параметр Sensor.Parameter4;
    // он отвечает за текущую влажность почвы:
    Serial.println("new parameter: /"+String(storage.moduleId)+ PARAM_HUMIDITY);    
    myMqtt.NewModuleParameter(storage.moduleId, PARAM_HUMIDITY);


    // задаем описание:
    Serial.println("set description: /"+String(storage.moduleId)+ PARAM_HUMIDITY);    
    myMqtt.SetParameterDescription(storage.moduleId, PARAM_HUMIDITY, "Soil hum.");

    // задаем единицу измерения:
    Serial.println("set Unit: /"+String(storage.moduleId)+ PARAM_HUMIDITY);    
    myMqtt.SetParameterUnit(storage.moduleId, PARAM_HUMIDITY, "%");

    // устанавливаем флаг «dbLogging»;
    // он отвечает за запись логов в базу данных:
    Serial.println("set Unit: /"+String(storage.moduleId)+ PARAM_HUMIDITY);    
    myMqtt.SetParameterDBLogging(storage.moduleId, PARAM_HUMIDITY, true);

    // сохраняем ID нового модуля:
    saveConfig();
  }

  subscribe();
  
  lastAnalogReading = analogRead(PIN_HUM_ANALOG); 

  autoModeOld = !autoMode;
}

void loop() {
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
#ifdef DEBUG        
    Serial.print(".");
#endif
  }

  int in = digitalRead(PIN_BUTTON);

  //Serial.println(in);
  if (in == 0)
  {
    while(digitalRead(PIN_BUTTON) == 0)
      delay(100);

    if (state == s_idle || state == s_irrigation_start)
      state = s_irrigation_start;
    else
      state = s_irrigation_stop;
  }
  


  // публикуем изменения автоматического режима:
  if (autoModeOld != autoMode)
  {
    autoModeOld = autoMode;

    if (autoMode)    
      valueStr = String("1");
    else
      valueStr = String("0");
    
    topic  = "/"+String(storage.moduleId)+ PARAM_MANUAL_AUTO_MODE;
    result = myMqtt.publish(topic, valueStr, 0, 1);

    Serial.print("Publish topic: ");  //  "Публикация в топик: "
    Serial.print(topic);
    Serial.print(" value: ");  //  " значение: "
    Serial.println(valueStr);  
  }

  // публикуем изменения порогового значения:
  if (soilHumidityThreshold != soilHumidityThresholdOld)
  {
    soilHumidityThresholdOld = soilHumidityThreshold;
    valueStr = String(soilHumidityThreshold);
    
    topic  = "/"+String(storage.moduleId)+ PARAM_HUMIDITY_TRESHOLD;
    result = myMqtt.publish(topic, valueStr, 0, 1);

    Serial.print("Publish topic: ");  //  "Публикация в топик: "
    Serial.print(topic);
    Serial.print(" value: ");         //  " значение: "
    Serial.println(valueStr);  
  }
  
  if (IsTimeout())
  {
    startTime = millis();
    // выполняем обработку данных каждую секунду:
    int aireading = analogRead(PIN_HUM_ANALOG);

    Serial.print("Analog value: ");  //  "Аналоговое значение: "
    Serial.print(aireading);
    Serial.print(" ");
    // фильтр:
    lastAnalogReading += (aireading - lastAnalogReading) / 10;  
    Serial.print(lastAnalogReading); 
   
   // вычисляем влажность почвы (в процентах): 
   int newSoilHum = map(lastAnalogReading, MIN_ANALOG_VAL, MAX_ANALOG_VAL, 100, 0);  
   Serial.print(", Soil hum %:");  //  ", Влажность почвы в %:"
   Serial.println(newSoilHum); 
        
   // ограничиваем до 0-100%:
   if (newSoilHum < 0)
      newSoilHum = 0;

    if (newSoilHum > 100)
      newSoilHum = 100;
 
   // если произошли изменения во влажности почвы, сообщаем о них: 
   if (soilHum != newSoilHum)
   {
     soilHum = newSoilHum;
     //esp.send(msgHum.set(soilHum)); 
     
     valueStr = String(soilHum);
     topic  = "/"+String(storage.moduleId)+ PARAM_HUMIDITY;
     result = myMqtt.publish(topic, valueStr, 0, 1);

     Serial.print("Publish topic: ");  //  "Публикация в топик: "
     Serial.print(topic);
     Serial.print(" value: ");  //  " значение: "
     Serial.println(valueStr);  
   }
   
   
   // код для работы с состоянием оросителя:
   switch(state)
   {
     case s_idle:     
       if (irrigatorCounter <= IRRIGATION_PAUSE_TIME)
         irrigatorCounter++;
       
       if (irrigatorCounter >= IRRIGATION_PAUSE_TIME && autoMode)
       {
         if (soilHum <= soilHumidityThreshold)
           state = s_irrigation_start;       
       }         
       break;
     case s_irrigation_start:
       irrigatorCounter = 0;
       digitalWrite(PIN_PUMP, HIGH);
       //esp.send(msgMotorPump.set((uint8_t)1));       
       valueStr = String(1);
       topic  = "/"+String(storage.moduleId)+ PARAM_PUMP_ON;
       result = myMqtt.publish(topic, valueStr, 0, 1);    

       Serial.print("Publish topic: ");  //  "Публикация в топик: "
       Serial.print(topic);
       Serial.print(" value: ");  //  " значение: "
       Serial.println(valueStr);  
 
       state = s_irrigation;
       break;
     case s_irrigation:
       if (irrigatorCounter++ > IRRIGATION_TIME)
         state = s_irrigation_stop;
       break;
     case s_irrigation_stop:
       irrigatorCounter = 0;
       state = s_idle;
       //esp.send(msgMotorPump.set((uint8_t)0));
       valueStr = String(0);
       topic  = "/"+String(storage.moduleId)+ PARAM_PUMP_ON;
       result = myMqtt.publish(topic, valueStr, 0, 1);    

       digitalWrite(PIN_PUMP, LOW);
       break;
   }
  }
  
}

void loadConfig() {
  // чтобы убедиться, что настройки есть, и они ваши;
  // если настроек найдено не будет, будут использованы 
  // настройки по умолчанию:
  if (EEPROM.read(CONFIG_START + 0) == CONFIG_VERSION[0] &&
      EEPROM.read(CONFIG_START + 1) == CONFIG_VERSION[1] &&
      EEPROM.read(CONFIG_START + 2) == CONFIG_VERSION[2])
    for (unsigned int t=0; t<sizeof(storage); t++)
      *((char*)&storage + t) = EEPROM.read(CONFIG_START + t);
}

void saveConfig() {
  for (unsigned int t=0; t<sizeof(storage); t++)
    EEPROM.write(CONFIG_START + t, *((char*)&storage + t));

  EEPROM.commit();
}


String macToStr(const uint8_t* mac)
{
  String result;
  for (int i = 0; i < 6; ++i) {
    result += String(mac[i], 16);
    if (i < 5)
      result += ':';
  }
  return result;
}

void waitOk()
{
  while(!stepOk)
    delay(100);
 
  stepOk = false;
}

boolean IsTimeout()
{
  unsigned long now = millis();
  if (startTime <= now)
  {
    if ( (unsigned long)(now - startTime )  < MS_IN_SEC ) 
      return false;
  }
  else
  {
    if ( (unsigned long)(startTime - now) < MS_IN_SEC ) 
      return false;
  }

  return true;
}


void subscribe()
{
  if (storage.moduleId != 0)
  {
    // параметр Sensor.Parameter1 – пороговое значение:
    myMqtt.subscribe("/"+String(storage.moduleId)+ PARAM_HUMIDITY_TRESHOLD);
  
    // параметр Sensor.Parameter2 – 
    // переключение между ручным и автоматическим режимами:
    myMqtt.subscribe("/"+String(storage.moduleId)+ PARAM_MANUAL_AUTO_MODE);
  
    // параметр Sensor.Parameter3 – включение/выключение помпы:
    myMqtt.subscribe("/"+String(storage.moduleId)+ PARAM_PUMP_ON);
  }
}


void myConnectedCb() {
#ifdef DEBUG
  Serial.println("connected to MQTT server");
             //  "Подключение к серверу MQTT"
#endif
  subscribe();
}

void myDisconnectedCb() {
#ifdef DEBUG
  Serial.println("disconnected. try to reconnect...");
             //  "Произошло отключение. Пытаемся переподключиться..."
#endif
  delay(500);
  myMqtt.connect();
}

void myPublishedCb() {
#ifdef DEBUG  
  Serial.println("published.");  //  "Опубликовано."
#endif
}

void myDataCb(String& topic, String& data) {  
#ifdef DEBUG
  Serial.print("Receive topic: ");  //  "Принимающий топик: "
  Serial.print(topic);
  Serial.print(": ");
  Serial.println(data);
#endif
  if (topic == String("/"+String(storage.moduleId)+ PARAM_HUMIDITY_TRESHOLD))
  {
    soilHumidityThreshold = data.toInt();
    Serial.println("soilHumidityThreshold");
    Serial.println(data);
  }

  else if (topic == String("/"+String(storage.moduleId)+ PARAM_MANUAL_AUTO_MODE))
  {
    autoMode = (data == String("1"));
    Serial.println("Auto mode");  //  "Автоматический режим"
    Serial.println(data);
  }
  else if (topic == String("/"+String(storage.moduleId)+ PARAM_PUMP_ON))
  {
    //switchState = (data == String("1"))? true: false;
    if (data == String("1"))
      state = s_irrigation_start;
    else
      state = s_irrigation_stop;
    Serial.println("Pump");  //  "Водяная помпа"
    Serial.println(data);
  }
}

Подключение компонентов

Чтобы упростить цепь проекта, мы воспользуемся платой NodeMCU. Впрочем, вы можете использовать любой другой модуль с чипом ESP8266, но вам дополнительно понадобится 3,3-вольтовое питание.

Подключение датчика влажности почвы

Чтобы обеспечить питанием датчик влажности почвы, подключите контакт VCC на датчике к 3.3V на ESP8266, а контакт GND на датчике – к контакту GND на ESP8266. Затем подключите контакт AO на датчике (он отвечает за вывод аналоговых данных) к входному аналоговому контакту A0 на ESP8266. Контакт DO на датчике оставьте неподключенным.

Подключение мотора водяной помпы к ESP8266

Для этого понадобится реле-модуль. Поскольку реле-модуль использует 5-вольтовое питание, а ESP82663,3-вольтовое, нам нужно будет добавить в цепь транзистор, чтобы управлять 5-вольтовым реле при помощи 3,3 вольт. Кроме того, к контакту D0 на ESP8266 нужно подключить транзистор, а между D0 и транзистором – резистор на 5 кОм. Если вы подключите реле-модуль напрямую к модулю ESP8266, то просто поджарите его.

Другая сторона реле-модуля должна быть подключена к мотору водяной помпы и регулируемому источнику питания. Напряжение для мотора водяной помпы нужно поставить между 6 и 9 вольтами.

Подключение кнопки для включения/выключения помпы

Если вы используете плату NodeMCU, то подключать кнопку не потребуется – можно воспользоваться кнопкой Flash на плате. Но если вы используете другой тип модуля ESP8266, тогда возьмите резистор на 10 кОм, а затем подключите одну его сторону к 3,3 вольтам, а вторую – к контакту D3. Потом подключите контакт D3 к контакту GND, а между ними – кнопку.

См.также

Внешние ссылки