<- Setting up an automatic camera for my cat feeder

Because the world definitely needed more cat GIFs

posted 2024-04-28
An orange cat eating food out of a bowl

This is Goblin, one of my cats. She is very stupid.

In my house, we own an automatic cat feeder. This feeder has two bowls and dispenses kibble on a timer. A bonus of this feeder is that it has a camera on the front, allowing you to record motion of the cats eating and save it to a microSD card located inside of the feeder.

A screenshot of the mobile app, showcasing the camera display

I wanted to take these videos and post them to my friends automatically, inspired by the Hello Street Cat streams and restreams. I researched the brand, coming to the conclusion that a previous model used Tuya but the one I own uses a new server. I planned to hook it up to my Home Assistant instance, but nobody had written an integration yet.

After a short bit of opening the mobile app in JADX and seeing the log messages being purely in Chinese, I decided I didn’t want to reverse engineer this protocol for the camera and I’d instead opt for a second one pointed at the feeder.

I found a spare Raspberry Pi laying around, sacrificed an old webcam, and bought a microSD card at the local Target. After some quick tape on the nearest cat tree:

A Raspberry Pi and webcam sitting on top of a cat tree

Writing all of the code

First of all, I knew that I didn’t want a 24/7 live stream like Hello Street Cat, given that this will be a stream of my kitchen and I don’t live alone. I enjoyed the model of motion detected clips that the mobile app for the feeder does, but I didn’t want it to detect any motion if someone was passing by or it was a false positive. I decided that I would upload every clip for “staging” into a private Discord channel, and then require manual approvals to send to my friends.

Luckily, the steps of both detecting motion and capturing clips can be solved by the program just called motion. This was a simple sudo apt install motion on my Pi and it detected the USB camera without problem (presumably because of some driver in the Linux kernel carrying the support).

After enabling the web control (and turning off localhost-only access) in the config file, I was able to see a preview of the camera from the web server on port 8081. It worked!

The next step is to instruct motion to capture videos when it detects motion. This is as simple as setting movie_output on in the config file. Reading the file, there is also on_movie_end to specify a command to run when a movie finishes recording.

The architecture I worked out was:

I decided to go with TypeScript for this project, as it was a language I’ve written both web servers and Discord bots in. I didn’t really need type safety, and the compile steps take super long on the Pi, but I’m fine with it.

The upload script looks pretty simple:

#!/usr/bin/env sh
set -e

tmpfile="/tmp/nya$(date +%s).gif"
# Clean up even if the connection fails
trap "rm -rf $tmpfile" EXIT
# I stole this off of StackOverflow
ffmpeg -y -i $1 -vf "fps=10,scale=320:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 $tmpfile
# Upload it, making sure to retry if it fails (like the server restarting)
curl --connect-timeout 5 --max-time 10 --retry 500 --retry-delay 5 --retry-max-time 40 http://localhost:8300/stage -X POST -H "Content-Type: multipart/form-data" -F "image=@$tmpfile"
# Delete the original video to save space
rm -rf $1

Then, in the server, I just forward that .gif to a Discord bot (effectively using Discord as a CDN, lol):

router.post("/stage", async (ctx) => {
  const file = ctx.request.files!["image"] as import("formidable").File;
  const data = fs.readFileSync(file.filepath);
  await bot.createMessage(config.stagingChannel, {
    attachments: [
      {
        filename: Date.now() + ".gif",
        file: data
      }
    ]
  });
  ctx.status = 204;
  ctx.body = "";
});

I note that formidable makes a copy of the file upload in /tmp, which is literally exactly what I’m doing before I upload it, so I’m wasting double the memory by doing this (when I could just be passing the file path). But I don’t care.

I then wrote a simple handler that checks when I reply to a gif the bot posted with the content “publish”. It then takes the gif and forwards it to the public channel, reacting with a checkmark to my message and the original message. If a reaction is already present on the gif from the bot (meaning it already uploaded it), it skips it and does nothing.

For testing, I replayed a video that motion captured earlier that morning by calling the script manually. The result:

A screenshot of a Discord bot posting a cat gif

It works!

The code grows and grows

My friends seem to really enjoy the concept of this. As I mentioned it to them, though, some asked to bring it to their Discord server. Didn’t really plan for that, but I can do it! I changed the config file format to take a string array instead of a single string for the channel IDs to post in. It loops through every channel and attempts to post it.

…Then, another friend asked to bring it into their server, but they didn’t want to invite the bot (given it had message content intents) and instead asked me to add webhook support. That’s okay! Just a simple await fetch. …And then another friend asked for a webhook! Okay, yep, yeah, this is getting a little bit out of hand for a thing that posts gifs but I can make it support multiple webhooks too.

And then a friend asked me to have the bot DM them personally when a new gif is posted so they don’t miss it. Sigh. I planned ahead for this one and made it an array to start with.

I also added integration with the Mastodon API, so anyone who wants to see it can follow my cats on the fediverse. I considered adding Twitter support, but I feared I didn’t have a spare phone number to use in case the account got locked. I also got asked to add Bluesky support but it looks like they don’t have the capability for custom gifs(?????).

I was also asked for a website, but I don’t want to deal with another service running on another server (especially when my infrastructure is all Nix in the middle of current events). Maybe some day.

It now takes like ten seconds to upload the gif everywhere, but I think it’s a fun idea. I hope people enjoy staring at my cats or something.