Wednesday, January 1, 2014

Fish Tank Automation with Arduino Uno.

I have a 10 gallon fish tank. I always face the same dillema when I travel for longer than 2 days - who will feed my fish? Who will turn the light on and off? I had a mechanical light timer lying around and I bought battery powered Automatic Fish Feeder by Aqua Culture in Walmart. It was simple and relatively reliable. But something was missing. I thought about it for a while (few seconds) and I knew - this was an ideal micro controller project!

OK, so I already had an Arduino Uno in my parts stash. To control light, I needed a relay, which I also already had - a Radio Shack's SPDT Micromini 5VDC Relay, rated 1A at 120AC/24VDC. However to control the feeding drum, I needed a precise electric motor or a servo. After some consideration I decided on a stepper motor. Quick research on the internet and then on e-bay resulted in me buying a cheap stepper motor with a controller (actually it was a 2-pack) with specs that fit the requirements for my project - 2pcs DC 5V Stepper Motor + ULN2003 Driver Test Module Board from a seller called "goldpart".
Next thing I needed was a proper time keeping facility so I would be able to create a precise schedule of feeding and tank's light on/off switching.
I already had a DS1307 based real-time clock I2C module, which I used previously with my Arduino Clock project, ideal for this purpose.
Since I have some one-wire temperature sensors lying around as well (DS18B20), as a bonus I decided to add water temperature logging to my project.

There is enough information on the internet regarding stepper motor and ULN2003 driver for anyone who wants to research the stepper motor control in detail. For the stepper motor control I used library written by someone else. The library and how to code to control the particular stepper motor I bought can be found here: http://arduino-info.wikispaces.com/SmallSteppers
How to use the one-wire temp. sensor is also well documented: http://www.pjrc.com/teensy/td_libs_OneWire.html


Here is the hand-drawn schematic of my circuit:


I started with the prototype of an electric circuit so I could write and test the code. The electric circuit was an easy part since it just required a small breadboard, some wires, power supply and LED to emulate the fish tank's light. Incorporating the stepper motor into the Automatic Fish Feeder was a bit more challenging. I achieved it by removing the battery powered clock mechanism from it, cutting a square portion of the case on the back away so the motor would be fitted inside, putting the stepper motor inside (and fitting its axle into the socket/hole in the center of the feeding drum) and then reinforcing the case with a few pieces of plastic and metal and screws (due to the part that was cut away, the case would not hold together). The result is presented on the pictures at the end of the article. It is not pretty, but it works. I am kind of proud of it since I am not very talented as far as handcrafting of anything is involved.

The sketch.

It would be cool to be able to control this contraption via internet, but then it would require some more hardware and code. Therefore I decided to keep it simple as far as user interface goes. I assumed the device should be controlled via USB/serial port with command line interface. All the feeding and light on/off scheduling can be setup by opening a terminal connection to the arduino and issuing commands with proper arguments. There are commands for setting up and retrieving time, setting up and displaying feeding and light on/off schedules etc. Here is the code:

/*
 * Fish tank automation.
 * Created by Marek Karcz 2013. 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).
 *
 * 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.
 * The temperature will be logged in Arduino's EEPROM.
 * Device will be programmed via USB/serial port using command line interface.
 *
 * 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.
 */

#include <avr/pgmspace.h>
#include <EEPROM.h> 
#include <Stepper.h>
#include <OneWire.h>
#include <Wire.h>
#include <RTClib.h>

//#define MYDEBUG0
//#define MYDEBUG1
//#define MYDEBUG2
//#define MYDEBUG3

const char *VERSION  = "Fish Tank Automation v.1.0.";

// -----------------------------------------------------------------------------
// Pin definitions.
// -----------------------------------------------------------------------------
#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_LTEMP,
  CMD_ADDFT,
  CMD_SHOWFS,
  CMD_DELFT,
  CMD_ADDLS,
  CMD_SHOWLS,
  CMD_DELLS,
  CMD_SETDT,
  CMD_HELP,
  CMD_VER,
  CMD_DEFFS,
  CMD_DEFLS,
  CMD_NIL
};

