Skip to main content

How to make an automatic dog feeder with Arduino and Linux

Try this DIY project to learn (or teach your family) to write code that interfaces with real hardware.
Image
Photo of a French bulldog eating out of a copper bowl

My daughter and I recently did a small automation project together. She wanted to make a food dispenser for our dog, Domino, and I thought it would be the perfect excuse to write some code that interfaces with real hardware.

[ Want to DIY? Download the Getting started with Raspberry Pi cheat sheet. ]

We chose Arduino hardware, as it is open source, has a huge support community, and the hardware and software are easy to use. It is also a very inexpensive introduction to do-it-yourself (DIY) electronics. Due to its small size and price, we used the Arduino Nano for this project.

Image
Arduino hardware
(Jose Vicente Nunez, CC BY-SA 4.0)

The project is not all original work. I found lots of inspiration (and instructions) online, and you should do the same. I didn't develop the idea or the code myself. ROBO HUB wrote a detailed tutorial on making a pet food dispenser with basic materials, and I took notes. He also made a video, which I watched many times over (one of many on his YouTube channel).

[ Looking for a project? Check out 10 DIY IoT projects to try using open source tools. ]

How does the food dispenser work?

In a nutshell, the food dispenser works like this:

  1. Food sits inside a cardboard enclosure. At the bottom, a lid opens to let the food fall when an obstacle is detected in front of the food dispenser.
  2. The lid opens only when the obstacle is less than 35cm from the food dispenser. It waits five seconds after it opens and then closes again.
  3. The servo rotates 90 degrees, just enough to open the lid fully, then moves back to the zero-degree position.

The materials list

All the links are to Amazon because it is easy to see the product before buying. I do not get a commission, and I encourage you to look around for the best price.

Electronics

[ Want to test your sysadmin skills? Take a skills assessment today. ]

Dog food enclosure

  • Glue gun: Any glue gun will do; there's no need to get something fancy.
  • Scissors
  • Folding utility knife: I used this to cut holes that were too small for scissors.
  • A cardboard box: Do not underestimate the height. Look for something no less than 12 inches tall. I used a square box, but you may use a cylinder like in the original tutorial (the square box made it easier for me to place the components inside). The same can be said for the width. You will need space to place the electronic components and the food inside.

Software

  • sudo privileges to install packages like Minicom (to read data from the serial port) and Fritz (to make some nice-looking drawings of the project).
  • Arduino IDE 2: Download this to write the code to control the electronics.
  • Python 3 to run a few scripts to publish ultrasonic sensor data as Prometheus metrics using the computer.

Before you start

Here is the most important advice I can give for the whole tutorial:

Perfect is the enemy of good.

Or as Voltaire quoted an Italian proverb:

Dans ses écrits, un sage Italien
Dit que le mieux est l'ennemi du bien.

The whole point of this exercise is to learn and make mistakes (trying to avoid repeating the old ones), not looking for the perfect mousetrap (or food dispenser), but rather one that works decently well and you can improve over several iterations.

Watch this video of the whole process here and then come back to follow this tutorial.

Connect the components

I'll start by showing how to connect the electronic components. The schematic looks like this:

Image
Food dispenser schematic
(Jose Vicente Nunez, CC BY-SA 4.0)

When you assemble a project in Arduino, you connect components to either the digital or analog pins, which are numbered. We did the following for this project:

  1. HC-SR04 ultrasonic trigger connects to pin D2 on Arduino Nano. The trigger side of the sensor sends the pulse that will bounce on the obstacle (for a more detailed explanation, watch this video).
  2. HC-SR04 ultrasonic echo connects to pin D3 on Arduino Nano. This device receives the bouncing pulse to get the distance between the food dispenser and the obstacle. It measures how long it took the pulse to travel and, using the speed of sound, we calculated the distance.
  3. SG90 servo connects to pin D9 on Arduino Nano. This piece moves at a 90-degree angle if an obstacle is detected and the distance is less than 35cm (or whatever distance you choose in the code).

The rest of the 5V and ground wires also connect to the Arduino Nano. Because it has so many connections, I used a solderless breadboard:

Image
Food dispenser breadboard
(Jose Vicente Nunez, CC BY-SA 4.0)

