Skip to content

Pi Image Setup Guide

Jasper van Loenen edited this page Apr 29, 2026 · 29 revisions

Little Printer Zigbee Bridge - Pi Image Setup Guide

This page describes how I create the image file used on the Raspberry Pi. Regular users don't need this, but I've put this here for reference and because others might find this useful.

All steps are performed on a Linux machine with the Raspberry Pi SD card mounted via a card reader. The root partition is assumed to be mounted at /media/$USER/rootfs and the boot partition at /media/$USER/bootfs.

Bookworm notes: Several things changed in Raspberry Pi OS Bookworm compared to older versions:

  • The default pi user no longer exists in fresh images and must be created manually
  • WiFi is managed by NetworkManager instead of wpa_supplicant
  • The boot partition is mounted at /boot/firmware instead of /boot
  • systemctl enable does not work inside a chroot - symlinks must be created manually

1. Find The SD Card Mount Points

lsblk

Identify the device (e.g. /dev/sda) and note where the partitions are mounted, for example:

  • Root partition -> /media/$USER/rootfs
  • Boot partition -> /media/$USER/bootfs

If the partitions are not mounted yet:

sudo mkdir -p /media/$USER/rootfs /media/$USER/bootfs
sudo mount /dev/sda2 /media/$USER/rootfs
sudo mount /dev/sda1 /media/$USER/bootfs

2. Add WiFi Config Template to Boot Partition

The user can add/edit this file to setup their WiFi without having to log into the Pi. A service will look for this file on boot and update the WiFi settings accordingly.

The boot partition is FAT32, so this file is editable on Windows, Mac, and Linux without needing root.

Create /media/$USER/bootfs/wifi.txt:

# Enter your WiFi credentials below, then save the file.
# This file will be deleted automatically on first boot for security.

SSID=YourNetworkName
PASSWORD=YourPassword

# Optional: set your country code (default: GB)
# COUNTRY=GB

3. Set Up QEMU and Enter Chroot

Use QEMU to emulate the Pi's ARM architecture on your desktop, allowing you to work inside the SD card image as if you were on the Pi itself.

# Install QEMU on your desktop
sudo apt install qemu-user-static binfmt-support

# Copy the QEMU ARM binary into the image so it can be used inside chroot
sudo cp /usr/bin/qemu-arm-static /media/$USER/rootfs/usr/bin/

# Mount required virtual filesystems
sudo mount --bind /dev /media/$USER/rootfs/dev
sudo mount --bind /proc /media/$USER/rootfs/proc
sudo mount --bind /sys /media/$USER/rootfs/sys

# Also mount the boot partition so /boot/firmware is accessible inside chroot
sudo mount --bind /media/$USER/bootfs /media/$USER/rootfs/boot/firmware

# Chroot in
sudo chroot /media/$USER/rootfs

# Fix DNS so apt can reach the internet
echo "nameserver 1.1.1.1" > /etc/resolv.conf
echo "little-printer-zigbee-bridge" > /etc/hostname

You are now inside the Pi's filesystem. All following commands in sections 4, 5, 6, and 7 run as if on the Pi itself.


4. Create the Pi User

Bookworm no longer ships with a default user. Without a pre-created user, the Pi will stop on first boot and prompt interactively to create one. Creating the user here prevents that prompt from appearing -- userconfig.service checks for an existing user and skips itself automatically.

# Create the pi user
useradd -m -s /bin/bash pi
echo "pi:littleprinterlives" | chpasswd

# Add to the standard Raspberry Pi groups
usermod -aG adm,dialout,cdrom,sudo,audio,video,plugdev,games,users,input,netdev,gpio,i2c,spi pi

5. Add the Required Files

5a. WiFi setup script

On Bookworm, WiFi is managed by NetworkManager. The setup script reads wifi.txt from the boot partition and creates a NetworkManager connection file.

Create /usr/local/bin/wifi-setup.sh:

cat > /usr/local/bin/wifi-setup.sh << 'EOF'
#!/bin/bash
set -e

WIFI_CONFIG="/boot/firmware/wifi.txt"
FLAG="/etc/wifi-configured"
NM_CONN_DIR="/etc/NetworkManager/system-connections"

# Only run once
if [ -f "$FLAG" ]; then
    exit 0
fi

# Check config file exists
if [ ! -f "$WIFI_CONFIG" ]; then
    echo "No wifi.txt found, skipping WiFi setup."
    exit 1
fi

# Parse credentials
SSID=$(grep -E "^SSID=" "$WIFI_CONFIG" | cut -d= -f2-)
PASSWORD=$(grep -E "^PASSWORD=" "$WIFI_CONFIG" | cut -d= -f2-)
COUNTRY=$(grep -E "^COUNTRY=" "$WIFI_CONFIG" | cut -d= -f2-)
COUNTRY=${COUNTRY:-GB}

if [ -z "$SSID" ] || [ -z "$PASSWORD" ]; then
    echo "SSID or PASSWORD missing in wifi.txt"
    exit 1
fi

# Set WiFi country
raspi-config nonint do_wifi_country "$COUNTRY"

# Generate a UUID for the connection (uses kernel, no extra packages needed)
UUID=$(cat /proc/sys/kernel/random/uuid)

# Create NetworkManager connection file
mkdir -p "$NM_CONN_DIR"
cat > "$NM_CONN_DIR/preconfigured.nmconnection" << NMEOF
[connection]
id=preconfigured
uuid=$UUID
type=wifi
autoconnect=true

[wifi]
mode=infrastructure
ssid=$SSID

[wifi-security]
auth-alg=open
key-mgmt=wpa-psk
psk=$PASSWORD

[ipv4]
method=auto

[ipv6]
addr-gen-mode=default
method=auto
NMEOF

# NetworkManager silently ignores connection files with wrong permissions
chmod 600 "$NM_CONN_DIR/preconfigured.nmconnection"
chown root:root "$NM_CONN_DIR/preconfigured.nmconnection"

# Shred and remove the config file for security
shred -u "$WIFI_CONFIG"

# Mark as done
touch "$FLAG"

echo "WiFi configured for SSID: $SSID"
EOF

chmod +x /usr/local/bin/wifi-setup.sh

5b. WiFi setup systemd service

Create /etc/systemd/system/wifi-setup.service:

cat > /etc/systemd/system/wifi-setup.service << 'EOF'
[Unit]
Description=First-boot WiFi configuration
Before=network.target NetworkManager.service
ConditionPathExists=!/etc/wifi-configured

[Service]
Type=oneshot
ExecStart=/usr/local/bin/wifi-setup.sh
RemainAfterExit=yes
SuccessExitStatus=0 1

[Install]
WantedBy=multi-user.target
EOF

5c. First-boot Python dependency installation service

Python dependencies are installed on first boot on the real hardware, which avoids architecture issues that can occur when installing via chroot emulation.

Create /etc/systemd/system/first-boot-setup.service:

cat > /etc/systemd/system/first-boot-setup.service << 'EOF'
[Unit]
Description=First-boot Python dependency installation
After=network-online.target wifi-setup.service
Wants=network-online.target
Requires=wifi-setup.service
ConditionPathExists=!/etc/first-boot-complete

[Service]
Type=oneshot
User=root
ExecStart=/bin/bash -c 'cd /home/pi/little-printer-zigbee-bridge && pip3 install -r bridge/requirements.txt --break-system-packages'
ExecStartPost=/bin/touch /etc/first-boot-complete
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

5d. Little Printer bridge systemd service

Create /etc/systemd/system/little-printer-zigbee-bridge.service:

cat > /etc/systemd/system/little-printer-zigbee-bridge.service << 'EOF'
[Unit]
Description=Little Printer Zigbee Bridge Script
After=network-online.target wifi-setup.service first-boot-setup.service
Wants=network-online.target
Requires=wifi-setup.service first-boot-setup.service

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/little-printer-zigbee-bridge
ExecStartPre=/bin/sh -c 'until ping -c1 1.1.1.1 > /dev/null 2>&1; do sleep 2; done'
ExecStart=/usr/bin/env python3 -m bridge.main --sirius
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

5e. SSH key regeneration service

Only needed if the Pi has been booted before!

cat > /etc/systemd/system/regen-ssh-keys.service << 'EOF'
[Unit]
Description=Regenerate SSH host keys on first boot
Before=ssh.service
ConditionPathExists=!/etc/ssh/ssh_host_rsa_key

[Service]
Type=oneshot
ExecStart=/usr/bin/ssh-keygen -A
ExecStartPost=/bin/systemctl disable regen-ssh-keys.service

[Install]
WantedBy=multi-user.target
EOF

6. Enable All Services

systemctl enable does not work inside a chroot because there is no running systemd instance. Instead, create the symlinks manually - this is all that systemctl enable does anyway.

mkdir -p /etc/systemd/system/multi-user.target.wants
mkdir -p /etc/systemd/system/network-online.target.wants

ln -sf /etc/systemd/system/wifi-setup.service \
       /etc/systemd/system/multi-user.target.wants/wifi-setup.service

ln -sf /etc/systemd/system/first-boot-setup.service \
       /etc/systemd/system/multi-user.target.wants/first-boot-setup.service

ln -sf /etc/systemd/system/little-printer-zigbee-bridge.service \
       /etc/systemd/system/multi-user.target.wants/little-printer-zigbee-bridge.service

# Only if Pi was booted before:
ln -sf /etc/systemd/system/regen-ssh-keys.service \
       /etc/systemd/system/multi-user.target.wants/regen-ssh-keys.service

ln -sf /lib/systemd/system/NetworkManager.service \
       /etc/systemd/system/multi-user.target.wants/NetworkManager.service

ln -sf /lib/systemd/system/NetworkManager-wait-online.service \
       /etc/systemd/system/network-online.target.wants/NetworkManager-wait-online.service

Verify:

ls /etc/systemd/system/multi-user.target.wants/

7. Install System Dependencies

# Update and upgrade packages
apt update
apt upgrade

# Install system dependencies
# libopenjp2-7 is required by Pillow; python3-pip is used by the first-boot service
apt install libopenjp2-7 python3-pip

# Exit the chroot
exit

Python packages from requirements.txt are installed automatically on first boot by first-boot-setup.service once the Pi is connected to WiFi.


8. Clean Up QEMU

Back on your desktop:

sudo umount /media/$USER/rootfs/boot/firmware
sudo umount /media/$USER/rootfs/dev
sudo umount /media/$USER/rootfs/proc
sudo umount /media/$USER/rootfs/sys
sudo rm /media/$USER/rootfs/usr/bin/qemu-arm-static

9. Clean Private Data Before Creating Image

This is only needed if you've been logged in to the device.

Wipe existing WiFi credentials

sudo rm -f /media/$USER/rootfs/etc/NetworkManager/system-connections/*.nmconnection

Restore wifi.txt to the blank template (or confirm it only contains placeholder values - see step 2).

First-boot flags

sudo rm -f /media/$USER/rootfs/etc/wifi-configured
sudo rm -f /media/$USER/rootfs/etc/first-boot-complete

This ensures WiFi setup and Python dependency installation run again on first boot for the new user.

SSH host keys

sudo rm -f /media/$USER/rootfs/etc/ssh/ssh_host_*

The regen-ssh-keys.service will generate fresh keys on first boot.

Shell history

sudo rm -f /media/$USER/rootfs/root/.bash_history
sudo rm -f /media/$USER/rootfs/home/pi/.bash_history

Machine ID

sudo rm -f /media/$USER/rootfs/etc/machine-id
sudo rm -f /media/$USER/rootfs/var/lib/dbus/machine-id
sudo touch /media/$USER/rootfs/etc/machine-id

An empty /etc/machine-id triggers a fresh ID to be generated on first boot.

Logs

sudo rm -rf /media/$USER/rootfs/var/log/journal/*
sudo rm -rf /media/$USER/rootfs/var/log/*.log
sudo rm -rf /media/$USER/rootfs/tmp/*
sudo rm -rf /media/$USER/rootfs/var/tmp/*

10. Create the .img File

Unmount the SD card partitions (keep the card inserted)

sudo umount /media/$USER/rootfs
sudo umount /media/$USER/bootfs

Image the SD card with dd

Replace /dev/sda with your actual device from lsblk:

sudo dd if=/dev/sda of=~/little-printer-zigbee-bridge.img bs=4M status=progress

This copies the full card. For a 16GB card it will produce a 16GB file - shrink it in the next step.

Shrink and compress with PiShrink

wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
chmod +x pishrink.sh
sudo ./pishrink.sh -z ~/little-printer-zigbee-bridge.img

This creates little-printer-zigbee-bridge.img.gz, compressed and shrunk to the minimum size needed (from SD card size to about 800MB).


11. User Instructions

  1. Download little-printer-zigbee-bridge.img.gz and flash it to an SD card using Raspberry Pi Imager or balenaEtcher
  2. After flashing, open the boot partition of the SD card (visible on Windows, Mac, and Linux as a small FAT drive)
  3. Open wifi.txt and fill in your WiFi SSID and PASSWORD
  4. Save the file, eject the SD card, and insert it into the Raspberry Pi
  5. Power on - the Pi will configure WiFi on first boot, install Python dependencies, and start the bridge automatically. The first boot takes a little longer than usual while dependencies are installed.

Troubleshooting

Service fails with "dependency" error Check whether wifi-setup.service succeeded:

sudo journalctl -b | grep wifi-setup

WiFi never comes up Check NetworkManager:

sudo systemctl status NetworkManager
sudo journalctl -b | grep NetworkManager

Bridge does not start on first boot Check whether the dependency installation completed:

sudo systemctl status first-boot-setup.service
sudo journalctl -b | grep first-boot-setup

Script starts before network is ready The ExecStartPre ping loop should make the script wait until internet is reachable.

Clone this wiki locally