PROGMEM prog_char Date[]   =   "date";
PROGMEM prog_char Temp[]   =   "temp";
PROGMEM prog_char Ltemp[]  =   "ltemp";
PROGMEM prog_char Addft[]  =   "addft";
PROGMEM prog_char Showfs[] =   "showfs";
PROGMEM prog_char Delft[]  =   "delft";
PROGMEM prog_char Addls[]  =   "addls";
PROGMEM prog_char Showls[] =   "showls";
PROGMEM prog_char Dells[]  =   "dells";
PROGMEM prog_char Setdt[]  =   "setdt";
PROGMEM prog_char Help[]   =   "help";
PROGMEM prog_char Ver[]    =   "ver";
PROGMEM prog_char Deffs[]  =   "deffs";
PROGMEM prog_char Defls[]  =   "defls";
PROGMEM prog_char Nil[]    =   "nil";

PROGMEM const char *cmdTable[] = {
  Date,          // display date/time
  Temp,          // display last temperature read
  Ltemp,         // display saved temperature log
  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
  Ver,           // show firmware version
  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)
  Nil            // do not remove, must be at the end
};

// 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   =   "- disp. D/T";
prog_char hlpstr_1[]   PROGMEM   =   "- disp. temp.";
prog_char hlpstr_2[]   PROGMEM   =   "- show temp. log";
prog_char hlpstr_3[]   PROGMEM   =   "hh:mm [hh:mm ...] - add feed times";
prog_char hlpstr_4[]   PROGMEM   =   "- show 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   =   "- show 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   =   "- this help screen";
prog_char hlpstr_11[]  PROGMEM   =   "- show firmware version";
prog_char hlpstr_12[]  PROGMEM   =   "- set deflt feed sch.";
prog_char hlpstr_13[]  PROGMEM   =   "- set deflt light sch.";
prog_char hlpstr_14[]  PROGMEM   =   "nil";

PROGMEM const char *cmdHelp[] = {
  hlpstr_0,
  hlpstr_1,
  hlpstr_2,
  hlpstr_3,
  hlpstr_4,
  hlpstr_5,
  hlpstr_6,
  hlpstr_7,
  hlpstr_8,
  hlpstr_9,
  hlpstr_10,
  hlpstr_11,
  hlpstr_12,
  hlpstr_13,
  hlpstr_14  
};

// this buffer must accomodate the longest string of hlpstr_N plus
// terminating NULL.
char progmembuf[36];

const char *PROMPT = "CMD> ";

// -----------------------------------------------------------------------------

// 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);

// -----------------------------------------------------------------------------
// 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;

// temperature log
//    note: temp. will be logged to EEPROM in 40 character long entries
//          last 10 readings will be kept in log in following format:
//          HH:MM SCCCCCC.CC SFFFFFF.FF
//          where:
//          HH - hour
//          MM - minute
//          S - sign (- or none)
//          C - digits of Celsius temperature value
//          F - digits of Fahrenheit temperature value
// char templogbuf[40];  // temperature log buffer
int  logAddr = 0;     // current temperature log address
int  logEntries = 0;  // total number of log entries in temp. log
int  logEntry = 0;    // recent log position # in temp. log (round-robin)

// 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  20      // how often to read temperature (minutes)
#define TEMP_LOG_LEN   25      // how long one temp. log entry
#define TEMP_LOG_MAX   12      // how many temperature log entries

#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
// -----------------------------------------------------------------------------
// Initialization sequence.
// -----------------------------------------------------------------------------
void setup()
{
  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();
  initTempLog();
  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]);
  }    
  Serial.println(VERSION);
  Serial.println("System online.");
  Serial.print(PROMPT);
}

// -----------------------------------------------------------------------------
// Main control loop.
// -----------------------------------------------------------------------------
void loop()
{
  // Read time.
  timeNow = RTC.now();
  
  // control light via scheduler
  controlLight();
  
  // control food dispenser
  controlFoodDisp();
  
  // Read temperature.
  readTemp();
  
  // command interpreter
  interpretCommand();

  delay(100);
}