Here is a photo of all the components connected to a breadboard:

Image
Attached components
(Jose Vicente Nunez, CC BY-SA 4.0)

You may be wondering how I made these diagrams. The open source application Fritzing is a great way to draw out electronic schematics and wiring diagrams, and it's available for Fedora as an RPM:

$ sudo dnf install -y fritzing.x86_64 fritzing-parts.noarch

On Red Hat Enterprise Linux (RHEL) and CentOS, you can install it as a Flatpak:

$ flatpak install org.fritzing.Fritzing

I've included the diagram source file (schematics/food_dispenser.fzz) in my Git repository, so you can open it and modify the contents.

[ Cheat sheet: Old Linux commands and their modern replacements ]

Code for the Arduino controller

If you haven't downloaded and installed the Arduino 2 IDE, please do it now. Just follow these instructions before moving on.

You can write code for the Arduino using its programing language. It looks a lot like C, and it has two very simple and important functions:

  • setup: You initialize the components and set up which ports can read and write data. It is called only once.
  • loop: You read and write data to the sensors.

I found that the original code needed a few updates to cover my use case, but it is a good idea to take a look and run it just to learn how it works:

$ curl --fail --location --remote-name 'http://letsmakeprojects.com/wp-content/uploads/2020/12/arduino-code.docx'

Yes, you will need to copy and paste it into the IDE.

I rewrote the original code, keeping most functionality intact, and added extra debugging to see if the ultrasound sensor was able to measure the distance when an obstacle was found:

/*
Sketch to control the motor that open/ closes the cap that lets the food drop on the dispenser.
References: 
* https://www.arduino.cc/reference/en/
* https://create.arduino.cc/projecthub/knackminds/how-to-measure-distance-using-ultrasonic-sensor-hc-sr04-a-b9f7f8

Modules:
- HC-SR04: Ultrasonic sensor distance module
- SG90 9g Micro Servos: Opens / closes lid on the food dispenser
*/
#include <Servo.h>
Servo servo;

unsigned int const DEBUG = 1;

/*
Pin choice is arbitrary.
 */ 
const unsigned int HC_SR04_TRIGGER_PIN = 2; // Send the ultrasound ping
const unsigned int HC_SR04_ECHO_PIN = 3; // Receive the ultrasound response
const unsigned int SG90_SERVO_PIN = 9;  // Activate the servo to open/ close lid

const unsigned int MEASUREMENTS = 3;
const unsigned int DELAY_BETWEEN_MEASUREMENTS_MILIS = 50;

const unsigned long ONE_MILISECOND = 1;
const unsigned long ONE_SECOND = 1000;
const unsigned long FIVE_SECONDS = 3000;

const unsigned long MIN_DISTANCE_IN_CM = 35; // Between 2cm - 500cm

const unsigned int OPEN_CAP_ROTATION_IN_DEGRESS = 90; // Between 0 - 180
const unsigned int CLOSE_CAP_ROTATION_IN_DEGRESS = 0;

const unsigned int CLOSE = 0;

/*
Speed of Sound: 340m/s = 29microseconds/cm
Sound wave reflects from the obstacle, so to calculate the distance we consider half of the distance traveled.  
DistanceInCms=microseconds/29/2 
*/
long microsecondsToCentimeters(long microseconds) {
  return microseconds / 29 / 2;
}

unsigned long measure() {
  /*
  Send the ultrasound ping
  */
  digitalWrite(HC_SR04_TRIGGER_PIN, LOW);
  delayMicroseconds(5);
  digitalWrite(HC_SR04_TRIGGER_PIN, HIGH);
  delayMicroseconds(15);
  digitalWrite(HC_SR04_TRIGGER_PIN, LOW);

  /*
  Receive the ultrasound ping and convert to distance
  */
  unsigned long pulse_duration_ms = pulseIn(HC_SR04_ECHO_PIN, HIGH);
  return microsecondsToCentimeters(pulse_duration_ms);
}


/*
- Close cap on power on startup
- Set servo, and read/ write pins
 */
