Water Plants with Arduino: Moisture Sensor and Water Pump (part 3)

Posted on

At this step, we will add a moisture sensor with a water pump to what we have built at the previous step.

Components needed:

ITEM PRICE
Water Pump $8.00 - $25.00
Moisture Sensor $4.00
Air Tube $2.00

Water pump plugs into the relay-controlled electric outlet. Connect air tube to the water pump. I made holes in the air tube for the water to flow out. Moisture sensor signal wire connects to one of the analog pins of the Arduino board. I chose to use pin A5. The is how to interpret the reading from the moisture sensor:

SENSOR VALUE DESCRIPTION
0 - 300 Dry soil
300 - 700 Humid soil
700 - 950 Wet soil/water

As you can see, these are relative measurements not based on any measurement unit. The range will shrink if you use long wires because of the nature of how the moisture sensor works. Think of the moisture sensor as an ohmmeter. When the soil is dry, there is no conductivity between two probes, so the resistance is infinite. In this case the moisture sensor will output 0. When the soil is dry, it conducts electricity and the moisture sensor can measure the conductivity and report the value other than 0. I have tried a few different power supplies for the Arduino board and found the quality of the power supply to affect the quality of readings. With a bad power supply, that generates a lot of noise in electronic circuit, consecutive reads would be about 100 units apart! The reading would show a number in 400s, 500s, 600s, and then goes back to 400s. With a better power supply (for example using laptop’s USB port), the reading would only fluctuate within 10-15 units. Build the sample project to make sure that the moisture sensor works as expected. Wire all of your components this way:

ARDUINO PIN COMPONENT PIN
8 (Digital Out) Relay Signal Pin
A5 (Analog In) Moisture Sensor Signal Pin
5V Relay VCC Pin
5V Moisture Sensor VCC Pin
GND Relay Ground Pin
GND Moisture Sensor Ground Pin

The Arduino program is a lot more involved this time.

enter image description here

A command that came from a serial port or Bluetooth is processed through CommunicationManager. Once the command is recognized. get_state; command is routed to ProcessManager. ProcessManager builds the state message and returns it to the CommunicationManager, which sends it back to the client. The value from set_moisture_threshold:XXX; command updates configuration setting in memory and updates EEPROM values using ConfigurationManager. Same goes for set_delay_between_watering_min:XXX; command. ProcessManager has access to configured values through СonfigurationManager. Another interesting challenge, which I didn’t expect to encounter, was working with EEPROM. Standard functions allow you to write only 1 byte of data. Because the moisture sensor returns a number between 0 and 1000 it would not fit into a byte. Here is the solution to save 2 bytes of variable value:

EEPROM.write(0, (value >> 0) & 0xFF);   //lowByte
EEPROM.write(1, value >> 8);            //highByte

Here is the rest of the program:

#include <SPP.h>
#include <usbhub.h>
#include <EEPROM.h>
#include <avr/wdt.h>

#define PUMP_PIN 8
#define MOISTURE_SENSOR_PIN  5
#define ON "on"
#define OFF "off"

// once the pump is on, how long does it stay on
//assuming that the moisture level not reached
const unsigned long WATERING_DURIATION_MS = 30000;

USB Usb;
USBHub Hub1(&Usb); // Some dongles have a hub inside
BTD Btd(&Usb); // You have to create the Bluetooth Dongle instance like so
/* You can create the instance of the class in two ways */
 // This will set the name to the defaults: "Arduino" and the pin to "1234"
SPP SerialBT(&Btd);
 // You can also set the name and pin like so
//SPP SerialBT(&Btd, "Lauszus's Arduino","0000");

//calling mills() seems to be costly, so let's do it only once per main loop iteration
unsigned long currentTimeMs;

class Switchable{
private:
  byte _pin;

public:
  Switchable(byte pin)
  {
    _pin = pin;
    pinMode(_pin, OUTPUT);
  }

  void On()
  {
    digitalWrite(_pin,HIGH);
  }

  void Off()
  {
    digitalWrite(_pin,LOW);  
  }

  boolean IsOn()
  {
    return digitalRead(_pin)==HIGH;
  }
};

class Sensor
{
private:
  int _pin;
  int _readings[3];
public:
  Sensor(byte pin)
  {
    _pin = pin;
    pinMode(_pin, INPUT);
  }

