/* Solar water heater differential controller based on ESP-12 Copyright(c) 2016 Nigel Morton nigel@ntpworld.co.uk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions : The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #define DEBUG /*-----( Import needed libraries )-----*/ #include #include //needed for WiFiManager library #include //https://github.com/tzapu/WiFiManager #include #include // Comes with Arduino IDE #include #include #include #include #include #include "Solar.h" //Variables for keyboard #define inDebounceCount 5 int in0BounceCount; int in0Value = 1; int in12BounceCount; int in12Value = 1; int in14BounceCount; int in14Value = 1; #define __builtin_va_start #define __builtin_va_end //Format and output string to serial debug port void SerialPrint2(char *format, ...) { char buff[128]; va_list args; va_start(args, format); vsnprintf(buff, sizeof(buff), format, args); va_end(args); buff[sizeof(buff) / sizeof(buff[0]) - 1] = '\0'; Serial.print(buff); } #ifdef DEBUG #define DEBUG_PRINT(x) Serial.print (x) #define DEBUG_PRINTST(x,...) SerialPrint2 (x,__VA_ARGS__) #define DEBUG_PRINTLN(x) Serial.println (x) #else #define DEBUG_PRINT(x) #define DEBUG_PRINTST(x,...) #define DEBUG_PRINTLN(x) #endif //If you want to connect to the device as an AP to configure your WiFi leave this defined. If not comment it out and use hard coded values #define SOFTAP //Set the port and WiFi parameters if you want to hard code your WiFi connection #define ETHPORT 8081 #define SSID "MY_SSID" // insert your SSID #define PASS "MY_PASSWORD" // insert your password ESP8266WebServer server(ETHPORT); // HTTP server will listen at port ETHPORT //Timers for functions Ticker tmrReadDS18B20, tmrStart18B20, tmrRefreshDisplay, tmrReadKeys, tmrProcessKeys; bool bRequestSensors; bool bReadSensors; bool bUpdateDisplay; bool bReadKeys; bool bProcessKeys; // set the LCD address to 0x207 for a 20 chars 4 line display //From https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library LiquidCrystal_I2C lcd(0x27, 16, 2); // Digital pin to use for the 1-wire sensor bus #define ONE_WIRE_BUS 2 // Initialize OneWire OneWire one_wire(ONE_WIRE_BUS); DallasTemperature sensors(&one_wire); // OneWire Sensor addresses for your devices DeviceAddress tank_sensor = { 0x28, 0xE9, 0x0F, 0x31, 0x04, 0x00, 0x00, 0x97 };//#3 DeviceAddress panel_sensor = { 0x28, 0x1c, 0x37, 0x31, 0x4, 0x0, 0x0, 0xb4 };//#6 void readKeys(void); int readCount = 0; String strHTML; void setup() /*----( SETUP: RUNS ONCE )----*/ { char result[16]; Serial.begin(115200); //Setup LCD display. Prompt to connect to AP if not connected // NOTE: Cursor Position: (CHAR, LINE) start at 0 Wire.begin(4, 5); // Initialize I2C SDA/SCL - ESP has SDA on GPIO4 and SCL on GPIO5 lcd.begin(); // initialize the lcd for 16 chars 2 lines, turn on backlight lcd.backlight(); // Turn backlight on #if defined SOFTAP sprintf(result, "Connect to AP"); lcd.setCursor(0, 0); lcd.print(result); // Print string on line 1 of LCD. sprintf(result, "SolarWater"); lcd.setCursor(0, 1); lcd.print(result); // Print blank line on line 2 of LCD. //WiFiManager //Local intialization. Once its business is done, there is no need to keep it around WiFiManager wifiManager; //reset saved settings (for testing) //wifiManager.resetSettings(); //fetches ssid and pass from eeprom and tries to connect if it does not connect it starts an access point with the name SolarWater //and goes into a blocking loop awaiting configuration wifiManager.autoConnect("SolarWater"); //if you get here you have connected to the WiFi Serial.println("connected...yeey :)"); #else //Include this if you want to fix your connection with defines WiFi.begin(SSID, PASS); // Connect to WiFi network while (WiFi.status() != WL_CONNECTED) { // Wait for connection delay(500); Serial.print("."); } #endif //Temperature to defrost the panel EEPROM.begin(1); defrostTemperature = EEPROM.read(DEFROSTTEMP) - DEFROST_OFFSET; if (defrostTemperature > 10) { defrostTemperature = 2; EEPROM.write(DEFROSTTEMP, defrostTemperature + DEFROST_OFFSET); EEPROM.commit(); } //Upper temperature EEPROM.begin(1); deltaOn = EEPROM.read(DELTAON); if ((deltaOn > 20) || (deltaOn < 5)) { deltaOn = 10; EEPROM.write(DELTAON, deltaOn); EEPROM.commit(); } //Lower temperature EEPROM.begin(1); deltaOff = EEPROM.read(DELTAOFF); if ((deltaOff > 20) || (deltaOff < 2)) { deltaOff = 2; EEPROM.write(DELTAOFF, deltaOff); EEPROM.commit(); } digitalWrite(PUMP, PUMP_OFF); pinMode(PUMP, OUTPUT); // sets the digital pin as output //Inputs for keyboard pinMode(0, INPUT_PULLUP); pinMode(12, INPUT_PULLUP); pinMode(14, INPUT_PULLUP); //Set up and start the DS18B20 sensors - MUST setup as output or sensors don't work! digitalWrite(ONE_WIRE_BUS, HIGH); // <- incantation code: same number as OneWire port pinMode(ONE_WIRE_BUS, OUTPUT); //<- incantation code: same number as OneWire port sensors.begin(); //Briefly display the IP address sprintf(result, "SolarWater IP"); lcd.setCursor(0, 0); lcd.print(result); // Print string on line 1 of LCD. sprintf(result, "%d.%d.%d.%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]); lcd.setCursor(0, 1); lcd.print(result); // Print blank line on line 2 of LCD. Serial.println(); Serial.println(result); requestSensorStart(); //Read the temperature sensors delay(2000); //Start timers for system functions tmrStart18B20.attach(15, startReadSensors); //Start timer to start temp sensor read tmrRefreshDisplay.attach(1, updateDisplay); //Timer to update LCD tmrReadKeys.attach(1, doReadKeys); //Timer for keyboard reads tmrProcessKeys.attach_ms(25, doProcessKeys); //Process keypresses // Set up the endpoints for HTTP server, Endpoints can be written as inline functions: server = ESP8266WebServer(ETHPORT); server.on("/", []() { buildWebPage(); server.send(200, "text/html", strHTML); }); server.on("/ajax_inputs", []() { buildXML(); server.send(200, "text/xml", strHTML); }); server.begin(); // Start the server defrostState = FALSE; //No defrost }/*--(end setup )---*/ //Turn pump (solid state relay) on/off void turnPump(bool pState) { digitalWrite(PUMP, pState); } //LOOP: RUNS CONSTANTLY int keyValue; void loop() { if (lcd_page == 0) { //Control the on/off time subject to the temperature difference to keep the difference as high as possible if ((pumpState == PUMP_ON) && (defrostState == FALSE)) turnPump((tempPanel - tempTank) >= pumpStep / 2 ? PUMP_ON : PUMP_OFF); if (defrostState == FALSE) { if (((tempPanel - tempTank) >= deltaOn)) { pumpState = PUMP_ON; turnPump(pumpState); } if (((tempPanel - tempTank) <= deltaOff)) { pumpState = PUMP_OFF; turnPump(pumpState); } } else { defrostDelayCount = millis() - defrostStartTime; if ((keyValue == 4) || (defrostDelayCount > 60000)) //1 minute { defrostState = FALSE; pumpState = PUMP_OFF; turnPump(pumpState); lastDefrost = 120; } } //Check if we are at defrost temperature. Add 20 as it removes need for handling negative numbers if ((tempPanel + 20 <= defrostTemperature + 20)) if ((lastDefrost == 0) && (defrostState == FALSE)) { defrostStartTime = millis(); defrostState = TRUE; pumpState = PUMP_ON; turnPump(pumpState); } } server.handleClient(); // checks for incoming tcp messages //See if any of the flags have been set (by the timers) to go perform functions. //Functions should NOT be in timer interrupts, only setting flags. if(bReadKeys == true) readKeys(); if (bProcessKeys == true) processKeys(); //Process key presses //Sensors if (bRequestSensors == true) requestSensorStart(); //Start a conversion if (bReadSensors == true) requestReadSensors(); //Read sensor values (timer for this is started after requestSensorStart has been called) //Refresh LCD if (bUpdateDisplay == true) displayLCD(); }// Forever loop. //Request the sensors to perform temperature calculation void requestSensorStart(){ bRequestSensors = false; sensors.requestTemperatures(); yield(); tmrReadDS18B20.attach(1.5, readSensors); //Start timer to kick off sensor reading } //Read sensors void requestReadSensors() { int sensorReading; bReadSensors = false; tmrReadDS18B20.detach(); sensorReading = sensors.getTempC(panel_sensor); if (sensorReading != -127) tempPanel = sensorReading; sensorReading = sensors.getTempC(tank_sensor); if (sensorReading != -127) tempTank = sensorReading; } //////////////////////////////////////////////// //Flags triggered by timers to perform functions //////////////////////////////////////////////// //Kick off a sensor read request and start the read timer void startReadSensors() {bRequestSensors = true;} //Read the sensors void readSensors() { bReadSensors = true; } //Read the sensors void updateDisplay() { bUpdateDisplay = true; } //Check if key presses void doReadKeys() { bReadKeys = true; } //Read the keyboard void doProcessKeys() { bProcessKeys = true; } //Read keyborad for key presses int processKeys(void) { if (keyValue != 0) { Serial.print("Key="); Serial.println(keyValue); if (keyValue == 1) { lcd_page = lcd_page++ >= LCD_PAGES ? 0 : lcd_page; //Cycle the lcd page count } if (keyValue == 2) { if (lcd_page == 1) { deltaOn = deltaOn-- <= 2 ? 2 : deltaOn; EEPROM.begin(1); EEPROM.write(DELTAON, deltaOn); EEPROM.commit(); } if (lcd_page == 2) { deltaOff = deltaOff-- <= 2 ? 2 : deltaOff; EEPROM.begin(1); EEPROM.write(DELTAOFF, deltaOff); EEPROM.commit(); } if (lcd_page == 3) { defrostTemperature = defrostTemperature-- <= 6 ? defrostTemperature : -5; EEPROM.begin(1); EEPROM.write(DEFROSTTEMP, defrostTemperature + DEFROST_OFFSET); EEPROM.commit(); } if (lcd_page == 4) { pumpState = PUMP_OFF; turnPump(pumpState); } } if (keyValue == 3) { if (lcd_page == 1) { deltaOn = deltaOn++ >= 21 ? 2 : deltaOn; EEPROM.begin(1); EEPROM.write(DELTAON, deltaOn); EEPROM.commit(); } if (lcd_page == 2) { deltaOff = deltaOff++ >= 21 ? 2 : deltaOff; EEPROM.begin(1); EEPROM.write(DELTAOFF, deltaOff); EEPROM.commit(); } if (lcd_page == 3) { defrostTemperature = defrostTemperature++ >= 6 ? -5 : defrostTemperature; EEPROM.begin(1); EEPROM.write(DEFROSTTEMP, defrostTemperature); EEPROM.commit(); } if (lcd_page == 4) { pumpState = PUMP_ON; turnPump(pumpState); } } } return keyValue; } // Update LCD with latest data void displayLCD(void){ bUpdateDisplay = false; tmrRefreshDisplay.detach(); char line1[20]; // used to store strings if (lcd_page == LCD_PAGE_MAIN) { if (tempPanel == -127) { sprintf(line1, "Panel:ERROR"); } else if (tempTank == -127) { sprintf(line1, "Tank:ERROR"); } else { sprintf(line1, "Panel:%2d Tank:%2d", tempPanel, tempTank); } lcd.setCursor(0, 0); lcd.print(line1); // Print string on line 1 of LCD. if (defrostState == PUMP_OFF) sprintf(line1, "Pump:%-12s", pumpState ? "ON" : "OFF"); else sprintf(line1, "Pump:%s Def:%-5d", pumpState ? "ON" : "OFF", 60 - (int)(defrostDelayCount / 1000)); lcd.setCursor(0, 1); lcd.print(line1); } else { switch (lcd_page) { case LCD_PAGE_DELTAON: sprintf(line1, "Delta on:%-7d", deltaOn); break; case LCD_PAGE_DELTAOFF: sprintf(line1, "Delta off:%-6d", deltaOff); break; case LCD_PAGE_DEFROST: sprintf(line1, "Defrost:%-6d", defrostTemperature); break; case LCD_PAGE_PUMP: sprintf(line1, "Pump:%-7s", pumpState ? "ON" : "OFF"); break; } lcd.setCursor(0, 0); lcd.print(line1); // Print string on line 1 of LCD. lcd.setCursor(0, 1); sprintf(line1, "%15s", " "); lcd.print(line1); // Print blank line on line 2 of LCD. } if (lcd_page == LCD_PAGE_MAIN) tmrRefreshDisplay.attach(1, updateDisplay); //Timer to update LCD else tmrRefreshDisplay.attach_ms(50, updateDisplay); //Timer to update LCD } // Build the XML file with status information for AJAX update to the web page void buildXML() { char buf[5]; char t[10]; strHTML = ""; sprintf(buf, "%s", pumpState ? "ON" : "OFF"); strHTML += buf; strHTML += ""; if (tempPanel == -127) { strHTML += "ERROR"; } else { sprintf(t, "%d", tempPanel); strHTML += t; } strHTML += ""; if (tempTank == -127) { strHTML += "ERROR"; } else { sprintf(t, "%d", tempTank); strHTML += t; } strHTML += ""; sprintf(t, "%d", readCount); strHTML += (t); strHTML += ""; strHTML += (defrostState == PUMP_OFF ? "Off" : "Running"); strHTML += ""; readCount++; } //Build main web page void buildWebPage() { Serial.println("Return web page"); strHTML = "\r\n"; strHTML += "\r\n"; strHTML += "Solar hot water controller status\r\n"; strHTML += "\r\n"; strHTML += "\r\n"; strHTML += "\r\n"; //strHTML += "\r\n"; strHTML += "Solar status
\r\n"; strHTML += "Panel: ...
\r\n"; strHTML += "Tank: ...
\r\n"; strHTML += "Pump: ...
\r\n"; strHTML += "Defrost: ...
\r\n"; strHTML += "DeltaOn:"; char t[10]; sprintf(t, "%d", deltaOn); strHTML += t; strHTML += "
DeltaOff:"; sprintf(t, "%d", deltaOff); strHTML += t; strHTML += "
Defrost:"; sprintf(t, "%d", defrostTemperature); strHTML += t; strHTML += "

timestamp: ...

\r\n"; strHTML += "\r\n\r\n"; } // Read keyboard. Debounce keys and if a key is pressed save the key value //GPIO14 = 1 //GPIO12 = 2 //GPIO0 = 3 void readKeys(void) { keyValue = 0; //No keys pressed so return 0 //GPIO 12 (Key 2) if (in12Value != digitalRead(12)) { in12BounceCount++; } else { in12BounceCount = 0; } if (in12BounceCount > inDebounceCount) { in12BounceCount = 0; in12Value = digitalRead(12); // Return key value if (in12Value) { keyValue = 2; } } if (in14Value != digitalRead(14)) in14BounceCount++; else in14BounceCount = 0; if (in14BounceCount > inDebounceCount) { in14BounceCount = 0; in14Value = digitalRead(14); // Return key value if (in14Value) { keyValue = 1; } } if (in0Value != digitalRead(0)) in0BounceCount++; else in0BounceCount = 0; if (in0BounceCount > inDebounceCount) { in0BounceCount = 0; in0Value = digitalRead(0); // Return key value if (in0Value) { keyValue = 3; } } }