mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-01 03:31:25 +02:00
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a polished SpoolBuddy boot splash image (1024x600).
|
||||
|
||||
Uses the SpoolBuddy logo with baked-in glow, radial gradient background,
|
||||
subtle light rays, and vignette effects for a premium kiosk boot screen.
|
||||
|
||||
Usage:
|
||||
python3 generate_splash.py [output_path]
|
||||
|
||||
Requires: Pillow (pip install Pillow)
|
||||
"""
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
# --- Configuration ---
|
||||
WIDTH, HEIGHT = 1024, 600
|
||||
BG_CENTER = (55, 55, 55) # Notably lighter center
|
||||
BG_EDGE = (2, 2, 2) # Nearly black edges
|
||||
ACCENT = (0, 200, 75) # Brighter SpoolBuddy green
|
||||
ACCENT_GLOW = (0, 255, 100) # Vivid glow core
|
||||
LOGO_SCALE = 0.50 # Scale logo to 50% of canvas width
|
||||
GLOW_RADIUS = 160 # Very wide glow spread
|
||||
VIGNETTE_STRENGTH = 0.85 # Heavy edge darkening
|
||||
RAY_COUNT = 32 # More radial light rays
|
||||
RAY_OPACITY = 50 # Clearly visible rays (0-255)
|
||||
|
||||
|
||||
def radial_gradient(size, center_color, edge_color):
|
||||
"""Create a radial gradient from center to edges."""
|
||||
w, h = size
|
||||
img = Image.new("RGB", size)
|
||||
pixels = img.load()
|
||||
cx, cy = w // 2, h // 2
|
||||
max_dist = math.sqrt(cx**2 + cy**2)
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
dist = math.sqrt((x - cx) ** 2 + (y - cy) ** 2)
|
||||
t = min(dist / max_dist, 1.0)
|
||||
# Ease-out curve for smoother falloff
|
||||
t = t * t
|
||||
r = int(center_color[0] + (edge_color[0] - center_color[0]) * t)
|
||||
g = int(center_color[1] + (edge_color[1] - center_color[1]) * t)
|
||||
b = int(center_color[2] + (edge_color[2] - center_color[2]) * t)
|
||||
pixels[x, y] = (r, g, b)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def create_light_rays(size, num_rays, opacity):
|
||||
"""Create subtle radial light rays emanating from center."""
|
||||
w, h = size
|
||||
rays = Image.new("RGBA", size, (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(rays)
|
||||
cx, cy = w // 2, h // 2
|
||||
max_radius = int(math.sqrt(cx**2 + cy**2)) + 50
|
||||
|
||||
for i in range(num_rays):
|
||||
angle = (2 * math.pi * i) / num_rays
|
||||
# Vary ray width slightly for organic feel
|
||||
half_width = math.radians(1.5 + (i % 3) * 0.5)
|
||||
|
||||
a1 = angle - half_width
|
||||
a2 = angle + half_width
|
||||
|
||||
points = [
|
||||
(cx, cy),
|
||||
(cx + int(max_radius * math.cos(a1)), cy + int(max_radius * math.sin(a1))),
|
||||
(cx + int(max_radius * math.cos(a2)), cy + int(max_radius * math.sin(a2))),
|
||||
]
|
||||
# Green-tinted rays
|
||||
draw.polygon(points, fill=(ACCENT[0], ACCENT[1], ACCENT[2], opacity))
|
||||
|
||||
# Heavy blur to make rays soft and diffuse
|
||||
rays = rays.filter(ImageFilter.GaussianBlur(radius=30))
|
||||
return rays
|
||||
|
||||
|
||||
def create_vignette(size, strength):
|
||||
"""Create a vignette (edge darkening) mask."""
|
||||
w, h = size
|
||||
vignette = Image.new("L", size, 255)
|
||||
pixels = vignette.load()
|
||||
cx, cy = w / 2, h / 2
|
||||
max_dist = math.sqrt(cx**2 + cy**2)
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
dist = math.sqrt((x - cx) ** 2 + (y - cy) ** 2)
|
||||
t = dist / max_dist
|
||||
# Ramp darkening from ~40% radius outward
|
||||
fade = max(0, (t - 0.4) / 0.6)
|
||||
fade = fade * fade # Quadratic ease
|
||||
val = int(255 * (1 - fade * strength))
|
||||
pixels[x, y] = max(0, val)
|
||||
|
||||
return vignette
|
||||
|
||||
|
||||
def create_glow(logo_img, color, radius, intensity=1.5):
|
||||
"""Create a colored glow effect from a logo's alpha channel."""
|
||||
# Extract alpha as the glow shape
|
||||
if logo_img.mode != "RGBA":
|
||||
return Image.new("RGBA", logo_img.size, (0, 0, 0, 0))
|
||||
|
||||
alpha = logo_img.split()[3]
|
||||
|
||||
# Create colored version of the alpha shape
|
||||
glow = Image.new("RGBA", logo_img.size, (0, 0, 0, 0))
|
||||
glow_pixels = glow.load()
|
||||
alpha_pixels = alpha.load()
|
||||
|
||||
for y in range(logo_img.height):
|
||||
for x in range(logo_img.width):
|
||||
a = alpha_pixels[x, y]
|
||||
if a > 0:
|
||||
boosted = min(255, int(a * intensity))
|
||||
glow_pixels[x, y] = (color[0], color[1], color[2], boosted)
|
||||
|
||||
# Blur to create the glow spread
|
||||
glow = glow.filter(ImageFilter.GaussianBlur(radius=radius))
|
||||
return glow
|
||||
|
||||
|
||||
def generate_splash(output_path):
|
||||
"""Generate the final splash image."""
|
||||
print(f"Generating {WIDTH}x{HEIGHT} splash image...")
|
||||
|
||||
# 1. Radial gradient background
|
||||
print(" Creating radial gradient background...")
|
||||
canvas = radial_gradient((WIDTH, HEIGHT), BG_CENTER, BG_EDGE)
|
||||
|
||||
# 2. Light rays
|
||||
print(" Adding light rays...")
|
||||
rays = create_light_rays((WIDTH, HEIGHT), RAY_COUNT, RAY_OPACITY)
|
||||
canvas.paste(
|
||||
Image.alpha_composite(
|
||||
Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0)),
|
||||
rays,
|
||||
),
|
||||
(0, 0),
|
||||
rays,
|
||||
)
|
||||
|
||||
# 3. Load and scale logo
|
||||
print(" Loading SpoolBuddy logo...")
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
logo_paths = [
|
||||
os.path.join(script_dir, "..", "..", "frontend", "public", "spoolbuddy_logo_dark.png"),
|
||||
os.path.join(script_dir, "..", "..", "frontend", "public", "img", "spoolbuddy_logo_dark.png"),
|
||||
]
|
||||
|
||||
logo = None
|
||||
for p in logo_paths:
|
||||
resolved = os.path.normpath(p)
|
||||
if os.path.exists(resolved):
|
||||
logo = Image.open(resolved).convert("RGBA")
|
||||
print(f" Loaded logo from {resolved}")
|
||||
break
|
||||
|
||||
if logo is None:
|
||||
print(" ERROR: Could not find spoolbuddy_logo_dark.png")
|
||||
sys.exit(1)
|
||||
|
||||
# Scale logo to target width
|
||||
target_w = int(WIDTH * LOGO_SCALE)
|
||||
scale = target_w / logo.width
|
||||
target_h = int(logo.height * scale)
|
||||
logo = logo.resize((target_w, target_h), Image.LANCZOS)
|
||||
|
||||
# Center position (shift up slightly for visual balance)
|
||||
logo_x = (WIDTH - target_w) // 2
|
||||
logo_y = (HEIGHT - target_h) // 2 - 10
|
||||
|
||||
# 4. Glow behind logo (two layers: wide diffuse + tight bright)
|
||||
print(" Rendering glow effects...")
|
||||
|
||||
# Wide diffuse glow — very prominent
|
||||
glow_wide = create_glow(logo, ACCENT, radius=GLOW_RADIUS, intensity=3.0)
|
||||
glow_canvas = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
|
||||
glow_canvas.paste(glow_wide, (logo_x, logo_y), glow_wide)
|
||||
canvas = Image.alpha_composite(canvas.convert("RGBA"), glow_canvas)
|
||||
|
||||
# Medium glow layer for extra punch
|
||||
glow_mid = create_glow(logo, ACCENT_GLOW, radius=GLOW_RADIUS // 2, intensity=2.5)
|
||||
glow_canvas_mid = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
|
||||
glow_canvas_mid.paste(glow_mid, (logo_x, logo_y), glow_mid)
|
||||
canvas = Image.alpha_composite(canvas, glow_canvas_mid)
|
||||
|
||||
# Tighter bright glow core
|
||||
glow_tight = create_glow(logo, ACCENT_GLOW, radius=GLOW_RADIUS // 4, intensity=2.0)
|
||||
glow_canvas2 = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
|
||||
glow_canvas2.paste(glow_tight, (logo_x, logo_y), glow_tight)
|
||||
canvas = Image.alpha_composite(canvas, glow_canvas2)
|
||||
|
||||
# 5. Composite logo on top
|
||||
print(" Compositing logo...")
|
||||
canvas.paste(logo, (logo_x, logo_y), logo)
|
||||
|
||||
# 6. Apply vignette
|
||||
print(" Applying vignette...")
|
||||
vignette = create_vignette((WIDTH, HEIGHT), VIGNETTE_STRENGTH)
|
||||
canvas_rgb = canvas.convert("RGB")
|
||||
|
||||
# Multiply canvas by vignette mask
|
||||
r, g, b = canvas_rgb.split()
|
||||
r = Image.composite(r, Image.new("L", (WIDTH, HEIGHT), 0), vignette)
|
||||
g = Image.composite(g, Image.new("L", (WIDTH, HEIGHT), 0), vignette)
|
||||
b = Image.composite(b, Image.new("L", (WIDTH, HEIGHT), 0), vignette)
|
||||
canvas = Image.merge("RGB", (r, g, b))
|
||||
|
||||
# 7. Save
|
||||
canvas.save(output_path, "PNG", optimize=True)
|
||||
file_size = os.path.getsize(output_path) / 1024
|
||||
print(f" Saved to {output_path} ({file_size:.0f} KB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
out = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(os.path.abspath(__file__)), "splash.png")
|
||||
generate_splash(out)
|
||||
+368
-24
@@ -12,10 +12,13 @@
|
||||
#
|
||||
# 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
|
||||
#
|
||||
@@ -49,6 +52,10 @@ SPOOLBUDDY_PIP_PACKAGES="spidev gpiod smbus2 httpx"
|
||||
|
||||
INSTALL_MODE="" # "spoolbuddy" or "full"
|
||||
INSTALL_PATH=""
|
||||
INSTALL_REPO=""
|
||||
INSTALL_REF=""
|
||||
DETECTED_INSTALLER_REPO=""
|
||||
DETECTED_INSTALLER_REF=""
|
||||
BAMBUDDY_URL=""
|
||||
API_KEY=""
|
||||
BAMBUDDY_PORT="8000"
|
||||
@@ -56,6 +63,7 @@ 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
|
||||
@@ -186,10 +194,13 @@ show_help() {
|
||||
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 ""
|
||||
@@ -205,6 +216,70 @@ show_help() {
|
||||
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
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -309,21 +384,32 @@ configure_boot_config() {
|
||||
|
||||
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_vc=on and dtoverlay=spi0-0cs"
|
||||
warn "You may need to manually add: dtparam=i2c_arm=on and dtoverlay=spi0-0cs"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Configuring $boot_config..."
|
||||
|
||||
# Enable I2C bus 0 (GPIO0/GPIO1) for NAU7802 scale
|
||||
if ! grep -q "^dtparam=i2c_vc=on" "$boot_config"; then
|
||||
echo "" >> "$boot_config"
|
||||
echo "# SpoolBuddy: I2C bus 0 for NAU7802 scale (GPIO0/GPIO1)" >> "$boot_config"
|
||||
echo "dtparam=i2c_vc=on" >> "$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 "Added dtparam=i2c_vc=on"
|
||||
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_vc=on already set"
|
||||
success "dtparam=i2c_arm=on already set"
|
||||
fi
|
||||
|
||||
# Disable SPI auto chip-select (manual CS on GPIO23 for PN5180)
|
||||
@@ -347,6 +433,63 @@ install_system_packages() {
|
||||
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
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -354,9 +497,11 @@ install_system_packages() {
|
||||
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 /usr/sbin/nologin --home-dir "$INSTALL_PATH" "$SPOOLBUDDY_SERVICE_USER"
|
||||
useradd --system --shell /bin/bash --home-dir "$INSTALL_PATH" "$SPOOLBUDDY_SERVICE_USER"
|
||||
success "Service user created"
|
||||
fi
|
||||
|
||||
@@ -367,6 +512,15 @@ create_spoolbuddy_user() {
|
||||
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 *
|
||||
SUDOERS
|
||||
chmod 440 /etc/sudoers.d/spoolbuddy
|
||||
success "Sudoers entries created for service and kiosk restart"
|
||||
}
|
||||
|
||||
download_spoolbuddy() {
|
||||
@@ -374,15 +528,14 @@ download_spoolbuddy() {
|
||||
info "Existing installation found, updating..."
|
||||
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
|
||||
git reset --hard origin/main > /dev/null 2>&1
|
||||
cd "$INSTALL_PATH"
|
||||
git checkout 0.2.2b1
|
||||
resolve_install_ref "$INSTALL_REF"
|
||||
else
|
||||
mkdir -p "$INSTALL_PATH"
|
||||
run_with_progress "Cloning repository" git clone "$GITHUB_REPO" "$INSTALL_PATH"
|
||||
run_with_progress "Cloning repository" git clone "$INSTALL_REPO" "$INSTALL_PATH"
|
||||
cd "$INSTALL_PATH"
|
||||
git checkout 0.2.2b1
|
||||
resolve_install_ref "$INSTALL_REF"
|
||||
fi
|
||||
|
||||
chown -R "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$INSTALL_PATH"
|
||||
@@ -412,13 +565,73 @@ 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"
|
||||
chmod 600 "$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..."
|
||||
|
||||
@@ -655,8 +868,29 @@ setup_kiosk() {
|
||||
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
|
||||
# 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"
|
||||
@@ -765,6 +999,10 @@ EOF
|
||||
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
|
||||
@@ -809,18 +1047,61 @@ EOF
|
||||
</labwc_config>
|
||||
EOF
|
||||
|
||||
# ── labwc autostart ───────────────────────────────────────────────────
|
||||
cat > "$labwc_dir/autostart" << EOF
|
||||
# ── 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
|
||||
|
||||
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 \
|
||||
"\$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 &
|
||||
|
||||
# Launch Chromium in kiosk mode (virtual keyboard is embedded in the web app)
|
||||
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 \\
|
||||
$KIOSK_URL &
|
||||
# Launch Chromium via helper that resolves URL from spoolbuddy/.env
|
||||
$kiosk_launcher &
|
||||
EOF
|
||||
|
||||
chown -R "$KIOSK_USER:$KIOSK_USER" "$labwc_dir"
|
||||
@@ -855,6 +1136,14 @@ parse_args() {
|
||||
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
|
||||
@@ -871,6 +1160,10 @@ parse_args() {
|
||||
BAMBUDDY_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-pubkey)
|
||||
SSH_PUBKEY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--yes|-y)
|
||||
NON_INTERACTIVE="true"
|
||||
shift
|
||||
@@ -927,6 +1220,47 @@ gather_config() {
|
||||
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 ""
|
||||
@@ -964,6 +1298,8 @@ gather_config() {
|
||||
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}"
|
||||
@@ -984,6 +1320,7 @@ gather_config() {
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
detect_installer_source_context
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||||
@@ -1037,6 +1374,8 @@ main() {
|
||||
|
||||
# ── Step 2: System packages ───────────────────────────────────────────
|
||||
install_system_packages
|
||||
install_wifi_safeguard
|
||||
upgrade_system_packages
|
||||
detect_python || error "Failed to install Python 3.10+"
|
||||
echo ""
|
||||
|
||||
@@ -1058,6 +1397,11 @@ main() {
|
||||
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 ""
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 118 KiB |
Reference in New Issue
Block a user