Overview
A low-cost electrical conductivity (EC) sensor using the Mayfly platform was developed in part to support Great Marsh Institute’s (GMI) efforts to study the effects of stormwater runoff into Great Marsh in Chester County, Pennsylvania. The main focus is road salt runoff from Route 401 and the PA Turnpike which border the marsh on the east and west sides.
Six monitoring sites are planned of which three are currently installed. This data will supplement EC data from Delaware River Watershed Initiative sensor stations SL149 and SL150 which are deployed at Moore’s Road and Fairfield Road on Marsh Creek.
Mayfly EC Sensor Details
Since EC is temperature dependent a temperature reading is necessary so readings can be corrected to 25º C. A 10 KΩ resistance temperature detector (RTD) is used to measure temperature which is then used to correct the reading to the reference temperature. Sensorex provides a probe an integrated RTD. This eliminates the need for a separate sensor which would have to be mounted with the probe and in contact with the water.
The probe signal is processed by the Atlas Scientific EZO circuit. This circuit provides calibration routines for the attached EC probe but does not correct for temperature. This is done in the Mayfly code using a 2nd order polynomial fit to the data on the test fluid bottle.
Prior to deployment the EZO Circuit needs to be configured. This includes setting up the EZO to send continuous readings at predetermined intervals and performing a probe calibration. The probe calibration can be done as single point or two point calibration depending on what calibration standards are available. Single point calibration should be adequate using 1410 µS calibration standard.
Required Hardware
- Sensorex CS150TC Light-Duty Contacting Conductivity / Total Dissolved Solids Sensor with Automatic Temperature Compensation, $140
- EnviroDIY Mayfly Data Logger Board, $60
- Atlas Scientific EZO-EC Embedded Conductivity Circuit 0.07 − 500,000+ μS/cm, $60
- Cable box, $12
- EnviroDIY Mayfly ProtoShield, $10
- 10 KΩ resistor
- 0.01 µF capacitor
- Six-position wire to board terminal block, $2
- Socket strip for EZO
- 2500 mAh lithium ion battery, $15
- 1 Watt solar panel, $20
Total cost: $319
Click on images to enlarge.
Mayfly Code
Initially the Atlas/Mayfly sensor needs to have the Atlas EZO circuit setup. This includes probe calibration and setting the operational mode to continuous (“C”) output rather than triggered (“R”). Refer to the Atlas EZO datasheet for details. Once this is done the operational code can be loaded which reads the EC and temperature data, corrects the EC reading to 25º C reference, and stores the data along with a time stamp on the micro SD card on board the Mayfly.
When the station wakes from a predefined sleep period the EZO is powered up and begins to take readings at two-second (recommended) intervals. It takes several readings for the results to stabilize so typically 10 readings are taken and the last reading is logged to the SD card. The station then goes to sleep for a specified time period.
Note: The code uses the D10 hardware interrupt, therefore SJ1 must be cut from A7 and connected to D10; see Mayfly schematic for details.
Mayfly Setup Procedure
- Using the Set Real-Time Clock Code below, set the Real-Time Clock (RTC) to local standard time for consistency. Note that opening the serial monitor will reset the time unless the rtc.setDateTime(dt) is commented out.
- Use the Atlas EZO Setup Code below to calibrate the EC probe per the Atlas EZO datasheet. A two-point calibration is recommended using 84 µS and 1413 µS calibration standards. Remember to use the value of the calibration standard at the ambient temperature of the standard when doing the calibration procedure. For example, if the ambient temperature of the calibration standard is 20º C, then “cal,high, 1278” should be entered.
- The RTD in the Sensorex probe seems to have fairly consistent calibration using the m and b coefficients in the code (float m = -.0927, b = 72.653;). The accuracy can be checked at two points; typically, an ice bath at 0º C and warm water at about 40º C with errors used to correct the coefficients.
- Before loading the EC Monitor Code be sure to set the EZO to continuous update of 2 seconds (“c,2”). It was found that with a one-second update some devices would produce a zero reading for unknown reasons. However, when the update was set to two seconds the problem did not reoccur.
- Load the EC Monitor Code with the values of sleepMinutes, FILE_NAME, and LOGGERNAME set to user preference.
Set Real-Time Clock Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// Date and time functions using a RX8025 RTC connected via I2C and Wire lib #include <Wire.h> #include "Sodaq_DS3231.h" char weekDay[][4] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; //year, month, date, hour, min, sec and week-day(starts from 0 and goes to 6) //writing any non-existent time-data may interfere with normal operation of the RTC. //Take care of week-day also. DateTime dt(2019, 8, 13, 23, 31 , 0, 2); void setup () { Serial.begin(9600); Wire.begin(); rtc.begin(); //Allow 8 sec to upload so as to match dt. Do not open serial monitor //untill code reloaded with the next line commented out //rtc.setDateTime(dt); //Adjust date-time as defined 'dt' above } void loop () { DateTime now = rtc.now(); //get the current date-time Serial.print(now.year(), DEC); Serial.print('/'); Serial.print(now.month(), DEC); Serial.print('/'); Serial.print(now.date(), DEC); Serial.print(' '); Serial.print(now.hour(), DEC); Serial.print(':'); Serial.print(now.minute(), DEC); Serial.print(':'); Serial.print(now.second(), DEC); Serial.println(); Serial.print(weekDay[now.dayOfWeek()]); Serial.println(); delay(1000); } |
Atlas EZO Setup Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
/* 3/16/19 Modified getTemp()for 10K RTD */ #include <SoftwareSerial.h> //we have to include the SoftwareSerial library, or else we can't use it. #include <SD.h> #include <RTCTimer.h> #include <Sodaq_DS3231.h> #define rx 7 //D7 to Tx on EZO (white on Grove conn) #define tx 6 //D6 to Rx on EZO (yellow on Grove conn) #define SD_SS_PIN 12 //orig had pin 11 //The data log file #define FILE_NAME "datafile.txt" //Data header #define LOGGERNAME "Atlas Sci Sensor" #define DATA_HEADER "DateTime_EST,Loggertime,BoardTemp_C,Battery_V,ECTemp,EC" SoftwareSerial myserial(rx, tx); //define how the soft serial port is going to work. String inputstring = ""; //a string to hold incoming data from the PC String sensorstring = ""; //a string to hold the data from the Atlas Scientific product boolean input_stringcomplete = false; //have we received all the data from the PC boolean sensor_stringcomplete = false; //have we received all the data from the Atlas Scientific product //used to hold a floating point number that is the pH. float Temp0 = 0; float m = -.0927, b = 72.653; //Calibration for RTD on Sensorex probe long currentepochtime = 0; int currentminute; String dataRec = ""; float boardtemp; int batteryPin = A6; // to read battery voltage int batterysenseValue = 0; // variable to store the value coming from the sensor float batteryvoltage; void setup() { //set up the hardware Serial.begin(9600); //set baud rate for the hardware serial port_0 to 9600 myserial.begin(9600); //set baud rate for software serial port_3 to 9600 inputstring.reserve(10); //set aside some bytes for receiving data from the PC sensorstring.reserve(30); //set aside some bytes for receiving data from Atlas Scientific product setupLogFile(); pinMode(22, OUTPUT); //Power to Grove connectors digitalWrite(22, true); //turn power on //pinMode(rx, INPUT); } void serialEvent() { //if the hardware serial port_0 receives a char char inchar = (char)Serial.read(); //get the char we just received inputstring += inchar; //add it to the inputString if (inchar == '\r') { input_stringcomplete = true; //if the incoming character is a <CR>, set the flag } } void loop() { //here we go... if (input_stringcomplete) { //if a string from the PC has been received in its entirety myserial.print(inputstring); //send that string to the Atlas Scientific product inputstring = ""; //clear the string input_stringcomplete = false; //reset the flag used to tell if we have received a completed string from the PC } if (myserial.available() > 0) { //if we see that the Atlas Scientific product has sent a character. char inchar = (char)myserial.read(); //get the char we just received sensorstring += inchar; if (inchar == '\r') { sensor_stringcomplete = true; //if the incoming character is a <CR>, set the flag } } if (sensor_stringcomplete) { //if a string from the Atlas Scientific product has been received in its entirety recieveSensorData(); } } //end of loop void setupLogFile() { //Initialise the SD card if (!SD.begin(SD_SS_PIN)) { Serial.println("Error: SD card failed to initialise or is missing."); //Hang // while (true); } //Check if the file already exists bool oldFile = SD.exists(FILE_NAME); //Open the file in write mode File logFile = SD.open(FILE_NAME, FILE_WRITE); //Add header information if the file did not already exist if (!oldFile) { logFile.println(LOGGERNAME); logFile.println(DATA_HEADER); } //Close the file to save it logFile.close(); } void logData(String rec) { //Re-open the file File logFile = SD.open(FILE_NAME, FILE_WRITE); //Write the CSV data logFile.println(rec); //Close the file to save it logFile.close(); } String getDateTime() { String dateTimeStr; //Create a DateTime object from the current time DateTime dt(rtc.makeDateTime(rtc.now().getEpoch())); currentepochtime = (dt.get()); //Unix time in seconds currentminute = (dt.minute()); //Convert it to a String dt.addToString(dateTimeStr); return dateTimeStr; } void recieveSensorData() { if (sensor_stringcomplete) { //if a string from the EZO product has been received //in its entirety dataRec = createDataRecord(); logData(dataRec); //Save the data record to the log file } Serial.print(" "); Serial.println(dataRec); sensorstring = ""; //clear the string: sensor_stringcomplete = false; //reset the flag Rec to tell if we have received a } String createDataRecord() { //Create a String type data record in csv format //TimeDate, Loggertime,Temp_DS, Diff1, Diff2, boardtemp String data = getDateTime(); data += ","; rtc.convertTemperature(); //convert current temperature into registers boardtemp = rtc.getTemperature(); //Read temperature sensor value batterysenseValue = analogRead(batteryPin); batteryvoltage = (3.3 / 1023.) * 4.7 * batterysenseValue; //4.7 for rev 0.5 data += currentepochtime; data += ","; addFloatToString(data, boardtemp, 3, 1); //float data += ","; addFloatToString(data, batteryvoltage, 4, 2); data += " , "; //adds a comma between values Temp0 = getTemp(); addFloatToString(data, Temp0, 4, 2); //float data += ","; data += sensorstring; //needs to be last record added to dataRecord //due to built in cr/lf return data; } float getTemp() { float Temp; Temp = analogRead(A0); Temp = m * Temp + b; return Temp; } static void addFloatToString(String & str, float val, char width, unsigned char precision) { char buffer[10]; dtostrf(val, width, precision, buffer); str += buffer; } |
EC Monitor Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
/* 3/26/19 added linear cal for RTD using 10k divider */ #include <Wire.h> #include <avr/sleep.h> #include <avr/wdt.h> #include <SPI.h> #include <SD.h> #include <SoftwareSerial.h> #include <RTCTimer.h> #include <Sodaq_DS3231.h> RTCTimer timer; #define rx 7 //D7 to Tx on EZO (white on Grove conn) #define tx 6 //D6 to Rx on EZO (yellow on Grove conn) SoftwareSerial myserial(rx, tx); //define how the soft serial port is going to work. String dataRec = ""; String sensorstring = "EZO data"; //a string to hold the data from the EZO product float Temp0 = 0; //float K_temp = 0.034; //2/17/19 update with new LM35 float m = -.0927, b = 72.653; //Calibration for RTD on Sensorex probe float EC, EC25; float alpha; //Temperature correction to EC at 25C float K0 = 0.018, K1 = 8E-5, K2 = 1E-6; int currentminute; int n = 0; int nMax = 10; //number of readings to take on each wake period int nSkip = 10; //readings to skip on startup so only valid data is logged int sleepMinutes = 5; //total minutes the Mayfly is in sleep mode long currentepochtime = 0; float boardtemp; int batteryPin = A6; // to read battery voltage int batterysenseValue = 0; // variable to store the value coming from the sensor float batteryvoltage; boolean sensor_stringcomplete = false; //have we received all the data from the EZO product //RTC Interrupt pin #define RTC_PIN 10 //Was A7 for old board #define RTC_INT_PERIOD EveryMinute #define SD_SS_PIN 12 //orig had pin 11 //The data log file #define FILE_NAME "EC3data.txt" //Data header #define LOGGERNAME "GMI_EC#3" #define DATA_HEADER "DateTime_EST,Loggertime,BoardTemp_C,Battery_V,Temp_C,EC_25" void setup() { //Initialise the serial connection Serial.begin(9600); myserial.begin(9600); //set baud rate for software serial port_3 to 9600 //if myserial active sleep doesn’t work rtc.begin(); pinMode(8, OUTPUT); pinMode(9, OUTPUT); pinMode(22, OUTPUT); //Power to Grove connectors sensorstring.reserve(30); //set aside some bytes for receiving data from EZO product greenred4flash(); //blink the LEDs to show the board is on setupLogFile(); setupTimer(); //Setup timer events setupSleep(); //Setup sleep mode digitalWrite(tx, false); //Turn off tx to EZO ckt digitalWrite(rx, false); //Turn off rx to EZO ckt digitalWrite(22, true); //Turn on power to EZO ckt Serial.println("Power On, running: mayfly_sleepEZO.ino"); showTime(getNow()); } void loop() { //Update the timer timer.update(); if (currentminute % sleepMinutes == 0) //will wake every minute for set delay untill condition //is satisfied { while (!sensor_stringcomplete && (n < nMax)) { recieveSensorData(); } n = 0; //reset EZO read counter } delay(100); //should be less than sensor turnon time which is ~1sec //Sleep systemSleep(); } //end loop void sensorsSleep() { Serial.println("..going to sleep!"); digitalWrite(22, false); //Turn off power to EZO ckt digitalWrite(tx, false); //Turn off tx to EZO ckt digitalWrite(rx, false); //Turn off rx to EZO ckt delay(100); //adding delay fixed the never-sleep problem } void sensorsWake() { digitalWrite(22, true); //Turn on power to EZO ckt Serial.println("..I'm awake!"); delay(100); } void recieveSensorData() { if (myserial.available() > 0) { //if we see that the EZO product has sent a character. char inchar = (char)myserial.read(); //get the char we just received sensorstring += inchar; if (inchar == '\r') { sensor_stringcomplete = true; //if the incoming character is a <CR>, set the flag n++; } } if (sensor_stringcomplete) { //if a string from the EZO product has been received //in its entirety dataRec = createDataRecord(); if (n >= nSkip) //don’t log sensor startup response { logData(dataRec); //Save the data record to the log file } Serial.print(" "); Serial.println(dataRec); Serial.print("n= "); Serial.println(n); sensorstring = ""; //clear the string: sensor_stringcomplete = false; //reset the flag used to tell if we have received a } } String createDataRecord() { //Create a String type data record in csv format //TimeDate, Loggertime,Temp_DS, Diff1, Diff2, boardtemp String data = getDateTime(); data += ","; rtc.convertTemperature(); //convert current temperature into registers boardtemp = rtc.getTemperature(); //Read temperature sensor value batterysenseValue = analogRead(batteryPin); batteryvoltage = (3.3 / 1023.) * 4.7 * batterysenseValue; //4.7 for rev 0.5 data += currentepochtime; data += ","; addFloatToString(data, boardtemp, 3, 1); //float data += ","; addFloatToString(data, batteryvoltage, 4, 2); data += " , "; //adds a comma between values Temp0 = getTemp(); addFloatToString(data, Temp0, 4, 2); //float data += ","; //correct EC to 25C reference using data from Hanna 1413uS standard EC = sensorstring.toFloat(); alpha = K2 * pow(Temp0, 2) + K1 * Temp0 + K0; EC25 = EC / (1 + alpha * (Temp0 - 25)); addFloatToString(data, EC25, 4, 1); //data += sensorstring; //needs to be last record added to dataRecord //due to built in cr/lf return data; } void showTime(uint32_t ts) { //Retrieve and display the current date/time String dateTime = getDateTime(); //Serial.println(dateTime); } void setupTimer() { //Schedule the wakeup every minute timer.every(1, showTime); //Instruct the RTCTimer how to get the current time reading timer.setNowCallback(getNow); } void wakeISR() { //Leave this blank } void setupSleep() { pinMode(RTC_PIN, INPUT_PULLUP); attachInterrupt(2, wakeISR, CHANGE); //this used "PcInt::" for old board //Setup the RTC in interrupt mode rtc.enableInterrupts(RTC_INT_PERIOD); //Set the sleep mode set_sleep_mode(SLEEP_MODE_PWR_DOWN); } void systemSleep() { //This method handles any sensor specific sleep setup sensorsSleep(); //Wait until the serial ports have finished transmitting Serial.flush(); // Serial1.flush(); //why?? //The next timed interrupt will not be sent until this is cleared rtc.clearINTStatus(); //Disable ADC ADCSRA &= ~_BV(ADEN); //Sleep time noInterrupts(); sleep_enable(); interrupts(); sleep_cpu(); sleep_disable(); //Enbale ADC ADCSRA |= _BV(ADEN); //This method handles any sensor specific wake setup sensorsWake(); } String getDateTime() { String dateTimeStr; //Create a DateTime object from the current time DateTime dt(rtc.makeDateTime(rtc.now().getEpoch())); currentepochtime = (dt.get()); //Unix time in seconds currentminute = (dt.minute()); //Convert it to a String dt.addToString(dateTimeStr); return dateTimeStr; } uint32_t getNow() { currentepochtime = rtc.now().getEpoch(); return currentepochtime; } void greenred4flash() { for (int i = 1; i <= 4; i++) { digitalWrite(8, HIGH); digitalWrite(9, LOW); delay(50); digitalWrite(8, LOW); digitalWrite(9, HIGH); delay(50); } digitalWrite(9, LOW); } void setupLogFile() { //Initialise the SD card if (!SD.begin(SD_SS_PIN)) { Serial.println("Error: SD card failed to initialise or is missing."); //Hang // while (true); } //Check if the file already exists bool oldFile = SD.exists(FILE_NAME); //Open the file in write mode File logFile = SD.open(FILE_NAME, FILE_WRITE); //Add header information if the file did not already exist if (!oldFile) { logFile.println(LOGGERNAME); logFile.println(DATA_HEADER); } //Close the file to save it logFile.close(); } void logData(String rec) { //Re-open the file File logFile = SD.open(FILE_NAME, FILE_WRITE); //Write the CSV data logFile.println(rec); //Close the file to save it logFile.close(); } static void addFloatToString(String & str, float val, char width, unsigned char precision) { char buffer[10]; dtostrf(val, width, precision, buffer); str += buffer; } float getTemp() { float Temp; Temp = analogRead(A0); //Temp = Temp * K_temp; for LM35 Temp = m * Temp + b; return Temp; } |
Will be happy to talk to anyone about this project at the water 102 seminar tomorrow and Wednesday.