/*
by Thomas H, Free for use and distribution
please link to this project
Version 0.3.0
*/
#include "pcf8574.h"
#include "LiquidCrystal_I2C.h" //https://github.com/johnrickman/LiquidCrystal_I2C/tree/master
//#include <Wire.h> /* I2C */ already in pcf8574.h and LiquidCrystal_I2C.h
#include <EEPROM.h>
#include "Button.h"
//CONSTANTS
const uint8_t VERSION[20] = "0.3.0";
const uint8_t RELAY_BOARDS_COUNT = 4;
const uint8_t RELAYS_PER_BOARD_COUNT = 8;
const uint8_t RELAYS_COUNT = RELAY_BOARDS_COUNT * RELAYS_PER_BOARD_COUNT;
const uint32_t MAX_START_TIME = 86399; //s, max. 23:59:59
const uint32_t MAX_RUN_TIME = 3600; //s, max. 1 hour
const uint8_t RELAY_ON = HIGH;
const uint8_t RELAY_OFF = LOW;
//EEPROM CONFIG
const uint16_t eeLength = EEPROM.length(); //whole size fo the EEPROM
const uint8_t eeStartAddress = 0; //EEPROM starting address
//I2C IO EXPANDER
PCF8574 *ex[4]; //4 io expanders
//I2C LCD
const uint8_t LCD_I2C_ADDRESS = 0x27;
const uint8_t LCD_CHARS_X_COUNT = 20;
const uint8_t LCD_CHARS_Y_COUNT = 4;
LiquidCrystal_I2C lcd(LCD_I2C_ADDRESS, LCD_CHARS_X_COUNT, LCD_CHARS_Y_COUNT); //I2C address, digits, lines
//LCD CHARS
byte charValveDisabled[] = {
B11111,
B10001,
B01010,
B00100,
B00100,
B01010,
B10001,
B11111
}; //as static image
byte charValveEnabled[] = {
B11111,
B11111,
B01110,
B00100,
B00100,
B01110,
B11111,
B11111
}; //as static image
byte charValveActiveAnim[5][8] = {
{
B10001,
B10001,
B01010,
B00100,
B00100,
B01010,
B10001,
B10001
},
{
B11111,
B11111,
B01010,
B00100,
B00100,
B01010,
B10001,
B10001
},
{
B10001,
B11111,
B01110,
B00100,
B00100,
B01110,
B10001,
B10001
},
{
B10001,
B10001,
B01110,
B00100,
B00100,
B01110,
B11111,
B10001
},
{
B10001,
B10001,
B01010,
B00100,
B00100,
B01010,
B11111,
B11111
}
}; //as animation with 5 images
byte charArrowLeft[] = {
B00000,
B00000,
B00100,
B01000,
B11111,
B01000,
B00100,
B00000
}; //as static image
//BUTTONS
Button btnUp(5);
Button btnDown(4);
Button btnEnter(3);
Button btnReturn(2);
//MENU
enum Menu {
MNone = 0,
MMain,
MSave,
MRelay,
MRelaySelection,
MRelayStartTime,
MRelayRunTime,
MRelayEnabling
};
String dbgMenuStr[8] {
"NONE",
"MAIN MENU",
"SAVE INFO",
"RELAY MENU",
"RELAY SELECT",
"RELAY START TIME",
"RELAY RUN TIME",
"RELAY ENABLING"
};
static uint8_t menuState = MMain;
static uint8_t prevMenuState = MNone; //need to be NONE at init to detect menu update
static bool menuUpdated = false;
static uint8_t MRelayIndex = 1;
static uint8_t relayIndex = 0; //initially points to first relay
static uint8_t prevRelayIndex = 255; //255 to detect a change to be able to see relay one at init
static uint8_t timerIndex = 0; //initially points to first relay
//DEL static bool updateMainMenu = false;
//DEL static bool updateRelayMenu = false;
//RTC
const uint8_t DS1307_ADDRESS = 0x68;
struct RtcData {
uint8_t seconds;
uint8_t minutes;
uint8_t hours;
uint8_t dayWeek;
uint8_t dayMonth;
uint8_t month;
uint8_t year;
uint32_t time = 0UL; //absolute time in seconds (hours, minutes, seconds) for saving/loading purposes
};
static RtcData rtc;
const byte ZERO = 0x00;
//Rain Sensor
static uint8_t rainIntensity = 0;
//EEPROM DATA
struct Relay {
uint32_t timeStart = 0; //time, 4 byte per 1 unsinged long
uint32_t timeRun = 0;
bool enabled = false; //enabler
bool active = false; //relay on/off
};
static Relay relay[32]; //initially the standard values are taken (0UL, false)
//LCD
enum LCD_CHARS {
DISABLED = 0,
ENABLED = 1,
ACTIVE_1 = 2,
ACTIVE_2 = 3,
ACTIVE_3 = 4,
ACTIVE_4 = 5,
ACTIVE_5 = 6,
ARROW = 7
};
void setup() {
delay(200); //wait for hardware init delays
Serial.begin(9600); // initialize serial for console prints for Arduino Uno
//PCF8574 based on setup by Pat Deegan with PCF8575
//uint8_t addr[4] = {0x20, 0x21, 0x22, 0x23 }; //issue to be checked
int addr[4] = {0x20, 0x21, 0x22, 0x23 }; // further io expanders: 0x24, 0x25, 0x26, 0x27 (reserved for LCD)
for(uint8_t i = 0; i < sizeof(addr)-1; i++)
ex[i] = new PCF8574(addr[i]);
for (uint8_t b = 0; b < RELAY_BOARDS_COUNT; b++) {
for (uint8_t r = 0; r < RELAYS_PER_BOARD_COUNT; r++) {
digitalWrite(*ex[b], r, RELAY_OFF); // expander 2: write 0 to P7
}
}
//LCD
lcd.init();
lcd.backlight();
lcd.createChar(DISABLED, charValveDisabled);
lcd.createChar(ENABLED, charValveEnabled);
lcd.createChar(ACTIVE_1, charValveActiveAnim[0]);
lcd.createChar(ACTIVE_2, charValveActiveAnim[1]);
lcd.createChar(ACTIVE_3, charValveActiveAnim[2]);
lcd.createChar(ACTIVE_4, charValveActiveAnim[3]);
lcd.createChar(ACTIVE_5, charValveActiveAnim[4]);
lcd.createChar(ARROW, charArrowLeft);
//lcd.createChar(8, <new char>); //produces garbage!
//ATTENTION! lcd.createChar() supports only 8 special chars
//in cse of more charcters needed, the method createChar could be used dynamically in the loop
//where the ID is used several times (overwriting)
lcdPrintStartInfo();
//BUTTONS
btnUp.begin();
btnDown.begin();
btnEnter.begin();
btnReturn.begin();
//DEBUG
const bool DebugEE = true; //manually chnage here if debug while simulate
if (DebugEE)
{
//TEST EEPROM DATA
//BOARD 1
relay[0].timeRun = 123UL;
relay[0].enabled = true; //RELAY 1/1
relay[1].timeRun = 350UL;
relay[1].enabled = true; //RELAY 1/2
relay[2].timeRun = 789UL;
relay[2].enabled = false; //RELAY 1/3
relay[3].timeRun = 200UL;
relay[3].enabled = true; //RELAY 1/4
//relay[3].active = true; //active by timeRun
relay[4].timeRun = 654UL;
relay[4].enabled = true; //RELAY 1/5
relay[5].timeStart = 1234UL;
relay[5].timeRun = 360UL;
relay[5].enabled = true; //RELAY 1/6
//BOARD 2
relay[12].timeRun = 250UL;
relay[12].enabled = true; //RELAY 2/5
//BOARD 3
relay[20].timeRun = 120UL;
relay[20].enabled = true; //RELAY 3/5
relay[21].timeRun = 140UL;
relay[21].enabled = true; //RELAY 3/6
relay[22].timeRun = 150UL;
relay[22].enabled = true; //RELAY 3/7
//BOARD 4
relay[29].timeRun = 230UL;
relay[29].enabled = true; //RELAY 4/6
relay[30].timeRun = 320UL;
relay[30].enabled = true; //RELAY 4/7
relay[31].timeRun = 789UL;
relay[31].enabled = true; //RELAY 4/8
//EEPROM.put(eeStartAddress, relay); //put whole relay data into EEPROM starting from address+sizeof(version)
//Serial.print("EEPROM test data written: "); Serial.print(sizeof(relay)); Serial.println(" bytes");
//delay(1000);
}
//check for undefined relay data and save initially standard data to the EEPROM
saveRelayData();
//READ EEPROM
readRelayData();
}
void loop() {
//BUTTONS
btnUp.update();
btnDown.update();
btnEnter.update();
btnReturn.update();
//RTC (DATE/TIME)
loopRTC();
//RAIN SENSOR
loopRainSensor();
//MENU
loopMenu();
//LCD
loopLCD();
//RELAY CONTROL
loopRelays();
}
void lcdPrintStartInfo() {
lcd.setCursor(0,0);
lcd.print("32 Valve Irrigation");
delay(2000);
lcd.clear();
lcd.backlight(); //lcd.noBacklight();
}
void saveRelayData() {
//check for invalid values and set the relay data intially to standard values
/*
for (int r = 0; r < RELAYS_COUNT; r++) {
Serial.print("CHECK: timeStart: ");
Serial.print(relay[r].timeStart);
Serial.print(" timeRun: ");
Serial.print(relay[r].timeRun);
Serial.print(" enabled: ");
Serial.print(relay[r].enabled);
Serial.print(" active: ");
Serial.println(relay[r].active);
}
*/
for (int r = 0; r < RELAYS_COUNT; r++) {
if (relay[r].timeRun == 0 ||
relay[r].timeRun > MAX_RUN_TIME ||
relay[r].timeStart == 0 ||
relay[r].timeStart > MAX_RUN_TIME) {
if (relay[r].timeRun > MAX_RUN_TIME) relay[r].timeRun = 0UL; //preventively reset undefined values
if (relay[r].timeStart > MAX_START_TIME) relay[r].timeStart = 0UL; //preventively reset undefined values
relay[r].enabled = false;
relay[r].active = false;
}
/*
Serial.print("SAVE: timeStart: ");
Serial.print(relay[r].timeStart);
Serial.print(" timeRun: ");
Serial.print(relay[r].timeRun);
Serial.print(" enabled: ");
Serial.print(relay[r].enabled);
Serial.print(" active: ");
Serial.println(relay[r].active);
*/
}
EEPROM.put(eeStartAddress, relay); //put whole relay data into EEPROM starting from address+sizeof(version)
Serial.println("EEPROM data written! ");
delay(200); //wait for write process
}
void readRelayData() {
//init array relay
for (int r = 0; r < RELAYS_COUNT; r++) {
relay[r].timeStart = 0UL;
relay[r].timeRun = 0UL;
relay[r].enabled = false;
relay[r].active = false;
/*
Serial.print("INIT: timeStart: ");
Serial.print(relay[r].timeStart);
Serial.print(" timeRun: ");
Serial.print(relay[r].timeRun);
Serial.print(" enabled: ");
Serial.print(relay[r].enabled);
Serial.print(" active: ");
Serial.println(relay[r].active);
*/
}
EEPROM.get(eeStartAddress, relay); //put data at startAddress into relay
for (int r = 0; r < RELAYS_COUNT; r++) {
Serial.print("READ: timeStart: ");
Serial.print(relay[r].timeStart);
Serial.print(" timeRun: ");
Serial.print(relay[r].timeRun);
Serial.print(" enabled: ");
Serial.print(relay[r].enabled);
Serial.print(" active: ");
Serial.println(relay[r].active);
}
}
bool isRelayEnabled(int ir) {
if (ir >= 0 && ir < RELAYS_COUNT) {
return relay[ir].enabled;
}
return false;
}
bool isRelayActive(int ir) {
return relay[ir].active;
}
byte convertToBCD(byte val) { //converts the decimal number to BCD
return ( (val / 10 * 16) + (val % 10) );
}
byte convertToDecimal(byte val) { //converts from BCD to decimal
return ( (val / 16 * 10) + (val % 16) );
}
void loopRTC() {
Wire.beginTransmission(DS1307_ADDRESS);
Wire.write(ZERO);
Wire.endTransmission();
Wire.requestFrom(DS1307_ADDRESS, 7);
rtc.seconds = convertToDecimal(Wire.read());
rtc.minutes = convertToDecimal(Wire.read());
rtc.hours = convertToDecimal(Wire.read() & 0b111111);
rtc.dayWeek = convertToDecimal(Wire.read());
rtc.dayMonth = convertToDecimal(Wire.read());
rtc.month = convertToDecimal(Wire.read());
rtc.year = convertToDecimal(Wire.read());
rtc.time = rtc.seconds + rtc.minutes*60 + rtc.hours*3600;
}
void loopRainSensor() {
uint16_t sensorRawValue = analogRead(A0); // input value 0..1023
rainIntensity = map(sensorRawValue, 0, 1023, 0, 100); // scale to 0..100 as percentage value
}
uint8_t getMenuState() {
return menuState;
}
void setMenuState(uint8_t newMenuState) {
Serial.print("new Menu State = ");
Serial.println(dbgMenuStr[newMenuState]);
menuState = newMenuState;
}
void setMenuUpdated(bool updateStatus) {
menuUpdated = updateStatus;
}
bool isMenuUpdated() {
return menuUpdated;
}
void loopMenu() {
//AUTOMATIC MENU CONTROL (BEFORE MANUAL MENU CONTROL!)
switch (getMenuState()) {
case MSave:
setMenuState(MMain);
break;
case MRelayEnabling:
setMenuState(MRelay);
break;
default:
break;
}
uint16_t step;
//MANUAL MENU CONTROL AT BUTTON ACTION
if (btnUp.isHold(&step)) {
//Serial.println("up is hold ");
//Serial.print("step = ");
//Serial.println(step);
switch (getMenuState()) {
case MRelayStartTime:
if (relay[relayIndex].timeStart < MAX_START_TIME - step) relay[relayIndex].timeStart += step;
break;
case MRelayRunTime:
if (relay[relayIndex].timeRun < MAX_RUN_TIME - step) relay[relayIndex].timeRun += step;
break;
default:
break;
}
}
if (btnDown.isHold(&step)) {
//Serial.print("step = ");
//Serial.println(step);
switch (getMenuState()) {
case MRelayStartTime:
if (relay[relayIndex].timeStart > step-1) relay[relayIndex].timeStart -= step;
break;
case MRelayRunTime:
if (relay[relayIndex].timeRun > step-1) relay[relayIndex].timeRun -= step;
break;
default:
break;
}
}
if (btnUp.wasClicked()) {
//Serial.println("up is pressed ");
switch (getMenuState()) {
case MRelay:
if (MRelayIndex > 1) MRelayIndex--;
break;
case MRelaySelection:
if (relayIndex < RELAYS_COUNT-1) relayIndex++;
break;
case MRelayStartTime:
if (relay[relayIndex].timeStart < MAX_START_TIME-60) relay[relayIndex].timeStart += 60;
break;
case MRelayRunTime:
if (relay[relayIndex].timeRun < MAX_RUN_TIME-60) relay[relayIndex].timeRun += 60;
break;
default:
break;
}
}
if (btnDown.wasClicked()) {
//Serial.println("down is pressed ");
switch (getMenuState()) {
case MRelay:
if (MRelayIndex < 4) MRelayIndex++;
break;
case MRelaySelection:
if (relayIndex > 0) relayIndex--;
break;
case MRelayStartTime:
if (relay[relayIndex].timeStart >= 60) relay[relayIndex].timeStart -= 60;
break;
case MRelayRunTime:
if (relay[relayIndex].timeRun >= 60) relay[relayIndex].timeRun -= 60;
break;
default:
break;
}
}
if (btnEnter.wasClicked()) {
//Serial.println("enter is pressed ");
switch (getMenuState()) {
case MMain:
setMenuUpdated(true);
setMenuState(MRelay);
break;
case MSave:
setMenuUpdated(true);
//automatically save, no confirmation needed
break;
case MRelay:
setMenuUpdated(true);
switch (MRelayIndex) {
case 1:
setMenuState(MRelaySelection);
break;
case 2:
setMenuState(MRelayStartTime);
break;
case 3:
setMenuState(MRelayRunTime);
break;
case 4:
setMenuState(MRelayEnabling);
break;
default:
break;
}
break;
case MRelayStartTime:
break;
case MRelayRunTime:
break;
default:
break;
}
}
if (btnReturn.wasClicked()) {
switch (getMenuState()) {
case MRelay:
setMenuUpdated(true);
setMenuState(MSave);
break;
case MRelaySelection:
setMenuState(MRelay);
break;
case MRelayStartTime:
setMenuState(MRelay);
break;
case MRelayRunTime:
setMenuState(MRelay);
break;
//case MRelayEnabling:
// setMenuState(MRelay);
// break;
default:
//printOnce("loopLCD(): unknown menu state: ", getMenuState());
break;
}
}
prevMenuState = getMenuState();
}
void loopLCD() {
uint8_t a = 0;
switch (getMenuState()) {
case MMain:
clearLCD();
//DEBUG printOnce("MAIN", -1);
//shows the rain intensity in percent
lcd.setCursor(0,0);
lcd.print("Rain ");
lcd.print(rainIntensity);
lcd.print(" % ");
//shows the real time clock
lcdPrintRtc();
//shows active valve info as text
lcd.setCursor(0,1);
a = 0;
for (uint8_t r = 0; r < RELAYS_COUNT-1; r++) {
if (isRelayActive(r)) {
a++;
}
}
if (a==0) {
lcd.print("No valve active ");
} else {
lcd.print(a);
lcd.print(" valves active ");
}
//shows relay overview as symbols in the main menu in 3rd and 4th LCD row
//relays 0..15 in the 3rd LCD row
//relays 16..31 in the 4th LCD row
static uint8_t iAnim[32] = { 2 }; //"active"-animation has 5 chars/images with char index 2..6
for (uint8_t r = 0; r < 32; r++) {
if (r == 0) lcd.setCursor(1,2);
if (r == 8) lcd.setCursor(12,2);
if (r == 16) lcd.setCursor(1,3);
if (r == 24) lcd.setCursor(12,3);
if (!isRelayActive(r)) {
//show non-animated char
//#####lcd.write(isRelayEnabled(r));
if (isRelayEnabled(r)) {
lcd.write(byte(ENABLED)); //enabled
continue;
}
lcd.write(byte(DISABLED)); //disabled
} else {
//animate valve char
//Serial.println(sizeof(relay)/sizeof(relay[0])); //if size dynamic, otherwise RELAYS_COUNT
if (isRelayActive(r)) {
//lcd.setCursor(4, 2);
lcd.write(byte(iAnim[r]));
if (iAnim[r] < 6) { iAnim[r]++; } else { iAnim[r] = 2; }
delay(10);
}
}
}
//numbering of the relay boards
lcd.setCursor(0,2);
lcd.print("A");
lcd.setCursor(11,2);
lcd.print("B");
lcd.setCursor(0,3);
lcd.print("C");
lcd.setCursor(11,3);
lcd.print("D");
//
//prevMenuState = getMenuState();
break;
case MSave:
clearLCD();
lcd.setCursor(0, 0);
lcd.print("Save Timer Data");
saveRelayData();
lcd.clear();
break;
case MRelay:
clearLCD();
//DEBUG Serial.print("GET RELAY "); Serial.println(relayIndex+1);
//Shows detailed relay info (time, enabling) in the relay menu
//ROW 1: RELAY INFO
lcd.setCursor(0, 0);
if (MRelayIndex == 1) {
lcd.print((char)62);
} else {
lcd.print(" ");
}
lcd.print("Relay");
lcd.setCursor(7, 0);
lcd.print(relayIndex+1);
//ROW 2: RELAY START TIME
lcd.setCursor(0, 1);
if (MRelayIndex == 2) {
lcd.print((char)62);
} else {
lcd.print(" ");
}
lcd.print("Start ");
timeOnLCD(relay[relayIndex].timeStart);
//ROW 3: RELAY RUN TIME
lcd.setCursor(0, 2);
if (MRelayIndex == 3) {
lcd.print((char)62);
} else {
lcd.print(" ");
}
lcd.print("Run ");
timeOnLCD(relay[relayIndex].timeRun);
//ROW 4: RELAY ENABLING
lcd.setCursor(0, 3);
if (MRelayIndex == 4) {
lcd.print((char)62);
} else {
lcd.print(" ");
}
if (relay[relayIndex].enabled) {
lcd.print("enabled "); //space chars to overwrite prev. char
} else {
lcd.print("disabled ");
}
break;
case MRelaySelection:
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(6, 0);
lcd.print((char)62);
lcd.setCursor(7, 0);
lcd.print(relayIndex+1);
if(relayIndex<10) {
lcd.print(" "); //clear one digit from previously shown double digit number
}
break;
case MRelayStartTime:
//move only the cursor and update only the time!
//all other chars are still shown from prev. LCD
//remove cursor from prev. menu MRelay
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 2);
lcd.print(" ");
lcd.setCursor(7, 1);
lcd.print((char)62);
timeOnLCD(relay[relayIndex].timeStart);
break;
case MRelayRunTime:
//move only the cursor and update only the time!
//all other chars are still shown from prev. LCD
//remove cursor from prev. menu MRelay
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 2);
lcd.print(" ");
lcd.setCursor(7, 2);
lcd.print((char)62);
timeOnLCD(relay[relayIndex].timeRun);
break;
case MRelayEnabling:
relay[relayIndex].enabled = !relay[relayIndex].enabled; //negate
break;
default:
//printOnce("loopLCD(): unknown menu state: ", getMenuState());
//prevMenuState = getMenuState();
break;
}
}
void clearLCD() {
if (isMenuUpdated()) {
Serial.println("CLEAR LCD");
lcd.clear();
setMenuUpdated(false);
}
}
void lcdPrintRtc() {
if (rtc.hours < 10) {
lcd.setCursor(12,0);
lcd.print("0"); //leading ZERO
lcd.setCursor(13,0);
lcd.print(rtc.hours);
} else {
lcd.setCursor(12,0);
lcd.print(rtc.hours);
}
lcd.setCursor(14,0);
lcd.print(":");
if (rtc.minutes < 10) {
lcd.setCursor(15,0);
lcd.print("0"); //leading ZERO
lcd.setCursor(16,0);
lcd.print(rtc.minutes);
} else {
lcd.setCursor(15,0);
lcd.print(rtc.minutes);
}
lcd.setCursor(17,0);
lcd.print(":");
if (rtc.seconds < 10) {
lcd.setCursor(18,0);
lcd.print("0"); //leading ZERO
lcd.setCursor(19,0);
lcd.print(rtc.seconds);
} else {
lcd.setCursor(18,0);
lcd.print(rtc.seconds);
}
}
//convert time to hours/minutes/seconds
//input: time, output: hours, minutes, seconds
void timeToHMS(uint32_t time, uint8_t &hours, uint8_t &minutes, uint8_t &seconds) {
uint32_t t = time;
hours = t/3600;
t = t%3600;
minutes = t/60;
t = t%60;
seconds = t;
}
void timeOnLCD(uint32_t time) {
uint8_t hours;
uint8_t minutes;
uint8_t seconds;
timeToHMS(time, hours, minutes, seconds);
if (hours < 10) lcd.print("0");
lcd.print(hours);
lcd.print(":");
if (minutes < 10) lcd.print("0");
lcd.print(minutes);
lcd.print(":");
if (seconds < 10) lcd.print("0");
lcd.print(seconds);
}
void loopRelays() {
for (uint8_t b = 0; b < RELAY_BOARDS_COUNT; b++) {
for (uint8_t r = 0; r < RELAYS_PER_BOARD_COUNT; r++) {
char c = b * RELAYS_PER_BOARD_COUNT + r; //current relay
uint32_t endTime = relay[c].timeStart + relay[c].timeRun;
if (rtc.time >= relay[c].timeStart && rtc.time < endTime) {
digitalWrite(*ex[b], r, RELAY_ON);
relay[c].active = true;
} else {
digitalWrite(*ex[b], r, RELAY_OFF);
relay[c].active = false;
}
}
}
}
void printOnce(String newText, uint8_t newVal) {
static String prevText = "";
static uint8_t prevVal = -1;
if ((newText != prevText) || (prevVal != newVal)) {
if (newVal > -1) {
Serial.print(newText);
Serial.println(newVal);
} else {
Serial.println(newText);
}
prevText = newText;
prevVal = newVal;
}
}
void printOnce(String newText) {
static String prevText = "";
if (newText != prevText) {
Serial.println(newText);
prevText = newText;
}
}