#!/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 Daynivo's raspberry-pi/install-kiosk.sh: take over its URL, then disable it.
LEGACY_KIOSK_UNIT=/etc/systemd/system/display-kiosk.service
if [[ -f $LEGACY_KIOSK_UNIT ]]; then
OLD_URL="$(grep -oE 'https://[^" ]+' "$LEGACY_KIOSK_UNIT" | tail -n1 || true)"
if [[ -n $OLD_URL && -z $URL && ! -f $ETC/config.json ]]; then
URL="$OLD_URL"
say "URL overgenomen van de Daynivo-kioskinstallatie"
fi
if [[ $LIVE -eq 1 ]]; then systemctl disable --now display-kiosk.service 2>/dev/null || true; fi
mv "$LEGACY_KIOSK_UNIT" "$LEGACY_KIOSK_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#8080#${ADMIN_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"