  int Read()
  {
    //stabilize the reading by
   // reading 3 times and returning the median
    _readings[0] = analogRead(_pin);
    delay(10);
    _readings[1] = analogRead(_pin);
    delay(10);
    _readings[2] = analogRead(_pin);

    if ((_readings[0]<_readings[1]) && (_readings[1]<_readings[2]))
    {
      return  _readings[1];
    }
    else if ((_readings[1]<_readings[0])&&(_readings[0]<_readings[2]))
    {
       return  _readings[0];    
    } else
    {
       return  _readings[2];          
    }
  }
};

class ConfigurationManager
{
  /*
   EEPROM Map:
   byte 0: _moistureTreshold
   byte 1: _moistureTreshold
   byte 2:_minDelayBetweenWateringInMinutes
   */
private:
  int  _moistureTreshold;
  byte _delayBetweenWateringInMinutes;

public:
  ConfigurationManager()
  {
    Load();
  }

  void Load()
  {
    _moistureTreshold = EEPROM.read(0);     //lowByte
    _moistureTreshold = _moistureTreshold | (EEPROM.read(1)<<8); //highByte

    _delayBetweenWateringInMinutes = EEPROM.read(2);
  }

  void SetMoistureThreshold(int value)
  {
    _moistureTreshold = value;

    EEPROM.write(0, (value >> 0) & 0xFF);   //lowByte
    EEPROM.write(1, value >> 8);            //highByte
  }

  byte SetDelayBetweenWateringInMinutes(byte value)
  {
    _delayBetweenWateringInMinutes = value;
    EEPROM.write(2, _delayBetweenWateringInMinutes);     
  }

  int GetMoistureThreshold()
  {
    return _moistureTreshold;
  }

  byte GetDelayBetweenWateringInMinutes()
  {   
    return _delayBetweenWateringInMinutes;
  }
};

class ProcessManager
{
private:
  Switchable* _pump;
  Sensor* _moistureSensor;
  ConfigurationManager* _configurationManager;
  unsigned long _pumpIsOnTimestamp;
  unsigned long _lastWateredMillis;
  String _stateMessage;

  /*
  Returns true if the conditions for watering are met
   Water only if:
   1) Soil is dry: moisture sensor has a low value: lower than _configurationManager->GetMoistureThreshold()
   2) We didn't water within the last X minutes: _configurationManager->GetMinDelayBetweenWateringInMinutes()
   */
  boolean  ShouldTurnOnPump()
  {   
    return (_moistureSensor->Read() < _configurationManager->GetMoistureThreshold()) &&
    //Honor DelayBetweenWateringInMinutes configuration. Set 10 second delay as minimum
    ((currentTimeMs - _lastWateredMillis) >= max( _configurationManager->GetDelayBetweenWateringInMinutes()*60000,10000));
  }

  boolean  ShouldTurnOffPump()
  {

      if (!_pump->IsOn()) return false;
      //turn off the pump if we watered long enough or
      //if MoistureThreshold is met
      unsigned long pumpWasOnMs = currentTimeMs-_pumpIsOnTimestamp;
      boolean moistureLevelReached = _moistureSensor->Read() >= _configurationManager->GetMoistureThreshold();
      return ( (pumpWasOnMs >= WATERING_DURIATION_MS) || moistureLevelReached);

  }

public:

  //constructor
  ProcessManager(Switchable* pump,Sensor* moistureSensor, ConfigurationManager* configurationManager)
  {
    _pump = pump;
    _moistureSensor = moistureSensor;
    _configurationManager = configurationManager;
    _lastWateredMillis = 0;
    _pumpIsOnTimestamp = 0;
  }

  //Water the plant if needed
  //Returns true if the plant was watered
  void TryWaterPlants()
  {
    //mills returns the number of milliseconds since the Arduino board began running the current program and is overflown every ~ 50 days
    //this condition is true when millis is overflown.
    if (currentTimeMs < _lastWateredMillis || currentTimeMs < _pumpIsOnTimestamp)
    {
      Serial.println("millis() is overflown");
      _lastWateredMillis = currentTimeMs;
      _pumpIsOnTimestamp = currentTimeMs;
    }

    if(ShouldTurnOnPump())
    {
      //Serial.println("pump is on\n");
      _pump->On();
      _pumpIsOnTimestamp = currentTimeMs;
      _lastWateredMillis = currentTimeMs;
    }else if (ShouldTurnOffPump())
    {
      _pump->Off();
    }
  }

