Created
September 11, 2017 14:34
-
-
Save ItKindaWorks/d94f6031fb377df8ddde56468c50833d to your computer and use it in GitHub Desktop.
An ESP8266 program to control valves for home irrigation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
watering.ino | |
Copyright (c) 2016 ItKindaWorks All right reserved. | |
github.com/ItKindaWorks | |
watering.ino is free software: you can redistribute it and/or modify | |
it under the terms of the GNU General Public License as published by | |
the Free Software Foundation, either version 3 of the License, or | |
(at your option) any later version. | |
watering.ino is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License for more details. | |
You should have received a copy of the GNU General Public License | |
along with watering.ino. If not, see <http://www.gnu.org/licenses/>. | |
*/ | |
#include "ESPHelper.h" | |
#include "Metro.h" | |
#include <TimeLib.h> | |
#include <WiFiUdp.h> | |
#include <Bounce2.h> | |
//setup macros for time | |
#define SECOND 1000L | |
#define MINUTE SECOND * 60L | |
#define HOUR MINUTE * 60L | |
//pin defs | |
const int relay1 = 13; | |
const int relay2 = 12; | |
const int relay3 = 14; | |
const int button1 = 5; | |
const int button2 = 4; | |
//debouncers for each button | |
Bounce buttonDebouncer1 = Bounce(); | |
Bounce buttonDebouncer2 = Bounce(); | |
//consts for the timers for a cycle and section | |
const unsigned long SECTION_TIME = 15L * MINUTE; | |
const unsigned long CYCLE_TIME = 3 * SECTION_TIME; | |
const int TRIGGER_HOUR = 6; | |
const int TRIGGER_MIN = 30; | |
const int TIMEZONE = -4; | |
//ESPHelper vars | |
const char* HOSTNAME = "ESP-Water"; | |
const char* OTA_PASS = "OTA_PASSWORD"; | |
const char* CALLBACK_TOPIC = "/home/watering"; | |
netInfo homeNet = { .mqttHost = "YOUR MQTT-IP", //can be blank if not using MQTT | |
.mqttUser = "YOUR MQTT USERNAME", //can be blank | |
.mqttPass = "YOUR MQTT PASSWORD", //can be blank | |
.mqttPort = 1883, //default port for MQTT is 1883 - only chance if needed. | |
.ssid = "YOUR SSID", | |
.pass = "YOUR NETWORK PASS"}; | |
ESPHelper myESP(&homeNet); | |
//NTP setup vars | |
static const char ntpServerName[] = "us.pool.ntp.org"; | |
const int timeZone = TIMEZONE; | |
WiFiUDP Udp; | |
unsigned int localPort = 8888; // local port to listen for UDP packets | |
time_t getNtpTime(); | |
void sendNTPpacket(IPAddress &address); | |
//runCycle & hasStarted are variables that keep track of whether or not a cycle should be running. | |
//runCycle tells the system that a cycle is running and hasStarted tells the system whether or not it has triggered | |
//the start to a cycle. Basically runCycle is the overall tracker and hasStarted just keeps track to make sure that we dont | |
//continually "start" a cycle | |
bool runCycle = false; | |
bool hasStarted = false; | |
//mode determines which section is being watered/which valve is active. | |
int mode = 0; | |
//allows user to send a command and the system will skip the next watering cycle | |
bool skipNext = false; | |
//testRunning is flagged when the system is running a test | |
bool testRunning = false; | |
//button states | |
bool buttonState1 = false; | |
bool buttonState2 = false; | |
//whether the NTP has been initialized | |
bool initDone = false; | |
//timers for various watering functions | |
Metro cycleTimer = Metro(CYCLE_TIME); | |
Metro sectionTimer = Metro(SECTION_TIME); | |
Metro testTimer = Metro(30 * SECOND); | |
void setup() { | |
//setup the relay pins | |
pinMode(relay1, OUTPUT); | |
pinMode(relay2, OUTPUT); | |
pinMode(relay3, OUTPUT); | |
delay(100); | |
setValve(0); //init to valves off | |
//setup the button pins and attach to debouncers | |
pinMode(button1, INPUT); | |
pinMode(button2, INPUT); | |
buttonDebouncer1.attach(button1); | |
buttonDebouncer1.interval(10); | |
buttonDebouncer2.attach(button2); | |
buttonDebouncer2.interval(10); | |
//setup ESPHelper | |
myESP.OTA_enable(); | |
myESP.OTA_setPassword(OTA_PASS); | |
myESP.OTA_setHostnameWithVersion(HOSTNAME); | |
myESP.enableHeartbeat(2); | |
myESP.setHopping(false); | |
myESP.addSubscription(CALLBACK_TOPIC); | |
myESP.setCallback(callback); | |
myESP.begin(); //start ESPHelper | |
//setup NTP | |
Udp.begin(localPort); | |
setSyncProvider(getNtpTime); | |
setSyncInterval(300); | |
} | |
void loop(){ | |
if(myESP.loop() >= WIFI_ONLY){ | |
//once we have a WiFi connection trigger some extra setup | |
if(!initDone){ | |
delay(100); | |
//setup the NTP connection and get the current time | |
setupNTP(); | |
delay(200); | |
//only set initDone to true if the time is set | |
if(timeStatus() != timeNotSet){ | |
initDone = true; | |
postTimeStamp("Watering System Started"); | |
} | |
} | |
if(initDone){ | |
//refresh the button states | |
buttonDebouncer1.update(); | |
buttonDebouncer2.update(); | |
//get the current time | |
time_t t = now(); | |
//button1 triggers the start of a cycle | |
if(buttonDebouncer1.fell() && !runCycle){ | |
postTimeStamp("Button initiated cycle"); | |
runCycle = true; | |
cycleTimer.reset(); | |
sectionTimer.reset(); | |
} | |
//button2 triggers a valve test | |
else if(buttonDebouncer2.fell()){ | |
postTimeStamp("Watering Test Started"); | |
testRunning = true; | |
testTimer.reset(); | |
} | |
//trigger the start of a cycle if needed based on time | |
if(hour(t) == TRIGGER_HOUR && minute(t) == TRIGGER_MIN && !runCycle && !skipNext){ | |
postTimeStamp("Time initiated cycle"); | |
runCycle = true; | |
cycleTimer.reset(); | |
sectionTimer.reset(); | |
} | |
else if(hour(t) == TRIGGER_HOUR && minute(t) == TRIGGER_MIN + 1 && skipNext){ | |
postTimeStamp("Cycle Skipped"); | |
skipNext = false; | |
} | |
//cycle timer resets/ends a cycle after a set period of time | |
if(cycleTimer.check()){runCycle = false;} | |
if(testRunning){runTest();} | |
else{cycleHandler();} | |
} | |
} | |
delay(20); | |
} | |
//MQTT callback | |
void callback(char* topic, byte* payload, unsigned int length) { | |
//Convert topic to String to make it easier to work with | |
//Also fix the payload by null terminating it and also convert that | |
String topicStr = topic; | |
char newPayload[256]; | |
memcpy(newPayload, payload, length); | |
newPayload[length] = '\0'; | |
String payloadStr = newPayload; | |
if(topicStr.equals(CALLBACK_TOPIC)){ | |
if(newPayload[0] == '1'){ | |
skipNext = true; | |
postTimeStamp("Skipping next watering cycle"); | |
} | |
else if(newPayload[0] == '0'){ | |
skipNext = false; | |
postTimeStamp("Not skipping next watering cycle"); | |
} | |
} | |
} | |
void cycleHandler(){ | |
//if we want to run but have not started yet | |
if(runCycle && !hasStarted){ | |
hasStarted = true; | |
mode = 1; | |
sectionTimer.reset(); | |
postTimeStamp("Started Cycle");\ | |
return; | |
} | |
//if we dont want to run but we are currently running | |
else if(!runCycle && hasStarted){ | |
hasStarted = false; | |
setValve(0); | |
postTimeStamp("Ended Cycle"); | |
return; | |
} | |
//manages valves while a watering cycle is running | |
if(runCycle){ | |
if(sectionTimer.check()){ | |
mode++; | |
postTimeStamp("Mode Change"); | |
} | |
if(mode < 4){ | |
setValve(mode); | |
} | |
else{ | |
hasStarted = false; | |
setValve(0); | |
postTimeStamp("Ended Cycle via mode change"); | |
mode = 0; | |
runCycle = false; | |
} | |
} | |
} | |
void runTest (){ | |
static int testState = 1; | |
if(testTimer.check()){ | |
testState++; | |
} | |
if(testState < 4){setValve(testState);} | |
else{ | |
postTimeStamp("Watering Test Ended"); | |
testRunning = false; | |
setValve(0); | |
testState = 1; | |
} | |
} | |
void setValve(int valveNum){ | |
static int lastSet = -1; | |
if(valveNum == 1 && lastSet != valveNum){ | |
postTimeStamp("Valve 1 Open"); | |
digitalWrite(relay1, HIGH); | |
delay(100); | |
digitalWrite(relay2, LOW); | |
delay(100); | |
digitalWrite(relay3, LOW); | |
lastSet = valveNum; | |
} | |
else if(valveNum == 2 && lastSet != valveNum){ | |
postTimeStamp("Valve 2 Open"); | |
digitalWrite(relay1, LOW); | |
delay(100); | |
digitalWrite(relay2, HIGH); | |
delay(100); | |
digitalWrite(relay3, LOW); | |
lastSet = valveNum; | |
} | |
else if(valveNum == 3 && lastSet != valveNum){ | |
postTimeStamp("Valve 3 Open"); | |
digitalWrite(relay1, LOW); | |
delay(100); | |
digitalWrite(relay2, LOW); | |
delay(100); | |
digitalWrite(relay3, HIGH); | |
lastSet = valveNum; | |
} | |
else if(lastSet != valveNum){ | |
postTimeStamp("All Valves Closed"); | |
digitalWrite(relay1, LOW); | |
digitalWrite(relay2, LOW); | |
digitalWrite(relay3, LOW); | |
lastSet = valveNum; | |
} | |
} | |
//take a char* and use it as a message with an appended timestamp | |
void postTimeStamp(const char* text){ | |
char timeStamp[30]; | |
createTimeString(timeStamp, 30); | |
String pubString = String(HOSTNAME); | |
pubString += " : "; | |
pubString += text; | |
pubString += " - "; | |
pubString += timeStamp; | |
//conver the String into a char* | |
char message[128]; | |
pubString.toCharArray(message, 128); | |
myESP.publish("/home/watering/status", message, true); | |
} | |
//create a timestamp string | |
void createTimeString(char* buf, int length){ | |
time_t t = now(); | |
String timeString = String(hour(t)); | |
timeString += ":"; | |
timeString += minute(t); | |
timeString += ":"; | |
timeString += second(t); | |
timeString += " "; | |
timeString += month(t); | |
timeString += "/"; | |
timeString += day(t); | |
timeString += "/"; | |
timeString += year(t); | |
timeString.toCharArray(buf, length); | |
} | |
/*-------- NTP code ----------*/ | |
void setupNTP(){ | |
delay(200); | |
Udp.begin(localPort); | |
setSyncProvider(getNtpTime); | |
setSyncInterval(300); | |
} | |
const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message | |
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets | |
time_t getNtpTime(){ | |
IPAddress ntpServerIP; // NTP server's ip address | |
while (Udp.parsePacket() > 0) ; // discard any previously received packets | |
// get a random server from the pool | |
WiFi.hostByName(ntpServerName, ntpServerIP); | |
sendNTPpacket(ntpServerIP); | |
uint32_t beginWait = millis(); | |
while (millis() - beginWait < 1500) { | |
int size = Udp.parsePacket(); | |
if (size >= NTP_PACKET_SIZE) { | |
Udp.read(packetBuffer, NTP_PACKET_SIZE); // read packet into the buffer | |
unsigned long secsSince1900; | |
// convert four bytes starting at location 40 to a long integer | |
secsSince1900 = (unsigned long)packetBuffer[40] << 24; | |
secsSince1900 |= (unsigned long)packetBuffer[41] << 16; | |
secsSince1900 |= (unsigned long)packetBuffer[42] << 8; | |
secsSince1900 |= (unsigned long)packetBuffer[43]; | |
return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR; | |
} | |
} | |
return 0; // return 0 if unable to get the time | |
} | |
// send an NTP request to the time server at the given address | |
void sendNTPpacket(IPAddress &address) | |
{ | |
// set all bytes in the buffer to 0 | |
memset(packetBuffer, 0, NTP_PACKET_SIZE); | |
// Initialize values needed to form NTP request | |
// (see URL above for details on the packets) | |
packetBuffer[0] = 0b11100011; // LI, Version, Mode | |
packetBuffer[1] = 0; // Stratum, or type of clock | |
packetBuffer[2] = 6; // Polling Interval | |
packetBuffer[3] = 0xEC; // Peer Clock Precision | |
// 8 bytes of zero for Root Delay & Root Dispersion | |
packetBuffer[12] = 49; | |
packetBuffer[13] = 0x4E; | |
packetBuffer[14] = 49; | |
packetBuffer[15] = 52; | |
// all NTP fields have been given values, now | |
// you can send a packet requesting a timestamp: | |
Udp.beginPacket(address, 123); //NTP requests are to port 123 | |
Udp.write(packetBuffer, NTP_PACKET_SIZE); | |
Udp.endPacket(); | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment