Raspberry Pi 5 This guide shows how to perform Thistle OTA on a Raspberry Pi 5. Start with a quick file‑update path (no A/B). Optionally, pre‑partition a Raspberry Pi OS image to add a rootfsB partition and pre‑enable headless SSH/Wi‑Fi before flashing.
Raspberry Pi 5 uses a Pi firmware boot chain loading from the FAT boot partition. For OTA A/B, set the bootloader in the Thistle config to RaspberryPi. Depending on the distro, the boot partition may be mounted at /boot or /boot/firmware.

Board setup (vendor/community docs)

  • Official imaging: Raspberry Pi Imager (“Use custom” supports local images)
  • General microSD flashing workflow: Prepare microSD

Quick path: File updates (no A/B)

Use Thistle’s file‑update mode to ship signed files and app bundles without changing partitions.
  1. Download tools
  • Workstation (TRH):
VER=1.5.0
curl -LO https://downloads.thistle.tech/embedded-client/$VER/trh-$VER-x86_64-unknown-linux-musl.gz
gunzip trh-$VER-x86_64-unknown-linux-musl.gz
chmod +x trh-$VER-x86_64-unknown-linux-musl && ln -sf trh-$VER-x86_64-unknown-linux-musl trh
  • Device (TUC on RPi5, aarch64):
# On the device (SSH or serial)
VER=1.5.0
curl -LO https://downloads.thistle.tech/embedded-client/$VER/tuc-$VER-aarch64-unknown-linux-musl.gz
gunzip tuc-$VER-aarch64-unknown-linux-musl.gz
chmod +x tuc-$VER-aarch64-unknown-linux-musl
sudo mv tuc-$VER-aarch64-unknown-linux-musl /usr/local/bin/tuc
  1. Initialize and release a simple file update on the workstation
export THISTLE_TOKEN=$(cat)   # paste token, Enter, then Ctrl-D
./trh init --persist="/boot"

mkdir -p example && echo "hello from thistle" > example/app
./trh prepare --target=./example --file-base-path=/opt/example
./trh release
  1. Copy config to device and run client
# On workstation — change host/user to what you configured when imaging
scp config.json <user>@<rpi-host-or-ip>:/tmp/tuc-config.json

# On device
sudo mv /tmp/tuc-config.json /boot/tuc-config.json
sudo tuc -c /boot/tuc-config.json
That’s it! File updates are fetched, verified, and installed atomically. Continue below only if you need rootfs A/B for boot‑time rollback.

Optional: Headless image prep (Raspberry Pi OS Bookworm, 64‑bit)

Prepare a Raspberry Pi OS image before flashing: create rootfsB, enable SSH, preconfigure Wi‑Fi, and set hostname. Docker is optional; any live Linux works.

1. Download the image

mkdir -p ~/rpi-img && cd ~/rpi-img
# Example: Raspberry Pi OS Lite (64‑bit) .img(.xz). Decompress if needed
# Use Raspberry Pi Imager or download a current Bookworm Lite image manually
# and set rpi5.img accordingly.
# e.g., with a downloaded file named 2025-05-13-raspios-bookworm-arm64-lite.img.xz
curl -LO https://downloads.raspberrypi.com/raspios_lite_arm64/images/2025-05-13/2025-05-13-raspios-bookworm-arm64-lite.img.xz
xz -d 2025-05-13-raspios-bookworm-arm64-lite.img.xz
mv 2025-05-13-raspios-bookworm-arm64-lite.img rpi5.img

2. Start a Linux environment in Docker (optional)

docker run --rm -it --privileged -v "$PWD":/work ubuntu:24.04 bash
Inside the container:
apt-get update
apt-get install -y parted e2fsprogs kpartx util-linux
cd /work

3. Expand image size & map partitions

truncate -s +8G rpi5.img                      # add 8 GiB free space
losetup -Pf --show rpi5.img                   # e.g. /dev/loop0
kpartx -av /dev/loop0                         # maps /dev/mapper/loop0p1, loop0p2

4. Resize rootfs (p2) to 6 GiB (example)

e2fsck -f /dev/mapper/loop0p2
resize2fs /dev/mapper/loop0p2 5900M   # adjust larger if your image requires more space
parted -s /dev/loop0 unit GiB resizepart 2 6
e2fsck -f /dev/mapper/loop0p2
resize2fs /dev/mapper/loop0p2

5. Create & format rootfsB

parted -s /dev/loop0 -a optimal mkpart primary ext4 6GiB 100%
kpartx -a /dev/loop0
mkfs.ext4 -F -L rootfsB /dev/mapper/loop0p3

6. Enable headless SSH + Wi‑Fi

Mount rootfs and boot partitions:
mkdir -p /mnt/root /mnt/boot
mount /dev/mapper/loop0p2 /mnt/root
mount /dev/mapper/loop0p1 /mnt/boot

