Hot plugging USB devices in balenaOS

Tomás Migone
The Startup
Published in
7 min readJul 31, 2019

--

Last week the team behind the balenaDash project released an update that added photo slideshow capabilities to its list of features. The new version allows you to turn your Raspberry Pi into a physical photo album that uses cloud hosted solutions to manage the pictures it displays (supported platforms include Google, Apple and Dropbox photo apps).

I’ve been wanting to build something like this for my grandparents for some time. As an 80 year old hobbyist photographer, my grandfather accumulated loads of family photos. Unfortunately he can’t really handle ‘cloud technology’ that well, so he relies on ‘old school’ storage techniques: CDs, DVDs and most recently external hard drives and USB sticks.

Before building one for him I needed to add usb storage support to balenaDash. The idea was that he would load a bunch of photos on a USB stick, plug it into the Raspberry Pi and they would automatically get picked up and displayed on the screen.

I’ll show you how I did it while covering the basics you need in order to work with dynamically plugged devices (such as a USB stick) when using balenaOS.

Udev in Linux systems

First things first, what happens when you plug in a USB device? You might have heard the phrase “Everything is a file in Linux”. Most hardware devices appear in the file system as special files that represent them; USB drives are not the exception. When you plug in a USB stick a file will be created on the /dev folder. That file will act as an identifier and handler, a way for any interested party to access the device.

Udev is the subsystem in charge of managing this special files, acting as a device manager for the linux kernel. Whenever a device is added or removed udev gets notified by the kernel so it can dynamically manage this files inside /dev. The udev daemon also provides a hook-like feature: every received event is matched against a set of configurable rules which allow you to specify a script or program to run if your conditions are met.

If you want to learn more about udev you can check out this excellent guide. For this project though this very simple overview will be enough. The main takeaway here is that our solution will consist of two parts:

  • A custom udev rule that fires whenever we plug in a USB drive and runs a custom script
  • A script that copies files from the USB stick to our file system

Udev in balenaOS

Since it’s based on Linux everything we discussed about udev also applies to balenaOS. I prepared a sample multi-container project to see this in action on a Raspberry Pi. We will be focusing on each of the project’s containers one by one.

If you need help getting started with balena deployments, you might want to check out this guides:

After you are done deploying the sample project, let’s connect a USB stick and see what happens on the Host OS — before plugging (top) / after plugging (bottom):

Awesome. Udev is working, has assigned the name /dev/sda to our USB stick and /dev/sda1 to the only partition on it (more on linux device naming here).

This is only valid at the Host OS level though. It’s important to remember that the Host OS is just a host for our container based application(s). By default, any containers we create will be isolated. This means that inside a container we won’t have access to hardware devices since udev events won’t be passed down from the host. You can verify this by running ls /dev inside the ‘udev-default’ container on the sample project (don’t peek into the other containers yet!).

How do we solve this? Dealing with udev on containers is hard so the good folks at balena made it easy for us as long as we work with one of their base images. You can read about this in detail on their website, but the key points are:

  • Use any of the base images for your Dockerfile. This images will run a script which does all the difficult stuff for you, including udev initialization (check out the script for balenalib/raspberrypi3-alpine for example: entry.sh).
  • Set the udev flag by adding ENV UDEV=onto your Dockerfile.
  • Run your container in privileged mode by adding privileged: true to your docker-compose file.

Just by following these simple rules we get udev events on our containers just as we wanted. If you are not a believer, you can now at ‘udev-enabled’ container and see for yourself!

Writing Udev rules

We are now ready to write a custom rule that handles the ‘I just plugged in a USB stick’ event. The only issue is that there are probably more than 10 different events being fired when you plug the stick.

In order to find the one we are interested in we can use udevadm utility. Running the following command udevadm monitor — environment will give you a real time monitor of udev events. If you run the command and then plug the USB stick, you will see that each event has properties or keys associated with it; we will use this keys to identify our event.

Depending on what you want to do you could theoretically use any event you see on the monitor. In our case since we want to mount the USB stick and access its contents, we need to know its device name. Taking a closer look, out of the 10+ events only one mentions /dev/sda1, so that’s our pick.

This approach will usually work for USB storage but it requires prior knowledge of the device name. A better solution is to identify the event by combining multiple key/value pairs. There are many ways of doing this. The only thing you need to consider is that the rule will match against all events, so you probably want to pick a combination that produces a unique event as a result (or else your script will run more than once). If you want to dive deeper into udev rules here is a good article you can read.

Highlighted in red are the key/values I picked:

Writing the rule is as simple as creating a *.rules file and copying it on this directory: /etc/udev/rules.d/(make sure to add this to your Dockerfile). The contents of the file will be comma separated key matches with a RUN action at the end:

udev.rules

A few pointers on writing udev rules (read more here and here):

  • There are different types of keys ( ENV{} and ATTR{}for example).
  • ENV{}keys get passed as environment variables to the program or script you provide on the RUN action.
  • All match keys should match for the rule to be fired.
  • There are multiple comparison operators ( ==, !=). Pattern matching via *, ? and [] is supported.
  • RUN does not execute the given command under a context of a shell. Ensure your script is executable and starts with the appropriate shebang.
  • The lack of shell context means operators |, > or >> won’t work unless you wrap your command with the appropriate shell.

You can test all this with the sample project by looking into the ‘udev-rule-hello-world’ container. A message is written to /home/usb.log every time you plug in a USB stick.

Writing the ‘copy’ script

Last but not least, it’s time to write the script that gets the contents of the USB drive. You can customize the script, in this case the tasks we need are:

  1. Mount the device using mount
  2. Copy the files to /usbstorage folder using rsync
  3. Unmount the device using unmount

The script takes advantage of the environment variables that are set by udev to identify the device. Also, remember to add the script execution to your udev rule, and account for it on the Dockerfile.

For a working example up to this point, check out ‘usb-copy’. This container will copy all files to /usbstorage when you plug in a USB stick:

Integration with balenaDash

A quick way to integrate what we’ve done with balenaDash would be to add the ‘usb-copy’ container to the project. We just need to create a named volume that links the /usbstorage folder on ‘usb-copy’ with the ‘photos’ container.

Since the ‘usb-copy’ container is not doing much (you’ll notice the CMD is just an infinite loop) I opted for adding the udev integration to the ‘photos’ container directly. Thus removing the need for a named volume.

To keep this post short-ish I won’t go into further details. The changes made to balenaDash are simple though, you can check it out at: https://github.com/tmigone/balena-dash.

TIP: To configure balenaDash on USB mode just set GALLERY_URL=USBDRIVE. The ‘photos’ app picks up new photos for display with a frequency given by CRON_SCHEDULE, make sure to update that accordingly.

--

--

Tomás Migone
The Startup

Electronic engineer. Software & hardware enthusiast 🇦🇷.