-
-
Save nandee95/91606f410fb104a0a48c46b8fb392d5c to your computer and use it in GitHub Desktop.
Fish Feeder
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
#include <LiquidCrystal.h> | |
#include <DS3231.h> | |
#include <EEPROM.h> | |
#define PIN_LCD_RS 12 | |
#define PIN_LCD_EN 11 | |
#define PIN_BTN_RIGHT 10 | |
#define PIN_BTN_LEFT 9 | |
#define PIN_BTN_DOWN 8 | |
#define PIN_BTN_UP 7 | |
#define PIN_LCD_D4 6 | |
#define PIN_LCD_D5 5 | |
#define PIN_LCD_D6 4 | |
#define PIN_LCD_D7 3 | |
#define PIN_RELAY1 A3 | |
#define PIN_RELAY2 A2 | |
#define PIN_RELAY3 A1 | |
#define PIN_RELAY4 A0 | |
#define BTN_DELAY 70 | |
#define MESSAGE_DELAY 700 | |
#define EXT_MESSAGE_DELAY 2000 | |
#define DELAY_BETWEEN_EXECUTIONS 5000 | |
#define EEPROM_TIMING_SHIFT 0 | |
#define EEPROM_ALARM_COUNT_SHIFT 10*2 //Leaving space for 10 timings (2 byte each) | |
#define EEPROM_ALARM_SHIFT EEPROM_ALARM_COUNT_SHIFT+1 //need one byte for the stack length | |
//Prototypes | |
bool Execute(int num = -1); | |
//#define DEBUG //Look for memory leak | |
//times between steps | |
constexpr unsigned char timeCount = 7; | |
unsigned short timeValues[timeCount] = {0}; | |
//Actions | |
/* | |
A: top plate pull R1 | |
B: top plate push R2 | |
C: bottom plate R3 | |
D: air R4 | |
B+ B- C+ C- A+ A- D+ D- | |
L H L H L H L H | |
*/ | |
constexpr unsigned char actionPins[7]= { | |
PIN_RELAY1, | |
PIN_RELAY2, | |
PIN_RELAY2, | |
PIN_RELAY3, | |
PIN_RELAY3, | |
PIN_RELAY4, | |
PIN_RELAY4 | |
}; | |
constexpr bool actionOuts[7]= {HIGH,LOW,HIGH,LOW,HIGH,LOW,HIGH}; | |
//alarms | |
unsigned char alarmCount = 0; | |
unsigned char alarms[10][3];//hour minute cycles | |
bool lastKeyDown = false; | |
constexpr byte char_up[8] = { | |
B00000, | |
B00000, | |
B00100, | |
B01110, | |
B11111, | |
B00000, | |
B00000, | |
B00000, | |
}; | |
constexpr byte char_down[8] = { | |
B00000, | |
B00000, | |
B00000, | |
B11111, | |
B01110, | |
B00100, | |
B00000, | |
B00000, | |
}; | |
enum Button | |
{ | |
BTN_UP,BTN_DOWN,BTN_LEFT,BTN_RIGHT | |
}; | |
LiquidCrystal lcd(PIN_LCD_RS, PIN_LCD_EN, PIN_LCD_D4, PIN_LCD_D5, PIN_LCD_D6, PIN_LCD_D7); | |
DS3231 rtc; | |
unsigned long lastButtonPressed=0; | |
char menuItems[10][16]= {0}; | |
unsigned char menuItemCount=0; | |
unsigned short closestAlarm = 0; | |
unsigned char lastMinuteChecked = 0; | |
#ifdef DEBUG | |
unsigned char menusInMemory = 0; | |
#endif | |
inline int Distance(int curHour,int curMin, int destHour, int destMin) { | |
int distance = (destHour - curHour) * 60 + (destMin - curMin); | |
if(distance < 0) distance+=1440; | |
return distance; | |
} | |
void UpdateNextAlarm() | |
{ | |
RTCDateTime dt = rtc.getDateTime(); | |
int closestDist = 999999; | |
for(int i=0; i<alarmCount; i++) | |
{ | |
int d = Distance(dt.hour,dt.minute,alarms[i][0],alarms[i][1]); //In minutes | |
if(d < closestDist && d != 0) | |
{ | |
closestDist = d; | |
closestAlarm = i; | |
} | |
} | |
} | |
inline void ShowMessage(String line1,String line2="", bool extended = false) | |
{ | |
lcd.clear(); | |
lcd.setCursor(0,0); | |
lcd.print(line1); | |
lcd.setCursor(0,1); | |
lcd.print(line2); | |
delay(extended? EXT_MESSAGE_DELAY : MESSAGE_DELAY); | |
} | |
void CheckAlarm() | |
{ | |
if(alarmCount == 0) return; | |
RTCDateTime dt = rtc.getDateTime(); | |
if(dt.minute == lastMinuteChecked) return; | |
lastMinuteChecked = dt.minute; | |
if(dt.hour == alarms[closestAlarm][0] && dt.minute == alarms[closestAlarm][1]) | |
{ | |
ShowMessage("Auto executing",String("#")+closestAlarm+" ("+alarms[closestAlarm][2]+" times)",true); | |
for(int i=0; i<alarms[closestAlarm][2]; i++) | |
{ | |
if(!Execute(i+1)) break; | |
if(i != alarms[closestAlarm][2]-1) | |
{ | |
lcd.clear(); | |
lcd.setCursor(0,0); | |
lcd.write("Waiting..."); | |
delay(DELAY_BETWEEN_EXECUTIONS); | |
} | |
} | |
UpdateNextAlarm(); | |
ShowMessage("Auto execution","complete",true); | |
} | |
} | |
inline void EEPROMWriteShort(const int address, const unsigned short value) | |
{ | |
EEPROM.write(address, (value & 0xFF)); | |
EEPROM.write(address + 1, ((value >> 8) & 0xFF)); | |
} | |
inline unsigned short EEPROMReadShort(const int address) | |
{ | |
return EEPROM.read(address) | ((unsigned short)EEPROM.read(address + 1) << 8); | |
} | |
void ReadTimings() | |
{ | |
for(int i=0; i<timeCount; i++) | |
{ | |
timeValues[i] = EEPROMReadShort(EEPROM_TIMING_SHIFT+(i*2)); | |
if(timeValues[i] > 25000) | |
{ | |
EEPROMWriteShort(EEPROM_TIMING_SHIFT+(i*2),5000); | |
timeValues[i] = 5000; | |
} | |
} | |
} | |
void ReadAlarms() | |
{ | |
alarmCount=EEPROM.read(EEPROM_ALARM_COUNT_SHIFT); | |
if(alarmCount > 10) | |
{ | |
EEPROM.write(EEPROM_ALARM_COUNT_SHIFT,0); | |
alarmCount = 0; | |
} | |
for(int i=0; i<alarmCount; i++) | |
{ | |
alarms[i][0] = EEPROM.read(EEPROM_ALARM_SHIFT+i*3);//hour | |
alarms[i][1] = EEPROM.read(EEPROM_ALARM_SHIFT+i*3+1);//minute | |
alarms[i][2] = EEPROM.read(EEPROM_ALARM_SHIFT+i*3+2);//count | |
} | |
} | |
void setup() { | |
//Set up LCD | |
lcd.begin(16, 2); | |
lcd.clear(); | |
lcd.print("Fish Feeder v2.0"); | |
lcd.setCursor(0,1); | |
lcd.print("By: Nandee"); | |
lcd.createChar(0, char_up); | |
lcd.createChar(1, char_down); | |
//Init RTC | |
rtc.begin(); | |
//Initializing pins | |
pinMode(PIN_BTN_UP, INPUT_PULLUP); | |
pinMode(PIN_BTN_DOWN, INPUT_PULLUP); | |
pinMode(PIN_BTN_LEFT, INPUT_PULLUP); | |
pinMode(PIN_BTN_RIGHT, INPUT_PULLUP); | |
pinMode(PIN_RELAY1, OUTPUT); | |
pinMode(PIN_RELAY2, OUTPUT); | |
pinMode(PIN_RELAY3, OUTPUT); | |
pinMode(PIN_RELAY4, OUTPUT); | |
digitalWrite(PIN_RELAY1, HIGH); //Relay module works with inverse logic | |
digitalWrite(PIN_RELAY2, HIGH); | |
digitalWrite(PIN_RELAY3, HIGH); | |
digitalWrite(PIN_RELAY4, HIGH); | |
#ifdef DEBUG | |
Serial.begin(9600); | |
#endif | |
ReadTimings(); | |
ReadAlarms(); | |
UpdateNextAlarm(); | |
ClearValves(); | |
delay(1500); | |
} | |
class Menu; | |
Menu* currentMenu= 0; | |
class Menu { | |
public: | |
unsigned char displayedItem = -1; | |
unsigned char currentItem=0; | |
Menu* parent; | |
Menu(Menu* parent) : parent(parent) | |
{ | |
#ifdef DEBUG | |
Serial.println("Menu created"); | |
menusInMemory++; | |
#endif | |
} | |
~Menu() | |
{ | |
#ifdef DEBUG | |
Serial.println("Menu destroyed"); | |
menusInMemory--; | |
#endif | |
} | |
AddItem(String item) | |
{ | |
while (item.length() < 16) item += " "; | |
item.toCharArray(menuItems[menuItemCount++], 16); | |
} | |
void Draw() | |
{ | |
if(displayedItem !=currentItem) { | |
lcd.clear(); | |
lcd.write(menuItems[currentItem]); | |
lcd.setCursor(0, 1); | |
lcd.write(byte(0)); | |
lcd.write("back "); | |
lcd.write(byte(1)); | |
lcd.write("select"); | |
displayedItem = currentItem; | |
} | |
} | |
void Activate() | |
{ | |
//currentItem = 0; | |
displayedItem = -1; //Redraw | |
menuItemCount = 0; | |
Init(); | |
} | |
virtual void Init() = 0; | |
void KeyPressed(Button btn) | |
{ | |
switch(btn) | |
{ | |
case BTN_UP: | |
{ | |
currentMenu = parent; | |
if(currentMenu) currentMenu->Activate(); | |
delete this; | |
return; | |
} | |
break; | |
case BTN_DOWN: | |
{ | |
Selected(); | |
} | |
break; | |
case BTN_LEFT: | |
{ | |
currentItem = (unsigned short)max(0,(int)currentItem-1); | |
} | |
break; | |
case BTN_RIGHT: | |
{ | |
currentItem = min((int)menuItemCount-1,(int)currentItem+1); | |
} | |
break; | |
} | |
} | |
virtual void Selected()=0; | |
void Quit() | |
{ | |
Menu* target = currentMenu; | |
while(target != 0) | |
{ | |
Menu* temp = target->parent; | |
delete target; | |
target = temp; | |
} | |
currentMenu = 0; | |
} | |
}; | |
void ClearValves() | |
{ | |
digitalWrite(PIN_RELAY2,LOW); | |
delay(500); | |
digitalWrite(PIN_RELAY2,HIGH); | |
} | |
int updateButtons() | |
{ | |
const unsigned long now = millis(); | |
if(now-lastButtonPressed < BTN_DELAY) return; | |
const bool anyPressed = !digitalRead(PIN_BTN_LEFT) || | |
!digitalRead(PIN_BTN_RIGHT) || | |
!digitalRead(PIN_BTN_UP) || | |
!digitalRead(PIN_BTN_DOWN); | |
if(!lastKeyDown&&anyPressed) | |
{ | |
lastButtonPressed = now; | |
lastKeyDown = anyPressed; | |
if(!digitalRead(PIN_BTN_LEFT))return BTN_LEFT; | |
else if(!digitalRead(PIN_BTN_RIGHT))return BTN_RIGHT; | |
else if(!digitalRead(PIN_BTN_UP))return BTN_UP; | |
else if(!digitalRead(PIN_BTN_DOWN))return BTN_DOWN; | |
lastButtonPressed = now; | |
} | |
lastKeyDown = anyPressed; | |
return -1; | |
} | |
int ValueSetup(unsigned short value, unsigned short minVal, unsigned short maxVal, unsigned short stepVal) { | |
lcd.clear(); | |
lcd.setCursor(0, 1); | |
lcd.write(byte(0)); | |
lcd.write("back "); | |
lcd.write(byte(1)); | |
lcd.write("save"); | |
int oldval = -1; | |
while (digitalRead(PIN_BTN_UP)) { | |
int btn = updateButtons(); | |
switch(btn) | |
{ | |
case BTN_LEFT: | |
{ | |
value = value > stepVal ? max(minVal, value - stepVal) : minVal; | |
} | |
break; | |
case BTN_RIGHT: | |
{ | |
value = min(maxVal, value + stepVal); | |
} | |
break; | |
case BTN_DOWN: | |
{ | |
currentMenu->displayedItem = -1; | |
ShowMessage("Value saved!"); | |
return value; | |
} | |
break; | |
//Display | |
} | |
if (value != oldval) | |
{ | |
lcd.setCursor(0, 0); | |
lcd.print("Value:"); | |
lcd.print(value, DEC); | |
lcd.print(" "); | |
oldval = value; | |
} | |
} | |
return -1; | |
} | |
class TimeSetMenu : public Menu | |
{ | |
public: | |
TimeSetMenu(Menu* parent) : Menu(parent) | |
{ | |
} | |
virtual void Init() | |
{ | |
AddItem("Set hour"); | |
AddItem("Set minute"); | |
} | |
virtual void Selected() | |
{ | |
RTCDateTime dt = rtc.getDateTime(); | |
if(currentItem == 0) | |
{ | |
char newVal = ValueSetup(dt.hour, 0, 23, 1); | |
if(newVal != -1) rtc.setDateTime(dt.year,dt.month,dt.day,newVal,dt.minute,dt.second); | |
} | |
else | |
{ | |
char newVal = ValueSetup(dt.minute, 0, 59, 1); | |
if(newVal != -1) rtc.setDateTime(dt.year,dt.month,dt.day,dt.hour,newVal,0); | |
} | |
} | |
}; | |
class TimingsMenu : public Menu | |
{ | |
public: | |
TimingsMenu(Menu* parent) : Menu(parent) | |
{ | |
} | |
virtual void Init() | |
{ | |
for(int i=0; i<timeCount; i++) | |
{ | |
String str="Set t"; | |
str+=char('0'+i+1); | |
str+=" "; | |
str+=timeValues[i]; | |
str+="ms"; | |
AddItem(str); | |
} | |
} | |
virtual void Selected() | |
{ | |
int newVal = ValueSetup(timeValues[currentItem], 0, 25000, 100); | |
if (newVal >= 0) timeValues[currentItem] = min(max(0,newVal),25000); | |
EEPROMWriteShort(EEPROM_TIMING_SHIFT+(currentItem*2),newVal); | |
Activate(); | |
} | |
}; | |
class EditAlarmMenu : public Menu | |
{ | |
public: | |
int location=0; | |
unsigned short hour=0,minute = 0,cycles=0; | |
EditAlarmMenu(Menu* parent) : Menu(parent) | |
{ | |
} | |
virtual void Init() | |
{ | |
AddItem("Edit hour"); | |
AddItem("Edit minute"); | |
AddItem("Edit cycles"); | |
AddItem("Remove"); | |
} | |
virtual void Selected() | |
{ | |
switch(currentItem) | |
{ | |
case 0: | |
{ | |
char newVal = ValueSetup(hour, 0, 23, 1); | |
if(newVal != -1) { | |
hour = newVal; | |
EEPROM.write(EEPROM_ALARM_SHIFT+location*3,hour); | |
alarms[location][0] = hour; | |
} | |
UpdateNextAlarm(); | |
displayedItem = -1; | |
break; | |
} | |
case 1: | |
{ | |
char newVal = ValueSetup(minute, 0, 59, 1); | |
if(newVal != -1) | |
{ | |
minute = newVal; | |
EEPROM.write(EEPROM_ALARM_SHIFT+location*3+1,minute); | |
alarms[location][1] = minute; | |
} | |
UpdateNextAlarm(); | |
displayedItem = -1; | |
break; | |
} | |
case 2: | |
{ | |
char newVal = ValueSetup(cycles, 1, 20, 1); | |
if(newVal != -1) | |
{ | |
cycles = newVal; | |
EEPROM.write(EEPROM_ALARM_SHIFT+location*3+2,cycles); | |
alarms[location][2] = cycles; | |
} | |
displayedItem = -1; | |
break; | |
} | |
case 3: | |
{ | |
if(alarmCount-1!= location) | |
{ | |
for(int i=0; i<3; i++) { | |
EEPROM.write(EEPROM_ALARM_SHIFT+location*3+i,EEPROM.read(EEPROM_ALARM_SHIFT+alarmCount-1+i)); | |
alarms[location][i]=alarms[alarmCount-1][i]; | |
} | |
} | |
//decrease count | |
EEPROM.write(EEPROM_ALARM_COUNT_SHIFT,alarmCount-1); | |
alarmCount--; | |
UpdateNextAlarm(); | |
ShowMessage("Alarm removed!"); | |
currentMenu = parent; | |
UpdateNextAlarm(); | |
parent->Activate(); | |
delete this; | |
return; | |
} | |
} | |
} | |
}; | |
class AddAlarmMenu : public Menu | |
{ | |
public: | |
unsigned char count =0; | |
unsigned char hour=12,minute =0,cycles = 1; | |
AddAlarmMenu(Menu* parent) : Menu(parent) | |
{ | |
} | |
virtual void Init() | |
{ | |
AddItem("Set hour"); | |
AddItem("Set minute"); | |
AddItem("Set cycle count"); | |
AddItem("Save"); | |
} | |
virtual void Selected() | |
{ | |
displayedItem = -1; | |
switch(currentItem) | |
{ | |
case 0: | |
{ | |
char newVal = ValueSetup(hour, 0, 23, 1); | |
if(newVal != -1) hour = newVal; | |
break; | |
} | |
case 1: | |
{ | |
char newVal = ValueSetup(minute, 0, 59, 1); | |
if(newVal != -1) minute = newVal; | |
break; | |
} | |
case 2: | |
{ | |
char newVal = ValueSetup(cycles, 1, 20, 1); | |
if(newVal != -1) cycles = newVal; | |
break; | |
} | |
case 3: | |
{ | |
EEPROM.write(EEPROM_ALARM_SHIFT+alarmCount*3,hour); | |
EEPROM.write(EEPROM_ALARM_SHIFT+alarmCount*3+1,minute); | |
EEPROM.write(EEPROM_ALARM_SHIFT+alarmCount*3+2,cycles); | |
alarms[alarmCount][0] = hour; | |
alarms[alarmCount][1] = minute; | |
alarms[alarmCount][2] = cycles; | |
alarmCount++; | |
EEPROM.write(EEPROM_ALARM_COUNT_SHIFT,alarmCount); | |
parent->Activate(); | |
UpdateNextAlarm(); | |
ShowMessage("Saved!"); | |
currentMenu = parent; | |
parent->displayedItem = -1; | |
delete this; | |
return; | |
} | |
} | |
} | |
}; | |
class AlarmMenu : public Menu | |
{ | |
public: | |
AlarmMenu(Menu* parent) : Menu(parent) | |
{ | |
} | |
virtual void Init() | |
{ | |
for(int i=0; i<alarmCount; i++) | |
{ | |
AddItem(String("#")+i+" "+(alarms[i][0] < 10 ? "0":"")+alarms[i][0]+":"+(alarms[i][1] < 10 ? "0":"")+alarms[i][1]+" "+alarms[i][2]+"x"); | |
} | |
if(alarmCount < 10) | |
AddItem("Add new"); | |
} | |
virtual void Selected() | |
{ | |
if(currentItem < alarmCount) //Edit | |
{ | |
EditAlarmMenu* editMenu = new EditAlarmMenu(this); | |
editMenu->location = currentItem; | |
editMenu->hour = alarms[currentItem][0]; | |
editMenu->minute = alarms[currentItem][1]; | |
editMenu->cycles = alarms[currentItem][2]; | |
currentMenu = editMenu; | |
currentMenu->Activate(); | |
} | |
else //Add | |
{ | |
AddAlarmMenu* addMenu = new AddAlarmMenu(this); | |
addMenu->cycles = 1; | |
addMenu->hour = 12; | |
addMenu->minute = 0; | |
currentMenu = addMenu; | |
currentMenu->Activate(); | |
} | |
} | |
}; | |
class MainMenu : public Menu | |
{ | |
public: | |
MainMenu() : Menu(0) {} | |
virtual void Init() | |
{ | |
AddItem("Execute once"); | |
AddItem("Edit alarms"); | |
AddItem("Set up time"); | |
AddItem("Set up gaps"); | |
} | |
virtual void Selected() | |
{ | |
switch(currentItem) | |
{ | |
case 0: | |
{ | |
Quit();//Clean the menu | |
Execute(); | |
} | |
break; | |
case 1: | |
{ | |
currentMenu = new AlarmMenu(this); | |
currentMenu->Activate(); | |
} | |
break; | |
case 2: | |
{ | |
currentMenu = new TimeSetMenu(this); | |
currentMenu->Activate(); | |
} | |
break; | |
case 3: | |
{ | |
currentMenu = new TimingsMenu(this); | |
currentMenu->Activate(); | |
} | |
break; | |
} | |
} | |
}; | |
void ActivateMainMenu() | |
{ | |
currentMenu = new MainMenu(); | |
currentMenu->Activate(); | |
} | |
void updateHome() | |
{ | |
lcd.setCursor(0, 0); | |
lcd.write("Time: "); | |
lcd.write(rtc.dateFormat("H:i:s",rtc.getDateTime())); | |
lcd.write(" "); | |
lcd.setCursor(0, 1); | |
lcd.write("Next: "); | |
if(alarmCount > 0) | |
{ | |
if(alarms[closestAlarm][0] < 10) lcd.write("0"); | |
lcd.print(alarms[closestAlarm][0],DEC); | |
lcd.write(":"); | |
if(alarms[closestAlarm][1] < 10) lcd.write("0"); | |
lcd.print(alarms[closestAlarm][1],DEC); | |
lcd.write(" "); | |
lcd.print(alarms[closestAlarm][2],DEC); | |
lcd.write("x "); | |
} | |
else | |
{ | |
lcd.write("none "); | |
} | |
} | |
void updateMenu() | |
{ | |
currentMenu->Draw(); | |
} | |
void Abort() | |
{ | |
lcd.clear(); | |
lcd.setCursor(0,0); | |
lcd.write("Aborting..."); | |
digitalWrite(PIN_RELAY1,HIGH); | |
digitalWrite(PIN_RELAY2,HIGH); | |
digitalWrite(PIN_RELAY3,HIGH); | |
digitalWrite(PIN_RELAY4,HIGH); | |
ClearValves(); | |
ShowMessage("Aborted"); | |
} | |
bool Execute(int num = -1) | |
{ | |
lcd.clear(); | |
lcd.setCursor(0, 0); | |
lcd.write("Executing..."); | |
if(num != -1) { | |
lcd.setCursor(14,0); | |
lcd.write("#"); | |
lcd.print(num,DEC); | |
} | |
lcd.setCursor(0, 1); | |
lcd.write(byte(0)); | |
lcd.write("abort"); | |
unsigned char execStep = 0; | |
unsigned long last = millis(); | |
digitalWrite(PIN_RELAY1,LOW); | |
lcd.setCursor(14,1); | |
lcd.write("#0"); | |
while(true) | |
{ | |
if(digitalRead(PIN_BTN_UP)== 0) | |
{ | |
Abort(); | |
lastKeyDown = true; | |
return false; | |
} | |
unsigned long now = millis(); | |
if(now - last > timeValues[execStep]) | |
{ | |
digitalWrite(actionPins[execStep],actionOuts[execStep]); | |
lcd.setCursor(14,1); | |
lcd.write("#"); | |
lcd.print((int)execStep+1,DEC); | |
execStep++; | |
if(execStep == 7) break; | |
last = now; | |
} | |
} | |
if(num==-1)ShowMessage("Execution","completed!"); | |
return true; | |
} | |
void loop() { | |
CheckAlarm(); | |
int btn = updateButtons(); | |
if(btn != -1) | |
{ | |
if(currentMenu) currentMenu->KeyPressed(btn); | |
else if(btn == BTN_DOWN) | |
ActivateMainMenu(); | |
} | |
if(currentMenu != 0) | |
updateMenu(); | |
else | |
updateHome(); | |
#ifdef DEBUG | |
Serial.print("menus:"); | |
Serial.println(menusInMemory); | |
#endif | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment