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 wo…

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.

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.