Cat Food Access Control (ESP8266 + Servo Motor + 3D Printing)

29,372

124

13

Introduction: Cat Food Access Control (ESP8266 + Servo Motor + 3D Printing)

About: Making and sharing are my two biggest passions! In total I've published hundreds of tutorials about everything from microcontrollers to knitting. I'm a New York City motorcyclist and unrepentant dog mom. My ...

This project goes over the process I used to create an automated cat food bowl, for my elderly diabetic cat Chaz. See, he needs to eat breakfast before he can get his insulin, but I often forget to pick up his food dish before I go to bed, which spoils his appetite and throws off his insulin schedule. This dish uses a servo motor to close a lid over the food between the hours of midnight and 7:30am. The NodeMCU ESP8266 microcontroller's Arduino sketch uses Network Time Protocol (NTP) to control the schedule.

This project may not be suitable for younger, more active cats. Chaz is so old and frail, he isn't inclined to try to pry the bowl open, but it is possible.

If you're new to the Arduino or the ESP8266, you may enjoy the following prerequisite guides:

Supplies:

To keep up with what I'm working on, follow me on YouTube, Instagram, Twitter, Pinterest, and subscribe to my newsletter. As an Amazon Associate I earn from qualifying purchases you make using my affiliate links.

Teacher Notes

Teachers! Did you use this instructable in your classroom?
Add a Teacher Note to share how you incorporated it into your lesson.

Step 1: 3D Printed Parts

The cat food bowl holder is based on Ardy Lai's design on Thingiverse. I made it bigger to accomodate my cat's bowl, and also made it shorter since scaling it up had made it too tall. I added a holder for a micro servo motor, and a couple holes for cables to route to the inside.

I modeled a simple lid using Tinkercad, designed to attach to the horn of the micro servo. You can grab my design directly from Tinkercad, and/or download the STLs attached to this step.

I printed the parts on my Creality CRT-10s Pro printer with gold PLA filament.

Step 2: Attach Lid to Servo Motor

I used a small drill bit to increase the size of the holes on the servo horn, then used screws to attach the servo to the 3D printed lid.

Step 3: Build NodeMCU ESP8266 Circuit

The circuit is controlled by a NodeMCU ESP8266 wifi microcontroller. I used header pins on a perma-proto board to make the micro servo motor easily detachable.The servo headers are connected to the NodeMCU as follows:

Yellow servo wire: NodeMCU D1

Red servo wire: NodeMCU power (3V3 or VIN)

Black servo wire: NodeMCU ground (GND)

Step 4: Upload Arduino Code and Test

Install your motor/lid assembly into the motor-shaped cutout on the bowl holder 3D printed part. Plug the motor header into the microcontroller board's header pins, and plug the circuit into your computer with a USB cable.

The Arduino sketch uses Network Time Protocol to fetch the current time and then opens or closes the lid according to a hard-coded schedule. Copy the following code, update your wifi credentials and UTC time offset, and upload it to your NodeMCU board using the Arduino IDE.

#include <Servo.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <WiFiUdp.h>

ESP8266WiFiMulti wifiMulti;      // Create an instance of the ESP8266WiFiMulti class, called 'wifiMulti'

WiFiUDP UDP;                     // Create an instance of the WiFiUDP class to send and receive

IPAddress timeServerIP;          // time.nist.gov NTP server address
const char* NTPServerName = "time.nist.gov";

const int NTP_PACKET_SIZE = 48;  // NTP time stamp is in the first 48 bytes of the message

byte NTPBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets

Servo myservo;  // create servo object to control a servo
// twelve servo objects can be created on most boards

int pos = 0;    // variable to store the servo position

void setup() {
  myservo.attach(5);  // attaches the servo on pin 5 aka D1 to the servo object

  //open the lid by default
    Serial.println("opening the lid");
    for (pos = 95; pos >= 0; pos -= 1) { // goes from 95 degrees to 0 degrees
      myservo.write(pos);              // tell servo to go to position in variable 'pos'
      delay(15);                       // waits 15ms for the servo to reach the position
    }
    
  Serial.begin(115200);          // Start the Serial communication to send messages to the computer
  delay(10);
  Serial.println("\r\n");

  startWiFi();                   // Try to connect to some given access points. Then wait for a connection

  startUDP();

  if(!WiFi.hostByName(NTPServerName, timeServerIP)) { // Get the IP address of the NTP server
    Serial.println("DNS lookup failed. Rebooting.");
    Serial.flush();
    ESP.reset();
  }
  Serial.print("Time server IP:\t");
  Serial.println(timeServerIP);
  
  Serial.println("\r\nSending NTP request ...");
  sendNTPpacket(timeServerIP);  
}

unsigned long intervalNTP = 60000; // Request NTP time every minute
unsigned long prevNTP = 0;
unsigned long lastNTPResponse = millis();
uint32_t timeUNIX = 0;

