In November 2016, I built an automated Cat Feeder, powered by Amazon Alexa (https://www.hackster.io/darian-johnson/alexa-powered-automated-cat-feeder-9416d4). The first build was very much a prototype: I needed a wooden dowel to hold the servo in place and the sensors were very "binary" in nature (they could not give me precise measurements e.g. - either the hopper was empty or 'not empty').
I kept making changes and updates, until I completed a more complete 'Version 2.0' of the Cat Feeder - with the following key changes:
- The Cat Feeder was now encased a better structure - leveraging a plastic two-gallon bucket and assorted rubber PVC pieces.
- An ultrasonic sensor was used to determine the amount of food in the Cat Feeder Hooper.
- Images from an attached camera are analyzed in a Python script to determine how full/empty the cat bowl is (0%, 25%, 50%, 75%, 100%).
- Amazon Dash Replenishment was build into the code - enabling the Cat Feeder to automatically order cat food when the supply was low.
Here a quick overview of the basic and Dash Replenishment functionality
Overview/High-Level FlowThe Cat Feeder is built from a number of components:
- An Alexa skill that controls the device (by translating my user requests into commands that are sent to the physical device via MQTT)
- A dry food dispenser connected to a Raspberry Pi — used to received the messages and control the device
- A camera with image comparison logic to determine if the cat food bowl is full or empty.
- An ultrasonic range sensor — used to measure the food in the hopper.
- A servo — used to turn the paddle wheel and dispense the food
- Amazon Dash Replenishment logic to re-order cat food when my supply was low.
Here is a high level flow of how a user interacts with the Cat Feeder.
Step 1: The user asks Alexa to feed the cat four ounces of food.
Step 2: The Alexa skill is called, which interacts with code/logic in AWS to obtain the specific serial (or topic) ID assigned to the cat feeder.
Step 3: The Skill sends a message using AWS IoT to feed the cat 4 ounces. The Skill also obtains:
- the amount of food stored in the hopper (using the ultrasonic sensor)
- The current state of the cat food bowl (empty, 50% full, 100% full) using the camera and image comparison logic
Note -these actions are represented in Step C. The state of the hopper and food bowl is updated every 15 minutes.
Step 4: AWS IoT sends a message to the Cat Feeder to dispense 4 ounces of food. The Cat Feeder Raspberry Pi spins the servo for 4 seconds to dispense the food.
Step 5: In parallel, the Skill calls Amazon DRS APIs to send the slot status (the “slot” is the cat food amount). If an order is needed, then the DRS Replenishment API is called as well (to place an order for new cat food).
Step 6: Dash Replenishment takes action if needed (to order more cat food)
Step 7: Alexa responds to the user with confirmation that the food has been dispensed.
- If the hopper is empty, Alexa tells the user to refill the hopper.
- If the cat food bowl is full, Alexa asks the user to confirm feeding the cat
- If a previous DRS order was cancelled, Alexa tells the user that a new order will be placed in 5 days
- If two consecutive orders were cancelled, Alexa tells the user that a replenishment ordering has been suspended and will not be enabled until the Cat Feeder hopper is refilled.
Amazon Dash APIs
DeviceStatus - called daily to provide the last date that the Cat Feeder device was used. This is executed in a stand alone Lambda program that is scheduled to run once a day
function DRS_DeviceStatus(token, isoDate, eventCallback){
var data = JSON.stringify({
mostRecentlyActiveDate: isoDate
});
var options = {
hostname: 'dash-replenishment-service-na.amazon.com',
port: 443,
path: '/deviceStatus' ,
method: 'POST',
headers: {'Authorization' : 'Bearer ' + token,
'x-amzn-accept-type': 'com.amazon.dash.replenishment.DrsDeviceStatusResult@1.0',
'x-amzn-type-version': 'com.amazon.dash.replenishment.DrsDeviceStatusInput@1.0',
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
}
};
var req = https.request(options, function(res) {
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log('statusCode:', res.statusCode);
console.log("body: " + chunk);
eventCallback(chunk);
});
});
req.write(data);
req.end();
}
SubscriptionInfo & SlotStatus - called every time the cat feeder dispenses food to the cat. If the user is subscribted to the slot, then the Cat Feeder Alexa Skill calls DRS to provide updated information on quantities in the cat feeder and on hand. The amount of food in the hopper is provided by the ultrasonic sensor in the Cat Feeder. The amount on hand is persisted in the Cat_Feeder_Config DynamoDB table, and updated every time the amount in the hopper increases (signifying that food was taken from the bag and placed into the hopper).
Replenish - called when the amount on food on hand is zero and the amount of food in the hopper will only last 8 days.
The SubscriptionInfo, SlotStatus, and Replenish APIs are executed in the Cat Feeder Alexa Skill (see the determineDRSaction function)
Customer Experience
Amazon Dash prefers that communication is handled via Dash except in two instances:
- A user cancels an order
- A user cancels two orders and dash replenishment is disables
I also needed to inform the user if there was a problem authenticating with DRS.
To do this, I have a third Lambda program that is triggered when a message is received from DRS via SNS queue (this is set up during the DRS set-up process). Fields are updated in the Cat_Feeder_Config table and appropriate actions is taken when the Cat Feeder is used.
Order Placed:
- When an Order Placed SNS messaged is received, the Order Status field in the CatFeeder_Config table is updated with an order placed status.
Item Shipped:
- When an Item Shipped SNS message is received, the On Hand amount in the CatFeeder_Config table is updated with the shipped amount (in ounces)
Order Cancelled Once:
- When an Order Cancelled SNS message is received, the Cancel Number is set to 1, the Reorder Date is set to 5 days from the cancellation date, and the "Notify User" field is set to Y.
- When the user next uses the cat feeder, s/he is notified that the item will be reordered on the reorder date.
- The "Notify User" field is changed to N once the user has been notified
Order Cancelled Twice:
- When an second Order Cancelled SNS message is received, the Cancel Number is set to 2 and the "Notify User" field is set to Y.
- When the user next uses the cat feeder, s/he is notified that the item will not be reorded an that the replenishment process will not resume until the Cat Feeder is refilled.
- The "Notify User" field is changed to N once the user has been notified
Device Unregistered:
- When a device is unregistered from DRS, the refresh token and associated DRS fields are removed from the record.
Token Invalid Unregistered:
- If a token is invalid, then the user is directed to validate the status of their DRS registration on the Amazon website.
Registration
I needed to create a device and security profile before I could integrate Dash Replenishment into my project. A great "getting started" guide can be found here: https://www.hackster.io/harshmangukiya/getting-started-with-amazon-drs-5078f0
Once that is complete, this is how a user interacts with the Cat Feeder to enable Amazon Dash Replenishment
Step 1 - Obtain a unique id using the Alexa Skill
1 -User enablesCat Feeder skill and ask for instuctions
2 - Alexa Service calls the Cat Feeder skill and a unique id (serial number or topic id) is created
3 - Cat Feeder skills interacts with AWS to save the ID
4 - Alexa responds; the ID is sent to the user in the Alexa App
Step 2 - Register with Amazon Dash Replenishment
1 -User build cat feeder per instructions (see below)
2 - Once Cat Feeder is built, user registered via Cat Feeder DRS Page
- Note - code for this page is in the GitHub library. I've created a seperate project that details this in more detail here - https://www.hackster.io/darian-johnson/using-login-with-amazon-to-enable-dash-replenishment-7ec83c
3 - User logs into "Login With Amazon"
4 - Once authenticated, LWA passes the user to the DRS Registration Page. User selects the type of Cat Food for replenishment and enter shipping and payment details.
5 - Once setup is complete, DRS sends the user back to the Cat Feeder Registration Page with a refresh and access token.
6 - The Refresh token is pulled from the querrystring and send to AWS (via a Lambda Function and API Gateway) and saved for future user
Screen shots of the Registration Flow are below:
The Cat Feeder skill allows the user to:
- Feed the cat (an amount of 1, 2, 3 or 4 ounces)
- Ask when Alexa last fed the cat
- Ask if the cat has food
- Ask if the cat feeder needs to be refilled
Commands interact with the physical Cat Feeder by sending messages to the device via MQTT or getting status of the feeder via device shadow (more on this in the technical sections).
The Alexa skill is somewhat advanced and uses multiple AWS "services" (in addition to Lambda). For those new to Alexa development, I suggest you start first with the Alexa getting started guide. If you've developed Alexa skills before, please continue.
Upfront Configuration
The skill (written in node js) is build on the Alexa Skills Kits SDK for Node.js and requires the following code dependencies
- AWS IOT SDK
- UUID
- Request
One DynamoDB table is needed:
- Table Name: Cat_Feeder_Config
- Primary partition key: UserId (String)
One S3 bucket is needed:
- I called mine CatFeeder
The role used will need to have permissions to IoT and DynamoDB
First Time Use
When the skill is engaged, the user id (session.user.userId) is validated in the 'Cat_Feeder_Config' table. If there is no entry, then a UUID is created and provided to the user. The end user uses this UUID (referred to as a Topic ID) to configure his/her code on the physical Raspberry Pi device. See the example when the "FeedCat" intent is called:
"FeedCat": function (intent, session, response) {
getTopic(session.user.userId, function(value){
topic_id = value;
if (topic_id === 'notset'){
firstTimeConfig(intent, session, response);
}else{
feedCat(intent, session, response);
}
});
}
After Configuration is complete, all commands can be used. Details on all commands (and their interaction) can be found in the VUI diagrams.
Interacting with the Raspberry Pi
Interaction with the raspberry pi is through two channels.
MQTT - the Alexa skill sens a command to the RPI via an MQTT topic. Each user has a unique topic-id (this is the code provided during configuration)
- Topic-id/feed - tells the servo to feed the cat (1 sec for 1 oz, 2 sec for 2 oz, etc)
- Topic-id/picture - tells the RPI to take a photo and upload the photo to S3 (for use in the Alexa App)
Device Shadow- a mechanism to get the state of a physical device. I use the device shadow to let me know:
- The amount of food in the hopper - getCatFeederStatusAPI
- The state of the cat food bowl (percentage full) - getFoodBowlStatusAPI
//AWS IOT Call to get the device status for the cat feeder hopper (does it need to be refilled)
function getCatFeederStatusAPI(userID, eventCallback){
var params = {
thingName: topic_id /* required */
};
iotdata.getThingShadow(params, function(err, body) {
if (err) {
console.log(err, err.stack); // an error occurred
eventCallback(-1, 0);
}else{
payload = JSON.parse(body.payload);
var amount = payload.state.desired.hopper;
//check to see how much food is left in the hopper
if (amount === 1000) { //there is an error
eventCallback(-1, amount);
}else{
if (amount < 4){
eventCallback(0, amount);
}else{
eventCallback(1, amount);
}
}
}
});
}
//AWS IOT Call to get the device status for the cat food bowl (does it need to be refilled)
function getFoodBowlStatusAPI(userID, eventCallback){
var params = {
thingName: topic_id /* required */
};
iotdata.getThingShadow(params, function(err, body) {
if (err) {
console.log(err, err.stack); // an error occurred
eventCallback(-1, 0);
}else{
payload = JSON.parse(body.payload);
var amount = payload.state.desired.foodbowl;
eventCallback(0,amount)
}
});
}
Code/Skill Availability
The code is available in my Github library and the certified skill can be enabled here - https://www.amazon.com/Darian-Johnson-Cat-Feeder/dp/B01N2OS3M8/
Solution Build: Raspberry Pi Cat FeederThe CatFeeder was build using a Zervo Dry Food Dispenser. Any dispenser can be user - you just need to be sure that it is controlled by a paddle wheel. Your servo needs to be a continuous rotation servo.
Connecting the Servo
This was the hardest part of the physical build. I originally built my own shaft to connect with the servo and paddle wheel.
The measurements will be different depending on the dispense and servo you use. I used a 1.125 in. (diameter) X 1 in. (length) wooden dowel to connect the paddle wheel handle to the servo.
The end result (after a lot of gorilla glue and staples) is as follows:
Stabilizing the Dispenser and Servo
My prototype cat feeder used the original dry food dispenser housing and a wooden dowel to hold to servo in place. That was suitable for a prototype, but not for the finished product, so I designed a holder for the hopper using a two gallon bucket from Home Depot.
Step 1 - Placing the top on the bucket, cut a 5 in hole in the top and a large hold in the rear of the bucket (see images below).
Step 2 - Use the PVC fitting to create a "holder" for the hopper. Be sure to cut out part of the PVC if needed for your paddle wheel servo.
Step 3 - Cut a 1.75- 2 in hole in the front of the cat feeder. Insert the smaller PVC pipe in the hole and attach the top to the assembled Cat Feeder (this is why you have a large hole in the back.
Step 4 - Cut a wooden plank to 9 inches in length. Cut a groove into the plank to hold the servo in place. (Note: I also painted the cat feeder before this step).
Step 5 - Connect the Raspberry Pi, Camera, and Ultrasonic Sensor (taped to the inside top of the Cat Feeder Hopper.
Schematic for the connections to the RPI are below:
Code to control the Servo
The Servo is controlled using the CatFeeder.py script (see script in GitHib library). The Alexa skill passes in (via the MQTT topic) the amount of time for the servo to run. The command 'gpio -g pwm 18 200' starts the servo; 'gpio -g pwm 18 0' stops the servo.
def on_message(client, userdata, msg):
print("payload: " + msg.payload)
parsed_json = json.loads(msg.payload)
#Logic to take the selfie
if msg.topic ==topic + "/feed":
try:
runtime = parsed_json["amount"]
t_end = time.time() + runtime
while time.time() < t_end:
os_string = "gpio -g pwm 18 200"
os.system(os_string)
print("done spinning")
os_string = "gpio -g pwm 18 0"
os.system(os_string)
Code to control the photos
A standard usb webcam is used to take pictures of the cat food bowl.
The Alexa skill sends a message to the MQTT topic, which triggers taking the photo and uploading it to S3.
elif msg.topic == topic + "/photo": #logic to take the photo
try:
photo_name = parsed_json["photo"]
#delete old photos
os.system("rm Photos/*.jpg")
#take a photo using the name passed to us from mqtt message
photo = "Photos/" + photo_name + ".jpg"
os_string = "fswebcam --no-banner " + photo
os.system(os_string)
#use tinyS3 to upload the photo to AWS S3
S3_SECRET_KEY = 'secret key'
S3_ACCESS_KEY = 'access key'
conn = tinys3.Connection(S3_ACCESS_KEY,S3_SECRET_KEY,tls=True)
f = open(photo,'rb')
conn.upload(photo,f,'catfeeder')
Code to get status of the Cat Feeder Hopper and Cat Food Bowl
The DeviceShadow for the CatFeeder is updated every 15 minutes.
Amount in the Cat Feeder Hopper
The code calls the ultrasonic sensor to determine the distance between the top of the hopper and the food in the hopper. In order to help with the readings, I placed a 5 in (diameter) cardboard circle in the hopper to get accurate readings.
I then translate the distance into an amount (weight - measured in ounces) remaining in the hopper.
def update_hopper():
#Setup Ultrasonic Sensor
GPIO.setmode(GPIO.BCM)
SIG = 17
GPIO.setup(SIG, GPIO.OUT)
print "Distance Measurement In Progress"
GPIO.setup(SIG, GPIO.OUT)
GPIO.output(SIG, False)
print "Waiting For Sensor To Settle"
time.sleep(2)
GPIO.output(SIG, True)
time.sleep(0.00001)
GPIO.output(SIG, False)
while GPIO.input(SIG)==0:
pulse_start = time.time()
while GPIO.input(SIG)==1:
pulse_end = time.time()
pulse_duration = pulse_end - pulse_start
distance = pulse_duration * 17150
distance = round(distance, 2)
if distance > 20:
amount = 1000
elif distance > 18:
amount = 3
elif distance > 16.24:
amount = 3.9
elif distance > 14.55:
amount = 5
elif distance > 12.87:
amount = 6.1
elif distance > 11.23:
amount = 7.2
elif distance > 9.66:
amount = 8.3
elif distance > 8.09:
amount = 9.4
elif distance > 7.39:
amount = 10.6
elif distance > 5.84:
amount = 11.7
elif distance > 4.7:
amount = 12.8
elif distance > 3.9:
amount = 13.9
elif distance > 2.81:
amount = 15
print "Distance:",distance,"cm"
print "Amount:",amount,"oz"
return amount
Amount in the Cat Bowl Bowl
In order to determine the amount of food in the cat food bowl, I first had to "calibrate" the photo comparison program. I did this by taking photos of the bowl at stages of completely full, 50% full, and empty.
I then used root mean squared analysis (http://code.activestate.com/recipes/577630-comparing-two-images/) o to determine how similar (or difference) a picture was.
def update_foodbowl():
#Setup ImageChops variables
#empty_path = "/home/pi/CatFeeder/CompareImages/empty.jpg"
full_path = "/home/pi/CatFeeder/CompareImages/full.jpg"
compare_path = "/home/pi/CatFeeder/CompareImages/compare.jpg"
#Take Photo
os_string = "fswebcam --no-banner " + compare_path
os.system(os_string)
time.sleep(2)
#Calculate the root-mean-square difference between two images
im1 = Image.open(full_path)
im2 = Image.open(compare_path)
diff = ImageChops.difference(im1, im2)
h = diff.histogram()
sq = (value*(idx**2) for idx, value in enumerate(h))
sum_of_squares = sum(sq)
rms = math.sqrt(sum_of_squares/float(im1.size[0] * im1.size[1]))
#Determine if bowl is full (100) or empty (0)
if rms < 580:
amount = 100
elif rms < 610:
amount = 75
elif rms < 660:
amount = 50
elif rms < 680:
amount =25
elif rms > 680:
amount = 0
print "rms:",rms
print "Amount:",amount,"%"
return amount
Updating the Device Shadow
Both functions run every 15 minutes; the output values are saved in the DeviceShadow.
def update_shadows(sc):
foodbowl_amount = update_foodbowl()
hopper_amount = update_hopper()
JSONPayload = ('{"state":{"desired":{"foodbowl":' +
str(foodbowl_amount) +
',"hopper":' + str(hopper_amount) + '}}}')
Bot.shadowUpdate(JSONPayload, customShadowCallback_Update, 5)
s.enter(time_delay_in_secs, 1, update_shadows, (sc,))
Building your own Automated Cat FeederI've build the skill and code to be used with minimal changes
Step 1 - Build the Cat Feeder
The most difficult part of replicating this project will be building your feeder. I suggest you follow my rough guide above, or check out the following posts:
- David Bryan's blog - http://drstrangelove.net/2013/12/raspberry-pi-power-cat-feeder-updates
- Dog feeder build by Brandon Krahn and Hunter Soper -https://www.hackster.io/31542/automated-dog-food-dispenser-514136
Install the code
Use git to download the code. Your code should be in the home/pi/CatFeeder location. You should also have certificates downloaded to the home/pi/certs folder (the certificates and keys for uploading to S3 are here - https://s3.amazonaws.com/catfeeder/CatFeederAuth.zip)
You will also need to install the following modules:
- AWSIoTPythonSDK
- gpiozero
- paho.mqtt.client
- tinys3
- uuid
Step 2 - Enable the Alexa Skill and obtain a serial ID
- Enable the Cat Feeder skill in the Alexa App.
- Ask Alexa "tell cat feeder to configure".
- This will trigger Alexa to send you a code in the Alexa app.
- Update the updateFeederStatus.py and CatFeeder.py python scripts with (A) this code (update the topic variable at the top of the script), (B) the sercet and access keys you downloaded in the auth zip file.
At this point, your the skill should communicate with the Raspberry Pi.
Step 3 - Enable Amazon DRS (optional)
- Visit the following website to register with DRS - https://darianbjohnson.com/catfeeder
Comments