We present a clock that synchs itself with the Network Time Protocol (NTP), feeds cats, and can be controlled wirelessly.
Features
This is the kind of fun that can happen when working with 7 to 9 year old children
The Cat Commander iot Clock and Kibble Dispenser has the following features:
- Timeless style that looks great with any décor
- Synchs automatically on startup for accurate time keeping using NTP
- Scrolling time and helpful messages displayed on RGB LED matrix
- Kibble reservoir stores enough food for two or more cats for several days
- Up to two feedings per day at user set times
- Adjustable feed amount
- Wireless User Interface works with smart phones, tablets, and computers
Design
The kids and I did an internet search for animal feeders and looked at a number of methods for metering the feed. We decided on the "Archimedes Screw" because a machine based on ancient Egyptian technology and looking this cool had to be the right answer. One of the designs we saw used a PVC tee as the body and we adopted that. I sketched everything out freehand and for no good reason made the storage cylindrical. It kind of looked like a rocket ship so a nose cone was drawn on top and the Cat Commander Kibble Dispenser was born. The detailed design and fabrication was done by me, but my grandson helped with assembly.
Mechanical Fabrication and Construction
The 3D printed parts were designed in Fusion 360 and printed on an Anycubic I3 Mega printer with PLA filament. The main body of the metering section is a 1 1/2 inch Sch. 40 PVC pipe tee from the local hardware store. The modeled parts are shown below and described in the sections that follow.
All parts can be printed without supports but the screw was split and printed in halves axially which were then glued together.
Kibble Storage
Parts: nose cone, body, and tail
Kibble (coarsely ground meal or grain typically used as animal feed per Merriam-Webster) is stored in the rocket ship shaped silo atop the metering section. The nose cone can be lifted off for refilling. The tail section has an internal funnel that fits snugly over the metering section and can be removed for cleaning. The tail section and body were glued together.
Metering and Delivery
Parts: PVC tee, metering throat, screw / auger, motor mount, 360 degree servo motor, forward mount, kibble slide, 2 mm nuts and bolts
The metering and delivery section is contained inside a 1 1/2 PVC tee and kibble is fed into the top of the tee. A 3D printed piece that is friction fit into the throat of the tee regulates and directs feed towards the back of the body of the tee. A screw (auger) in the body of the tee drives the kibble out of the front towards a slide. The device positively displaces the kibble so metering can be controlled by the number of rotations of the screw. The screw is driven by a 360 degree servo (MG-360) and a slide at the front of the tee directs the kibble towards a bowl.
Every so often the screw can eject kibble roughly as it exits due to catching on the face of the mount. This does not materially impact performance but could be improved with a change in geometry and tolerances.
Control Panel and Enclosure
Parts: control panel enclosure, control panel back, wire entry plug, 3 mm threaded brass insert and screw, cable management sleeve
The electronics are contained in a 3D printed enclosure with an integrated panel. The RGB LED matrix attaches to the panel with four 2 mm bolts, 6 mm long. The button switch with LED slips into a mount with fingers to hold it to the panel and are hot glued in place. The back to the enclosure was printed separately and presses into the enclosure with a single 3 mm screw to keep it firmly in place. A threaded brass insert heat pressed into the enclosure allows easy and repeated removal and reattachment of the back. Power for the electronics and control to the servo is through a hole in the enclosure back with a wire entry plug to keep things tidy. The USB cables were encased in a 1/4" cable management sleeve for tidiness.
Electronics
Parts: Arduino MKR 1010 microcontroller, Arduino MKR RGB shield, protoboard, dual USB wall wart and cables, momentary switch with LED, assorted bits and wire
Thanks to element14 and Tariq for providing the Arduino parts sometime back as a reward for participation in Project14. The electronics are straight forward and were prototyped on a breadboard before soldering up on protoboard configured as a breadboard which eased development and construction.
Unfortunately it was soldered "upside down" and as a result the display was upside down. This was not originally considered an issue as the Adafruit RGB libraries allow the display to be rotated in software. This turned out to not be the case for the Arduino library and due to time the protoboard was rotated and notched out so that it would fit the enclosure. To accomplish the fit of the USB connection it was necessary to move the RGB matrix further from the center than originally planned.
Because the motor draws a fair amount of current, especially when starting, it requires a separate power source and the motor used requires 5V or greater. Using what was at hand, a section from a surplus PCB with an SMD USB footprint was cut out and soldered to the protoboard (the green PCB section visible in the photo below) to provide power for the motor. The final assembly is tight and fiddly but neat.
Firmware
The firmware was developed using the Arduino IDE version 1.8.13 and developed on an Arduino MKR 1010. My contributions are entirely in the public domain. The major sections of the code rely on the following libraries:
- Display: Uses the Arduino_MKRRGB and ArduinoGraphics libraries for output
- Setting Time: Uses the WiFiUdp library to get time from an NTP server
- Real Time Clock: Uses RTCZero to display the time and trigger feeding
- Servo: Uses the Servo library to control the 360 degree servo motor
- User Interface: Sets up a server on the microcontroller using WiFiNINA
A line by line walkthrough of the code is not provided as it is rather lengthy but it is built off of examples in the libraries and I am happy to answer questions. Since the project has just finished and is not field tested there may be future changes. If so, it may be reposted or a link to a github repository made.
/* Cat Commander ntpTime_v8 * Tested on Arduino MKR 1010 with MKRRGB shield * * Gets time from NTP server and uses a servo to control cat food delivery. The User Interface * is over WiFi. The MKRRGB displays time and a countdown when close to feeding time. * * Servo Leads: Red = Power from separate source * Brown = Ground connected to Arduino * Yellow = Servo control, Arduino pin 5 * Button: Purple = Power from Arduino Vcc to button switch * Brown = Ground connected to Arduino * Blue = Button swich pulled down connected to Arduino pin 6 * White = LED on button switch connected thru 680R to Arduino pin 7 * * Modified sections of examples from the Arduino libraries were used in the development of this code. * The contributions of the authors are fully in the public domain. * * fmilburn Jan 2021 */ #include "arduino_secrets.h" #include <SPI.h> #include <WiFiNINA.h> #include <WiFiUdp.h> #include <RTCZero.h> #include <Servo.h> #include <ArduinoGraphics.h> // Arduino_MKRRGB needs ArduinoGraphics #include <Arduino_MKRRGB.h> #define DEBUG false const int SERVOPIN = 5; // Servo control pin const int SPEED = 30; // Servo speed const int BUTTONLED = 7; // LED inside button switch const int BUTTONPIN = 6; // Button switch pin, 10k pulldown volatile bool buttonPushed = false; bool displayStatus = true; char ssid[] = SECRET_SSID; // network SSID (name) char pass[] = SECRET_PASS; // network password int keyIndex = 0; // network key Index number (needed for WEP) int status = WL_IDLE_STATUS; const int GMT_OFFSET = -8; // local offset from GMT struct Feeding{ int h; // hr (24 hour) to feed cats int m; // minute of hour to feed cats }; const int FEEDINGTIMES = 2; struct Feeding feedingTime[FEEDINGTIMES] = { {8, 00}, // eg 08:00 {16, 00} // eg 16:00 }; int feedingTimeIndex = 0; volatile bool timeToFeed = false; // turns true when feed time occurs float feedRate = 2.0; // number of seconds the feed motor runs WiFiServer server(80); // server socket WiFiClient client = server.available(); RTCZero rtc; // Real Time Clock instance Servo myservo; // A servo instance to feed the cats // ----------------------------------------------------------------- // setup // ----------------------------------------------------------------- void setup() { if (DEBUG == true){ Serial.begin(115200); while (!Serial){ // wait for serial } Serial.println("Started..."); } initLedMatrix(); initGpio(); initWifi(); initRtc(); setFeedTime(); initServer(); // Interrupts attachInterrupt(digitalPinToInterrupt(BUTTONPIN), buttonPush, HIGH); rtc.attachInterrupt(rtcAlarm); } // ----------------------------------------------------------------- // loop // ----------------------------------------------------------------- void loop() { if (displayStatus == true){ displayTime(); } client = server.available(); if (client) { webPage(); } if (buttonPushed == true){ // button used to turn display on /off displayStatus = !displayStatus; buttonPushed = false; } if (timeToFeed == true){ feedCats(); // feed the cats } } // ----------------------------------------------------------------- // initialize the server // ----------------------------------------------------------------- void initServer(){ server.begin(); printWifiStatus(); } // ----------------------------------------------------------------- // start the web page // ----------------------------------------------------------------- void webPage() { if (client) { // check if there is a cllent String currentLine = ""; // string to hold incoming data from the client while (client.connected()) { // loop while the client's connected if (client.available()) { // check for byte to read from the client, char c = client.read(); // read a byte, then if (c == '\n') { // respond only when there is new line // if the current line is blank there were two newline characters in a row. // that's the end of the client HTTP request, so send a response: if (currentLine.length() == 0) { // standard response header client.println("HTTP/1.1 200 OK"); client.println("Content-Type: text/html"); client.println("Connection: close"); client.println(); // web page client.println("<!DOCTYPE html>"); client.println("<html>"); client.println("<head>"); client.println("<title>Cat Commander Kibble Dispenser</title>"); client.println("</head>"); client.println("<body>"); // Title client.println("<h1>Cat Commander Kibble Dispenser</h1>"); // feeding times client.println("<h2>"); client.print("<br><br>------------- Feed Times -------------<br>"); for (int i = 0; i < FEEDINGTIMES; i++){ client.print("Feeding Time "); client.print(i+1); client.print(": "); client.print(feedingTime[i].h); client.print(":"); if (feedingTime[i].m < 10){ client.print("0"); } client.print(feedingTime[i].m); client.print("<br>"); } // create the adjust time buttons client.print("<br>Click <a href=\"/UP1\">here</a> for later first feeding<br>"); client.print("Click <a href=\"/DOWN1\">here</a> for earlier first feeding<br>"); client.print("<br>Click <a href=\"/UP2\">here</a> for later second feeding<br>"); client.print("Click <a href=\"/DOWN2\">here</a> for earlier second feeding<br>"); // feed rate client.print("<br>-------------- Feed Rate --------------<br>"); client.print("Run motor for "); client.print(feedRate); client.print(" seconds<br><br>"); // create the change feed buttons client.print("Click <a href=\"/MORE\">here</a> to increase feed rate<br>"); client.print("Click <a href=\"/LESS\">here</a> to decrease feed rate<br><br>"); client.print("Click <a href=\"/FEED\">here</a> to feed now<br><br>"); client.println("<\h2>"); client.println(); client.println("</body>"); client.println("</html>"); // break out of the while loop: break; } else { // if you got a newline, then clear currentLine: currentLine = ""; } } else if (c != '\r') { // if you got anything else but a carriage return character, currentLine += c; // add it to the end of the currentLine } if (currentLine.endsWith("GET /MORE")) { if (feedRate < 10){ feedRate = feedRate + 0.5; } } if (currentLine.endsWith("GET /LESS")) { if (feedRate > 0) { feedRate = feedRate - 0.5; } } if (currentLine.endsWith("GET /FEED")) { // Check for last feeding? feedCats(); } if (currentLine.endsWith("GET /UP1")) { feedingTime[0].h++; if (feedingTime[0].h > 24){ feedingTime[0].h = 0; } } if (currentLine.endsWith("GET /DOWN1")) { feedingTime[0].h--; if (feedingTime[0].h < 0){ feedingTime[0].h = 24; } } if (currentLine.endsWith("GET /UP2")) { feedingTime[1].h++; if (feedingTime[1].h > 24){ feedingTime[1].h = 0; } } } if (currentLine.endsWith("GET /DOWN2")) { feedingTime[01].h--; if (feedingTime[1].h < 0){ feedingTime[1].h = 24; } } } // close the connection: client.stop(); // Serial.println("client disconnected"); } } // ----------------------------------------------------------------- // print the WiFi Status // ----------------------------------------------------------------- void printWifiStatus() { IPAddress ip = WiFi.localIP(); MATRIX.beginText(0, 0, 127, 127, 127); MATRIX.print(" http://"); MATRIX.print(ip); MATRIX.endText(SCROLL_LEFT); if (DEBUG == true){ // print the SSID of the network you're attached to: Serial.print("SSID: "); Serial.println(WiFi.SSID()); // print your board's IP address: Serial.print("IP Address: "); Serial.println(ip); // print the received signal strength: long rssi = WiFi.RSSI(); Serial.print("signal strength (RSSI):"); Serial.print(rssi); Serial.println(" dBm"); Serial.print("To see this page in action, open a browser to http://"); Serial.println(ip); } } // ----------------------------------------------------------------- // initialize GPIO // ----------------------------------------------------------------- void initGpio(){ pinMode(BUTTONPIN, INPUT); pinMode(BUTTONLED, OUTPUT); digitalWrite(BUTTONLED, HIGH); } // ----------------------------------------------------------------- // initialize and start WiFi // ----------------------------------------------------------------- void initWifi(){ // check for the WiFi module: if (WiFi.status() == WL_NO_MODULE) { while (true){ MATRIX.println("No WiFi"); MATRIX.endText(SCROLL_LEFT); } } // attempt to connect to Wifi network: while (status != WL_CONNECTED) { // Connect to WPA/WPA2 network. Change this line if using open or WEP network: status = WiFi.begin(ssid, pass); // wait 10 seconds for connection: delay(10000); } } // ----------------------------------------------------------------- // set feeding time by setting rtc alarm // ----------------------------------------------------------------- void setFeedTime(){ // adjust for GMT offset int adjTime = feedingTime[feedingTimeIndex].h - GMT_OFFSET; if (adjTime < 0){ adjTime = adjTime + 24; } if (adjTime > 24){ adjTime = adjTime - 24; } // set alarm to feed rtc.setAlarmTime(adjTime, feedingTime[feedingTimeIndex].m, 0); rtc.enableAlarm(rtc.MATCH_HHMMSS); // set index to next feeding time feedingTimeIndex++; if (feedingTimeIndex >= FEEDINGTIMES){ feedingTimeIndex = 0; } } // ----------------------------------------------------------------- // initialize the Real Time Clock // ----------------------------------------------------------------- void initRtc(){ rtc.begin(); unsigned long epoch = 0; do { epoch = WiFi.getTime(); delay(100); } while (epoch == 0); rtc.setEpoch(epoch + GMT_OFFSET); } // ----------------------------------------------------------------- // initialize the LED matrix // ----------------------------------------------------------------- void initLedMatrix(){ MATRIX.begin(); MATRIX.brightness(5); MATRIX.textScrollSpeed(200); MATRIX.beginText(0, 0, 127, 127, 127); // X, Y, then R, G, B } // ----------------------------------------------------------------- // display Time // ----------------------------------------------------------------- void displayTime(){ MATRIX.beginText(0, 0, 127, 127, 127); MATRIX.print(" "); int timeH = rtc.getHours() + GMT_OFFSET; if (timeH < 0){ timeH = timeH + 24; } if (timeH > 24){ timeH = timeH - 24; } if (timeH > 12) { timeH = timeH - 12; } MATRIX.print(timeH); MATRIX.print(":"); int timeM = rtc.getMinutes(); if (timeM < 10){ MATRIX.print("0"); } MATRIX.println(timeM); MATRIX.endText(SCROLL_LEFT); } // ----------------------------------------------------------------- // feed the cats! // ----------------------------------------------------------------- void feedCats(){ if (feedRate > 0){ // count down int i; for (i=10; i>0; i--){ MATRIX.beginText(0, 0, 127, 0, 0); if (i < 10){ MATRIX.print(" "); } MATRIX.println(i); MATRIX.endText(); delay(1000); } MATRIX.println(" BLAST OFF! "); MATRIX.endText(SCROLL_LEFT); // servo myservo.attach(SERVOPIN); myservo.write(SPEED); delay((int)(1000 * feedRate)); myservo.detach(); } // restore button pinMode(BUTTONLED, HIGH); delay(200); buttonPushed = false; // mark cats as fed timeToFeed = false; // set the next feeding time setFeedTime(); } // ----------------------------------------------------------------- // ISR for button // ----------------------------------------------------------------- void buttonPush(){ if (buttonPushed == false){ buttonPushed = true; // flag button push } } // ----------------------------------------------------------------- // ISR for rtc alarm // ----------------------------------------------------------------- void rtcAlarm(){ // Time to feed the cats! timeToFeed = true; }
User Interface
The User Interface runs on a server on the Arduino MKR 1010 and is only accessible locally. It is basic at the moment but we have plans to enhance it with improved HTML and CSS which are not in my core skills at the moment. Access from a PC, tablet, or smart phone is possible.
Demonstration
The following 90 second demonstration shows the time scrolling and manually activating a feeding cycle.
Conclusion
The project was a success in that the design criteria were met and the kids and I enjoyed the project and learned something. The positive aspects include:
- Overall design is both fun and functional
- Scrolling RGB matrix is eye catching
- Positive displacement screw pump works well
- Threaded brass inserts worked well and I will be using them again
Upgrades that can be considered in future and things that could have gone smoother include:
- The screw geometry and tolerances can be improved
- Modify the design for easier maintenance, particularly the ability to disassemble and clean
- Reassess power supply (currently USB power to microcontroller and separate USB to motor)
- Enhance the User Interface
Since the project is fully functional we are going to run it for a while and see if issues crop up. I would like to reprint some of the parts and include some of the upgrades mentioned above. Thanks for reading and as always comments, corrections, and suggestions for improvement are always welcome.
Top Comments