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