I want to share with you a small automation project me and my daughter did together (check the video here); She had an idea to make a food dispenser for our dog, Domino and I tough it would be the perfect excuse to write some code that also interfaced with real hardware.
We chose an Arduino hardware, as it has a huge support community, and it is very inexpensive way to introduce yourself to DYI electronics:
Arduino is an open-source electronics platform based on easy-to-use hardware and software.
We used the Arduino Nano for this project, due its very small size and price.
I didn’t come up with the idea or the code myself; ROBO HUB wrote a detailed tutorial on now to make a pet food dispenser with basic materials, and I took notes.
He also made a video, which I ended watching many times over (one of the many on his YouTube channel).
In a nutshell the food dispenser work like this:
All my links are to the Amazon store because it is easy to see the product there before buying; I do not get a commission, and I encourage you to look somewhere else to get the best price.
But next I will give you the most important piece of advice for the whole tutorial.
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 we can improve over several iterations.
Let’s get started and see how we can connect the electronic components first; You can watch a video of the whole process here and then come back to follow this tutorial.
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; For my project I did this:
The rest of the 5V and Ground connect to the Arduino Nano as well; because it has so many connections, I used a solderless breadboard:
But if you are like me you rather see a photo of all the components connected together with 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 on Fedora as an RPM:
sudo dnf install -y fritzing.x86_64 fritzing-parts.noarch
On 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 the Git repository, so you can open them and modify their contents at will.
If you haven’t downloaded and installed the Arduino 2 IDE please do it now, just follow these instructions, and then move on.
You can write code for the Arduino using their programing language; It looks a lot like C, and it has 2 very simple and important functions:
I found than 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 on the IDE.
In any case, I re-wrote the original code keeping the most functionality intact, and added extra debugging to see if the ultrasound sensor were able to pick 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 GUI is easy, just click the arrow icon, after selecting the board and port from the pulldown menu:
It would say something like this once 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):
The loop in the code constantly keeps sending ultrasonic “ping” 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 3 hours more after this change, but still I want to squeeze more power; The Arduino guide as lots more suggestions, and of course there is much more on the topic of saving energy, but this is a good start.
What other issues we have? Move on to the next item.
There is no way to disable, that also contributes to kill the battery; more research from my part is needed here.
Time to check the next issue.
A bigger container is needed as the electronics took most of the space; Our dimensions where width 5.5 x 2.5 inches x 18 inches tall (height was good enough).
I plan to build a wider, but not taller, enclosure; I did waste space on the bottom of the dispenser and this is maybe a good place to place the electronics.
The decision to use a small breadboard was good too.
Other decisions proved to be the solid ones:
If you are like me, you like to capture your data for later analysis; you may want to 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 that way):
Let’s see now how you can capture data from /dev/ttyUSB0
(that’s the name of the device on my Fedora Linux install).
You can open the serial port using minicom.
To install it on Fedora, CentOS, RHEL, and similar:
[josevnz@dmaf5 Documents]$ sudo dnf install minicom
Then to capture data, you can run like this (make sure you are not capturing from the Arduino IDE 2, otherwise the device will be locked):
[josevnz@dmaf5 Documents]$ minicom --capturefile=$HOME/Downloads/ultrasonic_sensor_cap.txt --baudrate 9600 --device /dev/ttyUSB0
See it in action:
But it is not the only way to capture data, what if we use Python for this?
Of course, you can achieve the same with a very simple Python script:
#!/usr/bin/env python
"""
Simple 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 our Python script doesn’t have to be poor copy of minicom; what if we export our data by using the Prometheus client SDK?.
Let me show you a demo below:
Then we can monitor our new datasource from the Prometheus scrapper UI, but first we need to tell our Prometheus agent where is it.
Below you can see several scrape job configs, 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 there to get notified 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: