#!/usr/bin/env bash set -euo pipefail SERVER_URL="__PISTATION_SERVER_URL__" PACKAGE_URL="" ENROLLMENT_KEY="" JOIN_URL="" KIOSK_USER="pistation" CA_CERT="" SKIP_REBOOT="no" FORCED_PROFILE="" FORCED_SWAP="" log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m==>\033[0m %s\n' "$*" >&2; } fail() { printf '\033[1;31m==>\033[0m %s\n' "$*" >&2; exit 1; } usage() { cat <<'USAGE' PiStation kiosk installer curl -fsSL /install.sh | sudo bash -s -- --key Options --key Enrollment token from the admin panel, required --server PiStation API base URL, defaults to the host this script came from --join-url Address shown on screen for people joining, defaults to the server host --package-url Override where the kiosk .deb is downloaded from --user System user to run the kiosk as, defaults to pistation --ca-cert Trust this certificate authority, needed for a self signed server --profile Force a video profile: zero2, pi4, pi5 or generic --swap Swap size in MB, defaults to the profile's value --skip-reboot Install and enable, but do not reboot at the end --help Show this message USAGE } while [[ $# -gt 0 ]]; do case "$1" in --key) ENROLLMENT_KEY="${2:-}"; shift 2 ;; --key=*) ENROLLMENT_KEY="${1#*=}"; shift ;; --server) SERVER_URL="${2:-}"; shift 2 ;; --server=*) SERVER_URL="${1#*=}"; shift ;; --join-url) JOIN_URL="${2:-}"; shift 2 ;; --join-url=*) JOIN_URL="${1#*=}"; shift ;; --package-url) PACKAGE_URL="${2:-}"; shift 2 ;; --package-url=*) PACKAGE_URL="${1#*=}"; shift ;; --user) KIOSK_USER="${2:-}"; shift 2 ;; --user=*) KIOSK_USER="${1#*=}"; shift ;; --ca-cert) CA_CERT="${2:-}"; shift 2 ;; --ca-cert=*) CA_CERT="${1#*=}"; shift ;; --profile) FORCED_PROFILE="${2:-}"; shift 2 ;; --profile=*) FORCED_PROFILE="${1#*=}"; shift ;; --swap) FORCED_SWAP="${2:-}"; shift 2 ;; --swap=*) FORCED_SWAP="${1#*=}"; shift ;; --skip-reboot) SKIP_REBOOT="yes"; shift ;; --help|-h) usage; exit 0 ;; *) fail "unknown option: $1" ;; esac done [[ $EUID -eq 0 ]] || fail "run this with sudo, for example: curl -fsSL $SERVER_URL/install.sh | sudo bash -s -- --key ..." [[ -n "$ENROLLMENT_KEY" ]] || { usage; fail "missing --key"; } [[ -n "$SERVER_URL" ]] || fail "missing --server" # The media stack ships prebuilt for arm64 and x86_64 only. A 32 bit Raspberry Pi OS will # never work, and failing here is far kinder than failing halfway through a build. ARCH="$(uname -m)" case "$ARCH" in aarch64 | arm64 | x86_64) ;; armv7l | armv6l) fail "this is a 32 bit system ($ARCH). PiStation needs the 64 bit Raspberry Pi OS." ;; *) warn "unrecognised architecture $ARCH, continuing anyway" ;; esac # Model decides how hard the kiosk may push video. A Zero 2W has four slow cores and half a # gigabyte of memory, so it gets very different settings from a Pi 5. MODEL="$(tr -d '\0' /dev/null || echo unknown)" case "$FORCED_PROFILE" in zero2) MODEL="Raspberry Pi Zero 2 W" ;; pi4) MODEL="Raspberry Pi 4 Model B" ;; pi5) MODEL="Raspberry Pi 5" ;; generic) MODEL="generic" ;; "") ;; *) fail "unknown profile: $FORCED_PROFILE" ;; esac case "$MODEL" in *"Zero 2"*) PROFILE="Pi Zero 2 W" VIDEO_MAX_WIDTH=960 VIDEO_FPS=12 VIDEO_QUALITY=70 GPU_MEM=128 SWAP_MB=2048 ;; *"Pi 5"*) PROFILE="Pi 5" VIDEO_MAX_WIDTH=1920 VIDEO_FPS=60 VIDEO_QUALITY=85 GPU_MEM="" SWAP_MB=512 ;; *"Pi 4"*|*"Compute Module 4"*) PROFILE="Pi 4" VIDEO_MAX_WIDTH=1600 VIDEO_FPS=30 VIDEO_QUALITY=80 GPU_MEM=128 SWAP_MB=1024 ;; *) PROFILE="generic" VIDEO_MAX_WIDTH=1280 VIDEO_FPS=30 VIDEO_QUALITY=80 GPU_MEM=128 SWAP_MB=1024 ;; esac [[ -n "$FORCED_SWAP" ]] && SWAP_MB="$FORCED_SWAP" SERVER_URL="${SERVER_URL%/}" [[ -n "$PACKAGE_URL" ]] || PACKAGE_URL="$SERVER_URL/downloads/pistation-kiosk.deb" [[ -n "$JOIN_URL" ]] || JOIN_URL="$(printf '%s' "$SERVER_URL" | sed -e 's|^https\?://||' -e 's|:.*$||')" if [[ -d /boot/firmware ]]; then BOOT_DIR="/boot/firmware" elif [[ -d /boot ]]; then BOOT_DIR="/boot" else fail "could not find the boot partition" fi log "installing runtime packages" export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y --no-install-recommends \ ca-certificates \ curl \ cage \ seatd \ libwebkit2gtk-4.1-0 \ libgtk-3-0 \ gstreamer1.0-plugins-base \ gstreamer1.0-plugins-good \ gstreamer1.0-plugins-bad \ gstreamer1.0-libav \ gstreamer1.0-nice \ libnice10 \ libgles2 # A kiosk has nobody to click through a certificate warning, so a self signed server has to be # trusted here or every request it makes fails, starting with the package download below. if [[ -n "$CA_CERT" ]]; then log "trusting the certificate authority at $CA_CERT" CA_TARGET="/usr/local/share/ca-certificates/pistation-ca.crt" if [[ "$CA_CERT" == http://* || "$CA_CERT" == https://* ]]; then curl -fsSL "$CA_CERT" -o "$CA_TARGET" || fail "could not download the certificate authority from $CA_CERT" else [[ -f "$CA_CERT" ]] || fail "no certificate authority at $CA_CERT" cp "$CA_CERT" "$CA_TARGET" fi chmod 644 "$CA_TARGET" update-ca-certificates >/dev/null || fail "the certificate authority was rejected" fi log "downloading the kiosk package from $PACKAGE_URL" PACKAGE_FILE="$(mktemp /tmp/pistation-kiosk.XXXXXX.deb)" trap 'rm -f "$PACKAGE_FILE"' EXIT if ! curl -fsSL "$PACKAGE_URL" -o "$PACKAGE_FILE"; then fail "could not download the kiosk package. Upload a build in the admin panel, or pass --package-url" fi [[ -s "$PACKAGE_FILE" ]] || fail "the downloaded package was empty" log "installing the kiosk application" apt-get install -y "$PACKAGE_FILE" # Raspberry Pi OS ships with 100 MB of swap. WebKit plus a media stack will exhaust that on # a 512 MB Zero 2W and the kernel will start killing processes, which looks like the kiosk # randomly restarting. configure_swap() { local wanted="$1" local current_mb=0 if [[ -f /proc/meminfo ]]; then current_mb=$(($(awk '/SwapTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo 0) / 1024)) fi if [[ "$current_mb" -ge "$wanted" ]]; then log "swap is already ${current_mb} MB, leaving it alone" return fi if [[ -f /etc/dphys-swapfile ]]; then log "raising swap from ${current_mb} MB to ${wanted} MB" sed -i "/^CONF_SWAPSIZE=/d;/^CONF_MAXSWAP=/d" /etc/dphys-swapfile { echo "CONF_SWAPSIZE=$wanted" echo "CONF_MAXSWAP=$wanted" } >>/etc/dphys-swapfile dphys-swapfile swapoff >/dev/null 2>&1 || true dphys-swapfile setup >/dev/null 2>&1 || warn "could not resize the swap file" dphys-swapfile swapon >/dev/null 2>&1 || warn "could not enable swap" return fi if [[ -f /swapfile ]]; then log "a swap file already exists, leaving it alone" return fi log "creating a ${wanted} MB swap file" fallocate -l "${wanted}M" /swapfile 2>/dev/null || dd if=/dev/zero of=/swapfile bs=1M count="$wanted" status=none chmod 600 /swapfile mkswap /swapfile >/dev/null swapon /swapfile || warn "could not enable the swap file" grep -q '^/swapfile' /etc/fstab || echo "/swapfile none swap sw 0 0" >>/etc/fstab } configure_swap "$SWAP_MB" if ! id "$KIOSK_USER" >/dev/null 2>&1; then log "creating the $KIOSK_USER user" useradd --create-home --shell /bin/bash "$KIOSK_USER" fi for group in video render input seat; do getent group "$group" >/dev/null 2>&1 && usermod -aG "$group" "$KIOSK_USER" done systemctl enable seatd >/dev/null 2>&1 || warn "could not enable seatd" log "writing the kiosk configuration" cat >"$BOOT_DIR/pistation.json" <>"$BOOT_CONFIG" else warn "no config.txt at $BOOT_CONFIG, skipping video tuning" fi KIOSK_UID="$(id -u "$KIOSK_USER")" KIOSK_BINARY="$(command -v pistation-kiosk || echo /usr/bin/pistation-kiosk)" log "installing the systemd unit" cat >/etc/systemd/system/pistation-kiosk.service </dev/null 2>&1 || true systemctl disable --now triggerhappy.service >/dev/null 2>&1 || true log "done" echo echo " model $MODEL" echo " profile $PROFILE, ${VIDEO_MAX_WIDTH}px at ${VIDEO_FPS}fps, quality ${VIDEO_QUALITY}" echo " swap ${SWAP_MB} MB" echo " server $SERVER_URL" echo " join url $JOIN_URL" echo " config $BOOT_DIR/pistation.json" echo if [[ "$SKIP_REBOOT" == "yes" ]]; then log "reboot when ready, or run: systemctl start pistation-kiosk" else log "rebooting into the kiosk in 5 seconds, press ctrl c to cancel" sleep 5 reboot fi