unsigned long prevActualTime = 0;

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - prevNTP > intervalNTP) { // If a minute has passed since last NTP request
    prevNTP = currentMillis;
    Serial.println("\r\nSending NTP request ...");
    sendNTPpacket(timeServerIP);               // Send an NTP request
  }

  uint32_t time = getTime();                   // Check if an NTP response has arrived and get the (UNIX) time
  if (time) {                                  // If a new timestamp has been received
    timeUNIX = time;
    Serial.print("NTP response:\t");
    Serial.println(timeUNIX);
    lastNTPResponse = currentMillis;
  } else if ((currentMillis - lastNTPResponse) > 3600000) {
    Serial.println("More than 1 hour since last NTP response. Rebooting.");
    Serial.flush();
    ESP.reset();
  }

  uint32_t actualTime = timeUNIX + (currentMillis - lastNTPResponse)/1000;
  uint32_t easternTime = timeUNIX - 18000 + (currentMillis - lastNTPResponse)/1000;
  if (actualTime != prevActualTime && timeUNIX != 0) { // If a second has passed since last print
    prevActualTime = actualTime;
    Serial.printf("\rUTC time:\t%d:%d:%d   ", getHours(actualTime), getMinutes(actualTime), getSeconds(actualTime));
    Serial.printf("\rEST (-5):\t%d:%d:%d   ", getHours(easternTime), getMinutes(easternTime), getSeconds(easternTime));
    Serial.println();
  } 

  // 7:30am
  if(getHours(easternTime) == 7 && getMinutes(easternTime) == 30 && getSeconds(easternTime) == 0){
  //open the lid
    Serial.println("opening the lid");
    for (pos = 95; pos >= 0; pos -= 1) { // goes from 95 degrees to 0 degrees
      myservo.write(pos);              // tell servo to go to position in variable 'pos'
      delay(15);                       // waits 15ms for the servo to reach the position
    }    
  }

    

  // midnight
  if(getHours(easternTime) == 0 && getMinutes(easternTime) == 0 && getSeconds(easternTime) == 0){
    //close the lid
    Serial.println("closing the lid");
    for (pos = 0; pos <= 95; pos += 1) { // goes from 0 degrees to 95 degrees
     // in steps of 1 degree
      myservo.write(pos);              // tell servo to go to position in variable 'pos'
      delay(15);                       // waits 15ms for the servo to reach the position
    }    
  }   

/*
// testing
  if(getHours(easternTime) == 12 && getMinutes(easternTime) == 45 && getSeconds(easternTime) == 0){
    //close the lid
    Serial.println("closing the lid");
    for (pos = 0; pos <= 95; pos += 1) { // goes from 0 degrees to 95 degrees
     // in steps of 1 degree
      myservo.write(pos);              // tell servo to go to position in variable 'pos'
      delay(15);                       // waits 15ms for the servo to reach the position
    }
  //open the lid
    Serial.println("opening the lid");
    for (pos = 95; pos >= 0; pos -= 1) { // goes from 95 degrees to 0 degrees
      myservo.write(pos);              // tell servo to go to position in variable 'pos'
      delay(15);                       // waits 15ms for the servo to reach the position
    }    
  }
 */ 
}

void startWiFi() { // Try to connect to some given access points. Then wait for a connection
  wifiMulti.addAP("ssid_from_AP_1", "your_password_for_AP_1");   // add Wi-Fi networks you want to connect to
  //wifiMulti.addAP("ssid_from_AP_2", "your_password_for_AP_2");
  //wifiMulti.addAP("ssid_from_AP_3", "your_password_for_AP_3");

  Serial.println("Connecting");
  while (wifiMulti.run() != WL_CONNECTED) {  // Wait for the Wi-Fi to connect
    delay(250);
    Serial.print('.');
  }
  Serial.println("\r\n");
  Serial.print("Connected to ");
  Serial.println(WiFi.SSID());             // Tell us what network we're connected to
  Serial.print("IP address:\t");
  Serial.print(WiFi.localIP());            // Send the IP address of the ESP8266 to the computer
  Serial.println("\r\n");
}

void startUDP() {
  Serial.println("Starting UDP");
  UDP.begin(123);                          // Start listening for UDP messages on port 123
  Serial.print("Local port:\t");
  Serial.println(UDP.localPort());
  Serial.println();
}

