Introduction: Automatic Arduino Powered Pet Feeder
This is my automatic pet feeder. Powered by arduino, using a 3D printed auger, and programmable with two feeding times with a user set quantity of food, with a battery backed up internal clock.
My cats drive me nuts in the morning begging for food so I decided it was time to robotize feeding time
I got frustrated with pet feeders available in the UK. They are either the type that rotate and unveil a meal, up to four times only, or the only half decent machine with a hopper is horrific to program, forgets everything if you remove the batteries, and frequently fails to even deliver any food! I program here and there, and the last machine I used made me so mad just trying to set the feeding times I nearly threw it out the window.
America has some good machines but they start at $300 USD, and shipping would be painful to the UK, so I decided to make one!
Step 1: Materials Needed
Electronics:
1 x normal RC servo - regular size
1 x continuous RC servo - off the shelf or hacked.
1 x 3D printed Auger parts (see auger step) (pennies if you have a 3D printer)
1 x 1 1/2" PVC T-Piece pipe (ebay £2.89) http://www.ebay.co.uk/itm/121534086943?_trksid=p20...
1 x Arduino (I used an Uno but any type will do)
OR ... for PCB (1 x ATMega328p, crystal, 2 caps, 5v voltage reg) - google "arduino breadboard"
1 x KY-040 Rotary encoder on board (ebay £3.85) http://www.ebay.co.uk/itm/181595206550?_trksid=p20...
1 x 16x2 LCD HD44780 compatible (ebay £2.53) http://www.ebay.co.uk/itm/281211020326?_trksid=p20...
1 x small push button switch (non-latching)
1 x food hopper - I used one for storing food with a firmly sealable lid - to stop feline criminal activity.
1 x mounting box - I built one from MDF
1 x 12v power supply - around 500Ma+ to be safe - driving the servos. You may need more.
Step 2: The Basics - Making the Auger (food Feed Mechanism)
The 1st hurdle is to make an auger (screw type feeder). Previously this was too much of a hurdle for me, but now with 3D printing, I just downloaded a design from thingiverse and printed it! If you like tinkering, build a 3D printer! - It's fun to make and amazingly useful to own. I built a Prusa i3 for around £350.
(http://www.thingiverse.com/thing:27854) - Credit to Kitlaan for his initial design which my machine is heavily based on.
His physical design is good, but the code is too simplistic for my needs, there is no display, no multiple feedings, and no user variable portion sizes. This is where the Arduino comes in and saves the day. I've previous experience with Arduino, but have never used an LCD or a rotary encoder - but it's no problem - the Arduino tutorials and examples are amazingly well written. I do have experience with battery backed up real time clocks (RTC) so adding one was a no-brainer.
The auger print comes in a few files - one for the auger, one for the servo mount/tube mount, and a servo spacer, which I didn't need. I printed the auger in one go (the author provides a split version too so you can print smaller objects and glue them together) - I have a Prusa i3 which had no issues with this. The auger is 10cm tall, the i3 prints up to 20cm tall. I printed with no support material and it came out fine. I had to trim the base with a knife a bit where the recess for the servo horn is, as it has collapsed a fraction due to the lack of support material. You can tell slic3r to print support material for only the 1st "x" rows, but I didn't think to do that.
I used a Futaba digital servo I had to hand, any servo will do, it doesn't need to be digital. You need it to be continuous ( no end stops) - these can be bought from places like sparkfun, but I just opened the servo , removed the plastic bump on one of the cogs that stops it rotating infinitely, and removed the potentiometer. Voila! - a continuous servo. Just search the web - there are loads of how-to's regarding changing regular servos to continuous servos.
I screwed the servo to the auger base, making sure it was level and rotated without wobble, and then test-fitted it in a piece of 1 1/2 inch plastic T-piece tubing (commonly used for hot tub plumbing) - cost £2.90 off ebay (PVC solvent weld pipe - ponds / hot tubs etc). Once I was happy it would rotate without binding I fixed it in place with hot glue where it inserts into the pipe.
Now I have an auger! (ho ho ho) :-)
Step 3: Making the Box
The box/base/enclosure is a simple MDF box (9mm). I first made a box, then cut out the base with a jigsaw, the holes for the auger with a hole cutting saw you attach to your drill, and the square holes for the control panel and servo with a jigsaw again.
I didn't want the hassle of trying to cut neat enough holes (square holes!) to mount the LCD so istead I 3D printed a home designed facia - this takes the LCD, push button (for manual feed) and the rotary encoder knob. You can then screw this to the wooden box and it looks nice and neat!
The food agitator is again 3D printed and screws directly to a servo horn. The round spout is also 3D printed (credit to Kitlaan again on Thingiverse http://www.thingiverse.com/thing:28483 ) I could have made the auger tube stick out the front of the box more negating the need for the spout but I wanted the top hole a littel further back from the edge so if I decide to replace the current food hopper with a larger one, the hole won't be right at the edge.
The food hopper screws onto the box and is easily replaceable / upgradable.
Time will tell if my cats learn to press the manual feed button!
Step 4: Arduino - the Hardware
This whole project is driven by an Arduino. I highly recommend you build a prototype with a regular arduino and a breadboard. It's the best way to understand what everything does and save mistake later. I've included the Fritzing file so you can load it up and see the breadboard layout in detail (cables are labelled in the program where nescessary). Fritzing software is free/donateware.
I built a breadboard with the LCD on first, then learnt the basics of how to control it. Then I added a rotary encoder and learnt how to use that, then I combined the two in code. Finally I added the real time clock , the servos, and started programming the final code.
You will notice I said "servos". The second servo is a regular servo, used to agitate the pet food before each feed, to stop it clumping (the pet food is known as kibbles to the USofA I believe :-) )
After I was happy with the arduino uno and breadboard test I replaced the Arduino uno with pure components - the ATMEGA328P chip, a few caps, 5v regulator (power supply) and a crystal - basically - an arduino without the circuit board. It's dead simple to knock up (google arduino breadboard) and is a good habit to get into - saves you a fortune on arduino uno's and means you can design one PCB to house everything. The picure of the breadboards with a rats nest of wiring is the "pure version".
This version tested ok so I then drew the circuit in PCB Wizard (a great package that Maplin sells for £40 ish) and converted it into a PCB drawing using the auto routing feature.I've included the file.
I print and etch my PCBs using the laser printer method. I've made an Instructable on it - have a look under my name - it works every time (the PCB, not my name). I've attached the PDFs for printing.
Finally I soldered all the components on and hooked up the LCD, rotary encoder, and manual feed button with ribbon cable.
You don't need to go that far - you can run your feeder with a regular Arduino, and a breadboard, or you could use that PCB board where you cut links with a knife - the choice is yours. The hardware is kind of academic - it's all in the code.
Step 5: Arduino - the Code
The code is not as intimidating as it first looks, and I've tried to annotate it as much as possible.
In summary it:
Reads the time from the RTC chip
Displays the time, and the default feed times and quantity
Checks to see if the rotary encoder has been pressed if it has: it then goes into the programming routine, cycling in order through clock, portion size, feed time 1, feed time 2. When you are done, it continues below....
Checks if it is feeding time 1 or 2 - if so it feeds.
Checks if you have pressed the manual feed button, if so it feeds. And then it starts all over again.
This is the first time I've used an interrupt service routine - for the rotary encoder. This means if you turn it, whatever the program is busy doing , it registers your action. So it's ALWAYS listening for the encoder.
I thought the routine for setting the clock/feed times & qty would be daunting, but I just broke it down into simple steps. 1st I learnt how to make the cursor flash on the screen, easy enough. Then I positioned the cursor on the hour digit of the clock, and told it to increase the hour as the encoder was turned (or decrease), then once the encoder was pressed, write the new "hour" to the real time clock, and then move the cursor to the minute, and start changing that, and so on. Was surprisingly easy.
The feeding routines are simple "if" routines - i.e. "if" the times matched, then do the feeding routine. Simple as that. I had to add a bit so it checks the seconds or else it feeds for a whole minute, as the program loops and checks the time again - and it is still feeding time! so it repeats... adding a check on the seconds stops this as it takes at least a second for the feeding cycle.
The program isn't perfect - sometimes turning the encoder doesn't always increment the number on screen. I think this is down to my inexperience. It doesn't however hinder the operation of the machine!
I've included a screen grab of my arduino IDE for no real reason other than I didn't want this section to be without a picture :-)
Obviously the video on the 1st page shows me pressing the manual feed button, but of course, it auto-feeds at the two set times (you could set both times the same and it would only feed once).
/* Automatic Auger Audiono pet feeder Copyright Roger Donoghue 28/03/2015 all rights reserved. For personal use only. Not for commercial use or resale. Allows you to set 2 feeding times and the quantity as a multiple of the default feed quantity. Uses a DS1307 real time clock to keep the time, with a rechargable battery built in. (You can use the arduino RTC example code in the IDE to set the clock , or use the rotary encoder as intended) */ // include the library code: #include <LiquidCrystal.h> #include <Wire.h> // needed for the RTC libraty #include <Time.h> #include <DS1307RTC.h> // Real Time Clock Library #include <Servo.h> // initialize the library with the numbers of the interface pins dor the LCD LiquidCrystal lcd(12, 11, 5, 8, 7, 6); #define PIN_SERVO 9 Servo feedServo; Servo stirServo; int pos = 0; volatile boolean TurnDetected; volatile boolean up; const int PinCLK=2; // Used for generating interrupts using CLK signal const int PinDT=3; // Used for reading DT signal const int PinSW=4; // Used for the push button switch of the Rotary Encoder const int buttonPin = A3; // the number of the pushbutton pin for manual feed 13 int buttonState = 0; // variable for reading the manual feed pushbutton status int feed1hour = 07; // variables for feeding times and quantity int feed1minute = 00; int feed2hour = 17; int feed2minute = 30; int feedQty = 4; int feedRate = 800; //a pwm rate the triggers forward on the servo 75 int feedReversal = 80; //a pwm rate that triggers reverse on the servo // play with these numbers for your servo. Mine is a Futaba digital servo // that I removed the pot from and the plastic lug, to make it continuous. void isr () { // Interrupt service routine is executed when a HIGH to LOW transition is detected on CLK if (digitalRead(PinCLK)) // this keeps an eye out for the rotary encoder being turned regardless of where the program is up = digitalRead(PinDT); // currently exectuting - in other words, during the main loop this ISR will always be active else up = !digitalRead(PinDT); TurnDetected = true; } void setup () { // set up the LCD's number of columns and rows: lcd.begin(16, 2); // setup the Rotary encoder pinMode(PinCLK,INPUT); pinMode(PinDT,INPUT); pinMode(PinSW,INPUT); pinMode(buttonPin, INPUT); attachInterrupt (0,isr,FALLING); // interrupt 0 is always connected to pin 2 on Arduino UNO lcd.setCursor(17,0); lcd.print("Roger Donoghue's"); // A bit of fun :-) lcd.setCursor(17,1); lcd.print(" Cat-O-Matic"); for (int positionCounter = 0; positionCounter < 17; positionCounter++) { // scroll one position left: lcd.scrollDisplayLeft(); // wait a bit: delay(150); } delay(3000); for (int positionCounter = 0; positionCounter < 17; positionCounter++) { // scroll one position left: lcd.scrollDisplayRight(); // wait a bit: delay(150); } // end of fun lcd.setCursor(17,0); lcd.print(" "); lcd.setCursor(17,1); lcd.print(" "); } void loop () { //Main program loop - most things in here! static long virtualPosition=0; // without STATIC it does not count correctly!!! tmElements_t tm; // This sectionm reads the time from the RTC, sets it in tmElements tm (nice to work with), then displays it. RTC.read(tm); lcd.setCursor(0, 0); printDigits(tm.Hour); //call to print digit function that adds leading zeros that may be missing lcd.print(":"); printDigits(tm.Minute); lcd.print(":"); printDigits(tm.Second); lcd.print(" "); lcd.print("Qty "); lcd.print(feedQty); lcd.print(" "); lcd.setCursor(0,1); lcd.print("1)"); printDigits(feed1hour); lcd.print(":"); printDigits(feed1minute); lcd.print(" 2)"); printDigits(feed2hour); lcd.print(":"); printDigits(feed2minute); // MAIN BREAKOUT "IF" SECION BELOW THAT MONITORS THE PUSH BUTTON AND ENTERS PROGRAMMING IF IT'S PUSHED if (!(digitalRead(PinSW))) { // check if pushbutton is pressed // if YES then enter the programming subroutine lcd.blink(); // Turn on the blinking cursor: lcd.setCursor(5,0); lcd.print(" SET"); virtualPosition = tm.Hour; //needed or the hour will be zero each time you change the clock. do { lcd.setCursor(0,0); // put cursor at Time Hour delay(500); // Delay needed or same button press will exit do-while as while is checking for another button push! if (TurnDetected) { // do this only if rotation was detected if (up) virtualPosition--; else virtualPosition++; TurnDetected = false; // do NOT repeat IF loop until new rotation detected } // Here I change the hour of time - tm.Hour = virtualPosition; RTC.write(tm); lcd.setCursor(0, 0); printDigits(tm.Hour); // then re-print the hour on the LCD } while ((digitalRead(PinSW))); // do this "do" loop while the PinSW button is NOT pressed lcd.noBlink(); delay(1000); // SET THE MINS lcd.blink(); // Turn on the blinking cursor: virtualPosition = tm.Minute; //needed or the minute will be zero each time you change the clock. do { lcd.setCursor(3,0); // put cursor at Time Mins delay(500); // Delay needed or same button press will exit do-while as while is checking for another button push! if (TurnDetected) { // do this only if rotation was detected if (up) virtualPosition--; else virtualPosition++; TurnDetected = false; // do NOT repeat IF loop until new rotation detected } // Here I change the min of time - tm.Minute = virtualPosition; RTC.write(tm); lcd.setCursor(3, 0); printDigits(tm.Minute); // then re-print the min on the LCD } while ((digitalRead(PinSW))); lcd.noBlink(); delay(1000); // SET THE QTY - Feed quantity lcd.blink(); // Turn on the blinking cursor: virtualPosition = feedQty; //needed or the qty will be zero. do { lcd.setCursor(14,0); // put cursor at QTY delay(500); // Delay needed or same button press will exit do-while as while is checking for another button push! if (TurnDetected) { // do this only if rotation was detected if (up) virtualPosition--; else virtualPosition++; TurnDetected = false; // do NOT repeat IF loop until new rotation detected } // Here I change the feed qty feedQty = virtualPosition; lcd.setCursor(14, 0); lcd.print(feedQty); } while ((digitalRead(PinSW))); lcd.noBlink(); delay(1000); // SET THE Feed1 Hour lcd.blink(); // Turn on the blinking cursor: virtualPosition = feed1hour; //needed or will be zero to start with. do { lcd.setCursor(2,1); // put cursor at feed1hour delay(500); // Delay needed or same button press will exit do-while as while is checking for another button push! if (TurnDetected) { // do this only if rotation was detected if (up) virtualPosition--; else virtualPosition++; TurnDetected = false; // do NOT repeat IF loop until new rotation detected } // Here I change the feed1 hour feed1hour = virtualPosition; lcd.setCursor(2,1); printDigits(feed1hour); } while ((digitalRead(PinSW))); lcd.noBlink(); delay(1000); // SET THE Feed1 Mins lcd.blink(); // Turn on the blinking cursor: virtualPosition = feed1minute; //needed or will be zero to start with. do { lcd.setCursor(5,1); // put cursor at feed1minute delay(500); // Delay needed or same button press will exit do-while as while is checking for another button push! if (TurnDetected) { // do this only if rotation was detected if (up) virtualPosition--; else virtualPosition++; TurnDetected = false; // do NOT repeat IF loop until new rotation detected } // Here I change the feed1 minute feed1minute = virtualPosition; lcd.setCursor(5,1); printDigits(feed1minute); } while ((digitalRead(PinSW))); lcd.noBlink(); delay(1000); // SET THE Feed2 Hour lcd.blink(); // Turn on the blinking cursor: virtualPosition = feed2hour; //needed or will be zero to start with. do { lcd.setCursor(10,1); // put cursor at feed1hour delay(500); // Delay needed or same button press will exit do-while as while is checking for another button push! if (TurnDetected) { // do this only if rotation was detected if (up) virtualPosition--; else virtualPosition++; TurnDetected = false; // do NOT repeat IF loop until new rotation detected } // Here I change the feed1 hour feed2hour = virtualPosition; lcd.setCursor(10,1); printDigits(feed2hour); } while ((digitalRead(PinSW))); lcd.noBlink(); delay(1000); // SET THE Feed2 Mins lcd.blink(); // Turn on the blinking cursor: virtualPosition = feed2minute; //needed or will be zero to start with. do { lcd.setCursor(13,1); // put cursor at feed1minute delay(500); // Delay needed or same button press will exit do-while as while is checking for another button push! if (TurnDetected) { // do this only if rotation was detected if (up) virtualPosition--; else virtualPosition++; TurnDetected = false; // do NOT repeat IF loop until new rotation detected } // Here I change the feed1 minute feed2minute = virtualPosition; lcd.setCursor(13,1); printDigits(feed2minute); } while ((digitalRead(PinSW))); lcd.noBlink(); delay(1000); } // end of main IF rotary encoder push button checker // CHECK FOR MANUAL FEED BUTTON buttonState = digitalRead(buttonPin); if (buttonState == HIGH) { feed(); } // CHECK FEEDING TIME AND FEED IF MATCHED if (tm.Hour == feed1hour && tm.Minute == feed1minute && tm.Second == 0) { // if I dont' check seconds are zero feed(); // then it'll feed continuously for 1 minute! } if (tm.Hour == feed2hour && tm.Minute == feed2minute && tm.Second == 0) { feed(); } } // End of main Loop void printDigits(int digits){ // utility function for digital clock display: prints leading 0 if(digits < 10) lcd.print('0'); lcd.print(digits); } void feed() { lcd.setCursor(17,0); lcd.print(" Meowwwww!"); for (int positionCounter = 0; positionCounter < 16; positionCounter++) { // scroll one position left: lcd.scrollDisplayLeft(); // wait a bit: delay(150); } // Stir servo section If you don't need a stir servo simply comment out all fo this until the Auger rotate section stirServo.attach(10); // I don't know if I need one either but I'm adding it now as it's easiest before I build it! for(pos = 0; pos <= 180; pos += 1) { stirServo.write(pos); delay(5); } for(pos = 180; pos>=0; pos-=1) { stirServo.write(pos); delay(10); } delay(200); for(pos = 0; pos <= 180; pos += 1) { stirServo.write(pos); delay(10); } for(pos = 180; pos>=0; pos-=1) { stirServo.write(pos); delay(5); } stirServo.detach(); // rotate the Auger feedServo.attach(PIN_SERVO); for (int cnt = 0; cnt < feedQty; cnt++) { feedServo.write(feedRate); //the feedrate is really the feed direction and rate. delay(600); //this delay sets how long the servo stays running from the previous command feedServo.write(feedReversal); //...until this command sets the servo a new task! delay(200); feedServo.write(feedRate); delay(600); feedServo.write(feedReversal); // if you want to increase the overall feedrate increase the forward delays (1000 at the moment) delay(200); // or better still just copy and past the forward & backwards code underneath to repeat } // that way the little reverse wiggle is always there to prevent jams feedServo.detach(); for (int positionCounter = 0; positionCounter < 16; positionCounter++) { // scroll one position left: lcd.scrollDisplayRight(); // wait a bit: delay(150); } }
Attachments
Step 6: Update: I've Replaced the Feed Servo With a NEMA 17 Style Stepper Motor
My feed servo started to refuse to go backwards recently. I'm not sure why, and I've never been keen on the PWM method of controlling it's direction. All seemed a bit like guess work, so, I replaced it with a Nema 17 style stepper motor, driven by an Easydriver stepper driver.
The changes are simple. Use a Nema 17 size stepper of your choice. I had one spare. Mine was a 6 wire - I idnetified which wires were the pair for coil "A" and which for coil "B" and ignored the remaining wires. With four wire steppers you won't need to worry about this!.
The wiring of the Easydriver (https://www.sparkfun.com/products/12779) goes like this:
Motor plus goes to your power source of 12v+
Motor Gnd goes to ground -
Motor "A" pins go to the motor A pair of wires of your stepper.
Motor "B" pins go to the motor B pair of wired of your stepper.
The "Step" pin goes to digital pin 13 on your arduino.
The "Dir" pin goes to analogue pin A0 on your arduino (although we are using this as a digital pin)
Finally, the "Enable" pin on the Easydriver goes to the arduino A1 pin. This is used to turn the Easydriver power on and off for power and heat saving..
I just soldered the wires directly onto my existing PCB so if you use my PCB design you can do the same. I wasn't going to redesign the PCB just for this change and it took less than 10 mins to add on the easydriver and it's connections. I just disconnected the old feed servo.
The code is not annoteded as well as I'd like as it was done in a hurry but the changes are obvious. I've used the accellstepper library to drive the stepper, and I've used the enable pin on the Easydriver to power down the motor coils when not in use, or it gets really warm being held in place for hours upon end. Using the enable pin to power it down makes good sense - it doesn't need to hold the auger still when not in use!