Saturday, November 8, 2014

Fish Tank Automation - finished.

Time for celebration has come as I finished my Fish Tank Automation project and the system is up and running now for almost a week with no problems so far. I updated the source code and documentation and all is available for download from my skydrive folder. Look for file fishtankautom_doc.zip, which contains documentation, pictures and source code. The Arduino sketch alone is also available under the same folder in archive fishtankautom6.zip, however the documentation archive contains all additional libraries needed to compile the code.

Pictures of the installed and running system:

Dry test run before installing the system on the fish tank.

Installed and running.

Food dispensing unit.

Control unit, temperature warning LED-s and Sel/Set buttons.

Temperature sensor is visible here just below the control unit - it is of course inside the fish tank.

My fish likes it :-)

The power housing unit is visible here to the left of the fish tank.
It is always good to see the project all the way through to the practical application. So many of my projects end up forgotten and unfinished. 
I used this one to teach myself some discipline.

Thank you for visiting my blog.

Marek Karcz 11/8/2014

Wednesday, September 17, 2014

Fish Tank Automation - casing.

I am done with prototyping and programming (am I? :-)), so it is time to move the circuit from prototype board to some housing such that it will have a practical application.
I decided to keep Arduino board with the supporting circuitry, LCD module, stepper motor control module, buttons, LED-s etc. in a separate case (which I call 'controller housing') from the 110V relay for obvious reasons - safety.
The other identical case will house the receptacle/110V socket for the aquarium's light and the relay (power housing). The power cord with the plug will go out of the case to the 110V socket and the receptacle mounted on the case will provide relay controlled/scheduled from Arduino power source for the fish tank light (see the pictures at the end of this article).
From the controller housing, two wires will go to the power housing that will provide current for the relay's coil. This way the electronics part will be completely isolated from the high-voltage part (unless I screw up insulation inside the power housing, hopefully not). Another 4-wire connector will go from the controller housing to the feeding drum (stepper motor). I still need to drill some more holes for Arduino's power/USB sockets, LED-s, perhaps I will add a reset button to the controller housing.
Considering the (poor) level of my manual skills, this looks quite OK, judge for yourself.







Thank you for your time.

Marek Karcz
9/18/2014

Sunday, September 7, 2014

Fish Tank Automation - upgrades.

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