void setup() {
  pinMode(HC_SR04_TRIGGER_PIN, OUTPUT);
  pinMode(HC_SR04_ECHO_PIN, INPUT);
  servo.attach(SG90_SERVO_PIN);
  servo.write(CLOSE_CAP_ROTATION_IN_DEGRESS);
  delay(ONE_SECOND);
  servo.detach();
  if (DEBUG) {
    Serial.begin(9600);
  }
}

void loop() {
  float dist = 0;
  for (int i = 0; i < MEASUREMENTS; i++) {  // Average distance
    dist += measure();
    delay(DELAY_BETWEEN_MEASUREMENTS_MILIS);  //delay between measurements
  }
  float avg_dist_cm = dist / MEASUREMENTS;

  /*
  If average distance is less than threshold then keep the door open for 5 seconds 
  to let enough food out, then close it.
  */
  if (avg_dist_cm < MIN_DISTANCE_IN_CM) {
    servo.attach(SG90_SERVO_PIN);
    delay(ONE_MILISECOND);
    servo.write(OPEN_CAP_ROTATION_IN_DEGRESS);
    delay(FIVE_SECONDS);
    servo.write(CLOSE_CAP_ROTATION_IN_DEGRESS);
    delay(ONE_SECOND);
    servo.detach();
  }

  if (DEBUG) {
    Serial.print(avg_dist_cm);
    Serial.print("cm");
    Serial.println();
  }

}

Compiling and deploying from the Arduino graphical user interface (GUI) is easy. Just click the arrow icon after selecting the board and port from the pulldown menu:

Image
Upload to Arduino IDE
(Jose Vicente Nunez, CC BY-SA 4.0)

It displays something like this after the code is uploaded:

Sketch uses 3506 bytes (11%) of program storage space. Maximum is 30720 bytes.
Global variables use 50 bytes (2%) of dynamic memory, leaving 1998 bytes for local variables. Maximum is 2048 bytes.

Initial issues

Not everything was perfect with this pet project (pun intended). We had a few issues once the prototype was up and running.

Short battery life

The battery life decreased dramatically just after a few hours. The loop in the code constantly keeps sending ultrasonic "pings" and checking the distance. After looking around, I found a library compatible with the ATMega328P controller (used on the Arduino One).

I enabled the debug code to monitor the serial port, and I constantly saw messages like this:

14:13:59.094 -> 281.00cm
14:13:59.288 -> 281.67cm
14:13:59.513 -> 280.67cm
14:13:59.706 -> 281.67cm
14:13:59.933 -> 281.33cm
14:14:00.126 -> 281.00cm
14:14:00.321 -> 300.33cm
...
14:20:00.321 -> 16.00cm
...

The new version that powers down for a bit to save energy is here:

/*
Sketch to control the motor that open/ closes the cap that lets the food drop on the dispenser.
References: 
* https://www.arduino.cc/reference/en/
* https://create.arduino.cc/projecthub/knackminds/how-to-measure-distance-using-ultrasonic-sensor-hc-sr04-a-b9f7f8

Modules:
- HC-SR04: Ultrasonic sensor distance module
- SG90 9g Micro Servos: Opens / closes lid on the food dispenser
*/
#include "LowPower.h"
#include <Servo.h>
Servo servo;

unsigned int const DEBUG = 1;

/*
Pin choice is arbitrary.
 */ 
const unsigned int HC_SR04_TRIGGER_PIN = 2; // Send the ultrasound ping
const unsigned int HC_SR04_ECHO_PIN = 3; // Receive the ultrasound response
const unsigned int SG90_SERVO_PIN = 9;  // Activate the servo to open/ close lid

const unsigned int MEASUREMENTS = 3;
const unsigned int DELAY_BETWEEN_MEASUREMENTS_MILIS = 50;

const unsigned long ONE_MILISECOND = 1;
const unsigned long ONE_SECOND = 1000;
const unsigned long FIVE_SECONDS = 3000;

const unsigned long MIN_DISTANCE_IN_CM = 35; // Between 2cm - 500cm

const unsigned int OPEN_CAP_ROTATION_IN_DEGRESS = 90; // Between 0 - 180
const unsigned int CLOSE_CAP_ROTATION_IN_DEGRESS = 0;

const unsigned int CLOSE = 0;

