/* * CanGrow - simply DIY automatic plant grow system (for cannabis). * */ /* * Includes * */ // Libraries // https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/SPI #include // https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/Wire #include // https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/EEPROM #include // https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi #include #include // https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer #include // https://github.com/adafruit/Adafruit-GFX-Library #include // https://github.com/adafruit/Adafruit_SSD1306 #include // https://github.com/adafruit/DHT-sensor-library #include "DHT.h" // https://github.com/bblanchon/ArduinoJson #include // https://github.com/arduino-libraries/NTPClient #include // https://github.com/PaulStoffregen/Time #include /* * * Variables * */ // valSoilmoisture - contains the value of getSoilmoisture() byte valSoilmoisture; // valTemperature - contains the value of getTemperature() float valTemperature; // valTemperature - contains the value of getHumidity() float valHumidity; // valWaterlevel - contains the value of getWaterlevel() // do we need a restart? (e.g. after wifi settings change) bool NeedRestart; bool FirstRun; /* * EEPROM saved variables */ // // WiFi // // if empty, CanGrow start in AccessPoint mode char WIFIssid[32]; char WIFIpassword[64]; // WIFIuseDHCP - if true, get IP by DHCP bool WIFIuseDHCP; IPAddress WIFIip(192,168,4,20); IPAddress WIFInetmask(255,255,255,0); IPAddress WIFIgateway(192,168,4,254); IPAddress WIFIdns(0,0,0,0); char WebUiUsername[16] = "cangrow"; char WebUiPassword[32] = "cangrow"; // // System // // configured - if false, let the user configure system settings first bool configured = false; // NTP Offset int ntpOffset; // MoistureSensor_Type - contains which moisture sensor to use // 1: analog capacitive sensor // 2: I2C chirp sensor from catnip electronics byte MoistureSensor_Type; // SoilmoistureLow - contains the value , when soil moisture is assumed to be low, byte SoilmoistureLow = 20; // UsePump - is the pump used? bool bool UsePump; // UseFan - is the fan used? bool byte PumpOnTime = 3; bool UseFan; // In case the user uses no 12V LED on the LED output and an relais instead // we have to disable PWM. So we ask here for what kind of light user is going bool UseLEDrelais; // Which temperature sensor to use? byte TemperatureSensor_Type; // // Grow Stuff // // GrowName - contains the name of the grow/plant. Up to 32 byte // if empty, let the user setup grow settings first char GrowName[32]; // GrowStart - contains unix timestamp from date where grow starts (00:00) // unsigned long is 8 byte unsigned long GrowStart; // DayOfGrow contains on which day the grow is byte DayOfGrow; // DaysVeg - contains how many days to be in vegetation phase byte DaysVeg = 35; // DaysBloom - contains how many days to be in bloom phase byte DaysBloom = 49; // LighthoursVeg - contains how many hours the Growlight is on in Veg byte LighthoursVeg = 16; // LighthoursBloom - contains how many hours the Growlight is on in Bloom byte LighthoursBloom = 12; // SunriseHour - contains to which hour of day the growlight turns on byte SunriseHour = 7; // SunriseHour - contains to which minute of SunriseHour the growlight turns on byte SunriseMinute = 0; // PINled_PWM - contains the PWM value for dimming the grow light // default is 255 to ensure it is just on for the case UseLEDrelais is true byte PINled_PWM = 255; /* * * Constants * */ /* * WiFi */ const char* APssid = "CanGrow-unconfigured"; /* * TODO - does not work atm. idk why. * const char* APpass = "CanGrow"; const int APchannel = 6; const bool APhidden = false; * */ /* * NTP */ WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP); /* * * Webserver * */ ESP8266WebServer webserver(80); /* * HTML constants for header, footer, css, ... */ // Template: const char HTMLexamplepage[] PROGMEM = R"EOF()EOF"; const char HTMLheader[] PROGMEM = R"EOF( CanGrow
)EOF"; const char HTMLfooter[] PROGMEM = R"EOF(
)EOF"; /* * I decided to embed CSS into HTMLheader because after * a wifi change triggerd restart of the esp, the the style.css never get deliverd * because the esp restarts after the webpage was delivered. style.css would be a second call * but when the browser sends this, it's too late. const char HTMLstyleCSS[] PROGMEM = R"EOF( )EOF"; */ const char HTMLsuccess[] PROGMEM = R"EOF(
Successfully saved!
)EOF"; const char HTMLneedRestart[] PROGMEM = R"EOF(
Restart is required to apply new WiFi settings!
)EOF"; const char HTMLhelp[] PROGMEM = R"EOF(

CanGrow help

Here you will get some helpful help. )EOF"; const char JSreplaceStr[] PROGMEM = R"EOF( )EOF"; const char JSconvertDateToEpoch[] PROGMEM = R"EOF( )EOF"; /* * * Pin assignments * * D0 - MOSFET Fan * D1, D2 - I2C * D3 - DHT11 * D4 - PIN_WIPE * D5 - MOSFET Pump * D6 - MOSFET Grow LED, PWM * D7 - waterlevel (set HIGH to read value) * D8 - analog soil moisture (set HIGH to read value) * A0 - analog input for soil moisture and waterlevel readings * * D4 and D7 cannot be HIGH at the same time! */ // D0 is HIGH at boot, no PWM const uint8_t PINfan = D0; // If D3 is pulled to LOW, boot fails const uint8_t PINdht = D3; // D4 is HIGH at boot, boot fail if pulled to LOW // During Start Screen you can pull D4 to LOW to wipe saved data in EEPROM // DO NOT PULL D4 DOWN AT WHEN POWERING ON !!! BOOT WILL FAIL const uint8_t PIN_WIPE = D4; const uint8_t PINpump = D5; const uint8_t PINled = D6; // const uint8_t PINwaterlevel = D7; const uint8_t PINsoilmoisture = D8; const uint8_t PINanalog = A0; /* * millis timer * */ unsigned long outputPrevTime = 0; /* * Status vars * */ int D6status = false; /* I2C Stuff * */ #define WIRE Wire /* * DHT Stuff * */ #define DHTTYPE DHT11 DHT dht(PINdht, DHTTYPE); /* * Display Stuff */ Adafruit_SSD1306 display = Adafruit_SSD1306(128, 32, &WIRE); // 'CanGrow_Logo', 128x32px const unsigned char bmpCanGrow_Logo [] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x1f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x38, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x70, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x03, 0x00, 0x00, 0x00, 0x04, 0x07, 0xe0, 0x20, 0x60, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x03, 0x00, 0x00, 0x00, 0x06, 0x07, 0xe0, 0xe0, 0x60, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x03, 0x87, 0xe1, 0xc0, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x3f, 0xc3, 0xff, 0x03, 0xc7, 0xe3, 0xc0, 0xcf, 0xf9, 0xff, 0xe3, 0xfc, 0xc1, 0x83, 0x30, 0x00, 0x7f, 0xe3, 0xff, 0x83, 0xe7, 0xe7, 0xc0, 0xcf, 0xfb, 0xff, 0xe7, 0xfe, 0xc3, 0x87, 0x30, 0x00, 0xe0, 0x73, 0x80, 0xc1, 0xf7, 0xef, 0xc0, 0xc0, 0x1b, 0x80, 0x0e, 0x03, 0xc3, 0x86, 0x30, 0x00, 0xc0, 0x33, 0x00, 0xc1, 0xff, 0xff, 0x80, 0xc0, 0x1b, 0x00, 0x0c, 0x03, 0xc7, 0x8e, 0x30, 0x01, 0xc0, 0x37, 0x00, 0xc0, 0xff, 0xff, 0x80, 0xc0, 0x3b, 0x00, 0x1c, 0x03, 0xc7, 0x8c, 0x60, 0x01, 0xc0, 0x37, 0x00, 0xc0, 0xff, 0xff, 0x01, 0x80, 0x3f, 0x00, 0x18, 0x03, 0xcf, 0x9c, 0x60, 0x00, 0x00, 0x37, 0x00, 0xc0, 0x7f, 0xfe, 0x01, 0x80, 0x37, 0x00, 0x18, 0x03, 0xcf, 0x9c, 0x60, 0x00, 0x00, 0x76, 0x01, 0xc0, 0x1f, 0xfc, 0x01, 0x80, 0x36, 0x00, 0x18, 0x06, 0xdf, 0xb8, 0x60, 0x00, 0x7f, 0xe6, 0x01, 0x9f, 0x9f, 0xfc, 0xf9, 0x80, 0x36, 0x00, 0x18, 0x06, 0xdd, 0xb8, 0x60, 0x00, 0xff, 0xe6, 0x01, 0x87, 0xff, 0xff, 0xf1, 0x80, 0x76, 0x00, 0x18, 0x06, 0xdd, 0xb0, 0xc0, 0x01, 0xc0, 0xee, 0x01, 0x83, 0xff, 0xff, 0xc3, 0x00, 0x7e, 0x00, 0x30, 0x06, 0xf9, 0xf0, 0xc0, 0x0b, 0x80, 0x6e, 0x01, 0x81, 0xff, 0xff, 0x83, 0x00, 0x6e, 0x00, 0x30, 0x06, 0xf9, 0xe0, 0xc0, 0x1b, 0x00, 0xec, 0x01, 0x80, 0x1f, 0xf8, 0x03, 0x00, 0x6c, 0x00, 0x30, 0x0e, 0xf1, 0xe0, 0xc0, 0x3b, 0x00, 0xcc, 0x03, 0x80, 0x3f, 0xfc, 0x03, 0x00, 0xec, 0x00, 0x30, 0x0c, 0xf1, 0xc0, 0xc0, 0x7b, 0x01, 0xcc, 0x03, 0x00, 0x7f, 0xfe, 0x03, 0x01, 0xec, 0x00, 0x30, 0x1c, 0xe1, 0xc0, 0x7f, 0xf1, 0xff, 0xdc, 0x03, 0x00, 0xf0, 0x8f, 0x01, 0xff, 0xfc, 0x00, 0x1f, 0xf8, 0xe1, 0xc0, 0x3f, 0xe0, 0xff, 0xcc, 0x03, 0x00, 0x00, 0x80, 0x00, 0xff, 0xcc, 0x00, 0x0f, 0xf0, 0xc1, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // Array of all bitmaps for convenience. (Total bytes used to store images in PROGMEM = 528) const int bmpallArray_LEN = 1; const unsigned char* bmpallArray[1] = { bmpCanGrow_Logo }; /* * * * Functions * * */ /* * Chirp functions */ void writeI2CRegister8bit(int addr, int value) { Wire.beginTransmission(addr); Wire.write(value); Wire.endTransmission(); } unsigned int readI2CRegister16bit(int addr, int reg) { Wire.beginTransmission(addr); Wire.write(reg); Wire.endTransmission(); delay(20); Wire.requestFrom(addr, 2); unsigned int t = Wire.read() << 8; t = t | Wire.read(); return t; } /* * * Sensor functions * */ int getWaterlevel() { /* * waterlevelRAW * =========== * 0 - 199 : CRITICAL * 200 - 399 : WARNING * >400 : OK * * waterlevel * ========== * 2 : CRITICAL * 1 : WARNING * 0 : OK */ byte waterlevelWARN = 200; byte waterlevelOK = 400; byte waterlevelRAW = 0; byte waterlevel = 0; // enable Vcc for water level sensor digitalWrite(PINwaterlevel, HIGH); // wait a bit to let the circuit stabilize // TODO: replace delay() with millis() delay(100); // get the value waterlevelRAW = analogRead(PINanalog); // disable Vcc for the sensor to prevent electrolysis effect and release analog pin digitalWrite(PINwaterlevel, LOW); if( waterlevelRAW >= waterlevelOK) { waterlevel = 0; } else if( waterlevelRAW >= waterlevelWARN) { waterlevel = 1; } else { waterlevel = 2; } return waterlevel; } float getTemperature(byte tempSensor) { /* * tempSensor * ========== * 1 : DHT11 temp sensor * 2 : chirp I2C temp sensor */ float temperature = 0; switch(tempSensor) { case 1: // read temperature from DHT11 temperature = dht.readTemperature(); break; case 2: // read temperature from chrip I2C temperature = readI2CRegister16bit(0x20, 5) * 0.10 ; break; default: // if sensor type is not recognized, return 99 temperature = 99.99; } return temperature; } float getHumidity() { float humidity = dht.readHumidity(); return humidity; } int getSoilmoisture(byte moistureSensor) { /* * moistureSensor * ============== * 1 : analog capacitive moisture sensor * 2 : chirp I2C moisture sensor */ // value to return int soilmoisture; // value for wet int wet; // value for dry int dry; switch(moistureSensor) { case 1: // read analog value from analog moisture sensor wet = 180; dry= 590; digitalWrite(PINsoilmoisture, HIGH); // wait a bit to let the circuit stabilize delay(100); // get analog input value soilmoisture = analogRead(PINanalog); // disable Vcc for the sensor to release analog pin digitalWrite(PINsoilmoisture, LOW); break; case 2: // read soil moisture from chrip I2C wet = 560; dry= 250; // get raw value from I2C chirp sensor soilmoisture = readI2CRegister16bit(0x20, 0); break; default: wet = 0; dry = 1; soilmoisture = -1; } return map(soilmoisture, wet, dry, 100, 0); } int getLightchirp() { // get the "light value" from I2C chirp module writeI2CRegister8bit(0x20, 3); //request light measurement int lightchirp = readI2CRegister16bit(0x20, 4); return lightchirp; } void wipeEEPROM() { Serial.println(":: wipe EEPROM ::"); // wipeMsg is helper variable to know if the Serial.print Message was // already sent byte wipeMsg = 0; while(digitalRead(PIN_WIPE) == LOW ) { // only show the Serial message once if(wipeMsg == 0) { Serial.println("Please release PIN_WIPE to erase all data saved in EEPROM"); Serial.println("LAST CHANCE TO KEEP THE DATA BY RESETTING NOW!!"); // increase i to show the serial message only once wipeMsg = 1; } delay(500); } // write a 0 to all 512 bytes of the EEPROM Serial.print("wiping EEPROM... "); for (int i = 0; i < 512; i++) { EEPROM.write(i, 0); } // commit everything to EEPROM and end here EEPROM.end(); Serial.println("DONE"); // set D4 PIN_WIPE internal LED to Output to give feedback WIPE // was done pinMode(PIN_WIPE, OUTPUT); Serial.println("!! Device will restart in 3 seconds !!"); // let the internal led blink fast to signalize wipe is done for(byte i = 0; i <= 24 ; i++) { if(i % 2) { digitalWrite(PIN_WIPE, LOW); } else { digitalWrite(PIN_WIPE, HIGH); } delay(125); } ESP.restart(); } bool loadEEPROM() { Serial.println(":: loading EEPROM ::"); // read var WIFIssid from address 0, 32 byte long // read this first, because we decide on the ssid length (>0?) if // we run in unconfigured AP mode, nor not EEPROM.get(0, WIFIssid); // when length is > 0 then read furter EEPROM config data if(strlen(WIFIssid)) { /* * WIFI settings */ // read var WIFIpassword from address 32, 64 byte long EEPROM.get(32, WIFIpassword); // read var WIFIip from address 96, 16 byte long EEPROM.get(96, WIFIip); // read var WIFInetmask from address 112, 16 byte long EEPROM.get(112, WIFInetmask); // read var WIFIgateway from address 128, 16 byte long EEPROM.get(128, WIFIgateway); // read var WIFIgateway from address 128, 16 byte long EEPROM.get(144, WIFIdns); // read var WIFIuseDHCP from Address 160, 1 byte long EEPROM.get(160, WIFIuseDHCP); /* * System settings */ // size is 1 byte EEPROM.get(161, configured); if(configured == true) { // size is 1 byte EEPROM.get(162, UseFan); // size is 1 byte EEPROM.get(163, UsePump); // size is 1 byte EEPROM.get(164, PumpOnTime); // size is 1 byte EEPROM.get(165, MoistureSensor_Type); // size is 1 byte EEPROM.get(166, SoilmoistureLow); // size is 2 byte EEPROM.get(167, ntpOffset); // size is 1 byte EEPROM.get(169, UseLEDrelais); // size is 1 byte // I forgot to add TemperatureSensor_Type to EEPROM and noticed it // quite late in the process of programming. So this value residents // in 214 and not directly after UseLEDrelais EEPROM.get(214, TemperatureSensor_Type); } // TODO auth does not work atm // EEPROM.get(160, WebUiUsername); // EEPROM.get(176, WebUiPassword); /* * Grow settings */ // size is 32 byte EEPROM.get(170, GrowName); if(strlen(GrowName) > 0) { // size is 4 byte EEPROM.get(202, GrowStart); // size is 1 byte EEPROM.get(206, DaysVeg); // size is 1 byte EEPROM.get(207, DaysBloom); // size is 1 byte EEPROM.get(208, LighthoursVeg); // size is 1 byte EEPROM.get(209, LighthoursBloom); // size is 1 byte EEPROM.get(210, SunriseHour); // size is 1 byte EEPROM.get(211, SunriseMinute); // size is 1 byte EEPROM.get(212, DayOfGrow); // to ensure PINled_PWM always set to 255 when UseLEDrelais is true // we set it here again, does not matter whats stored. // size is 1 byte if(UseLEDrelais == true) { PINled_PWM = 255; } else { EEPROM.get(213, PINled_PWM); } } // print values to Serial output Serial.println("---- WiFi values ----"); Serial.print("WIFIssid: "); Serial.println(WIFIssid); Serial.print("WIFIpassword: "); Serial.println(WIFIpassword); Serial.print("Use DHCP: "); Serial.println(WIFIuseDHCP); Serial.println("---- System values ----"); Serial.print("configured: "); Serial.println(configured); Serial.print("UseFan: "); Serial.println(UseFan); Serial.print("UsePump: "); Serial.println(UsePump); Serial.print("PumpOnTime: "); Serial.println(PumpOnTime); Serial.print("MoistureSensor_Type: "); Serial.println(MoistureSensor_Type); Serial.print("SoilmoistureLow: "); Serial.println(SoilmoistureLow); Serial.print("ntpOffset: "); Serial.println(ntpOffset); Serial.print("UseLEDrelais: "); Serial.println(UseLEDrelais); Serial.println("---- Grow values ----"); Serial.print("GrowName: "); Serial.println(GrowName); Serial.print("GrowStart: "); Serial.println(GrowStart); Serial.print("DaysVeg: "); Serial.println(DaysVeg); Serial.print("DaysBloom: "); Serial.println(DaysBloom); Serial.print("LighthoursVeg: "); Serial.println(LighthoursVeg); Serial.print("LighthoursBloom: "); Serial.println(LighthoursBloom); Serial.print("SunriseHour: "); Serial.println(SunriseHour); Serial.print("SunriseMinute: "); Serial.println(SunriseMinute); Serial.print("DayOfGrow: "); Serial.println(DayOfGrow); Serial.print("PINled_PWM: "); Serial.println(PINled_PWM); } else { Serial.println("EEPROM value WIFIssid is empty"); } Serial.println(":: EEPROM loaded ::"); return(strlen(WIFIssid)); } void wifiConnect() { Serial.println(":: Connecting to WiFi ::"); FirstRun = false; Serial.print("SSID: "); Serial.println(WIFIssid); // Start WiFi connection WiFi.begin(WIFIssid, WIFIpassword); if(WIFIuseDHCP == false) { WiFi.config(WIFIip, WIFIdns, WIFIgateway, WIFInetmask); } // wait until WiFi connection is established while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(" CONNECTED!"); Serial.print("IP: "); Serial.println(WiFi.localIP()); Serial.println(":: Getting time from NTP ::"); timeClient.begin(); timeClient.setTimeOffset(ntpOffset * 60 * 60); timeClient.update(); while ( ! timeClient.isTimeSet()) { timeClient.update(); delay(500); Serial.print("."); } Serial.println(timeClient.getFormattedTime()); Serial.println(timeClient.getEpochTime()); display.clearDisplay(); display.display(); display.setCursor(0,0); // display text display.print("IP: "); display.print(WiFi.localIP()); display.display(); } void wifiAp() { Serial.println(":: Creating Accesspoint ::"); FirstRun = true; // configure WiFi Access Point WiFi.softAPConfig(WIFIip, WIFIgateway, WIFInetmask); // start Access Point // TODO make AP with password - does not work atm. idk why. WiFi.softAP(APssid); Serial.print("SSID: "); Serial.println(APssid); Serial.print("CanGrow IP address: "); Serial.println(WiFi.softAPIP()); // TODO does not work atm, idk why //Serial.println("The login credentials for the WebUI are 'cangrow' for username and password"); } /* * Setup * */ void setup() { // Start EEPROM EEPROM.begin(512); // setup pins pinMode(PINfan, OUTPUT); pinMode(PINdht, INPUT); pinMode(PINwaterlevel, OUTPUT); pinMode(PINsoilmoisture, OUTPUT); pinMode(PINled, OUTPUT); pinMode(PINpump, OUTPUT); // set all OUTPUT to low digitalWrite(PINfan, LOW); digitalWrite(PINwaterlevel, LOW); digitalWrite(PINsoilmoisture, LOW); digitalWrite(PINled, LOW); digitalWrite(PINpump, LOW); // Start Serial Serial.begin(115200); // Write a line before doing serious output, because before there is some garbage in serial // whats get the cursor somewhere over the place // output Serial.println("420"); Serial.println(".:: CanGrow Start ::."); // initialise Wire for I2C Wire.begin(); // initialise I2C display display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // Address 0x3C for 128x32 display.clearDisplay(); display.display(); // set display settings display.setTextSize(1); display.setTextColor(SSD1306_WHITE, SSD1306_BLACK); // display Logo display.drawBitmap(0, 0, bmpCanGrow_Logo, 128, 32, WHITE); display.display(); // reset chirp writeI2CRegister8bit(0x20, 6); //TODO: Do only, when configured // initialise DHT11 dht.begin(); //TODO: Do only, when configured Serial.println("To wipe the EEPROM saved data, set D4 (PIN_WIPE) to LOW - NOW! (2 seconds left)"); // wait a few seconds to let the user pull D4 down to wipe EEPROM // and we can enjoy the boot screen meanwhile :p // meanwhile blink with the led onboad :) // 333 * 6 =~ 2 seconds pinMode(PIN_WIPE, OUTPUT); for(byte i = 0; i <= 6 ; i++) { if(i % 2) { digitalWrite(PIN_WIPE, LOW); } else { digitalWrite(PIN_WIPE, HIGH); } delay(333); } // set back to HIGH because thats the default digitalWrite(PIN_WIPE, HIGH); //delay(2000); pinMode(PIN_WIPE, INPUT); // read status from PIN_WIPE to WIPE // when PIN_WIPE is set to LOW, wipe EEPROM if(digitalRead(PIN_WIPE) == LOW) { // wipe EEPROM wipeEEPROM(); } /* * load EEPROM and Setup WiFi * * call loadEEPROM() which returns a bool * When true, CanGrow is already configured and EEPROM values are applied * When false, CanGrow is unconfigured and we need to run the setup assistant */ // load stored values from EEPROM and check what var configured is returned if(loadEEPROM()) { // connect to wifi wifiConnect(); // configured is 0, setup Access Point } else { // start an wifi accesspoint wifiAp(); } // set web handler WebHandler(); // start webserver webserver.begin(); } /* * * * Loop * * */ void loop() { // var definition unsigned int secondsSunrise = (SunriseHour * 60 * 60) + (SunriseMinute * 60); unsigned int secondsToday = (timeClient.getHours() * 60 * 60) + (timeClient.getMinutes() * 60) + timeClient.getSeconds(); unsigned long currentRuntime = millis(); // first we call webserver handle client webserver.handleClient(); // do every second if(currentRuntime - outputPrevTime >= 1000) { // debug output Serial.print("secondsSunrise: "); Serial.println(secondsSunrise); Serial.print("secondsToday: "); Serial.println(secondsToday); //Serial.println("yolo"); // check if secondsToday is larger then secondsSunrise time if(secondsToday >= secondsSunrise) { // turn on light analogWrite(PINled, PINled_PWM); } else { // turn off digitalWrite(PINled, LOW); } outputPrevTime = currentRuntime; } //delay(1000); } /* * Web Handler */ void WebHandler() { /* * Webserver handlers * here are the generic webserver handlers like 404 not found * wifiSettings, ... * * if you are looking for the single webpages handler, have a look to * * WebHandler_unconfigured() and WebHandler_configured() */ // style.css //webserver.on("/style.css", HTTP_GET, WEBstyleCSS); // Web root webserver.on("/", HTTP_GET, WEBroot); // WiFi Stuff webserver.on("/wifiSettings", HTTP_GET, WEBwifiSettings); webserver.on("/wifiSettings/save", HTTP_POST, POSTwifiSettings); // System stuff webserver.on("/systemSettings", HTTP_GET, WEBsystemSettings); webserver.on("/systemSettings/save", HTTP_POST, POSTsystemSettings); // Grow stuff webserver.on("/growSettings", HTTP_GET, WEBgrowSettings); webserver.on("/growSettings/save", HTTP_POST, POSTgrowSettings); // help webserver.on("/help", HTTP_GET, WEBhelp); // restart when NeedRestart is true webserver.on("/restart", HTTP_GET, WebRestart); // does not work atm TODO //webserver.on("/logout", [](){ webserver.send(401, "text/html", "logged out!"); }); // 404 handling // favicon.ico is a special one, because its requested everytime and i dont wont to deliver the // failed whole page every call. we can save up this 0,5kb traffic :o) webserver.on("/favicon.ico", [](){ webserver.send(404, "text/html", "404 - not found"); }); webserver.onNotFound(WEB404); // switching MOSFETs webserver.on("/switch", HTTP_POST, POSTswitchMOSFET); } void WebRestart() { String body = FPSTR(HTMLheader); // TODO only debug and development solution, remove this later if(true) { body += "

