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

37K17418

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

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:

STEP 1: 3D Printed Parts

The cat food bowl holder is based on Ardy Lai's design on Thingiverse. I made it bigger to accommodate 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 of 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 CR-10s Pro printer with gold PLA filament.

Disclosure: at the time of this writing, I'm an employee of Autodesk, which makes Tinkercad.

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.

14 Comments

I am getting the error message 'getHours' was not declared in this scope when I compile this sketch. I suspect I am missing an ESP8266 library but cannot figure out which one I need. Help would be appreciated.
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? ;)
Interesting project. What I find more interesting though is how fortunate that kitty is & the fact that the floor around the bowl is clean.
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
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.
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.
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
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?
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.

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!
Good idea for my cat and dog. Thanks for Sharing!
Very nice - thanks for sharing!
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.