/* Version 1.0.0 scraped for new design using recirc valve alone to control flow to growbed Version 1.1.0 Started over with just code for pH probe. This code was directly from Atlas Scientific. Version 1.1.1 Assigned float for pH. version 1.2.0 Added logic for fill and drain functionality. Version 1.2.1 Added "hasFlood" integer triggered by exceeding "floodThreshold". This was needed because erronious readings from drainSensor caused this to be a one shot.. this ensures that we truly have a drain situation before recirculating. Version 1.3.0 Added Water level probe function following lessons learned from the flood/drain situation. Version 1.4.0 Added ability to correct pH during acid/baseOn and acid/baseOff window. Adjust these windows to add more or less acid/base for correction. Version 1.4.1 Added a temperature average function to smooth out temperature readings over a second. Version 1.4.2 Added temperature compensation to pH circuit Version 1.4.3 Removed response code from ph circuit (circuit was returning *OK everytime we sent it the new temperature. The arduino saw this as the current pH and base pump would run) Version 1.4.4 Increased Drain threshold to be sure grow bed does drain ( 100 - 1000) Version 1.5.0 Added SD capabilities. Will add another version once I get it to initialize Version 1.5.1 Completed SD capabilities logging an ID, timestamp (in millis), pH, and Temperature to LOG.CSV file Version 1.6.0 Adding Web Server capabilities Version 1.6.1 Added more values to be saved in csv file ID, Time In Cycle (mS), pH, Temperature, Acid Pump Status, Base Pump Status, Recirc Status, Drain Sensor, Fill Sensor, Fill Status Version 2.0.0 Removed self filling capabilities. This enables me to allow for faster pH corrections since the system naturally buffers itself after 5 minutes and it takes 3 minutes for dosing to become apparent in THIS system. Changing total cycle to 5 minutes to allow dosing to mix within system and for pH to equalize. Version 2.1.0 Changed main cycle time to 10 minutes since new growbed medium requires more flow time to fill. Version 2.2.0 Added Real Time Clock capabilities (very buggy) Version 2.2.1 Bug fixes for Real Time Clock capabilities. Also added more expainations to most variables. I was bored on an international flight to Hong Kong from Chicago Version 3.0.0 Added online data storage capabilities through PHP and MySQL. Website to follow. This will allow for monitoring anywhere. Version 3.0.1 Moved data logging to the loop and added a timer to upload data to MySQL only ever 5 seconds to reduce the amount of data sent to the server. Current settings will use approximately 600MB in a year. ***NOTE 8/3/2015 webpage for system is online and operating Version 3.0.2 Using MYSQL current time and date variables to perform time and date stamps. This should take some load off of the Controller Version 3.1.0 Noticed that after 12 hours or so the Data logging lags over the web. Suspect memory capacity on Arduino is being used due to high amounts of data logging. Added an auto reset function to automatically reset the controller ever 21600000 milliseconds (6 hours) Version 3.1.1 Changed reset value to 4 hours instead of 6 Version 3.2.0 Removed reset function. Decided to try only posting to php once instead of twice for each table. Will modify the add.php on the webserver to query the database to both tables instead of a separate php file and multiple lines of code on Arduino. This, hopefully, should eliminate the memory issues and conflicts. */ // WHAT LIBRARIES ARE TO BE USED #include #include #include #include #include #include #include // LETS SETUP SOME VALUES // TIMING VARIABLES const int timeZone = -5; // WHAT TIME ZONE SET TO Central Daylight Time unsigned long previousRate = 0; // ASSIGNS 0 TO PREVIOUS RATE ON INITIALIZATION unsigned long previousMillis = 0; // ASSIGNS 0 TO PREVIOUS TIMING ON INTIALIZATION unsigned long previouspH = 0; // ASSIGNS 0 TO PREVIOUS PH TIMING ON INITIALIZATION unsigned long previousLogRate = 0; // ASSIGNS 0 TO PREVIOUS LOG RATE ON INITIALIZATION const long acidOn = 278000; // MILLISECONDS IN CYCLE TO ENABLE ACID DOSING (IF NEEDED) const long acidOff = 290000; // MILLISECONDS IN CYCLE TO DISABLE ACID DOSING (IF NEEDED) const long acidOn2 = 578000; const long acidOff2 = 590000; const long baseOn = 570000; // MILLISECONDS IN CYCLE TO ENABLE BASE DOSING (IF NEEDED) const long baseOff = 590000; // MILLISECONDS IN CYCLE TO DISABLE BASE DOSING (IF NEEDED) const long rate = 1000; // MILLISECONDS BETWEEN TEMPERATURE READINGS const long logRate = 5000; // MILLISECONDS BETWEEN WRITING NEW LINE TO CSV LOGFILE //const long fillOn = 601000; // MILLISECONDS IN CYCLE TO ENABLE WATER FILLING (IF NEEDED) //const long fillOff = 899999; // MILLISECONDS IN CYCLE TO DISABLE WATER FILLING (IF NEEDED) const long recircOn = 301000; // MILLISECONDS IN CYCLE TO ENABLE WATER RECIRCULATION (IF NEEDED) const long recircOff = 899999; // MILLISECONDS IN CYCLE TO DISABLE WATER RECIRCULATION (IF NEEDED) const long cycle = 600000; // MILLISECONDS TOTAL IN MAIN CYCLE const long pHcycle = 600000; // MILLISECONDS TOTAL IN PH CYCLE //NETWORKING VARIABLES byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED}; // MAC ADDRESS OF ETHERNET CARD IPAddress ip(192, 168, 2, 50); // IP ADDRESS OF ETHERNET CARD IPAddress timeServer(132, 163, 4, 101); // time-a.timefreq.bldrdoc.gov // IPAddress timeServer(132, 163, 4, 102); // time-b.timefreq.bldrdoc.gov UNSWITCH COMMENT TO ACTIVATE THIS SERVER // IPAddress timeServer(132, 163, 4, 103); // time-c.timefreq.bldrdoc.gov UNSWITCH COMMENT TO ACTIVATE THIS SERVER EthernetUDP Udp; unsigned int localPort = 8888; // local port to listen for UDP packets EthernetClient client; char server[] = "myserver"; // IP Address (or name) of server to dump data to // I/O VARIABLES const int acidPump = 44; // ACID PUMP OUTPUT ON PIN 44 const int basePump = 42; // BASE PUMP OUTPUT ON PIN 44 const int recirc = 48; // RECIRC VALVE OUTPUT ON PIN 48 const int drainSensor = 49; // DRAIN SENSOR INPUT ON PIN 49 const int levelSensor = 47; // LEVEL SENSOR INPUT ON PIN 47 const int fillValve = 46; // FILL VALVE OUTPUT ON const int resetPin = 40; // AUTO RESET PIN Thermistor temp(0); // THERMISTOR 1 IS CONNECTED TO ANALOG PIN 0 int CS_pin = 10; // CHIP SELECTOR FOR SD CARD IS PIN 10 // MATH VARIABLES int acidStatus = 0; // IS ACID PUMP RUNNING int baseStatus = 0; // IS BASE PUMP RUNNING int recircStatus = 0; // IS WATER RECIRCULATING int fillStatus = 0; // IS FILL VALVE OPEN int hasFlood = 0; // HAS THERE BEEN A GROWBED FLOOD WITHIN THE MAIN CYCLE int drainSensorVal = 0; // ACCUMULATION OF DRAIN VALUES WORKS WITH floodThreshold int levelSensorVal = 0; // ACCUMULATION OF LEVEL VALUES WORKS WITH fullThreshold int fillEnable = 0; // ARE WE WITHIN THE FILL WINDOW OF TIME int recircEnable = 0; // ARE WE WITHIN THE RECIRCULATION WINDOW OF TIME int waterDetect = 0; // DEFAULT VALUE FOR DRAIN DETECTION int floodThreshold = 1500; // HOW HIGH DOES drainSensorVal HAVE TO BE TO DECLARE FLOOD IN GROWBED int fullDetect = 0; // DEFAULT VALUE FOR FILL DETECTION int fullThreshold = 5; // HOW HIGH DOES levelSensorVal HAVE TO BE TO DECLARE FULL int isFull = 0; // HAS FULL THRESHOLD BEEN SATISFIED int temperature = 0; // PLACE HOLDER FOR TEMPERATURE READINGS (IN *C) int tempReadings = 0; // ACCUMULATION OF NUMBER OF TEMPERATURE READINGS IN "RATE" float tempTotal = 0; // MATHEMATICAL ACCUMULATION OF TEMERATURE READINGS float tempAverage = 0; // DIVIDEND OF tempTotal/tempReadings float gtemperature = 0; // PLACE HOLDER FOR GROW BED TEMPERATURE READINGS (IN *C) int gtempReadings = 0; // ACCUMULATION OF NUMBER OF TEMPERATURE READINGS IN "RATE" FOR GROW BED float gtempTotal = 0; // MATHEMATICAL ACCUMULATION OF GROW BED TEMPERATURE READINGS float gtempAverage = 0; // DIVIDEND OF gtempTotal/gtempReadings int hourRaw = 0; int hourCorrected = 0; int minuteRaw = 0; int minuteCorrected = 0; int secondRaw = 0; int secondCorrected = 0; int gtemp = 0; float ph; // CREATION OF A PH FLOAT float dissolved = 0; // CRATEION OF A DISSOLVED OXYGEN FLOAT***this circuit does not exist...YET!**** int acidEnable = 0; // ARE WE WITHIN THE ACID DOSING WINDOW OF TIME int baseEnable = 0; // ARE WE WITHIN THE BASE DOSING WINDOW OF TIME int waterDetectFail = 0; // IF YOU HAD WATER DETECTION BUT IT DIDNT LAST LONG ENOUGH TO SATISFY floodThreshold THIS BECOMES 1 float acidThreshold = 7.10; // KEEP PH BELOW THIS VALUE BUT... float baseThreshold = 6.70; // ABOVE THIS VALUE long id = 1; // IDENTIFICATION OF EACH LINE IN CSV FILE String astatus = ""; // STRING FOR RUN/OFF FOR ACID PUMP FOR MYSQL String bstatus = ""; // STRING FOR RUN/OFF FOR BASE PUMP FOR MYSQL String rstatus = ""; // STRING FOR RECIRC/FLOOD FOR RECIRC FOR MYSQL String dsensor = ""; // STRING FOR WATER/DRY FOR DRAIN FOR MYSQL String fsensor = ""; // STRING FOR WATER/DRY FOR FILL FOR MYSQL String fstatus = "Off"; // STRING FOR FILL/OFF FOR DRAIN FOR MYSQL //ATLAS SCIENTIFIC VALUES 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 time_t prevDisplay = 0; // when the digital clock was displayed void setup(){ // set up the hardware // OUTPUTS pinMode(acidPump, OUTPUT); pinMode(basePump, OUTPUT); pinMode(recirc, OUTPUT); pinMode(resetPin, OUTPUT); pinMode(fillValve, OUTPUT); //INPUTS pinMode(drainSensor, INPUT); pinMode(levelSensor, INPUT); Serial.begin(9600); //set baud rate for the hardware serial port_0 to 9600 Serial3.begin(9600); //set baud rate for software serial port_3 to 9600 inputstring.reserve(5); //set aside some bytes for receiving data from the PC sensorstring.reserve(30); //set aside some bytes for receiving data from Atlas Scientific product Serial.println("Initializing Card"); Serial.println("McMillin Aquaponics version 3.2.0"); //disable w5100 SPI while setting up SD pinMode(CS_pin,OUTPUT); digitalWrite(CS_pin,HIGH); // set up SD if(SD.begin(4) == 0) Serial.println("Card Initialization Failed"); else Serial.println("Card Initialization Successful!"); // set up w5100 if (Ethernet.begin(mac) == 0) { // no point in carrying on, so do nothing forevermore: while (1) { Serial.println("Failed to configure Ethernet using DHCP"); delay(10000); } } Serial.print("IP number assigned by DHCP is "); Serial.println(Ethernet.localIP()); Udp.begin(localPort); Serial.println("waiting for sync"); setSyncProvider(getNtpTime); ////////////////////////////////////////////// // takes a second for the w5100 to get ready delay(1000); Serial.println("Setup done"); Serial3.println("RESPONSE,0/r"); //Write Log.csv File Header File logFile = SD.open("LOG.csv", FILE_WRITE); if (logFile) { logFile.println(", , ,"); //Just a leading blank line, incase there was previous data logFile.println("Date, Time, pH, Temperature, Acid Pump Status, Base Pump Status, Recirc Status, Drain Sensor, Fill Sensor, Fill Status"); logFile.close(); } else { Serial.println("Couldn't open log file"); return; } } 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 , set the flag } void serialEvent3(){ //if the hardware serial port_3 receives a char char inchar = (char)Serial3.read(); //get the char we just received sensorstring += inchar; //add it to the inputString if(inchar == '\r') {sensor_stringcomplete = true;} //if the incoming character is a , set the flag } void loop(){ //here we go.... unsigned long currentRate=millis(); unsigned long currentMillis=millis(); unsigned long currentpH=millis(); unsigned long currentLogRate=millis(); drainSensorVal = digitalRead(drainSensor); levelSensorVal = digitalRead(levelSensor); { if (timeStatus() != timeNotSet) { if (now() != prevDisplay) { //update the display only if time has changed prevDisplay = now(); digitalClockDisplay(); } } } if ((currentLogRate - previousLogRate) >= logRate){ String dataString1 = String(ph) + ", " + String(tempAverage) + ", " + String(acidStatus) + ", " + String(baseStatus) + ", " + String(recircStatus) + ", " + String(drainSensorVal) + ", " + String(levelSensorVal) + ", " + String(fillStatus); File logFile = SD.open("LOG.csv", FILE_WRITE); while(logFile) { logFile.print(month()); logFile.print("/"); logFile.print(day()); logFile.print("/"); logFile.print(year()); logFile.print(", "); logFile.print(hour()); logFile.print(":"); logFile.print(minute()); logFile.print(":"); logFile.print(second()); logFile.print(","); logFile.println(dataString1); logFile.close(); } } if ((currentLogRate - previousLogRate) >= logRate && (client.connect(server, 80))) { client.print( "GET /add.php?"); client.print("date=CURDATE()"); //client.print(month()); // client.print("-"); // client.print(day()); // client.print("-"); // client.print(year()); Serial.println("Debug 1"); client.print("&&"); client.print("time=CURTIME()"); // client.print(hour()); // client.print(":"); // client.print(minute()); // client.print(":"); // client.print(second()); Serial.println("Debug 2"); client.print("&&"); client.print("ph="); client.print( ph ); Serial.println("Debug 3"); client.print("&&"); client.print("dissolved="); client.print( dissolved ); Serial.println("Debug 4"); client.print("&&"); client.print("wtemp="); client.print(tempAverage); Serial.println("Debug 5"); client.print("&&"); client.print("gtemp="); client.print( gtemp ); Serial.println("Debug 6"); client.print("&&"); client.print("astatus="); client.print( astatus ); Serial.println("Debug 7"); client.print("&&"); client.print("bstatus="); client.print( bstatus ); Serial.println("Debug 8"); client.print("&&"); client.print("rstatus="); client.print( rstatus ); Serial.println("Debug 9"); client.print("&&"); client.print("dsensor="); client.print( dsensor ); Serial.println("Debug 10"); client.print("&&"); client.print("fsensor="); client.print( fsensor ); Serial.println("Debug 11"); client.print("&&"); client.print("fstatus="); client.print( fstatus ); Serial.println("Debug 12"); client.println( " HTTP/1.1"); client.println( "Host: myserver" ); //client.println( "Content-Type: application/x-www-form-urlencoded" ); //client.println( "Connection: close" ); client.println(); client.println(); client.stop(); Serial.println("Debug COMPLETE"); client.flush(); previousLogRate = currentLogRate; } /*if ((currentMillis - previousMillis) >= fillOn && (currentMillis - previousMillis) <= fillOff) { fillEnable = 1; } else { fillEnable = 0; }*/ if ((currentMillis - previousMillis) >= recircOn && (currentMillis - previousMillis) <= recircOff){ recircEnable = 1; } else { recircEnable = 0; } if (((currentpH - previouspH) >= acidOn && (currentpH - previouspH) <= acidOff) || ((currentpH - previouspH) >= acidOn2 && (currentpH - previouspH) <= acidOff2)){ acidEnable = 1; } else { acidEnable = 0; } if ((currentpH - previouspH) >= baseOn && (currentpH - previouspH) <= baseOff){ baseEnable = 1; } else { baseEnable = 0; } if (baseEnable == 1 && ph < baseThreshold) { digitalWrite(basePump, HIGH); baseStatus = 1; bstatus = "Run"; // Serial.println("Base Pump Running"); } else if (acidEnable == 1 && ph > acidThreshold) { digitalWrite(acidPump, HIGH); acidStatus = 1; astatus = "Run"; // Serial.println("Acid Pump Running"); } else { digitalWrite(acidPump, LOW); digitalWrite(basePump, LOW); baseStatus = 0; acidStatus = 0; astatus = "Off"; bstatus = "Off"; } if (hasFlood == 1 || recircEnable == 1){ digitalWrite(recirc, HIGH); recircStatus = 1; rstatus = "Recirc"; //Serial.println("Recirculating"); } else {digitalWrite(recirc, LOW); recircStatus = 0; rstatus = "Flood"; //Serial.println("Filling Grow Bed"); } /*if (isFull ==0 && fillEnable == 1){ digitalWrite(fillValve, HIGH); fillStatus = 1; fstatus = "Fill"; Serial.println("Demanding Water"); } else{ digitalWrite (fillValve, LOW); fillStatus = 0; fstatus = "Off"; }*/ if (levelSensorVal == 0) { fullDetect = fullDetect + 1; fsensor = "Water"; } else { fullDetect = 0; fsensor = "Dry"; } if (fullDetect > fullThreshold) { isFull = 1; } //Serial.print("drainSensor: "); //Serial.print(drainSensorVal); //Serial.print(" hasFlood: "); //Serial.println(hasFlood); /* Serial.print(" fillEnable: "); Serial.println(fillEnable); */ if (drainSensorVal == 0){ dsensor = "Water"; } else { dsensor = "Dry"; } if (drainSensorVal == 0 && hasFlood < 1){ waterDetect++; waterDetectFail = 0; Serial.print(" waterDetect: "); Serial.print(waterDetect); } else { waterDetectFail++; } if (waterDetectFail >=10) { waterDetect = 0; } if (waterDetect > floodThreshold) { hasFlood = 1; } temperature = temp.getTemp(); tempTotal = tempTotal + temperature; tempReadings = tempReadings + 1; if ((currentRate-previousRate) > rate){ tempAverage = 0; tempAverage = tempTotal / tempReadings; float rounded_up = ceilf(tempAverage * 100) / 100; Serial.print("Water Temp: "); Serial.print(rounded_up); Serial.println("*F"); Serial3.print("T,"); Serial3.print(rounded_up); Serial3.print("\r"); previousRate=currentRate; tempTotal = 0; tempReadings = 0; } if (input_stringcomplete){ //if a string from the PC has been received in its entirety Serial3.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 (sensor_stringcomplete){ //if a string from the Atlas Scientific product has been received in its entierty Serial.print("pH: "); Serial.println(sensorstring); //send that string to to the PC's serial monitor // char floatbuf[8]; //sensorstring.toCharArray(floatbuf,sizeof(floatbuf)); //float ph = atof(floatbuf); ph = (atof(sensorstring.c_str())); Serial.print("pH float: "); Serial.println(ph); sensorstring = ""; //clear the string: sensor_stringcomplete = false; //reset the flag used to tell if we have received a completed string from the Atlas Scientific product } if (currentMillis - previousMillis >= cycle) { previousMillis = currentMillis; hasFlood = 0; isFull = 0; waterDetect = 0; } if (currentpH - previouspH >= pHcycle) { previouspH = currentpH; } /* Serial.print("Time: "); Serial.print((currentMillis-previousMillis)); Serial.print(" ph: "); Serial.print(ph); Serial.print(" acidEnable: "); Serial.print(acidEnable); Serial.print(" baseEnable: "); Serial.println(baseEnable); */ } void digitalClockDisplay(){ // digital clock display of the time //sensorValue = analogRead(sensorPin); //float temp = getTemp(); //Serial.println( temp ); //Serial.println(sensorValue); //while(!logFile) //{ //Serial.println("Couldn't access file"); //} //increment ID # id++; } void printDigits(int digits){ // utility for digital clock display: prints preceding colon and leading 0 Serial.print(":"); if(digits < 10) Serial.print('0'); Serial.print(digits); } /*-------- NTP code ----------*/ const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets time_t getNtpTime() { while (Udp.parsePacket() > 0) ; // discard any previously received packets Serial.println("Transmit NTP Request"); sendNTPpacket(timeServer); uint32_t beginWait = millis(); while (millis() - beginWait < 1500) { int size = Udp.parsePacket(); if (size >= NTP_PACKET_SIZE) { Serial.println("Receive NTP Response"); Udp.read(packetBuffer, NTP_PACKET_SIZE); // read packet into the buffer unsigned long secsSince1900; // convert four bytes starting at location 40 to a long integer secsSince1900 = (unsigned long)packetBuffer[40] << 24; secsSince1900 |= (unsigned long)packetBuffer[41] << 16; secsSince1900 |= (unsigned long)packetBuffer[42] << 8; secsSince1900 |= (unsigned long)packetBuffer[43]; return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR; } } Serial.println("No NTP Response :-("); return 0; // return 0 if unable to get the time } // send an NTP request to the time server at the given address void sendNTPpacket(IPAddress &address) { // set all bytes in the buffer to 0 memset(packetBuffer, 0, NTP_PACKET_SIZE); // Initialize values needed to form NTP request // (see URL above for details on the packets) packetBuffer[0] = 0b11100011; // LI, Version, Mode packetBuffer[1] = 0; // Stratum, or type of clock packetBuffer[2] = 6; // Polling Interval packetBuffer[3] = 0xEC; // Peer Clock Precision // 8 bytes of zero for Root Delay & Root Dispersion packetBuffer[12] = 49; packetBuffer[13] = 0x4E; packetBuffer[14] = 49; packetBuffer[15] = 52; // all NTP fields have been given values, now // you can send a packet requesting a timestamp: Udp.beginPacket(address, 123); //NTP requests are to port 123 Udp.write(packetBuffer, NTP_PACKET_SIZE); Udp.endPacket(); }