Restarting

"; body += "
After restart CanGrow will be connected to WiFi SSID
"; body += WIFIssid; body += "
You get its IP-Address from the display or serial console.
"; body += returnHTMLfooter(); webserver.send(200, "text/html", body); Serial.println("Restarting... see you soon space cowboy!"); delay(1000); ESP.restart(); } else { body += "

Not allowed

"; body += returnHTMLfooter(); webserver.send(405, "text/html", body); } } void WebAuth() { /* * TODO * DOES NOT WORK WHEN CONNECTED TO EXISTING WIFI * IDK WHY * */ char webAuthRealm[] = "CanGrowRealm"; if(!webserver.authenticate(WebUiUsername, WebUiPassword)) { String body = FPSTR(HTMLheader); body += "

Login failed.

"; body += FPSTR(HTMLfooter); webserver.requestAuthentication(DIGEST_AUTH, webAuthRealm, body); } } void WebAuthApi() { /* * TODO * DOES NOT WORK WHEN CONNECTED TO EXISTING WIFI * IDK WHY * */ char webAuthRealm[] = "CanGrowRealm"; if(!webserver.authenticate(WebUiUsername, WebUiPassword)) { webserver.requestAuthentication(DIGEST_AUTH, webAuthRealm); } } // returns footer with javascript stuff String returnHTMLfooter() { String footer; // add replaceStr javascript function from PROGMEM // first is dst ID, second is content (&#.. is a seedling emoji ) footer += FPSTR(JSreplaceStr); // print actual time in header footer += ""; // show the GrowName in the menu if set if(strlen(GrowName) > 0){ footer += ""; } footer += FPSTR(HTMLfooter); return footer; } /* * returnSelected(bool) * returns char[] "selected" if bool is true * useful for html forms, to preset a saved value as selected */ String returnStrSelected(byte savedValue, byte selectId) { String returnStr; if(configured == true) { if(savedValue == selectId) { returnStr = "selected"; } else { returnStr = ""; } } return returnStr; } String returnStrDateFromEpoch(unsigned long epochTime) { String dateStr; byte Day = day(epochTime); byte Month = month(epochTime); unsigned int Year = year(epochTime); dateStr = Year; dateStr += "-"; if(Month < 10) { dateStr += "0"; dateStr += Month; } else { dateStr += Month; } dateStr += "-"; if(Day < 10) { dateStr += "0"; dateStr += Day; } else { dateStr += Day; } return dateStr; } /* * * Web pages * */ // not really a webpage, but CSS is important for them :p /* void WEBstyleCSS() { webserver.send(200, "text/css", HTMLstyleCSS); } */ void WEB404() { String body = FPSTR(HTMLheader); body += "

