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.
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:
Download Android App code .