/*
Speed of Sound: 340m/s = 29microseconds/cm
Sound wave reflects from the obstacle, so to calculate the distance we consider half of the distance traveled.  
DistanceInCms=microseconds/29/2 
*/
long microsecondsToCentimeters(long microseconds) {
  return microseconds / 29 / 2;
}

unsigned long measure() {
  /*
  Send the ultrasound ping
  */
  digitalWrite(HC_SR04_TRIGGER_PIN, LOW);
  delayMicroseconds(5);
  digitalWrite(HC_SR04_TRIGGER_PIN, HIGH);
  delayMicroseconds(15);
  digitalWrite(HC_SR04_TRIGGER_PIN, LOW);

  /*
  Receive the ultrasound ping and convert to distance
  */
  unsigned long pulse_duration_ms = pulseIn(HC_SR04_ECHO_PIN, HIGH);
  return microsecondsToCentimeters(pulse_duration_ms);
}


/*
- Close cap on power on startup
- Set servo, and read/ write pins
 */
void setup() {
  pinMode(HC_SR04_TRIGGER_PIN, OUTPUT);
  pinMode(HC_SR04_ECHO_PIN, INPUT);
  servo.attach(SG90_SERVO_PIN);
  servo.write(CLOSE_CAP_ROTATION_IN_DEGRESS);
  delay(ONE_SECOND);
  servo.detach();
    if (DEBUG) {
    Serial.begin(9600);
  }
}

void loop() {

  float dist = 0;
  for (int i = 0; i < MEASUREMENTS; i++) {  // Average distance
    dist += measure();
    delay(DELAY_BETWEEN_MEASUREMENTS_MILIS);  //delay between measurements
  }
  float avg_dist_cm = dist / MEASUREMENTS;

  /*
  If average distance is less than threshold then keep the door open for 5 seconds 
  to let enough food out, then close it.
  */
  if (avg_dist_cm < MIN_DISTANCE_IN_CM) {
    servo.attach(SG90_SERVO_PIN);
    delay(ONE_MILISECOND);
    servo.write(OPEN_CAP_ROTATION_IN_DEGRESS);
    delay(FIVE_SECONDS);
    servo.write(CLOSE_CAP_ROTATION_IN_DEGRESS);
    delay(ONE_SECOND);
    servo.detach();
    // Pet is eating and in front of the dispenser, we can definitely sleep longer
    LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
  } else {
    LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF);
  }

  if (DEBUG) {
    Serial.print(avg_dist_cm);
    Serial.print(" cm");
    Serial.println();
  }
}

The battery lasted around three hours after this change, but I wanted to squeeze more power. The Arduino guide has many more suggestions, and of course, there is much more on the topic of saving energy, but this is a good start.

Power LED on continually

The 3.3V/ 5V MB102 solderless breadboard power supply module kept the power LED on all the time. There is no way to disable the breadboard's power LED. That also contributes to killing the battery. I need to do more research here.

Limited food supply

The food capacity tank is very limited, but the enclosure to put the electronics works well. A bigger container is needed as the electronics take up most of the space. The dimensions were 5.5 x 2.5 inches wide and 18 inches tall (the height is good enough).

I plan to build a wider, but not taller, enclosure. I did waste space on the bottom of the dispenser, so that may be a good place to place the electronics. The decision to use a small breadboard was good too.

[ Get the guide to installing applications on Linux. ]

What went well?

Things went well from the beginning, and many of our decisions proved to be solid ones.

  • The only part I had to glue was attaching the servo to the cardboard. All the pieces, including the lid, can be easily disassembled (including the battery), so I can still tweak the code without painful reassembling. I like the design.
  • Food doesn't get stuck inside the container. Gravity works, and there is no waste once all the food is gone.
  • Using a rectangle rather than a cylinder for the enclosure made fitting the parts much easier. If you have a small, long box that you can recycle, then you are all set.

Capture information from the Arduino

We fine-tuned the dispenser by capturing output from the Arduino serial port. If you want to capture your data for later analysis, you can do the following:

  • Set an alert if the external battery is nearly depleted or monitor how much charge remains (if possible).
  • Get the distance when the ultrasonic sensor is tripped. You can use this to calibrate the food dispenser better, for example, deciding whether the plate is too close or too far.
  • Configure an alert if the food inside the dispenser is about to run out before your pet files a bug request.