404 - not found

"; body += returnHTMLfooter(); webserver.send(404, "text/html", body); } void WEBlogout() { String body = FPSTR(HTMLheader); body += "

you are logged out.

"; body += FPSTR(HTMLfooter); // TODO does not work atm webserver.send(401, "text/html", body); } void WEBhelp() { String body = FPSTR(HTMLheader); body += FPSTR(HTMLhelp); body += returnHTMLfooter(); webserver.send(200, "text/html", body); } /* * Root pages */ void WEBroot() { if(FirstRun == true) { webserver.sendHeader("Location", String("/wifiSettings"), true); webserver.send(302, "text/plain", "please configure wifiSettings first"); } else if(configured == false){ webserver.sendHeader("Location", String("/systemSettings"), true); webserver.send(302, "text/plain", "please configure systemSettings first"); } else if(strlen(GrowName) < 1){ webserver.sendHeader("Location", String("/growSettings"), true); webserver.send(302, "text/plain", "please configure growSettings first"); } else { String body = FPSTR(HTMLheader); body += "

🌱 "; body += GrowName; body += "

"; body += "Grow started: "; body += returnStrDateFromEpoch(GrowStart); body += "
"; body += "Day of Grow: "; body += DayOfGrow; body += "

"; body += "Soil Moisture: "; body += getSoilmoisture(MoistureSensor_Type); body += " %
"; body += "Humidity: "; body += getHumidity(); body += " %
"; body += "Temperature: "; body += getTemperature(TemperatureSensor_Type); body += " °C
"; if(UsePump == true) { body += "Pump water level: "; switch(getWaterlevel()) { case 0: body += "OK"; break; case 1: body += "Warning"; break; case 2: body += "Critical"; break; } } float growLightBrightnes = round((PINled_PWM / 255) * 100); Serial.print("growLightBrightnes: "); Serial.println(growLightBrightnes); body += "
"; body += "Growlight brightnes: "; body += growLightBrightnes; body += " %
"; body += "
"; body += "MOSFET
"; body += "On/Off:
\n"; body += "\n"; body += "
"; body += returnHTMLfooter(); webserver.send(200, "text/html", body); } } /* * Config pages */ void WEBwifiSettings() { byte ssidsAvail = WiFi.scanNetworks(); String body = FPSTR(HTMLheader); if(FirstRun == true) { body += "

