mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
Nightly backups to a mounted NAS share ran from May and then stopped, failing with [Errno 30] Read-only file system. The reporter checked folder permissions -- correctly: the mount is gid=backup,dir_mode=0775, the service user is in that group, and his own shell writes to the share fine. Errno 30 is EROFS. A permission problem is errno 13. EROFS means the filesystem refused the write, and it refused because we told it to: our systemd unit ships ProtectSystem=strict, which mounts everything read-only inside the service's mount namespace and carves back out only ReadWritePaths=<install> <data> <logs>. A NAS share is not one of those three. Reads are unaffected -- which is why the UI happily listed his existing backups from the share while being unable to write a new one -- and his shell is outside the namespace entirely, so every check he could think to run said the directory was fine. Both installers write the unit file wholesale, so a ReadWritePaths line added by hand disappeared on the next install, taking the backups with it. They now back the old unit up (.bak-<timestamp>) and carry the operator's extra writable paths forward, reporting which ones they kept. The unit template documents the carve-out. The output directory is probed with a real write when it is saved and when the backup card loads, so an unwritable path is caught there rather than at 03:00 for a week. On failure the card names the cause and hands over the fix with the operator's path already in it (systemctl edit bambuddy -> ReadWritePaths=...), and a failed run reports the same diagnosis rather than the raw OSError. EROFS outside systemd, permission-denied, out-of-space, not-a-directory and missing are told apart, in all 11 locales. Docker: a backup path that is not bind-mounted is writable -- the write lands in the container's ephemeral layer and is lost on the next compose up. The probe compares the directory's device against the container root and warns, with the compose snippet that mounts it properly.
1689 lines
64 KiB
Bash
Executable File
1689 lines
64 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# SpoolBuddy Installation Script for Raspberry Pi
|
|
#
|
|
# Supports two scenarios:
|
|
# 1) SpoolBuddy only — NFC/scale companion connecting to a remote Bambuddy instance
|
|
# 2) SpoolBuddy + Bambuddy — both running natively on this Raspberry Pi
|
|
#
|
|
# Usage:
|
|
# Interactive: curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/spoolbuddy/install.sh -o install.sh && chmod +x install.sh && sudo ./install.sh
|
|
# Unattended: sudo ./install.sh --mode spoolbuddy --bambuddy-url http://192.168.1.100:8000 --api-key bb_xxx --yes
|
|
#
|
|
# Options:
|
|
# --mode MODE Installation mode: "spoolbuddy" (companion only) or "full" (both)
|
|
# --repo URL Git repository URL to install from (default: upstream repo)
|
|
# --ref REF Git ref to install (branch/tag/commit, default: main)
|
|
# --bambuddy-url URL Bambuddy server URL (required for spoolbuddy mode)
|
|
# --api-key KEY Bambuddy API key (required for spoolbuddy mode)
|
|
# --path PATH Installation directory (default: /opt/spoolbuddy or /opt/bambuddy)
|
|
# --port PORT Bambuddy port (full mode only, default: 8000)
|
|
# --ssh-pubkey KEY Bambuddy SSH public key for remote updates
|
|
# --yes, -y Non-interactive mode, accept defaults
|
|
# --help, -h Show this help message
|
|
#
|
|
|
|
set -e
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Constants
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
NC='\033[0m'
|
|
|
|
GITHUB_REPO="https://github.com/maziggy/bambuddy.git"
|
|
SPOOLBUDDY_SERVICE_USER="spoolbuddy"
|
|
BAMBUDDY_SERVICE_USER="bambuddy"
|
|
|
|
# Packages needed for SpoolBuddy hardware (NFC reader + scale)
|
|
SYSTEM_PACKAGES="python3 python3-pip python3-venv python3-dev python3-spidev python3-libgpiod gpiod libgpiod-dev i2c-tools git plymouth-themes"
|
|
|
|
# Python packages for SpoolBuddy daemon
|
|
SPOOLBUDDY_PIP_PACKAGES="spidev gpiod smbus2 httpx"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Variables (set by args or prompts)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
INSTALL_MODE="" # "spoolbuddy" or "full"
|
|
INSTALL_PATH=""
|
|
INSTALL_REPO=""
|
|
INSTALL_REF=""
|
|
DETECTED_INSTALLER_REPO=""
|
|
DETECTED_INSTALLER_REF=""
|
|
BAMBUDDY_URL=""
|
|
API_KEY=""
|
|
BAMBUDDY_PORT="8000"
|
|
NON_INTERACTIVE="false"
|
|
REBOOT_NEEDED="false"
|
|
KIOSK_USER="" # auto-detected from $SUDO_USER
|
|
KIOSK_URL="" # derived from $BAMBUDDY_URL/spoolbuddy?token=$API_KEY
|
|
SSH_PUBKEY="" # Bambuddy's SSH public key for remote updates
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Helpers
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
info() { echo -e "${CYAN}[INFO]${NC} $1"; }
|
|
success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
|
|
|
# Run a long-running command with a spinner + live progress output.
|
|
# Usage: run_with_progress "description" command [args...]
|
|
run_with_progress() {
|
|
local desc="$1"
|
|
shift
|
|
|
|
local log_file
|
|
log_file=$(mktemp /tmp/spoolbuddy-install.XXXXXX)
|
|
local start_time=$SECONDS
|
|
|
|
# Run command in background, capture stdout+stderr
|
|
"$@" > "$log_file" 2>&1 &
|
|
local pid=$!
|
|
|
|
# Spinner frames (braille pattern)
|
|
local -a spin=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
|
|
local i=0
|
|
|
|
while kill -0 "$pid" 2>/dev/null; do
|
|
local elapsed=$(( SECONDS - start_time ))
|
|
local time_str
|
|
if (( elapsed >= 60 )); then
|
|
time_str="$(( elapsed / 60 ))m$(printf '%02d' $(( elapsed % 60 )))s"
|
|
else
|
|
time_str="${elapsed}s"
|
|
fi
|
|
|
|
# Last chunk of output (handles \r progress lines and regular \n lines)
|
|
local last_line=""
|
|
last_line=$(tail -c 4096 "$log_file" 2>/dev/null | tr '\r' '\n' | sed 's/\x1b\[[0-9;]*[mGKHJ]//g' | sed '/^[[:space:]]*$/d' | tail -1 | sed 's/^[[:space:]]*//' | cut -c1-50) || true
|
|
|
|
printf "\r ${spin[$((i % 10))]} %-36s ${CYAN}%6s${NC} %s\033[K" "$desc" "$time_str" "$last_line"
|
|
i=$(( i + 1 ))
|
|
sleep 0.15
|
|
done
|
|
|
|
local exit_code=0
|
|
wait "$pid" || exit_code=$?
|
|
|
|
# Clear spinner line
|
|
printf "\r\033[K"
|
|
|
|
# Format elapsed time for summary
|
|
local elapsed=$(( SECONDS - start_time ))
|
|
local time_suffix=""
|
|
if (( elapsed >= 60 )); then
|
|
time_suffix=" ($(( elapsed / 60 ))m $(( elapsed % 60 ))s)"
|
|
elif (( elapsed >= 5 )); then
|
|
time_suffix=" (${elapsed}s)"
|
|
fi
|
|
|
|
if [[ $exit_code -eq 0 ]]; then
|
|
success "${desc}${time_suffix}"
|
|
rm -f "$log_file"
|
|
else
|
|
echo -e "${RED}[FAIL]${NC} ${desc}${time_suffix}"
|
|
echo ""
|
|
echo -e " ${YELLOW}Last 20 lines:${NC}"
|
|
tail -20 "$log_file" 2>/dev/null | sed 's/^/ /'
|
|
echo ""
|
|
echo -e " Full log: ${CYAN}$log_file${NC}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
prompt() {
|
|
local prompt_text="$1"
|
|
local default_value="$2"
|
|
local var_name="$3"
|
|
|
|
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
eval "$var_name=\"$default_value\""
|
|
return
|
|
fi
|
|
|
|
if [[ -n "$default_value" ]]; then
|
|
echo -en "${BOLD}$prompt_text${NC} [${CYAN}$default_value${NC}]: "
|
|
else
|
|
echo -en "${BOLD}$prompt_text${NC}: "
|
|
fi
|
|
|
|
read -r input
|
|
if [[ -z "$input" ]]; then
|
|
eval "$var_name=\"$default_value\""
|
|
else
|
|
eval "$var_name=\"$input\""
|
|
fi
|
|
}
|
|
|
|
prompt_yes_no() {
|
|
local prompt_text="$1"
|
|
local default="$2"
|
|
|
|
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
[[ "$default" == "y" ]] && return 0 || return 1
|
|
fi
|
|
|
|
local yn_hint="[y/n]"
|
|
[[ "$default" == "y" ]] && yn_hint="[Y/n]"
|
|
[[ "$default" == "n" ]] && yn_hint="[y/N]"
|
|
|
|
while true; do
|
|
echo -en "${BOLD}$prompt_text${NC} $yn_hint: "
|
|
read -r yn
|
|
[[ -z "$yn" ]] && yn="$default"
|
|
case "$yn" in
|
|
[Yy]* ) return 0;;
|
|
[Nn]* ) return 1;;
|
|
* ) echo "Please answer yes or no.";;
|
|
esac
|
|
done
|
|
}
|
|
|
|
show_help() {
|
|
echo "SpoolBuddy Installation Script for Raspberry Pi"
|
|
echo ""
|
|
echo "Usage: sudo $0 [OPTIONS]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --mode MODE \"spoolbuddy\" (companion only) or \"full\" (Bambuddy + SpoolBuddy)"
|
|
echo " --repo URL Git repository URL to install from"
|
|
echo " --ref REF Git ref to install (branch/tag/commit)"
|
|
echo " --bambuddy-url URL Bambuddy server URL (required for spoolbuddy mode)"
|
|
echo " --api-key KEY Bambuddy API key (required for spoolbuddy mode)"
|
|
echo " --path PATH Installation directory (default: /opt/spoolbuddy or /opt/bambuddy)"
|
|
echo " --port PORT Bambuddy port (full mode only, default: 8000)"
|
|
echo " --ssh-pubkey KEY Bambuddy SSH public key for remote updates"
|
|
echo " --yes, -y Non-interactive mode, accept defaults"
|
|
echo " --help, -h Show this help message"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " Interactive:"
|
|
echo " sudo ./install.sh"
|
|
echo ""
|
|
echo " SpoolBuddy companion (unattended):"
|
|
echo " sudo ./install.sh --mode spoolbuddy --bambuddy-url http://192.168.1.100:8000 --api-key bb_xxx -y"
|
|
echo ""
|
|
echo " Full install (unattended):"
|
|
echo " sudo ./install.sh --mode full --port 8000 -y"
|
|
exit 0
|
|
}
|
|
|
|
normalize_github_repo_url() {
|
|
local url="$1"
|
|
if [[ -z "$url" ]]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
|
|
# Convert git@github.com:owner/repo(.git) to https://github.com/owner/repo.git
|
|
if [[ "$url" =~ ^git@github.com:(.+)$ ]]; then
|
|
url="https://github.com/${BASH_REMATCH[1]}"
|
|
fi
|
|
|
|
# Keep remote URL style consistent.
|
|
url="${url%.git}"
|
|
echo "${url}.git"
|
|
}
|
|
|
|
detect_installer_source_context() {
|
|
local script_dir
|
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
if git -C "$script_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
DETECTED_INSTALLER_REF="$(git -C "$script_dir" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
local origin_url
|
|
origin_url="$(git -C "$script_dir" remote get-url origin 2>/dev/null || true)"
|
|
DETECTED_INSTALLER_REPO="$(normalize_github_repo_url "$origin_url")"
|
|
fi
|
|
|
|
# Optional environment overrides for raw-download installs.
|
|
if [[ -n "${SPOOLBUDDY_INSTALL_REPO:-}" ]]; then
|
|
DETECTED_INSTALLER_REPO="$(normalize_github_repo_url "$SPOOLBUDDY_INSTALL_REPO")"
|
|
fi
|
|
if [[ -n "${SPOOLBUDDY_INSTALL_REF:-}" ]]; then
|
|
DETECTED_INSTALLER_REF="$SPOOLBUDDY_INSTALL_REF"
|
|
fi
|
|
|
|
if [[ -z "$INSTALL_REPO" ]]; then
|
|
if [[ -n "$DETECTED_INSTALLER_REPO" ]]; then
|
|
INSTALL_REPO="$DETECTED_INSTALLER_REPO"
|
|
else
|
|
INSTALL_REPO="$GITHUB_REPO"
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$INSTALL_REF" ]]; then
|
|
if [[ -n "$DETECTED_INSTALLER_REF" && "$DETECTED_INSTALLER_REF" != "HEAD" ]]; then
|
|
INSTALL_REF="$DETECTED_INSTALLER_REF"
|
|
else
|
|
INSTALL_REF="main"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
resolve_install_ref() {
|
|
local ref="$1"
|
|
# If ref exists on origin as a branch, track/reset it. Otherwise treat it as tag/commit.
|
|
if git ls-remote --exit-code --heads origin "$ref" >/dev/null 2>&1; then
|
|
git checkout -B "$ref" "origin/$ref" > /dev/null 2>&1
|
|
git reset --hard "origin/$ref" > /dev/null 2>&1
|
|
else
|
|
git checkout "$ref" > /dev/null 2>&1
|
|
fi
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Pre-flight Checks
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
check_root() {
|
|
if [[ $EUID -ne 0 ]]; then
|
|
error "This script must be run as root (use sudo)"
|
|
fi
|
|
}
|
|
|
|
check_raspberry_pi() {
|
|
if ! grep -q "Raspberry Pi\|BCM2" /proc/cpuinfo 2>/dev/null; then
|
|
error "This script is designed for Raspberry Pi only"
|
|
fi
|
|
|
|
# Detect Pi model for hardware recommendations
|
|
local model
|
|
model=$(tr -d '\0' < /proc/device-tree/model 2>/dev/null) || model="Unknown"
|
|
success "Detected: $model"
|
|
}
|
|
|
|
check_raspberry_pi_os() {
|
|
if [[ ! -f /etc/os-release ]]; then
|
|
error "Cannot detect operating system"
|
|
fi
|
|
|
|
. /etc/os-release
|
|
if [[ "$ID" != "raspbian" && "$ID" != "debian" ]]; then
|
|
warn "Expected Raspberry Pi OS (Debian-based), found: $ID"
|
|
if ! prompt_yes_no "Continue anyway?" "n"; then
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
success "OS: $PRETTY_NAME"
|
|
}
|
|
|
|
detect_python() {
|
|
local cmd=""
|
|
if command -v python3 &>/dev/null; then
|
|
cmd="python3"
|
|
elif command -v python &>/dev/null; then
|
|
local ver
|
|
ver=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1)
|
|
if [[ "$ver" -ge 3 ]]; then
|
|
cmd="python"
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$cmd" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
local version
|
|
version=$($cmd -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
|
local major minor
|
|
major=$(echo "$version" | cut -d'.' -f1)
|
|
minor=$(echo "$version" | cut -d'.' -f2)
|
|
|
|
if [[ "$major" -lt 3 ]] || { [[ "$major" -eq 3 ]] && [[ "$minor" -lt 10 ]]; }; then
|
|
warn "Python $version found, but 3.10+ is required"
|
|
return 1
|
|
fi
|
|
|
|
PYTHON_CMD="$cmd"
|
|
success "Found Python $version"
|
|
return 0
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Raspberry Pi Hardware Configuration
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
enable_spi() {
|
|
if raspi-config nonint get_spi 2>/dev/null | grep -q "1"; then
|
|
info "Enabling SPI..."
|
|
raspi-config nonint do_spi 0
|
|
REBOOT_NEEDED="true"
|
|
success "SPI enabled"
|
|
else
|
|
success "SPI already enabled"
|
|
fi
|
|
}
|
|
|
|
enable_i2c() {
|
|
if raspi-config nonint get_i2c 2>/dev/null | grep -q "1"; then
|
|
info "Enabling I2C..."
|
|
raspi-config nonint do_i2c 0
|
|
REBOOT_NEEDED="true"
|
|
success "I2C enabled"
|
|
else
|
|
success "I2C already enabled"
|
|
fi
|
|
}
|
|
|
|
configure_boot_config() {
|
|
# Find the boot config file (Bookworm+ uses /boot/firmware/config.txt)
|
|
local boot_config="/boot/firmware/config.txt"
|
|
if [[ ! -f "$boot_config" ]]; then
|
|
boot_config="/boot/config.txt"
|
|
fi
|
|
|
|
if [[ ! -f "$boot_config" ]]; then
|
|
warn "Boot config not found at /boot/firmware/config.txt or /boot/config.txt"
|
|
warn "You may need to manually add: dtparam=i2c_arm=on and dtoverlay=spi0-0cs"
|
|
return
|
|
fi
|
|
|
|
info "Configuring $boot_config..."
|
|
|
|
# Migrate legacy SpoolBuddy setting (bus 0 / i2c_vc) to bus 1 / i2c_arm.
|
|
if grep -q "^dtparam=i2c_vc=on" "$boot_config"; then
|
|
sed -i "s/^dtparam=i2c_vc=on$/# dtparam=i2c_vc=on (disabled by SpoolBuddy installer; use i2c_arm bus 1)/" "$boot_config"
|
|
REBOOT_NEEDED="true"
|
|
success "Disabled legacy dtparam=i2c_vc=on"
|
|
fi
|
|
|
|
if grep -q "^# SpoolBuddy: I2C bus 0 for NAU7802 scale (GPIO0/GPIO1)" "$boot_config"; then
|
|
sed -i "s/^# SpoolBuddy: I2C bus 0 for NAU7802 scale (GPIO0\/GPIO1)$/# SpoolBuddy: I2C bus 1 for NAU7802 scale (GPIO2\/GPIO3)/" "$boot_config"
|
|
fi
|
|
|
|
# Ensure I2C bus 1 (GPIO2/GPIO3) is enabled for NAU7802 scale
|
|
if ! grep -q "^dtparam=i2c_arm=on" "$boot_config"; then
|
|
echo "" >> "$boot_config"
|
|
echo "# SpoolBuddy: I2C bus 1 for NAU7802 scale (GPIO2/GPIO3)" >> "$boot_config"
|
|
echo "dtparam=i2c_arm=on" >> "$boot_config"
|
|
REBOOT_NEEDED="true"
|
|
success "Added dtparam=i2c_arm=on"
|
|
else
|
|
success "dtparam=i2c_arm=on already set"
|
|
fi
|
|
|
|
# Disable SPI auto chip-select (manual CS on GPIO23 for PN5180)
|
|
if ! grep -q "^dtoverlay=spi0-0cs" "$boot_config"; then
|
|
echo "" >> "$boot_config"
|
|
echo "# SpoolBuddy: Disable SPI auto CS (manual CS on GPIO23 for PN5180)" >> "$boot_config"
|
|
echo "dtoverlay=spi0-0cs" >> "$boot_config"
|
|
REBOOT_NEEDED="true"
|
|
success "Added dtoverlay=spi0-0cs"
|
|
else
|
|
success "dtoverlay=spi0-0cs already set"
|
|
fi
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Package Installation
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
install_system_packages() {
|
|
run_with_progress "Updating package lists" apt-get update
|
|
run_with_progress "Installing system packages" apt-get install -y $SYSTEM_PACKAGES
|
|
}
|
|
|
|
install_wifi_safeguard() {
|
|
# Protect WiFi credentials from being wiped by apt upgrades.
|
|
# Raspberry Pi OS Bookworm migrated from wpa_supplicant/dhcpcd to
|
|
# NetworkManager, but certain package upgrades (raspberrypi-sys-mods,
|
|
# raspi-config, NetworkManager itself) can delete saved connections
|
|
# from /etc/NetworkManager/system-connections/. This hook backs them
|
|
# up before dpkg runs and restores them if they vanish.
|
|
local hook_file="/etc/apt/apt.conf.d/80-preserve-wifi"
|
|
|
|
if [[ -f "$hook_file" ]]; then
|
|
success "WiFi safeguard already installed"
|
|
return
|
|
fi
|
|
|
|
# Only install if NetworkManager is the active network manager
|
|
if ! systemctl is-active --quiet NetworkManager 2>/dev/null; then
|
|
return
|
|
fi
|
|
|
|
# Write a helper script (avoids quote escaping issues in APT config)
|
|
local helper="/usr/local/sbin/preserve-wifi"
|
|
cat > "$helper" << 'HELPEREOF'
|
|
#!/bin/sh
|
|
# Called by APT hooks to preserve NetworkManager WiFi connections.
|
|
NM_DIR="/etc/NetworkManager/system-connections"
|
|
BAK_DIR="/etc/NetworkManager/system-connections.bak"
|
|
case "$1" in
|
|
backup)
|
|
if [ -d "$NM_DIR" ] && [ -n "$(ls -A "$NM_DIR" 2>/dev/null)" ]; then
|
|
cp -a "$NM_DIR/" "$BAK_DIR/"
|
|
fi
|
|
;;
|
|
restore)
|
|
if [ -d "$BAK_DIR" ] && [ -z "$(ls -A "$NM_DIR" 2>/dev/null)" ]; then
|
|
cp -a "$BAK_DIR"/* "$NM_DIR"/
|
|
nmcli general reload 2>/dev/null
|
|
fi
|
|
rm -rf "$BAK_DIR" 2>/dev/null
|
|
;;
|
|
esac
|
|
HELPEREOF
|
|
chmod +x "$helper"
|
|
|
|
cat > "$hook_file" << 'APTEOF'
|
|
// Preserve NetworkManager WiFi connections across apt upgrades.
|
|
// Installed by SpoolBuddy.
|
|
DPkg::Pre-Invoke {"/usr/local/sbin/preserve-wifi backup";};
|
|
DPkg::Post-Invoke {"/usr/local/sbin/preserve-wifi restore";};
|
|
APTEOF
|
|
|
|
success "WiFi safeguard installed (${hook_file})"
|
|
}
|
|
|
|
upgrade_system_packages() {
|
|
run_with_progress "Upgrading system packages" apt-get upgrade -y
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# SpoolBuddy Installation
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
create_spoolbuddy_user() {
|
|
if id "$SPOOLBUDDY_SERVICE_USER" &>/dev/null; then
|
|
info "User '$SPOOLBUDDY_SERVICE_USER' already exists"
|
|
# Ensure existing installs get a real shell for SSH access
|
|
usermod --shell /bin/bash "$SPOOLBUDDY_SERVICE_USER" 2>/dev/null || true
|
|
else
|
|
info "Creating service user '$SPOOLBUDDY_SERVICE_USER'..."
|
|
useradd --system --shell /bin/bash --home-dir "$INSTALL_PATH" "$SPOOLBUDDY_SERVICE_USER"
|
|
success "Service user created"
|
|
fi
|
|
|
|
# Add to hardware access groups (gpio, spi, i2c, video for backlight)
|
|
for group in gpio spi i2c video; do
|
|
if getent group "$group" &>/dev/null; then
|
|
usermod -aG "$group" "$SPOOLBUDDY_SERVICE_USER" 2>/dev/null || true
|
|
fi
|
|
done
|
|
success "User added to gpio, spi, i2c, video groups"
|
|
|
|
# Allow passwordless restart of daemon + kiosk (needed for SSH-based updates from Bambuddy)
|
|
cat > /etc/sudoers.d/spoolbuddy << 'SUDOERS'
|
|
spoolbuddy ALL=(root) NOPASSWD: /usr/bin/systemctl restart spoolbuddy.service
|
|
spoolbuddy ALL=(root) NOPASSWD: /usr/bin/systemctl restart getty@tty1.service
|
|
spoolbuddy ALL=(root) NOPASSWD: /usr/bin/find /home -maxdepth 5 *
|
|
spoolbuddy ALL=(root) NOPASSWD: /sbin/reboot
|
|
spoolbuddy ALL=(root) NOPASSWD: /sbin/shutdown -h now
|
|
SUDOERS
|
|
chmod 440 /etc/sudoers.d/spoolbuddy
|
|
success "Sudoers entries created for service, kiosk restart, reboot and shutdown"
|
|
}
|
|
|
|
download_spoolbuddy() {
|
|
if [[ -d "$INSTALL_PATH/.git" ]]; then
|
|
info "Existing installation found, updating..."
|
|
# Pre-emptively normalise tree ownership before git touches anything.
|
|
# Stray root-owned files (e.g. static/assets/* left by an earlier
|
|
# `sudo` run, or a frontend build that wrote as root) make
|
|
# `git reset --hard` fail with EACCES on the unlink/replace step,
|
|
# aborting the update before the post-install chown below ever
|
|
# runs. The script already runs as root (see check_root), so this
|
|
# is always safe.
|
|
chown -R "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$INSTALL_PATH"
|
|
git config --global --add safe.directory "$INSTALL_PATH" 2>/dev/null || true
|
|
cd "$INSTALL_PATH"
|
|
git remote set-url origin "$INSTALL_REPO" 2>/dev/null || true
|
|
run_with_progress "Fetching updates" git fetch origin
|
|
resolve_install_ref "$INSTALL_REF"
|
|
else
|
|
mkdir -p "$INSTALL_PATH"
|
|
run_with_progress "Cloning repository" git clone "$INSTALL_REPO" "$INSTALL_PATH"
|
|
cd "$INSTALL_PATH"
|
|
resolve_install_ref "$INSTALL_REF"
|
|
fi
|
|
|
|
chown -R "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$INSTALL_PATH"
|
|
}
|
|
|
|
setup_spoolbuddy_venv() {
|
|
cd "$INSTALL_PATH/spoolbuddy"
|
|
|
|
run_with_progress "Creating SpoolBuddy venv" $PYTHON_CMD -m venv --system-site-packages venv
|
|
run_with_progress "Upgrading pip" "$INSTALL_PATH/spoolbuddy/venv/bin/pip" install --upgrade pip
|
|
run_with_progress "Installing SpoolBuddy packages" "$INSTALL_PATH/spoolbuddy/venv/bin/pip" install $SPOOLBUDDY_PIP_PACKAGES
|
|
|
|
chown -R "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$INSTALL_PATH/spoolbuddy/venv"
|
|
}
|
|
|
|
create_spoolbuddy_env() {
|
|
info "Creating SpoolBuddy configuration..."
|
|
|
|
local env_file="$INSTALL_PATH/spoolbuddy/.env"
|
|
|
|
cat > "$env_file" << EOF
|
|
# SpoolBuddy Configuration
|
|
# Generated by install.sh on $(date)
|
|
|
|
# Bambuddy backend URL
|
|
SPOOLBUDDY_BACKEND_URL=$BAMBUDDY_URL
|
|
|
|
# API key (create one in Bambuddy Settings -> API Keys)
|
|
SPOOLBUDDY_API_KEY=$API_KEY
|
|
|
|
# NAU7802 scale bus (RPi GPIO2/GPIO3)
|
|
SPOOLBUDDY_I2C_BUS=1
|
|
EOF
|
|
|
|
chown "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$env_file"
|
|
# Keep secrets owner-writable while allowing kiosk user (in spoolbuddy group)
|
|
# to read backend URL/API key for dynamic launcher URL resolution.
|
|
chgrp "$SPOOLBUDDY_SERVICE_USER" "$env_file"
|
|
chmod 640 "$env_file"
|
|
success "Configuration saved to $env_file"
|
|
}
|
|
|
|
ensure_kiosk_env_access() {
|
|
local env_file="$INSTALL_PATH/spoolbuddy/.env"
|
|
|
|
if [[ ! -f "$env_file" ]]; then
|
|
warn "SpoolBuddy env file not found at $env_file"
|
|
return
|
|
fi
|
|
|
|
# Ensure kiosk user is known even when this function is called outside setup_kiosk.
|
|
if [[ -z "$KIOSK_USER" ]]; then
|
|
KIOSK_USER="${SUDO_USER:-$(logname 2>/dev/null || echo pi)}"
|
|
fi
|
|
|
|
if id "$KIOSK_USER" &>/dev/null; then
|
|
usermod -aG "$SPOOLBUDDY_SERVICE_USER" "$KIOSK_USER" 2>/dev/null || true
|
|
fi
|
|
|
|
chgrp "$SPOOLBUDDY_SERVICE_USER" "$env_file"
|
|
chmod 640 "$env_file"
|
|
|
|
if ! su -s /bin/sh -c "test -r '$env_file'" "$KIOSK_USER"; then
|
|
error "Kiosk user '$KIOSK_USER' cannot read $env_file (required for dynamic kiosk URL). Check groups/permissions."
|
|
fi
|
|
|
|
success "Verified kiosk user '$KIOSK_USER' can read SpoolBuddy env"
|
|
}
|
|
|
|
setup_ssh_key() {
|
|
info "Setting up SSH access for Bambuddy remote updates..."
|
|
|
|
local ssh_dir="$INSTALL_PATH/.ssh"
|
|
local auth_keys="$ssh_dir/authorized_keys"
|
|
|
|
mkdir -p "$ssh_dir"
|
|
chmod 700 "$ssh_dir"
|
|
|
|
if [[ -n "$SSH_PUBKEY" ]]; then
|
|
# Manual key provided via --ssh-pubkey flag
|
|
if [[ -f "$auth_keys" ]] && grep -qF "$SSH_PUBKEY" "$auth_keys" 2>/dev/null; then
|
|
info "SSH key already present in authorized_keys"
|
|
else
|
|
echo "$SSH_PUBKEY" >> "$auth_keys"
|
|
success "SSH public key added"
|
|
fi
|
|
else
|
|
# No manual key — the daemon will auto-deploy it on first registration
|
|
info "SSH key will be deployed automatically when the daemon connects to Bambuddy"
|
|
touch "$auth_keys"
|
|
fi
|
|
|
|
chmod 600 "$auth_keys"
|
|
chown -R "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$ssh_dir"
|
|
}
|
|
|
|
create_spoolbuddy_service() {
|
|
info "Creating SpoolBuddy systemd service..."
|
|
|
|
local after_line="After=network-online.target"
|
|
if [[ "$INSTALL_MODE" == "full" ]]; then
|
|
after_line="After=network-online.target bambuddy.service"
|
|
fi
|
|
|
|
cat > /etc/systemd/system/spoolbuddy.service << EOF
|
|
[Unit]
|
|
Description=SpoolBuddy - NFC Spool Management Daemon
|
|
Documentation=https://github.com/maziggy/bambuddy
|
|
$after_line
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=$SPOOLBUDDY_SERVICE_USER
|
|
WorkingDirectory=$INSTALL_PATH/spoolbuddy
|
|
EnvironmentFile=$INSTALL_PATH/spoolbuddy/.env
|
|
ExecStart=$INSTALL_PATH/spoolbuddy/venv/bin/python -m daemon.main
|
|
Restart=always
|
|
RestartSec=5
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable spoolbuddy.service
|
|
success "SpoolBuddy service created and enabled"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Bambuddy Installation (full mode only)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
create_bambuddy_user() {
|
|
if id "$BAMBUDDY_SERVICE_USER" &>/dev/null; then
|
|
info "User '$BAMBUDDY_SERVICE_USER' already exists"
|
|
return
|
|
fi
|
|
|
|
info "Creating service user '$BAMBUDDY_SERVICE_USER'..."
|
|
useradd --system --shell /usr/sbin/nologin --home-dir "$INSTALL_PATH" "$BAMBUDDY_SERVICE_USER"
|
|
success "Service user created"
|
|
}
|
|
|
|
setup_bambuddy_venv() {
|
|
cd "$INSTALL_PATH"
|
|
|
|
run_with_progress "Creating Bambuddy venv" $PYTHON_CMD -m venv venv
|
|
run_with_progress "Upgrading pip" "$INSTALL_PATH/venv/bin/pip" install --upgrade pip
|
|
run_with_progress "Installing Bambuddy dependencies" "$INSTALL_PATH/venv/bin/pip" install -r requirements.txt
|
|
|
|
chown -R "$BAMBUDDY_SERVICE_USER:$BAMBUDDY_SERVICE_USER" "$INSTALL_PATH/venv"
|
|
}
|
|
|
|
install_nodejs() {
|
|
if command -v node &>/dev/null; then
|
|
local version
|
|
version=$(node --version 2>/dev/null | sed 's/^v//')
|
|
local major
|
|
major=$(echo "$version" | cut -d'.' -f1)
|
|
if [[ "$major" -ge 20 ]]; then
|
|
success "Found Node.js v$version"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
apt-get remove -y nodejs npm > /dev/null 2>&1 || true
|
|
run_with_progress "Setting up Node.js repository" bash -c "curl -fsSL https://deb.nodesource.com/setup_22.x | bash -"
|
|
run_with_progress "Installing Node.js" apt-get install -y nodejs
|
|
hash -r 2>/dev/null || true
|
|
success "Node.js installed: $(node --version)"
|
|
}
|
|
|
|
build_frontend() {
|
|
cd "$INSTALL_PATH/frontend"
|
|
|
|
run_with_progress "Installing frontend dependencies" npm ci
|
|
run_with_progress "Building frontend" npm run build
|
|
}
|
|
|
|
create_bambuddy_env() {
|
|
info "Creating Bambuddy configuration..."
|
|
|
|
local env_file="$INSTALL_PATH/.env"
|
|
|
|
cat > "$env_file" << EOF
|
|
# Bambuddy Configuration
|
|
# Generated by install.sh on $(date)
|
|
|
|
DEBUG=false
|
|
LOG_LEVEL=INFO
|
|
LOG_TO_FILE=true
|
|
EOF
|
|
|
|
chown "$BAMBUDDY_SERVICE_USER:$BAMBUDDY_SERVICE_USER" "$env_file"
|
|
chmod 600 "$env_file"
|
|
success "Configuration saved to $env_file"
|
|
}
|
|
|
|
create_bambuddy_directories() {
|
|
mkdir -p "$INSTALL_PATH/data" "$INSTALL_PATH/logs"
|
|
chown -R "$BAMBUDDY_SERVICE_USER:$BAMBUDDY_SERVICE_USER" "$INSTALL_PATH/data" "$INSTALL_PATH/logs"
|
|
success "Data directories created"
|
|
}
|
|
|
|
create_bambuddy_service() {
|
|
info "Creating Bambuddy systemd service..."
|
|
|
|
# Overwriting the unit used to silently drop any ReadWritePaths the operator
|
|
# had added — a NAS share for Scheduled Backups, typically — after which the
|
|
# backups failed with EROFS, which looks like a permission problem and is not
|
|
# one (issue #2544). Back the old unit up and carry those paths forward.
|
|
local existing_unit="/etc/systemd/system/bambuddy.service"
|
|
local extra_rw=""
|
|
if [[ -f "$existing_unit" ]]; then
|
|
local backup_unit="${existing_unit}.bak-$(date +%Y%m%d-%H%M%S)"
|
|
cp "$existing_unit" "$backup_unit"
|
|
info "Existing service backed up to $backup_unit"
|
|
|
|
local prev_rw p
|
|
prev_rw=$(grep -hE '^ReadWritePaths=' "$existing_unit" 2>/dev/null | sed 's/^ReadWritePaths=//' || true)
|
|
for p in $prev_rw; do
|
|
case "$p" in
|
|
"$INSTALL_PATH/data" | "$INSTALL_PATH/logs" | "$INSTALL_PATH") continue ;;
|
|
esac
|
|
extra_rw+=" $p"
|
|
done
|
|
if [[ -n "$extra_rw" ]]; then
|
|
info "Keeping custom writable paths from the previous service:$extra_rw"
|
|
fi
|
|
fi
|
|
|
|
cat > /etc/systemd/system/bambuddy.service << EOF
|
|
[Unit]
|
|
Description=Bambuddy - Bambu Lab Print Management
|
|
Documentation=https://github.com/maziggy/bambuddy
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=$BAMBUDDY_SERVICE_USER
|
|
Group=$BAMBUDDY_SERVICE_USER
|
|
WorkingDirectory=$INSTALL_PATH
|
|
EnvironmentFile=$INSTALL_PATH/.env
|
|
Environment="DATA_DIR=$INSTALL_PATH/data"
|
|
Environment="LOG_DIR=$INSTALL_PATH/logs"
|
|
# --loop asyncio required: uvloop can truncate VP FTP uploads (#1896)
|
|
# --timeout-graceful-shutdown required: uvicorn otherwise waits forever for
|
|
# in-flight requests, and an MJPEG camera stream never completes — one open
|
|
# camera tile hangs the stop until systemd SIGKILLs, skipping the WAL
|
|
# checkpoint and the MQTT / virtual-printer teardown. A kiosk sitting on the
|
|
# printers page holds exactly such a stream open, so this bites every reboot.
|
|
ExecStart=$INSTALL_PATH/venv/bin/uvicorn backend.app.main:app --host 0.0.0.0 --port $BAMBUDDY_PORT --loop asyncio --timeout-graceful-shutdown 5
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
# Backstop only — uvicorn bounds its own wait at 5s and teardown takes ~1-2s.
|
|
TimeoutStopSec=30
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
ProtectSystem=strict
|
|
ProtectHome=true
|
|
# ProtectSystem=strict makes every path outside the ones below read-only for this
|
|
# service. To back up to a NAS share, add it here or in a drop-in that survives a
|
|
# reinstall: sudo systemctl edit bambuddy → [Service] → ReadWritePaths=/mnt/share
|
|
ReadWritePaths=$INSTALL_PATH/data $INSTALL_PATH/logs $INSTALL_PATH$extra_rw
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable bambuddy.service
|
|
success "Bambuddy service created and enabled"
|
|
}
|
|
|
|
bootstrap_spoolbuddy_kiosk_key() {
|
|
# Provision an API key for the local SpoolBuddy kiosk and write it into
|
|
# spoolbuddy/.env. Runs against the Bambuddy DB directly (via the CLI),
|
|
# so the bambuddy service does not need to be running yet.
|
|
info "Provisioning SpoolBuddy kiosk API key..."
|
|
|
|
local env_file="$INSTALL_PATH/spoolbuddy/.env"
|
|
if [[ ! -f "$env_file" ]]; then
|
|
warn "SpoolBuddy env file not found at $env_file — skipping kiosk key bootstrap"
|
|
return
|
|
fi
|
|
|
|
# CWD must be $INSTALL_PATH so `python -m backend.app.cli` finds the backend
|
|
# package on sys.path (matches the systemd unit's WorkingDirectory).
|
|
local kiosk_key
|
|
if ! kiosk_key="$(cd "$INSTALL_PATH" && sudo -u "$BAMBUDDY_SERVICE_USER" \
|
|
env DATA_DIR="$INSTALL_PATH/data" LOG_DIR="$INSTALL_PATH/logs" \
|
|
"$INSTALL_PATH/venv/bin/python" -m backend.app.cli kiosk-bootstrap --force)"; then
|
|
error "Failed to bootstrap SpoolBuddy kiosk API key"
|
|
fi
|
|
|
|
if [[ -z "$kiosk_key" || "$kiosk_key" != bb_* ]]; then
|
|
error "CLI returned an invalid API key (got: ${kiosk_key:0:8}...)"
|
|
fi
|
|
|
|
if ! grep -q '^SPOOLBUDDY_API_KEY=' "$env_file"; then
|
|
error "Sentinel 'SPOOLBUDDY_API_KEY=' line missing in $env_file"
|
|
fi
|
|
|
|
# Escape for sed replacement (the key is base64url-safe, no slashes, but be defensive)
|
|
local escaped_key
|
|
escaped_key=$(printf '%s\n' "$kiosk_key" | sed -e 's/[\/&]/\\&/g')
|
|
sed -i "s/^SPOOLBUDDY_API_KEY=.*/SPOOLBUDDY_API_KEY=${escaped_key}/" "$env_file"
|
|
|
|
success "SpoolBuddy kiosk API key provisioned"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# System Strip-Down (dedicated appliance — remove unnecessary services/packages)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
strip_services() {
|
|
info "Disabling unnecessary services..."
|
|
|
|
local services=(
|
|
bluetooth.service
|
|
lightdm.service
|
|
cloud-init-local.service
|
|
cloud-init.service
|
|
cloud-init-network.service
|
|
cloud-config.service
|
|
cloud-final.service
|
|
cloud-init-hotplugd.socket
|
|
avahi-daemon.service
|
|
avahi-daemon.socket
|
|
ModemManager.service
|
|
udisks2.service
|
|
apparmor.service
|
|
man-db.timer
|
|
e2scrub_all.timer
|
|
e2scrub_reap.service
|
|
# Audio stack (no speakers on a spool reader)
|
|
pipewire.service
|
|
pipewire.socket
|
|
pipewire-pulse.service
|
|
pipewire-pulse.socket
|
|
wireplumber.service
|
|
# Printing
|
|
cups.service
|
|
cups.socket
|
|
cups-browsed.service
|
|
# Desktop services
|
|
accounts-daemon.service
|
|
upower.service
|
|
polkit.service
|
|
# Flatpak portals (not using Flatpak)
|
|
xdg-desktop-portal.service
|
|
xdg-desktop-portal-gtk.service
|
|
xdg-document-portal.service
|
|
xdg-permission-store.service
|
|
# NFS/RPC (unnecessary + security surface)
|
|
rpcbind.service
|
|
rpcbind.socket
|
|
# Bluetooth media proxy
|
|
mpris-proxy.service
|
|
)
|
|
|
|
local disabled=0
|
|
for svc in "${services[@]}"; do
|
|
if systemctl is-enabled "$svc" &>/dev/null; then
|
|
systemctl disable "$svc" 2>/dev/null || true
|
|
systemctl mask "$svc" 2>/dev/null || true
|
|
(( ++disabled ))
|
|
fi
|
|
done
|
|
|
|
if (( disabled > 0 )); then
|
|
success "Disabled $disabled unnecessary services"
|
|
else
|
|
success "No unnecessary services to disable"
|
|
fi
|
|
|
|
# Mask user-level services globally via /etc/systemd/user/ overrides.
|
|
# The su-based approach doesn't reliably reach the user's systemd instance
|
|
# when run from sudo, so we create global masks that apply before login.
|
|
local user_services=(
|
|
pipewire.service
|
|
pipewire.socket
|
|
pipewire-pulse.service
|
|
pipewire-pulse.socket
|
|
wireplumber.service
|
|
xdg-desktop-portal.service
|
|
xdg-desktop-portal-gtk.service
|
|
xdg-document-portal.service
|
|
xdg-permission-store.service
|
|
mpris-proxy.service
|
|
)
|
|
mkdir -p /etc/systemd/user
|
|
local user_masked=0
|
|
for svc in "${user_services[@]}"; do
|
|
if [[ ! -L "/etc/systemd/user/$svc" ]] || [[ "$(readlink /etc/systemd/user/$svc)" != "/dev/null" ]]; then
|
|
ln -sf /dev/null "/etc/systemd/user/$svc"
|
|
(( ++user_masked ))
|
|
fi
|
|
done
|
|
if (( user_masked > 0 )); then
|
|
success "Masked $user_masked unnecessary user services globally"
|
|
fi
|
|
}
|
|
|
|
strip_packages() {
|
|
info "Removing unnecessary packages..."
|
|
|
|
local packages=(
|
|
mkvtoolnix
|
|
firmware-atheros
|
|
firmware-mediatek
|
|
cloud-init
|
|
rpi-cloud-init-mods
|
|
rpi-connect-lite
|
|
avahi-daemon
|
|
modemmanager
|
|
udisks2
|
|
pipewire
|
|
pipewire-pulse
|
|
wireplumber
|
|
cups
|
|
cups-browsed
|
|
cups-common
|
|
cups-client
|
|
rpcbind
|
|
)
|
|
|
|
local to_remove=()
|
|
for pkg in "${packages[@]}"; do
|
|
if dpkg -l "$pkg" &>/dev/null 2>&1; then
|
|
to_remove+=("$pkg")
|
|
fi
|
|
done
|
|
|
|
if (( ${#to_remove[@]} > 0 )); then
|
|
run_with_progress "Removing ${#to_remove[@]} packages" apt-get remove --purge -y "${to_remove[@]}"
|
|
run_with_progress "Cleaning up dependencies" apt-get autoremove --purge -y
|
|
else
|
|
success "No unnecessary packages to remove"
|
|
fi
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Kiosk Setup (labwc + Chromium + Plymouth splash)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
setup_kiosk() {
|
|
info "Setting up touchscreen kiosk..."
|
|
|
|
# Detect kiosk user (the human user who ran sudo)
|
|
KIOSK_USER="${SUDO_USER:-$(logname 2>/dev/null || echo pi)}"
|
|
KIOSK_URL="${BAMBUDDY_URL}/spoolbuddy?token=${API_KEY}"
|
|
local KIOSK_HOME
|
|
KIOSK_HOME=$(eval echo "~$KIOSK_USER")
|
|
|
|
info "Kiosk user: $KIOSK_USER (home: $KIOSK_HOME)"
|
|
info "Kiosk URL: $KIOSK_URL"
|
|
|
|
# Allow kiosk user to read SpoolBuddy env so launcher can resolve backend URL
|
|
# and API key dynamically instead of using stale install-time fallback values.
|
|
local spoolbuddy_env="$INSTALL_PATH/spoolbuddy/.env"
|
|
if [[ -f "$spoolbuddy_env" ]]; then
|
|
usermod -aG "$SPOOLBUDDY_SERVICE_USER" "$KIOSK_USER" 2>/dev/null || true
|
|
chgrp "$SPOOLBUDDY_SERVICE_USER" "$spoolbuddy_env" 2>/dev/null || true
|
|
chmod 640 "$spoolbuddy_env" 2>/dev/null || true
|
|
fi
|
|
|
|
# ── Install kiosk packages ────────────────────────────────────────────
|
|
# Temporarily block initramfs rebuilds during package install — we rebuild
|
|
# once at the end after the Plymouth theme is configured, saving ~4 runs
|
|
# (one per installed kernel per hook trigger).
|
|
if [[ -x /usr/sbin/update-initramfs ]]; then
|
|
dpkg-divert --local --rename --add /usr/sbin/update-initramfs >/dev/null 2>&1 || true
|
|
ln -sf /bin/true /usr/sbin/update-initramfs
|
|
fi
|
|
run_with_progress "Installing kiosk packages" apt-get install -y labwc chromium plymouth wlr-randr swayidle wlopm jq curl
|
|
# Restore real update-initramfs
|
|
if dpkg-divert --list /usr/sbin/update-initramfs 2>/dev/null | grep -q local; then
|
|
rm -f /usr/sbin/update-initramfs
|
|
dpkg-divert --local --rename --remove /usr/sbin/update-initramfs >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# ── config.txt tweaks ─────────────────────────────────────────────────
|
|
local boot_config="/boot/firmware/config.txt"
|
|
if [[ ! -f "$boot_config" ]]; then
|
|
boot_config="/boot/config.txt"
|
|
fi
|
|
|
|
if [[ -f "$boot_config" ]]; then
|
|
info "Configuring $boot_config for kiosk..."
|
|
|
|
# Disable audio (change existing on→off)
|
|
sed -i 's/^dtparam=audio=on/dtparam=audio=off/' "$boot_config"
|
|
|
|
# Disable camera auto-detect (change existing 1→0)
|
|
sed -i 's/^camera_auto_detect=1/camera_auto_detect=0/' "$boot_config"
|
|
|
|
# Append if missing: gpu_mem=32
|
|
if ! grep -q "^gpu_mem=" "$boot_config"; then
|
|
echo "" >> "$boot_config"
|
|
echo "# Kiosk: Minimal GPU firmware memory (KMS uses CMA from system RAM)" >> "$boot_config"
|
|
echo "gpu_mem=32" >> "$boot_config"
|
|
fi
|
|
|
|
# Append if missing: dtoverlay=disable-bt
|
|
if ! grep -q "^dtoverlay=disable-bt" "$boot_config"; then
|
|
echo "" >> "$boot_config"
|
|
echo "# Kiosk: Disable Bluetooth hardware" >> "$boot_config"
|
|
echo "dtoverlay=disable-bt" >> "$boot_config"
|
|
fi
|
|
|
|
# Append if missing: disable_splash=1
|
|
if ! grep -q "^disable_splash=" "$boot_config"; then
|
|
echo "" >> "$boot_config"
|
|
echo "# Kiosk: Disable Raspberry Pi firmware splash, use custom splash.png" >> "$boot_config"
|
|
echo "disable_splash=1" >> "$boot_config"
|
|
fi
|
|
|
|
success "Boot config updated"
|
|
fi
|
|
|
|
# ── cmdline.txt tweaks ────────────────────────────────────────────────
|
|
local cmdline="/boot/firmware/cmdline.txt"
|
|
if [[ ! -f "$cmdline" ]]; then
|
|
cmdline="/boot/cmdline.txt"
|
|
fi
|
|
|
|
if [[ -f "$cmdline" ]]; then
|
|
info "Configuring $cmdline for kiosk..."
|
|
|
|
# Remove serial console (Plymouth needs tty-only console)
|
|
sed -i 's/console=serial0,[0-9]* //' "$cmdline"
|
|
|
|
# Disable console blanking (kernel default is 600s, can blank during boot transition)
|
|
grep -q "consoleblank=" "$cmdline" || sed -i 's/$/ consoleblank=0/' "$cmdline"
|
|
|
|
# Add splash quiet loglevel=3 logo.nologo if missing
|
|
grep -q "splash" "$cmdline" || sed -i 's/$/ splash quiet loglevel=3 logo.nologo/' "$cmdline"
|
|
|
|
# Add video mode if missing
|
|
grep -q "video=HDMI-A-1" "$cmdline" || sed -i 's/$/ video=HDMI-A-1:1024x600@60/' "$cmdline"
|
|
|
|
success "Kernel cmdline updated"
|
|
fi
|
|
|
|
# ── Plymouth splash theme ─────────────────────────────────────────────
|
|
info "Installing Plymouth boot splash..."
|
|
local theme_dir="/usr/share/plymouth/themes/spoolbuddy"
|
|
mkdir -p "$theme_dir"
|
|
|
|
# Copy bundled splash image from the install directory
|
|
local script_dir
|
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
if [[ -f "$script_dir/splash.png" ]]; then
|
|
cp "$script_dir/splash.png" "$theme_dir/splash.png"
|
|
elif [[ -f "$INSTALL_PATH/spoolbuddy/install/splash.png" ]]; then
|
|
cp "$INSTALL_PATH/spoolbuddy/install/splash.png" "$theme_dir/splash.png"
|
|
else
|
|
warn "splash.png not found — Plymouth splash will not display an image"
|
|
fi
|
|
|
|
# Write .plymouth theme file
|
|
cat > "$theme_dir/spoolbuddy.plymouth" << 'EOF'
|
|
[Plymouth Theme]
|
|
Name=SpoolBuddy
|
|
Description=SpoolBuddy boot splash
|
|
ModuleName=script
|
|
|
|
[script]
|
|
ImageDir=/usr/share/plymouth/themes/spoolbuddy
|
|
ScriptFile=/usr/share/plymouth/themes/spoolbuddy/spoolbuddy.script
|
|
EOF
|
|
|
|
# Write .script theme file
|
|
cat > "$theme_dir/spoolbuddy.script" << 'EOF'
|
|
wallpaper_image = Image("splash.png");
|
|
screen_width = Window.GetWidth();
|
|
screen_height = Window.GetHeight();
|
|
resized_wallpaper_image = wallpaper_image.Scale(screen_width, screen_height);
|
|
wallpaper_sprite = Sprite(resized_wallpaper_image);
|
|
wallpaper_sprite.SetZ(-100);
|
|
EOF
|
|
|
|
plymouth-set-default-theme spoolbuddy
|
|
run_with_progress "Updating initramfs" update-initramfs -u
|
|
success "Plymouth splash installed"
|
|
|
|
# ── Auto-login on tty1 ────────────────────────────────────────────────
|
|
info "Configuring auto-login for $KIOSK_USER..."
|
|
mkdir -p /etc/systemd/system/getty@tty1.service.d
|
|
|
|
cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << EOF
|
|
[Unit]
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
ExecStart=
|
|
ExecStart=-/sbin/agetty --autologin $KIOSK_USER --noclear %I \$TERM
|
|
EOF
|
|
|
|
success "Auto-login configured"
|
|
|
|
# ── labwc rc.xml (no decorations, no keybinds) ────────────────────────
|
|
info "Configuring labwc window manager..."
|
|
local labwc_dir="$KIOSK_HOME/.config/labwc"
|
|
mkdir -p "$labwc_dir"
|
|
|
|
cat > "$labwc_dir/rc.xml" << 'EOF'
|
|
<?xml version="1.0"?>
|
|
<labwc_config>
|
|
|
|
<!-- Disable screen blanking — kiosk must stay on -->
|
|
<core>
|
|
<screenBlankTimeout>0</screenBlankTimeout>
|
|
</core>
|
|
|
|
<theme>
|
|
<name></name>
|
|
<cornerRadius>0</cornerRadius>
|
|
</theme>
|
|
|
|
<!-- Disable all keybindings - kiosk lockdown -->
|
|
<keyboard>
|
|
</keyboard>
|
|
|
|
<!-- Disable right-click menu -->
|
|
<mouse>
|
|
<default />
|
|
</mouse>
|
|
|
|
<!-- Remove window decorations, maximize Chromium, prevent unfullscreen -->
|
|
<windowRules>
|
|
<windowRule identifier="*">
|
|
<serverDecoration>no</serverDecoration>
|
|
</windowRule>
|
|
<windowRule identifier="chromium">
|
|
<skipTaskbar>yes</skipTaskbar>
|
|
<fixedPosition>yes</fixedPosition>
|
|
</windowRule>
|
|
</windowRules>
|
|
|
|
</labwc_config>
|
|
EOF
|
|
|
|
# ── Override Debian/RPi Chromium defaults for kiosk performance ──────
|
|
cat > /etc/chromium.d/spoolbuddy-kiosk << 'CHROMIUM_EOF'
|
|
# SpoolBuddy kiosk: add kiosk-specific flags on top of Pi defaults.
|
|
# Preserves Pi GPU settings (gpu-rasterization, ANGLE/GLES) for stability.
|
|
CHROMIUM_FLAGS="$CHROMIUM_FLAGS --disable-smooth-scrolling"
|
|
CHROMIUM_FLAGS="$CHROMIUM_FLAGS --disable-extensions"
|
|
CHROMIUM_FLAGS="$CHROMIUM_FLAGS --disable-background-timer-throttling"
|
|
CHROMIUM_FLAGS="$CHROMIUM_FLAGS --disable-renderer-backgrounding"
|
|
CHROMIUM_FLAGS="$CHROMIUM_FLAGS --disable-crash-reporter"
|
|
CHROMIUM_EOF
|
|
success "Chromium kiosk performance flags installed"
|
|
|
|
# ── kiosk launcher (dynamic URL from spoolbuddy/.env) ─────────────────
|
|
local kiosk_launcher="/usr/local/bin/spoolbuddy-kiosk-launch"
|
|
cat > "$kiosk_launcher" << EOF
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ENV_FILE="$INSTALL_PATH/spoolbuddy/.env"
|
|
FALLBACK_URL="$KIOSK_URL"
|
|
|
|
backend_url=""
|
|
api_key=""
|
|
|
|
if [[ -r "\$ENV_FILE" ]]; then
|
|
backend_url="\$(sed -n 's/^SPOOLBUDDY_BACKEND_URL=//p' "\$ENV_FILE" | tail -n1 | tr -d '\r')"
|
|
api_key="\$(sed -n 's/^SPOOLBUDDY_API_KEY=//p' "\$ENV_FILE" | tail -n1 | tr -d '\r')"
|
|
backend_url="\${backend_url%\"}"
|
|
backend_url="\${backend_url#\"}"
|
|
api_key="\${api_key%\"}"
|
|
api_key="\${api_key#\"}"
|
|
elif [[ -f "\$ENV_FILE" ]]; then
|
|
echo "spoolbuddy-kiosk-launch: ERROR: \$ENV_FILE exists but is not readable" >&2
|
|
echo "spoolbuddy-kiosk-launch: Fix permissions (group-readable by kiosk user) and restart kiosk" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "\$backend_url" && -n "\$api_key" ]]; then
|
|
backend_url="\${backend_url%/}"
|
|
kiosk_url="\${backend_url}/spoolbuddy?token=\${api_key}"
|
|
else
|
|
kiosk_url="\$FALLBACK_URL"
|
|
fi
|
|
|
|
# Wait for the Bambuddy backend to be reachable before launching Chromium.
|
|
# Without this the browser opens before uvicorn has bound to the port on a
|
|
# cold boot and the user sees an ERR_CONNECTION_REFUSED splash until they
|
|
# manually reload. Probe /health (no auth, no body) with a short timeout.
|
|
probe_url="\${backend_url:-http://localhost}/health"
|
|
for _i in \$(seq 1 60); do
|
|
if curl -sf --max-time 2 "\$probe_url" >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
# Ephemeral user-data-dir under /tmp + wipe on every launch.
|
|
#
|
|
# The kiosk has no per-user state worth persisting (the auth token is in
|
|
# the URL query, not a stored cookie), but the default profile at
|
|
# ~/.config/chromium was accumulating two specific kinds of state across
|
|
# reboots that broke deploys badly:
|
|
#
|
|
# 1. HTTP disk cache holding old index.html across browser restarts.
|
|
# Chromium's heuristic-cache freshness window kept the old HTML
|
|
# "fresh" for days, which referenced an old content-hashed bundle,
|
|
# so newly deployed code never reached the running tab even after
|
|
# pkill+relaunch. Reproduced in the wild during the #1133 rollout
|
|
# — the kiosk kept showing the pre-fix picker for hours after every
|
|
# cache-clear attempt because the persistent profile would re-seed
|
|
# the cache from disk on next start.
|
|
# 2. A stuck Service Worker registration, which intercepted requests
|
|
# with its own cache layer (CacheStorage), independent of the HTTP
|
|
# cache. Even after \`rm -rf Default/Cache/*\` the SW could replay
|
|
# stale responses from CacheStorage until explicitly unregistered.
|
|
#
|
|
# Wiping the user-data-dir on every launch is the simplest, most
|
|
# bulletproof escape hatch — every kiosk restart is now functionally
|
|
# equivalent to a private-window first-load. Future deploys propagate
|
|
# automatically: the next chromium launch picks up the latest bundle
|
|
# without any extra tooling. Trade-off is a slightly slower first paint
|
|
# (no warm cache) and zero offline support, neither of which matter for
|
|
# a single-purpose kiosk facing a backend on the same LAN.
|
|
USER_DATA_DIR="/tmp/spoolbuddy-kiosk-userdata"
|
|
rm -rf "\$USER_DATA_DIR"
|
|
|
|
exec chromium --kiosk --no-first-run --disable-infobars \
|
|
--disable-session-crashed-bubble --disable-features=TranslateUI \
|
|
--noerrdialogs --disable-component-update \
|
|
--overscroll-history-navigation=0 \
|
|
--ozone-platform=wayland \
|
|
--disable-crash-reporter --disable-breakpad \
|
|
--user-data-dir="\$USER_DATA_DIR" \
|
|
"\$kiosk_url"
|
|
EOF
|
|
|
|
chmod 755 "$kiosk_launcher"
|
|
|
|
# Tiny self-check: ensure sed command substitutions were not expanded
|
|
# while generating the launcher script.
|
|
if ! grep -Fq 'backend_url="$(sed -n' "$kiosk_launcher" || ! grep -Fq 'api_key="$(sed -n' "$kiosk_launcher"; then
|
|
error "Kiosk launcher generation failed: dynamic env parsing commands were expanded unexpectedly"
|
|
fi
|
|
|
|
# ── labwc autostart ───────────────────────────────────────────────────
|
|
cat > "$labwc_dir/autostart" << EOF
|
|
# Force 1024x600 (panel doesn't advertise this natively)
|
|
wlr-randr --output HDMI-A-1 --custom-mode 1024x600@60 &
|
|
|
|
# Idle watchdog: powers off HDMI via wlopm after the configured inactivity
|
|
# timeout (SpoolBuddy Settings → Display → Screen blank timeout). Reads the
|
|
# current value from the backend on startup; UI changes take effect on the
|
|
# next reboot / kiosk restart.
|
|
$INSTALL_PATH/spoolbuddy/install/spoolbuddy-idle.sh &
|
|
|
|
# Launch Chromium via helper that resolves URL from spoolbuddy/.env
|
|
$kiosk_launcher &
|
|
EOF
|
|
|
|
chown -R "$KIOSK_USER:$KIOSK_USER" "$labwc_dir"
|
|
|
|
# ── .bash_profile (source .bashrc, exec labwc on tty1) ────────────────
|
|
cat > "$KIOSK_HOME/.bash_profile" << 'EOF'
|
|
# Source .bashrc if it exists
|
|
if [ -f ~/.bashrc ]; then
|
|
. ~/.bashrc
|
|
fi
|
|
|
|
# Auto-start kiosk on tty1
|
|
if [ "$(tty)" = "/dev/tty1" ]; then
|
|
exec labwc
|
|
fi
|
|
EOF
|
|
|
|
chown "$KIOSK_USER:$KIOSK_USER" "$KIOSK_HOME/.bash_profile"
|
|
|
|
REBOOT_NEEDED="true"
|
|
success "Kiosk setup complete"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# User Prompts
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--mode)
|
|
INSTALL_MODE="$2"
|
|
shift 2
|
|
;;
|
|
--repo)
|
|
INSTALL_REPO="$(normalize_github_repo_url "$2")"
|
|
shift 2
|
|
;;
|
|
--ref)
|
|
INSTALL_REF="$2"
|
|
shift 2
|
|
;;
|
|
--bambuddy-url)
|
|
BAMBUDDY_URL="$2"
|
|
shift 2
|
|
;;
|
|
--api-key)
|
|
API_KEY="$2"
|
|
shift 2
|
|
;;
|
|
--path)
|
|
INSTALL_PATH="$2"
|
|
shift 2
|
|
;;
|
|
--port)
|
|
BAMBUDDY_PORT="$2"
|
|
shift 2
|
|
;;
|
|
--ssh-pubkey)
|
|
SSH_PUBKEY="$2"
|
|
shift 2
|
|
;;
|
|
--yes|-y)
|
|
NON_INTERACTIVE="true"
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
show_help
|
|
;;
|
|
*)
|
|
error "Unknown option: $1 (use --help for usage)"
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
ask_install_mode() {
|
|
if [[ -n "$INSTALL_MODE" ]]; then
|
|
return
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${BOLD}How would you like to set up SpoolBuddy?${NC}"
|
|
echo ""
|
|
echo -e " ${CYAN}1)${NC} SpoolBuddy only"
|
|
echo " NFC reader + scale on this RPi, Bambuddy runs on another device"
|
|
echo ""
|
|
echo -e " ${CYAN}2)${NC} SpoolBuddy + Bambuddy"
|
|
echo " Both running natively on this Raspberry Pi"
|
|
echo ""
|
|
|
|
while true; do
|
|
echo -en "${BOLD}Choose${NC} [${CYAN}1${NC}/${CYAN}2${NC}]: "
|
|
read -r choice
|
|
case "$choice" in
|
|
1) INSTALL_MODE="spoolbuddy"; return;;
|
|
2) INSTALL_MODE="full"; return;;
|
|
*) echo "Please enter 1 or 2.";;
|
|
esac
|
|
done
|
|
}
|
|
|
|
gather_config() {
|
|
echo ""
|
|
echo -e "${BOLD}Configuration${NC}"
|
|
echo -e "${CYAN}─────────────────────────────────────────${NC}"
|
|
echo ""
|
|
|
|
# Set default install path based on mode
|
|
if [[ -z "$INSTALL_PATH" ]]; then
|
|
if [[ "$INSTALL_MODE" == "full" ]]; then
|
|
INSTALL_PATH="/opt/bambuddy"
|
|
else
|
|
INSTALL_PATH="/opt/bambuddy"
|
|
fi
|
|
fi
|
|
prompt "Installation directory" "$INSTALL_PATH" INSTALL_PATH
|
|
|
|
if [[ -z "$INSTALL_REPO" ]]; then
|
|
INSTALL_REPO="$GITHUB_REPO"
|
|
fi
|
|
prompt "Git repository URL" "$INSTALL_REPO" INSTALL_REPO
|
|
INSTALL_REPO="$(normalize_github_repo_url "$INSTALL_REPO")"
|
|
|
|
if [[ -z "$INSTALL_REF" ]]; then
|
|
INSTALL_REF="main"
|
|
fi
|
|
|
|
if [[ "$NON_INTERACTIVE" != "true" && -n "$DETECTED_INSTALLER_REF" && "$DETECTED_INSTALLER_REF" != "HEAD" ]]; then
|
|
echo ""
|
|
echo -e "${BOLD}Install Source Ref${NC}"
|
|
echo "1) main"
|
|
echo "2) $DETECTED_INSTALLER_REF (detected from installer context)"
|
|
echo "3) custom"
|
|
while true; do
|
|
echo -en "${BOLD}Choose${NC} [1/2/3]: "
|
|
read -r ref_choice
|
|
case "$ref_choice" in
|
|
""|1)
|
|
INSTALL_REF="main"
|
|
break
|
|
;;
|
|
2)
|
|
INSTALL_REF="$DETECTED_INSTALLER_REF"
|
|
break
|
|
;;
|
|
3)
|
|
prompt "Git ref (branch/tag/commit)" "$INSTALL_REF" INSTALL_REF
|
|
break
|
|
;;
|
|
*)
|
|
echo "Please enter 1, 2, or 3."
|
|
;;
|
|
esac
|
|
done
|
|
else
|
|
prompt "Git ref (branch/tag/commit)" "$INSTALL_REF" INSTALL_REF
|
|
fi
|
|
|
|
if [[ "$INSTALL_MODE" == "spoolbuddy" ]]; then
|
|
# Need remote Bambuddy URL and API key
|
|
echo ""
|
|
info "SpoolBuddy needs to connect to your Bambuddy server."
|
|
info "You can find/create an API key in Bambuddy under Settings -> API Keys."
|
|
echo ""
|
|
|
|
while [[ -z "$BAMBUDDY_URL" ]]; do
|
|
prompt "Bambuddy server URL (e.g. http://192.168.1.100:8000)" "" BAMBUDDY_URL
|
|
if [[ -z "$BAMBUDDY_URL" ]]; then
|
|
warn "Bambuddy URL is required"
|
|
fi
|
|
done
|
|
|
|
while [[ -z "$API_KEY" ]]; do
|
|
prompt "Bambuddy API key" "" API_KEY
|
|
if [[ -z "$API_KEY" ]]; then
|
|
warn "API key is required"
|
|
fi
|
|
done
|
|
else
|
|
# Full mode — Bambuddy runs locally
|
|
prompt "Bambuddy port" "$BAMBUDDY_PORT" BAMBUDDY_PORT
|
|
BAMBUDDY_URL="http://localhost:$BAMBUDDY_PORT"
|
|
|
|
echo ""
|
|
info "After installation, create an API key in Bambuddy (Settings -> API Keys)"
|
|
info "and update it in: $INSTALL_PATH/spoolbuddy/.env"
|
|
API_KEY="CHANGE_ME_AFTER_SETUP"
|
|
fi
|
|
|
|
# Summary
|
|
echo ""
|
|
echo -e "${BOLD}Installation Summary${NC}"
|
|
echo -e "${CYAN}─────────────────────────────────────────${NC}"
|
|
echo -e " Mode: ${GREEN}$([ "$INSTALL_MODE" == "full" ] && echo "Bambuddy + SpoolBuddy" || echo "SpoolBuddy only")${NC}"
|
|
echo -e " Install path: ${GREEN}$INSTALL_PATH${NC}"
|
|
echo -e " Git repo: ${GREEN}$INSTALL_REPO${NC}"
|
|
echo -e " Git ref: ${GREEN}$INSTALL_REF${NC}"
|
|
if [[ "$INSTALL_MODE" == "full" ]]; then
|
|
echo -e " Bambuddy port: ${GREEN}$BAMBUDDY_PORT${NC}"
|
|
echo -e " Bambuddy URL: ${GREEN}$BAMBUDDY_URL${NC}"
|
|
else
|
|
echo -e " Bambuddy URL: ${GREEN}$BAMBUDDY_URL${NC}"
|
|
fi
|
|
echo ""
|
|
|
|
if ! prompt_yes_no "Proceed with installation?" "y"; then
|
|
echo "Installation cancelled."
|
|
exit 0
|
|
fi
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Main
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
detect_installer_source_context
|
|
|
|
echo ""
|
|
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${CYAN}║ ║${NC}"
|
|
echo -e "${CYAN}║ ____ _ ____ _ _ ║${NC}"
|
|
echo -e "${CYAN}║ / ___| _ __ ___ ___ | | __ ) _ _ __| | __| |_ _ ║${NC}"
|
|
echo -e "${CYAN}║ \\___ \\| '_ \\ / _ \\ / _ \\| | _ \\| | | |/ _\` |/ _\` | | | |║${NC}"
|
|
echo -e "${CYAN}║ ___) | |_) | (_) | (_) | | |_) | |_| | (_| | (_| | |_| |║${NC}"
|
|
echo -e "${CYAN}║ |____/| .__/ \\___/ \\___/|_|____/ \\__,_|\\__,_|\\__,_|\\__, |║${NC}"
|
|
echo -e "${CYAN}║ |_| |___/ ║${NC}"
|
|
echo -e "${CYAN}║ ║${NC}"
|
|
echo -e "${CYAN}║ NFC Spool Management for Bambuddy ║${NC}"
|
|
echo -e "${CYAN}║ ║${NC}"
|
|
echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
|
|
# Check if running via pipe without -y
|
|
if [[ ! -t 0 ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then
|
|
error "Interactive mode requires a terminal. Use -y for unattended install, or download and run directly."
|
|
fi
|
|
|
|
# Pre-flight checks
|
|
check_root
|
|
check_raspberry_pi
|
|
check_raspberry_pi_os
|
|
|
|
if ! detect_python; then
|
|
info "Python 3.10+ not found, will install..."
|
|
fi
|
|
|
|
# Gather user preferences
|
|
ask_install_mode
|
|
gather_config
|
|
|
|
# Validate mode
|
|
if [[ "$INSTALL_MODE" != "spoolbuddy" && "$INSTALL_MODE" != "full" ]]; then
|
|
error "Invalid mode: $INSTALL_MODE (must be 'spoolbuddy' or 'full')"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Starting Installation${NC}"
|
|
echo -e "${CYAN}─────────────────────────────────────────${NC}"
|
|
echo ""
|
|
|
|
# ── Step 1: Raspberry Pi hardware config ──────────────────────────────
|
|
info "Configuring Raspberry Pi hardware..."
|
|
enable_spi
|
|
enable_i2c
|
|
configure_boot_config
|
|
echo ""
|
|
|
|
# ── Step 2: System packages ───────────────────────────────────────────
|
|
install_system_packages
|
|
install_wifi_safeguard
|
|
upgrade_system_packages
|
|
detect_python || error "Failed to install Python 3.10+"
|
|
echo ""
|
|
|
|
# ── Step 2b: Strip unnecessary services & packages ────────────────────
|
|
strip_services
|
|
strip_packages
|
|
echo ""
|
|
|
|
# ── Step 3: Download source code ──────────────────────────────────────
|
|
create_spoolbuddy_user
|
|
download_spoolbuddy
|
|
echo ""
|
|
|
|
# ── Step 3b: Kiosk setup (labwc + Chromium + squeekboard + Plymouth) ──
|
|
setup_kiosk
|
|
echo ""
|
|
|
|
# ── Step 4: SpoolBuddy setup ──────────────────────────────────────────
|
|
info "Setting up SpoolBuddy..."
|
|
setup_spoolbuddy_venv
|
|
create_spoolbuddy_env
|
|
# Kiosk env access: only needed if actual kiosk hardware is available
|
|
if [[ -f /boot/firmware/config.txt ]] || [[ -f /boot/config.txt ]]; then
|
|
ensure_kiosk_env_access
|
|
fi
|
|
setup_ssh_key
|
|
create_spoolbuddy_service
|
|
echo ""
|
|
|
|
# ── Step 5: Bambuddy setup (full mode only) ───────────────────────────
|
|
if [[ "$INSTALL_MODE" == "full" ]]; then
|
|
info "Setting up Bambuddy..."
|
|
create_bambuddy_user
|
|
setup_bambuddy_venv
|
|
install_nodejs
|
|
build_frontend
|
|
create_bambuddy_directories
|
|
create_bambuddy_env
|
|
create_bambuddy_service
|
|
bootstrap_spoolbuddy_kiosk_key
|
|
echo ""
|
|
fi
|
|
|
|
# ── Done ──────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}║ Installation Complete! ║${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
|
|
local ip_addr
|
|
ip_addr=$(hostname -I 2>/dev/null | awk '{print $1}') || ip_addr="<your-ip>"
|
|
|
|
if [[ "$INSTALL_MODE" == "full" ]]; then
|
|
echo -e " ${BOLD}Bambuddy:${NC} ${CYAN}http://$ip_addr:$BAMBUDDY_PORT${NC}"
|
|
else
|
|
echo -e " ${BOLD}SpoolBuddy:${NC} Connecting to ${CYAN}$BAMBUDDY_URL${NC}"
|
|
fi
|
|
echo -e " ${BOLD}Kiosk URL:${NC} ${CYAN}$KIOSK_URL${NC}"
|
|
echo -e " ${BOLD}Kiosk user:${NC} ${CYAN}$KIOSK_USER${NC}"
|
|
echo ""
|
|
|
|
if [[ "$INSTALL_MODE" == "full" ]]; then
|
|
echo -e " ${BOLD}Next steps:${NC}"
|
|
echo -e " 1. Reboot (required for kiosk, Plymouth splash, and hardware changes)"
|
|
echo -e " 2. The touchscreen kiosk will start automatically after reboot"
|
|
echo -e " 3. On another device, open ${CYAN}http://$ip_addr:$BAMBUDDY_PORT${NC} to complete first-run admin setup"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e " ${BOLD}Manage services:${NC}"
|
|
echo -e " SpoolBuddy status: ${CYAN}sudo systemctl status spoolbuddy${NC}"
|
|
echo -e " SpoolBuddy logs: ${CYAN}sudo journalctl -u spoolbuddy -f${NC}"
|
|
if [[ "$INSTALL_MODE" == "full" ]]; then
|
|
echo -e " Bambuddy status: ${CYAN}sudo systemctl status bambuddy${NC}"
|
|
echo -e " Bambuddy logs: ${CYAN}sudo journalctl -u bambuddy -f${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e " ${BOLD}Configuration:${NC} ${CYAN}$INSTALL_PATH/spoolbuddy/.env${NC}"
|
|
echo -e " ${BOLD}Hardware wiring:${NC} ${CYAN}$INSTALL_PATH/spoolbuddy/README.md${NC}"
|
|
echo -e " ${BOLD}Diagnostics:${NC} ${CYAN}sudo $INSTALL_PATH/spoolbuddy/venv/bin/python $INSTALL_PATH/spoolbuddy/pn5180_diag.py${NC}"
|
|
echo ""
|
|
|
|
echo -e " ${YELLOW}A reboot is required to apply all changes (kiosk, Plymouth splash, hardware).${NC}"
|
|
echo ""
|
|
if prompt_yes_no "Reboot now?" "y"; then
|
|
reboot
|
|
else
|
|
echo -e " Run ${CYAN}sudo reboot${NC} when ready."
|
|
fi
|
|
|
|
echo ""
|
|
}
|
|
|
|
main "$@"
|