Image

Photo by Karsten Winegeart on Unsplash
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.
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. ]
In a nutshell, the food dispenser works like this:
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.
[ Want to test your sysadmin skills? Take a skills assessment today. ]
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.
I'll start by showing how to connect the electronic components. The schematic looks like this:
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:
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:
Here is a photo of all the components connected to a breadboard:
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 ]
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:
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:
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.
Not everything was perfect with this pet project (pun intended). We had a few issues once the prototype was up and running.
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.
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.
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. ]
Things went well from the beginning, and many of our decisions proved to be solid ones.
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:
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):
Next, I'll show how you can capture data from /dev/ttyUSB0
(the name of the device on my Fedora Linux install).
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. ]
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:
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).
This project was very exciting. Nothing beats mixing hardware and software development.
Below are some ideas I want to share with you:
Proud dad and husband, software developer and sysadmin. Recreational runner and geek. More about me