Welcome!

"; body += "

CanGrow is actually unconfigured. You need to Setup your WiFi first down below.
"; body += "
After you entered your WiFi connection details, you need to restart and are step closer to your grow 🥦"; body += "
"; body += "

"; } body += "

WiFi config

\n"; if(NeedRestart == true) { body += FPSTR(HTMLneedRestart); } if(webserver.hasArg("success")) { body += FPSTR(HTMLsuccess); } body += "

Select your wifi network from the SSID list.
To use DHCP leave IP, Subnet, Gateway and DNS fields blank!

"; body += "
\n"; body += "SSID:
\n"; if(strlen(WIFIssid) > 0) { body += "Currently connected to: "; body += WIFIssid; body += "
\n"; } body += "Password:
\n"; body += "IP:
\n"; body += "Subnet mask:
\n"; body += "Gateway:
\n"; body += "DNS:
\n"; body += "\n"; body += "
\n"; body += returnHTMLfooter(); webserver.send(200, "text/html", body); } void WEBsystemSettings() { // if wifi settings are unconfigured, we cannot proceed with systemSettings if(FirstRun == true) { webserver.sendHeader("Location", String("/wifiSettings"), true); webserver.send(302, "text/plain", "please configure wifiSettings first"); } else { String body = FPSTR(HTMLheader); if(configured == false) { body += "