// -----------------------------------------------------------------------------
// Interpret command.
// -----------------------------------------------------------------------------
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_LTEMP:
        serialTempLog();
        break;
        
      case CMD_ADDFT:
        err = serialAddFeedingSchedule(memCmd);
        break;
        
      case CMD_SHOWFS:
        serialShowFeedingSchedule();
        break;
        
      case CMD_DELFT:
        serialDeleteFeedingSchedule();
        break;
        
      case CMD_ADDLS:
        err = serialAddLightSchedule(memCmd);
        break;
        
      case CMD_SHOWLS:
        serialShowLightSchedule();
        break;
        
      case CMD_DELLS:
        serialDeleteLightSchedule();
        break;
        
      case CMD_SETDT:
        err = serialSetDateTime(memCmd);
        break;
        
      case CMD_HELP:
        serialHelp();
        break;      
        
      case CMD_VER:
        Serial.println(VERSION);
        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()
{
  boolean doloop = true;
  
  for (int i = 0; doloop; i++)
  {
    strcpy_P(progmembuf, (char*)pgm_read_word(&(cmdTable[i])));
    if (strcmp(progmembuf, "nil"))
    {
      Serial.print(progmembuf);
      Serial.print(' ');
      strcpy_P(progmembuf, (char*)pgm_read_word(&(cmdHelp[i])));
      Serial.println(progmembuf);
    }
    else
      doloop = false;
  }
}

// -----------------------------------------------------------------------------
// 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. Save log.
// -----------------------------------------------------------------------------
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;
    logTemp();
  }  
}

// -----------------------------------------------------------------------------
// Control light per light on/off scheduler setup.
// Scheduler table contains accelerated ON/OFF times.
// -----------------------------------------------------------------------------
void controlLight()
{
  boolean turnon = true;
  
  if (lsLen <= 0)
    lightOff();  // 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].minute > 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, tuen it ON, flip the flag
      lightOn();
      lightswitch = true;
    }
  }
  else
  {
    if (lightswitch)
    {
      // if the light is ON, turn it OFF, flip the flag
      lightOff();
      lightswitch = false;
    }
  }  
}

// -----------------------------------------------------------------------------
// 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)
      {
        dispenseFood();
        foodDispTime = timeNow.unixtime();
      }
    }
  }
}

// -----------------------------------------------------------------------------
// 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("WARN: Missing OFF time.");
}

// -----------------------------------------------------------------------------
// 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]);
  }
}

// -----------------------------------------------------------------------------
// 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 = TEMP_LOG_LEN * TEMP_LOG_MAX;
  
  while (addr < TEMP_LOG_LEN * TEMP_LOG_MAX + LS_LEN * LS_MAX)
    EEPROM.write(addr++, 0);
    
  lsLen = -1;
  Serial.println("OK");
}

// -----------------------------------------------------------------------------
// Delete entire feeding schedule.
// -----------------------------------------------------------------------------
void serialDeleteFeedingSchedule()
{
  int addr = TEMP_LOG_LEN * TEMP_LOG_MAX + LS_LEN * LS_MAX;
  
  while (addr < TEMP_LOG_LEN * TEMP_LOG_MAX + LS_LEN * LS_MAX + FS_LEN * FS_MAX)
    EEPROM.write(addr++, 0);
    
  fsLen = -1;
  Serial.println("OK");
}

// -----------------------------------------------------------------------------
// 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, 
                     TEMP_LOG_LEN*TEMP_LOG_MAX, 
                     TEMP_LOG_LEN*TEMP_LOG_MAX+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, 
                     TEMP_LOG_LEN*TEMP_LOG_MAX+LS_LEN*LS_MAX, 
                     TEMP_LOG_LEN*TEMP_LOG_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(TEMP_LOG_LEN*TEMP_LOG_MAX,
                          TEMP_LOG_LEN*TEMP_LOG_MAX+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(TEMP_LOG_LEN*TEMP_LOG_MAX+LS_LEN*LS_MAX,
                          TEMP_LOG_LEN*TEMP_LOG_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));
    
    Serial.println("Set D/T to:");
    Serial.print(setYear); Serial.print('/');
    Serial.print(setMonth); Serial.print('/');
    Serial.print(setDay); Serial.print(' ');
    Serial.print(setHour); Serial.print(':');
    if(setMinute < 10)
      Serial.print('0');
    Serial.println(setMinute);    
  }
  
  return err;
}

