2783 lines
77 KiB
C++
2783 lines
77 KiB
C++
/*
|
|
* CanGrow - simply DIY automatic plant grow system (for cannabis).
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* Includes
|
|
*
|
|
*/
|
|
|
|
// Libraries
|
|
// https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/SPI
|
|
#include <SPI.h>
|
|
// https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/Wire
|
|
#include <Wire.h>
|
|
// https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/EEPROM
|
|
#include <EEPROM.h>
|
|
// https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi
|
|
#include <ESP8266WiFi.h>
|
|
#include <WiFiUdp.h>
|
|
// https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer
|
|
#include <ESP8266WebServer.h>
|
|
// https://github.com/adafruit/Adafruit-GFX-Library
|
|
#include <Adafruit_GFX.h>
|
|
// https://github.com/adafruit/Adafruit_SSD1306
|
|
#include <Adafruit_SSD1306.h>
|
|
// https://github.com/adafruit/DHT-sensor-library
|
|
#include "DHT.h"
|
|
// https://github.com/bblanchon/ArduinoJson
|
|
#include <ArduinoJson.h>
|
|
// https://github.com/arduino-libraries/NTPClient
|
|
#include <NTPClient.h>
|
|
// https://github.com/PaulStoffregen/Time
|
|
#include <TimeLib.h>
|
|
|
|
|
|
/*
|
|
*
|
|
* Constants
|
|
*
|
|
*/
|
|
const char* CanGrowVer = "0.1";
|
|
const char* APssid = "CanGrow-unconfigured";
|
|
/*
|
|
* TODO - does not work atm. idk why.
|
|
* const char* APpass = "CanGrow";
|
|
const int APchannel = 6;
|
|
const bool APhidden = false;
|
|
*
|
|
*/
|
|
|
|
/*
|
|
*
|
|
* 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()
|
|
byte valWaterlevel;
|
|
// do we need a restart? (e.g. after wifi settings change)
|
|
bool NeedRestart;
|
|
bool FirstRun;
|
|
// which screen should be actually displayed
|
|
byte ScreenToDisplay = 0;
|
|
byte DisplayScreenDuration = 3;
|
|
// how many seconds actual screen got displayed
|
|
byte ScreenIterationPassed = 0;
|
|
|
|
bool MaintenanceMode = false;
|
|
unsigned long MaintenanceStarted = 0;
|
|
|
|
/*
|
|
*
|
|
* 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
|
|
short 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;
|
|
bool UseFANrelais;
|
|
// Which temperature sensor to use?
|
|
byte TemperatureSensor_Type;
|
|
unsigned short MaintenanceDuration = 60;
|
|
|
|
//
|
|
// 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;
|
|
// PinLEDPWM - 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 PinLEDPWM = 255;
|
|
byte PinFANPWM = 255;
|
|
|
|
// fade in and out sunrise and sunset?
|
|
bool SunFade;
|
|
byte SunFadeDuration = 30;
|
|
|
|
/*
|
|
*
|
|
* NTP
|
|
*
|
|
*/
|
|
|
|
WiFiUDP ntpUDP;
|
|
NTPClient timeClient(ntpUDP);
|
|
|
|
/*
|
|
*
|
|
* Webserver
|
|
*
|
|
*/
|
|
|
|
ESP8266WebServer webserver(80);
|
|
|
|
/*
|
|
* HTML constants for header, footer, css, ...
|
|
* Note: I know of the existence of SPIFFS and ESPHtmlTemplateProcessor,
|
|
* but to keep things simple for compiling and upload for others, I decided
|
|
* to not use those.
|
|
*/
|
|
|
|
// Template: const char HTMLexamplepage[] PROGMEM = R"EOF()EOF";
|
|
|
|
// first part of HTML header stuff
|
|
const char HTMLheaderP1[] PROGMEM = R"EOF(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset='UTF-8'>
|
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
|
)EOF";
|
|
// here comes the page title in returnHTMLheader()
|
|
|
|
// second part of HTML header stuff
|
|
// Having the whole CSS here ensures it's all the time present
|
|
const char HTMLheaderP2[] PROGMEM = R"EOF(
|
|
<link rel='icon' href='data:;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsTAAALEwEAmpwYAAABcElEQVQ4y42TzU/bQBDFf7Nx1qGuAYVgQSuo2khBggPhyIH//9AiJAQ9tEeLqCKiUD6sxF52OMSEBCdW57aa9968fTsr3V5XWVLPO6sANNL7ZRAMNeU6Ea4T1UEI6pr55kcAwhpMrYOpk2/r/yEQmKWkIonf+TZVgex4Fw0bIEtIAALF3gbZ8U5VwKa3PJ18JT9IpiLvyflBwuhLG5veVUM0/0aoCONPa2hQjWZ8uEVeupJnXSBwO8YOH8iTeAKc2Q4Xt2C1VZL93F7MjbK/bxDnp5Zn7b+So+9pdQ+K/Q5qJlrRj5Ts6DM+rK7Ih7Mr3HaM7jYQVZqXQ6Tb6yqBYdTfomhHiFfUyMI3f+01/z7RHNzTGDyWGThP63SA2d8EEfIkrgQpzmOvH0AV+3M4zegNpUwagAYG8Yp4BS0nl4Kz5Mpf0JXJMby6w/66Aa+M+9uE53/Iexsggq4ESOYWC0jmsBfX8xdXhcJjL4cLc3kBl8uJGQ/CrpAAAAAASUVORK5CYII='>
|
|
<style>
|
|
body {
|
|
color: #cae0d0;
|
|
background-color: #1d211e;
|
|
font-family: helvetica;
|
|
}
|
|
|
|
.center {
|
|
width: 100%;
|
|
margin: auto;
|
|
}
|
|
|
|
h1, h2, h3, h4, h5 {
|
|
text-align: center;
|
|
}
|
|
a:link, a:visited {
|
|
color: #04AA6D;
|
|
}
|
|
a:hover {
|
|
color: #64AA6D;
|
|
}
|
|
a:active {
|
|
color: #04AA6D;
|
|
}
|
|
.infomsg , .warnmsg {
|
|
color: #fff;
|
|
border-radius: 3px;
|
|
padding: 4px;
|
|
width: fit-content; min-width: 200px; max-width: 420px;
|
|
margin: auto;
|
|
margin-bottom: 5px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
text-decoration: none;
|
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5);
|
|
}
|
|
.infomsg {
|
|
background: #04AA6D;
|
|
}
|
|
.warnmsg {
|
|
background: #aa4204;
|
|
}
|
|
.inputShort {
|
|
width: 42px;
|
|
}
|
|
|
|
.nav {
|
|
background: #333;
|
|
width: 100%;
|
|
margin: auto;
|
|
margin-bottom: 10px;
|
|
padding: 0;
|
|
position: relative;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.nav li {
|
|
display: inline-block;
|
|
list-style: none;
|
|
}
|
|
|
|
.nav li:first-of-type {
|
|
background: #026b45;
|
|
border-top-left-radius: 3px;
|
|
border-bottom-left-radius: 3px;
|
|
}
|
|
.nav li a , .nav span {
|
|
color: #ddd;
|
|
display: block;
|
|
font-family: 'Lucida Sans Unicode', 'Lucida Grande', sans-serif;
|
|
font-size:0.8em;
|
|
padding: 10px 20px;
|
|
text-decoration: none;
|
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5);
|
|
}
|
|
.nav li a:hover , .activeMenu , .menuTime{
|
|
background: #04AA6D;
|
|
color: #fff;
|
|
border-radius: 3px;
|
|
}
|
|
.nav li a:active {
|
|
color: #cae0d0;
|
|
}
|
|
.activeMenu {
|
|
background: #444;
|
|
}
|
|
.MenuTime {
|
|
background: #292929;
|
|
}
|
|
|
|
@media only screen and (min-width: 1280px) {
|
|
.center, .nav {
|
|
width: 60%; min-width: 420px;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<ul class='nav'>)EOF";
|
|
// here comes the menu as unordered List in returnHTMLheader()
|
|
|
|
|
|
const char HTMLfooter[] PROGMEM = R"EOF(
|
|
</div>
|
|
</body>
|
|
</html>
|
|
)EOF";
|
|
|
|
|
|
const char HTMLsuccess[] PROGMEM = R"EOF(
|
|
<div class='infomsg'>✅ Successfully saved!</div>
|
|
)EOF";
|
|
|
|
const char HTMLneedRestart[] PROGMEM = R"EOF(
|
|
<div class='warnmsg'>❗ Restart is required to apply new WiFi settings!
|
|
<form action='/system/restart'>
|
|
<input type='submit' value='Restart now' />
|
|
</form>
|
|
</div>
|
|
)EOF";
|
|
|
|
const char HTMLhelp[] PROGMEM = R"EOF(
|
|
<h2>❓ Help</h2>
|
|
Here you will get some helpful help.
|
|
)EOF";
|
|
|
|
|
|
const char JSconvertDateToEpoch[] PROGMEM = R"EOF(
|
|
<script>
|
|
function convertDateToEpoch(src, dst) {
|
|
var valGrowStart = document.getElementById(src).value ;
|
|
document.getElementById(dst).value = new Date(valGrowStart).getTime() / 1000;
|
|
}
|
|
</script>
|
|
)EOF";
|
|
|
|
// The gauge meter are based on sathomas' gaugemeter
|
|
// https://github.com/sathomas/material-gauge
|
|
|
|
const char CSSgauge[] PROGMEM = R"EOF(
|
|
.gauge {
|
|
position: relative;
|
|
}
|
|
|
|
.gaugeWrapper {
|
|
overflow: hidden;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.gauge__container {
|
|
margin: 0;
|
|
padding: 0;
|
|
position: absolute;
|
|
left: 50%;
|
|
overflow: hidden;
|
|
text-align: center;
|
|
-webkit-transform: translateX(-50%);
|
|
-moz-transform: translateX(-50%);
|
|
-ms-transform: translateX(-50%);
|
|
-o-transform: translateX(-50%);
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.gauge__background {
|
|
z-index: 0;
|
|
position: absolute;
|
|
background-color: #cae0d0;
|
|
top: 0;
|
|
border-radius: 300px 300px 0 0;
|
|
}
|
|
|
|
.gauge__data {
|
|
z-index: 1;
|
|
position: absolute;
|
|
background-color: #04AA6D;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
border-radius: 300px 300px 0 0;
|
|
-webkit-transform-origin: center bottom;
|
|
-moz-transform-origin: center bottom;
|
|
-ms-transform-origin: center bottom;
|
|
-o-transform-origin: center bottom;
|
|
transform-origin: center bottom;
|
|
}
|
|
|
|
.gauge__center {
|
|
z-index: 2;
|
|
position: absolute;
|
|
background-color: #1d211e;
|
|
margin-right: auto;
|
|
border-radius: 300px 300px 0 0;
|
|
}
|
|
|
|
.gauge__marker {
|
|
z-index: 3;
|
|
background-color: #fff;
|
|
position: absolute;
|
|
width: 1px;
|
|
}
|
|
|
|
.gauge__needle {
|
|
z-index: 4;
|
|
background-color: #E91E63;
|
|
height: 3px;
|
|
position: absolute;
|
|
-webkit-transform-origin: left center;
|
|
-moz-transform-origin: left center;
|
|
-ms-transform-origin: left center;
|
|
-o-transform-origin: left center;
|
|
transform-origin: left center;
|
|
}
|
|
|
|
.gauge__labels {
|
|
display: table;
|
|
margin: 0 auto;
|
|
position: relative;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.gauge__label--low {
|
|
display: table-cell;
|
|
text-align: center;
|
|
}
|
|
|
|
.gauge__label--spacer {
|
|
display: table-cell;
|
|
}
|
|
|
|
.gauge__label--high {
|
|
display: table-cell;
|
|
text-align: center;
|
|
}
|
|
|
|
|
|
.gauge { height: calc(60px + 3em); }
|
|
.gauge__container { width: 120px; height: 60px; }
|
|
.gauge__marker { height: 60px; left: 59.5px; }
|
|
.gauge__background { width: 120px; height: 60px; }
|
|
.gauge__center { width: 72px; height: 36px; top: 24px; margin-left: 24px; }
|
|
.gauge__data { width: 120px; height: 60px; }
|
|
.gauge__needle { left: 60px; top: 58px; width: 60px; }
|
|
.gauge__labels { top: 60px; width: 120px; }
|
|
.gauge__label--low { width: 24px; }
|
|
.gauge__label--spacer { width: 72px; text-align: center;}
|
|
.gauge__label--high { width: 24px; }
|
|
|
|
@media only screen and (min-width: 720px) {
|
|
.gauge { height: calc(120px + 4.2em); }
|
|
.gauge__container { width: 240px; height: 120px; }
|
|
.gauge__marker { height: 120px; left: 119.5px; }
|
|
.gauge__background { width: 240px; height: 120px; }
|
|
.gauge__center { width: 144px; height: 72px; top: 48px; margin-left: 48px; }
|
|
.gauge__data { width: 240px; height: 120px; }
|
|
.gauge__needle { left: 120px; top: 117px; width: 120px; }
|
|
.gauge__labels { top: 120px; width: 240px; }
|
|
.gauge__label--low { width: 48px; }
|
|
.gauge__label--spacer { width: 144px; text-align: center;}
|
|
.gauge__label--high { width: 48px; }
|
|
.gaugeLabel { font-size: 1.3em; }
|
|
.gauge__labels { font-size: 2em; }
|
|
}
|
|
|
|
.gauge--liveupdate .gauge__data,
|
|
.gauge--liveupdate .gauge__needle {
|
|
-webkit-transition: all 1s ease-in-out;
|
|
-moz-transition: all 1s ease-in-out;
|
|
-ms-transition: all 1s ease-in-out;
|
|
-o-transition: all 1s ease-in-out;
|
|
transition: all 1s ease-in-out;
|
|
}
|
|
|
|
|
|
.gauge__data {
|
|
-webkit-transform: rotate(-.50turn);
|
|
-moz-transform: rotate(-.50turn);
|
|
-ms-transform: rotate(-.50turn);
|
|
-o-transform: rotate(-.50turn);
|
|
transform: rotate(-.50turn);
|
|
}
|
|
.gauge__needle {
|
|
-webkit-transform: rotate(-.50turn);
|
|
-moz-transform: rotate(-.50turn);
|
|
-ms-transform: rotate(-.50turn);
|
|
-o-transform: rotate(-.50turn);
|
|
transform: rotate(-.50turn);
|
|
}
|
|
|
|
)EOF";
|
|
|
|
const char JSgauge[] PROGMEM = R"EOF(
|
|
|
|
function Gauge(el) {
|
|
|
|
var element, // Containing element for the info component
|
|
data, // `.gauge__data` element
|
|
needle, // `.gauge__needle` element
|
|
value = 0.0, // Current gauge value from 0 to 1
|
|
prop, // Style for transform
|
|
valueLabel; // `.gauge__label--spacer` element
|
|
|
|
var setElement = function(el) {
|
|
// Keep a reference to the various elements and sub-elements
|
|
element = el;
|
|
data = element.querySelector('.gauge__data');
|
|
needle = element.querySelector('.gauge__needle');
|
|
valueLabel = element.querySelector('.gauge__label--spacer');
|
|
|
|
};
|
|
|
|
var setValue = function(x, max, unit) {
|
|
percentage = x * 100 / max;
|
|
value = percentage / 100;
|
|
var turns = -0.5 + (value * 0.5);
|
|
data.style[prop] = 'rotate(' + turns + 'turn)';
|
|
needle.style[prop] = 'rotate(' + turns + 'turn)';
|
|
valueLabel.textContent = x + unit;
|
|
|
|
};
|
|
|
|
function exports() { };
|
|
|
|
exports.element = function(el) {
|
|
if (!arguments.length) { return element; }
|
|
setElement(el);
|
|
return this;
|
|
};
|
|
|
|
exports.value = function(x, max=100, unit='%') {
|
|
if (!arguments.length) { return value; }
|
|
setValue(x, max, unit);
|
|
return this;
|
|
};
|
|
|
|
var body = document.getElementsByTagName('body')[0];
|
|
['webkitTransform', 'mozTransform', 'msTransform', 'oTransform', 'transform'].
|
|
forEach(function(p) {
|
|
if (typeof body.style[p] !== 'undefined') { prop = p; }
|
|
}
|
|
);
|
|
|
|
if (arguments.length) {
|
|
setElement(el);
|
|
}
|
|
|
|
return exports;
|
|
|
|
};
|
|
|
|
|
|
)EOF";
|
|
|
|
const char HTMLgauge[] PROGMEM = R"EOF(
|
|
|
|
<div class='gaugeWrapper'>
|
|
<div class='gauge gauge--liveupdate spacer' id='gaugeTemperature' style='float:left; margin-right: 10px;'>
|
|
<span class='gaugeLabel'>Temperature</span>
|
|
<div class='gauge__container'>
|
|
<div class='gauge__background'></div>
|
|
<div class='gauge__center'></div>
|
|
<div class='gauge__data'></div>
|
|
<div class='gauge__needle'></div>
|
|
</div>
|
|
<div class='gauge__labels mdl-typography__headline'>
|
|
<span class='gauge__label--low'></span>
|
|
<span class='gauge__label--spacer'></span></span>
|
|
<span class='gauge__label--high'></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class='gauge gauge--liveupdate spacer' id='gaugeHumidity' style='float:left; margin-right: 10px;'>
|
|
<span class='gaugeLabel'>Humidity</span>
|
|
<div class='gauge__container'>
|
|
<div class='gauge__background'></div>
|
|
<div class='gauge__center'></div>
|
|
<div class='gauge__data'></div>
|
|
<div class='gauge__needle'></div>
|
|
</div>
|
|
<div class='gauge__labels mdl-typography__headline'>
|
|
<span class='gauge__label--low'></span>
|
|
<span class='gauge__label--spacer'></span>
|
|
<span class='gauge__label--high'></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class='gauge gauge--liveupdate' id='gaugeSoilmoisture' style='float:left;'>
|
|
<span class='gaugeLabel'>Soilmoisture</span>
|
|
<div class='gauge__container'>
|
|
<div class='gauge__background'></div>
|
|
<div class='gauge__center'></div>
|
|
<div class='gauge__data'></div>
|
|
<div class='gauge__needle'></div>
|
|
</div>
|
|
<div class='gauge__labels mdl-typography__headline'>
|
|
<span class='gauge__label--low'></span>
|
|
<span class='gauge__label--spacer'></span>
|
|
<span class='gauge__label--high'></span>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
<script src='gauge.js'></script>
|
|
<script>
|
|
var gaugeTemperature = new Gauge(document.getElementById('gaugeTemperature'));
|
|
var gaugeHumidity = new Gauge(document.getElementById('gaugeHumidity'));
|
|
var gaugeSoilmoisture = new Gauge(document.getElementById('gaugeSoilmoisture'));
|
|
</script>
|
|
|
|
)EOF";
|
|
/*
|
|
*
|
|
* Pin assignments
|
|
*
|
|
* D0 - MOSFET Pump
|
|
* D1, D2 - I2C
|
|
* D3 - DHT11
|
|
* D4 - PinWIPE
|
|
* D5 - MOSFET Fan
|
|
* 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 PinPUMP = 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 PinWIPE = D4;
|
|
const uint8_t PinFAN = 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, 64, &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
|
|
*/
|
|
|
|
short waterlevelWARN = 200;
|
|
short waterlevelOK = 400;
|
|
short 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(PinWIPE) == LOW ) {
|
|
// only show the Serial message once
|
|
if(wipeMsg == 0) {
|
|
Serial.println("Please release PinWIPE to erase all data saved in EEPROM");
|
|
Serial.println("LAST CHANCE TO KEEP THE DATA BY RESETTING NOW!!");
|
|
|
|
display.clearDisplay();
|
|
display.setCursor(0,0);
|
|
display.println("!!!!!!!!!!!!!!!!!!!!!");
|
|
display.println("");
|
|
display.println("RELEASE PinWIPE");
|
|
display.println("TO WIPE EEPROM");
|
|
display.display();
|
|
|
|
// 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... ");
|
|
display.println("Wiping EEPROM...");
|
|
display.println("Will restart in 3s");
|
|
display.display();
|
|
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 PinWIPE internal LED to Output to give feedback WIPE
|
|
// was done
|
|
pinMode(PinWIPE, 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(PinWIPE, LOW);
|
|
} else {
|
|
digitalWrite(PinWIPE, HIGH);
|
|
}
|
|
delay(125);
|
|
}
|
|
ESP.restart();
|
|
}
|
|
|
|
|
|
bool loadEEPROM() {
|
|
/*
|
|
* EEPROM Save table
|
|
*
|
|
* 0 WIFIssid
|
|
* 32 WIFIpassword
|
|
* 96 WIFIip
|
|
* 112 WIFInetmask
|
|
* 128 WIFIgateway
|
|
* 144 WIFIdns
|
|
* 160 WIFIuseDHCP
|
|
*
|
|
* 161 configured
|
|
* 162 UseFan
|
|
* 163 UsePump
|
|
* 164 PumpOnTime
|
|
* 165 MoistureSensor_Type
|
|
* 166 SoilmoistureLow
|
|
* 167 NtpOffset
|
|
* 169 UseLEDrelais
|
|
*
|
|
* 170 GrowName
|
|
* 202 GrowStart
|
|
* 206 DaysVeg
|
|
* 207 DaysBloom
|
|
* 208 LighthoursVet
|
|
* 209 LighthoursBloom
|
|
* 210 SunriseHour
|
|
* 211 SunriseMinute
|
|
* 212 DayOfGrow
|
|
*
|
|
* -- afterwards added, need to sort --
|
|
*
|
|
* 213 PinLEDPWM
|
|
* 214 TemperatureSensor_Type
|
|
* 215 UseFANrelais
|
|
* 216 PinFANPWM
|
|
* 217 SunFade
|
|
* 218 SunFadeDuration
|
|
* 219 MaintenanceDuration
|
|
* 221 ..
|
|
*
|
|
*/
|
|
|
|
Serial.println(":: loading EEPROM ::");
|
|
|
|
display.setCursor(0,36);
|
|
display.fillRect(0,36,128,64-36, 0);
|
|
display.println("loading EEPROM");
|
|
display.display();
|
|
// 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
|
|
EEPROM.get(214, TemperatureSensor_Type);
|
|
// size is 1 byte
|
|
EEPROM.get(215, UseFANrelais);
|
|
// size is 2 byte
|
|
EEPROM.get(219, MaintenanceDuration);
|
|
}
|
|
// 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);
|
|
// size is 1 byte
|
|
EEPROM.get(213, PinLEDPWM);
|
|
// size is 1 byte
|
|
EEPROM.get(216, PinFANPWM);
|
|
EEPROM.get(217, SunFade);
|
|
EEPROM.get(218, SunFadeDuration);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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("TemperatureSensor_Type: ");
|
|
Serial.println(TemperatureSensor_Type);
|
|
Serial.print("SoilmoistureLow: ");
|
|
Serial.println(SoilmoistureLow);
|
|
Serial.print("NtpOffset: ");
|
|
Serial.println(NtpOffset);
|
|
Serial.print("UseLEDrelais: ");
|
|
Serial.println(UseLEDrelais);
|
|
Serial.print("UseFANrelais: ");
|
|
Serial.println(UseFANrelais);
|
|
Serial.print("MaintenanceDuration: ");
|
|
Serial.println(MaintenanceDuration);
|
|
|
|
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("PinLEDPWM: ");
|
|
Serial.println(PinLEDPWM);
|
|
Serial.print("PinFANPWM: ");
|
|
Serial.println(PinFANPWM);
|
|
Serial.print("SunFade: ");
|
|
Serial.println(SunFade);
|
|
Serial.print("SunFadeDuration: ");
|
|
Serial.println(SunFadeDuration);
|
|
|
|
|
|
} else {
|
|
Serial.println("EEPROM value WIFIssid is empty");
|
|
}
|
|
Serial.println(":: EEPROM loaded ::");
|
|
|
|
display.setCursor(0,42);
|
|
display.println("EEPROM loaded");
|
|
display.display();
|
|
|
|
return(strlen(WIFIssid));
|
|
}
|
|
|
|
void wifiConnect() {
|
|
Serial.println(":: Connecting to WiFi ::");
|
|
FirstRun = false;
|
|
Serial.print("SSID: ");
|
|
Serial.println(WIFIssid);
|
|
|
|
display.fillRect(0,36,128,64-36, 0);
|
|
display.setCursor(0,36);
|
|
display.println("Connecting to WiFi");
|
|
display.println(WIFIssid);
|
|
display.display();
|
|
|
|
// 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 ::");
|
|
display.fillRect(0,36,128,64-36, 0);
|
|
display.setCursor(0,36);
|
|
display.println("Getting NTP time");
|
|
display.display();
|
|
|
|
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.println(timeClient.getFormattedTime());
|
|
display.display();
|
|
display.print("IP: ");
|
|
display.print(WiFi.localIP());
|
|
display.display();
|
|
}
|
|
|
|
void wifiAp() {
|
|
Serial.println(":: Creating Accesspoint ::");
|
|
|
|
display.fillRect(0,36,128,64-36, 0);
|
|
display.setCursor(0,36);
|
|
display.println("Creating AccessPoint");
|
|
display.println(APssid);
|
|
display.display();
|
|
|
|
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());
|
|
|
|
display.print("IP: ");
|
|
display.println(WiFi.softAPIP());
|
|
display.display();
|
|
// TODO does not work atm, idk why
|
|
//Serial.println("The login credentials for the WebUI are 'cangrow' for username and password");
|
|
}
|
|
|
|
void setOutput(byte Output, byte OutputState) {
|
|
/*
|
|
* Pin assignments
|
|
*
|
|
* 1 - LED
|
|
* 2 - FAN
|
|
* 3 - PUMP
|
|
*
|
|
*/
|
|
bool UseRelais = true;
|
|
byte OutputPin;
|
|
|
|
switch(Output) {
|
|
case 1:
|
|
OutputPin = PinLED;
|
|
if(UseLEDrelais == true) {
|
|
UseRelais = true;
|
|
} else {
|
|
UseRelais = false;
|
|
}
|
|
break;
|
|
case 2:
|
|
OutputPin = PinFAN;
|
|
if(UseFANrelais == true) {
|
|
UseRelais = true;
|
|
} else {
|
|
UseRelais = false;
|
|
}
|
|
break;
|
|
// PUMP Pin (D0) does not support PWM, so we do not need to care about
|
|
case 3:
|
|
OutputPin = PinPUMP;
|
|
break;
|
|
}
|
|
|
|
//~ Serial.print("Output: ");
|
|
//~ Serial.println(Output);
|
|
//~ Serial.print("OutputPin: ");
|
|
//~ Serial.println(OutputPin);
|
|
//~ Serial.print("OutputState: ");
|
|
//~ Serial.println(OutputState);
|
|
//~ Serial.print("UseRelais: ");
|
|
//~ Serial.println(UseRelais);
|
|
|
|
if( (UseRelais == true) || (OutputPin == PinPUMP) ) {
|
|
digitalWrite(OutputPin, OutputState);
|
|
} else {
|
|
analogWrite(OutputPin, OutputState);
|
|
}
|
|
}
|
|
|
|
|
|
void controlLED() {
|
|
byte lightHours;
|
|
byte PinLEDPWM_tmp;
|
|
unsigned int secondsSunrise = (SunriseHour * 60 * 60) + (SunriseMinute * 60);
|
|
unsigned int secondsToday = (timeClient.getHours() * 60 * 60) + (timeClient.getMinutes() * 60) + timeClient.getSeconds();
|
|
|
|
if(DayOfGrow > DaysVeg ) {
|
|
lightHours = LighthoursBloom;
|
|
} else {
|
|
lightHours = LighthoursVeg;
|
|
}
|
|
|
|
// check if secondsToday is larger then secondsSunrise time AND if
|
|
// secondsToday is smaller then the sum of secondsSunrise + seconds of lightHours
|
|
if( ((secondsToday >= secondsSunrise) && (secondsToday <= ( secondsSunrise + (lightHours * 60 * 60))) ) ){
|
|
//Serial.println("light on time");
|
|
|
|
// when SunFade is true, fade LED light. Otherwise just turn on or off
|
|
if( (SunFade == true) && (UseLEDrelais == false) && (secondsSunrise + SunFadeDuration * 60 >= secondsToday) ) {
|
|
// in the first n minutes of lighting (SunFadeDuration), we want
|
|
// to raise the light slowly to prevent stress from the plant
|
|
// convert progress sunrise to PWM value
|
|
PinLEDPWM_tmp = (SunFadeDuration * 60 - ((secondsSunrise + SunFadeDuration * 60) - secondsToday)) * PinLEDPWM / (SunFadeDuration * 60);
|
|
setOutput(1, PinLEDPWM_tmp);
|
|
//Serial.print("sunrise PWM; ");
|
|
//Serial.println(PinLEDPWM_tmp);
|
|
|
|
} else if( (SunFade == true) && (UseLEDrelais == false) && (secondsToday >= ((secondsSunrise + lightHours * 60 * 60) - SunFadeDuration * 60) ) ) {
|
|
// calculate progress sunset to PWM value
|
|
PinLEDPWM_tmp = (secondsSunrise + (lightHours * 60 * 60) - secondsToday) * PinLEDPWM / (SunFadeDuration * 60);
|
|
setOutput(1, PinLEDPWM_tmp);
|
|
//Serial.print("sunset PWM: ");
|
|
//Serial.println(PinLEDPWM_tmp);
|
|
|
|
} else {
|
|
//Serial.println("just turn on the light");
|
|
// no sunrise or sunset, just keep the LED turned on
|
|
setOutput(1, PinLEDPWM);
|
|
}
|
|
|
|
} else {
|
|
//Serial.println("good night time");
|
|
// turn off
|
|
setOutput(1, 0);
|
|
}
|
|
}
|
|
|
|
void refreshSensors() {
|
|
valSoilmoisture = getSoilmoisture(MoistureSensor_Type);
|
|
valHumidity = getHumidity();
|
|
valTemperature = getTemperature(TemperatureSensor_Type);
|
|
valWaterlevel = getWaterlevel();
|
|
}
|
|
|
|
void displayScreens() {
|
|
/*
|
|
* which screen to display
|
|
* interate through different screens
|
|
*
|
|
*/
|
|
if(ScreenIterationPassed > DisplayScreenDuration){
|
|
ScreenIterationPassed = 0;
|
|
// helper variable, maybe i find a better way in future
|
|
byte LastScreen = 2;
|
|
// when the next screen gets displayed, clear display
|
|
display.clearDisplay();
|
|
display.display();
|
|
// when ScreenToDisplay has reach last number of screens, reset to first (0)
|
|
if(ScreenToDisplay >= LastScreen) {
|
|
ScreenToDisplay = 0;
|
|
} else {
|
|
ScreenToDisplay++;
|
|
}
|
|
}
|
|
|
|
display.setCursor(0,0);
|
|
// in this switch case the single screens gets defined
|
|
switch(ScreenToDisplay) {
|
|
case 0:
|
|
display.print("Humidity: ");
|
|
display.print(valHumidity);
|
|
display.println(" %");
|
|
display.println("");
|
|
display.print("Temperature: ");
|
|
display.print(valTemperature);
|
|
display.println(" C");
|
|
display.println("");
|
|
display.print("Moisture: ");
|
|
display.print(valSoilmoisture);
|
|
display.println(" %");
|
|
display.println("");
|
|
if(UsePump == true) {
|
|
display.print("Pump Waterlvl: ");
|
|
switch(valWaterlevel) {
|
|
case 0:
|
|
display.println("OK");
|
|
break;
|
|
case 1:
|
|
display.println("Warn");
|
|
break;
|
|
case 2:
|
|
display.println("Crit");
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 1:
|
|
display.print("LED: ");
|
|
display.print(PinLEDPWM * 100 / 255);
|
|
display.println(" %");
|
|
display.print("State: ");
|
|
display.println(digitalRead(PinLED));
|
|
display.println("");
|
|
display.print("FAN: ");
|
|
display.print(PinFANPWM * 100 / 255);
|
|
display.println(" %");
|
|
display.print("State: ");
|
|
display.println(digitalRead(PinFAN));
|
|
display.println("");
|
|
display.print("Pump state: ");
|
|
display.println(digitalRead(PinPUMP));
|
|
break;
|
|
|
|
case 2:
|
|
// display Logo
|
|
display.drawBitmap(0, 0, bmpCanGrow_Logo, 128, 32, WHITE);
|
|
display.display();
|
|
display.setCursor(0,32);
|
|
display.println(GrowName);
|
|
display.print("DoG: ");
|
|
display.print(DayOfGrow);
|
|
display.print(", ");
|
|
display.println(timeClient.getFormattedTime());
|
|
display.print("IP: ");
|
|
display.println(WiFi.localIP());
|
|
break;
|
|
}
|
|
ScreenIterationPassed++;
|
|
display.display();
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
* Setup
|
|
*
|
|
*/
|
|
void setup() {
|
|
|
|
// setup pins
|
|
pinMode(PinFAN, OUTPUT);
|
|
pinMode(PINdht, INPUT);
|
|
pinMode(PINwaterlevel, OUTPUT);
|
|
pinMode(PINsoilmoisture, OUTPUT);
|
|
pinMode(PinLED, OUTPUT);
|
|
pinMode(PinPUMP, OUTPUT);
|
|
pinMode(PinWIPE, OUTPUT);
|
|
|
|
|
|
// set all OUTPUT to low
|
|
digitalWrite(PinFAN, LOW);
|
|
digitalWrite(PINwaterlevel, LOW);
|
|
digitalWrite(PINsoilmoisture, LOW);
|
|
digitalWrite(PinLED, LOW);
|
|
digitalWrite(PinPUMP, LOW);
|
|
|
|
// set PWM frequency lower to avoid annoying noises
|
|
// in combination with 47uF at fan output, 220Hz is kinda sweetspot
|
|
// 220Hz is note A3 btw.
|
|
analogWriteFreq(220);
|
|
|
|
// Start EEPROM
|
|
EEPROM.begin(512);
|
|
|
|
// 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
|
|
Serial.println("420");
|
|
Serial.println(".:: CanGrow Start ::.");
|
|
|
|
// initialise Wire for I2C
|
|
Wire.begin();
|
|
|
|
// initialise I2C display
|
|
display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // Address 0x3C for 128x64
|
|
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 (PinWIPE) 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
|
|
|
|
display.fillRect(0,36,128,64-36, 0);
|
|
display.setCursor(0,36);
|
|
display.println("To wipe EEPROM pull");
|
|
display.println("D4 (PinWIPE) to GND");
|
|
display.display();
|
|
|
|
// blink with the onboard LED on D4 (PinWIPE)
|
|
for(byte i = 0; i <= 6 ; i++) {
|
|
if(i % 2) {
|
|
digitalWrite(PinWIPE, LOW);
|
|
} else {
|
|
digitalWrite(PinWIPE, HIGH);
|
|
}
|
|
delay(333);
|
|
}
|
|
// set back to HIGH because thats the default
|
|
digitalWrite(PinWIPE, HIGH);
|
|
//delay(2000);
|
|
|
|
// read status from PinWIPE to WIPE
|
|
// when PinWIPE is set to LOW, wipe EEPROM
|
|
if(digitalRead(PinWIPE) == 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();
|
|
|
|
Serial.println(".:: CanGrow Ready ::.");
|
|
delay(1000);
|
|
if(strlen(GrowName) > 0 ) {
|
|
display.clearDisplay();
|
|
display.display();
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
*
|
|
*
|
|
* Loop
|
|
*
|
|
*
|
|
*/
|
|
void loop() {
|
|
// var definition
|
|
unsigned long currentRuntime = millis();
|
|
|
|
|
|
// first we call webserver handle client
|
|
webserver.handleClient();
|
|
|
|
|
|
// do every second when everything is configured and grow is started
|
|
if( (configured == true) && (strlen(GrowName) > 0) && (currentRuntime - outputPrevTime >= 1000) ){
|
|
|
|
// refresh all sensor values
|
|
refreshSensors();
|
|
|
|
// calculate acutal DayOfGrow
|
|
DayOfGrow = int(ceil(float((timeClient.getEpochTime() - GrowStart) / 60 / 60 / 24)));
|
|
// decide if we are in Veg or Bloom phase of grow
|
|
// when DayOfGrow is larger then DaysVeg we must be in Bloom
|
|
|
|
// set the actual state of the Grow LED
|
|
// when being in Maintenance Mode and UseRelaisLED not true,
|
|
// dimm the light
|
|
if(MaintenanceMode == true) {
|
|
if((currentRuntime - MaintenanceStarted <= MaintenanceDuration * 1000 ) && (UseLEDrelais == false)) {
|
|
// in case of being in Maintenance Mode , dimm the grow light when not a relais is used
|
|
setOutput(1, 20);
|
|
} else {
|
|
MaintenanceMode = false;
|
|
}
|
|
} else {
|
|
controlLED();
|
|
}
|
|
displayScreens();
|
|
|
|
// current time gets previous time for new interval
|
|
outputPrevTime = currentRuntime;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
*
|
|
*
|
|
* Web related stuff
|
|
*
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* 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("/system/restart", HTTP_GET, SysRestart);
|
|
// wipe eeprom triggered from WebGui
|
|
webserver.on("/system/wipe", HTTP_GET, SysWipe);
|
|
// 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(Sys404);
|
|
|
|
// switching MOSFETs
|
|
webserver.on("/switch", HTTP_POST, POSTsetOutput);
|
|
|
|
// api stuff
|
|
webserver.on("/api/sensors", HTTP_GET, APIgetSensors);
|
|
|
|
|
|
// gauge meter stuff
|
|
webserver.on("/gauge.css", HTTP_GET, WEBgaugeCss);
|
|
webserver.on("/gauge.js", HTTP_GET, WEBgaugeJs);
|
|
}
|
|
|
|
/*
|
|
*
|
|
* return functions, they return things for the web stuff
|
|
*/
|
|
|
|
String returnHTMLheader(String MenuEntry = "") {
|
|
String header;
|
|
String activeMenu = "class='activeMenu'";
|
|
// add first part of the header
|
|
header += FPSTR(HTMLheaderP1);
|
|
// add title tag
|
|
header += "<title>";
|
|
// check if GrowName was set. if yes, its part of the page title.
|
|
if(strlen(GrowName) > 0) {
|
|
header += "CanGrow - ";
|
|
header += GrowName;
|
|
} else {
|
|
header += "CanGrow";
|
|
}
|
|
// close title tag
|
|
header += "</title>\n";
|
|
|
|
// add additional header stuff, like loading guage files
|
|
if(MenuEntry == "root") {
|
|
header += "<link rel='stylesheet' type='text/css' href='gauge.css'>";
|
|
}
|
|
|
|
header += FPSTR(HTMLheaderP2);
|
|
|
|
// first menu entry
|
|
header += "<li><a href='/'>🌱 ";
|
|
if(strlen(GrowName) > 0) {
|
|
header += GrowName;
|
|
} else {
|
|
header += "CanGrow";
|
|
}
|
|
header += "</a></li>\n";
|
|
|
|
// second menu entry
|
|
header += "<li><a href='/growSettings' ";
|
|
if(MenuEntry == "growSettings") {
|
|
header += activeMenu;
|
|
}
|
|
header += ">🔆 Grow settings</a></li>\n";
|
|
|
|
// third menu entry
|
|
header += "<li><a href='/systemSettings' ";
|
|
if(MenuEntry == "systemSettings") {
|
|
header += activeMenu;
|
|
}
|
|
header += ">⚙ System settings</a></li>\n";
|
|
|
|
// fourth menu entry
|
|
header += "<li><a href='/wifiSettings' ";
|
|
if(MenuEntry == "wifiSettings") {
|
|
header += activeMenu;
|
|
}
|
|
header += ">📡 WiFi settings</a></li>\n";
|
|
|
|
// fifth menu entry
|
|
header += "<li><a href='/help' ";
|
|
if(MenuEntry == "help") {
|
|
header += activeMenu;
|
|
}
|
|
header += ">❓ Help</a></li>\n";
|
|
|
|
// sixth menu entry
|
|
header += "<li><span class='MenuTime'>";
|
|
header += timeClient.getFormattedTime();
|
|
header += "</span></li>\n";
|
|
|
|
// seventh menu entry
|
|
header += "<li><a href='https://git.la10cy.net/DeltaLima/CanGrow' target='_blank'>CanGrow v";
|
|
header += CanGrowVer;
|
|
header += "</a></li>\n";
|
|
|
|
if(MaintenanceMode == true) {
|
|
// status icons
|
|
header += "<li><span>🇲</span></li>\n";
|
|
}
|
|
|
|
// close <ul> and start <div>
|
|
header += "</ul><div class='center'>";
|
|
|
|
if(NeedRestart == true) {
|
|
header += FPSTR(HTMLneedRestart);
|
|
}
|
|
|
|
return header;
|
|
}
|
|
|
|
/*
|
|
* returnSelected(bool)
|
|
* returns char[] "selected" if bool is true
|
|
* useful for html forms, to represet 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;
|
|
}
|
|
|
|
|
|
/*
|
|
*
|
|
* System pages like infos, errors
|
|
*
|
|
*/
|
|
|
|
void SysRestart() {
|
|
String body = returnHTMLheader();
|
|
// TODO only debug and development solution, remove this later
|
|
if( (webserver.hasArg("confirmed")) || (NeedRestart == true) ) {
|
|
body += "<h1>❗ Restarting</h1>";
|
|
body += "<div class='infomsg'>After restart CanGrow will be connected to WiFi SSID<br><b>";
|
|
body += WIFIssid;
|
|
body += "</b><br>You get its IP-Address from the display or serial console.</div>";
|
|
body += FPSTR(HTMLfooter);
|
|
webserver.send(200, "text/html", body);
|
|
Serial.println("Restarting... see you soon space cowboy!");
|
|
delay(1000);
|
|
ESP.restart();
|
|
} else {
|
|
body += "<h1>❗ Restart CanGrow</h1>";
|
|
body += "<div class='infomsg'>Do you want to restart CanGrow?";
|
|
body += "<br>Please confirm.";
|
|
body += "<form action='/system/restart'><input type='hidden' name='confirmed' value='true' /><input type='submit' value='Confirm restart' /></form>";
|
|
body += "</div>";
|
|
body += FPSTR(HTMLfooter);
|
|
webserver.send(200, "text/html", body);
|
|
}
|
|
}
|
|
|
|
void SysWipe() {
|
|
String body = returnHTMLheader();
|
|
// TODO only debug and development solution, remove this later
|
|
if(webserver.hasArg("confirmed")) {
|
|
body += "<div class='warnmsg'><h2>!! Wiping CanGrow's EEPROM !!</h2><br>Device will restart in a few seconds.</div>";
|
|
body += FPSTR(HTMLfooter);
|
|
webserver.send(200, "text/html", body);
|
|
wipeEEPROM();
|
|
} else {
|
|
body += "<h1>❗❗ Wipeing EEPROM</h1>";
|
|
body += "<div class='warnmsg'>All settings will be removed!!<br>";
|
|
body += "<br>Please confirm wiping the EEPROM";
|
|
body += "<form action='/system/wipe'><input type='hidden' name='confirmed' value='true' /><input type='submit' value='Confirm wiping' /></form>";
|
|
body += "</div>";
|
|
body += FPSTR(HTMLfooter);
|
|
webserver.send(200, "text/html", body);
|
|
}
|
|
}
|
|
|
|
void Sys404() {
|
|
String body = returnHTMLheader();
|
|
body += "<div class='warnmsg'><h1>❗ ️ 404 - not found</h1></div>";
|
|
body += FPSTR(HTMLfooter);
|
|
webserver.send(404, "text/html", body);
|
|
}
|
|
|
|
void Syslogout() {
|
|
String body = returnHTMLheader();
|
|
body += "<h1>you are logged out.</h1>";
|
|
body += FPSTR(HTMLfooter);
|
|
|
|
// TODO does not work atm
|
|
webserver.send(401, "text/html", body);
|
|
}
|
|
|
|
|
|
/*
|
|
* TODO
|
|
* DOES NOT WORK WHEN CONNECTED TO EXISTING WIFI
|
|
* IDK WHY
|
|
*
|
|
|
|
* void WebAuth() {
|
|
*
|
|
* char webAuthRealm[] = "CanGrowRealm";
|
|
* if(!webserver.authenticate(WebUiUsername, WebUiPassword)) {
|
|
* String body = FPSTR(HTMLheader);
|
|
* body += "<h1>Login failed.</h1>";
|
|
* 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);
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
/*
|
|
*
|
|
* Main UI pages
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* Gauge meter files
|
|
*/
|
|
void WEBgaugeCss() {
|
|
//String css = CSSgauge;
|
|
webserver.send(200, "text/css", FPSTR(CSSgauge));
|
|
}
|
|
|
|
void WEBgaugeJs() {
|
|
//String javascript = JSgauge;
|
|
webserver.send(200, "text/javascript", FPSTR(JSgauge));
|
|
}
|
|
|
|
/*
|
|
* Root page
|
|
*/
|
|
|
|
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 = returnHTMLheader("root");
|
|
|
|
body += "<h2>🌱 ";
|
|
body += GrowName;
|
|
body += "</h2>";
|
|
|
|
// add gauge meter
|
|
body += FPSTR(HTMLgauge);
|
|
// and give javascript the values
|
|
// todo: auto refresh by api call
|
|
body += "<script>";
|
|
|
|
body += "gaugeTemperature.value('";
|
|
body += valTemperature;
|
|
body += "', 42, ' °C'); ";
|
|
|
|
body += "gaugeHumidity.value('";
|
|
body += valHumidity;
|
|
body += "'); ";
|
|
|
|
body += "gaugeSoilmoisture.value('";
|
|
body += valSoilmoisture;
|
|
body += "'); ";
|
|
|
|
body += "</script>";
|
|
|
|
body += "Grow started: ";
|
|
body += returnStrDateFromEpoch(GrowStart);
|
|
body += "<br>\n";
|
|
body += "Day of Grow: ";
|
|
body += DayOfGrow;
|
|
body += "<br>\n";
|
|
|
|
if(UsePump == true) {
|
|
body += "Pump water level: ";
|
|
switch(getWaterlevel()) {
|
|
case 0:
|
|
body += "<span style='color: green;'>OK</span>";
|
|
break;
|
|
case 1:
|
|
body += "<span style='color: yellow;'>Warning</span>";
|
|
break;
|
|
case 2:
|
|
body += "<span style='color: red;'>Critical</span>";
|
|
break;
|
|
}
|
|
}
|
|
body += "<br>\n";
|
|
body += "Growlight brightness: ";
|
|
body += ((PinLEDPWM * 100) / 255);
|
|
body += " %<br>\n";
|
|
body += "<form method='post' action='/switch'>\n";
|
|
body += "MOSFET<select id='output' name='output' >\n";
|
|
body += "<option disabled value='' selected hidden>---</option>\n";
|
|
body += "<option value='1'>LED</option>\n";
|
|
body += "<option value='2'>FAN</option>\n";
|
|
body += "<option value='3'>PUMP</option>\n";
|
|
body += "</select><br>";
|
|
|
|
body += "On/Off: <select id='state' name='state' >\n";
|
|
body += "<option disabled value='' selected hidden>---</option>\n";
|
|
body += "<option value='1'>On</option>\n";
|
|
body += "<option value='0'>Off</option>\n";
|
|
body += "</select><br>\n";
|
|
|
|
body += "Intensity: <input type='range' id='OutputPWM' name='OutputPWM' min='1' max='255' value='255'/><br>\n";
|
|
body += "Maintenance: <select id='EnableMaintenance' name='EnableMaintenance'>\n";
|
|
body += "<option disabled value='' selected hidden>---</option>\n";
|
|
body += "<option value='1'>On</option>\n";
|
|
body += "<option value='0'>Off</option>\n";
|
|
body += "</select><br>\n";
|
|
|
|
body += "<input type='submit' value='Save'>\n";
|
|
body += "</form>";
|
|
|
|
|
|
body += FPSTR(HTMLfooter);
|
|
|
|
webserver.send(200, "text/html", body);
|
|
}
|
|
}
|
|
|
|
/*
|
|
*
|
|
* settings pages
|
|
*
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* Grow page
|
|
*/
|
|
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 = returnHTMLheader("growSettings");
|
|
|
|
if(strlen(GrowName) < 1) {
|
|
body += "<h1>Final step: Grow settings</h1>";
|
|
body += "<p>Please configure all settings<br>";
|
|
body += "</p>";
|
|
GrowStart = timeClient.getEpochTime();
|
|
}
|
|
|
|
body += "<h2>🔆 Grow settings</h2>";
|
|
if(webserver.hasArg("success")) {
|
|
body += FPSTR(HTMLsuccess);
|
|
}
|
|
body += "<p>Here you can set everything grow related, like light hours, how much water, LED brightness<br>";
|
|
body += "</p>";
|
|
|
|
body += "<form method='post' action='/growSettings/save'>\n";
|
|
|
|
|
|
|
|
body += "Grow name: <input type='text' name='GrowName' maxlength='32' value='";
|
|
body += GrowName;
|
|
body+= "' required><br>\n";
|
|
|
|
|
|
// the input field, which calls javascript convertDateToEpoch() to write data to transmit to id GrowStart
|
|
body += "Grow start date: <input type='date' id='GrowStart_sel' onChange='convertDateToEpoch(\"GrowStart_sel\", \"GrowStart\");' value='";
|
|
body += returnStrDateFromEpoch(GrowStart);
|
|
body += "' required><br>\n";
|
|
|
|
body += "<input type='hidden' id='GrowStart' name='GrowStart' value='";
|
|
body += GrowStart;
|
|
body+= "' required>\n";
|
|
|
|
|
|
|
|
body += "Vegetation duration: <input class='inputShort' type='number' name='DaysVeg' min='0' max='255' value='";
|
|
body += DaysVeg;
|
|
body+= "' required>Days<br>\n";
|
|
|
|
body += "Bloom duration: <input class='inputShort' type='number' name='DaysBloom' min='0' max='255' value='";
|
|
body += DaysBloom;
|
|
body+= "' required> Days<br>\n";
|
|
|
|
body += "Time LED ON vegetation: <input class='inputShort' type='number' name='LighthoursVeg' min='0' max='255' value='";
|
|
body += LighthoursVeg;
|
|
body+= "' required> Hours<br>\n";
|
|
|
|
body += "Time LED ON bloom: <input class='inputShort' type='number' name='LighthoursBloom' min='0' max='255' value='";
|
|
body += LighthoursBloom;
|
|
body+= "' required> Hours<br>\n";
|
|
|
|
body += "Sunrise: <input class='inputShort' type='number' name='SunriseHour' min='0' max='23' value='";
|
|
body += SunriseHour;
|
|
body+= "' required>\n";
|
|
body += " <b>:</b> <input class='inputShort' type='number' name='SunriseMinute' min='0' max='59' value='";
|
|
body += SunriseMinute;
|
|
body+= "' required><br>\n";
|
|
|
|
// SunFade bool
|
|
body += "Fade in/out sunrise/sunset?: <select id='SunFade' name='SunFade' required>\n";
|
|
body += "<option value='1'" + returnStrSelected(SunFade, 1) + ">Yes</option>\n";
|
|
body += "<option value='0'" + returnStrSelected(SunFade, 0) + ">No</option>\n";
|
|
body += "</select><br>\n";
|
|
|
|
body += "Fade duration: <input class='inputShort' type='number' name='SunFadeDuration' min='1' max='255' value='";
|
|
body += SunFadeDuration;
|
|
body+= "' required> Minutes<br>\n";
|
|
|
|
if(UseLEDrelais == false) {
|
|
body += "LED brightness: <input type='range' id='PinLEDPWM' name='PinLEDPWM' min='1' max='255' value='";
|
|
body += PinLEDPWM;
|
|
body += "'/> %<br>\n";
|
|
}
|
|
|
|
if(UseFANrelais == false) {
|
|
body += "FAN speed: <input type='range' id='PinFANPWM' name='PinFANPWM' min='1' max='255' value='";
|
|
body += PinFANPWM;
|
|
body += "'/> %<br>\n";
|
|
}
|
|
|
|
body += "<input type='submit' value='Save'>\n";
|
|
body += "</form>\n";
|
|
body += FPSTR(JSconvertDateToEpoch);
|
|
body += FPSTR(HTMLfooter);
|
|
|
|
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 = returnHTMLheader("systemSettings");
|
|
|
|
if(configured == false) {
|
|
body += "<h1>Step 2: System settings</h1>";
|
|
body += "<p>Please configure all settings<br>";
|
|
body += "</p>";
|
|
}
|
|
|
|
body += "<h2>⚙ System settings</h2>";
|
|
if(webserver.hasArg("success")) {
|
|
body += FPSTR(HTMLsuccess);
|
|
}
|
|
body += "<p>here you can set which features and sensors you use<br>";
|
|
body += "</p>";
|
|
|
|
// form starts
|
|
body += "<form method='post' action='/systemSettings/save'>\n";
|
|
|
|
// UseFan bool
|
|
body += "Use FAN: <select id='UseFan' name='UseFan' required>\n";
|
|
if(configured == false){body += "<option disabled value='' selected hidden>---</option>\n";}
|
|
body += "<option value='1'" + returnStrSelected(UseFan, 1) + ">Yes</option>\n";
|
|
body += "<option value='0'" + returnStrSelected(UseFan, 0) + ">No</option>\n";
|
|
body += "</select><br>\n";
|
|
|
|
// UsePump bool
|
|
body += "Use PUMP: <select id='UsePump' name='UsePump' required>\n";
|
|
if(configured == false){body += "<option disabled value='' selected hidden>---</option>\n";}
|
|
body += "<option value='1'" + returnStrSelected(UsePump, 1) + ">Yes</option>\n";
|
|
body += "<option value='0'" + returnStrSelected(UsePump, 0) + ">No</option>\n";
|
|
body += "</select><br>\n";
|
|
|
|
// UseLEDrelais bool
|
|
body += "Use relais for LED: <select id='UseLEDrelais' name='UseLEDrelais' required>\n";
|
|
if(configured == false){body += "<option disabled value='' selected hidden>---</option>\n";}
|
|
body += "<option value='1'" + returnStrSelected(UseLEDrelais, 1) + ">Yes</option>\n";
|
|
body += "<option value='0'" + returnStrSelected(UseLEDrelais, 0) + ">No</option>\n";
|
|
body += "</select><br>\n";
|
|
|
|
// UseFANrelais bool
|
|
body += "Use relais for FAN: <select id='UseFANrelais' name='UseFANrelais' required>\n";
|
|
if(configured == false){body += "<option disabled value='' selected hidden>---</option>\n";}
|
|
body += "<option value='1'" + returnStrSelected(UseFANrelais, 1) + ">Yes</option>\n";
|
|
body += "<option value='0'" + returnStrSelected(UseFANrelais, 0) + ">No</option>\n";
|
|
body += "</select><br>\n";
|
|
|
|
// TODO ugly. can this done be better?
|
|
// PumpOnTime int
|
|
body += "PUMP ON time: <input class='inputShort' type='number' name='PumpOnTime' min='0' max='255' value='";
|
|
body += PumpOnTime;
|
|
body += "' required> Seconds<br>\n";
|
|
|
|
// MoistureSensor_Type byte
|
|
body += "Soilmoisture sensor: <select id='MoistureSensor_Type' name='MoistureSensor_Type' required>\n";
|
|
if(configured == false) {
|
|
body += "<option disabled value='' selected hidden>---</option>\n";
|
|
}
|
|
body += "<option value='1'" + returnStrSelected(MoistureSensor_Type, 1) + ">Analog capacitive</option>\n";
|
|
body += "<option value='2'" + returnStrSelected(MoistureSensor_Type, 2) + ">I2C chirp</option>\n";
|
|
body += "</select><br>\n";
|
|
|
|
// SoilmoistureLow byte
|
|
body += "Soilmoisture low: <input class='inputShort' type='number' name='SoilmoistureLow' min='0' value='";
|
|
body += SoilmoistureLow;
|
|
body += "' required> %<br>\n";
|
|
|
|
// TemperatureSensor_Type byte
|
|
body += "Temperature sensor: <select id='TemperatureSensor_Type' name='TemperatureSensor_Type' required>\n";
|
|
if(configured == false) {
|
|
body += "<option disabled value='' selected hidden>---</option>\n";
|
|
}
|
|
body += "<option value='1'" + returnStrSelected(TemperatureSensor_Type, 1) + ">DHT11/22</option>\n";
|
|
body += "<option value='2'" + returnStrSelected(TemperatureSensor_Type, 2) + ">I2C chirp</option>\n";
|
|
body += "</select><br>\n";
|
|
|
|
// NtpOffset int
|
|
body += "NTP offset: <input class='inputShort' type='number' name='NtpOffset' min='-12' max='14' value='";
|
|
body += NtpOffset;
|
|
body+= "' required> Hours<br>\n";
|
|
|
|
body += "Maintenance Duration: <input class='inputShort' type='number' name='MaintenanceDuration' min='0' max='900' value='";
|
|
body += MaintenanceDuration;
|
|
body += "' required> Seconds<br>\n";
|
|
|
|
body += "<input type='submit' value='Save'>\n";
|
|
body += "</form>\n";
|
|
|
|
body += FPSTR(HTMLfooter);
|
|
|
|
webserver.send(200, "text/html", body);
|
|
}
|
|
}
|
|
|
|
void WEBwifiSettings() {
|
|
byte ssidsAvail = WiFi.scanNetworks();
|
|
String body = returnHTMLheader("wifiSettings");
|
|
if(FirstRun == true) {
|
|
body += "<h1>Welcome!</h1>";
|
|
body += "<p>CanGrow is actually unconfigured. You need to Setup your WiFi first down below.<br>";
|
|
body += "<br>After you entered your WiFi connection details, you need to restart and are step closer to your grow 🥦";
|
|
body += "<br>";
|
|
body += "</p>";
|
|
}
|
|
body += "<h2>📡 WiFi settings</h2>\n";
|
|
|
|
if(webserver.hasArg("success")) {
|
|
body += FPSTR(HTMLsuccess);
|
|
}
|
|
|
|
if(FirstRun == false) {
|
|
body += "<u>Current Settings:</u><br>";
|
|
body += "WiFi SSID: <b>";
|
|
body += WIFIssid;
|
|
body += "</b><br>\n";
|
|
body += "Use DHCP: <b>";
|
|
body += WIFIuseDHCP;
|
|
body += "</b><br>\n";
|
|
body += "IP address: <b>";
|
|
body += WiFi.localIP().toString();
|
|
body += "</b><br>\n";
|
|
body += "Subnet mask: <b>";
|
|
body += WiFi.subnetMask().toString();
|
|
body += "</b><br>\n";
|
|
body += "Gateway: <b>";
|
|
body += WiFi.gatewayIP().toString();
|
|
body += "</b><br>\n";
|
|
body += "DNS: <b>";
|
|
body += WiFi.dnsIP().toString();
|
|
body += "</b><br><br>\n";
|
|
}
|
|
|
|
body += "<p>Select your wifi network from the SSID list.<br>To use DHCP leave IP, Subnet, Gateway and DNS fields blank!</p>";
|
|
body += "<form method='post' action='/wifiSettings/save'>\n";
|
|
body += "SSID: <select id='WIFIssid' name='WIFIssid' required>\n";
|
|
body += "<option disabled value='' selected hidden>-Select your network-</option>";
|
|
|
|
// build option list for selecting wifi
|
|
Serial.println("Available Wifis: ");
|
|
for(int i = 0 ; i < ssidsAvail; i++) {
|
|
String wifiName = WiFi.SSID(i);
|
|
Serial.println(wifiName);
|
|
body += "<option value='" + wifiName + "'>";
|
|
body += wifiName + "</option>\n";
|
|
}
|
|
body += "</select><br>\n";
|
|
body += "Password: <input type='password' name='WIFIpassword'><br>\n";
|
|
body += "IP: <input type='text' name='WIFIip'><br>\n";
|
|
body += "Subnet mask: <input type='text' name='WIFInetmask'><br>\n";
|
|
body += "Gateway: <input type='text' name='WIFIgateway'><br>\n";
|
|
body += "DNS: <input type='text' name='WIFIdns'><br>\n";
|
|
body += "<input type='submit' value='Save'>\n";
|
|
body += "</form>\n";
|
|
body += FPSTR(HTMLfooter);
|
|
|
|
webserver.send(200, "text/html", body);
|
|
}
|
|
|
|
void WEBhelp() {
|
|
String body = returnHTMLheader("help");
|
|
body += FPSTR(HTMLhelp);
|
|
body += FPSTR(HTMLfooter);
|
|
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
|
|
PinLEDPWM = 255;
|
|
} else {
|
|
// otherwise just do PWM
|
|
PinLEDPWM = webserver.arg("PinLEDPWM").toInt();
|
|
}
|
|
|
|
if(UseFANrelais == true) {
|
|
// if a relais is used to turn on grow light, we force PWM to max val
|
|
PinFANPWM = 255;
|
|
} else {
|
|
// otherwise just do PWM
|
|
PinFANPWM = webserver.arg("PinFANPWM").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();
|
|
SunFade = webserver.arg("SunFade").toInt();
|
|
SunFadeDuration = webserver.arg("SunFadeDuration").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, PinLEDPWM);
|
|
// size is 1 byte
|
|
EEPROM.put(216, PinFANPWM);
|
|
EEPROM.put(217, SunFade);
|
|
EEPROM.put(218, SunFadeDuration);
|
|
|
|
EEPROM.commit();
|
|
|
|
|
|
|
|
|
|
//analogWrite(PinLED, PinLEDPWM);
|
|
|
|
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("PinLEDPWM: ");
|
|
Serial.println(PinLEDPWM);
|
|
Serial.print("PinFANPWM: ");
|
|
Serial.println(PinFANPWM);
|
|
|
|
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();
|
|
UseFANrelais = webserver.arg("UseFANrelais").toInt();
|
|
TemperatureSensor_Type = webserver.arg("TemperatureSensor_Type").toInt();
|
|
MaintenanceDuration = 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);
|
|
// size is 1 byte
|
|
EEPROM.put(215, UseFANrelais);
|
|
EEPROM.put(219, MaintenanceDuration);
|
|
|
|
// write data to EEPROM
|
|
EEPROM.commit();
|
|
|
|
|
|
|
|
// update time with new offset
|
|
timeClient.setTimeOffset(NtpOffset * 60 * 60);
|
|
timeClient.update();
|
|
|
|
Serial.println(":: POSTsystemSettings ::");
|
|
|
|
// when user uses an relais for LED control, we force here PinLEDPWM to 255
|
|
// to ensure nothing bad happens
|
|
if(UseLEDrelais == true) {
|
|
PinLEDPWM = 255;
|
|
EEPROM.put(213, PinLEDPWM);
|
|
EEPROM.commit();
|
|
Serial.println("UseLEDrelais is 1, forcing PinLEDPWM to max to prevent relais damage");
|
|
}
|
|
|
|
if(UseFANrelais == true) {
|
|
PinFANPWM = 255;
|
|
EEPROM.put(215, PinFANPWM);
|
|
EEPROM.commit();
|
|
Serial.println("UseFANrelais is 1, forcing PinFANPWM to max to prevent relais damage");
|
|
}
|
|
|
|
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.print("UseFANrelais: ");
|
|
Serial.println(UseFANrelais);
|
|
Serial.print("TemperatureSensor_Type: ");
|
|
Serial.println(TemperatureSensor_Type);
|
|
Serial.print("MaintenanceDuration: ");
|
|
Serial.println(MaintenanceDuration);
|
|
|
|
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 POSTsetOutput() {
|
|
byte OutputState = webserver.arg("state").toInt();
|
|
byte OutputNr = webserver.arg("output").toInt();
|
|
//PinLEDPWM = webserver.arg("PinLEDPWM").toInt();
|
|
byte OutputPWM = webserver.arg("OutputPWM").toInt();
|
|
byte EnableMaintenance = webserver.arg("EnableMaintenance").toInt();
|
|
|
|
Serial.println(":: POSTsetOutput ::");
|
|
Serial.print("OutputState: ");
|
|
Serial.println(OutputState);
|
|
Serial.print("OutputNr: ");
|
|
Serial.println(OutputNr);
|
|
Serial.print("EnableMaintenance: ");
|
|
Serial.println(EnableMaintenance);
|
|
|
|
if((OutputNr > 3) || (OutputNr < 1) || (OutputState > 255) || (OutputState < 0)) {
|
|
webserver.send(400, "text/plain", "not valid\n");
|
|
} else {
|
|
|
|
if(OutputState > 0){
|
|
setOutput(OutputNr, OutputPWM);
|
|
} else {
|
|
setOutput(OutputNr, 0);
|
|
}
|
|
|
|
|
|
webserver.sendHeader("Location", String("/?success"), true);
|
|
webserver.send(302, "text/plain", "switch: success!\n");
|
|
}
|
|
|
|
if(EnableMaintenance > 0 ) {
|
|
MaintenanceMode = true;
|
|
MaintenanceStarted = millis();
|
|
} else {
|
|
MaintenanceMode = false;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* API section
|
|
*
|
|
*/
|
|
|
|
// return as json all sensor readings
|
|
void APIgetSensors() {
|
|
|
|
JsonDocument jsonSensors;
|
|
|
|
JsonArray arraySoilmoisture = jsonSensors["soilmoisture"].to<JsonArray>();
|
|
arraySoilmoisture.add(getSoilmoisture(1));
|
|
arraySoilmoisture.add(getSoilmoisture(2));
|
|
JsonArray arrayTemperature = jsonSensors["temperature"].to<JsonArray>();
|
|
arrayTemperature.add(getTemperature(1));
|
|
arrayTemperature.add(getTemperature(2));
|
|
jsonSensors["humidity"] = getHumidity();
|
|
jsonSensors["chirpLight"] = getLightchirp();
|
|
|
|
String body;
|
|
serializeJsonPretty(jsonSensors, body);
|
|
webserver.send(200, "text/json", body);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
*
|
|
* TODO LIST / NOTES
|
|
*
|
|
*
|
|
* - when PWM for fan is set, set fan speed to regulate humidity and
|
|
* temperature, depending on which phase of grow the plant is
|
|
* (https://www.royalqueenseeds.de/blog-cannabisanbau-im-grow-room-relative-luftfeuchtigkeit-und-temperaturen-n243)
|
|
* - re-organize EEPROM saved values.
|
|
* - prevent GrowStart to be in the future
|
|
* - Display values on OLED screen, iterate through different vies
|
|
* - maybe let the user configure some screens to display.
|
|
* - collect sensor data within 1 sec millis() case, dont collect them when e.g. a page is called
|
|
*
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
*
|
|
*
|
|
* 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);
|
|
}
|
|
*/
|
|
|
|
|