Step 2: System settings

"; body += "

Please configure all settings
"; body += "

"; } body += "

System settings

"; if(webserver.hasArg("success")) { body += FPSTR(HTMLsuccess); } body += "

here you can set which features and sensors you use
"; body += "

"; // form starts body += "
\n"; // UseFan bool body += "Use fan:
\n"; // UsePump bool body += "Use pump:
\n"; // UseLEDrelais bool body += "Use relais for LED:
\n"; // TODO ugly. can this done be better? // PumpOnTime int body += "Pump on time:
\n"; // MoistureSensor_Type byte body += "Moisture sensor type:
\n"; // SoilmoistureLow byte body += "Soil moisture low:
\n"; // TemperatureSensor_Type byte body += "Temperature sensor type:
\n"; // ntpOffset int body += "NTP offset:
\n"; body += "\n"; body += "
\n"; body += returnHTMLfooter(); webserver.send(200, "text/html", body); } } /* * Grow pages */ void WEBgrowSettings() { // if system settings are unconfigured, we cannot proceed with growSettings if(configured == false) { webserver.sendHeader("Location", String("/systemSettings"), true); webserver.send(302, "text/plain", "please configure systemSettings first"); } else { String body = FPSTR(HTMLheader); if(strlen(GrowName) < 1) { body += "

