Skip to the content.

Automatic boot setup

Leer en español

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):

  1. Choose OS → “Raspberry Pi OS (other)” → Raspberry Pi OS Lite (Legacy, 32-bit) – what MASTER_SPECIFICATION.md names as this project’s base OS.
  2. Choose storage → your SD card.
  3. 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 pedal example in this repo’s docs) uses pedal. Use whatever you like, just substitute it mentally everywhere these docs say pedal/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.
  4. 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).
  5. 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 and ssh <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

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

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):

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.