Making Time

Enter Your Electronics & Design Project for a chance to win a $200 shopping cart!

Submit an EntrySubmit an Entry  Back to homepage
Project14 Home
Monthly Themes
Monthly Theme Poll

 

We present a clock that synchs itself with the Network Time Protocol (NTP), feeds cats, and can be controlled wirelessly.

Cat Commander Logo

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.

3D Printed Components

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.

 

Tail Section Analysis

 

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.

 

Double Helix Feed Auger

 

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.

 

Control Panel and Enclosure

 

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.

 

Cat Commander Schematic

 

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.

 

Cat Commander Electronics

 

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.

User Interface

 

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.

 

Satisfied Customer