uint32_t getTime() {
  if (UDP.parsePacket() == 0) { // If there's no response (yet)
    return 0;
  }
  UDP.read(NTPBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
  // Combine the 4 timestamp bytes into one 32-bit number
  uint32_t NTPTime = (NTPBuffer[40] << 24) | (NTPBuffer[41] << 16) | (NTPBuffer[42] << 8) | NTPBuffer[43];
  // Convert NTP time to a UNIX timestamp:
  // Unix time starts on Jan 1 1970. That's 2208988800 seconds in NTP time:
  const uint32_t seventyYears = 2208988800UL;
  // subtract seventy years:
  uint32_t UNIXTime = NTPTime - seventyYears;
  return UNIXTime;
}

void sendNTPpacket(IPAddress& address) {
  memset(NTPBuffer, 0, NTP_PACKET_SIZE);  // set all bytes in the buffer to 0
  // Initialize values needed to form NTP request
  NTPBuffer[0] = 0b11100011;   // LI, Version, Mode
  // send a packet requesting a timestamp:
  UDP.beginPacket(address, 123); // NTP requests are to port 123
  UDP.write(NTPBuffer, NTP_PACKET_SIZE);
  UDP.endPacket();
}

inline int getSeconds(uint32_t UNIXTime) {
  return UNIXTime % 60;
}

inline int getMinutes(uint32_t UNIXTime) {
  return UNIXTime / 60 % 60;
}

inline int getHours(uint32_t UNIXTime) {
  return UNIXTime / 3600 % 24;
}

Step 5: Use It!

Route your wires to the inside of the bowl holder, and plug your cat feeder into an outlet using a USB AC adapter. The way the simple code is written, it is meant to be booted up in the "open" state, and will only change its lid position at the time thresholds specified in the Arduino sketch.

Thanks for following along! If you make your own version, I'd love to see it in the I Made It section below!

If you like this project, you may be interested in some of my others:

To keep up with what I'm working on, follow me on YouTube, Instagram, Twitter, and Pinterest.

Be the First to Share

    Recommendations

    • Trash to Treasure Contest

      Trash to Treasure Contest
    • Raspberry Pi Contest 2020

      Raspberry Pi Contest 2020
    • Wearables Contest

      Wearables Contest

    13 Discussions

    0
    deluges
    deluges

    15 days ago

    How long do you think until the cats figure out that they can take all the food out of the bowl during the day, keep it neatly on the floor and eat it at night? ;)

    0
    lorenkinzel
    lorenkinzel

    6 weeks ago

    Interesting project. What I find more interesting though is how fortunate that kitty is & the fact that the floor around the bowl is clean.

    1
    pablopiter
    pablopiter

    7 weeks ago on Step 5

    Hi Becky
    Excellent project! It would be good to make an improvement to identify which cat is about to eat if it opens the lid or not.
    I have two cats and they have to eat different foods, always imagine such a project with some tag system on the collars to identify if you are authorized to eat or not.
    Best regards from Buenos Aires

    Pablo

    0
    sbrown9578
    sbrown9578

    Reply 6 weeks ago

    I was thinking the same exact thing. My cats need to lose weight badly. I have one over 20 pounds and the other male isn't far behind him. My female cat is perfect. She never over eats. When she is full she walks away and doesn't try to push the others from their dishes to steal their food. From now on I am only going to have female cats. They are less of a headache. Especially when it comes to health.

    0
    charlessenf-gm
    charlessenf-gm

    Reply 6 weeks ago

    There is an RFID controlled pet access door out there somewhere - or it was years ago. Hang the RFID ot the animal's collar. Lets any animal out, only RFID'd animals in.

    1
    DeejayO1
    DeejayO1

    Reply 7 weeks ago

    You can use load cell as carpet in front of the feeder for understand the mass of the cats or rfid chip if the cat's have the chip installed on their body

    0
    sbrown9578
    sbrown9578

    6 weeks ago

    This project comes close to what I need. I have overweight pets (2) and I was looking for a bowl that opens to each specific pet. The 2 fat cats always push each other and my 3rd cat out of their food dishes and over eat. Is it possible to add an RFID so it only opens to a specific animal with the matching tag? If so maybe you could work that out and post it?

    1
    hank-cowdog
    hank-cowdog

    6 weeks ago

    Cute design and nice repurposing of the existing 3D object.

    Looking at the code, I see you query the NIST NTP servers every _minute_. Is that frequency really necessary? I would think once every several hours would be sufficient as the clock drift on the crystal is small and the need for 1 second or better accuracy is not really needed for this project.

    Looks like all that would be needed to be change is a single line (Line 52):

    unsigned long intervalNTP = 60000; // Request NTP time every minute
    would become:
    unsigned long intervalNTP = 1000*60*60*6; // Request NTP time every six hours (in millisecs)

    Switching to every six hours would drop the load on the NIST servers from 1,440 hits a day to 4, with no other apparent change in the device's behavior.

    0
    JeremyH117
    JeremyH117

    Reply 6 weeks ago

    I've got to say that this was my first reaction looking at the code. Polling the time every minute is certainly more than really needed. I like hank-cowdog's proposed change to 6 hours.

    Other than that, I really like this project. I'd been considering something similar for our cats. We can't leave the food out all the time since one of them doesn't know when to stop!

    0
    CarlosV170
    CarlosV170

    6 weeks ago

    Good idea for my cat and dog. Thanks for Sharing!

    0
    kk6by
    kk6by

    6 weeks ago

    Very nice - thanks for sharing!

    0
    Chelo.
    Chelo.

    6 weeks ago

    Thanks for sharing! That's a simple but very useful design. My cat is usually very impatient to eat in the morning so it could be a great relief for that. It would be a great improvement attaching an IR sensor to sense the amount of food or know when the tray is empty. Greetings from Uruguay.

    0
    MissionCritical
    MissionCritical

    7 weeks ago

    amazing work! thank you for sharing :)