/* * First implementation of the aaquaponics project on esp8266 with arduino * - Get information from DHT sensor * - Logging on thingspeak * - The use of timers allows to optimize and creat time based environment * - Configuration from serial link * - Configuration saved in eeprom * * * Developed from : * https://github.com/atwc/ESP8266-arduino-demos * Use the librairies * - DHT * - Timer in user_interface * * Comments: * - Relays are ON with 0 * Led are ON with 0 * * Serial Link commandes is composed by one letter and the value : YXXXXXXX * Commande detail : * - L > change start time of LAMP * - D > change day time duration of LAMP * - R > change pump flooding frequence * - F > change flooding duration * - P > Change the wifi pasword * - S > Change the SSID * - W > Write parameters in eeprom" * - T > Reset timing * - H > Change thingspeak channel * * * TODO * - Add NTP time read function * - See for start time up counter only when water is up * - See for start next filling action timer when the water is down * - Add config from internet * - Use a table for thingspeak inputs * - Check for use of hardware interrupt for water sensor, May no be needed * */ #include "hardware_def.h" #include "aquaponics.h" #include #include #include extern "C" { #include "user_interface.h" } #define DEBUG 1 //Use DEBUG to reduce the time 1 minute = 5s //Serial Config #define BAUDRATE 115200 // Serial link speed //Timer elements os_timer_t myTimer1; os_timer_t myTimer2; os_timer_t myTimer3; // boolean to activate timer events bool tick1Occured; bool tick2Occured; bool tick3Occured; // Need to have a global address for the timer id const char tickId1=1; const char tickId2=2; const char tickId3=3; // Functions declaration void timersSetup(void); void timerCallback(void *); void timerInit(os_timer_t *pTimerPointer, uint32_t milliSecondsValue, os_timer_func_t *pFunction, char *pId); //Wifi config //Function declarations void wifiConnect(void); //Wifi Global variables WiFiClient client; #define MAX_CONNEXION_TRY 50 //Function declarations void thingSpeakWrite (String, unsigned long, float, float, float, float, float, float, float); //Sensors config DHT dht(DHTPIN, DHT22,15); //Application config AquaponicsConfig conf; unsigned long getMacAddress(void); void ioInits(void); void printInfo(void); void waterControl(void); //Application values float temperature; float humidity; byte waterLevelState = 0; // water level status memory // Time counter variables volatile int hoursCounter; // 0 to 23 volatile int secondsCounter; // 0 to 59 volatile int minutesCounter; // 0 to 59 //EEPROM Function declarations void eepromWrite(void); void eepromRead(void); //EEPROM Global variables bool needSave; // Serial string management variables void executeCommand(); void serialStack(); String inputString = ""; // a string to hold incoming data boolean stringComplete = false; // whether the string is complete //Web parameters Function declarations void checkWebValues(void); void getConfFromWeb(void); //Web parameters Variables byte webDayStart = 0; // Hour of the day that the lamp starts byte webDayTime = 0; // Number of hours of daylight (LAMP ON) byte webPumpFreq = 0; // Time between 2 pump cycles byte webFloodedTime = 0; // Time of water at high level /* void setup(void) * Setup software run only once * */ void setup() { //Start Serial Serial.begin(BAUDRATE); Serial.println(""); Serial.println("--------------------------"); Serial.println(" ESP8266 Full Test "); Serial.println("--------------------------"); timersSetup(); ioInits(); conf.mac = getMacAddress(); eepromRead(); hoursCounter = DAY_START; // Init the time to DAY_START at startup minutesCounter = 1; digitalWrite(LAMP, HIGH); Serial.println("------"); //Init wifi and web server wifiConnect(); //Init DHT temperature and Himidity reading dht.begin(); } /* void loop(void) * Main program automatically loaded * */ void loop() { //Check if a timer occured to execute the action //Timer 1 action every seconds if (tick1Occured == true){ tick1Occured = false; //Toggle LED digitalWrite(RED_LED, !digitalRead(RED_LED)); } //Check if eeprom parameters need to be saved if (needSave){ needSave = 0; eepromWrite(); } //Check for serial input serialStack(); //Execute recived command executeCommand(); //Timer 2 action every minutes if (tick2Occured == true){ tick2Occured = false; minutesCounter ++; //Increment the minutes counter //check the config values on web checkWebValues(); // Every 60min increment the hours counter if (0 == minutesCounter % 60) { hoursCounter ++; // Increments the hours counter minutesCounter = 0; // Clears the minutes counter // At the end of the day reset the hours counter if (23 < hoursCounter) { hoursCounter = 0; // Clear the hours counter } // Execute following code once every hour //Check if the lamp has to be turn on or off if (conf.dayStart == hoursCounter) digitalWrite(LAMP, 1); if ((conf.dayStart + conf.dayTime) == hoursCounter) digitalWrite(LAMP, 0); } printInfo(); } //Timer 3 action every 10mn if (tick3Occured == true){ tick3Occured = false; //Temperature and Humidity Reading humidity = dht.readHumidity(); temperature = dht.readTemperature(); thingSpeakWrite ( conf.thingspeakApi, conf.mac, temperature, humidity, NAN, conf.dayStart, conf.dayTime, conf.pumpFreq, conf.floodedTime); } waterControl(); //Give th time to th os to do his things yield(); // or delay(0); } /* void timerSetup(void *pArg) * Setup all timers * * Input : * Output : */ void timersSetup(void) { //Init and start timers tick1Occured = false; tick2Occured = false; tick3Occured = false; if(DEBUG) { //Reduce timing for test and debug timerInit(&myTimer1, 1000, timerCallback, (char*)&tickId1); timerInit(&myTimer2, 1000*5, timerCallback, (char*)&tickId2); timerInit(&myTimer3, 1000*60, timerCallback, (char*)&tickId3); } else { //Normal timing timerInit(&myTimer1, TIMER1, timerCallback, (char*)&tickId1); timerInit(&myTimer2, TIMER2, timerCallback, (char*)&tickId2); timerInit(&myTimer3, TIMER3, timerCallback, (char*)&tickId3); } } /* void timerCallback(void *pArg) * Function called by the os_timers at every execution * Only one function is used for all timers, the timer id comm in the pArg * * Input : * Output : */ void timerCallback(void *pArg) { char timerId = *(char*)pArg; //Value inside (*) of pArg, casted into a char pointer switch (timerId){ case 1 : tick1Occured = true; break; case 2 : tick2Occured = true; break; case 3 : tick3Occured = true; break; default : //Nothings to do break; } } /* timerInit(os_timer_t *pTimerPointer, uint32_t milliSecondsValue, os_timer_func_t *pFunction, char *pId) * Start and init all timers * * Input : * Output : */ void timerInit(os_timer_t *pTimerPointer, uint32_t milliSecondsValue, os_timer_func_t *pFunction, char *pId) { /* Maximum 7 timers os_timer_setfn - Define a function to be called when the timer fires void os_timer_setfn(os_timer_t *pTimer, os_timer_func_t *pFunction, void *pArg) Define the callback function that will be called when the timer reaches zero. The pTimer parameters is a pointer to the timer control structure. The pFunction parameters is a pointer to the callback function. The pArg parameter is a value that will be passed into the called back function. The callback function should have the signature: void (*functionName)(void *pArg) The pArg parameter is the value registered with the callback function. */ os_timer_setfn(pTimerPointer, pFunction, pId); /* os_timer_arm - Enable a millisecond granularity timer. void os_timer_arm( os_timer_t *pTimer, uint32_t milliseconds, bool repeat) Arm a timer such that is starts ticking and fires when the clock reaches zero. The pTimer parameter is a pointed to a timer control structure. The milliseconds parameter is the duration of the timer measured in milliseconds. The repeat parameter is whether or not the timer will restart once it has reached zero. */ os_timer_arm(pTimerPointer, milliSecondsValue, true); } /* void wifiConnect(void) * Try to connect to the wifi, stop after a time without success * * Input : None * Output : None */ void wifiConnect(void){ int wifiErrorCount; // Connect to WiFi network Serial.println(); Serial.println("Connecting to "); Serial.println(conf.ssid); Serial.println("Connecting to "); Serial.println(conf.password); WiFi.begin(conf.ssid, conf.password); wifiErrorCount = 0; while (WiFi.status() != WL_CONNECTED and wifiErrorCount < MAX_CONNEXION_TRY ) { delay(500); Serial.print("."); wifiErrorCount++; } Serial.println(""); if (wifiErrorCount < MAX_CONNEXION_TRY) { Serial.println("WiFi connected"); } else { Serial.println("WiFi not connected"); } } /* void thingSpeakWrite(void) * Write data to thingspeak server * TODO Use a table as input * * Input : APIKey - the write api key from the channel * fieldX - every channel values or NAN if not used * Output : */ void thingSpeakWrite (String APIKey, unsigned long field1, float field2, float field3, float field4, float field5, float field6, float field7, float field8) { const char* thingspeakServer = "api.thingspeak.com"; if (client.connect(thingspeakServer,80)) { // "184.106.153.149" or api.thingspeak.com String postStr = APIKey; if (!isnan(field1)) { postStr +="&field1="; postStr += String(field1); } if (!isnan(field2)) { postStr +="&field2="; postStr += String(field2); } if (!isnan(field3)) { postStr +="&field3="; postStr += String(field3); } if (!isnan(field4)) { postStr +="&field4="; postStr += String(field4); } if (!isnan(field5)) { postStr +="&field5="; postStr += String(field5); } if (!isnan(field6)) { postStr +="&field6="; postStr += String(field6); } if (!isnan(field7)) { postStr +="&field7="; postStr += String(field7); } if (!isnan(field8)) { postStr +="&field8="; postStr += String(field8); } postStr += "\r\n\r\n"; client.print("POST /update HTTP/1.1\n"); client.print("Host: api.thingspeak.com\n"); client.print("Connection: close\n"); client.print("X-THINGSPEAKAPIKEY: "+String(conf.thingspeakApi)+"\n"); client.print("Content-Type: application/x-www-form-urlencoded\n"); client.print("Content-Length: "); client.print(postStr.length()); client.print("\n\n"); client.print(postStr); } client.stop(); } /* void printInfo(void) * Print all application info on the serial link * * Input : * Output : */ void printInfo(void) { Serial.print("----->>>>>> Time : "); Serial.print(hoursCounter); Serial.print(":"); Serial.print(minutesCounter); Serial.print(" MAC : "); Serial.println(String(conf.mac,HEX)); Serial.print("Flood every "); Serial.print(conf.pumpFreq); Serial.print("mn for "); Serial.print(conf.floodedTime); Serial.print("mn."); Serial.print(" Start the lamp at "); Serial.print(conf.dayStart); Serial.print("h for "); Serial.print(conf.dayTime); Serial.println("h."); Serial.print("Water level : "); Serial.print(waterLevelState); Serial.print(" PI["); Serial.print(digitalRead(PUMP_IN)); Serial.print("] PO["); Serial.print(digitalRead(PUMP_OUT)); Serial.print("] L["); Serial.print(digitalRead(LAMP)); Serial.print("] U["); Serial.print(digitalRead(WATER_UP)); Serial.print("] D["); Serial.print(digitalRead(WATER_DOWN)); Serial.println("]"); Serial.print("Temperature : "); Serial.print(temperature); Serial.print(" - Humidity : "); Serial.println(humidity); } /* void ioInits(void) * Init all Inputs and Outputs for the application * * Input : * Output : */ void ioInits(void) { //Init leds pinMode(BLUE_LED, OUTPUT); pinMode(RED_LED, OUTPUT); digitalWrite(BLUE_LED, HIGH); digitalWrite(RED_LED, HIGH); // Outputs for relays pinMode(LAMP, OUTPUT); digitalWrite(LAMP, HIGH); pinMode(FOG, OUTPUT); digitalWrite(FOG, HIGH); pinMode(PUMP_IN, OUTPUT); digitalWrite(PUMP_IN, HIGH); pinMode(PUMP_OUT, OUTPUT); digitalWrite(PUMP_OUT, HIGH); //Sensore inputs pinMode(WATER_UP, INPUT_PULLUP); pinMode(WATER_DOWN, INPUT_PULLUP); } /* void waterControl(void) * Control the pumps depending of the actual water state * * Input : * Output : */ void waterControl(void) { switch (waterLevelState) { case DOWN: //Wait for pump frequence time to activate pump if(0 == (minutesCounter % PUMP_FREQ)) { digitalWrite(PUMP_IN, 0); // Turn ON the filling pump digitalWrite(PUMP_OUT, 1); // Turn OFF the clearing pump Serial.println("Turn ON the finning pump"); waterLevelState = FILLING; } break; case FILLING: // wait for water up sensor to be activated if(1 == digitalRead(WATER_UP)) { digitalWrite(PUMP_IN, 1); // Turn OFF the filling pump digitalWrite(PUMP_OUT, 1); // Turn OFF the clearing pump Serial.println("Turn OFF the finning pump"); waterLevelState = UP; } break; case UP: //Wait for level up time passed to clear if(0 == ((minutesCounter % PUMP_FREQ) % WATER_UP_TIME)) { digitalWrite(PUMP_IN, 1); // Turn OFF the filling pump digitalWrite(PUMP_OUT, 0); // Turn ON the clearing pump Serial.println("Turn ON the clearing pump"); waterLevelState = CLEARING; } break; break; case CLEARING: // wait for water down sensor to be activated if(0 == digitalRead(WATER_DOWN)) { digitalWrite(PUMP_IN, 1); // Turn OFF the filling pump digitalWrite(PUMP_OUT, 1); // Turn OFF the clearing pump Serial.println("Turn OFF the clearing pump"); waterLevelState = DOWN; } break; default: // default is optional break; } } /* void eepromWrite(void) * Write all application parameters in eeprom * * Input : * Output : */ void eepromWrite(void) { char letter; int i, addr; //Activate eeprom EEPROM.begin(512); Serial.println("Save application parameters in eeprom"); // save wifi ssid in eeprom addr = eeAddrSSID; for (i = 0 ; i < eeSizeSSID ; i++) { EEPROM.write(addr, conf.ssid[i]); if('\0' == conf.ssid[i]) break; addr++; } // save wifi password in eeprom addr = eeAddrPASS; for (i = 0 ; i < eeSizePASS ; i++) { EEPROM.write(addr, conf.password[i]); if('\0' == conf.password[i]) break; addr++; } // save thingspeak api in eeprom addr = eeAddrTSAPI; for (i = 0 ; i < eeSizeTSAPI ; i++) { EEPROM.write(addr, conf.thingspeakApi[i]); if('\0' == conf.thingspeakApi[i]) break; addr++; } EEPROM.write(eeAddrDayStart, conf.dayStart); // Hour of the day that the lamp starts EEPROM.write(eeAddrDayTime, conf.dayTime); // Number of hours of daylight (LAMP ON) EEPROM.write(eeAddrPumpFreq, conf.pumpFreq); // Time between 2 pump cycles EEPROM.write(eeAddrFloodedTime, conf.floodedTime); // Time of water at high level EEPROM.end(); } /* void eepromRead(void) * Read all application parameters from eeprom * * Input : * Output : */ void eepromRead(void) { char letter; int i, addr; //Activate eeprom EEPROM.begin(512); // Get wifi SSID from eeprom addr = eeAddrSSID; for (i = 0 ; i < eeSizeSSID ; i++) { conf.ssid[i] = EEPROM.read(addr); if('\0' == conf.ssid[i]) break; addr++; } // Get wifi PASSWORD from eeprom addr = eeAddrPASS; for (i = 0 ; i < eeSizePASS ; i++) { conf.password[i] = EEPROM.read(addr); if('\0' == conf.password[i]) break; addr++; } // Get thingspeak api from eeprom addr = eeAddrTSAPI; for (i = 0 ; i < eeSizeTSAPI ; i++) { conf.thingspeakApi[i] = EEPROM.read(addr); if('\0' == conf.thingspeakApi[i]) break; addr++; } conf.dayStart = EEPROM.read(eeAddrDayStart); // Hour of the day that the lamp starts conf.dayTime = EEPROM.read(eeAddrDayTime); // Number of hours of daylight (LAMP ON) conf.pumpFreq = EEPROM.read(eeAddrPumpFreq); // Time between 2 pump cycles conf.floodedTime = EEPROM.read(eeAddrFloodedTime); // Time of water at high level Serial.println("Application parameters read from eeprom"); printInfo(); } /* void serialStack(void) * Stack all serial char received in one string until a '\n' * Set stringComplete to True when '\n' is received * This implementation is good enough for this project because serial commands * will be send slowly. */ void serialStack() { if(Serial.available()) { while (Serial.available()) { // get the new byte: char inChar = (char)Serial.read(); // if the incoming character is a newline, set a flag if (inChar == '\n' or inChar == '\r') {// so the main loop can do something about it inputString += '\0'; stringComplete = true; } else {// add it to the inputString inputString += inChar; } } } } /* void executeCommand(void) * Execute received serial command * Commandes list : * - "i", "I", "info", "Info" : Return basic system informations, mainly for debug purpose * - "FXX" : Setup the minimum temperature delta to activate the Airflow (0 to 100 C) * - "fXX" : Setup the maximum temperature delta to disable the Airflow (0 to 100 C) * - "S1" or "S0" : Enable ("S1") or disable ("S0") the summer mode * */ void executeCommand() { if (stringComplete) { // Define the appropriate action depending on the first character of the string switch (inputString[0]) { // INFO Request case 'i': case 'I': printInfo(); // Print on serial the system info break; // Day start time case 'l': case 'L': inputString.remove(0,1); Serial.print("L > change start time of LAMP to : "); Serial.println(inputString); conf.dayStart = byte(inputString.toInt()); break; // Day duration time case 'd': case 'D': inputString.remove(0,1); Serial.print("D > change day time duration of LAMP to : "); Serial.println(inputString); conf.dayTime = byte(inputString.toInt()); break; // Flooding pump frequency case 'R': case 'r': inputString.remove(0,1); Serial.print("P > change pump flooding frequence to : "); Serial.println(inputString); conf.pumpFreq = byte(inputString.toInt()); break; // Flooding duration case 'f': case 'F': inputString.remove(0,1); Serial.print("F > change flooding duration to : "); Serial.println(inputString); conf.floodedTime = byte(inputString.toInt()); break; // PASSWORD case 'P': case 'p': inputString.remove(0,1); Serial.print("P > Change the wifi pasword to : "); Serial.println(inputString); strcpy (conf.password, inputString.c_str()); break; // SSID case 'S': case 's': inputString.remove(0,1); Serial.print("S > Change the SSID to : "); Serial.println(inputString); strcpy (conf.ssid, inputString.c_str()); break; // Thingspeak channel case 'H': case 'h': inputString.remove(0,1); Serial.print("H > Change the thingspeak write api to : "); Serial.println(inputString); strcpy (conf.thingspeakApi, inputString.c_str()); break; // Write command case 'W': case 'w': inputString.remove(0,1); Serial.println("W > Write parameters in eeprom"); eepromWrite(); break; // Reset default timing config case 'T': case 't': Serial.println("T > Reset timing"); conf.dayStart = DAY_START; conf.dayTime = DAY_TIME; conf.pumpFreq = PUMP_FREQ; conf.floodedTime = WATER_UP_TIME; break; // Reset Wifi Credential // Reboot // Ignore (in case of /r/n) case '\r': case '\n': case '\0': break; // Unknown command default: Serial.print(inputString[0]); Serial.println(" > ?"); } inputString = ""; stringComplete = false; } } /* void executeCommand(void) * Get the board mac address * * Input : * Output : result mac address as int for id use */ unsigned long getMacAddress() { byte mac[6]; unsigned long intMac = 0; unsigned long intMacConv = 0; //TODO see for full mac (6 numbers not 4) WiFi.macAddress(mac); for (int i = 0; i < 4; ++i) { intMacConv = mac[i]; intMac = intMac + (intMacConv <<(8*i)); //Serial.println(String(i)); //Serial.println(String(intMacConv<<(8*i),HEX)); //Serial.println(String(intMac, HEX)); } return intMac; } /* void getConfFromWeb(void) * Download config file from webserver * * TODO: improve the seach pattern * TODO Make it not stoping the app (dedicated timer app?) */ void getConfFromWeb(void) { const char* host = "nebulair.co"; String parametersString; char parameters[32]; //Serial.print("connecting to "); //Serial.println(host); // Use WiFiClient class to create TCP connections WiFiClient client; const int httpPort = 80; if (!client.connect(host, httpPort)) { Serial.print("connecting to "); Serial.print(host); Serial.println(" failed"); webDayStart = 0; webDayTime = 0; webPumpFreq = 0; webFloodedTime = 0; return; } // We now create a URI for the request String url = "/config-files/conf.310366044.txt"; //Serial.print("Requesting URL: "); //Serial.println(url); // This will send the request to the server client.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n"); delay(500); // Read all the lines of the reply from server and print them to Serial while(client.available()){ String line = client.readStringUntil('\r'); if (-1 != line.indexOf('>')){ parametersString = line.substring(line.indexOf('>')+1, line.indexOf('<')); parametersString.toCharArray(parameters, parametersString.length()+1); //Convert values webDayStart = atoi(strtok(parameters, ";")); webDayTime = atoi(strtok(NULL, ";")); webPumpFreq = atoi(strtok(NULL, ";")); webFloodedTime = atoi(strtok(NULL, ";")); //Serial.println(parameters); //Serial.println(webDayStart); //Serial.println(webDayTime); //Serial.println(webPumpFreq); //Serial.println(webFloodedTime); //Stop looking break; } } //Serial.println(); //Serial.println("closing connection"); } /* void checkWebValues(void) * Chack and update config from web * * TODO Make it not stoping the app */ void checkWebValues(void) { getConfFromWeb(); //Check if parameters need to be adjusted if( webDayStart != 0 & webDayTime != 0& webPumpFreq != 0 & webFloodedTime != 0) { //Paremeters are valid, check if they are new if( webDayStart != conf.dayStart| webDayTime != conf.dayTime| webPumpFreq != conf.pumpFreq| webFloodedTime != conf.floodedTime) { //New parameters, Update values conf.dayStart = webDayStart; conf.dayTime = webDayTime; conf.pumpFreq = webPumpFreq; conf.floodedTime = webFloodedTime; eepromWrite(); Serial.println("Config Updated from web"); } } }