Since
my original article about Fish Tank Automation project, I did not
proceed to the practical execution of it (remained a prototype). I
recently reviewed that project and found it inadequate. I decided
that the command line interface to program and operate the module is
not good enough for a micro-controller and added LCD module and
buttons. I also added low/high temperature warning LED-s. This can be
easily expanded to automatically control the water heater, however I
have no use for it since the water heater module has a thermostat of
its own. I was able to keep the original command line interface code,
however I used up almost the entire available program storage in Arduino (less than 1kB left).
Therefore if anybody would like to expand this project, I suggest to
remove either command line interface code or the buttons/LCD UI part.
Here
is the modified circuit diagram:
Perhaps
one day I will draw it using some more professional software than Paint and scan hybrid, but it must suffice for now.
There
were many additions to the sketch code as well as a nasty bug was
corrected in the aquarium light switching code.
The sketch (updated 9/9/2014):
/*
* Fish tank automation.
* Version 3.0
*
* Changes (from 2.0):
*
* - Bug fixes (light scheduler).
* - Temperature log discontinued (useless).
* - Temperature is measured every 5 minutes (was 20).
*
* Created by Marek Karcz 2013,2014. All rights reserved.
* Free to copy for personal use.
*
* Hardware:
*
* 1) Arduino Uno
* 2) ULN2003 stepper motor driver module.
* 3) 28BYJ-48 stepper motor (propelling food distribution mechanism).
* 4) Food distributing drum.
* Taken from cheap Walmart fish automated feeder propelled by battery
* operated clock.
* 5) Radio Shack's SPDT Micromini 5VDC Relay (275-0240) - light control.
* 6) DS18B20 one-wire temperature sensor in water proof casing or coating.
* Currently the temperature read is only for informational/logging
* purpose since my fish tank water heater has its own thermostate.
* 7) Tiny RTC, I2C module (DS1307 AT24C32).
* 8) I2C LCD 20x4 display from SainSmart.
* 9) Green LED, red LED and 2 buttons (SET and SEL).
* NOTE: Button pins must be connected to pull-up resistors.
* Per Arduino's reference:
* "If the pin isn't connected to anything, digitalRead() can
* return either HIGH or LOW (and this can change randomly)."
*
* Theory of operation:
*
* The RTC clock will synchronize the scheduled tasks.
* Scheduler will program the feeding and light on-off times.
* The feeding drum will be spinned by a stepper motor.
* The setup will be stored in EEPROM of the RTC clock module or Arduino's EEPROM.
* Device will be programmed with 3-buttons and LCD UI as well as via serial port
* (command line interface).
* Green and Red LED-s will show the temperature status (too low or too high).
*
* Credits/copyright acknowledgements/references:
*
* - The stepper motor code inspired by:
* http://arduino-info.wikispaces.com/SmallSteppers
* - The Dallas temperature sensor code ripped from:
* http://www.pjrc.com/teensy/td_libs_OneWire.html
* and refactored.
* - The I2c bit-banged library:
* 2010-12 Tod E. Kurt, http://todbot.com/blog/
*
*/
#include <avr/pgmspace.h>
#include <EEPROM.h>
#include <Stepper.h>
#include <OneWire.h>
#include <Wire.h>
#include <RTClib.h>
#include <SoftI2CMaster.h>
#include <LCD_I2C_BB.h>
#define MAXCOL 20 // columns on LCD disp.
#define MAXROW 4 // rows on LCD disp.
#define LCDUPD 200 // loop iterations between LCD updates
#define SHOWFSOUT 50 // loop iter. before show food schedule disp. tout
#define SHOWLSOUT 100 // loop iter. before show light schedule disp. tout
#define BACKLTOUT 150 // loop iter. before lcd backlight timeout
#define MENUOUT 200 // loop iter. before menu timeout
//#define MYDEBUG0
//#define MYDEBUG1
//#define MYDEBUG2
//#define MYDEBUG3
// -----------------------------------------------------------------------------
// Pin definitions.
// -----------------------------------------------------------------------------
#define REDLEDPIN 4
#define GREENLEDPIN 13
#define BTNSEL 2
#define BTNSET 3
#define DTROW 0
#define TEMPROW 1
#define FSROW 2
#define LSROW 0
#define STPMIN1PIN 8
#define STPMIN2PIN 9
#define STPMIN3PIN 10
#define STPMIN4PIN 11
#define LIGHTPIN 12
#define DS1820PIN 7 // did not work on pin 13 (because of the LED connected)
// and on pin 7 works every other time
// (Number of steps per revolution of INTERNAL motor in 4-step mode)
#define STEPS_PER_MOTOR_REVOLUTION 32
// (Steps per OUTPUT SHAFT of gear reduction)
#define STEPS_PER_OUTPUT_REVOLUTION 32 * 64 //2048
// Calendar definitions.
prog_char Sun[] PROGMEM = "Sun";
prog_char Mon[] PROGMEM = "Mon";
prog_char Tue[] PROGMEM = "Tue";
prog_char Wed[] PROGMEM = "Wed";
prog_char Thu[] PROGMEM = "Thu";
prog_char Fri[] PROGMEM = "Fri";
prog_char Sat[] PROGMEM = "Sat";
PROGMEM const char *daysOfWeek[] = {
Sun, Mon, Tue, Wed, Thu, Fri, Sat
};
// Error codes
enum eErrors {
ERR_OK = 0,
ERR_ARGTOOLONG, // 1 : argument too long
ERR_TOOMANYARGS, // 2 : too many arguments
ERR_TOOMANYLS, // 3 : too many entries in light on/off schedule
ERR_LSEEPROMOOR, // 4 : light on/off schedule setup exceeds EEPROM range
ERR_LSENTINVFMT, // 5 : invalid entry format (light on/off schedule)
ERR_TOOMANYFS, // 6 : too many entries in feeding schedule
ERR_FSEEPROMOOR, // 7 : feeding schedule setup exceeds EEPROM range
ERR_FSENTINVFMT, // 8 : invalid entry format (feeding schedule)
ERR_UNKNCMD, // 9 : unknown command
ERR_TEMPRD, // 10 : temperature sensor read failed
ERR_NIL
};
// -----------------------------------------------------------------------------
// recognized commands
// -----------------------------------------------------------------------------
enum eCommands {
CMD_DATE = 0,
CMD_TEMP,
CMD_ADDFT,
CMD_SHOWFS,
CMD_DELFT,
CMD_ADDLS,
CMD_SHOWLS,
CMD_DELLS,
CMD_SETDT,
CMD_HELP,
CMD_DEFFS,
CMD_DEFLS,
CMD_NIL
};
// states of the LCD display/UI input
enum eLcdDispStates {
LCD_START = 0,
LCD_SHOWSTATUS,
LCD_SHOWFS,
LCD_SHOWLS,
LCD_MAINMENU,
LCD_SETDTMENU,
LCD_SETFSMENU,
LCD_SETFSADD,
LCD_SETFSDEL,
LCD_SETFSDFLT,
LCD_SETLSMENU,
LCD_SETLSADD,
LCD_SETLSDEL,
LCD_SETLSDFLT,
LCD_END
};
enum eButtons {
BTN_NONE = 0,
BTN_SEL,
BTN_SET
};
#define CMDL 5
PROGMEM prog_char Date[CMDL] = "date";
PROGMEM prog_char Temp[CMDL] = "temp";
PROGMEM prog_char Addft[CMDL] = "addf";
PROGMEM prog_char Showfs[CMDL] = "shfs";
PROGMEM prog_char Delft[CMDL] = "delf";
PROGMEM prog_char Addls[CMDL] = "addl";
PROGMEM prog_char Showls[CMDL] = "shls";
PROGMEM prog_char Dells[CMDL] = "dell";
PROGMEM prog_char Setdt[CMDL] = "setd";
PROGMEM prog_char Help[CMDL] = "help";
PROGMEM prog_char Deffs[CMDL] = "deff";
PROGMEM prog_char Defls[CMDL] = "defl";
PROGMEM const char *cmdTable[] = {
Date, // display date/time
Temp, // display last temperature read
Addft, // add feeding times to feeding schedule
Showfs, // show feeding schedule
Delft, // delete feeding schedule
Addls, // add times to the light on/off schedule
Showls, // show light on/off schedule
Dells, // delete light on/off schedule
Setdt, // set date/time
Help, // show help
Deffs, // reset feeding schedule to default (9 AM, 9 PM).
Defls // reset light on/off schedule to default (8:30 AM on, 9:30 PM off)
};
// Help for commands.
// PROGMEM directive forces these variables into program memory
// instead of SRAM. Supported types must be used and special API
// functions to use these variables.
prog_char hlpstr_0[] PROGMEM = "- view D/T";
prog_char hlpstr_1[] PROGMEM = "- view temp.";
prog_char hlpstr_3[] PROGMEM = "hh:mm [hh:mm ...] - add feed times";
prog_char hlpstr_4[] PROGMEM = "- view feed sched.";
prog_char hlpstr_5[] PROGMEM = "- del. feed sched.";
prog_char hlpstr_6[] PROGMEM = "hh:mm [hh:mm ...] - add light times";
prog_char hlpstr_7[] PROGMEM = "- view light sched.";
prog_char hlpstr_8[] PROGMEM = "- del. light sched.";
prog_char hlpstr_9[] PROGMEM = "Yr Mon Day Hr Min - set D/T";
prog_char hlpstr_10[] PROGMEM = "- help";
prog_char hlpstr_12[] PROGMEM = "- set deflt feed sch.";
prog_char hlpstr_13[] PROGMEM = "- set deflt light sch.";
PROGMEM const char *cmdHelp[] = {
hlpstr_0,
hlpstr_1,
hlpstr_3,
hlpstr_4,
hlpstr_5,
hlpstr_6,
hlpstr_7,
hlpstr_8,
hlpstr_9,
hlpstr_10,
hlpstr_12,
hlpstr_13,
};
// this buffer must accomodate the longest string of hlpstr_N plus
// terminating NULL.
char progmembuf[36];
// -----------------------------------------------------------------------------
// Stepper motor
// The pin connections need to be 4 pins connected
// to Motor Driver In1, In2, In3, In4 and then the pins entered
// here in the sequence 1-3-2-4 for proper sequencing
Stepper small_stepper(STEPS_PER_MOTOR_REVOLUTION, STPMIN1PIN, STPMIN3PIN, STPMIN2PIN, STPMIN4PIN);
// Real time clock
RTC_DS1307 RTC;
// Temperature sensor
OneWire ds(DS1820PIN);
// set the LCD i2c address and display dimensions
LCD_I2C_BB lcd(0x3f,MAXCOL,MAXROW);
// -----------------------------------------------------------------------------
// global variables
// -----------------------------------------------------------------------------
int Steps2Take; // stepper motor
char textbuf[10]; // text buffer for temp. conversions
boolean cmdReady; // a flag - command is entered
String cmdTmp = ""; // temporary string buffer for command
String cmd = ""; // command string buffer
int cmdCode = -1; // command code
long lastTempRead = 0; // the unixtime second of last temperature read
boolean bTempRead = false; // if the temp. read was successfull
// temp. sensor read protocol flags and variables
byte present = 0;
byte type_s;
byte data[12];
byte addr[8];
float celsius, fahrenheit;
DateTime timeNow;
// UI control variables
byte lcdupdate; // lcd update counter
byte lcdstate; // current state of UI
byte lcdbacklt; // lcd backlight on counter (If > 0, backlight is on)
byte lcdmenuct; // counter for selected menu on lcd timeout
// scheduler flags and variables
// Schedule structure
struct SchedTbl {
int hour;
int minute;
};
#define LS_LEN 6 // lenght of the light schedule entry (HH:MM)
#define LS_MAX 8 // maximum number of light on/off schedule entries
int lsLen = -1; // the length of light on/off schedule (# of entries)
String lsTable[LS_MAX]; // the String form of light on/off scheduler, as saved in EEPROM
SchedTbl lsSched[LS_MAX]; // numeric table of light on/off scheduler
boolean lightswitch = false; // current status of the light switch
// Other definitions
#define TEMP_RD_EVERY 5 // how often to read temperature (minutes)
#define FS_LEN 6 // length of the feeding schedule entry (HH:MM)
#define FS_MAX 6 // maximum number of feeding schedule entries
int fsLen = -1; // the length of food dispensing schedule (# of entries)
String fsTable[FS_MAX]; // the String form of food disp. scheduler, as saved in EEPROM
SchedTbl fsSched[FS_MAX]; // numeric table of food disp scheduler
long foodDispTime = 0; // time (seconds) when food last dispensed
// constants
const float lowtemp = 70.0;
const float hitemp = 80.0;
const char *PROMPT = "CMD> ";
const char *LCDHELP = " Press SET for Menu";
const char *VERSION = "Fish Tank Ctrl. v3.0";
// -----------------------------------------------------------------------------
// Initialization sequence.
// -----------------------------------------------------------------------------
void setup()
{
lcd.init();
// Print a message to the LCD.
lcd.backlight();
lcd.setCursor(0,0);
lcd.print(VERSION);
lcd.setCursor(0,1);
lcd.print("(C) Marek Karcz 2014");
lcd.setCursor(0,2);
lcd.print("Init...");
lcdstate = LCD_START;
pinMode(GREENLEDPIN, OUTPUT);
pinMode(REDLEDPIN, OUTPUT);
digitalWrite(GREENLEDPIN, LOW);
digitalWrite(REDLEDPIN, LOW);
lcdupdate = LCDUPD;
lcdmenuct = MENUOUT;
cmdReady = false;
Serial.begin(9600); // serial port will be the main mode of communication
// with the device and programming the light and feeding
// scheduler
pinMode(LIGHTPIN, OUTPUT);
lightOn();
// power to i2c_ds1307_at24c32 module provided via A2, A3 pins
// for Uno: A4 = SDA, A5 = SCL
pinMode(A3, OUTPUT);
digitalWrite(A3, HIGH);
pinMode(A2, OUTPUT);
digitalWrite(A2, LOW);
// start communication, I2C and RTC
Wire.begin();
RTC.begin();
ds.reset();
// NOTE: Stepper Library sets pins as outputs
// Rotate CW 1/8 turn forward and backwards to show the system is working
Steps2Take = STEPS_PER_OUTPUT_REVOLUTION / 8;
small_stepper.setSpeed(600);
small_stepper.step(Steps2Take);
Steps2Take = - Steps2Take;
small_stepper.setSpeed(600); // 700 a good max speed??
small_stepper.step(Steps2Take);
lightOff();
initLsTable();
lsLen = loadSavedLS2Table();
for (int i = 0; i < lsLen && i < LS_MAX; i++)
{
lsSched[i].hour = getHourFromTimeEntry(lsTable[i]);
lsSched[i].minute = getMinuteFromTimeEntry(lsTable[i]);
}
initFsTable();
fsLen = loadSavedFS2Table();
for (int i = 0; i < fsLen && i < FS_MAX; i++)
{
fsSched[i].hour = getHourFromTimeEntry(fsTable[i]);
fsSched[i].minute = getMinuteFromTimeEntry(fsTable[i]);
}
pinMode(BTNSET, INPUT);
pinMode(BTNSEL, INPUT);
digitalWrite(GREENLEDPIN, HIGH);
digitalWrite(REDLEDPIN, HIGH);
Serial.println(VERSION);
Serial.println("Ready.");
Serial.print(PROMPT);
lcd.setCursor(0,3);
lcd.print("Done.");
delay(500);
lcdDispMainScrFrame();
lcdstate = LCD_SHOWSTATUS;
lcdbacklt = BACKLTOUT;
}
// -----------------------------------------------------------------------------
// Main control loop.
// -----------------------------------------------------------------------------
void loop()
{
// Read time.
timeNow = RTC.now();
// control light via scheduler
controlLight();
// control food dispenser
controlFoodDisp();
// Read temperature.
readTemp();
// refresh LCD, read user input
if (lcdstate >= LCD_MAINMENU)
{
switch (lcdstate)
{
case LCD_MAINMENU:
lcdstate = execMainMenu();
break;
case LCD_SETDTMENU:
lcdstate = execSetDtMenu();
break;
case LCD_SETFSMENU:
lcdstate = execSetFsMenu();
if (lcdstate == LCD_SHOWFS)
lcdShowFeedingSchedule();
break;
case LCD_SETFSDFLT:
serialDefaultFS();
lcdShowFeedingSchedule();
break;
case LCD_SETFSDEL:
serialDeleteFeedingSchedule();
lcdShowFeedingSchedule();
break;
case LCD_SETFSADD:
execAddFeedingTimeMenu();
lcdShowFeedingSchedule();
break;
case LCD_SETLSMENU:
lcdstate = execSetLsMenu();
if (lcdstate == LCD_SHOWLS)
lcdShowLightSchedule();
break;
case LCD_SETLSDFLT:
serialDefaultLS();
lcdShowLightSchedule();
break;
case LCD_SETLSDEL:
serialDeleteLightSchedule();
lcdShowLightSchedule();
break;
case LCD_SETLSADD:
execAddLightTimeMenu();
lcdShowLightSchedule();
break;
default:
lcdstate = LCD_SHOWSTATUS;
break;
}
if (lcdstate != LCD_SHOWFS && lcdstate != LCD_SHOWLS)
lcdDispMainScrFrame();
}
if (lcdupdate == LCDUPD && lcdstate == LCD_SHOWSTATUS)
{
lcdWriteDTNow();
lcdConvWriteTemp();
String ft = getNextFeedingTime();
String lt = getNextLightSwitchTime();
lcdClearRow(2);
lcd.print("Nxt F<");
lcd.print(ft);
lcd.print(">L<");
lcd.print(lt);
lcd.print('>');
}
// command interpreter (via serial)
interpretCommand();
lcdupdate--;
if ((lcdstate == LCD_SHOWFS || lcdstate == LCD_SHOWLS)
&& lcdupdate == 0
)
{
lcdstate = LCD_SHOWSTATUS;
lcdDispMainScrFrame();
}
if (lcdupdate == 0)
lcdupdate = LCDUPD;
if (lcdbacklt > 0)
lcdbacklt--;
if (lcdbacklt == 0) // time-out LCD backlight
lcd.noBacklight();
if (lcdstate >= LCD_MAINMENU && lcdmenuct > 0)
lcdmenuct--;
if (lcdmenuct == 0) // time-out any opened menu
{
lcdmenuct = MENUOUT;
lcdstate = LCD_SHOWSTATUS;
}
int selbtn = BTN_NONE;
// check buttons input
if ((selbtn = readButtons()) != BTN_NONE)
{
lcdbacklt = BACKLTOUT;
lcd.backlight();
switch (selbtn)
{
case BTN_SET:
if (lcdstate < LCD_MAINMENU)
lcdstate = LCD_MAINMENU;
break;
case BTN_SEL:
break;
default:
break;
}
}
delay(100);
}
// -----------------------------------------------------------------------------
// Basic screen re-draw with static data.
// -----------------------------------------------------------------------------
void lcdDispMainScrFrame()
{
lcd.clear();
lcd.setCursor(0,0);
lcd.print(VERSION);
lcd.setCursor(0,3);
lcd.print(LCDHELP);
}
// -----------------------------------------------------------------------------
// Simple up/down set number UI with buttons and LCD.
// -----------------------------------------------------------------------------
int setNumberMenu(int start,
int low,
int high,
int row,
unsigned int iter)
{
boolean loopmenu = true;
int setNumber = start;
int tout = iter;
while (loopmenu)
{
int btnsel = readButtons();
switch (btnsel)
{
case BTN_SET:
if (setNumber > low)
{
setNumber--;
lcdClearRow(row);
lcd.print(setNumber);
}
tout = iter;
break;
case BTN_SEL:
if (setNumber < high)
{
setNumber++;
lcdClearRow(row);
lcd.print(setNumber);
}
tout = iter;
break;
default:
break;
}
tout--;
if (tout == 0)
loopmenu = false;
delay(100);
}
return setNumber;
}
// -----------------------------------------------------------------------------
// Execute UI/Menu - add feeding time.
// -----------------------------------------------------------------------------
void execAddFeedingTimeMenu()
{
int setHour = 0;
int setMinute = 0;
int n = 0;
initFsTable();
fsLen = loadSavedFS2Table(); // load feeding schedule from EEPROM to lsTable
for (int i = 0; i < fsLen && i < FS_MAX; i++, n++)
{
fsSched[i].hour = getHourFromTimeEntry(fsTable[i]);
fsSched[i].minute = getMinuteFromTimeEntry(fsTable[i]);
}
if (n < FS_MAX)
{
setHour = timeNow.hour();
setMinute = timeNow.minute();
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Feeding Hour:");
lcd.setCursor(0,1);
lcd.print(setHour);
setHour = setNumberMenu(setHour, 0, 23, 1, 30);
//lcd.clear();
lcd.setCursor(0,2);
lcd.print("Feeding Minute:");
lcd.setCursor(0,3);
lcd.print(setMinute);
setMinute = setNumberMenu(setMinute, 0, 59, 3, 30);
fsSched[n].hour = setHour;
fsSched[n].minute = setMinute;
fsLen = n + 1;
sortFsTables(); // sort feeding schedule numeric tables
convFsTblToStringTbl(); // convert fsHoursTbl and fsMinutesTbl to fsTable
int err = saveFsTable(); // save feeding schedule table to EEPROM
if (err != ERR_OK)
{
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Err# ");
lcd.print(err);
delay(2000);
}
}
else
{
lcd.clear();
lcd.setCursor(0,0);
lcd.print("ERR: Schedule full.");
delay(2000);
}
}
// -----------------------------------------------------------------------------
// Execute UI/Menu - add light switch time.
// Note - number of entries should be even to match every ON with OFF.
// Therefore it is normal to see a warning about missing OFF switch
// after adding the entry. This is a remainder, not an error.
// Just add another switch time later.
// -----------------------------------------------------------------------------
void execAddLightTimeMenu()
{
int setHour = 0;
int setMinute = 0;
int n = 0;
initLsTable();
fsLen = loadSavedLS2Table(); // load light schedule from EEPROM to lsTable
for (int i = 0; i < lsLen && i < LS_MAX; i++, n++)
{
lsSched[i].hour = getHourFromTimeEntry(lsTable[i]);
lsSched[i].minute = getMinuteFromTimeEntry(lsTable[i]);
}
if (n < LS_MAX)
{
setHour = timeNow.hour();
setMinute = timeNow.minute();
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Light Switch Hour:");
lcd.setCursor(0,1);
lcd.print(setHour);
setHour = setNumberMenu(setHour, 0, 23, 1, 30);
//lcd.clear();
lcd.setCursor(0,2);
lcd.print("Ligth Switch Minute:");
lcd.setCursor(0,3);
lcd.print(setMinute);
setMinute = setNumberMenu(setMinute, 0, 59, 3, 30);
lsSched[n].hour = setHour;
lsSched[n].minute = setMinute;
lsLen = n + 1;
sortLsTables(); // sort light schedule numeric tables
convLsTblToStringTbl(); // convert lsHoursTbl and lsMinutesTbl to lsTable
int err = saveLsTable(); // save light schedule table to EEPROM
if (err != ERR_OK)
{
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Err# ");
lcd.print(err);
delay(2000);
}
}
else
{
lcd.clear();
lcd.setCursor(0,0);
lcd.print("ERR: Schedule full.");
delay(2000);
}
}
// -----------------------------------------------------------------------------
// Execute UI/Menu - select feeding schedule setup option.
// -----------------------------------------------------------------------------
int execSetFsMenu()
{
int sel = 0;
boolean loopmenu = true;
int tout = MENUOUT;
int ret = LCD_SHOWSTATUS;
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Show Feeding Sched.");
lcd.setCursor(0,1);
lcd.print("Add Feeding Time");
lcd.setCursor(0,2);
lcd.print("Del. Feeding Sched.");
lcd.setCursor(0,3);
lcd.print("Set Default");
lcd.setCursor(MAXCOL-1,sel);
lcd.print('<');
while (loopmenu)
{
int selbtn = BTN_NONE;
if ((selbtn = readButtons()) != BTN_NONE)
{
switch (selbtn)
{
case BTN_SEL:
tout = MENUOUT;
sel++;
if (sel > 3)
sel = 0;
break;
case BTN_SET:
loopmenu = false;
switch (sel)
{
case 0:
ret = LCD_SHOWFS;
break;
case 1:
ret = LCD_SETFSADD;
break;
case 2:
ret = LCD_SETFSDEL;
break;
case 3:
ret = LCD_SETFSDFLT;
break;
default:
break;
}
default:
break;
}
}
tout--;
if (tout == 0)
loopmenu = false;
delay(100);
lcdDispMenuSel(sel);
}
return ret;
}
// -----------------------------------------------------------------------------
// Execute UI/Menu - select light switch schedule setup option.
// -----------------------------------------------------------------------------
int execSetLsMenu()
{
int sel = 0;
boolean loopmenu = true;
int tout = MENUOUT;
int ret = LCD_SHOWSTATUS;
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Show Light Schedule");
lcd.setCursor(0,1);
lcd.print("Add Lite OnOff Time");
lcd.setCursor(0,2);
lcd.print("Del. Light Schedule");
lcd.setCursor(0,3);
lcd.print("Set Default");
lcd.setCursor(MAXCOL-1,sel);
lcd.print('<');
while (loopmenu)
{
int selbtn = BTN_NONE;
if ((selbtn = readButtons()) != BTN_NONE)
{
switch (selbtn)
{
case BTN_SEL:
tout = MENUOUT;
sel++;
if (sel > 3)
sel = 0;
break;
case BTN_SET:
loopmenu = false;
switch (sel)
{
case 0:
ret = LCD_SHOWLS;
break;
case 1:
ret = LCD_SETLSADD;
break;
case 2:
ret = LCD_SETLSDEL;
break;
case 3:
ret = LCD_SETLSDFLT;
break;
default:
break;
}
default:
break;
}
}
tout--;
if (tout == 0)
loopmenu = false;
delay(100);
lcdDispMenuSel(sel);
}
return ret;
}
// -----------------------------------------------------------------------------
// Execute UI/Menu - select setup option.
// -----------------------------------------------------------------------------
int execMainMenu()
{
int sel = 1;
boolean loopmenu = true;
int tout = MENUOUT;
int ret = LCD_SHOWSTATUS;
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Setup:");
lcd.setCursor(0,1);
lcd.print("Date/Time");
lcd.setCursor(0,2);
lcd.print("Feeding Schedule");
lcd.setCursor(0,3);
lcd.print("Light Schedule");
lcd.setCursor(MAXCOL-1,sel);
lcd.print('<');
while (loopmenu)
{
int selbtn = BTN_NONE;
if ((selbtn = readButtons()) != BTN_NONE)
{
switch (selbtn)
{
case BTN_SEL:
tout = MENUOUT;
sel++;
if (sel > 3)
sel = 1;
break;
case BTN_SET:
loopmenu = false;
switch (sel)
{
case 1:
ret = LCD_SETDTMENU;
break;
case 2:
ret = LCD_SETFSMENU;
break;
case 3:
ret = LCD_SETLSMENU;
break;
default:
break;
}
default:
break;
}
}
tout--;
if (tout == 0)
loopmenu = false;
delay(100);
lcdDispMenuSel(sel);
}
return ret;
}
// -----------------------------------------------------------------------------
// Execute UI/Menu - set the date/time (Real Time Clock).
// -----------------------------------------------------------------------------
int execSetDtMenu()
{
int ret = LCD_SHOWSTATUS;
int setYear = 2013;
int setMonth = 12;
int setDay = 7;
int setHour = 0;
int setMinute = 0;
timeNow = RTC.now();
setYear = timeNow.year();
setMonth = timeNow.month();
setDay = timeNow.day();
setHour = timeNow.hour();
setMinute = timeNow.minute();
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Year:");
lcd.setCursor(0,1);
lcd.print(setYear);
setYear = setNumberMenu(setYear, 2014, 2100, 1, 20);
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Month:");
lcd.setCursor(0,1);
lcd.print(setMonth);
setMonth = setNumberMenu(setMonth, 1, 12, 1, 20);
//lcd.clear();
lcd.setCursor(0,2);
lcd.print("Day:");
lcd.setCursor(0,3);
lcd.print(setDay);
setDay = setNumberMenu(setDay, 1, 31, 3, 20);
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Hour:");
lcd.setCursor(0,1);
lcd.print(setHour);
setHour = setNumberMenu(setHour, 0, 23, 1, 20);
//lcd.clear();
lcd.setCursor(0,2);
lcd.print("Minute:");
lcd.setCursor(0,3);
lcd.print(setMinute);
setMinute = setNumberMenu(setMinute, 0, 59, 3, 20);
RTC.adjust(DateTime(setYear, setMonth, setDay, setHour, setMinute, 0));
return ret;
}
// -----------------------------------------------------------------------------
// LCD - display marker '<' next to the currently selected menu item.
// -----------------------------------------------------------------------------
void lcdDispMenuSel(int sel)
{
for (int i=0; i<MAXROW; i++)
{
lcd.setCursor(MAXCOL-1, i);
if (i == sel)
lcd.print('<');
else
lcd.print(' ');
}
}
// -----------------------------------------------------------------------------
// Determine if/which button is pressed (and released).
// NOTE: Button must be released after it is pressed in order to proceed.
// -----------------------------------------------------------------------------
int readButtons()
{
int ret = BTN_NONE;
if (digitalRead(BTNSEL) == LOW)
{
while(digitalRead(BTNSEL) == LOW);
ret = BTN_SEL;
}
else if (digitalRead(BTNSET) == LOW)
{
while(digitalRead(BTNSET) == LOW);
ret = BTN_SET;
}
return ret;
}
// -----------------------------------------------------------------------------
// Clear row on the lcd display and position the cursor at the start of the row.
// -----------------------------------------------------------------------------
void lcdClearRow(unsigned int row)
{
String clr = String(" ");
clr.substring(0,MAXCOL);
lcd.setCursor(0,row);
lcd.print(clr);
/* this loop seems a bit slower than above code
for(int i=0; i<MAXCOL; i++)
lcd.print(' ');
*/
lcd.setCursor(0,row);
}
// -----------------------------------------------------------------------------
// Interpret command - serial port interface, this is a separate means of
// communicating with device, apart from buttons/LCD.
// -----------------------------------------------------------------------------
void interpretCommand()
{
if (cmdReady)
{
int err = 0;
String memCmd;
#ifdef MYDEBUG0
Serial.print("DBG: command=\"");
Serial.print(cmd);
Serial.println("\"");
#endif
cmdCode = getCmdCode();
cmdReady = false;
memCmd = cmd;
cmd = "";
switch (cmdCode) {
case CMD_DATE:
serialWriteDTNow();
break;
case CMD_TEMP:
if (bTempRead)
serialConvWriteTemp();
else
err = ERR_TEMPRD;
break;
case CMD_ADDFT:
err = serialAddFeedingSchedule(memCmd);
break;
case CMD_SHOWFS:
serialShowFeedingSchedule();
lcdShowFeedingSchedule();
break;
case CMD_DELFT:
serialDeleteFeedingSchedule();
break;
case CMD_ADDLS:
err = serialAddLightSchedule(memCmd);
break;
case CMD_SHOWLS:
serialShowLightSchedule();
lcdShowLightSchedule();
break;
case CMD_DELLS:
serialDeleteLightSchedule();
break;
case CMD_SETDT:
err = serialSetDateTime(memCmd);
break;
case CMD_HELP:
serialHelp();
break;
case CMD_DEFFS:
err = serialDefaultFS();
break;
case CMD_DEFLS:
err = serialDefaultLS();
break;
default:
err = ERR_UNKNCMD;
break;
}
if (0 != err)
{
Serial.print("ERR: #");
Serial.println(err);
}
Serial.print(PROMPT);
}
}
// -----------------------------------------------------------------------------
// Remove current light on/off schedule and replace it with a default one.
// -----------------------------------------------------------------------------
int serialDefaultLS()
{
int err = ERR_OK;
String deflscmd;
strcpy_P(progmembuf, (char*)pgm_read_word(&(cmdTable[CMD_ADDLS])));
deflscmd = String(progmembuf) + " 08:30 21:30";
serialDeleteLightSchedule();
err = serialAddLightSchedule(deflscmd);
return err;
}
// -----------------------------------------------------------------------------
// Remove current feeding schedule and replace it with a default one.
// -----------------------------------------------------------------------------
int serialDefaultFS()
{
int err = ERR_OK;
String deffscmd;
strcpy_P(progmembuf, (char*)pgm_read_word(&(cmdTable[CMD_ADDFT])));
deffscmd = String(progmembuf) + " 09:00 21:00";
serialDeleteFeedingSchedule();
err = serialAddFeedingSchedule(deffscmd);
return err;
}
// -----------------------------------------------------------------------------
// Display the list of commands.
// -----------------------------------------------------------------------------
void serialHelp()
{
for (int i = 0; i < CMD_NIL; i++)
{
strcpy_P(progmembuf, (char*)pgm_read_word(&(cmdTable[i])));
Serial.print(progmembuf);
Serial.print(' ');
strcpy_P(progmembuf, (char*)pgm_read_word(&(cmdHelp[i])));
Serial.println(progmembuf);
}
}
// -----------------------------------------------------------------------------
// Process addft command. Add feeding times to food dispenser scheduler.
// addft hh:mm [hh:mm ...]
// -----------------------------------------------------------------------------
int serialAddFeedingSchedule(String command)
{
int err = ERR_OK;
int n = 0;
boolean token = false;
String argBuf;
initFsTable();
fsLen = loadSavedFS2Table(); // load feeding schedule from EEPROM to lsTable
for (int i = 0; i < fsLen && i < FS_MAX; i++, n++)
{
fsSched[i].hour = getHourFromTimeEntry(fsTable[i]);
fsSched[i].minute = getMinuteFromTimeEntry(fsTable[i]);
}
#ifdef MYDEBUG3
Serial.print("DBG: Loaded ");
Serial.print(n);
Serial.println(" entries.");
#endif
strcpy_P(progmembuf, (char*)pgm_read_word(&(cmdTable[CMD_ADDFT])));
for (int i = strlen(progmembuf); i <= command.length() && ERR_OK == err; i++)
{
if (token && (command.charAt(i) == ' ' || i == command.length()))
{
#ifdef MYDEBUG3
Serial.print("DBG: Adding item #");
Serial.print(n);
Serial.println(".");
Serial.print("DBG: val=\"");
Serial.print(argBuf);
Serial.println("\"");
#endif
if (n < FS_MAX)
{
if (5 == argBuf.length())
{
fsTable[n] = argBuf;
fsSched[n].hour = getHourFromTimeEntry(argBuf);
fsSched[n].minute = getMinuteFromTimeEntry(argBuf);
}
else
{
err = ERR_FSENTINVFMT;
#ifdef MYDEBUG3
Serial.println("DBG: Wrong argument format.");
Serial.print("DBG: val=\"");
Serial.print(argBuf);
Serial.println("\"");
#endif
break;
}
}
else
{
err = ERR_TOOMANYFS;
#ifdef MYDEBUG3
Serial.println("DBG: Too many arguments.");
Serial.print("DBG: FS_MAX=");
Serial.print(FS_MAX);
Serial.println(".");
#endif
break;
}
token = false;
argBuf = "";
n++;
}
else if (command.charAt(i) != ' ')
{
if (false == token)
token = true;
if (5 > argBuf.length())
argBuf = argBuf + command.charAt(i);
else
{
err = ERR_ARGTOOLONG;
#ifdef MYDEBUG3
Serial.println("DBG: Argument too long (>5).");
#endif
break;
}
}
}
if (ERR_OK == err)
{
fsLen = n;
sortFsTables(); // sort feeding schedule numeric tables
convFsTblToStringTbl(); // convert fsHoursTbl and fsMinutesTbl to fsTable
err = saveFsTable(); // save feeding schedule table to EEPROM
}
return err;
}
// -----------------------------------------------------------------------------
// Read temperature sensor. Turn the temp. warning LED-s on/off.
// -----------------------------------------------------------------------------
void readTemp()
{
byte i;
if (0 == lastTempRead || timeNow.unixtime() - lastTempRead > TEMP_RD_EVERY * 60)
{
bTempRead = false;
// try 2 times
if ( !ds.search(addr)) {
ds.reset_search();
delay(250);
if ( !ds.search(addr)) {
ds.reset_search();
delay(250);
return;
}
}
if (OneWire::crc8(addr, 7) != addr[7]) {
// Invalid CRC
return;
}
type_s = determineDSChip();
if (2 == type_s) {
// Device is not a DS18x20 family device.
return;
}
ds.reset();
ds.select(addr);
ds.write(0x44, 1); // start conversion, with parasite power on at the end
delay(1000); // maybe 750ms is enough, maybe not
// we might do a ds.depower() here, but the reset will take care of it.
present = ds.reset();
ds.select(addr);
ds.write(0xBE); // Read Scratchpad
for ( i = 0; i < 9; i++) { // we need 9 bytes
data[i] = ds.read();
}
bTempRead = true;
lastTempRead = timeNow.unixtime();
// Convert the data to actual temperature
// because the result is a 16 bit signed integer, it should
// be stored to an "int16_t" type, which is always 16 bits
// even when compiled on a 32 bit processor.
int16_t raw = (data[1] << 8) | data[0];
if (type_s) {
raw = raw << 3; // 9 bit resolution default
if (data[7] == 0x10) {
// "count remain" gives full 12 bit resolution
raw = (raw & 0xFFF0) + 12 - data[6];
}
} else {
byte cfg = (data[4] & 0x60);
// at lower res, the low bits are undefined, so let's zero them
if (cfg == 0x00) raw = raw & ~7; // 9 bit resolution, 93.75 ms
else if (cfg == 0x20) raw = raw & ~3; // 10 bit res, 187.5 ms
else if (cfg == 0x40) raw = raw & ~1; // 11 bit res, 375 ms
//// default is 12 bit resolution, 750 ms conversion time
}
celsius = (float)raw / 16.0;
fahrenheit = celsius * 1.8 + 32.0;
if (fahrenheit < lowtemp)
{
digitalWrite(GREENLEDPIN, LOW);
digitalWrite(REDLEDPIN, HIGH);
}
else if (fahrenheit > hitemp)
{
digitalWrite(REDLEDPIN, LOW);
digitalWrite(GREENLEDPIN, HIGH);
}
else
{
digitalWrite(REDLEDPIN, HIGH);
digitalWrite(GREENLEDPIN, HIGH);
}
}
}
// -----------------------------------------------------------------------------
// Control light per light on/off scheduler setup.
// Scheduler table contains accelerated ON/OFF times.
// -----------------------------------------------------------------------------
void controlLight()
{
boolean turnon = true;
if (lsLen <= 0)
flipLightSwitch(false); // there is no light on/off schedule, keep the light off
else
{
// check the light on/off schedule entries against current time
for (int i = 0; i < lsLen; i++)
{
// check if the current time falls between current and next scheduled times
if ((lsSched[i].hour == timeNow.hour() && lsSched[i].minute <= timeNow.minute())
||
lsSched[i].hour < timeNow.hour()
)
{
// we are past the currently checked scheduled time,
// check if we are before the next scheduled time
if (i+1 < lsLen)
{
// the next schedule exists, check if current time falls before it
if ((lsSched[i+1].hour == timeNow.hour() && lsSched[i+1].minute > timeNow.minute())
||
lsSched[i+1].hour > timeNow.hour()
)
{
// yes, the current time falls within currently checked period
flipLightSwitch(turnon);
}
}
else
{
// there is no next schedule, so turn the light ON or OFF accordingly
flipLightSwitch(turnon);
}
}
turnon = ((turnon) ? false : true);
}
}
}
// -----------------------------------------------------------------------------
// Turn the light ON or OFF depending on the argument and current status of
// the light switch flag.
// -----------------------------------------------------------------------------
void flipLightSwitch(boolean turnon)
{
if (turnon)
{
if (false == lightswitch)
{
// if the light is OFF, turn it ON, flip the flag
lightOn();
lightswitch = true;
lcdbacklt = BACKLTOUT;
lcd.backlight();
}
}
else
{
if (lightswitch)
{
// if the light is ON, turn it OFF, flip the flag
lightOff();
lightswitch = false;
lcdbacklt = BACKLTOUT;
lcd.backlight();
}
}
}
// -----------------------------------------------------------------------------
// Control food dispenser per feeding schedule setup.
// -----------------------------------------------------------------------------
void controlFoodDisp()
{
for (int i = 0; i < fsLen; i++)
{
if (fsSched[i].hour == timeNow.hour() && fsSched[i].minute == timeNow.minute())
{
if (foodDispTime == 0 || timeNow.unixtime() - foodDispTime > 60)
{
lcdbacklt = BACKLTOUT;
lcd.backlight();
dispenseFood();
foodDispTime = timeNow.unixtime();
}
}
}
}
// -----------------------------------------------------------------------------
// Get string representation of the next feeding time.
// -----------------------------------------------------------------------------
String getNextFeedingTime()
{
String ret = "??:??";
boolean found = false;
for (int i = 0; i < fsLen; i++)
{
if ((fsSched[i].hour == timeNow.hour() && fsSched[i].minute >= timeNow.minute())
||
(fsSched[i].hour > timeNow.hour())
)
{
ret = fsTable[i];
found = true;
break;
}
}
// not found, perhaps there is one the next day?
if (false == found)
{
for (int i = 0; i < fsLen; i++)
{
if ((fsSched[i].hour == 0 && fsSched[i].minute >= timeNow.minute())
||
(fsSched[i].hour > 0)
)
{
ret = fsTable[i];
break;
}
}
}
return ret;
}
// -----------------------------------------------------------------------------
// Get string representation of the next light switch time.
// -----------------------------------------------------------------------------
String getNextLightSwitchTime()
{
String ret = "??:??";
boolean found = false;
for (int i = 0; i < lsLen; i++)
{
if ((lsSched[i].hour == timeNow.hour() && lsSched[i].minute >= timeNow.minute())
||
(lsSched[i].hour > timeNow.hour())
)
{
ret = lsTable[i];
found = true;
break;
}
}
// not found, perhaps there is one the next day?
if (false == found)
{
for (int i = 0; i < lsLen; i++)
{
if ((lsSched[i].hour == 0 && lsSched[i].minute >= timeNow.minute())
||
(lsSched[i].hour > 0)
)
{
ret = lsTable[i];
break;
}
}
}
return ret;
}
// -----------------------------------------------------------------------------
// Send light on/off schedule to serial line.
// -----------------------------------------------------------------------------
void serialShowLightSchedule()
{
boolean turnon = true;
initLsTable();
lsLen = loadSavedLS2Table();
for (int i = 0; i < lsLen && i < LS_MAX; i++)
{
lsSched[i].hour = getHourFromTimeEntry(lsTable[i]);
lsSched[i].minute = getMinuteFromTimeEntry(lsTable[i]);
Serial.print(lsTable[i]);
Serial.print(" ");
Serial.println(((turnon) ? "ON" : "OFF"));
turnon = ((turnon) ? false : true);
}
if (false == turnon)
Serial.println("! Missing OFF time.");
}
// -----------------------------------------------------------------------------
// Show light on/off schedule on LCD.
// -----------------------------------------------------------------------------
void lcdShowLightSchedule()
{
boolean turnon = true;
int row = 1;
initLsTable();
lsLen = loadSavedLS2Table();
lcd.clear();
lcd.setCursor(0,0);
//lcd.print("Light schedule:");
//lcd.setCursor(0,1);
for (int i = 0; i < lsLen && i < LS_MAX; i++)
{
lsSched[i].hour = getHourFromTimeEntry(lsTable[i]);
lsSched[i].minute = getMinuteFromTimeEntry(lsTable[i]);
lcd.print(lsTable[i]);
lcd.print(' ');
lcd.print(((turnon) ? "ON" : "OFF"));
lcd.print(' ');
turnon = ((turnon) ? false : true);
if (i < lsLen-1 && (i+1)%2 == 0)
lcd.setCursor(0,row++);
}
if (false == turnon)
{
// this is just a remainder to enter matching OFF time
// nothing to worry about
lcd.setCursor(0,3);
lcd.print("! Missing OFF time.");
}
lcdstate = LCD_SHOWLS;
lcdupdate = SHOWLSOUT;
}
// -----------------------------------------------------------------------------
// Send feeding schedule to serial line.
// -----------------------------------------------------------------------------
void serialShowFeedingSchedule()
{
initFsTable();
fsLen = loadSavedFS2Table();
for (int i = 0; i < fsLen && i < FS_MAX; i++)
{
fsSched[i].hour = getHourFromTimeEntry(fsTable[i]);
fsSched[i].minute = getMinuteFromTimeEntry(fsTable[i]);
Serial.println(fsTable[i]);
}
}
// -----------------------------------------------------------------------------
// Send feeding schedule to lcd.
// -----------------------------------------------------------------------------
void lcdShowFeedingSchedule()
{
int row = 2;
initFsTable();
fsLen = loadSavedFS2Table();
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Feeding schedule:");
lcd.setCursor(0,1);
for (int i = 0; i < fsLen && i < FS_MAX; i++)
{
fsSched[i].hour = getHourFromTimeEntry(fsTable[i]);
fsSched[i].minute = getMinuteFromTimeEntry(fsTable[i]);
lcd.print(fsTable[i]);
lcd.print(' ');
if ((i+1)%3 == 0)
lcd.setCursor(0,row++);
}
lcdstate = LCD_SHOWFS;
lcdupdate = SHOWFSOUT;
}
// -----------------------------------------------------------------------------
// Initialize light on/off schedule tables.
// -----------------------------------------------------------------------------
void initLsTable()
{
for (int i = 0; i < LS_MAX; i++)
{
lsTable[i] = "";
lsSched[i].hour = 0;
lsSched[i].minute = 0;
}
}
// -----------------------------------------------------------------------------
// Initialize feeding schedule tables.
// -----------------------------------------------------------------------------
void initFsTable()
{
for (int i = 0; i < FS_MAX; i++)
{
fsTable[i] = "";
fsSched[i].hour = 0;
fsSched[i].minute = 0;
}
}
// -----------------------------------------------------------------------------
// Delete entire light on/off schedule.
// -----------------------------------------------------------------------------
void serialDeleteLightSchedule()
{
int addr = 0;
while (addr < LS_LEN * LS_MAX)
EEPROM.write(addr++, 0);
lsLen = -1;
}
// -----------------------------------------------------------------------------
// Delete entire feeding schedule.
// -----------------------------------------------------------------------------
void serialDeleteFeedingSchedule()
{
int addr = LS_LEN * LS_MAX;
while (addr < LS_LEN * LS_MAX + FS_LEN * FS_MAX)
EEPROM.write(addr++, 0);
fsLen = -1;
}
// -----------------------------------------------------------------------------
// Parse/interpret addls command, add times to light on/off schedule.
// addls hh:mm [hh:mm ...]
// -----------------------------------------------------------------------------
int serialAddLightSchedule(String command)
{
int err = ERR_OK;
int n = 0;
boolean token = false;
String argBuf;
initLsTable();
lsLen = loadSavedLS2Table(); // load light on/off schedule from EEPROM to lsTable
for (int i = 0; i < lsLen && i < LS_MAX; i++, n++)
{
lsSched[i].hour = getHourFromTimeEntry(lsTable[i]);
lsSched[i].minute = getMinuteFromTimeEntry(lsTable[i]);
}
#ifdef MYDEBUG2
Serial.print("DBG: Loaded ");
Serial.print(n);
Serial.println(" entries.");
#endif
strcpy_P(progmembuf, (char*)pgm_read_word(&(cmdTable[CMD_ADDLS])));
for (int i = strlen(progmembuf); i <= command.length() && ERR_OK == err; i++)
{
if (token && (command.charAt(i) == ' ' || i == command.length()))
{
#ifdef MYDEBUG2
Serial.print("DBG: Adding item #");
Serial.print(n);
Serial.println(".");
Serial.print("DBG: val=\"");
Serial.print(argBuf);
Serial.println("\"");
#endif
if (n < LS_MAX)
{
if (5 == argBuf.length())
{
lsTable[n] = argBuf;
lsSched[n].hour = getHourFromTimeEntry(argBuf);
lsSched[n].minute = getMinuteFromTimeEntry(argBuf);
}
else
{
err = ERR_LSENTINVFMT;
#ifdef MYDEBUG2
Serial.println("DBG: Wrong argument format.");
Serial.print("DBG: val=\"");
Serial.print(argBuf);
Serial.println("\"");
#endif
break;
}
}
else
{
err = ERR_TOOMANYLS;
#ifdef MYDEBUG2
Serial.println("DBG: Too many arguments.");
Serial.print("DBG: LS_MAX=");
Serial.print(LS_MAX);
Serial.println(".");
#endif
break;
}
token = false;
argBuf = "";
n++;
}
else if (command.charAt(i) != ' ')
{
if (false == token)
token = true;
if (5 > argBuf.length())
argBuf = argBuf + command.charAt(i);
else
{
err = ERR_ARGTOOLONG;
#ifdef MYDEBUG2
Serial.println("DBG: Argument too long (>5).");
#endif
break;
}
}
}
if (ERR_OK == err)
{
lsLen = n;
sortLsTables(); // sort light on/off schedule numeric tables
convLsTblToStringTbl(); // convert lsHoursTbl and LsMinutesTbl to lsTable
err = saveLsTable(); // save light on/off schedule table to EEPROM
}
return err;
}
// -----------------------------------------------------------------------------
// Save schedule to EEPROM.
// -----------------------------------------------------------------------------
int saveSchedule(String tbl[],
int startaddr,
int endaddr,
int numofentries,
int arglen,
int rangeerr,
int fmterr)
{
int err = ERR_OK;
int addr = 0;
addr = startaddr;
for (int i = 0; i < numofentries && ERR_OK == err; i++)
{
if (arglen == tbl[i].length())
{
for (int j = 0; j < tbl[i].length(); j++)
{
if (addr < endaddr)
{
EEPROM.write(addr++, tbl[i].charAt(j));
}
else
{
err = rangeerr; // too many entries, exceeds alloted EEPROM space
#ifdef MYDEBUG1
Serial.print("DBG: Saving table, address out of range: ");
Serial.print(addr);
Serial.println(".");
#endif
break;
}
}
EEPROM.write(addr++, 0);
}
else
{
err = fmterr; // invalid entry format
#ifdef MYDEBUG1
Serial.print("DBG: Saving table, invalid format, value=\"");
Serial.print(tbl[i]);
Serial.println("\"");
#endif
break;
}
}
return err;
}
// -----------------------------------------------------------------------------
// Save light on/off schedule table to EEPROM.
// -----------------------------------------------------------------------------
int saveLsTable()
{
int err = ERR_OK;
// light on/off schedule is saved after temperature log.
err = saveSchedule(lsTable,
0,
LS_LEN*LS_MAX,
lsLen,
5,
ERR_LSEEPROMOOR,
ERR_LSENTINVFMT);
return err;
}
// -----------------------------------------------------------------------------
// Save feeding schedule table to EEPROM.
// -----------------------------------------------------------------------------
int saveFsTable()
{
int err = ERR_OK;
int addr = 0;
// Feeding schedule is saved after temperature log and light on/off schedule.
err = saveSchedule(fsTable,
LS_LEN*LS_MAX,
LS_LEN*LS_MAX+FS_LEN*FS_MAX,
fsLen,
5,
ERR_FSEEPROMOOR,
ERR_FSENTINVFMT);
return err;
}
// -----------------------------------------------------------------------------
// Convert lsHoursTbl and LsMinutesTbl to lsTable.
// -----------------------------------------------------------------------------
void convLsTblToStringTbl()
{
char txtbuf[6];
for (int i = 0; i < lsLen; i++)
{
sprintf(txtbuf, "%02d:%02d\0", lsSched[i].hour, lsSched[i].minute);
lsTable[i] = String(txtbuf);
#ifdef MYDEBUG0
Serial.print("DBG: lsSched[");
Serial.print(i);
Serial.print("].hour=");
Serial.println(lsSched[i].hour);
Serial.print("DBG: lsSched[");
Serial.print(i);
Serial.print("].minute=");
Serial.println(lsSched[i].minute);
Serial.print("DBG: txtbuf=\"");
Serial.print(txtbuf);
Serial.println("\"");
Serial.print("DBG: lsTable[");
Serial.print(i);
Serial.print("]=\"");
Serial.print(lsTable[i]);
Serial.println("\"");
#endif
}
}
// -----------------------------------------------------------------------------
// Convert fsHoursTbl and fsMinutesTbl to fsTable.
// -----------------------------------------------------------------------------
void convFsTblToStringTbl()
{
char txtbuf[6];
for (int i = 0; i < fsLen; i++)
{
sprintf(txtbuf, "%02d:%02d\0", fsSched[i].hour, fsSched[i].minute);
fsTable[i] = String(txtbuf);
#ifdef MYDEBUG3
Serial.print("DBG: fsSched[");
Serial.print(i);
Serial.print("].hour=");
Serial.println(fsSched[i].hour);
Serial.print("DBG: fsSched[");
Serial.print(i);
Serial.print("].minute=");
Serial.println(fsSched[i].minute);
Serial.print("DBG: txtbuf=\"");
Serial.print(txtbuf);
Serial.println("\"");
Serial.print("DBG: fsTable[");
Serial.print(i);
Serial.print("]=\"");
Serial.print(fsTable[i]);
Serial.println("\"");
#endif
}
}
// -----------------------------------------------------------------------------
// Sort numeric light on/off schedule tables.
// -----------------------------------------------------------------------------
void sortLsTables()
{
int tmp;
int v1, v2;
for (int i = 0; i < lsLen - 1; i++)
{
for (int j = i + 1; j < lsLen; j++)
{
v1 = lsSched[j].hour*100 + lsSched[j].minute;
v2 = lsSched[i].hour*100 + lsSched[i].minute;
if (v1 < v2)
{
tmp = lsSched[i].hour;
lsSched[i].hour = lsSched[j].hour;
lsSched[j].hour = tmp;
tmp = lsSched[i].minute;
lsSched[i].minute = lsSched[j].minute;
lsSched[j].minute = tmp;
}
}
}
}
// -----------------------------------------------------------------------------
// Sort numeric feeding schedule tables.
// -----------------------------------------------------------------------------
void sortFsTables()
{
int tmp;
int v1, v2;
for (int i = 0; i < fsLen - 1; i++)
{
for (int j = i + 1; j < fsLen; j++)
{
v1 = fsSched[j].hour*100 + fsSched[j].minute;
v2 = fsSched[i].hour*100 + fsSched[i].minute;
if (v1 < v2)
{
tmp = fsSched[i].hour;
fsSched[i].hour = fsSched[j].hour;
fsSched[j].hour = tmp;
tmp = fsSched[i].minute;
fsSched[i].minute = fsSched[j].minute;
fsSched[j].minute = tmp;
}
}
}
}
// -----------------------------------------------------------------------------
// Convert hour part of light on/off schedule entry in String format to integer.
// -----------------------------------------------------------------------------
int getHourFromTimeEntry(String timestr)
{
String hr;
hr = timestr.substring(0, timestr.indexOf(':'));
return hr.toInt();
}
// -----------------------------------------------------------------------------
// Convert minute part of light on/off schedule entry in String format to
// integer.
// -----------------------------------------------------------------------------
int getMinuteFromTimeEntry(String timestr)
{
String mn;
mn = timestr.substring(timestr.indexOf(':')+1);
return mn.toInt();
}
// -----------------------------------------------------------------------------
// Load saved schedule from EEPROM to lsTable or fsTable as pointed by lsfs
// argument.
// -----------------------------------------------------------------------------
int loadSavedSchedule(int startaddr,
int endaddr,
int arglen,
int maxitems,
boolean lsfs // true - ls, false - fs
)
{
int n = 0, l = 0, addr = 0;
char val, prev = 0;
int ret = -1;
boolean noerr = true;
addr = startaddr;
while (addr < endaddr && n < maxitems)
{
val = EEPROM.read(addr++);
if (0 != val)
{
if (arglen > l)
{
if (lsfs)
lsTable[n] += val;
else
fsTable[n] += val;
l++;
}
else
{
// error, each entry should be only <arglen> characters long
noerr = false;
break;
}
}
else
{
if (prev == 0)
break; // two zeroes in a row - the end of setup in EEPROM
n++;
l = 0;
}
prev = val;
}
if (noerr)
ret = n;
return ret;
}
// -----------------------------------------------------------------------------
// Load light on/off schedule from EEPROM to lsTable.
// -----------------------------------------------------------------------------
int loadSavedLS2Table()
{
int ret = -1;
// light on/off schedule is saved after temperature log
ret = loadSavedSchedule(0,
LS_LEN*LS_MAX,
5,
LS_MAX,
true);
return ret;
}
// -----------------------------------------------------------------------------
// Load feeding schedule from EEPROM to lsTable.
// -----------------------------------------------------------------------------
int loadSavedFS2Table()
{
int ret = -1;
// feeding schedule is saved after temperature log and light on/off schedule.
ret = loadSavedSchedule(LS_LEN*LS_MAX,
LS_LEN*LS_MAX+FS_LEN*FS_MAX,
5,
FS_MAX,
false);
return ret;
}
// -----------------------------------------------------------------------------
// Parse/interpret setdt command, set RTC date/time.
// setdt YYYY MM DD hh mm
// -----------------------------------------------------------------------------
int serialSetDateTime(String command)
{
int err = ERR_OK;
char numBuf[5];
int setYear = 2013;
int setMonth = 12;
int setDay = 7;
int setHour = 0;
int setMinute = 0;
boolean token = false;
int argnum = 0, n = 0;
timeNow = RTC.now();
setYear = timeNow.year();
setMonth = timeNow.month();
setDay = timeNow.day();
setHour = timeNow.hour();
setMinute = timeNow.minute();
for (n = 0; n < 5; n++)
numBuf[n] = 0;
strcpy_P(progmembuf, (char*)pgm_read_word(&(cmdTable[CMD_SETDT])));
for (int i = strlen(progmembuf), n = 0; i <= command.length() && ERR_OK == err; i++)
{
if (token && (command.charAt(i) == ' ' || i == command.length()))
{
argnum++;
if (n < 5)
numBuf[n] = 0;
else
{
err = ERR_ARGTOOLONG;
break;
}
switch (argnum)
{
case 1:
setYear = atoi(numBuf);
break;
case 2:
setMonth = atoi(numBuf);
break;
case 3:
setDay = atoi(numBuf);
break;
case 4:
setHour = atoi(numBuf);
break;
case 5:
setMinute = atoi(numBuf);
break;
default:
err = ERR_TOOMANYARGS;
break;
}
token = false;
for (n = 0; n < 5; n++)
numBuf[n] = 0;
n = 0;
}
else if (command.charAt(i) != ' ')
{
if (false == token)
token = true;
if (n < 4)
numBuf[n++] = command.charAt(i);
else
{
err = ERR_ARGTOOLONG;
break;
}
}
}
if (ERR_OK == err)
RTC.adjust(DateTime(setYear, setMonth, setDay, setHour, setMinute, 0));
return err;
}
// -----------------------------------------------------------------------------
// Interpret command string, return command code.
// Works best if all commands are CMDL-characters long.
// -----------------------------------------------------------------------------
int getCmdCode()
{
int ret = -1;
char buf[6] = {0,0,0,0,0,0};
cmd.toCharArray(buf, 5);
for (byte i = 0; i < CMD_NIL; i++)
{
memset(progmembuf,'\0',sizeof(progmembuf));
strcpy_P(progmembuf, (char*)pgm_read_word(&(cmdTable[i])));
if (0 == strncmp(progmembuf, buf, CMDL))
{
ret = i;
break;
}
}
return ret;
}
// -----------------------------------------------------------------------------
// Send date/time to serial line.
// -----------------------------------------------------------------------------
void serialWriteDTNow()
{
strcpy_P(progmembuf, (char*)pgm_read_word(&(daysOfWeek[timeNow.dayOfWeek()])));
Serial.print(progmembuf);
Serial.print(' ');
Serial.print(timeNow.year());
Serial.print('/');
if (timeNow.month() < 10)
Serial.print('0');
Serial.print(timeNow.month());
Serial.print('/');
if (timeNow.day() < 10)
Serial.print('0');
Serial.print(timeNow.day());
Serial.print(' ');
if (timeNow.hour() < 10)
Serial.print('0');
Serial.print(timeNow.hour());
Serial.print(':');
if (timeNow.minute() < 10)
Serial.print('0');
Serial.print(timeNow.minute());
Serial.println();
}
// -----------------------------------------------------------------------------
// Send date/time to lcd.
// -----------------------------------------------------------------------------
void lcdWriteDTNow()
{
lcdClearRow(DTROW);
strcpy_P(progmembuf, (char*)pgm_read_word(&(daysOfWeek[timeNow.dayOfWeek()])));
lcd.print(progmembuf);
lcd.print(' ');
lcd.print(timeNow.year());
lcd.print('/');
if (timeNow.month() < 10)
lcd.print('0');
lcd.print(timeNow.month());
lcd.print('/');
if (timeNow.day() < 10)
lcd.print('0');
lcd.print(timeNow.day());
lcd.print(' ');
if (timeNow.hour() < 10)
lcd.print('0');
lcd.print(timeNow.hour());
lcd.print(':');
if (timeNow.minute() < 10)
lcd.print('0');
lcd.print(timeNow.minute());
}
// -----------------------------------------------------------------------------
// Send temperature readings in human readable form to serial line.
// -----------------------------------------------------------------------------
void serialConvWriteTemp()
{
Serial.print("T = ");
dtostrf(celsius, 8, 2, textbuf);
Serial.print(textbuf);
Serial.print(" C, ");
dtostrf(fahrenheit, 8, 2, textbuf);
Serial.print(textbuf);
Serial.println(" F");
}
// -----------------------------------------------------------------------------
// Display temperature readings in human readable form on a LCD.
// -----------------------------------------------------------------------------
void lcdConvWriteTemp()
{
lcdClearRow(TEMPROW);
dtostrf(celsius, 8, 2, textbuf);
lcd.print('T');
lcd.print(textbuf);
lcd.print("C ");
dtostrf(fahrenheit, 8, 2, textbuf);
lcd.print(textbuf);
lcd.print("F");
}
// -----------------------------------------------------------------------------
// DS temperature sensor chip type determination.
// -----------------------------------------------------------------------------
byte determineDSChip()
{
byte ret = 2;
// the first ROM byte indicates which chip
switch (addr[0]) {
case 0x10:
// Chip = DS18S20 or old DS1820
ret = 1;
break;
case 0x28:
// Chip = DS18B20
ret = 0;
break;
case 0x22:
// Chip = DS1822
ret = 0;
break;
default:
// Device is not a DS18x20 family device.
break;
}
return ret;
}
// -----------------------------------------------------------------------------
// Turn the light on.
// -----------------------------------------------------------------------------
void lightOn()
{
digitalWrite(LIGHTPIN, HIGH);
}
// -----------------------------------------------------------------------------
// Turn the light off.
// -----------------------------------------------------------------------------
void lightOff()
{
digitalWrite(LIGHTPIN, LOW);
}
// -----------------------------------------------------------------------------
// Run food dispenser. Perform fool 360 deg. revolution and some (1/8)
// of the stepper motor.
// During this time, the serial console to the controller will be unresponsive.
// -----------------------------------------------------------------------------
void dispenseFood()
{
Steps2Take = STEPS_PER_OUTPUT_REVOLUTION + STEPS_PER_OUTPUT_REVOLUTION/8;
small_stepper.setSpeed(250);
small_stepper.step(Steps2Take);
}
// -----------------------------------------------------------------------------
/*
* SerialEvent occurs whenever a new data comes in the
* hardware serial RX. This routine is run between each
* time loop() runs, so using delay inside loop can delay
* response. Multiple bytes of data may be available.
* Here it is used to read user input via serial port/USB.
*/
// -----------------------------------------------------------------------------
void serialEvent()
{
while (Serial.available()) {
// get the new byte:
char inChar = (char)Serial.read();
// send echo
Serial.write(inChar);
// add it to the command string
if (inChar != '\r')
cmdTmp += inChar;
// if the incoming character is a newline, set a flag
// so the main loop can do something about it:
// if (inChar == '\r') {
else {
cmd = cmdTmp;
cmdTmp = "";
Serial.println();
cmdReady = true;
}
}
}
Note that I used software I2C for the LCD panel to avoid any potential issues/conflicts between RTC and LCD modules if ran on the same I2C bus. The bit-banged i2c works quite well, however it is a little slower than the one provided by Arduino. It can however be used with long wires, which may allow some flexibility in mounting LCD module away from the main unit, if needed. The software controlled bus does not require pull-up resistors with the short connection, but I did not test it with the long wires.
Because I used the sotware i2c, I had to make a copy of original LiquidCrystal_I2C library, renamed LCD_I2C_BB, which uses SoftI2CMaster library code for i2c communication.
The archive of the entire project with updated code and documentation can be downloaded here: Fish Tank Automation 2
NOTE 9/9/2014: File fishtankautom2.zip contains original documentation and source code of version #2. I recommend downloading fishtankautom3.zip for updated code version #3 (quoted in this article), with bug fixes and temperature log (eeprom) code removed. The documentation is only in fishtankautom2.zip archive for now, but it is mostly accurate. Pictures below were made while running version #2 of the firmware, however there are only cosmetic differences between versions #2 and #3 as far as UI is concerned.
Pictures of the prototype:
Thank you for your interest.
Marek Karcz
9/7/2014