This repository contains the software component of a project to show custom messages on a Raspberry Pi (RPi) hooked up to an e-Print display (EPD), intended to act as a radio-controlled (RC) sticky note on an office door.
An important aspect of this project is that it is a testbed for using the blobman framework to construct the necessary software in as reproducible a fashion as can be managed.
This project is currently executed on a Raspberry Pi 4 Model B, which turns out to be way overpowered for this application.
The EPD is a 680×374 pixel, 7.5", black-and-white display from Waveshare (SKU 13187). This particular model, the "V1" seems not to be available anymore — instead there are options with the same resolution but three colors, or still monochrome but at higher resolution (the "V2").
There are two software components: a “hub” that runs on a persistent server, and a “display client” that runs on the RPi. Both are written in Rust. The Pi needs to be able to SSH into the hub server.
In order for this project to work, we need to create a custom Raspberry Pi OS image that will be flashed onto an SD card. Creating this image requires some low-level custom Linux-y work, so we do the operations inside a Vagrant virtual machine (“box”) for reproducibility.
The basis for the builder box is the 20200116.1 version of the
ubuntu/bionic64
Vagrant box. This can be initialized reproducibly with:
blobman provide bionic-server-cloudimg-amd64-vagrant.box
vagrant box add --name ubuntu_bionic64_20200116.1 bionic-server-cloudimg-amd64-vagrant.box
Running vagrant up
will initialize and provision the builder box. TODO:
the provisioning uses Apt commands and therefore touches the network! The goal
is to get blobman infrastructure such that we can provision the box even when
running fully offline, but we're not there yet.
Various configuration files need to be set up in the directory local/
.
- Create an SSH keypair that has no protective passphrase. This will be used
by the Pi to connect to the Hub. Run:
ssh-keygen -t ed25519 -f local/stickynote_ed25519_key
- Configure the client to talk to the hub. Copy
local/client-config.example.toml
tolocal/client-config.toml
and customize as appropriate for your hub setup, as described in comments in that file. - Configure the Raspberry Pi OS image build. Copy
local/pi-gen-config.example
tolocal/pi-gen-config
and customize as desired, as described in comments in that file.
Building the software requires a Rust toolchain and the Rust cross tool.
To build the hub, run:
cargo build --bin rc_stickynote_hub --release
If your server runs an OS that's not fully compatible with your build machine (e.g., it runs an older version of glibc), you can cross-compile it if you'd like:
cross build --target x86_64-unknown-linux-musl --release
To cross-compile the display client for the RPi, run:
cross build --target armv7-unknown-linux-gnueabihf --release
We build the Raspberry Pi OS image using scripts derived from the
pi-gen tool used for the official RPi images. The forked scripts
live in the semi-pi-gen/
directory.
To build the image, run:
vagrant ssh -c "cd /vagrant/semi-pi-gen && sudo STAGE_LIST='stage0 stage1 stage2' ./build.sh"
This will output the image file in
semi-pi-gen/deploy/YYYY-MM-DD-rc-stickynote.img
, where the YYYY-MM-DD
corresponds to today’s date. This command does not automatically rebuild the
custom software, so make sure to recompile beforehand if needed. The
from-scratch build process takes about 20 minutes on my machine.
To rebuild a new image with updated configuration or stickynote executables, you
can modify the STAGE_LIST
in the above command to contain just stage2
. Note,
however, that this will reuse the existing filesystem tree, so operations should
be idempotent. To avoid this, add CLEAN=1 PREV_ROOTFS_DIR=/pigenwork/stage1/rootfs
to the build.sh
environment, where
PREV_ROOTFS_DIR
specifies the rootfs
directory of the previous stage (e.g.,
stage1
if you're just rerunning stage2
).
To write the image to an SD card, first insert the card into your machine. If it
already contained an OS image, your computer might mount the partitions, which
we definitely don't want! Ummount anything like /run/$USER/media/boot
or
/run/$USER/media/rootfs
that appears.
Once that's done, the obvious GUI method didn't work for me and I don't feel like futzing around with this too much, so here's the command-line method to flash the disk. Make sure you specify the right device otherwise you might make your machine unbootable, etc., because we're writing data straight to a block device node!
sudo dd bs=4M if=semi-pi-gen/deploy/*-rc-stickynote.img of=/dev/mmcblk0 conv=fsync
If the dd
seems to exit instantly in a suspicious fashion, you may have a
stale /dev/mmcblk0
device. Try removing the SD card, deleting that device
node, and re-inserting.
To run a “simulator” version of the client that uses SDL to draw graphics to a window rather than the EPD, run:
cd displayer && cargo run --no-default-features --features=simulator -- client
This program will require a client configuration file, which should be placed
in ~/.config/rc-stickynote-client/rc-stickynote-client.toml
. This is the
same file format as used in local/client-config.toml
.
To mount the RPi OS image on your (Linux) machine and poke around its filesystem, you can mount it with a loopback device. The only tricky part is that the image file is a partitioned disk image, not just a single filesystem partition, so you need to be able to tell the mount program the right offset into the disk image to find the filesystem.
The pi-gen
build process outputs this information while creating the image. It
will emit some output that looks like this:
[19:26:46] Begin /vagrant/semi-pi-gen/export-image/prerun.sh
/boot: offset 4194304, length 268435456
/: offset 272629760, length 1635778560
In the above, the number 272629760 is the offset we want to know.
Alternatively, you can use fdisk
. If you run fdisk -lu /path/to/stickynote.img
, the
output will look something like:
Disk stickynote.img: 1.8 GiB, 1908408320 bytes, 3727360 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x8319fb68
Device Boot Start End Sectors Size Id Type
stickynote.img1 8192 532479 524288 256M c W95 FAT32 (LBA)
stickynote.img2 532480 3727359 3194880 1.5G 83 Linux
In this example, the blocksize is the 512 in the Units:
line, and the offset
in blocks is the "start" value, 532480, in the final output line. The offset
in bytes is the product of these two numbers, 512 * 532480 = 272629760
.
Once you have the offset, mounting the image is as simple as:
sudo mount -o loop,offset=$OFFSET /path/to/stickynote.img /tmp/img