screentinker/scripts/raspberry-pi-setup.sh
ScreenTinker 0efbc6040e Pi installer: ask the operator, not the pipe; and stop assuming X11
Five defects from #245, all found by a user on a Pi 5 because nothing in this
repo has ever executed either of these scripts.

THE MENU IGNORED THE OPERATOR.

The documented install is `curl … | sudo bash`, which makes stdin the SCRIPT.
bash has consumed it by the time any `read` runs, so every prompt got EOF
instantly: the mode menu "chose" All-in-One without anyone touching it, and
Player-Only could not be reached that way at all. Reported as the menu being
skipped, because it was. Prompts now read the controlling terminal, and when
there genuinely is no terminal the script SAYS which way it went instead of
letting an empty answer look like a decision.

X11 TOOLS ON A WAYLAND PI.

Pi 5 on Bookworm defaults to Wayland, where xset, unclutter and xrandr are
no-ops that log an error and do nothing. The Pi therefore got no blanking
suppression and no cursor hiding while looking configured. The launcher now
detects the session and branches: X11 keeps what it had, Wayland gets wlopm and
--ozone-platform=wayland, and the compositor-side alternatives are documented
rather than silently assumed.

THE KEYRING PROMPT.

"Choose password for keyring" on every boot is Chromium reaching for
gnome-keyring. A kiosk has nobody to answer it. --password-store=basic.

THE WHITE PAGE ON EVERY BOOT BUT THE FIRST.

Chromium restoring a session it believes crashed — a kiosk is killed by
shutdown and never exits cleanly, so it returns with a restore surface over the
player. That is why ALT+F4 "fixed" it: it closed the surface, not the player.
Rewriting exited_cleanly was never enough on its own because the previous window
set is replayed from Sessions/, so that goes too.

THE BANNER SPELLED THE PRODUCT WRONG.

The ASCII art read "Scree Tinker" — the n was missing, and it is the first thing
anyone sees over SSH.

Also answered the reporter's Overlay FS question in the README, including the
part that bites: an All-in-One Pi IS the server, so an overlay discards the
database, uploads and JWT secret at every reboot. Safe for Player-Only; needs
DATA_DIR moved off the overlay otherwise.

The new test generates the kiosk launcher exactly as the installer writes it and
runs bash -n over it, because `bash -n` on the outer script cannot see inside a
heredoc — a syntax error in there is just text until it reaches a screen.

Reported-by: carloblu74
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014skWYXJUWhF73EvNPgB2AS
2026-08-07 09:08:17 -05:00

742 lines
25 KiB
Bash

