Files
daynivo-kiosk-os/install.sh
T
rothmanandClaude Opus 5.5 4d061f5211
CI / test-bookworm (push) Failing after 2s
CI / test-latest (push) Successful in 30s
Boot splash with the InfoFlow logo instead of the Raspberry Pi one
Own plymouth theme (logo centred on white), quiet kernel command line
without boot text, Raspberry logos or cursor, and no firmware rainbow
square. The initramfs is only rebuilt when the theme changes;
uninstall.sh restores the previous theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:19:32 +01:00

340 lines
15 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# VoxIP Kiosk OS installer.
#
# Turns Raspberry Pi OS Lite (Bookworm/Trixie, NetworkManager) into a VoxIP
# kiosk. Also used by the image builder (inside the pi-gen chroot), so a
# flashed image and a manual install are identical.
#
# sudo ./install.sh [--url https://…] [--name "Hal begane grond"] [--hostname hal-bg]
# sudo ./install.sh --upgrade # update code, keep configuration
# ./install.sh --image … # used by image/stage-voxip (no service starts)
#
# Existing configuration in /etc/voxip-kiosk is never overwritten.
set -euo pipefail
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODE="install"
URL=""
NAME=""
NEW_HOSTNAME=""
FORCE=0
LIB=/usr/lib/voxip-kiosk
SHARE=/usr/share/voxip-kiosk
DOC=/usr/share/doc/voxip-kiosk
ETC=/etc/voxip-kiosk
UNITS=(voxip-kiosk-x.service voxip-kiosk-browser.service voxip-kiosk-agent.service voxip-kiosk-manager.service
voxip-kiosk-healthcheck.service voxip-kiosk-healthcheck.timer voxip-network-guard.service voxip-boot-import.service)
ENABLE=(voxip-kiosk-x.service voxip-kiosk-browser.service voxip-kiosk-agent.service voxip-kiosk-manager.service
voxip-kiosk-healthcheck.timer voxip-network-guard.service voxip-boot-import.service)
say() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33mLet op:\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31mFout:\033[0m %s\n' "$*" >&2; exit 1; }
usage() {
sed -n '3,15p' "$0" | sed 's/^# \{0,1\}//'
exit "${1:-0}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--url) URL="${2:?--url vereist een waarde}"; shift 2 ;;
--name) NAME="${2:?--name vereist een waarde}"; shift 2 ;;
--hostname) NEW_HOSTNAME="${2:?--hostname vereist een waarde}"; shift 2 ;;
--upgrade) MODE="upgrade"; shift ;;
--image) MODE="image"; shift ;;
--force) FORCE=1; shift ;;
-h|--help) usage 0 ;;
*) warn "Onbekende optie: $1"; usage 1 ;;
esac
done
[[ $EUID -eq 0 ]] || die "Voer dit script uit met sudo."
[[ -f "$SRC/src/voxip_kiosk/config.py" ]] || die "Start install.sh vanuit de VoxIP Kiosk OS-repository."
in_chroot() {
if command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt --chroot --quiet; then
return 0
fi
[[ "$(stat -c %d:%i /)" != "$(stat -c %d:%i /proc/1/root/. 2>/dev/null || echo x)" ]]
}
LIVE=1
if [[ $MODE == image ]] || in_chroot; then LIVE=0; fi
# --- checks ---------------------------------------------------------------
# In a subshell: os-release defines NAME and VERSION, which must not clobber ours.
# shellcheck disable=SC1091
CODENAME="$(. /etc/os-release && echo "${VERSION_CODENAME:-}")"
case "$CODENAME" in
bookworm|trixie) ;;
*) [[ $FORCE -eq 1 ]] || die "Niet-ondersteunde release '${CODENAME:-?}'. Ondersteund: bookworm, trixie (--force om toch door te gaan)." ;;
esac
if [[ $LIVE -eq 1 && $MODE == install ]]; then
if ! systemctl is-enabled --quiet NetworkManager.service 2>/dev/null && systemctl is-enabled --quiet dhcpcd.service 2>/dev/null; then
die "Dit systeem gebruikt dhcpcd. VoxIP Kiosk OS vereist NetworkManager (standaard sinds Bookworm). Schakel om via 'sudo raspi-config' → Advanced Options → Network Config, of gebruik de kant-en-klare image."
fi
fi
VERSION="$(tr -d '[:space:]' < "$SRC/VERSION")"
say "VoxIP Kiosk OS $VERSION installeren (modus: $MODE)"
# --- packages -----------------------------------------------------------------
pkg_available() { apt-cache policy "$1" 2>/dev/null | grep -q 'Candidate: [0-9]'; }
{
say "Pakketten installeren"
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
CHROMIUM=chromium
if pkg_available chromium-browser && [[ $CODENAME == bookworm ]]; then CHROMIUM=chromium-browser; fi
PKGS=(
"$CHROMIUM" xserver-xorg-core xserver-xorg-input-libinput xserver-xorg-legacy x11-xserver-utils xauth
fonts-dejavu-core fonts-noto-color-emoji
python3 python3-flask python3-argon2 python3-websocket gunicorn
network-manager avahi-daemon iw rfkill iproute2 openssl logrotate sudo kbd plymouth
)
for opt in systemd-zram-generator libraspberrypi-bin raspi-utils-core; do
if pkg_available "$opt"; then PKGS+=("$opt"); fi
done
# The "chromium" package needs its setuid sandbox where unprivileged user namespaces are unavailable.
if [[ $CHROMIUM == chromium ]] && pkg_available chromium-sandbox; then PKGS+=(chromium-sandbox); fi
apt-get install -y --no-install-recommends "${PKGS[@]}"
}
# --- users ----------------------------------------------------------------------
say "Gebruikers aanmaken"
getent group kiosk >/dev/null || groupadd --system kiosk
id kiosk >/dev/null 2>&1 || useradd --system --gid kiosk --home-dir /home/kiosk --create-home \
--shell /usr/sbin/nologin --comment "VoxIP kiosk display" kiosk
for g in video render input audio; do
getent group "$g" >/dev/null && usermod -aG "$g" kiosk
done
getent group voxip >/dev/null || groupadd --system voxip
id voxip >/dev/null 2>&1 || useradd --system --gid voxip --home-dir /var/lib/voxip-kiosk --no-create-home \
--shell /usr/sbin/nologin --comment "VoxIP Kiosk Manager" voxip
for g in systemd-journal video; do
getent group "$g" >/dev/null && usermod -aG "$g" voxip
done
# --- application files --------------------------------------------------------------
say "Programmabestanden installeren"
install -d -m 0755 "$LIB" "$SHARE" "$DOC"
rm -rf "$LIB/python.new"
install -d -m 0755 "$LIB/python.new"
tar -C "$SRC/src" --exclude='__pycache__' --exclude='*.pyc' -cf - voxip_kiosk | tar -C "$LIB/python.new" -xf -
chown -R root:root "$LIB/python.new"
chmod -R u=rwX,go=rX "$LIB/python.new"
python3 -m compileall -q "$LIB/python.new" >/dev/null
rm -rf "$LIB/python.old"
[[ -d "$LIB/python" ]] && mv "$LIB/python" "$LIB/python.old"
mv "$LIB/python.new" "$LIB/python"
rm -rf "$LIB/python.old"
for bin in voxip-kiosk-agent voxip-kiosk-browser voxip-kiosk-healthcheck voxip-kiosk-helper voxip-kiosk-manager voxip-kiosk-x; do
install -m 0755 -o root -g root "$SRC/bin/$bin" "$LIB/$bin"
done
install -m 0755 -o root -g root "$SRC/bin/voxip-kiosk-ctl" /usr/local/sbin/voxip-kiosk-ctl
rm -rf "$SHARE/www"
install -d -m 0755 "$SHARE/www"
install -m 0644 "$SRC"/www/* "$SHARE/www/"
install -m 0644 "$SRC/VERSION" "$SHARE/VERSION"
install -m 0644 "$SRC/README.md" "$DOC/README.md"
install -m 0644 "$SRC"/docs/*.md "$DOC/"
# --- configuration -------------------------------------------------------------------
say "Configuratie"
install -d -o voxip -g kiosk -m 2750 "$ETC"
install -d -o voxip -g voxip -m 0750 /var/lib/voxip-kiosk
install -d -o root -g root -m 0755 /var/lib/voxip-kiosk-network
install -d -o voxip -g voxip -m 0750 /var/log/voxip-kiosk
# Migrate from InfoFlow's raspberry-pi/install-kiosk.sh: take over its URL, then disable it.
INFOFLOW_UNIT=/etc/systemd/system/display-kiosk.service
if [[ -f $INFOFLOW_UNIT ]]; then
OLD_URL="$(grep -oE 'https://[^" ]+' "$INFOFLOW_UNIT" | tail -n1 || true)"
if [[ -n $OLD_URL && -z $URL && ! -f $ETC/config.json ]]; then
URL="$OLD_URL"
say "URL overgenomen van de InfoFlow-kioskinstallatie"
fi
if [[ $LIVE -eq 1 ]]; then systemctl disable --now display-kiosk.service 2>/dev/null || true; fi
mv "$INFOFLOW_UNIT" "$INFOFLOW_UNIT.voxip-disabled"
fi
PYRUN=(python3 -I -c)
"${PYRUN[@]}" '
import json, os, sys
sys.path.insert(0, "/usr/lib/voxip-kiosk/python")
from voxip_kiosk import config as c
path, default, url, name = sys.argv[1:5]
updates = {}
if url: updates["url"] = url
if name: updates["device_name"] = name
if os.path.exists(path):
if updates:
c.save_config(c.Path(path), updates)
print("Bestaande configuratie bijgewerkt:", ", ".join(updates))
else:
print("Bestaande configuratie behouden")
else:
with open(default, encoding="utf-8") as fh:
data = json.load(fh)
data.update(c.validate(updates))
c.validate(data)
c.atomic_write_json(c.Path(path), data)
print("Nieuwe configuratie aangemaakt")
' "$ETC/config.json" "$SRC/config/config.default.json" "$URL" "$NAME" || die "Ongeldige --url of --name."
chown voxip:kiosk "$ETC/config.json" "$ETC/config.json.bak" 2>/dev/null || true
chmod 0640 "$ETC/config.json"
touch "$ETC/.config.lock" && chown voxip:kiosk "$ETC/.config.lock" && chmod 0640 "$ETC/.config.lock"
[[ -f $ETC/admin.json ]] && chown voxip:voxip "$ETC/admin.json" && chmod 0600 "$ETC/admin.json"
# --- system integration ----------------------------------------------------------------
say "Systeemintegratie"
TMP_SUDOERS="$(mktemp)"
install -m 0440 "$SRC/config/sudoers" "$TMP_SUDOERS"
visudo -cqf "$TMP_SUDOERS" || die "sudoers-bestand ongeldig"
mv "$TMP_SUDOERS" /etc/sudoers.d/voxip-kiosk
chown root:root /etc/sudoers.d/voxip-kiosk
install -D -m 0644 "$SRC/config/journald.conf" /etc/systemd/journald.conf.d/voxip-kiosk.conf
install -D -m 0644 "$SRC/config/watchdog.conf" /etc/systemd/system.conf.d/voxip-watchdog.conf
install -D -m 0644 "$SRC/config/logrotate" /etc/logrotate.d/voxip-kiosk
if [[ -f /etc/X11/Xwrapper.config && ! -f /etc/X11/Xwrapper.config.voxip-orig ]]; then
cp -a /etc/X11/Xwrapper.config /etc/X11/Xwrapper.config.voxip-orig
fi
install -D -m 0644 "$SRC/config/Xwrapper.config" /etc/X11/Xwrapper.config
for dir in /etc/chromium/policies/managed /etc/chromium-browser/policies/managed; do
install -D -m 0644 "$SRC/config/chromium/voxip-kiosk.json" "$dir/voxip-kiosk.json"
done
ADMIN_PORT="$(python3 -I -c 'import sys; sys.path.insert(0, "/usr/lib/voxip-kiosk/python"); from voxip_kiosk.config import load_config; print(load_config(__import__("pathlib").Path(sys.argv[1]))["admin_port"])' "$ETC/config.json")"
install -d -m 0755 /etc/avahi/services
sed "s#<port>8080</port>#<port>${ADMIN_PORT}</port>#" "$SRC/config/avahi-voxip.service" > /etc/avahi/services/voxip-kiosk.service
chmod 0644 /etc/avahi/services/voxip-kiosk.service
# Swap: zram in RAM instead of a swap file on the SD card (Trixie's rpi-swap already does this).
if dpkg -s systemd-zram-generator >/dev/null 2>&1 && ! dpkg -s rpi-swap >/dev/null 2>&1; then
install -m 0644 "$SRC/config/zram-generator.conf" /etc/systemd/zram-generator.conf
if dpkg -s dphys-swapfile >/dev/null 2>&1; then
if [[ $LIVE -eq 1 ]]; then dphys-swapfile swapoff 2>/dev/null || true; fi
systemctl disable dphys-swapfile.service 2>/dev/null || true
rm -f /var/swap
fi
fi
# /tmp in RAM (default on Trixie, opt-in on Bookworm).
if [[ -f /usr/share/systemd/tmp.mount && ! -e /etc/systemd/system/tmp.mount ]] && ! systemctl is-enabled --quiet tmp.mount 2>/dev/null; then
cp /usr/share/systemd/tmp.mount /etc/systemd/system/tmp.mount
systemctl enable tmp.mount 2>/dev/null || true
fi
# Kernel command line: no console blanking (the kiosk runs on vt7, but keep the console
# awake too), and a quiet boot behind the splash: no boot text, Raspberry logos or cursor.
add_cmdline() { # add "key" or "key=value" unless the key is already present
local key="${2%%=*}"
grep -qE "(^| )${key//./\\.}(=[^ ]*)?( |\$)" "$1" || sed -i "1 s/\$/ $2/" "$1"
}
for cmdline in /boot/firmware/cmdline.txt /boot/cmdline.txt; do
if [[ -f $cmdline ]]; then
for opt in consoleblank=0 quiet splash plymouth.ignore-serial-consoles logo.nologo vt.global_cursor_default=0; do
add_cmdline "$cmdline" "$opt"
done
config_txt="$(dirname "$cmdline")/config.txt"
# No rainbow square from the firmware. [all] in case the file ends in a model section.
if [[ -f $config_txt ]] && ! grep -q '^disable_splash=' "$config_txt"; then
printf '\n[all]\ndisable_splash=1\n' >> "$config_txt"
fi
break
fi
done
# Boot splash: logo on white (config/plymouth). Rebuilding the initramfs takes a while
# on a Pi 3, so only when the theme changed.
SPLASH_DIR=/usr/share/plymouth/themes/voxip-kiosk
install -d -m 0755 "$SPLASH_DIR"
install -m 0644 "$SRC"/config/plymouth/voxip-kiosk/* "$SPLASH_DIR/"
if [[ ! -f /var/lib/voxip-kiosk/previous-plymouth-theme ]]; then
plymouth-set-default-theme > /var/lib/voxip-kiosk/previous-plymouth-theme 2>/dev/null || true
fi
SPLASH_SUM="$(cat "$SPLASH_DIR"/* | sha256sum | cut -d' ' -f1)"
if [[ "$(plymouth-set-default-theme 2>/dev/null)" != voxip-kiosk || "$(cat /var/lib/voxip-kiosk/splash.sha256 2>/dev/null)" != "$SPLASH_SUM" ]]; then
say "Opstartscherm instellen (initramfs bijwerken, dit duurt even)"
plymouth-set-default-theme voxip-kiosk
if update-initramfs -u -k all >/dev/null 2>&1; then
echo "$SPLASH_SUM" > /var/lib/voxip-kiosk/splash.sha256
else
warn "initramfs bijwerken mislukt; het opstartscherm verschijnt mogelijk pas later tijdens het opstarten."
fi
fi
# A desktop display manager would fight over vt7 and display :0 (Pi OS Desktop images).
# uninstall.sh re-enables the ones recorded here.
for dm in lightdm.service gdm3.service sddm.service; do
if systemctl is-enabled --quiet "$dm" 2>/dev/null; then
warn "$dm wordt uitgeschakeld (de kiosk gebruikt een eigen minimale X-sessie)."
systemctl disable "$dm" 2>/dev/null || true
grep -qx "$dm" /var/lib/voxip-kiosk/disabled-display-managers 2>/dev/null \
|| echo "$dm" >> /var/lib/voxip-kiosk/disabled-display-managers
fi
if [[ $LIVE -eq 1 ]] && systemctl is-active --quiet "$dm" 2>/dev/null; then
warn "$dm wordt gestopt: de desktop op het scherm sluit nu af."
systemctl stop "$dm" || true
fi
done
# --- services ------------------------------------------------------------------------------
say "Services installeren"
for unit in "${UNITS[@]}"; do
install -m 0644 "$SRC/systemd/$unit" "/etc/systemd/system/$unit"
done
if [[ $LIVE -eq 1 ]]; then systemctl daemon-reload; fi
systemctl enable "${ENABLE[@]}" NetworkManager.service avahi-daemon.service >/dev/null
if [[ ! -f /var/lib/voxip-kiosk/previous-default-target ]]; then
systemctl get-default > /var/lib/voxip-kiosk/previous-default-target 2>/dev/null || true
fi
systemctl set-default multi-user.target >/dev/null
if [[ -n $NEW_HOSTNAME && $LIVE -eq 0 ]]; then
warn "--hostname wordt genegeerd in image-modus (gebruik TARGET_HOSTNAME van pi-gen)."
elif [[ -n $NEW_HOSTNAME ]]; then
"$LIB/voxip-kiosk-helper" set-hostname "$NEW_HOSTNAME" >/dev/null || die "Ongeldige hostname: $NEW_HOSTNAME"
fi
if [[ $LIVE -eq 0 ]]; then
say "Klaar (image-modus: services starten bij de eerste boot)"
exit 0
fi
say "Services starten"
systemctl restart systemd-journald.service || true
systemctl restart avahi-daemon.service || true
systemctl restart voxip-kiosk-manager.service voxip-kiosk-agent.service
systemctl start voxip-kiosk-healthcheck.timer
systemctl reset-failed voxip-kiosk-x.service voxip-kiosk-browser.service 2>/dev/null || true
systemctl restart voxip-kiosk-x.service
systemctl restart voxip-kiosk-browser.service
HOST="$(hostname)"
SCHEME=http
grep -q '"admin_https": true' "$ETC/config.json" && SCHEME=https
echo
say "VoxIP Kiosk OS $VERSION is geïnstalleerd."
echo " Beheerinterface: ${SCHEME}://${HOST}.local:${ADMIN_PORT}"
for ip in $(hostname -I 2>/dev/null); do
[[ $ip == *:* ]] || echo " ${SCHEME}://${ip}:${ADMIN_PORT}"
done
if [[ ! -f $ETC/admin.json ]]; then
for _ in $(seq 1 20); do [[ -f $ETC/setup-code ]] && break; sleep 0.5; done
if [[ -f $ETC/setup-code ]]; then
echo " Setupcode: $(cat "$ETC/setup-code") (staat ook op het scherm)"
fi
fi
echo " Status: sudo voxip-kiosk-ctl status"