a) Enable SSH at boot (RPi OS mechanism)

# An empty file named 'ssh' in the boot (FAT) partition enables SSH on first boot
: > /mnt/boot/ssh

b) Configure Wi‑Fi (wpa_supplicant via boot)

cat > /mnt/boot/wpa_supplicant.conf <<'EOF'
country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
    ssid="YOUR_SSID"
    psk="YOUR_PASSWORD"
}
EOF

c) (Optional) Pre‑create a user for headless login

On recent Raspberry Pi OS releases, a user is required. Either use Raspberry Pi Imager’s customization to set username/password, or add userconf.txt to the boot partition with a SHA‑512 hashed password:
# Replace 'pi' and the hash accordingly. Generate hash with: echo 'mypassword' | openssl passwd -6 -stdin
echo 'pi:$6$REDACTED_HASH' > /mnt/boot/userconf.txt

d) Set hostname

echo "rpi5" | tee /mnt/root/etc/hostname >/dev/null
# Ensure /etc/hosts maps 127.0.1.1 to the hostname
sed -i 's/^127.0.1.1.*/127.0.1.1\trpi5/' /mnt/root/etc/hosts || echo "127.0.1.1\trpi5" >> /mnt/root/etc/hosts
Unmount and clean up:
umount /mnt/boot /mnt/root
kpartx -dv /dev/loop0
losetup -d /dev/loop0
exit

7. Flash to SD

  • Using Raspberry Pi Imager: select “Use custom” and choose rpi5.img.
  • Or on macOS:
diskutil list                         # locate your SD (e.g., /dev/disk3)
diskutil unmountDisk /dev/diskN       # replace diskN with your SD identifier
sudo dd if=rpi5.img of=/dev/rdiskN bs=4m status=progress
sync

8. First boot

  • Partitions:
    • p1 → boot (FAT)
    • p2 → rootfs (6 GiB ext4)
    • p3 → rootfsB (ext4, label rootfsB)
  • SSH enabled automatically if ssh file present
  • Wi‑Fi auto‑connects if wpa_supplicant.conf was provided
  • Hostname set to rpi5 (unless changed)
Login:
ssh <user>@rpi5.local
# or: ssh <user>@<rpi-ip>

Thistle Tools

On the workstation and the device, download TRH/TUC as appropriate, then export your Project Access Token on the workstation.
# Workstation: TRH (prepare/release)
VER=1.5.0
curl -LO https://downloads.thistle.tech/embedded-client/$VER/trh-$VER-x86_64-unknown-linux-musl.gz
gunzip trh-$VER-x86_64-unknown-linux-musl.gz
chmod +x trh-$VER-x86_64-unknown-linux-musl && ln -sf trh-$VER-x86_64-unknown-linux-musl trh

# Device: TUC (install)
# Use the user/host you configured; example uses mDNS hostname
# Note: Use the aarch64 binary for 64‑bit images. If you chose a 32‑bit image,
# use the armv7 binary instead.
ssh <user>@rpi5.local 'VER=1.5.0; curl -LO https://downloads.thistle.tech/embedded-client/$VER/tuc-$VER-aarch64-unknown-linux-musl.gz && gunzip tuc-$VER-aarch64-unknown-linux-musl.gz && chmod +x tuc-$VER-aarch64-unknown-linux-musl && sudo mv tuc-$VER-aarch64-unknown-linux-musl /usr/local/bin/tuc && tuc --help | head -n 3'

# Workstation: Project token for TRH
export THISTLE_TOKEN=$(cat)

Initialize and Prepare Rootfs Release

./trh init --persist="/boot"
./trh prepare --target=myrootfs.img
./trh release

Device Configuration

Create tuc-config.json (adjust paths if your boot is /boot/firmware):
{
  "name": "rpi5",
  "persistent_directory": "/boot",
  "public_keys": ["<YOUR_PUBLIC_KEY>"],
  "bootloader": "RaspberryPi",
  "part_a": "/dev/mmcblk0p2",
  "part_b": "/dev/mmcblk0p3"
}
Copy configuration to the board and persist under /boot:
scp tuc-config.json <user>@rpi5.local:/tmp/
ssh <user>@rpi5.local 'sudo cp /tmp/tuc-config.json /boot/tuc-config.json'

Run Update

ssh <user>@rpi5.local
sudo tuc -c /boot/tuc-config.json
The device installs to the inactive slot and reboots. After reboot, run the client again to latch‑in the update.

Notes and Tips

  • If your distro mounts the boot partition at /boot/firmware, point persistent_directory there.
  • For headless provisioning on Bookworm, Raspberry Pi Imager customization is the simplest route (set SSH, Wi‑Fi, user, hostname). Manual ssh + wpa_supplicant.conf still works and is widely documented.
  • For more background on headless setup nuances on Bookworm vs Bullseye, see community articles (e.g., first‑run mechanisms and userconf.txt).