Skip to content

Instantly share code, notes, and snippets.

@nandee95
Created December 8, 2019 13:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nandee95/91606f410fb104a0a48c46b8fb392d5c to your computer and use it in GitHub Desktop.
Save nandee95/91606f410fb104a0a48c46b8fb392d5c to your computer and use it in GitHub Desktop.
Fish Feeder
#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