The Arduino UI has a nice way to show the activity on the USB serial port (and also allows you to send commands to the Arduino):

Image
Arduino serial port capture
(Jose Vicente Nunez, CC BY-SA 4.0)

Next, I'll show how you can capture data from /dev/ttyUSB0 (the name of the device on my Fedora Linux install).

Capture serial port data with Minicom

The easiest way is using Minicom. You can open the serial port using Minicom.

To install it on Fedora, CentOS, RHEL, and similar:

$ sudo dnf install minicom

Run the following command to capture data, but make sure you are not capturing from the Arduino IDE 2; otherwise, the device will be locked:

$ minicom --capturefile=$HOME/Downloads/ultrasonic_sensor_cap.txt --baudrate 9600 --device /dev/ttyUSB0

See it in action:

But that is not the only way to capture data. What about using Python?

[ Get started with IT automation with the Ansible Automation Platform beginner's guide. ]

Capture serial port data with Python

You can also capture serial port data with a Python script:

#!/usr/bin/env python
"""
Script to dump the contents of a serial port (ideally your Arduino USB port)
Author: Jose Vicente Nunez (kodegeek.com@protonmail.com)
"""
import serial

BAUD = 9600
TIMEOUT = 2
PORT = "/dev/ttyUSB0"

if __name__ == "__main__":
    serial_port = serial.Serial(port=PORT, baudrate=BAUD, bytesize=8, timeout=TIMEOUT, stopbits=serial.STOPBITS_ONE)
    try:
        while True:
            # Wait until there is data waiting in the serial buffer
            if serial_port.in_waiting > 0:
                serialString = serial_port.readline()
                # Print the contents of the serial data
                print(serialString.decode('utf-8').strip())
    except KeyboardInterrupt:
        pass

But the Python script doesn't have to be a poor copy of Minicom. What if you export the data using the Prometheus client SDK?

Here is a demonstration:

Then you can monitor the new data source from the Prometheus scraper's user interface. First, tell the Prometheus agent where it is. Below are several scrape job configurations. The last one (yafd) is the new Python script:

--
global:
    scrape_interval: 30s
    evaluation_interval: 30s
    scrape_timeout: 10s
    external_labels:
        monitor: 'nunez-family-monitor'

scrape_configs:
    - job_name: 'node-exporter'
      static_configs:
          - targets: ['raspberrypi.home:9100', 'dmaf5:9100']
    - job_name: 'docker-exporter'
      static_configs:
          - targets: ['raspberrypi.home:9323', 'dmaf5:9323']
    - job_name: 'yafd-exporter'
      static_configs:
          - targets: ['dmaf5:8000']
      tls_config:
          insecure_skip_verify: true

After that, you can check it on the Prometheus dashboard:

Image
Prometheus dashboard
(Jose Vicente Nunez, CC BY-SA 4.0)

Then you can add it to Grafana and even add alerts to notify you when your dog gets food (OK, maybe that's too much).

What's next?

This project was very exciting. Nothing beats mixing hardware and software development.

Below are some ideas I want to share with you:

  • If you have kids, get them involved: The best part was getting my daughter interested in electronics and software. She had a blast putting this together with me. Our design sessions were probably the most productive parts of the project.
  • You may want to buy an inexpensive learning kit with an Arduino controller. Mine has good documentation and there are lots of tutorials you can follow online. Because the hardware is open source, there are many makers out there.
  • While looking into lowering the power consumption on my Arduino board, I stumbled into using solar power with the boards. I strongly recommend you read this article. It may make a difference in your next project.
  • Looking for a module for your board? The Arduino Libraries open source community may have it already.
  • There are plenty of hardware improvements I can make to this food dispenser, like adding more food capacity, the ability to upload instrumentation data so it can be monitored remotely, and tracking how much food remains in the container. So many possibilities!

Topics:   Sysadmin culture   Programming   Hardware  
Author’s photo

Jose Vicente Nunez

Proud dad and husband, software developer and sysadmin. Recreational runner and geek. More about me

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.