  String& GetProccessStateMessage()
  {
    _stateMessage="";
    String tmp;
    tmp = digitalRead(PUMP_PIN)==HIGH? ON : OFF;
    _stateMessage += "current_moisture:"+String(_moistureSensor->Read())+"\n";
    _stateMessage += "pump:"+tmp+"\n";
    _stateMessage += "last_watered_sec_ago:"+String((currentTimeMs - _lastWateredMillis)/1000)+"\n";
    _stateMessage += "delay_between_watering_min:"+String(_configurationManager->GetDelayBetweenWateringInMinutes())+"\n";    
    _stateMessage += "moisture_threshold:"+String(_configurationManager->GetMoistureThreshold())+"\n";

    return _stateMessage;
  }
};

class CommunicationManager
{
private:
  char _inputChar;
  String _btIncomingMsg;
  String _spIncomingMsg;
  ProcessManager* _processManager;
  ConfigurationManager* _configurationManager;

  void ProccessIncomingData(char inputChar, String& incomingMsg)
  {
    if (inputChar == ';')
    {
      if (incomingMsg=="get_state")
      {
        Serial.println(_processManager->GetProccessStateMessage());
        if(SerialBT.connected)
        {
          SerialBT.println(_processManager->GetProccessStateMessage());
        }
      }
      else if (incomingMsg.length()>=String("set_moisture_threshold:").length() && incomingMsg.startsWith("set_moisture_threshold:"))
      {

        int val = atoi( incomingMsg.substring(String("set_moisture_threshold:").length()).c_str());
        _configurationManager->SetMoistureThreshold(val);
      }
      else if (incomingMsg.length()>=String("set_delay_between_watering_min:").length()  && incomingMsg.startsWith("set_delay_between_watering_min:"))
      {
        int val = atoi( incomingMsg.substring(String("set_delay_between_watering_min:").length()).c_str());
        _configurationManager->SetDelayBetweenWateringInMinutes(val);
      }
      //Serial.print("incomingMsg:"+incomingMsg+"\n");
      incomingMsg = "";
      //Serial.write("Clearing IncomingMsg");
    }
    else
    {
      incomingMsg.concat(inputChar);
    }
  }

public:
  CommunicationManager(ProcessManager* processManager, ConfigurationManager* configurationManager)
  {
    _configurationManager = configurationManager;
    _processManager = processManager;
    _btIncomingMsg = "";
  }

  void ProccessCommunicationMessages()
  {
  // Execute Bluetooth SPP tasks
    Usb.Task();
    //process serial port communication
    if(Serial.available())
    {
      _inputChar = Serial.read();
      ProccessIncomingData(_inputChar, _spIncomingMsg);
    }

    //process bluetooth communication
    if(SerialBT.connected && SerialBT.available())
    {
      _inputChar = SerialBT.read();
      //Serial.write(_inputChar);
      ProccessIncomingData(_inputChar, _btIncomingMsg);
    }
  }
};
ProcessManager* processManager;
CommunicationManager* communicationManager;
void setup() {
  Serial.begin(115200);

  //construct objects
  Switchable *pump = new Switchable(PUMP_PIN);
  Sensor*  moistureSensor = new Sensor(MOISTURE_SENSOR_PIN);
  ConfigurationManager* configurationManager = new ConfigurationManager();
  processManager = new ProcessManager(pump, moistureSensor, configurationManager);
  communicationManager = new CommunicationManager(processManager, configurationManager);
  //reboot if the board hanges for more than 4 seconds
  wdt_enable(WDTO_4S);

   if (Usb.Init() == -1) {
    Serial.println(F("\r\nOSC did not start"));
    while(1){wdt_reset();}; //halt
  }
  Serial.println(F("\r\nSPP Bluetooth Library Started"));
}

void loop() {
  wdt_reset();
  currentTimeMs = millis();
  processManager->TryWaterPlants();
  communicationManager->ProccessCommunicationMessages();
}

Android App has to display a few more pieces of information now and also needs to take an input from the user to configure the settings. Here is the how the mock interface looks like:

Android App

Download Android App code .