Raspberry Pi Birdhouse Software

In my last post I went over the components of the bird house camera I set up this year to get a better look at the local black-capped chickadees. Today I’ll talk about the software that makes it work.

Initial Setup

Being relatively new to Raspberry Pi, I started off with a full install of Raspbian Deskop. It’s more than I need, since I won’t have anything like a desktop setup, but with a 64GB SD card I wasn’t concerned about the space. At least I knew it was likely to have everything I needed, even if it had more than that as well. I could probably have used the Raspbian “lite” option and added anything not included.

Since I would be running the device headless, with no display or keyboard, I made sure to set it up to connect to my wifi network and allow SSH connections automatically. There are various sites describing how to do this, for example here.

I considered a few approaches to getting photos off of the Pi. Maybe I could set up Samba and connect to a shared folder of pictures? But I didn’t want the Pi to have to do any more work than necessary, since it would be runing off of a battery and solar panel. I don’t know how much power Samba and photo browsing would use, but I thought I could avoid it.

Instead I decided to use rsync to periodically copy files to a remote device– in this case, my desktop Mac. Take a photo, send it somewhere, and let me deal with browsing on a different device where power was less of an issue. Also, rsync was already installed and doesn’t need a server to work.

For that to be automatic I needed to be able to ssh from the Pi to my Mac without asking for a password every time. Thanks to ssh-keygen and ssh-copy-id, that’s easy.

Now I had my Pi ready to connect to my network and send photos to my Mac. What about taking those photos?

Look at the Birdie

The most obvious option for still photos on a camera-equipped Pi is the built-in raspistill command. It works pretty well, but I knew I couldn’t use it on its own. If nothing else I needed to take a photo and then copy it to my Mac. Other requirements might come up as I went along. I decided to write a script to handle the entire process.

Raspbian includes a Python library to work with the camera, which is great! Or would be if I knew Python. Fortunately there’s a lot of good documentation online about both the language and the library. Also I have past experience at getting stuff done in programming languages I don’t actually know.

The plan for the script is:

  1. Take a picture and save it in a file with a unique name that indicates when the picture was taken. For example, a photo taken on May 8 2022 at 11:25 AM would be named photo-2022-05-08-11-25-02.jpg.
  2. Use rsync to copy the files to my Mac.

That’s enough for now.

The format of the filenames might seem a little odd. Why “year-month-date-hour-minute-second”? It’s handy because it means that in a list of photo files, sorting by name is the same as sorting by date. It’s the only sane format to keep things in order.

The first version of the script looked like this:

#!/usr/bin/python

# Take a single still photo, save it in "Pictures", and then rsync it.
from time import sleep
from picamera import PiCamera, Color
from datetime import datetime
import subprocess
from os import chdir

chdir("/home/pi/Pictures")

with PiCamera() as camera:
	# Let camera warm up
	sleep(2)

	camera.resolution = (3280, 2464)
    # Annotate photo with the current time and date
	camera.annotate_text = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
	camera.annotate_text_size = 50
	camera.flash_mode = 'on'
	# This produces a monochrome image
	camera.color_effects = (128, 128)

    # Use a filename that includes the date and time
	filename = datetime.now().strftime("photo-%Y-%m-%d-%H-%M-%S.jpg")

	camera.capture(filename)
	print('capturing %s' % filename)

subprocess.call(["rsync", "-avrzul", "/home/pi/Pictures/", "neutrino.local:/Users/tph/Library/Mobile\ Documents/com~apple~CloudDocs/zeroone"])

If you know Python, please be kind, because I was figuring stuff out as I went along. Constructive feedback is welcome.

A few of things worth mentioning here:

  • The chdir means that all of the photos end up in a photo-specific directory, which is convenient.
  • I made the picture monochrome because, as I described in my last post, I’m using an IR sensitive camera. Color photos work but they don’t look good for my purposes.
  • The IR flash is always on, because I anticipated there wouldn’t me much light in the bird house
  • The rsync at the end sends files in the Pictures directory to my Mac (neutrino.local). The target directory is in my iCloud Drive folder, which means that the photos also appear on my iPad and iPhone with no extra work by me. (And zeroone is the Pi’s hostname, so I used that as the iCloud folder name).

To automate the photo process, I used good old Unix/Linix cron to run the script once every five minutes. I didn’t know when birds might be active so I guessed that I should have it run from 5AM to 7PM every day at first.

So. Many. Photos.

That worked pretty well, but it did mean that the pictures directory had a lot of photos pretty quickly. I added a new requirement to help deal with this:

  1. Make a daily directory of photos, to keep them organized. Photos from May 8, 2022 should be in a directory named 2022-05-08.

The revised script wasn’t very different. I replaced the chdir above with this:

photoDir = "/home/pi/Pictures"
todaysDirname = photoDir + "/" + datetime.now().strftime("%Y-%m-%d")
if not path.isdir(todaysDirname):
	mkdir(todaysDirname)
chdir(todaysDirname)

