Automatic boot setup
Makes the pedal start playing (standby loop, listening for the MIDI
controller) automatically when the Raspberry Pi is powered on, with no
screen or keyboard needed (section 1 of MASTER_SPECIFICATION.md).
Five pieces, applied in this order: flashing the OS and configuring its
first boot, the software this project depends on, an /etc/fstab entry
so the library USB mounts on its own, a systemd service that runs
src/main.py, and (as the final, deliberately-last step) a read-only
overlay on the Pi’s own root filesystem.
0. Flash Raspberry Pi OS and configure first boot
Using Raspberry Pi Imager (official tool, Windows/macOS/Linux):
- Choose OS → “Raspberry Pi OS (other)” → Raspberry Pi OS Lite
(Legacy, 32-bit) – what
MASTER_SPECIFICATION.mdnames as this project’s base OS. - Choose storage → your SD card.
- Before writing, click the gear icon (or press
Ctrl+Shift+X) to open the advanced options and set, in the same sitting:- Hostname: this project’s own reference deployment (and every
ssh pedalexample in this repo’s docs) usespedal. Use whatever you like, just substitute it mentally everywhere these docs saypedal/pedal.local. - Enable SSH, password authentication (or paste a public key if you’d rather not use a password at all).
- Username and password: your choice, no fixed requirement –
whatever you set here becomes
<YOUR_USER>in section 3 (pedal-core.service) later. - Configure WiFi (SSID, password, country) if this Pi won’t be on
Ethernet – or skip it and use Ethernet instead, which is what this
project’s own testing fell back to when WiFi got flaky (see
TESTING.md). - Locale/timezone/keyboard layout as appropriate.
- Hostname: this project’s own reference deployment (and every
- Write, then move the card to the Pi and power it on. First boot takes a minute or two longer than normal (partition resize, SSH host keys).
- From another machine on the same network:
ssh <your-username>@<hostname>.local.local(mDNS) resolution isn’t fully reliable in practice (known flaky over WiFi). If it doesn’t resolve, get the Pi’s IP from your router’s DHCP client list instead andssh <your-username>@<that-ip>.
1. Software prerequisites
Raspberry Pi OS (this project was developed against Lite) already ships
python3; install the rest:
sudo apt update
sudo apt install -y mpv ffmpeg ntfs-3g
mpv– drives all playback (src/core/player.py, over its JSON IPC socket; nopython-mpvor other third-party Python package needed).ffmpeg– only used byscripts/generate_fallback_standby.sh, to generate the local fallback standby video once.ntfs-3g– only needed if your library USB is formatted NTFS, as in this project’s own reference setup; use whatever driver matches your own USB drive’s filesystem instead (e.g.exfat-fusefor exFAT).
No requirements.txt: the Python side of this project (src/) is
standard-library only, deliberately, so there’s nothing to pip install.
2. Library USB — /etc/fstab
Add a line like this (get the real UUID for your own USB drive with
sudo blkid /dev/sda1, or whatever device it shows up as):
UUID=07C1339846657D95 /media/usb ntfs-3g ro,nofail,x-systemd.device-timeout=10 0 0
ro: mounted read-only by default, matching how this project always operates day to day (section 2 ofMASTER_SPECIFICATION.md– the library USB must never be auto-formatted or have files auto-deleted). Remount read-write by hand (sudo mount -o remount,rw /media/usb) only for deliberate library management, then remountroagain afterward.nofail+x-systemd.device-timeout=10: if the USB isn’t plugged in at boot, don’t hang the boot sequence waiting for it – give up after 10s and continue.pedal-core.service(below) handles the USB still being absent after that by falling back to the local standby video (seesrc/core/player.py).
Test the line without rebooting before trusting it:
sudo mount -a
mount | grep /media/usb
3. The service — pedal-core.service
sudo cp systemd/pedal-core.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now pedal-core.service
Check it:
sudo systemctl status pedal-core
journalctl -u pedal-core -f
The unit as committed here uses placeholders – <YOUR_USER> and
<YOUR_USB_UUID> – in User= and ExecStart=. Replace both with your
own values before copying it in (this project’s own reference deployment
uses User=hesner, checkout path /home/hesner/chocolatepi, and
UUID 07C1339846657D95, matching the /etc/fstab entry from section 2).
Edit them again later if the checkout path, user, or library USB drive
ever changes.
Restart=always means the service keeps retrying every 5s if it exits
for any reason (M-VAVE not enumerated yet, USB not mounted yet, …) –
there’s no keyboard/screen to restart it by hand on a real appliance, so
it needs to recover on its own.
Deliberately uses Wants=/After= for the USB mount, not
RequiresMountsFor=: the latter is a hard dependency, so unplugging the
USB while the service is running makes systemd stop the whole service
(video, audio, everything) instead of letting Player fall back to the
local standby video the way it’s designed to – confirmed the hard way,
by unplugging the USB during a live test and getting neither the real
nor the fallback standby on screen, because nothing was running at all.
Wants=/After= only affects the order things start in at boot; it
never tears this service down because of what the USB does afterward.
USB behavior (final decision)
Approved operational policy: the musician powers the Pi off, swaps the USB’s content on a separate computer, plugs the USB back into the Pi, and powers the Pi back on. Editing the library while the show is actively running is explicitly not a supported workflow.
A fully automatic hot-swap (unplug, edit, replug, no reboot) was
attempted via udev + a remount service and separately via background
polling; both were abandoned as unreliable on this hardware/filesystem
combination – see CHANGELOG.md/git history if you’re tempted to
rebuild one.
Decided final behavior, implemented in Player (src/core/player.py):
- Whether the library USB is present is checked exactly once, at
startup – via
/dev/disk/by-uuid/<usb_uuid>(the same UUID as in/etc/fstaband--usb-uuid). Checking--standby’s path or its mount point directly was tried first and found unreliable under the root filesystem overlay below (see the docstring onPlayer._usb_device_is_present()for specifics). - USB missing at boot: the local fallback standby plays instead (“Please insert the USB into the Raspberry Pi”).
- USB removed while already running: not detected – the system keeps showing/playing whatever it already had. Recovering (or first picking up a library update made while off) always requires a reboot; there is no supported way to make it happen without one.
4. Read-only root filesystem (final lock-down step)
Requirement: it must be safe to power the Pi off at any moment (pull the
plug) without risking corruption of its own filesystem – this appliance
has no shutdown button. MASTER_SPECIFICATION.md’s read-only-library
requirement (section 2) already covers the USB; this covers the Pi’s own
SD card.
Enabled via Raspberry Pi OS’s built-in overlay filesystem (raspi-config
→ Performance Options → Overlay File System):
sudo raspi-config nonint do_overlayfs 0 # enable (1 to disable again)
Then edit /boot/firmware/cmdline.txt (remount it rw first: sudo
mount -o remount,rw /boot/firmware) and append :recurse=0 to the
overlayroot=tmpfs parameter it just added, so the line reads
overlayroot=tmpfs:recurse=0. Remount /boot/firmware back to ro
and sudo reboot.
recurse=0 is required, not optional: the default (recurse=1)
wraps every mount in its own overlay, including /media/usb – and that
auto-generated overlay has no nofail, so booting without the library
USB dropped straight into systemd emergency mode (no SSH, unrecoverable
on a headless appliance) instead of falling back to the local standby
the way section 2/3 intend. recurse=0 limits the overlay to / only;
/media/usb and /boot/firmware already have their own ro in
/etc/fstab regardless, so they lose no protection.
After reboot, / is an overlay (mount | grep ' / ' shows
lowerdir=/media/root-ro – the real SD card, mounted ro – with
upperdir=/media/root-rw on tmpfs, i.e. RAM). Every write during
normal operation lands in RAM and is discarded on every reboot; the SD
card itself is never touched, so an abrupt power loss can’t corrupt it.
Apply this last, once there’s no more Pi-side development expected:
anything written to the Pi while the overlay is active (including
syncing a new version of this code) is lost on the next reboot, since it
only ever lands in the RAM-backed upper layer. To make further changes:
temporarily disable (do_overlayfs 1, reboot), make and verify the
changes normally, then re-enable – do_overlayfs 0 resets
overlayroot=tmpfs without :recurse=0, so redo that edit to
cmdline.txt every time before rebooting back into it.
Accepted trade-off, confirmed acceptable: ~/pedal-core.log and the
systemd journal become ephemeral too (wiped every reboot, along with
everything else on /) – acceptable since they’re only ever used
live, during an active debugging session over SSH, not read back after
the fact.
Maintenance / physical access
Running the service means mpv permanently occupies the HDMI output
(see --force-window=yes in src/core/player.py) – this is a
software-level thing, not an OS-level lockout. To get the physical
console/login back for maintenance:
sudo systemctl stop pedal-core
SSH access is unaffected either way, regardless of what the service is
doing. If the overlay filesystem (section 4) is active, note that
sudo commands still work as usual – only writes to / and
/boot/firmware land in the RAM-backed overlay instead of the real SD
card, they don’t fail.