#!/bin/bash
# ScreenTinker - Raspberry Pi Setup Script
#
# All-in-One: runs the ScreenTinker server AND kiosk player on one Pi
# Player-Only: connects to an existing ScreenTinker server
#
# Usage:
# All-in-One: curl -sSL https://screentinker.com/scripts/raspberry-pi-setup.sh | sudo bash
# Player-Only: curl -sSL https://screentinker.com/scripts/raspberry-pi-setup.sh | sudo bash -s -- --player-only https://screentinker.com
#
# Or clone and run:
# git clone https://github.com/screentinker/screentinker.git
# cd screentinker/scripts && sudo ./raspberry-pi-setup.sh
#
# Works on Raspberry Pi OS Lite or Desktop (Bookworm / Bullseye)
# Tested on Pi 3B+, Pi 4, Pi 5
set -euo pipefail
# -- Configuration --
SCREENTINKER_DIR="/opt/screentinker"
SCREENTINKER_PORT=3001
NODE_MAJOR=20
LOG_FILE="/var/log/screentinker-setup.log"
# -- Colors --
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() { echo -e "${GREEN}[ScreenTinker]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
err() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
# -- Parse arguments --
PLAYER_ONLY=false
SERVER_URL=""
while [[ $# -gt 0 ]]; do
case "$1" in
--player-only) PLAYER_ONLY=true; shift ;;
--help|-h)
echo "Usage: sudo ./raspberry-pi-setup.sh [OPTIONS] [SERVER_URL]"
echo ""
echo "Options:"
echo " --player-only URL Player-only mode (no local server)"
echo " --help Show this help"
echo ""
echo "Examples:"
echo " sudo ./raspberry-pi-setup.sh # All-in-One (interactive)"
echo " sudo ./raspberry-pi-setup.sh --player-only https://screentinker.com"
exit 0
;;
http*) SERVER_URL="$1"; shift ;;
*) shift ;;
esac
done
# -- Prompting when we are being piped --
#
# The documented install is `curl -sL … | sudo bash`, which makes stdin the SCRIPT, not the
# operator. bash has already consumed it by the time any `read` runs, so every prompt got EOF
# instantly: the mode menu "chose" All-in-One without the operator touching anything, and the
# Player-Only branch could never be reached that way at all. It looked like the menu was being
# skipped, because it was.
#
# So prompts read from the controlling terminal instead. When there genuinely is no terminal
# (cloud-init, a provisioning pipeline), we say so and take the documented default rather than
# pretending a choice was made — the operator can pass --player-only / --server-url to decide
# without a prompt.
if [ -r /dev/tty ] && [ -t 1 ]; then
exec 3</dev/tty
HAVE_TTY=true
else
HAVE_TTY=false
fi
# ask <varname> <prompt> [read-args…]
ask() {
local __var="$1"; shift
local __prompt="$1"; shift
if [ "$HAVE_TTY" = true ]; then
read "$@" -u 3 -r -p "$__prompt" "$__var"
else
eval "$__var=''"
fi
}
# -- Root check --
if [ "$(id -u)" -ne 0 ]; then
err "This script must be run as root. Try: curl -sL https://screentinker.com/scripts/raspberry-pi-setup.sh | sudo bash"
fi
# -- Architecture check --
ARCH=$(uname -m)
if [[ "$ARCH" != "aarch64" && "$ARCH" != "armv7l" ]]; then
warn "Detected architecture: $ARCH (expected aarch64 or armv7l for Raspberry Pi)"
if [ "$HAVE_TTY" = true ]; then
ask REPLY "Continue anyway? (y/N) " -n 1; echo
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 1
else
err "Refusing to continue on $ARCH without a terminal to confirm at. Re-run interactively, or on the intended hardware."
fi
fi
# -- Interactive mode selection (if no flags passed) --
if [ "$PLAYER_ONLY" = false ] && [ -z "$SERVER_URL" ]; then
echo ""
echo -e "${BLUE}======================================${NC}"
echo -e "${BLUE} ScreenTinker Raspberry Pi Setup${NC}"
echo -e "${BLUE}======================================${NC}"
echo ""
echo " 1) All-in-One (recommended)"
echo " Runs the server AND player on this Pi."
echo " Manage everything from your phone."
echo ""
echo " 2) Player Only"
echo " Connects to an existing ScreenTinker server."
echo " This Pi just displays content."
echo ""
if [ "$HAVE_TTY" = false ]; then
# No terminal to ask at. Say which way we went, rather than letting an empty answer
# look like a decision — this is the exact confusion the piped-stdin bug produced.
warn "No terminal available for the menu — defaulting to All-in-One."
warn "To choose Player-Only non-interactively: ... | sudo bash -s -- --player-only https://your-server"
else
ask MODE_CHOICE "Choose [1/2]: "
case "$MODE_CHOICE" in
2)
PLAYER_ONLY=true
while [ -z "$SERVER_URL" ]; do
ask SERVER_URL "Server URL (e.g., https://screentinker.com): "
[ -z "$SERVER_URL" ] && warn "Player-Only needs a server URL."
done
;;
*) ;;
esac
fi
fi
# Strip trailing slash from server URL
SERVER_URL="${SERVER_URL%/}"
# Set kiosk URL
if [ "$PLAYER_ONLY" = true ]; then
[ -z "$SERVER_URL" ] && err "Player-only mode requires a server URL"
KIOSK_URL="${SERVER_URL}/player"
log "Player-only mode: $SERVER_URL"
else
KIOSK_URL="http://localhost:${SCREENTINKER_PORT}/player"
log "All-in-One mode: server + player"
fi
echo ""
log "Setup log: $LOG_FILE"
exec > >(tee -a "$LOG_FILE") 2>&1
# -- Detect Pi OS variant --
HAS_DESKTOP=false
if dpkg -l xserver-xorg 2>/dev/null | grep -q "^ii"; then
HAS_DESKTOP=true
log "Detected: Pi OS with Desktop"
else
log "Detected: Pi OS Lite (headless)"
fi
# ============================================================
# 1. System packages
# ============================================================
log "Updating system packages..."
apt-get update -qq
apt-get upgrade -y -qq
log "Installing base dependencies..."
apt-get install -y -qq \
git curl wget unzip htop \
avahi-daemon \
fonts-liberation fonts-noto-color-emoji \
>> "$LOG_FILE" 2>&1
# ============================================================
# 2. Node.js (all-in-one only)
# ============================================================
if [ "$PLAYER_ONLY" = false ]; then
NEED_NODE=true
if command -v node &>/dev/null; then
CUR=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$CUR" -ge "$NODE_MAJOR" ]; then
log "Node.js $(node -v) already installed"
NEED_NODE=false
fi
fi
if [ "$NEED_NODE" = true ]; then
log "Installing Node.js ${NODE_MAJOR}.x..."
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - >> "$LOG_FILE" 2>&1
apt-get install -y -qq nodejs >> "$LOG_FILE" 2>&1
log "Node.js $(node -v) installed"
fi
fi
# ============================================================
# 3. Clone / update ScreenTinker (all-in-one only)
# ============================================================
if [ "$PLAYER_ONLY" = false ]; then
if [ -d "$SCREENTINKER_DIR/.git" ]; then
log "Repo exists at $SCREENTINKER_DIR, pulling latest..."
cd "$SCREENTINKER_DIR" && git pull origin main >> "$LOG_FILE" 2>&1
else
log "Cloning ScreenTinker..."
git clone https://github.com/screentinker/screentinker.git "$SCREENTINKER_DIR" >> "$LOG_FILE" 2>&1
fi
log "Installing Node.js dependencies..."
cd "$SCREENTINKER_DIR/server"
npm install --production >> "$LOG_FILE" 2>&1
# Data directories
mkdir -p "$SCREENTINKER_DIR/server/db"
mkdir -p "$SCREENTINKER_DIR/server/uploads"
fi
# Determine the runtime user
PI_USER="${SUDO_USER:-pi}"
PI_HOME=$(eval echo "~$PI_USER")
# Set ownership (all-in-one only)
if [ "$PLAYER_ONLY" = false ]; then
chown -R "$PI_USER":"$PI_USER" "$SCREENTINKER_DIR"
fi
# ============================================================
# 4. Server systemd service (all-in-one only)
# ============================================================
if [ "$PLAYER_ONLY" = false ]; then
log "Creating screentinker-server service..."
cat > /etc/systemd/system/screentinker-server.service << EOF
[Unit]
Description=ScreenTinker Digital Signage Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${PI_USER}
WorkingDirectory=${SCREENTINKER_DIR}/server
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=5
StartLimitBurst=5
StartLimitIntervalSec=60
Environment=NODE_ENV=production
Environment=PORT=${SCREENTINKER_PORT}
Environment=SELF_HOSTED=true
Environment=HOST=0.0.0.0
StandardOutput=journal
StandardError=journal
SyslogIdentifier=screentinker-server
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable screentinker-server.service
log "Server service enabled"
fi
# ============================================================
# 5. Kiosk display packages
# ============================================================
log "Installing kiosk packages..."
# Chromium's package name differs by distro: Raspberry Pi OS / Ubuntu ship
# 'chromium-browser', Debian ships 'chromium'. Try the Pi/Ubuntu name first, then
# fall back to Debian's, so a bundled apt-get can't hard-fail (set -e) on a
# Debian-based player. No-op if a chromium is already present.
install_chromium() {
if command -v chromium-browser &>/dev/null || command -v chromium &>/dev/null; then
return 0
fi
apt-get install -y -qq chromium-browser >> "$LOG_FILE" 2>&1 || \
apt-get install -y -qq chromium >> "$LOG_FILE" 2>&1
}
if [ "$HAS_DESKTOP" = false ]; then
# Lite: install X11 + helpers, then Chromium (portable package name)
apt-get install -y -qq \
xserver-xorg x11-xserver-utils xinit \
unclutter xdotool \
>> "$LOG_FILE" 2>&1
install_chromium
else
# Desktop: X already running, just ensure Chromium + helpers
apt-get install -y -qq unclutter xdotool >> "$LOG_FILE" 2>&1
install_chromium
fi
# Find Chromium binary
CHROMIUM_BIN=$(command -v chromium-browser 2>/dev/null || command -v chromium 2>/dev/null || echo "/usr/bin/chromium-browser")
# ============================================================
# 6. Kiosk launcher script
# ============================================================
log "Creating kiosk launcher..."
cat > "$PI_HOME/screentinker-kiosk.sh" << KIOSKEOF
#!/bin/bash
# ScreenTinker Kiosk - launches Chromium in fullscreen player mode
KIOSK_URL="${KIOSK_URL}"
# Wait for display
sleep 2
# Which display server are we actually on? Pi 5 on Bookworm defaults to WAYLAND, where every
# X11 tool below is a no-op that prints an error into the journal and silently does nothing —
# so a Wayland Pi got no blanking suppression and no cursor hiding while appearing configured.
SESSION_TYPE="\${XDG_SESSION_TYPE:-}"
if [ -z "\$SESSION_TYPE" ]; then
if [ -n "\${WAYLAND_DISPLAY:-}" ]; then SESSION_TYPE=wayland
elif [ -n "\${DISPLAY:-}" ]; then SESSION_TYPE=x11
fi
fi
echo "Display server: \${SESSION_TYPE:-unknown}"
if [ "\$SESSION_TYPE" = "wayland" ]; then
# Blanking/DPMS belong to the compositor here, not to us. wlopm is present on Pi OS
# (wlroots-based wayfire/labwc); if it is not, the compositor's own idle config is the
# documented fallback and README says so.
command -v wlopm >/dev/null 2>&1 && wlopm --on '*' 2>/dev/null || true
# unclutter is X11-only. Under wayfire the cursor is hidden by the compositor
# (hide_cursor / idle plugin), which the installer writes below when wayfire.ini exists.
else
# Disable screen blanking and power management
xset s off
xset s noblank
xset -dpms
xset s 0 0
# Hide cursor after 3 seconds of inactivity (X11 only — no Wayland equivalent)
unclutter -idle 3 -root &
fi
# Clean Chromium crash flags (prevents restore session dialogs).
#
# The white page on every boot after the first is Chromium restoring a session it thinks
# crashed: a kiosk is killed by the shutdown, never exits cleanly, and comes back with a
# restore surface on top of the player — which is why ALT+F4 "fixed" it (it closed the
# surface, not the player). Rewriting the flags is not enough on its own because Chromium
# also replays the previous window set from Sessions/, so those go too.
CDIR="\$HOME/.config/chromium/Default"
mkdir -p "\$CDIR"
if [ -f "\$CDIR/Preferences" ]; then
sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' "\$CDIR/Preferences" 2>/dev/null || true
sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' "\$CDIR/Preferences" 2>/dev/null || true
fi
rm -rf "\$CDIR/Sessions" "\$CDIR/Session Storage" 2>/dev/null || true
# Wait for local server if running all-in-one
if echo "\$KIOSK_URL" | grep -q "localhost"; then
echo "Waiting for ScreenTinker server..."
for i in \$(seq 1 30); do
if curl -sf "http://localhost:${SCREENTINKER_PORT}/api/status" >/dev/null 2>&1; then
echo "Server ready"
break
fi
sleep 2
done
fi
# Detect screen resolution so Chromium fills the display on minimal X11 (no WM)
SCREEN_RES=\$(xrandr 2>/dev/null | grep ' connected' | grep -oE '[0-9]+x[0-9]+' | head -1)
SCREEN_W=\${SCREEN_RES%%x*}
SCREEN_H=\${SCREEN_RES##*x}
if [ -z "\$SCREEN_W" ] || [ -z "\$SCREEN_H" ]; then
SCREEN_W=1920
SCREEN_H=1080
fi
# Wayland needs the ozone backend named explicitly on some Bookworm builds; on X11 the flag
# is absent so nothing changes there.
OZONE=""
[ "\$SESSION_TYPE" = "wayland" ] && OZONE="--ozone-platform=wayland"
exec ${CHROMIUM_BIN} \\
--kiosk \\
\$OZONE \\
--password-store=basic \\
--window-position=0,0 \\
--window-size=\${SCREEN_W},\${SCREEN_H} \\
--noerrdialogs \\
--disable-infobars \\
--disable-session-crashed-bubble \\
--disable-features=TranslateUI \\
--disable-component-update \\
--check-for-update-interval=31536000 \\
--autoplay-policy=no-user-gesture-required \\
--no-first-run \\
--disable-pinch \\
--overscroll-history-navigation=0 \\
--disable-translate \\
--disable-sync \\
--disable-background-networking \\
--disable-default-apps \\
--disable-extensions \\
--disable-hang-monitor \\
--disable-popup-blocking \\
--disable-prompt-on-repost \\
--metrics-recording-only \\
--safebrowsing-disable-auto-update \\
--ignore-certificate-errors \\
"\$KIOSK_URL"
KIOSKEOF
chmod +x "$PI_HOME/screentinker-kiosk.sh"
chown "$PI_USER":"$PI_USER" "$PI_HOME/screentinker-kiosk.sh"
# ============================================================
# 7. Xinitrc (Pi OS Lite - starts kiosk from console)
# ============================================================
if [ "$HAS_DESKTOP" = false ]; then
cat > "$PI_HOME/.xinitrc" << 'EOF'
#!/bin/bash
exec ~/screentinker-kiosk.sh
EOF
chmod +x "$PI_HOME/.xinitrc"
chown "$PI_USER":"$PI_USER" "$PI_HOME/.xinitrc"
fi
# ============================================================
# 8. Kiosk systemd service
# ============================================================
log "Creating kiosk service..."
if [ "$HAS_DESKTOP" = false ]; then
# Lite: start X ourselves
if [ "$PLAYER_ONLY" = false ]; then
KIOSK_AFTER="After=screentinker-server.service"
KIOSK_REQ="Requires=screentinker-server.service"
else
KIOSK_AFTER="After=network-online.target"
KIOSK_REQ="Wants=network-online.target"
fi
cat > /etc/systemd/system/screentinker-kiosk.service << EOF
[Unit]
Description=ScreenTinker Kiosk Display
${KIOSK_AFTER}
${KIOSK_REQ}
[Service]
Type=simple
User=${PI_USER}
Environment=DISPLAY=:0
Environment=XAUTHORITY=${PI_HOME}/.Xauthority
ExecStartPre=/bin/sleep 3
ExecStart=/usr/bin/startx ${PI_HOME}/.xinitrc -- :0 -nolisten tcp vt1
Restart=always
RestartSec=10
TTYPath=/dev/tty1
StandardInput=tty
StandardOutput=journal
StandardError=journal
SyslogIdentifier=screentinker-kiosk
[Install]
WantedBy=multi-user.target
EOF
else
# Desktop: X already running, just launch Chromium
if [ "$PLAYER_ONLY" = false ]; then
KIOSK_AFTER="After=screentinker-server.service graphical.target"
KIOSK_REQ="Requires=screentinker-server.service"
else
KIOSK_AFTER="After=graphical.target"
KIOSK_REQ="Wants=graphical.target"
fi
cat > /etc/systemd/system/screentinker-kiosk.service << EOF
[Unit]
Description=ScreenTinker Kiosk Display
${KIOSK_AFTER}
${KIOSK_REQ}
[Service]
Type=simple
User=${PI_USER}
Environment=DISPLAY=:0
ExecStartPre=/bin/sleep 5
ExecStart=/bin/bash ${PI_HOME}/screentinker-kiosk.sh
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=screentinker-kiosk
[Install]
WantedBy=graphical.target
EOF
fi
systemctl daemon-reload
systemctl enable screentinker-kiosk.service
log "Kiosk service enabled"
# Desktop: autostart entry as fallback
if [ "$HAS_DESKTOP" = true ]; then
AUTOSTART_DIR="$PI_HOME/.config/autostart"
mkdir -p "$AUTOSTART_DIR"
cat > "$AUTOSTART_DIR/screentinker.desktop" << EOF
[Desktop Entry]
Type=Application
Name=ScreenTinker Player
Exec=${PI_HOME}/screentinker-kiosk.sh
X-GNOME-Autostart-enabled=true
EOF
chown -R "$PI_USER":"$PI_USER" "$AUTOSTART_DIR"
fi
# ============================================================
# 9. Auto-login on tty1 (Lite only)
# ============================================================
if [ "$HAS_DESKTOP" = false ]; then
log "Configuring auto-login on tty1..."
mkdir -p /etc/systemd/system/getty@tty1.service.d
cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << EOF
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin ${PI_USER} --noclear %I \$TERM
EOF
fi
# ============================================================
# 10. Pi display and boot optimizations
# ============================================================
log "Applying display optimizations..."
# Find config.txt (Pi 5 uses /boot/firmware/, older uses /boot/)
CONFIG_FILE=""
for p in /boot/firmware/config.txt /boot/config.txt; do
[ -f "$p" ] && CONFIG_FILE="$p" && break
done
if [ -n "$CONFIG_FILE" ]; then
# GPU memory for video playback
if ! grep -q "^gpu_mem=" "$CONFIG_FILE"; then
echo -e "\n# ScreenTinker: GPU memory for smooth video" >> "$CONFIG_FILE"
echo "gpu_mem=128" >> "$CONFIG_FILE"
log "GPU memory: 128MB"
fi
# Disable overscan (removes black borders on TVs)
if ! grep -q "^disable_overscan=1" "$CONFIG_FILE"; then
echo "disable_overscan=1" >> "$CONFIG_FILE"
log "Overscan disabled"
fi
fi
# Disable console blanking
for p in /boot/firmware/cmdline.txt /boot/cmdline.txt; do
if [ -f "$p" ]; then
if ! grep -q "consoleblank=0" "$p"; then
sed -i 's/$/ consoleblank=0/' "$p"
log "Console blanking disabled"
fi
break
fi
done
# Lightdm screen blanking (Desktop only)
if [ "$HAS_DESKTOP" = true ] && [ -f /etc/lightdm/lightdm.conf ]; then
sed -i 's/#xserver-command=X/xserver-command=X -s 0 -dpms/' /etc/lightdm/lightdm.conf
fi
# Hardware watchdog for auto-recovery from system hangs
if grep -q "#RuntimeWatchdogSec=0" /etc/systemd/system.conf 2>/dev/null; then
sed -i 's/#RuntimeWatchdogSec=0/RuntimeWatchdogSec=10/' /etc/systemd/system.conf
log "Hardware watchdog enabled (10s)"
fi
# ============================================================
# 11. Management scripts (all-in-one only)
# ============================================================
if [ "$PLAYER_ONLY" = false ]; then
log "Creating management scripts..."
cat > /usr/local/bin/screentinker-update << 'UPDATEEOF'
#!/bin/bash
echo "Stopping services..."
sudo systemctl stop screentinker-kiosk.service 2>/dev/null || true
sudo systemctl stop screentinker-server.service 2>/dev/null || true
echo "Pulling latest..."
cd /opt/screentinker && git pull origin main
echo "Installing dependencies..."
cd server && npm install --production
echo "Starting services..."
sudo systemctl start screentinker-server.service
sleep 3
sudo systemctl start screentinker-kiosk.service
echo ""
echo "Done! Server: $(systemctl is-active screentinker-server.service)"
echo " Kiosk: $(systemctl is-active screentinker-kiosk.service)"
UPDATEEOF
chmod +x /usr/local/bin/screentinker-update
cat > /usr/local/bin/screentinker-status << 'STATUSEOF'
#!/bin/bash
echo ""
echo "=== ScreenTinker Status ==="
echo ""
IP=$(hostname -I | awk '{print $1}')
if systemctl is-active screentinker-server.service &>/dev/null; then
echo "Server: RUNNING (PID $(systemctl show screentinker-server.service -p MainPID --value))"
else
echo "Server: STOPPED"
fi
if systemctl is-active screentinker-kiosk.service &>/dev/null; then
echo "Kiosk: RUNNING"
else
echo "Kiosk: STOPPED"
fi
echo ""
echo "Uptime: $(uptime -p)"
echo "CPU Temp: $(vcgencmd measure_temp 2>/dev/null | cut -d= -f2 || echo 'n/a')"
echo "Disk: $(df -h /opt/screentinker 2>/dev/null | tail -1 | awk '{print $3 "/" $2 " (" $5 " used)"}')"
echo "Memory: $(free -h | awk '/Mem:/ {print $3 " / " $2}')"
echo ""
echo "Dashboard: http://${IP}:3001"
echo "Player: http://${IP}:3001/player"
echo "mDNS: http://$(hostname).local:3001"
echo ""
STATUSEOF
chmod +x /usr/local/bin/screentinker-status
cat > /usr/local/bin/screentinker-logs << 'LOGSEOF'
#!/bin/bash
case "${1:-server}" in
server) journalctl -u screentinker-server.service -f --no-hostname ;;
kiosk) journalctl -u screentinker-kiosk.service -f --no-hostname ;;
all) journalctl -u screentinker-server.service -u screentinker-kiosk.service -f --no-hostname ;;
*) echo "Usage: screentinker-logs [server|kiosk|all]" ;;
esac
LOGSEOF
chmod +x /usr/local/bin/screentinker-logs
fi
# ============================================================
# 12. MOTD
# ============================================================
cat > /etc/motd << 'MOTDEOF'
____ _____ _ _
/ ___| ___ _ __ ___ ___ _ __ |_ _|(_) _ __ | | __ ___ _ __
\___ \ / __|| '__| / _ \ / _ \| '_ \ | | | || '_ \ | |/ / / _ \| '__|
___) || (__ | | | __/| __/| | | | | | | || | | || < | __/| |
|____/ \___||_| \___| \___||_| |_| |_| |_||_| |_||_|\_\ \___||_|
Open-Source Digital Signage for Any Screen
Commands:
screentinker-status Show system info and URLs
screentinker-update Pull latest and restart
screentinker-logs Follow logs (server|kiosk|all)
MOTDEOF
# ============================================================
# 13. Clean up legacy remotedisplay naming
# ============================================================
if [ -f /etc/systemd/system/remotedisplay.service ]; then
log "Cleaning up legacy remotedisplay service..."
systemctl stop remotedisplay.service 2>/dev/null || true
systemctl disable remotedisplay.service 2>/dev/null || true
rm -f /etc/systemd/system/remotedisplay.service
rm -f "$PI_HOME/remotedisplay-kiosk.sh"
rm -f "$PI_HOME/.config/autostart/remotedisplay.desktop"
systemctl daemon-reload
fi
# ============================================================
# Done
# ============================================================
echo ""
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN} ScreenTinker Setup Complete!${NC}"
echo -e "${GREEN}======================================${NC}"
echo ""
IP=$(hostname -I | awk '{print $1}')
if [ "$PLAYER_ONLY" = false ]; then
echo "Mode: All-in-One (server + player)"
echo ""
echo "After reboot this Pi will:"
echo " - Start the ScreenTinker server on port $SCREENTINKER_PORT"
echo " - Display the player fullscreen on the connected screen"
echo ""
echo "First steps:"
echo " 1. Reboot: sudo reboot"
echo " 2. From your phone, go to http://${IP}:${SCREENTINKER_PORT}"
echo " (or http://$(hostname).local:${SCREENTINKER_PORT})"
echo " 3. Register - first user gets full admin access"
echo " 4. Add a display and enter the pairing code from the TV"
echo " 5. Upload content and push it to the screen"
echo ""
echo "Management:"
echo " screentinker-status Check everything is running"
echo " screentinker-update Update to latest version"
echo " screentinker-logs Watch server logs"
else
echo "Mode: Player Only"
echo "Server: $SERVER_URL"
echo ""
echo "After reboot this Pi will:"
echo " - Open the player in fullscreen kiosk mode"
echo " - Auto-reconnect if the server goes down"
echo ""
echo "To pair:"
echo " 1. Reboot: sudo reboot"
echo " 2. The pairing screen will appear on the TV"
echo " 3. Enter the code in your ScreenTinker dashboard"
fi
echo ""
echo "Services:"
if [ "$PLAYER_ONLY" = false ]; then
echo " sudo systemctl [start|stop|restart] screentinker-server"
fi
echo " sudo systemctl [start|stop|restart] screentinker-kiosk"
echo ""
echo -e "${YELLOW}Reboot to start: sudo reboot${NC}"
echo ""