Final step: Grow settings

"; body += "

Please configure all settings
"; body += "

"; GrowStart = timeClient.getEpochTime(); } body += "

Grow Settings

"; if(webserver.hasArg("success")) { body += FPSTR(HTMLsuccess); } body += "

Here you can set everything grow related, like light hours, how much water, LED brightness
"; body += "

"; body += "
\n"; body += "Grow name:
\n"; // the input field, which calls javascript convertDateToEpoch() to write data to transmit to id GrowStart body += "Grow start date:
\n"; body += "\n"; body += "Days of vegetation:
\n"; body += "Days of bloom:
\n"; body += "Hours light on vegetation:
\n"; body += "Hours light on bloom:
\n"; body += "Sunrise: \n"; body += " :
\n"; if(UseLEDrelais == false) { body += "Brightness LED:
\n"; } body += "\n"; body += "
\n"; body += FPSTR(JSconvertDateToEpoch); body += returnHTMLfooter(); webserver.send(200, "text/html", body); } } /* * * POSTs * */ void POSTgrowSettings() { if(UseLEDrelais == true) { // if a relais is used to turn on grow light, we force PWM to max val PINled_PWM = 255; } else { // otherwise just do PWM PINled_PWM = webserver.arg("PINled_PWM").toInt(); } String GrowName_tmp = webserver.arg("GrowName"); GrowName_tmp.toCharArray(GrowName, 32); GrowStart = webserver.arg("GrowStart").toInt(); DaysVeg = webserver.arg("DaysVeg").toInt(); DaysBloom = webserver.arg("DaysBloom").toInt(); LighthoursVeg = webserver.arg("LighthoursVeg").toInt(); LighthoursBloom = webserver.arg("LighthoursBloom").toInt(); SunriseHour = webserver.arg("SunriseHour").toInt(); SunriseMinute = webserver.arg("SunriseMinute").toInt(); // size is 32 byte EEPROM.put(170, GrowName); // size is 4 byte EEPROM.put(202, GrowStart); // size is 1 byte EEPROM.put(206, DaysVeg); // size is 1 byte EEPROM.put(207, DaysBloom); // size is 1 byte EEPROM.put(208, LighthoursVeg); // size is 1 byte EEPROM.put(209, LighthoursBloom); // size is 1 byte EEPROM.put(210, SunriseHour); // size is 1 byte EEPROM.put(211, SunriseMinute); // size is 1 byte EEPROM.put(213, PINled_PWM); EEPROM.commit(); //analogWrite(PINled, PINled_PWM); Serial.println(":: POSTgrowSettings ::"); Serial.print("GrowName: "); Serial.println(GrowName); Serial.print("GrowStart: "); Serial.println(GrowStart); Serial.print("DaysVeg: "); Serial.println(DaysVeg); Serial.print("DaysBloom: "); Serial.println(DaysBloom); Serial.print("LighthoursVeg: "); Serial.println(LighthoursVeg); Serial.print("LighthoursBloom: "); Serial.println(LighthoursBloom); Serial.print("SunriseHour: "); Serial.println(SunriseHour); Serial.print("SunriseMinute: "); Serial.println(SunriseMinute); Serial.print("PINled_PWM: "); Serial.println(PINled_PWM); webserver.sendHeader("Location", String("/growSettings?success"), true); webserver.send(302, "text/plain", "growSettings/save: success!\n"); } void POSTsystemSettings() { ntpOffset = webserver.arg("ntpOffset").toInt(); MoistureSensor_Type = webserver.arg("MoistureSensor_Type").toInt(); SoilmoistureLow = webserver.arg("SoilmoistureLow").toInt(); UsePump = webserver.arg("UsePump").toInt(); PumpOnTime = webserver.arg("PumpOnTime").toInt(); UseFan = webserver.arg("UseFan").toInt(); UseLEDrelais = webserver.arg("UseLEDrelais").toInt(); TemperatureSensor_Type = webserver.arg("TemperatureSensor_Type").toInt(); configured = true; // size is 1 byte EEPROM.put(161, configured); // size is 1 byte EEPROM.put(162, UseFan); // size is 1 byte EEPROM.put(163, UsePump); // size is 1 byte EEPROM.put(164, PumpOnTime); // size is 1 byte EEPROM.put(165, MoistureSensor_Type); // size is 1 byte EEPROM.put(166, SoilmoistureLow); // size is 2 byte EEPROM.put(167, ntpOffset); // size is 1 byte EEPROM.put(169, UseLEDrelais); // size is 1 byte EEPROM.put(214, TemperatureSensor_Type); EEPROM.commit(); // update time with new offset timeClient.setTimeOffset(ntpOffset * 60 * 60); timeClient.update(); Serial.println(":: POSTsystemSettings ::"); Serial.print("configured: "); Serial.println(configured); Serial.print("UseFan: "); Serial.println(UseFan); Serial.print("UsePump: "); Serial.println(UsePump); Serial.print("PumpOnTime: "); Serial.println(PumpOnTime); Serial.print("MoistureSensor_Type: "); Serial.println(MoistureSensor_Type); Serial.print("SoilmoistureLow: "); Serial.println(SoilmoistureLow); Serial.print("ntpOffset: "); Serial.println(ntpOffset); if(strlen(GrowName) < 1) { webserver.sendHeader("Location", String("/growSettings?success"), true); } else { webserver.sendHeader("Location", String("/systemSettings?success"), true); } webserver.send(302, "text/plain", "systemSettings/save: success!\n"); } void POSTwifiSettings() { String WIFIssid_new = webserver.arg("WIFIssid"); String WIFIpassword_new = webserver.arg("WIFIpassword"); String WIFIip_new = webserver.arg("WIFIip"); String WIFInetmask_new = webserver.arg("WIFInetmask"); String WIFIgateway_new = webserver.arg("WIFIgateway"); String WIFIdns_new = webserver.arg("WIFIdns"); // convert String we got from webserver.arg to EEPROM friendly char[] WIFIssid_new.toCharArray(WIFIssid, 32); WIFIpassword_new.toCharArray(WIFIpassword, 64); // if WIFIip_new was not set, we assume DHCP should be used if(WIFIip_new.length() > 0) { WIFIip.fromString(WIFIip_new); WIFInetmask.fromString(WIFInetmask_new); WIFIgateway.fromString(WIFIgateway_new); WIFIdns.fromString(WIFIdns_new); // WIFIuseDHCP = false; } else { WIFIuseDHCP = true; } // restart is needed to load the new settings NeedRestart = true; EEPROM.put(0, WIFIssid); EEPROM.put(32, WIFIpassword); EEPROM.put(96, WIFIip); EEPROM.put(112, WIFInetmask); EEPROM.put(128, WIFIgateway); EEPROM.put(144, WIFIdns); EEPROM.put(160, WIFIuseDHCP); EEPROM.commit(); Serial.println(":: POSTwifiSettings ::"); Serial.print("WIFIssid: "); Serial.println(WIFIssid_new); Serial.println(WIFIssid); Serial.print("WIFIpassword: "); Serial.println(WIFIpassword_new); Serial.println(WIFIpassword); Serial.print("WIFIip: "); Serial.println(WIFIip_new); Serial.print("WIFInetmask: "); Serial.println(WIFInetmask_new); Serial.print("WIFIgateway: "); Serial.println(WIFIgateway_new); Serial.print("WIFIdns: "); Serial.println(WIFIdns_new); Serial.print("WIFIuseDHCP: "); Serial.println(WIFIuseDHCP); webserver.sendHeader("Location", String("/wifiSettings?success"), true); webserver.send(302, "text/plain", "wifiSettings/save: success!\n"); } void POSTswitchMOSFET() { byte MosfetState = webserver.arg("state").toInt(); byte MosfetNr = webserver.arg("output").toInt(); Serial.println(":: GETswitchMOSFET ::"); Serial.print("MosfetState: "); Serial.println(MosfetState); Serial.print("MosfetNr: "); Serial.println(MosfetNr); if((MosfetState > 1) || (MosfetState < 0)) { webserver.send(400, "text/plain", "not valid\n"); } else { switch(MosfetNr) { case 1: if( MosfetState == 1) { analogWrite(PINled, PINled_PWM); } else { digitalWrite(PINled, MosfetState); } break; case 2: digitalWrite(PINpump, MosfetState); break; case 3: digitalWrite(PINfan, MosfetState); break; default: webserver.send(400, "text/plain", "not valid\n"); break; } webserver.sendHeader("Location", String("/?success"), true); webserver.send(302, "text/plain", "switch: success!\n"); } } /* String JSreplaceStr(String elementID, String content) { String jsReturn = ""; return jsReturn; } */ /* * * * PLAYGROUND / TRASH * * */ /* unsigned long currentTime = millis(); int valSoilmoisture0 = getSoilmoisture(0); int valSoilmoisture1 = getSoilmoisture(1); float valTemperature0 = getTemperature(0); float valTemperature1 = getTemperature(1); float valHumidity = getHumidity(); int valWaterlevel = getWaterlevel(); switch(valWaterlevel) { case 0: digitalWrite(PINled, HIGH); digitalWrite(PINpump, LOW); digitalWrite(PINfan, LOW); break; case 1: digitalWrite(PINled, LOW); digitalWrite(PINpump, HIGH); digitalWrite(PINfan, LOW); break; case 2: digitalWrite(PINled, LOW); digitalWrite(PINpump, LOW); digitalWrite(PINfan, HIGH); break; } // OUTPUT if(currentTime - outputPrevTime >= 1000) { // set display cursor to top left display.setCursor(0,0); // display text display.print("I2C: "); display.print(valSoilmoisture1); display.print(", "); display.println(valTemperature1); Serial.print("I2C: "); Serial.print(valSoilmoisture1); Serial.print(", "); Serial.println(valTemperature1); display.print("DHT11: "); display.print(valTemperature0); display.print(", "); display.println(valHumidity); Serial.print("DHT11: "); Serial.print(valTemperature0); Serial.print(", "); Serial.println(valHumidity); display.print("Water Status: "); display.println(valWaterlevel); Serial.print("Water Status: "); Serial.println(valWaterlevel); display.print("ASM: "); display.print(valSoilmoisture0); display.println(", "); Serial.print("ASM: "); Serial.println(valSoilmoisture0); // print everything on the display display.display(); Serial.println("Test"); outputPrevTime = currentTime; */ /* if(D6status == true) { digitalWrite(PINled, LOW); digitalWrite(PINpump, LOW); digitalWrite(PINfan, LOW); D6status = false; Serial.println("D6 is off now"); } else { digitalWrite(PINled, HIGH); digitalWrite(PINpump, HIGH); digitalWrite(PINfan, HIGH); D6status = true; Serial.println("D6 is ON now"); } */ /* for(int dutyCycle = 0; dutyCycle < 255; dutyCycle++){ // changing the LED brightness with PWM analogWrite(PINled, dutyCycle); delay(1); } // decrease the LED brightness for(int dutyCycle = 255; dutyCycle > 0; dutyCycle--){ // changing the LED brightness with PWM analogWrite(PINled, dutyCycle); delay(1); } */