It’s still a lot of photos but this has made them a lot more manageable for me.

Reach Up for the Sunrise

So far I had only guessed about what times of day would be best for photos. Using 5AM to 7PM was OK at first. As the solstice approaches though, days get longer and birds may be active later on. I knew I’d have to adjust the schedule, and I thought there had to be a better way than just changing the stop time from 7PM to 8PM or 9PM or whatever.

Using Python came in handy, because although I stumbled some with the language, there are a ton of useful open source packages to add abilities. I found the Astral package, which provides details like sunset and sunset times for whatever latitude, longitude, and date you’re interested in.

Adding it was easy. Getting time zones and UTC conversions working was harder, because that stuff always is. Eventually I added this to the top of my script:

from astral import LocationInfo
from astral.sun import sun
from datetime import datetime, date
import pytz

city = LocationInfo("Colorado Springs", "USA", "America/Denver", 38.833889, -104.825278)
s = sun(city.observer, date=date.today())

# Use UTC now for time of day checks
utc = pytz.UTC
utcnow=utc.localize(datetime.utcnow())

if utcnow < s["dawn"] or utcnow > s["dusk"]:
    exit(0)

This looks up sun-related information for my location on the current date, then checks to see if the current time is before dawn or after dusk. If either of those is true, it stops. No photo is taken.

Again, I don’t really know Python, so constructive feedback on the above is welcome.

With that in place I updated the cron schedule to run the script every 5 minites at any time of day or night. The script can figure out if it should take a photo. So now, I’ll automatically get a schedule that follows longer or shorter days for as long as I keep it running.

Have You Tried Turning it Off and On Again?

All was well until one day when the Pi just wasn’t responding. I couldn’t tell why. Since the birds had not moved in yet, I took down the bird house and reopened it to have a look.

When I disconnected the power briefly and then plugged it back in, the Pi worked normally. So uh, WTF, basically?

At about the same time I was messing around with a separate camera-equipped Pi, for a different project. While testing, the terminal suddenly printed messages about a “kernel oops”, followed by a kernel panic. The Pi stopped responding until I power-cycled it. Was this what stopped the bird house’s Pi that day? I couldn’t be certain, but it fit the scanty details I had.

In the hope of keeping the kernel happy when it’s running in an inhabited bird house, I edited the root crontab (sudo crontab -e) on the Pi to reboot once per day.

0 5 * * * /usr/sbin/shutdown -r now

Will this help? Hell I don’t even know if a panic was the problem, much less if this is a real fix. It’s been operating normally for six weeks now though, so, maybe? Next year I’ll make sure to have an easily-accessible way to just power cycle the Pi.

One more thing…

Now that I was getting regular photos, I found that the bird house has a lot more light during the day than I expected. The flash isn’t necessary except for the earliest or latest photos of the day. I went back to the camera portion of the script and changed the flash setting from on to auto. That should save some power but still use a flash when it’s needed.

The Latest Script

After the above, the latest version of the script looks like this

#!/usr/bin/python3

from time import sleep
from picamera import PiCamera, Color
import subprocess
from os import chdir, mkdir, path
from astral import LocationInfo
from astral.sun import sun
from datetime import datetime, date
import pytz

city = LocationInfo("Colorado Springs", "USA", "America/Denver", 38.833889, -104.825278)
s = sun(city.observer, date=date.today())

# Use UTC now for time of day checks

utc = pytz.UTC
utcnow=utc.localize(datetime.utcnow())

if utcnow < s["dawn"] or utcnow > s["dusk"]:
    exit(0)

photoDir = "/home/pi/Pictures"

# Use local time for dir and file names
now = datetime.now()
todaysDirname = photoDir + "/" + now.strftime("%Y-%m-%d")
if not path.isdir(todaysDirname):
	mkdir(todaysDirname)

chdir(todaysDirname)

# Use a filename that includes the date and time
filename = now.strftime("photo-%Y-%m-%d-%H-%M-%S.jpg")

with PiCamera() as camera:
	# Let camera warm up
	sleep(2)

	camera.resolution = (3280, 2464)
    # Annotate photo with the current time and date
	camera.annotate_text = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
	camera.annotate_text_size = 50
	camera.flash_mode = 'auto'
	# This produces a monochrome image
	camera.color_effects = (128, 128)
	camera.capture(filename)
	print('capturing %s' % filename)

# rsync to a folder in iCloud Drive
subprocess.call(["rsync", "-avrzul", "/home/pi/Pictures/", "neutrino.local:/Users/tph/Library/Mobile\ Documents/com~apple~CloudDocs/zeroone"])

I don’t expect any more changes this year but we’ll see what happens.

For My Next Trick…

I have one more followup post in the works, where I’ll talk about the solar panel-based power setup for the Pi, and the ups and downs I’ve had with that.

In the meantime though, another photo of how things are going so far. The chickadees moved in and have been laying eggs for the past several days. As of today they have six!

Bird nest with six eggs