// -----------------------------------------------------------------------------
// Interpret command string, return command code.
// -----------------------------------------------------------------------------
int getCmdCode()
{
  int ret = -1;
  boolean doloop = true;
  char buf[8] = {0,0,0,0,0,0,0,0};

  cmd.toCharArray(buf, 8);
  for (int i = 0; doloop; i++)
  {
    strcpy_P(progmembuf, (char*)pgm_read_word(&(cmdTable[i])));
    if (strcmp(progmembuf, "nil"))
    {
      if (0 == strcmp(progmembuf, buf) || 0 == strncmp(progmembuf, buf, strlen(progmembuf)))
      {
        ret = i;
        break;
      }
    }
    else
      doloop = false;
  }
  
  return ret;
}

// -----------------------------------------------------------------------------
// Send date/time to serial line.
// -----------------------------------------------------------------------------
void serialWriteDTNow()
{
  Serial.print("DT:");
  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 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");          
}

// -----------------------------------------------------------------------------
// 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;
}

// -----------------------------------------------------------------------------
// Initialize EEPROM for temperature log.
// -----------------------------------------------------------------------------
void initTempLog()
{
  for (int i = 0; i < TEMP_LOG_LEN * TEMP_LOG_MAX; i++)
    EEPROM.write(i, 0);
}

// -----------------------------------------------------------------------------
// Log current time and temperature read in EEPROM.
// -----------------------------------------------------------------------------
void logTemp()
{
  int i = 0;
  
  progmembuf[i++] = (char) ((int)(timeNow.hour() / 10) + 48);
  progmembuf[i++] = (char) ((int)timeNow.hour() - (int)(timeNow.hour() / 10)*10 + 48);
  progmembuf[i++] = ':';
  progmembuf[i++] = (char) ((int)(timeNow.minute() / 10) + 48);
  progmembuf[i++] = (char) ((int)timeNow.minute() - (int)(timeNow.minute() / 10)*10 + 48);  
  progmembuf[i++] = ' ';
  dtostrf(celsius, 8, 2, textbuf);
  for (int n = 0; n < strlen(textbuf) && i < TEMP_LOG_LEN-1; i++, n++)
    progmembuf[i] = textbuf[n];
  progmembuf[i++] = ' ';    
  dtostrf(fahrenheit, 8, 2, textbuf);
  for (int n = 0; n < strlen(textbuf) && i < TEMP_LOG_LEN-1; i++, n++)
    progmembuf[i] = textbuf[n];
  for (; i < TEMP_LOG_LEN-1; i++)
    progmembuf[i] = ' ';
  progmembuf[TEMP_LOG_LEN-1] = 0;
  if (logEntry == TEMP_LOG_MAX)
  {
    // reset the log address and entries (start on top again)
    logAddr = 0;
    logEntry = 0;
  }
  for (i = 0; i < TEMP_LOG_LEN; i++)
    EEPROM.write(logAddr++, progmembuf[i]);
  if (logEntries < TEMP_LOG_MAX)
    logEntries++;
  logEntry++;
}

// -----------------------------------------------------------------------------
// Send the contents of temperature log to serial line.
// -----------------------------------------------------------------------------
void serialTempLog()
{
  int addr = 0;
  for (int i = 0; i < logEntries && addr < 512; i++)
  {
    char val = EEPROM.read(addr++);
    
    while (0 != val)
    {
      Serial.print(val);
      val = EEPROM.read(addr++);
    }
    Serial.println();
  }
}

// -----------------------------------------------------------------------------
// 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.
 */
// ----------------------------------------------------------------------------- 
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;
    } 
  }
}

I consider the code to be finished, so now awaits the most difficult and boring part for me - mounting this in an aesthetic and safe way in my fish tank. I also think that the device should be powered via a battery backup unit (the Arduino Uno and food dispensing unit) so there are no surprises due to prolonged power outages. I also do not have any ideas yet how to make the temperature sensor waterproof.

And now the pictures:

Image 1 - The food dispensing unit with the stepper motor that replaced the factory clock mechanism.
Image 2 - The food dispensing unit, front view.
Image 3 - The circuit prototype.

Image 4 - Working prototype.

Image 5 - Command line/terminal session to the device.

This is it. Thank you for reading my blog.

Marek Karcz
1/1/2014