Appendix 2 The main program code
/* Author: Olivia De Meo ()
Virginia Commonwealth University, Richmond, VA
Version: March 2016 (written with Arduino 1.0.6)
Description: This program controls the automated irrigation system installed at
Cumberland Marsh in New Kent, VA. The program runs continuously, filling the holding
tanks at each high tide, and dispensing 25 L of water to our experimental plots twice daily
at low tide. The program also monitors several components of the system and will send
an error message if any malfunctions occur.
*/
//------------------------------------------------------------------------------------
// Add Libraries
#include <GSM.h> // For texting results to cell phone
#include <NewPing.h> // For use with the ultrasonic sensor
#include <Smoother.h> // For averaging 10 values from the ultrasonic sensor
#include <Wire.h> // For communication with the real time clock
#include <SPI.h> // Needed to prevent a RTClib compile error
#include <RTClib.h> // Real time clock for keeping time
#include <elapsedMillis.h> // Determines the number of elapsed milliseconds
/* Several of these libraries are not included with Arduino but can be downloaded at the
following sites:
NewPing: http://code.google.com/p/arduino-new-ping/
Smoother: https://github.com/fughilli/MagEncoderTest
RTClib: https://github.com/millerlp/Thermocouple_datalogger/tree/master/RTClib
elapsedMillis: http://playground.arduino.cc/Code/ElapsedMillis
*/
// Declare time clock variables
RTC_DS3231 RTC;
float initialHoursMinutesPing = 0.0;
float currentMinutesPing;
float currentHoursPing;
float currentHoursMinutesPing;
float initialHoursMinutesText = 0.0;
float currentMinutesText;
float currentHoursText;
float currentHoursMinutesText;
float hoursTime;
float minutesTime;
float hoursMinutesTime;
elapsedMillis timeElapsed; // Counts time elapsed
unsigned long dispenseInterval_valve = 240000; // Shut off solenoid after 4 minutes
unsigned long dispenseInterval = 900000; // Shut off pump after 15 minutes
// Declare ping sensor variables
long distance; // Distance calculated by the ping sensor
Smoother<int, 10> mySmoother; // Create a smoother to smooth integer values from a history of 10 pushed values
long avg_distance; // Averaged distance from smoothing function
float ping_interval = 0.05; // Ping interval for the ultrasonic sensor set to once every three minutes (MUST BE ENTERED IN DECIMAL HOURS)
float pump1000gph_height = 137; // Distance in centimeters from the ultrasonic sensor to the pump in the creek where the water level is high enough to activate the pumps
float lowTideHeight = 147; // Distance in centimeters from the ultrasonic sensor to the water when the experimental plots are no longer flooded
// Declare float switch variables
int floatSwitchPin_fresh = 42;
int floatSwitchPin_salt = 44;
int floatSwitchPin_lowBrine = 46;
int floatSwitchState_fresh; // Variables to represent the state of the float switches
int floatSwitchState_salt;
int floatSwitchState_lowBrine;
boolean freshTankFilled; // Flag for the freshwater tank float switch
boolean saltTankFilled; // Flag for the saltwater tank float switch
// Declare flow meter variables
char flowdataFresh [20]; // A 20 byte character array to hold incoming data from the flow meter
char flowdataSalt [20]; // A 20 byte character array to hold incoming data from the flow meter
byte receivedFresh = 0; // Number of characters received from the flow meter
byte receivedSalt = 0; // Number of characters received from the flow meter
byte serial_event_fresh = 0; // A flag to signal when data has been received from the flow meter
byte serial_event_salt = 0; // A flag to signal when data has been received from the flow meter
char *totalVolumeFresh; // Char pointer used in string parsing
char *totalVolumeSalt; // Char pointer used in string parsing
float plotWaterVolume = 25; // Dispense 25 liters to each plot
float flowMeterVolume_fresh = 0; // Volume read from flow meter after conversion to a float
float flowMeterVolume_salt = 0; // Volume read from flow meter after conversion to a float
// Declare pump variables
int brinePumpRelayPin = 32; // Peristaltic Pump
int freshPumpRelayPin = 33; // Creek Pump
int saltPumpRelayPin = 34; // Creek Pump
unsigned long brinePumpTime = 660000; // Add brine water to the saltwater tank for 11 minutes. Peristaltic pump rate is about 4.8 mL/second.
boolean FWstopLoop; // Flag to stop freshwater loop
boolean SWstopLoop; // Flag to stop saltwater loop
// Declare tank mixer variables
int brineMixerRelayPin = 35; // Brine tank mixer
int saltMixerRelayPin = 36; // Salt tank mixer
unsigned long saltMixerTime = 300000; // Mix saltwater tank for 5 minutes
unsigned long mixingTime = 300000; // Mix brine tank for 5 minutes
// Declare solenoid valve variables
int solenoidRelayPin1 = 22; // Freshwater Solenoids
int solenoidRelayPin2 = 23;
int solenoidRelayPin3 = 24;
int solenoidRelayPin4 = 25;
int solenoidRelayPin5 = 26;
int solenoidRelayPin6 = 27; // Saltwater Solenoids
int solenoidRelayPin7 = 28;
int solenoidRelayPin8 = 29;
int solenoidRelayPin9 = 30;
int solenoidRelayPin10 = 31;
// Create arrays with the pin numbers for the solenoid valves that need to open when dispensing water
int freshSolenoids[] = {solenoidRelayPin1, solenoidRelayPin2, solenoidRelayPin3, solenoidRelayPin4, solenoidRelayPin5};
int saltSolenoids[] = {solenoidRelayPin6, solenoidRelayPin7, solenoidRelayPin8, solenoidRelayPin9, solenoidRelayPin10};
// Declare text messaging variables
GSM gsmAccess; // Initialize the library instance
GSM_SMS sms;
#define PINNUMBER #### // Pin number for the SIM card to access the cell phone provider network
char remoteNum[20] = "##########"; // Telephone number that text messages will be sent to
boolean notConnected; // SIM card connection status
boolean brineTextSent; // Brine text message status
String relayMessage; // Relay text message
boolean relayState; // State of the relays
boolean relayTextSent; // Relay text message status
String textFW; // Text string
String textSW; // Text string
String textPing; // Text string
String pingMonitorMessage; // Full text message for monitoring the ping sensor and switches
String textTime; // Text string
String fillMessageFW; // Full text message for filling the FW tank
String fillMessageSW; // Full text message for filling the SW tank
String dispenseMessageFW; // Full text message for dispensing FW tank
String dispenseMessageSW; // Full text message for dispensing SW tank
String solenoidMessage; // Error message if solenoids are open
String FWpumpMessage; // Full text message for filling the FW tank
String SWpumpMessage; // Full text message for filling the SW tank
boolean FWsolenoidTextSent; // Solenoid text message status
boolean SWsolenoidTextSent; // Solenoid text message status
boolean FWpumpTextSent; // FW pump text message status
boolean SWpumpTextSent; // SW pump text message status
int text_interval = 1; // Set the interval for text monitoring messages in hours
// Declare brine tank fill switch variables
int fillBrineSwitchPin = 48;
boolean fillBrineSwitchState;
//----------------------------------------------------------------------------
void setup()
{
Serial.begin(9600);
Serial3.begin(9600);
Serial2.begin(9600);
Serial3.print("pre,m\r"); // Select medium flow meter size
Serial2.print("pre,m\r"); // Select medium flow meter size
Serial3.print("c,1\r"); // Set flow meter to continuous mode
Serial2.print("c,1\r"); // Set flow meter to continuous mode
Serial3.print("clear\r"); // Set the initial volume of the flow meter to zero
Serial2.print("clear\r"); // Set the initial volume of the flow meter to zero
// Set up the real time clock
Wire.begin();
RTC.begin();
RTC.adjust(DateTime(__DATE__, __TIME__)); // Sets the RTC to the date & time this sketch was compiled from the computer
DateTime now = RTC.now();
// Set ping sensor to be an input
pinMode(A0, INPUT); // AO refers to analog #0 pin
// Set the float switches to be inputs
pinMode(floatSwitchPin_fresh, INPUT);
pinMode(floatSwitchPin_salt, INPUT);
pinMode(floatSwitchPin_lowBrine, INPUT);
// Set the flags for the fresh and salt tanks to either filled or not filled
floatSwitchState_fresh = digitalRead(floatSwitchPin_fresh);
if(floatSwitchState_fresh == LOW){
freshTankFilled = false;}
else if(floatSwitchState_fresh == HIGH){
freshTankFilled = true;}
floatSwitchState_salt = digitalRead(floatSwitchPin_salt);
if(floatSwitchState_salt == LOW){
saltTankFilled = false;}
else if(floatSwitchState_salt == HIGH){
saltTankFilled = true;}
// Set brine tank fill switch to be an input
pinMode(fillBrineSwitchPin, INPUT);
// Set all relays to be off when the program starts (THIS MUST HAPPEN BEFORE THEY ARE DECLARED AS OUTPUTS BECAUSE THE RELAY IS AUTOMATICALLY ON)
digitalWrite(brinePumpRelayPin, HIGH); // Pumps
digitalWrite(freshPumpRelayPin, HIGH);
digitalWrite(saltPumpRelayPin, HIGH);
digitalWrite(brineMixerRelayPin, HIGH); // Mixers
digitalWrite(saltMixerRelayPin, HIGH);
digitalWrite(solenoidRelayPin1, HIGH); // Freshwater Solenoids
digitalWrite(solenoidRelayPin2, HIGH);
digitalWrite(solenoidRelayPin3, HIGH);
digitalWrite(solenoidRelayPin4, HIGH);
digitalWrite(solenoidRelayPin5, HIGH);
digitalWrite(solenoidRelayPin6, HIGH); // Saltwater Solenoids
digitalWrite(solenoidRelayPin7, HIGH);
digitalWrite(solenoidRelayPin8, HIGH);
digitalWrite(solenoidRelayPin9, HIGH);
digitalWrite(solenoidRelayPin10, HIGH);
// Set all relays to be outputs
pinMode(brinePumpRelayPin, OUTPUT); // Pumps
pinMode(freshPumpRelayPin, OUTPUT);
pinMode(saltPumpRelayPin, OUTPUT);
pinMode(brineMixerRelayPin, OUTPUT); // Mixers
pinMode(saltMixerRelayPin, OUTPUT);
pinMode(solenoidRelayPin1, OUTPUT); // Freshwater Solenoids
pinMode(solenoidRelayPin2, OUTPUT);
pinMode(solenoidRelayPin3, OUTPUT);
pinMode(solenoidRelayPin4, OUTPUT);
pinMode(solenoidRelayPin5, OUTPUT);
pinMode(solenoidRelayPin6, OUTPUT); // Saltwater Solenoids
pinMode(solenoidRelayPin7, OUTPUT);
pinMode(solenoidRelayPin8, OUTPUT);
pinMode(solenoidRelayPin9, OUTPUT);
pinMode(solenoidRelayPin10, OUTPUT);
// Track the status of the SIM card's connection to the network
notConnected = true;
while (notConnected){
if (gsmAccess.begin(PINNUMBER) == GSM_READY) // Connect to the network
notConnected = false;
else{
delay(2000);
}
}
sms.beginSMS(remoteNum);
sms.print("System Reset");
sms.endSMS();
// Set brine and relay text messages to false indicating message not sent
brineTextSent = false;
relayTextSent = false;
FWsolenoidTextSent = false;
SWsolenoidTextSent = false;
}
//----------------------------------------------------------------------------
// FUNCTIONS
// Read freshwater flow meter data
void serialEvent3(){
receivedFresh = Serial3.readBytesUntil(13, flowdataFresh, 20); // Read the data sent from the serial monitor until we see a <CR> and count how many characters have been received
flowdataFresh[receivedFresh] = 0; // Stop the buffer from transmitting leftovers
serial_event_fresh = 1; // Flag to indicate that we have received a string
}
// Read saltwater flow meter data
void serialEvent2(){
receivedSalt = Serial2.readBytesUntil(13, flowdataSalt, 20); // Read the data sent from the serial monitor until we see a <CR> and count how many characters have been received
flowdataSalt[receivedSalt] = 0; // Stop the buffer from transmitting leftovers
serial_event_salt = 1; // Flag to indicate that we have received a string
}
//----------------------------------------------------------------------------
void loop() // BEGIN LOOP
{
// ULTRASONIC SENSOR
// Ping the water in the creek every set interval to detect the water height
DateTime nowPing = RTC.now(); // Read the time and store in object "now"
currentHoursPing = nowPing.hour(); // Query the object "now" for the minutes
currentMinutesPing = nowPing.minute();
currentHoursMinutesPing = currentHoursPing + currentMinutesPing/60;
if ((currentHoursMinutesPing-initialHoursMinutesPing) < 0){
initialHoursMinutesPing = 0.0; // Set initial time back to 0 when going from midnight into the next morning
}
if ((currentHoursMinutesPing-initialHoursMinutesPing) >= ping_interval){
distance = analogRead(0); // Distance
mySmoother.pushValue(distance); // Insert distance value into smoothing function
avg_distance = mySmoother.pullValue(); // Average of the last 10 values
initialHoursMinutesPing = currentHoursMinutesPing;
}
//----------------------------------------------------------------------------
// FILL THE FRESHWATER AND SALTWATER TANKS
// When the water level is high enough and the tank is empty, fill the freshwater tank
floatSwitchState_fresh = digitalRead(floatSwitchPin_fresh); // Read the float switch state
FWstopLoop = false;
if ((floatSwitchState_fresh == LOW) && (avg_distance < pump1000gph_height)){ // When the distance from the ultrasonic sensor to the water is less than the depth of pump
// Record the time
DateTime nowTime = RTC.now(); // Read the time and store in object "now"
hoursTime = nowTime.hour();
minutesTime = nowTime.minute();
hoursMinutesTime = hoursTime + minutesTime/60;
// Fill the freshwater tank
timeElapsed = 0; // Reset elapsed time to zero
digitalWrite(freshPumpRelayPin, LOW); // Turn the freshwater pump on
while(floatSwitchState_fresh == LOW && FWstopLoop == false){ // While the float switch has not been triggered
floatSwitchState_fresh = digitalRead(floatSwitchPin_fresh); // Continue reading the state of the float switch
if (timeElapsed >= dispenseInterval){ // Monitor the time elapsed while the pump is running
digitalWrite(freshPumpRelayPin, HIGH); // If the pump runs too long and the tank hasn't filled, close the pump
// Send an error message indicating the malfunction
if(FWpumpTextSent == false){
FWpumpMessage = "FW pump off. Error.";
sms.beginSMS(remoteNum);
sms.print(FWpumpMessage);
sms.endSMS();
FWpumpTextSent = true;
}
FWstopLoop = true; // Exit the “while” loop
}
}
if(FWpumpTextSent == false){
digitalWrite(freshPumpRelayPin, HIGH); // When the float switch is triggered, exit the "while" loop and turn the freshwater pump off
freshTankFilled = true; // Set flag to true when the tank has filled
// Compile text message
textTime = "; Time: ";
textPing = "; Ping: ";
fillMessageFW = "Freshwater tank filled";
fillMessageFW = fillMessageFW + textPing + avg_distance + textTime + hoursMinutesTime;
// Send text message that the freshwater tank has filled
sms.beginSMS(remoteNum);
sms.print(fillMessageFW);
sms.endSMS();
}
}
// When the water level is high enough and the tank is empty, fill the saltwater tank
floatSwitchState_salt = digitalRead(floatSwitchPin_salt); // Read the float switch state
SWstopLoop = false;
if ((floatSwitchState_salt == LOW) && (avg_distance < pump1000gph_height)){ // When the distance from the ultrasonic sensor to the water is less than the depth of pump
// Record the time
DateTime nowTime = RTC.now(); // Read the time and store in object "now"
hoursTime = nowTime.hour();
minutesTime = nowTime.minute();
hoursMinutesTime = hoursTime + minutesTime/60;
// Creek Pump
timeElapsed = 0; // Reset elapsed time to zero
digitalWrite(saltPumpRelayPin, LOW); // Turn the saltwater pump on
while(floatSwitchState_salt == LOW && SWstopLoop == false){ // While the float switch has not been triggered
floatSwitchState_salt = digitalRead(floatSwitchPin_salt); // Continue reading the state of the float switch
if (timeElapsed >= dispenseInterval){ // Monitor the time elapsed while the pump is running
digitalWrite(saltPumpRelayPin, HIGH); // If the pump runs too long and the tank hasn't filled, close the pump
// Send an error message indicating the malfunction
if(SWpumpTextSent == false){
SWpumpMessage = "SW pump off. Error.";
sms.beginSMS(remoteNum);
sms.print(SWpumpMessage);
sms.endSMS();
SWpumpTextSent = true;
}
SWstopLoop = true; // Exit the “while” loop
}
}
if(SWpumpTextSent == false){
digitalWrite(saltPumpRelayPin, HIGH); // When the float switch is triggered, exit the "while" loop and turn the saltwater pump off
saltTankFilled = true; // Set flag to true when the tank has filled
// Mix saltwater tank
digitalWrite(saltMixerRelayPin, LOW); // Turn on mixer
delay(saltMixerTime); // Mix tank for 5 minutes
digitalWrite(saltMixerRelayPin, HIGH); // Turn off mixer
// Mix brine tank
floatSwitchState_lowBrine = digitalRead(floatSwitchPin_lowBrine); // Read the float switch state
if(floatSwitchState_lowBrine == HIGH){ // The tank has to have water in it because we don't want the pump to run dry
digitalWrite(brineMixerRelayPin, LOW); // Turn on mixing pump
delay(mixingTime); // Mix tank for 5 minutes
digitalWrite(brineMixerRelayPin, HIGH); // Turn off mixing pump
}
// Compile text message
textTime = "; Time: ";
textPing = "; Ping: ";
fillMessageSW = "Saltwater tank filled and mixed";
fillMessageSW = fillMessageSW + textPing + avg_distance + textTime + hoursMinutesTime;
// Send text message that the saltwater tank has filled
sms.beginSMS(remoteNum);
sms.print(fillMessageSW);
sms.endSMS();
}
}
//----------------------------------------------------------------------------
// DISPENSE FRESHWATER AND SALTWATER ONTO 10 PLOTS IN THE MARSH
// When the water level is low enough and the tank is full, dispense freshwater to the plots
if ((freshTankFilled == true) && (avg_distance > lowTideHeight)){ // If the tank is full and distance from the ultrasonic sensor to the water is greater than the height of the marsh
// Record the time
DateTime nowTime = RTC.now(); // Read the time and store in object "now"
hoursTime = nowTime.hour();
minutesTime = nowTime.minute();
hoursMinutesTime = hoursTime + minutesTime/60;
// Dispense water
Serial3.print("clear\r"); // Reset the flow meter to zero
timeElapsed = 0; // Reset elapsed time to zero
int i = 0;
for (i = 0; i < 5;){ // We will cycle through opening 5 solenoid valves
digitalWrite(freshSolenoids[i], LOW); // Open solenoid valve
serialEvent3(); // Read flow meter
if (serial_event_fresh == 1){ // If the serial_event flag is set we examine what data was sent