#!/bin/sh # Copyright (C) 2025-2026 356C LLC # SPDX-License-Identifier: GPL-3.0-or-later # # HelixScreen Installer # This file is auto-generated by bundle-installer.sh # # Usage: # curl -sSL https://raw.githubusercontent.com/prestonbrown/helixscreen/main/scripts/install.sh | sh # # Or download and run: # wget https://raw.githubusercontent.com/prestonbrown/helixscreen/main/scripts/install.sh # chmod +x install.sh # ./install.sh # # Options: # --update Update existing installation (preserves config) # --uninstall Remove HelixScreen # --clean Remove old installation completely before installing (no config backup) # --version Specify version (default: latest) # # Mark as bundled installer (modules check this to skip sourcing) _HELIX_BUNDLED_INSTALLER=1 # Fail fast on any error set -eu # Configuration GITHUB_REPO="prestonbrown/helixscreen" SERVICE_NAME="helixscreen" # ============================================ # Module: common.sh # ============================================ # # Default configuration (can be overridden before sourcing) : "${GITHUB_REPO:=prestonbrown/helixscreen}" : "${INSTALL_DIR:=/opt/helixscreen}" : "${SERVICE_NAME:=helixscreen}" # Well-known paths (used by uninstall, clean, stop_service) # AD5M: /opt/helixscreen or /root/printer_software/helixscreen # K1: /usr/data/helixscreen # Pi: /opt/helixscreen HELIX_INSTALL_DIRS="/root/printer_software/helixscreen /opt/helixscreen /usr/data/helixscreen" # Init script locations vary by platform/firmware # AD5M Klipper Mod: S80, AD5M Forge-X: S90, K1: S99 HELIX_INIT_SCRIPTS="/etc/init.d/S80helixscreen /etc/init.d/S90helixscreen /etc/init.d/S99helixscreen" # HelixScreen process names (order matters: watchdog first to prevent crash dialog) HELIX_PROCESSES="helix-watchdog helix-screen helix-splash" # Track what we've done for cleanup CLEANUP_TMP=false CLEANUP_SERVICE=false BACKUP_CONFIG="" ORIGINAL_INSTALL_EXISTS=false # Colors (if terminal supports it) setup_colors() { if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' else RED='' GREEN='' YELLOW='' CYAN='' BOLD='' NC='' fi } # Initialize colors immediately setup_colors # Logging functions log_info() { echo "${CYAN}[INFO]${NC} $1" >&2; } log_success() { echo "${GREEN}[OK]${NC} $1" >&2; } log_warn() { echo "${YELLOW}[WARN]${NC} $1" >&2; } log_error() { echo "${RED}[ERROR]${NC} $1" >&2; } # Error handler - cleanup and report what went wrong # Usage: trap 'error_handler $LINENO' ERR error_handler() { local exit_code=$? local line_no=$1 echo "" log_error "==========================================" log_error "Installation FAILED at line $line_no" log_error "Exit code: $exit_code" log_error "==========================================" echo "" # Cleanup temporary files if [ "$CLEANUP_TMP" = true ] && [ -d "$TMP_DIR" ]; then log_info "Cleaning up temporary files..." rm -rf "$TMP_DIR" fi # If we backed up config and install failed, try to restore state if [ -n "$BACKUP_CONFIG" ] && [ -f "$BACKUP_CONFIG" ]; then log_info "Restoring backed up configuration..." if $SUDO mkdir -p "${INSTALL_DIR}/config" 2>/dev/null; then if $SUDO cp "$BACKUP_CONFIG" "${INSTALL_DIR}/config/helixconfig.json" 2>/dev/null; then log_success "Configuration restored" else log_warn "Could not restore config. Backup saved at: $BACKUP_CONFIG" fi else log_warn "Could not create config directory. Backup saved at: $BACKUP_CONFIG" fi fi echo "" log_error "Installation was NOT completed." log_error "Your system should be in its original state." echo "" log_info "For help, please:" log_info " 1. Check the error message above" log_info " 2. Verify network connectivity" log_info " 3. Report issues at: https://github.com/${GITHUB_REPO}/issues" echo "" exit $exit_code } # Cleanup function for normal exit cleanup_on_success() { if [ -d "$TMP_DIR" ]; then rm -rf "$TMP_DIR" fi } # Kill process(es) by name, using killall or pidof fallback # Works on both GNU systems and BusyBox (AD5M/K1) # Args: process_name [process_name2 ...] # Returns: 0 if any process was killed, 1 if none found kill_process_by_name() { local killed_any=false for proc in "$@"; do if command -v killall >/dev/null 2>&1; then if killall -0 "$proc" 2>/dev/null; then $SUDO killall "$proc" 2>/dev/null || true killed_any=true fi elif command -v pidof >/dev/null 2>&1; then local pids pids=$(pidof "$proc" 2>/dev/null) if [ -n "$pids" ]; then for pid in $pids; do $SUDO kill "$pid" 2>/dev/null || true done killed_any=true fi fi done [ "$killed_any" = true ] } # Print post-install commands for the user # Reads: INIT_SYSTEM, SERVICE_NAME, INIT_SCRIPT_DEST print_post_install_commands() { echo "Useful commands:" if [ "$INIT_SYSTEM" = "systemd" ]; then echo " systemctl status ${SERVICE_NAME} # Check status" echo " journalctl -u ${SERVICE_NAME} -f # View logs" echo " systemctl restart ${SERVICE_NAME} # Restart" else echo " ${INIT_SCRIPT_DEST} status # Check status" echo " cat /tmp/helixscreen.log # View logs" echo " ${INIT_SCRIPT_DEST} restart # Restart" fi } # ============================================ # Module: platform.sh # ============================================ # # Default paths (may be overridden by set_install_paths) : "${INSTALL_DIR:=/opt/helixscreen}" : "${TMP_DIR:=/tmp/helixscreen-install}" # Capture user-provided INSTALL_DIR before we potentially override it. # If the user explicitly set INSTALL_DIR (and it's not the default), # we respect their choice over auto-detection. _USER_INSTALL_DIR="${INSTALL_DIR}" [ "$_USER_INSTALL_DIR" = "/opt/helixscreen" ] && _USER_INSTALL_DIR="" INIT_SCRIPT_DEST="" PREVIOUS_UI_SCRIPT="" AD5M_FIRMWARE="" K1_FIRMWARE="" KLIPPER_USER="" KLIPPER_HOME="" # Detect platform # Returns: "ad5m", "k1", "pi", "pi32", or "unsupported" detect_platform() { local arch kernel arch=$(uname -m) kernel=$(uname -r) # Check for AD5M (armv7l with specific kernel) if [ "$arch" = "armv7l" ]; then # AD5M has a specific kernel identifier if echo "$kernel" | grep -q "ad5m\|5.4.61"; then echo "ad5m" return fi fi # Check for Creality K1 series (Simple AF or stock with Klipper) # K1 uses buildroot and has /usr/data structure if [ -f /etc/os-release ] && grep -q "buildroot" /etc/os-release 2>/dev/null; then # Buildroot-based system - check for K1 indicators if [ -d "/usr/data" ]; then # Check for K1-specific indicators (require at least 2 for confidence) # - get_sn_mac.sh is a Creality-specific script # - /usr/data/pellcorp is Simple AF # - /usr/data/printer_data with klipper is a strong K1 indicator local k1_indicators=0 [ -x "/usr/bin/get_sn_mac.sh" ] && k1_indicators=$((k1_indicators + 1)) [ -d "/usr/data/pellcorp" ] && k1_indicators=$((k1_indicators + 1)) [ -d "/usr/data/printer_data" ] && k1_indicators=$((k1_indicators + 1)) [ -d "/usr/data/klipper" ] && k1_indicators=$((k1_indicators + 1)) # Also check for Creality-specific paths [ -f "/usr/data/creality/userdata/config/system_config.json" ] && k1_indicators=$((k1_indicators + 1)) if [ "$k1_indicators" -ge 2 ]; then echo "k1" return fi fi fi # Check for Raspberry Pi (aarch64 or armv7l) # Returns "pi" for 64-bit, "pi32" for 32-bit if [ "$arch" = "aarch64" ] || [ "$arch" = "armv7l" ]; then local is_pi=false if [ -f /etc/os-release ] && grep -q "Raspbian\|Debian" /etc/os-release; then is_pi=true fi # Also check for MainsailOS / BTT Pi / MKS if [ -d /home/pi ] || [ -d /home/mks ] || [ -d /home/biqu ]; then is_pi=true fi if [ "$is_pi" = true ]; then if [ "$arch" = "aarch64" ]; then echo "pi" else echo "pi32" fi return fi fi # Unknown ARM device - don't assume it's a Pi # Require explicit platform indicators to avoid false positives if [ "$arch" = "aarch64" ] || [ "$arch" = "armv7l" ]; then log_warn "Unknown ARM platform. Cannot auto-detect." echo "unsupported" return fi echo "unsupported" } # Detect the Klipper ecosystem user (who runs klipper/moonraker services) # Detection cascade (most reliable first): # 1. systemd: systemctl show klipper.service # 2. Process table: ps for running klipper # 3. printer_data scan: /home/*/printer_data # 4. Well-known users: biqu, pi, mks # 5. Fallback: root # Sets: KLIPPER_USER, KLIPPER_HOME detect_klipper_user() { # 1. systemd service owner (most reliable on Pi) if command -v systemctl >/dev/null 2>&1; then local svc_user svc_user=$(systemctl show -p User --value klipper.service 2>/dev/null) || true if [ -n "$svc_user" ] && [ "$svc_user" != "root" ] && id "$svc_user" >/dev/null 2>&1; then KLIPPER_USER="$svc_user" KLIPPER_HOME=$(eval echo "~$svc_user") log_info "Klipper user (systemd): $KLIPPER_USER" return 0 fi fi # 2. Process table (catches running instances) local ps_user ps_user=$(ps -eo user,comm 2>/dev/null | awk '/klipper$/ && !/grep/ {print $1; exit}') || true if [ -n "$ps_user" ] && [ "$ps_user" != "root" ] && id "$ps_user" >/dev/null 2>&1; then KLIPPER_USER="$ps_user" KLIPPER_HOME=$(eval echo "~$ps_user") log_info "Klipper user (process): $KLIPPER_USER" return 0 fi # 3. printer_data directory scan local pd_dir for pd_dir in /home/*/printer_data; do [ -d "$pd_dir" ] || continue local pd_user pd_user=$(echo "$pd_dir" | sed 's|^/home/||;s|/printer_data$||') if [ -n "$pd_user" ] && id "$pd_user" >/dev/null 2>&1; then KLIPPER_USER="$pd_user" KLIPPER_HOME="/home/$pd_user" log_info "Klipper user (printer_data): $KLIPPER_USER" return 0 fi done # 4. Well-known users (checked in priority order) local known_user for known_user in biqu pi mks; do if id "$known_user" >/dev/null 2>&1; then KLIPPER_USER="$known_user" KLIPPER_HOME="/home/$known_user" log_info "Klipper user (well-known): $KLIPPER_USER" return 0 fi done # 5. Fallback: root (embedded platforms, AD5M, K1) KLIPPER_USER="root" KLIPPER_HOME="/root" log_info "Klipper user (fallback): root" return 0 } # Detect AD5M firmware variant (Klipper Mod vs Forge-X) # Only called when platform is "ad5m" # Returns: "klipper_mod" or "forge_x" detect_ad5m_firmware() { # Klipper Mod indicators - check for its specific directory structure # Klipper Mod runs in a chroot on /mnt/data/.klipper_mod/chroot # and puts printer software in /root/printer_software/ if [ -d "/root/printer_software" ] || [ -d "/mnt/data/.klipper_mod" ]; then echo "klipper_mod" return fi # Forge-X indicators - check for its mod overlay structure if [ -d "/opt/config/mod/.root" ]; then echo "forge_x" return fi # Default to forge_x (original behavior, most common) echo "forge_x" } # Detect K1 firmware variant (Simple AF vs other) # Only called when platform is "k1" # Returns: "simple_af" or "stock_klipper" detect_k1_firmware() { # Simple AF (pellcorp/creality) indicators if [ -d "/usr/data/pellcorp" ]; then echo "simple_af" return fi # Check for GuppyScreen which Simple AF installs if [ -d "/usr/data/guppyscreen" ] && [ -f "/etc/init.d/S99guppyscreen" ]; then echo "simple_af" return fi # Default to stock_klipper (generic K1 with Klipper) echo "stock_klipper" } # Detect Pi install directory based on Klipper ecosystem presence # Cascade (first match wins): # 1. User explicitly set INSTALL_DIR → keep it # 2. ~/klipper or ~/moonraker exists → ~/helixscreen # 3. ~/printer_data exists → ~/helixscreen # 4. moonraker.service is active → ~/helixscreen # 5. Fallback → /opt/helixscreen # Requires: KLIPPER_HOME to be set (by detect_klipper_user) # Sets: INSTALL_DIR detect_pi_install_dir() { # 1. User explicitly set INSTALL_DIR — respect their choice if [ -n "$_USER_INSTALL_DIR" ]; then INSTALL_DIR="$_USER_INSTALL_DIR" log_info "Install directory (user override): $INSTALL_DIR" return 0 fi # Need KLIPPER_HOME for ecosystem detection if [ -z "$KLIPPER_HOME" ]; then INSTALL_DIR="/opt/helixscreen" return 0 fi # 2. Klipper or Moonraker source directories if [ -d "$KLIPPER_HOME/klipper" ] || [ -d "$KLIPPER_HOME/moonraker" ]; then INSTALL_DIR="$KLIPPER_HOME/helixscreen" log_info "Install directory (klipper ecosystem): $INSTALL_DIR" return 0 fi # 3. printer_data directory (Klipper config structure) if [ -d "$KLIPPER_HOME/printer_data" ]; then INSTALL_DIR="$KLIPPER_HOME/helixscreen" log_info "Install directory (printer_data): $INSTALL_DIR" return 0 fi # 4. Moonraker service running (ecosystem present but maybe different layout) if command -v systemctl >/dev/null 2>&1; then if systemctl is-active --quiet moonraker.service 2>/dev/null || \ systemctl is-active --quiet moonraker 2>/dev/null; then INSTALL_DIR="$KLIPPER_HOME/helixscreen" log_info "Install directory (moonraker service): $INSTALL_DIR" return 0 fi fi # 5. Fallback: no ecosystem detected INSTALL_DIR="/opt/helixscreen" return 0 } # Set installation paths based on platform and firmware # Sets: INSTALL_DIR, INIT_SCRIPT_DEST, PREVIOUS_UI_SCRIPT, TMP_DIR set_install_paths() { local platform=$1 local firmware=${2:-} if [ "$platform" = "ad5m" ]; then case "$firmware" in klipper_mod) INSTALL_DIR="/root/printer_software/helixscreen" INIT_SCRIPT_DEST="/etc/init.d/S80helixscreen" PREVIOUS_UI_SCRIPT="/etc/init.d/S80klipperscreen" # Klipper Mod has small tmpfs (~54MB), package is ~70MB # Use /mnt/data which has 4+ GB available TMP_DIR="/mnt/data/helixscreen-install" log_info "AD5M firmware: Klipper Mod" log_info "Install directory: ${INSTALL_DIR}" log_info "Using /mnt/data for temp files (tmpfs too small)" ;; forge_x|*) INSTALL_DIR="/opt/helixscreen" INIT_SCRIPT_DEST="/etc/init.d/S90helixscreen" PREVIOUS_UI_SCRIPT="/opt/config/mod/.root/S80guppyscreen" TMP_DIR="/tmp/helixscreen-install" log_info "AD5M firmware: Forge-X" log_info "Install directory: ${INSTALL_DIR}" ;; esac elif [ "$platform" = "k1" ]; then # Creality K1 series - uses /usr/data structure case "$firmware" in simple_af|*) INSTALL_DIR="/usr/data/helixscreen" INIT_SCRIPT_DEST="/etc/init.d/S99helixscreen" PREVIOUS_UI_SCRIPT="/etc/init.d/S99guppyscreen" TMP_DIR="/tmp/helixscreen-install" log_info "K1 firmware: Simple AF" log_info "Install directory: ${INSTALL_DIR}" ;; esac else # Pi and other platforms — detect klipper user, then auto-detect install dir INIT_SCRIPT_DEST="/etc/init.d/S90helixscreen" PREVIOUS_UI_SCRIPT="" TMP_DIR="/tmp/helixscreen-install" detect_klipper_user detect_pi_install_dir fi } # ============================================ # Module: permissions.sh # ============================================ # # Initialize SUDO (will be set by check_permissions) SUDO="" # Check if running as root (required for AD5M/K1, optional for Pi) # Sets: SUDO variable ("sudo" or "") check_permissions() { local platform=$1 if [ "$platform" = "ad5m" ] || [ "$platform" = "k1" ]; then if [ "$(id -u)" != "0" ]; then log_error "Installation on $platform requires root privileges." log_error "Please run: sudo $0 $*" exit 1 fi SUDO="" else # Pi: warn if not root but allow sudo if [ "$(id -u)" != "0" ]; then if ! command -v sudo >/dev/null 2>&1; then log_error "Not running as root and sudo is not available." log_error "Please run as root or install sudo." exit 1 fi log_info "Not running as root. Will use sudo for privileged operations." SUDO="sudo" else SUDO="" fi fi } # ============================================ # Module: requirements.sh # ============================================ # # Initialize INIT_SYSTEM (will be set by detect_init_system) INIT_SYSTEM="" # Check required commands exist # Requires: curl or wget, tar, gunzip check_requirements() { local missing="" # Need either curl or wget if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then missing="curl or wget" fi # Need tar if ! command -v tar >/dev/null 2>&1; then if [ -n "$missing" ]; then missing="$missing, tar" else missing="tar" fi fi # Need gunzip (for AD5M BusyBox tar which doesn't support -z) if ! command -v gunzip >/dev/null 2>&1; then if [ -n "$missing" ]; then missing="$missing, gunzip" else missing="gunzip" fi fi if [ -n "$missing" ]; then log_error "Missing required commands: $missing" log_error "Please install them and try again." exit 1 fi log_success "All required commands available" } # Install runtime dependencies for Pi platform # Required for DRM display and evdev input handling # AD5M uses framebuffer with static linking, no deps needed install_runtime_deps() { local platform=$1 # Only needed for Pi (32-bit and 64-bit) - AD5M uses framebuffer with static linking if [ "$platform" != "pi" ] && [ "$platform" != "pi32" ]; then return 0 fi log_info "Checking runtime dependencies for display/input..." # Required libraries for DRM display and libinput # Note: GPU libs (libgles2, libegl1, libgbm1) not needed - using software rendering local deps="libdrm2 libinput10" local missing="" for dep in $deps; do # Check if package is installed (dpkg-query returns 0 if installed) if ! dpkg-query -W -f='${Status}' "$dep" 2>/dev/null | grep -q "install ok installed"; then if [ -n "$missing" ]; then missing="$missing $dep" else missing="$dep" fi fi done if [ -n "$missing" ]; then log_info "Installing missing libraries: $missing" $SUDO apt-get update -qq # shellcheck disable=SC2086 $SUDO apt-get install -y --no-install-recommends $missing log_success "Runtime libraries installed" else log_success "All runtime libraries already installed" fi } # Check available disk space # Requires at least 50MB free on the install directory's filesystem # Note: INSTALL_DIR must be set before calling this function check_disk_space() { local platform=$1 local required_mb=50 # Get the parent directory of install location (the filesystem to check) local check_dir check_dir=$(dirname "${INSTALL_DIR:-/opt/helixscreen}") # Walk up until we find an existing directory while [ ! -d "$check_dir" ] && [ "$check_dir" != "/" ]; do check_dir=$(dirname "$check_dir") done if [ "$check_dir" = "/" ]; then check_dir="/" fi # Get available space in MB local available_mb if [ "$platform" = "ad5m" ] || [ "$platform" = "k1" ]; then # BusyBox df output format: blocks are in KB by default available_mb=$(df "$check_dir" 2>/dev/null | tail -1 | awk '{print int($4/1024)}') else # GNU df with -m flag outputs in MB available_mb=$(df -m "$check_dir" 2>/dev/null | tail -1 | awk '{print $4}') fi if [ -n "$available_mb" ] && [ "$available_mb" -lt "$required_mb" ]; then log_error "Insufficient disk space on $check_dir" log_error "Required: ${required_mb}MB, Available: ${available_mb}MB" exit 1 fi log_info "Disk space check: ${available_mb}MB available on $check_dir" } # Detect init system (systemd vs SysV) # Sets: INIT_SYSTEM to "systemd" or "sysv" detect_init_system() { # Check for systemd if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then INIT_SYSTEM="systemd" log_info "Init system: systemd" return fi # Check for SysV init (BusyBox or traditional) if [ -d /etc/init.d ]; then INIT_SYSTEM="sysv" log_info "Init system: SysV (BusyBox/traditional)" return fi log_error "Could not detect init system." log_error "Neither systemd nor /etc/init.d found." exit 1 } # ============================================ # Module: forgex.sh # ============================================ # # Configure ForgeX display settings for HelixScreen # We use GUPPY mode because ForgeX handles backlight properly in this mode. # STOCK mode expects ffstartup-arm to manage display/backlight which doesn't work for us. # We disable GuppyScreen's init scripts so HelixScreen takes over the display. configure_forgex_display() { var_file="/opt/config/mod_data/variables.cfg" guppy_init="/opt/config/mod/.root/S80guppyscreen" tslib_init="/opt/config/mod/.root/S35tslib" changed=false # Set display mode to GUPPY (required for backlight to work) if [ -f "$var_file" ]; then if grep -q "display[[:space:]]*=[[:space:]]*'STOCK'" "$var_file"; then log_info "Setting ForgeX display mode to GUPPY..." $SUDO sed -i "s/display[[:space:]]*=[[:space:]]*'STOCK'/display = 'GUPPY'/" "$var_file" changed=true elif grep -q "display[[:space:]]*=[[:space:]]*'HEADLESS'" "$var_file"; then log_info "Setting ForgeX display mode to GUPPY..." $SUDO sed -i "s/display[[:space:]]*=[[:space:]]*'HEADLESS'/display = 'GUPPY'/" "$var_file" changed=true fi fi # Disable GuppyScreen init script (remove execute permission) if [ -x "$guppy_init" ]; then log_info "Disabling GuppyScreen init script..." $SUDO chmod -x "$guppy_init" changed=true fi # Disable tslib init script (GuppyScreen's touch input layer) # HelixScreen uses its own input handling if [ -x "$tslib_init" ]; then log_info "Disabling tslib init script..." $SUDO chmod -x "$tslib_init" changed=true fi if [ "$changed" = true ]; then log_success "ForgeX configured for HelixScreen (GUPPY mode, GuppyScreen disabled)" return 0 fi return 1 } # Patch ForgeX screen.sh to skip non-100 backlight control when HelixScreen is active # ForgeX's headless.cfg runs a delayed_gcode that dims the backlight 3 seconds after # Klipper starts. This patch blocks dimming calls but allows the S99root 0→100 cycle. # # The smart patch: # - Allows "backlight 100" (needed for S99root initialization cycle) # - Blocks other values (10, 0, etc.) when helixscreen_active flag exists patch_forgex_screen_sh() { screen_sh="/opt/config/mod/.shell/screen.sh" if [ ! -f "$screen_sh" ]; then log_info "ForgeX screen.sh not found, skipping patch" return 1 fi # Check if already patched (look for the smart patch signature) if grep -q 'helixscreen_active.*!=.*100' "$screen_sh" 2>/dev/null; then log_info "ForgeX screen.sh already has smart patch" return 0 fi # Remove old-style patch if present (blocks ALL backlight when flag exists) if grep -q "helixscreen_active" "$screen_sh" 2>/dev/null; then log_info "Removing old-style patch from screen.sh..." tmp_file="${screen_sh}.tmp" grep -v "helixscreen_active\|# Skip if HelixScreen" "$screen_sh" > "$tmp_file" $SUDO mv "$tmp_file" "$screen_sh" $SUDO chmod +x "$screen_sh" fi # Find the backlight) case and add our guard if ! grep -q "^[[:space:]]*backlight)" "$screen_sh"; then log_info "Could not find backlight case in screen.sh" return 1 fi log_info "Patching ForgeX screen.sh with smart backlight control..." # Use awk to insert our check after "backlight)" line (BusyBox compatible) # Smart patch: only block non-100 values, allowing S99root's 0→100 init cycle tmp_file="${screen_sh}.tmp" awk ' /^[[:space:]]*backlight\)/ { print print " # Skip non-100 backlight changes when HelixScreen is controlling display" print " # Allows S99root 0->100 init cycle but blocks Klipper eco dimming" print " if [ -f /tmp/helixscreen_active ] && [ \"$2\" != \"100\" ]; then" print " exit 0" print " fi" next } { print } ' "$screen_sh" > "$tmp_file" if [ -s "$tmp_file" ] && grep -q 'helixscreen_active.*!=.*100' "$tmp_file" 2>/dev/null; then $SUDO mv "$tmp_file" "$screen_sh" $SUDO chmod +x "$screen_sh" log_success "ForgeX screen.sh patched with smart backlight control" return 0 else rm -f "$tmp_file" log_warn "Failed to patch ForgeX screen.sh" return 1 fi } # Remove HelixScreen patch from ForgeX screen.sh (for uninstall) unpatch_forgex_screen_sh() { screen_sh="/opt/config/mod/.shell/screen.sh" if [ ! -f "$screen_sh" ]; then return 1 fi # Check if patched if ! grep -q "helixscreen_active" "$screen_sh" 2>/dev/null; then log_info "ForgeX screen.sh not patched, nothing to remove" return 0 fi log_info "Removing HelixScreen patch from ForgeX screen.sh..." # Use awk to remove only our specific block (BusyBox compatible) # Match and skip: comment line, if line with helixscreen_active, exit 0, fi tmp_file="${screen_sh}.tmp" awk ' /# Skip if HelixScreen is controlling the display/ { skip=1; next } /if \[ -f \/tmp\/helixscreen_active \]; then/ { skip=1; next } skip && /^[[:space:]]*exit 0[[:space:]]*$/ { next } skip && /^[[:space:]]*fi[[:space:]]*$/ { skip=0; next } { print } ' "$screen_sh" > "$tmp_file" if [ -s "$tmp_file" ]; then $SUDO mv "$tmp_file" "$screen_sh" $SUDO chmod +x "$screen_sh" else rm -f "$tmp_file" log_warn "Failed to unpatch ForgeX screen.sh" return 1 fi # Verify removal if grep -q "helixscreen_active" "$screen_sh" 2>/dev/null; then log_warn "Could not fully remove patch from screen.sh" return 1 fi log_success "ForgeX screen.sh patch removed" return 0 } # Disable stock FlashForge UI in auto_run.sh # The stock firmware UI (ffstartup-arm/firmwareExe) is started by /opt/auto_run.sh # which runs AFTER init scripts. We comment out the line to prevent it starting. disable_stock_firmware_ui() { auto_run="/opt/auto_run.sh" if [ -f "$auto_run" ]; then # Check if ffstartup-arm line exists and is not already commented if grep -q "^/opt/PROGRAM/ffstartup-arm" "$auto_run"; then log_info "Disabling stock FlashForge UI in auto_run.sh..." # Comment out the ffstartup-arm line $SUDO sed -i 's|^/opt/PROGRAM/ffstartup-arm|# Disabled by HelixScreen: /opt/PROGRAM/ffstartup-arm|' "$auto_run" log_success "Stock FlashForge UI disabled" return 0 fi fi return 1 } # Re-enable stock FlashForge UI in auto_run.sh (for uninstall) restore_stock_firmware_ui() { auto_run="/opt/auto_run.sh" if [ -f "$auto_run" ]; then # Check if our disabled line exists if grep -q "^# Disabled by HelixScreen: /opt/PROGRAM/ffstartup-arm" "$auto_run"; then log_info "Re-enabling stock FlashForge UI in auto_run.sh..." # Uncomment the ffstartup-arm line $SUDO sed -i 's|^# Disabled by HelixScreen: /opt/PROGRAM/ffstartup-arm|/opt/PROGRAM/ffstartup-arm|' "$auto_run" log_success "Stock FlashForge UI re-enabled" return 0 fi fi return 1 } # Patch ForgeX screen.sh to skip screen drawing when HelixScreen is active # ForgeX's S99root calls draw_splash, draw_loading, and boot_message which write # directly to the framebuffer, overwriting our splash screen during boot. patch_forgex_screen_drawing() { screen_sh="/opt/config/mod/.shell/screen.sh" if [ ! -f "$screen_sh" ]; then log_info "ForgeX screen.sh not found, skipping screen drawing patch" return 1 fi # Check if already patched (look for our signature in draw_splash) if grep -q 'draw_splash)' "$screen_sh" && \ grep -A2 'draw_splash)' "$screen_sh" | grep -q 'helixscreen_active'; then log_info "ForgeX screen.sh already has screen drawing patches" return 0 fi log_info "Patching ForgeX screen.sh to skip drawing when HelixScreen active..." # Patch draw_loading, draw_splash, and boot_message cases # Add helixscreen_active check after each case label tmp_file="${screen_sh}.tmp" awk ' /^[[:space:]]*(draw_loading|draw_splash|boot_message)\)/ { print print " # Skip when HelixScreen is controlling display" print " if [ -f /tmp/helixscreen_active ]; then" print " exit 0" print " fi" next } { print } ' "$screen_sh" > "$tmp_file" if [ -s "$tmp_file" ] && grep -q 'helixscreen_active' "$tmp_file" 2>/dev/null; then $SUDO mv "$tmp_file" "$screen_sh" $SUDO chmod +x "$screen_sh" log_success "ForgeX screen.sh patched for screen drawing" return 0 else rm -f "$tmp_file" log_warn "Failed to patch ForgeX screen.sh for screen drawing" return 1 fi } # Remove screen drawing patches from ForgeX screen.sh (for uninstall) unpatch_forgex_screen_drawing() { screen_sh="/opt/config/mod/.shell/screen.sh" if [ ! -f "$screen_sh" ]; then return 1 fi # Check if our drawing patches are present if ! grep -q '# Skip when HelixScreen is controlling display' "$screen_sh" 2>/dev/null; then log_info "ForgeX screen.sh has no drawing patches, nothing to remove" return 0 fi log_info "Removing HelixScreen drawing patches from ForgeX screen.sh..." # Remove our 4-line block: comment + if + exit 0 + fi tmp_file="${screen_sh}.tmp" awk ' /# Skip when HelixScreen is controlling display/ { skip=1; next } skip && /if \[ -f \/tmp\/helixscreen_active \]; then/ { next } skip && /^[[:space:]]*exit 0[[:space:]]*$/ { next } skip && /^[[:space:]]*fi[[:space:]]*$/ { skip=0; next } { print } ' "$screen_sh" > "$tmp_file" if [ -s "$tmp_file" ]; then $SUDO mv "$tmp_file" "$screen_sh" $SUDO chmod +x "$screen_sh" else rm -f "$tmp_file" log_warn "Failed to unpatch ForgeX screen.sh drawing patches" return 1 fi # Verify removal if grep -q '# Skip when HelixScreen is controlling display' "$screen_sh" 2>/dev/null; then log_warn "Could not fully remove drawing patches from screen.sh" return 1 fi log_success "ForgeX screen.sh drawing patches removed" return 0 } # Install logged wrapper to prevent direct framebuffer writes during boot # ForgeX's 'logged' binary writes directly to /dev/fb0 when --send-to-screen is used, # bypassing our screen.sh patches. This wrapper strips that flag when HelixScreen is active. install_forgex_logged_wrapper() { logged_bin="/opt/config/mod/.bin/exec/logged" logged_real="/opt/config/mod/.bin/exec/logged-real" logged_wrapper="/opt/config/mod/.bin/exec/logged-wrapper" if [ ! -f "$logged_bin" ]; then log_info "ForgeX logged binary not found, skipping wrapper" return 1 fi # Check if already wrapped if [ -L "$logged_bin" ] && [ -f "$logged_real" ]; then log_info "ForgeX logged wrapper already installed" return 0 fi # Don't wrap if it's already a symlink to something else if [ -L "$logged_bin" ]; then log_warn "ForgeX logged is already a symlink, skipping wrapper" return 1 fi log_info "Installing ForgeX logged wrapper..." # Create the wrapper script cat > "$logged_wrapper" << 'WRAPPER_EOF' # Wrapper for logged that strips --send-to-screen when HelixScreen is active # The logged binary writes directly to /dev/fb0, bypassing screen.sh patches if [ -f /tmp/helixscreen_active ]; then # Remove --send-to-screen and related args, pass everything else through # Build a clean argument list preserving quoting skip_next=0 set -- for arg in "$@"; do if [ $skip_next -eq 1 ]; then skip_next=0 continue fi case "$arg" in --send-to-screen) continue ;; --screen-no-followup) continue ;; --screen-level) skip_next=1; continue ;; --screen-queue) skip_next=1; continue ;; *) set -- "$@" "$arg" ;; esac done exec /opt/config/mod/.bin/exec/logged-real "$@" else exec /opt/config/mod/.bin/exec/logged-real "$@" fi WRAPPER_EOF $SUDO chmod +x "$logged_wrapper" # Move original to logged-real and symlink logged to wrapper $SUDO mv "$logged_bin" "$logged_real" $SUDO ln -s "$logged_wrapper" "$logged_bin" if [ -L "$logged_bin" ] && [ -f "$logged_real" ]; then log_success "ForgeX logged wrapper installed" return 0 else # Rollback on failure [ -f "$logged_real" ] && $SUDO mv "$logged_real" "$logged_bin" rm -f "$logged_wrapper" log_warn "Failed to install ForgeX logged wrapper" return 1 fi } # Remove logged wrapper (for uninstall) uninstall_forgex_logged_wrapper() { logged_bin="/opt/config/mod/.bin/exec/logged" logged_real="/opt/config/mod/.bin/exec/logged-real" logged_wrapper="/opt/config/mod/.bin/exec/logged-wrapper" if [ ! -f "$logged_real" ]; then return 0 # Not installed fi log_info "Removing ForgeX logged wrapper..." $SUDO rm -f "$logged_bin" $SUDO mv "$logged_real" "$logged_bin" $SUDO rm -f "$logged_wrapper" log_success "ForgeX logged wrapper removed" return 0 } # Uninstall ForgeX-specific configuration (for uninstall) # Restores display mode, stock UI, screen.sh, GuppyScreen/tslib init scripts, # and cleans up backup files from manual patches. # Note: Sets caller's `restored_ui` variable via dynamic scoping. uninstall_forgex() { # Restore ForgeX display mode to GUPPY (from HEADLESS or STOCK) if [ -f "/opt/config/mod_data/variables.cfg" ]; then if grep -q "display[[:space:]]*=[[:space:]]*'HEADLESS'" "/opt/config/mod_data/variables.cfg"; then log_info "Restoring ForgeX display mode to GUPPY..." $SUDO sed -i "s/display[[:space:]]*=[[:space:]]*'HEADLESS'/display = 'GUPPY'/" "/opt/config/mod_data/variables.cfg" elif grep -q "display[[:space:]]*=[[:space:]]*'STOCK'" "/opt/config/mod_data/variables.cfg"; then log_info "Restoring ForgeX display mode to GUPPY..." $SUDO sed -i "s/display[[:space:]]*=[[:space:]]*'STOCK'/display = 'GUPPY'/" "/opt/config/mod_data/variables.cfg" fi fi # Restore stock FlashForge UI in auto_run.sh restore_stock_firmware_ui || true # Remove HelixScreen patches from screen.sh unpatch_forgex_screen_sh || true unpatch_forgex_screen_drawing || true # Remove logged wrapper uninstall_forgex_logged_wrapper || true # Re-enable GuppyScreen and tslib init scripts if [ -f "/opt/config/mod/.root/S80guppyscreen" ]; then $SUDO chmod +x "/opt/config/mod/.root/S80guppyscreen" 2>/dev/null || true restored_ui="GuppyScreen (/opt/config/mod/.root/S80guppyscreen)" fi if [ -f "/opt/config/mod/.root/S35tslib" ]; then $SUDO chmod +x "/opt/config/mod/.root/S35tslib" 2>/dev/null || true fi # Clean up any leftover backup files from manual patches for backup_file in /opt/config/mod/.shell/*.helix-backup /opt/config/mod/.shell/*.bak; do if [ -f "$backup_file" ] 2>/dev/null; then log_info "Removing leftover backup: $backup_file" $SUDO rm -f "$backup_file" fi done } # ============================================ # Module: competing_uis.sh # ============================================ # # Known competing screen UIs to stop # Includes: GuppyScreen (AD5M/K1), Grumpyscreen (K1/Simple AF), KlipperScreen, FeatherScreen COMPETING_UIS="guppyscreen GuppyScreen grumpyscreen Grumpyscreen KlipperScreen klipperscreen featherscreen FeatherScreen" # Record a disabled service for later re-enablement # Args: $1 = type ("systemd" or "sysv-chmod"), $2 = target (service name or script path) record_disabled_service() { local type="$1" local target="$2" local entry="${type}:${target}" local state_file="${INSTALL_DIR}/config/.disabled_services" # Ensure config directory exists if [ -n "${INSTALL_DIR:-}" ] && [ ! -d "${INSTALL_DIR}/config" ]; then $SUDO mkdir -p "${INSTALL_DIR}/config" fi # Don't duplicate entries if [ -f "$state_file" ] && grep -qF "$entry" "$state_file" 2>/dev/null; then return 0 fi echo "$entry" | $SUDO tee -a "$state_file" >/dev/null } # Stop ForgeX-specific competing UIs (stock FlashForge firmware UI) stop_forgex_competing_uis() { # Stop stock FlashForge firmware UI (AD5M/Adventurer 5M) # ffstartup-arm is the startup manager that launches firmwareExe (the stock Qt UI) if [ -f /opt/PROGRAM/ffstartup-arm ]; then log_info "Stopping stock FlashForge UI..." kill_process_by_name firmwareExe ffstartup-arm || true found_any=true fi } # Stop Klipper Mod-specific competing UIs (Xorg, KlipperScreen) stop_kmod_competing_uis() { # Stop Xorg first (required for framebuffer access) # Xorg takes over /dev/fb0 layer, preventing direct framebuffer rendering if [ -x "/etc/init.d/S40xorg" ]; then log_info "Stopping Xorg (Klipper Mod display server)..." $SUDO /etc/init.d/S40xorg stop 2>/dev/null || true # Disable Xorg init script (non-destructive, reversible) $SUDO chmod -x /etc/init.d/S40xorg 2>/dev/null || true record_disabled_service "sysv-chmod" "/etc/init.d/S40xorg" # Kill any remaining Xorg processes kill_process_by_name Xorg X || true found_any=true fi # Kill python processes running KlipperScreen (common on Klipper Mod) # BusyBox ps doesn't support 'aux', use portable approach # shellcheck disable=SC2009 for pid in $(ps -ef 2>/dev/null | grep -E 'KlipperScreen.*screen\.py' | grep -v grep | awk '{print $2}'); do log_info "Killing KlipperScreen python process (PID $pid)..." $SUDO kill "$pid" 2>/dev/null || true found_any=true done } # Stop competing screen UIs (GuppyScreen, KlipperScreen, Xorg, etc.) # Dispatches platform-specific logic, then runs generic UI stopping stop_competing_uis() { log_info "Checking for competing screen UIs..." found_any=false # Platform-specific competing UI handling case "$AD5M_FIRMWARE" in forge_x) stop_forgex_competing_uis ;; klipper_mod) stop_kmod_competing_uis ;; esac # Handle the specific previous UI if we know it (for clean reversibility) if [ -n "$PREVIOUS_UI_SCRIPT" ] && [ -x "$PREVIOUS_UI_SCRIPT" ] 2>/dev/null; then log_info "Stopping previous UI: $PREVIOUS_UI_SCRIPT" $SUDO "$PREVIOUS_UI_SCRIPT" stop 2>/dev/null || true # Disable by removing execute permission (non-destructive, reversible) $SUDO chmod -x "$PREVIOUS_UI_SCRIPT" 2>/dev/null || true record_disabled_service "sysv-chmod" "$PREVIOUS_UI_SCRIPT" found_any=true fi for ui in $COMPETING_UIS; do # Check systemd services if [ "$INIT_SYSTEM" = "systemd" ]; then if $SUDO systemctl is-active --quiet "$ui" 2>/dev/null; then log_info "Stopping $ui (systemd service)..." $SUDO systemctl stop "$ui" 2>/dev/null || true $SUDO systemctl disable "$ui" 2>/dev/null || true record_disabled_service "systemd" "$ui" found_any=true fi fi # Check SysV init scripts (various locations) for initscript in /etc/init.d/S*${ui}* /etc/init.d/${ui}* /opt/config/mod/.root/S*${ui}*; do # Skip if glob didn't match any files (literal pattern returned) [ -e "$initscript" ] || continue # Skip if this is the PREVIOUS_UI_SCRIPT we already handled if [ "$initscript" = "$PREVIOUS_UI_SCRIPT" ]; then continue fi if [ -x "$initscript" ]; then log_info "Stopping $ui ($initscript)..." $SUDO "$initscript" stop 2>/dev/null || true # Disable by removing execute permission (non-destructive) $SUDO chmod -x "$initscript" 2>/dev/null || true record_disabled_service "sysv-chmod" "$initscript" found_any=true fi done # Kill any remaining processes by name if kill_process_by_name "$ui"; then log_info "Killed remaining $ui processes" found_any=true fi done if [ "$found_any" = true ]; then log_info "Waiting for competing UIs to stop..." sleep 2 else log_info "No competing UIs found" fi } # ============================================ # Module: release.sh # ============================================ # # R2 CDN configuration (overridable via environment) : "${R2_BASE_URL:=https://releases.helixscreen.org}" : "${R2_CHANNEL:=stable}" # Cached manifest from R2 (set by get_latest_version, consumed by download_release) _R2_MANIFEST="" # Fetch a URL to stdout using curl or wget # Returns non-zero if neither is available or fetch fails fetch_url() { local url=$1 if command -v curl >/dev/null 2>&1; then curl -sSL --connect-timeout 10 "$url" 2>/dev/null elif command -v wget >/dev/null 2>&1; then wget -qO- --timeout=10 "$url" 2>/dev/null else return 1 fi } # Download a URL to a file # Returns 0 on success (file exists and is non-empty), non-zero on failure download_file() { local url=$1 dest=$2 if command -v curl >/dev/null 2>&1; then local http_code http_code=$(curl -sSL --connect-timeout 30 -w "%{http_code}" -o "$dest" "$url" 2>/dev/null) || true [ "$http_code" = "200" ] && [ -f "$dest" ] && [ -s "$dest" ] elif command -v wget >/dev/null 2>&1; then wget -q --timeout=30 -O "$dest" "$url" 2>/dev/null && [ -f "$dest" ] && [ -s "$dest" ] else return 1 fi } # Extract "version" value from manifest JSON on stdin # Uses POSIX basic regex only (BusyBox compatible) parse_manifest_version() { sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 } # Extract platform asset URL from manifest JSON on stdin # Greps for the platform-specific filename pattern then extracts the URL # Uses POSIX basic regex only (BusyBox compatible) parse_manifest_platform_url() { local platform=$1 grep "helixscreen-${platform}-" | \ sed -n 's/.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 } # Validate a tarball is a valid gzip archive and not truncated # Args: tarball_path, context (e.g., "Downloaded" or "Local") # Exits on failure validate_tarball() { local tarball=$1 local context=${2:-""} # Verify it's a valid gzip file if ! gunzip -t "$tarball" 2>/dev/null; then log_error "${context}file is not a valid gzip archive." [ -n "$context" ] && log_error "The ${context}may have been corrupted or incomplete." exit 1 fi # Verify file isn't truncated (releases should be >1MB) local size_kb size_kb=$(du -k "$tarball" 2>/dev/null | cut -f1) if [ "${size_kb:-0}" -lt 1024 ]; then log_error "${context}file too small (${size_kb}KB). File may be incomplete." exit 1 fi } # Check if we can download from HTTPS URLs # BusyBox wget on AD5M doesn't support HTTPS check_https_capability() { # curl with SSL support works if command -v curl >/dev/null 2>&1; then # Test if curl can reach HTTPS (quick timeout) if curl -sSL --connect-timeout 5 -o /dev/null "https://github.com" 2>/dev/null; then return 0 fi fi # Check if wget supports HTTPS if command -v wget >/dev/null 2>&1; then # BusyBox wget outputs "not an http or ftp url" for https if wget --help 2>&1 | grep -qi "https"; then return 0 fi # Try a test fetch - BusyBox wget fails immediately on https URLs if wget -q --timeout=5 -O /dev/null "https://github.com" 2>/dev/null; then return 0 fi fi return 1 } # Show manual install instructions when HTTPS download isn't available show_manual_install_instructions() { local platform=$1 local version=${2:-latest} echo "" log_error "==========================================" log_error " HTTPS Download Not Available" log_error "==========================================" echo "" log_error "This system cannot download from HTTPS URLs." log_error "BusyBox wget (common on embedded devices) doesn't support HTTPS." echo "" log_info "To install HelixScreen, download the release on another computer" log_info "and copy it to this device:" echo "" echo " 1. Download the release:" if [ "$version" = "latest" ]; then echo " ${CYAN}https://github.com/${GITHUB_REPO}/releases/latest${NC}" else echo " ${CYAN}https://github.com/${GITHUB_REPO}/releases/tag/${version}${NC}" fi echo "" echo " 2. Download: ${BOLD}helixscreen-${platform}-${version}.tar.gz${NC}" echo "" echo " 3. Copy to this device (note: AD5M needs -O flag):" if [ "$platform" = "ad5m" ]; then # AD5M /tmp is a tiny tmpfs (~54MB), use /data/ instead echo " ${CYAN}scp -O helixscreen-${platform}.tar.gz root@:/data/${NC}" echo "" echo " 4. Run the installer with the local file:" echo " ${CYAN}sh /data/install.sh --local /data/helixscreen-${platform}.tar.gz${NC}" else echo " ${CYAN}scp helixscreen-${platform}.tar.gz root@:/tmp/${NC}" echo "" echo " 4. Run the installer with the local file:" echo " ${CYAN}sh /tmp/install.sh --local /tmp/helixscreen-${platform}.tar.gz${NC}" fi echo "" exit 1 } # Get latest release version from GitHub (with R2 CDN as primary source) # Returns the tag name as-is (e.g., "v0.9.3") # Args: platform (for error message if HTTPS unavailable) get_latest_version() { local platform=${1:-unknown} local version="" # Check HTTPS capability first if ! check_https_capability; then show_manual_install_instructions "$platform" "latest" fi # Try R2 manifest first (faster CDN, no API rate limits) local manifest_url="${R2_BASE_URL}/${R2_CHANNEL}/manifest.json" log_info "Fetching latest version from CDN..." _R2_MANIFEST=$(fetch_url "$manifest_url") || true if [ -n "$_R2_MANIFEST" ]; then version=$(echo "$_R2_MANIFEST" | parse_manifest_version) if [ -n "$version" ]; then # Manifest has bare version (e.g., "0.9.5"), we need the tag (e.g., "v0.9.5") version="v${version}" log_info "Latest version (CDN): ${version}" echo "$version" return 0 fi log_warn "CDN manifest found but version could not be parsed, trying GitHub..." _R2_MANIFEST="" else log_warn "CDN unavailable, trying GitHub..." fi # Fallback: GitHub API local url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest" log_info "Fetching latest version from GitHub..." if command -v curl >/dev/null 2>&1; then # Use basic sed regex (no -E flag) for BusyBox compatibility version=$(curl -sSL --connect-timeout 10 "$url" 2>/dev/null | grep '"tag_name"' | sed 's/.*"\([^"][^"]*\)".*/\1/') elif command -v wget >/dev/null 2>&1; then # Use basic sed regex (no -E flag) for BusyBox compatibility version=$(wget -qO- --timeout=10 "$url" 2>/dev/null | grep '"tag_name"' | sed 's/.*"\([^"][^"]*\)".*/\1/') fi if [ -z "$version" ]; then log_error "Failed to fetch latest version." log_error "Check your network connection and try again." log_error "Tried: $manifest_url" log_error "Tried: $url" exit 1 fi echo "$version" } # Download release tarball (tries R2 CDN first, falls back to GitHub) download_release() { local version=$1 local platform=$2 local filename="helixscreen-${platform}-${version}.tar.gz" local dest="${TMP_DIR}/helixscreen.tar.gz" mkdir -p "$TMP_DIR" CLEANUP_TMP=true # Try R2 CDN first local r2_url="" if [ -n "$_R2_MANIFEST" ]; then # Extract URL from cached manifest r2_url=$(echo "$_R2_MANIFEST" | parse_manifest_platform_url "$platform") fi # Fall back to constructed R2 URL if manifest didn't have it if [ -z "$r2_url" ]; then r2_url="${R2_BASE_URL}/${R2_CHANNEL}/${filename}" fi log_info "Downloading HelixScreen ${version} for ${platform}..." log_info "URL: $r2_url" if download_file "$r2_url" "$dest"; then # Quick validation — make sure it's actually a gzip file if gunzip -t "$dest" 2>/dev/null; then local size size=$(ls -lh "$dest" | awk '{print $5}') log_success "Downloaded ${filename} (${size}) from CDN" return 0 fi log_warn "CDN download corrupt, trying GitHub..." rm -f "$dest" else log_warn "CDN download failed, trying GitHub..." rm -f "$dest" fi # Fallback: GitHub Releases local gh_url="https://github.com/${GITHUB_REPO}/releases/download/${version}/${filename}" log_info "URL: $gh_url" local http_code="" if command -v curl >/dev/null 2>&1; then http_code=$(curl -sSL --connect-timeout 30 -w "%{http_code}" -o "$dest" "$gh_url") elif command -v wget >/dev/null 2>&1; then if wget -q --timeout=30 -O "$dest" "$gh_url"; then http_code="200" else http_code="failed" fi fi if [ ! -f "$dest" ] || [ ! -s "$dest" ]; then log_error "Failed to download release." log_error "Tried: $r2_url" log_error "Tried: $gh_url" if [ -n "$http_code" ] && [ "$http_code" != "200" ]; then log_error "HTTP status: $http_code" fi log_error "" log_error "Possible causes:" log_error " - Version ${version} may not exist for platform ${platform}" log_error " - Network connectivity issues" log_error " - CDN and GitHub may be unavailable" exit 1 fi validate_tarball "$dest" "Downloaded " local size size=$(ls -lh "$dest" | awk '{print $5}') log_success "Downloaded ${filename} (${size}) from GitHub" } # Use a local tarball instead of downloading use_local_tarball() { local src=$1 log_info "Using local tarball: $src" validate_tarball "$src" "Local " # Point TMP_DIR tarball location to the source file directly # This avoids copying large files on space-constrained systems mkdir -p "$TMP_DIR" CLEANUP_TMP=true # Create symlink or use directly based on what the extraction expects # The extract_release function looks for ${TMP_DIR}/helixscreen.tar.gz local dest="${TMP_DIR}/helixscreen.tar.gz" if [ "$src" != "$dest" ]; then # Use symlink if possible, otherwise copy ln -sf "$src" "$dest" 2>/dev/null || cp "$src" "$dest" fi local size size=$(ls -lh "$src" | awk '{print $5}') log_success "Using local tarball (${size})" } # Validate binary architecture matches the current system # Args: binary_path platform # Returns: 0 if valid, 1 if mismatch or error validate_binary_architecture() { local binary=$1 local platform=$2 if [ ! -f "$binary" ]; then log_error "Binary not found: $binary" return 1 fi # Read first 20 bytes of ELF header using dd + hexdump # hexdump -v -e is POSIX and available in BusyBox local header header=$(dd if="$binary" bs=1 count=20 2>/dev/null | hexdump -v -e '1/1 "%02x "' 2>/dev/null) || true if [ -z "$header" ]; then log_error "Cannot read binary header (file may be empty or corrupted)" return 1 fi # Parse space-separated hex bytes into individual values # Header format: "7f 45 4c 46 CC ... XX XX MM MM ..." # Byte 0-3: ELF magic (7f 45 4c 46) # Byte 4: EI_CLASS (01=32-bit, 02=64-bit) # Byte 18-19: e_machine LE (28 00=ARM, b7 00=AARCH64) local magic magic=$(echo "$header" | awk '{printf "%s%s%s%s", $1, $2, $3, $4}') if [ "$magic" != "7f454c46" ]; then log_error "Binary is not a valid ELF file" return 1 fi local elf_class elf_class=$(echo "$header" | awk '{print $5}') local machine_lo machine_hi machine_lo=$(echo "$header" | awk '{print $19}') machine_hi=$(echo "$header" | awk '{print $20}') # Determine expected values based on platform local expected_class expected_machine_lo expected_desc case "$platform" in ad5m|k1|pi32) expected_class="01" expected_machine_lo="28" expected_desc="ARM 32-bit (armv7l)" ;; pi) expected_class="02" expected_machine_lo="b7" expected_desc="AARCH64 64-bit" ;; *) log_warn "Unknown platform '$platform', skipping architecture validation" return 0 ;; esac local actual_desc if [ "$elf_class" = "01" ] && [ "$machine_lo" = "28" ]; then actual_desc="ARM 32-bit (armv7l)" elif [ "$elf_class" = "02" ] && [ "$machine_lo" = "b7" ]; then actual_desc="AARCH64 64-bit" else actual_desc="unknown (class=$elf_class, machine=$machine_lo)" fi if [ "$elf_class" != "$expected_class" ] || [ "$machine_lo" != "$expected_machine_lo" ]; then log_error "Architecture mismatch!" log_error " Expected: $expected_desc (for platform '$platform')" log_error " Got: $actual_desc" log_error " This binary was built for the wrong platform." return 1 fi log_info "Architecture validated: $actual_desc" return 0 } # Extract tarball with atomic swap and rollback protection extract_release() { local platform=$1 local tarball="${TMP_DIR}/helixscreen.tar.gz" local extract_dir="${TMP_DIR}/extract" local new_install="${extract_dir}/helixscreen" log_info "Extracting release..." # Phase 1: Extract to temporary directory mkdir -p "$extract_dir" cd "$extract_dir" || exit 1 if [ "$platform" = "ad5m" ] || [ "$platform" = "k1" ]; then # BusyBox tar doesn't support -z if ! gunzip -c "$tarball" | $SUDO tar xf -; then log_error "Failed to extract tarball." log_error "The archive may be corrupted." rm -rf "$extract_dir" exit 1 fi else if ! $SUDO tar -xzf "$tarball"; then log_error "Failed to extract tarball." log_error "The archive may be corrupted." rm -rf "$extract_dir" exit 1 fi fi # Phase 2: Validate extracted content if [ ! -f "${new_install}/bin/helix-screen" ]; then log_error "Extraction failed - helix-screen binary not found." log_error "Expected: helixscreen/bin/helix-screen in tarball" rm -rf "$extract_dir" exit 1 fi # Phase 3: Validate architecture if ! validate_binary_architecture "${new_install}/bin/helix-screen" "$platform"; then log_error "Aborting installation due to architecture mismatch." rm -rf "$extract_dir" exit 1 fi # Phase 4: Backup existing installation (if present) if [ -d "${INSTALL_DIR}" ]; then ORIGINAL_INSTALL_EXISTS=true # Backup config (check new location first, then legacy) if [ -f "${INSTALL_DIR}/config/helixconfig.json" ]; then BACKUP_CONFIG="${TMP_DIR}/helixconfig.json.backup" cp "${INSTALL_DIR}/config/helixconfig.json" "$BACKUP_CONFIG" log_info "Backed up existing configuration (from config/)" elif [ -f "${INSTALL_DIR}/helixconfig.json" ]; then BACKUP_CONFIG="${TMP_DIR}/helixconfig.json.backup" cp "${INSTALL_DIR}/helixconfig.json" "$BACKUP_CONFIG" log_info "Backed up existing configuration (legacy location)" fi # Atomic swap: move old install to .old backup if ! $SUDO mv "${INSTALL_DIR}" "${INSTALL_DIR}.old"; then log_error "Failed to backup existing installation." rm -rf "$extract_dir" exit 1 fi fi # Phase 5: Move new install into place $SUDO mkdir -p "$(dirname "${INSTALL_DIR}")" if ! $SUDO mv "${new_install}" "${INSTALL_DIR}"; then log_error "Failed to install new release." # ROLLBACK: restore old installation if [ -d "${INSTALL_DIR}.old" ]; then log_warn "Rolling back to previous installation..." if $SUDO mv "${INSTALL_DIR}.old" "${INSTALL_DIR}"; then log_warn "Rollback complete. Previous installation restored." else log_error "CRITICAL: Rollback failed! Previous install at ${INSTALL_DIR}.old" log_error "Manually restore with: mv ${INSTALL_DIR}.old ${INSTALL_DIR}" fi fi rm -rf "$extract_dir" exit 1 fi # Phase 6: Restore config if [ -n "${BACKUP_CONFIG:-}" ] && [ -f "$BACKUP_CONFIG" ]; then $SUDO mkdir -p "${INSTALL_DIR}/config" $SUDO cp "$BACKUP_CONFIG" "${INSTALL_DIR}/config/helixconfig.json" log_info "Restored existing configuration to config/" fi # Cleanup rm -rf "$extract_dir" log_success "Extracted to ${INSTALL_DIR}" } # Remove backup of previous installation (call after service starts successfully) cleanup_old_install() { if [ -d "${INSTALL_DIR}.old" ]; then $SUDO rm -rf "${INSTALL_DIR}.old" log_info "Cleaned up previous installation backup" fi } # ============================================ # Module: service.sh # ============================================ # # SERVICE_NAME is defined in common.sh # Install service (dispatcher) # Calls install_service_systemd or install_service_sysv based on INIT_SYSTEM install_service() { local platform=$1 if [ "$INIT_SYSTEM" = "systemd" ]; then install_service_systemd else install_service_sysv fi } # Install systemd service install_service_systemd() { log_info "Installing systemd service..." local service_src="${INSTALL_DIR}/config/helixscreen.service" local service_dest="/etc/systemd/system/${SERVICE_NAME}.service" if [ ! -f "$service_src" ]; then log_error "Service file not found: $service_src" log_error "The release package may be incomplete." exit 1 fi $SUDO cp "$service_src" "$service_dest" # Template placeholders (match SysV pattern in install_service_sysv) local helix_user="${KLIPPER_USER:-root}" local helix_group="${KLIPPER_USER:-root}" local install_dir="${INSTALL_DIR:-/opt/helixscreen}" $SUDO sed -i "s|@@HELIX_USER@@|${helix_user}|g" "$service_dest" 2>/dev/null || \ $SUDO sed -i '' "s|@@HELIX_USER@@|${helix_user}|g" "$service_dest" 2>/dev/null || true $SUDO sed -i "s|@@HELIX_GROUP@@|${helix_group}|g" "$service_dest" 2>/dev/null || \ $SUDO sed -i '' "s|@@HELIX_GROUP@@|${helix_group}|g" "$service_dest" 2>/dev/null || true $SUDO sed -i "s|@@INSTALL_DIR@@|${install_dir}|g" "$service_dest" 2>/dev/null || \ $SUDO sed -i '' "s|@@INSTALL_DIR@@|${install_dir}|g" "$service_dest" 2>/dev/null || true # Filter SupplementaryGroups to only groups that exist on this system. # The template lists the ideal set (video input render) but not all systems # have every group (e.g. 'render' is missing on older Pi OS installs). local desired_groups existing_groups="" desired_groups=$(grep '^SupplementaryGroups=' "$service_dest" 2>/dev/null | sed 's/^SupplementaryGroups=//') if [ -n "$desired_groups" ]; then for grp in $desired_groups; do if getent group "$grp" >/dev/null 2>&1; then existing_groups="${existing_groups:+$existing_groups }$grp" else log_info "Group '$grp' not found on this system, skipping" fi done if [ -n "$existing_groups" ]; then $SUDO sed -i "s|^SupplementaryGroups=.*|SupplementaryGroups=$existing_groups|" "$service_dest" 2>/dev/null || \ $SUDO sed -i '' "s|^SupplementaryGroups=.*|SupplementaryGroups=$existing_groups|" "$service_dest" 2>/dev/null || true else # No matching groups — remove the directive entirely $SUDO sed -i '/^SupplementaryGroups=/d' "$service_dest" 2>/dev/null || \ $SUDO sed -i '' '/^SupplementaryGroups=/d' "$service_dest" 2>/dev/null || true fi fi if ! $SUDO systemctl daemon-reload; then log_error "Failed to reload systemd daemon." exit 1 fi CLEANUP_SERVICE=true log_success "Installed systemd service" } # Install SysV init script install_service_sysv() { log_info "Installing SysV init script..." local init_src="${INSTALL_DIR}/config/helixscreen.init" if [ ! -f "$init_src" ]; then log_error "Init script not found: $init_src" log_error "The release package may be incomplete." exit 1 fi # Use the dynamically set INIT_SCRIPT_DEST (varies by firmware) $SUDO cp "$init_src" "$INIT_SCRIPT_DEST" $SUDO chmod +x "$INIT_SCRIPT_DEST" # Update the DAEMON_DIR in the init script to match the install location # This is important for Klipper Mod which uses a different path $SUDO sed -i "s|DAEMON_DIR=.*|DAEMON_DIR=\"${INSTALL_DIR}\"|" "$INIT_SCRIPT_DEST" 2>/dev/null || \ $SUDO sed -i '' "s|DAEMON_DIR=.*|DAEMON_DIR=\"${INSTALL_DIR}\"|" "$INIT_SCRIPT_DEST" 2>/dev/null || true CLEANUP_SERVICE=true log_success "Installed SysV init script at $INIT_SCRIPT_DEST" } # Start service (dispatcher) # Calls start_service_systemd or start_service_sysv based on INIT_SYSTEM start_service() { if [ "$INIT_SYSTEM" = "systemd" ]; then start_service_systemd else start_service_sysv fi } # Start service (systemd) start_service_systemd() { log_info "Enabling and starting HelixScreen (systemd)..." if ! $SUDO systemctl enable "$SERVICE_NAME"; then log_error "Failed to enable ${SERVICE_NAME} service." exit 1 fi if ! $SUDO systemctl start "$SERVICE_NAME"; then log_error "Failed to start ${SERVICE_NAME} service." log_error "Check logs with: journalctl -u ${SERVICE_NAME} -n 50" exit 1 fi # Wait for service to start (may be slow on embedded hardware) local i for i in 1 2 3 4 5; do sleep 1 if $SUDO systemctl is-active --quiet "$SERVICE_NAME"; then log_success "HelixScreen is running!" return fi done log_warn "Service may still be starting..." log_warn "Check status with: systemctl status $SERVICE_NAME" } # Start service (SysV init) start_service_sysv() { log_info "Starting HelixScreen (SysV init)..." if [ ! -x "$INIT_SCRIPT_DEST" ]; then log_error "Init script not executable: $INIT_SCRIPT_DEST" exit 1 fi if ! $SUDO "$INIT_SCRIPT_DEST" start; then log_error "Failed to start HelixScreen." log_error "Check logs in: /tmp/helixscreen.log" exit 1 fi # Wait for service to start (may be slow on embedded hardware) local i for i in 1 2 3 4 5; do sleep 1 if $SUDO "$INIT_SCRIPT_DEST" status >/dev/null 2>&1; then log_success "HelixScreen is running!" return fi done log_warn "Service may still be starting..." log_warn "Check: $INIT_SCRIPT_DEST status" } # Deploy platform-specific hook file # Copies the correct hook file to $INSTALL_DIR/platform/hooks.sh so the # init script can source it at runtime. deploy_platform_hooks() { local install_dir="$1" local platform="$2" # "ad5m-forgex", "ad5m-kmod", "pi", "k1" local hooks_src="${install_dir}/config/platform/hooks-${platform}.sh" if [ ! -f "$hooks_src" ]; then log_warn "No platform hooks for: $platform" return 0 fi $SUDO mkdir -p "${install_dir}/platform" $SUDO cp "$hooks_src" "${install_dir}/platform/hooks.sh" $SUDO chmod +x "${install_dir}/platform/hooks.sh" log_info "Deployed platform hooks: $platform" } # Fix ownership of config directory for non-root Klipper users # Binaries stay root-owned for security; only config needs user write access fix_install_ownership() { local user="${KLIPPER_USER:-}" if [ -n "$user" ] && [ "$user" != "root" ] && [ -d "$INSTALL_DIR" ]; then log_info "Setting ownership to ${user}..." if [ -d "${INSTALL_DIR}/config" ]; then $SUDO chown -R "${user}:${user}" "${INSTALL_DIR}/config" fi fi } # Stop service for update stop_service() { if [ "$INIT_SYSTEM" = "systemd" ]; then if $SUDO systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then log_info "Stopping existing HelixScreen service (systemd)..." $SUDO systemctl stop "$SERVICE_NAME" || true fi else # Try the configured init script location first if [ -n "$INIT_SCRIPT_DEST" ] && [ -x "$INIT_SCRIPT_DEST" ]; then log_info "Stopping existing HelixScreen service (SysV)..." $SUDO "$INIT_SCRIPT_DEST" stop 2>/dev/null || true fi # Also check all possible locations (for updates/uninstalls) for init_script in $HELIX_INIT_SCRIPTS; do if [ -x "$init_script" ]; then log_info "Stopping HelixScreen at $init_script..." $SUDO "$init_script" stop 2>/dev/null || true fi done # Also try to kill by name (watchdog first to prevent crash dialog flash) # shellcheck disable=SC2086 kill_process_by_name $HELIX_PROCESSES fi } # ============================================ # Module: moonraker.sh # ============================================ # # Common moonraker.conf locations MOONRAKER_CONF_PATHS=" /home/pi/printer_data/config/moonraker.conf /home/biqu/printer_data/config/moonraker.conf /home/mks/printer_data/config/moonraker.conf /root/printer_data/config/moonraker.conf /opt/config/moonraker.conf /usr/data/printer_data/config/moonraker.conf " # Find moonraker.conf # Returns: path to moonraker.conf or empty string find_moonraker_conf() { # Dynamic: check detected user's home first if [ -n "${KLIPPER_HOME:-}" ]; then local user_conf="${KLIPPER_HOME}/printer_data/config/moonraker.conf" if [ -f "$user_conf" ]; then echo "$user_conf" return 0 fi fi # Static fallback for conf in $MOONRAKER_CONF_PATHS; do if [ -f "$conf" ]; then echo "$conf" return 0 fi done echo "" } # Check if update_manager section for helixscreen already exists # Args: $1 = moonraker.conf path # Returns: 0 if exists, 1 if not has_update_manager_section() { local conf="$1" grep -q '^\[update_manager helixscreen\]' "$conf" 2>/dev/null } # Generate update_manager configuration block generate_update_manager_config() { cat << EOF # HelixScreen Update Manager # Added by HelixScreen installer - enables one-click updates from Mainsail/Fluidd [update_manager helixscreen] type: zip channel: stable repo: prestonbrown/helixscreen path: ${INSTALL_DIR} managed_services: helixscreen persistent_files: config/helixconfig.json config/.disabled_services EOF } # Add update_manager section to moonraker.conf # Args: $1 = moonraker.conf path add_update_manager_section() { local conf="$1" # Create backup $SUDO cp "$conf" "${conf}.bak.helixscreen" 2>/dev/null || true # Append configuration generate_update_manager_config | $SUDO tee -a "$conf" >/dev/null log_success "Added update_manager section to $conf" log_info "You can now update HelixScreen from the Mainsail/Fluidd web interface!" } # Check if moonraker.conf has old git_repo-style helixscreen section # Args: $1 = moonraker.conf path # Returns: 0 if old git_repo section found, 1 if not has_old_git_repo_section() { local conf="$1" # Look for helixscreen section with type: git_repo if grep -q '^\[update_manager helixscreen\]' "$conf" 2>/dev/null; then # Extract the section and check for git_repo type awk '/^\[update_manager helixscreen\]/{found=1; next} found && /^\[/{exit} found && /^type:/{print; exit}' "$conf" | grep -q 'git_repo' return $? fi return 1 } # Migrate old git_repo section to zip # Args: $1 = moonraker.conf path migrate_git_repo_to_zip() { local conf="$1" log_info "Migrating update_manager from git_repo to zip..." # Remove old section remove_update_manager_section "$conf" 2>/dev/null || true # Add new zip section add_update_manager_section "$conf" # Clean up old sparse clone directory if it exists local old_repo_dir="${INSTALL_DIR}-repo" if [ -d "$old_repo_dir" ]; then log_info "Removing old updater repo at $old_repo_dir..." $SUDO rm -rf "$old_repo_dir" fi log_success "Migrated to type: zip update manager" } # Write release_info.json if not already present # Moonraker type:zip needs this file to detect installed version write_release_info() { local release_info="${INSTALL_DIR}/release_info.json" if [ -f "$release_info" ]; then return 0 fi # Try to detect version from binary local version="" if [ -x "${INSTALL_DIR}/bin/helix-screen" ]; then version=$("${INSTALL_DIR}/bin/helix-screen" --version 2>/dev/null | head -1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+[^ ]*' || echo "") fi if [ -z "$version" ]; then log_warn "Could not detect version for release_info.json" return 0 fi # Determine platform-specific asset name local asset_name="helixscreen-pi.zip" case "${PLATFORM:-}" in pi32) asset_name="helixscreen-pi32.zip" ;; ad5m) asset_name="helixscreen-ad5m.zip" ;; k1) asset_name="helixscreen-k1.zip" ;; k1-dynamic) asset_name="helixscreen-k1-dynamic.zip" ;; k2) asset_name="helixscreen-k2.zip" ;; esac log_info "Writing release_info.json (${version})..." cat > "${release_info}.tmp" << EOF {"project_name":"helixscreen","project_owner":"prestonbrown","version":"${version}","asset_name":"${asset_name}"} EOF $SUDO mv "${release_info}.tmp" "$release_info" } # Restart Moonraker to pick up configuration changes restart_moonraker() { if command -v systemctl >/dev/null 2>&1 && $SUDO systemctl is-active --quiet moonraker 2>/dev/null; then log_info "Restarting Moonraker to apply configuration..." $SUDO systemctl restart moonraker || true elif [ -x "/etc/init.d/S56moonraker_service" ]; then # K1/Simple AF uses SysV init log_info "Restarting Moonraker to apply configuration..." if ! $SUDO /etc/init.d/S56moonraker_service restart 2>/dev/null; then log_warn "Could not restart Moonraker - you may need to restart it manually" fi fi } # Configure Moonraker update_manager # Called during installation on platforms with web UI (Pi, K1 with Simple AF) configure_moonraker_updates() { local platform=$1 # Skip on AD5M (typically no Mainsail/Fluidd web UI) if [ "$platform" = "ad5m" ]; then log_info "Skipping Moonraker update_manager on AD5M (typically no web UI)" return 0 fi log_info "Configuring Moonraker update_manager..." # Write release_info.json if not already present (fallback for older tarballs) write_release_info local conf conf=$(find_moonraker_conf) if [ -z "$conf" ]; then log_warn "Could not find moonraker.conf" log_warn "To enable web UI updates, manually add to your moonraker.conf:" echo "" generate_update_manager_config echo "" return 0 fi # Migrate old git_repo config to zip if has_old_git_repo_section "$conf"; then migrate_git_repo_to_zip "$conf" restart_moonraker return 0 fi if has_update_manager_section "$conf"; then log_info "update_manager section already exists in $conf" return 0 fi add_update_manager_section "$conf" restart_moonraker } # Remove update_manager section from moonraker.conf # Called during uninstallation remove_update_manager_section() { local conf conf=$(find_moonraker_conf) if [ -z "$conf" ]; then return 0 fi if ! has_update_manager_section "$conf"; then return 0 fi log_info "Removing update_manager section from $conf..." # Create backup $SUDO cp "$conf" "${conf}.bak.helixscreen-uninstall" 2>/dev/null || true # Remove the section (from [update_manager helixscreen] to next section or EOF) # This uses awk to skip lines between [update_manager helixscreen] and the next [section] # Note: Need to run awk through sudo to handle permission on output file $SUDO sh -c "awk ' /^\[update_manager helixscreen\]/ { skip=1; next } /^\[/ { skip=0 } !skip { print } ' \"$conf\" > \"${conf}.tmp\"" && $SUDO mv "${conf}.tmp" "$conf" # Also remove any "Added by HelixScreen" comment lines that precede it $SUDO sed -i '/# HelixScreen Update Manager/d' "$conf" 2>/dev/null || \ $SUDO sed -i '' '/# HelixScreen Update Manager/d' "$conf" 2>/dev/null || true $SUDO sed -i '/# Added by HelixScreen installer/d' "$conf" 2>/dev/null || \ $SUDO sed -i '' '/# Added by HelixScreen installer/d' "$conf" 2>/dev/null || true log_success "Removed update_manager section from $conf" } # ============================================ # Module: uninstall.sh # ============================================ # # Re-enable services that were disabled during installation # Reads the state file and reverses each recorded disable action reenable_disabled_services() { local state_file="${INSTALL_DIR}/config/.disabled_services" [ -f "$state_file" ] || return 0 log_info "Re-enabling previously disabled services..." while IFS= read -r entry; do # Skip empty lines and comments case "$entry" in ""|\#*) continue ;; esac local type="${entry%%:*}" local target="${entry#*:}" case "$type" in systemd) log_info "Re-enabling systemd service: $target" $SUDO systemctl enable "$target" 2>/dev/null || true ;; sysv-chmod) if [ -f "$target" ]; then log_info "Re-enabling init script: $target" $SUDO chmod +x "$target" 2>/dev/null || true fi ;; esac done < "$state_file" } # Uninstall HelixScreen # Args: platform (optional) uninstall() { local platform=${1:-} log_info "Uninstalling HelixScreen..." # Detect init system first detect_init_system if [ "$INIT_SYSTEM" = "systemd" ]; then # Stop and disable systemd service $SUDO systemctl stop "$SERVICE_NAME" 2>/dev/null || true $SUDO systemctl disable "$SERVICE_NAME" 2>/dev/null || true $SUDO rm -f "/etc/systemd/system/${SERVICE_NAME}.service" $SUDO systemctl daemon-reload else # Stop and remove SysV init scripts (check all possible locations) for init_script in $HELIX_INIT_SCRIPTS; do if [ -f "$init_script" ]; then log_info "Stopping and removing $init_script..." $SUDO "$init_script" stop 2>/dev/null || true $SUDO rm -f "$init_script" fi done fi # Kill any remaining processes (watchdog first to prevent crash dialog flash) # shellcheck disable=SC2086 kill_process_by_name $HELIX_PROCESSES # Clean up PID files and log file $SUDO rm -f /var/run/helixscreen.pid 2>/dev/null || true $SUDO rm -f /var/run/helix-splash.pid 2>/dev/null || true $SUDO rm -f /tmp/helixscreen.log 2>/dev/null || true # Re-enable services from state file (before removing install dir) reenable_disabled_services # Remove installation (check all possible locations) local removed_dir="" for install_dir in $HELIX_INSTALL_DIRS; do if [ -d "$install_dir" ]; then $SUDO rm -rf "$install_dir" log_success "Removed ${install_dir}" removed_dir="$install_dir" # Also remove the updater repo clone if present if [ -d "${install_dir}-repo" ]; then $SUDO rm -rf "${install_dir}-repo" log_success "Removed ${install_dir}-repo" fi fi done if [ -z "$removed_dir" ]; then log_warn "No HelixScreen installation found" fi # Re-enable the previous UI based on firmware log_info "Re-enabling previous screen UI..." local restored_ui="" local restored_xorg="" if [ "$AD5M_FIRMWARE" = "klipper_mod" ] || [ -f "/etc/init.d/S80klipperscreen" ]; then # Klipper Mod - restore Xorg and KlipperScreen if [ -f "/etc/init.d/S40xorg" ]; then $SUDO chmod +x "/etc/init.d/S40xorg" 2>/dev/null || true restored_xorg="Xorg (/etc/init.d/S40xorg)" fi if [ -f "/etc/init.d/S80klipperscreen" ]; then $SUDO chmod +x "/etc/init.d/S80klipperscreen" 2>/dev/null || true restored_ui="KlipperScreen (/etc/init.d/S80klipperscreen)" fi fi # Check for K1/Simple AF GuppyScreen if [ -z "$restored_ui" ] && [ -f "/etc/init.d/S99guppyscreen" ]; then $SUDO chmod +x "/etc/init.d/S99guppyscreen" 2>/dev/null || true restored_ui="GuppyScreen (/etc/init.d/S99guppyscreen)" fi # ForgeX - restore GuppyScreen and stock UI settings if [ -z "$restored_ui" ] && [ "$AD5M_FIRMWARE" = "forge_x" ]; then uninstall_forgex fi # Clean up helixscreen cache directories for cache_dir in /root/.cache/helix /tmp/helix_thumbs /.cache/helix; do if [ -d "$cache_dir" ] 2>/dev/null; then log_info "Removing cache: $cache_dir" $SUDO rm -rf "$cache_dir" fi done # Clean up active flag file $SUDO rm -f /tmp/helixscreen_active 2>/dev/null || true # Clean up macOS resource fork files (created by scp from Mac) for pattern in /opt/._helixscreen /root/._helixscreen; do $SUDO rm -f "$pattern" 2>/dev/null || true done # Remove update_manager section from moonraker.conf (if present) if type remove_update_manager_section >/dev/null 2>&1; then remove_update_manager_section || true fi log_success "HelixScreen uninstalled" if [ -n "$restored_xorg" ]; then log_info "Re-enabled: $restored_xorg" fi if [ -n "$restored_ui" ]; then log_info "Re-enabled: $restored_ui" log_info "Reboot to start the previous UI" else log_info "Note: No previous UI found to restore" fi } # Clean up old installation completely (for --clean flag) # Removes all files, config, and caches without backup # Args: platform clean_old_installation() { local platform=$1 log_warn "==========================================" log_warn " CLEAN INSTALL MODE" log_warn "==========================================" log_warn "" log_warn "This will PERMANENTLY DELETE:" log_warn " - All HelixScreen files in ${INSTALL_DIR}" log_warn " - Your configuration (helixconfig.json)" log_warn " - Thumbnail cache files" log_warn "" # Interactive confirmation if stdin is a terminal if [ -t 0 ]; then printf "Are you sure? [y/N] " read -r response case "$response" in [yY][eE][sS]|[yY]) ;; *) log_info "Clean install cancelled." exit 0 ;; esac fi log_info "Cleaning old installation..." # Stop any running services stop_service # Remove installation directories (check all possible locations) for install_dir in $HELIX_INSTALL_DIRS; do if [ -d "$install_dir" ]; then log_info "Removing $install_dir..." $SUDO rm -rf "$install_dir" fi done # Remove thumbnail caches (POSIX-compatible: no arrays) for cache_pattern in \ "/root/.cache/helix/helix_thumbs" \ "/home/*/.cache/helix/helix_thumbs" \ "/tmp/helix_thumbs" \ "/var/tmp/helix_thumbs" do for cache_dir in $cache_pattern; do if [ -d "$cache_dir" ] 2>/dev/null; then log_info "Removing cache: $cache_dir" $SUDO rm -rf "$cache_dir" fi done done # Remove init scripts (check all possible locations) for init_script in $HELIX_INIT_SCRIPTS; do if [ -f "$init_script" ]; then log_info "Removing init script: $init_script" $SUDO rm -f "$init_script" fi done # Remove systemd service if present if [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then log_info "Removing systemd service..." $SUDO systemctl disable "$SERVICE_NAME" 2>/dev/null || true $SUDO rm -f "/etc/systemd/system/${SERVICE_NAME}.service" $SUDO systemctl daemon-reload 2>/dev/null || true fi log_success "Old installation cleaned" echo "" } # ============================================ # Main orchestration # ============================================ # Set up error trap (ERR is bash-specific, skip on POSIX shells like dash/ash) # shellcheck disable=SC3047 trap 'error_handler $LINENO' ERR 2>/dev/null || true # Print usage usage() { echo "HelixScreen Installer" echo "" echo "Usage: $0 [options]" echo "" echo "Options:" echo " --update Update existing installation (preserves config)" echo " --uninstall Remove HelixScreen" echo " --clean Clean install: remove old installation completely," echo " including config and caches (asks for confirmation)" echo " --version VER Install specific version (default: latest)" echo " --local FILE Install from local tarball (skip download)" echo " --help Show this help message" echo "" echo "Examples:" echo " $0 # Fresh install, latest version" echo " $0 --update # Update existing installation" echo " $0 --clean # Remove old install completely, then install" echo " $0 --version v1.1.0 # Install specific version" echo " $0 --local /tmp/helixscreen-ad5m.tar.gz # Install from local file" } # Configure platform-specific settings before stopping competing UIs # (ForgeX display mode, stock UI disable, screen.sh patching) configure_platform() { case "$AD5M_FIRMWARE" in forge_x) configure_forgex_display || true disable_stock_firmware_ui || true patch_forgex_screen_sh || true patch_forgex_screen_drawing || true install_forgex_logged_wrapper || true ;; klipper_mod) # Klipper Mod-specific install-time configuration (if any) ;; esac } # Deploy platform-specific hooks for the init script # Must be called after extract_release (hooks are in the release package) install_platform_hooks() { local platform_hook="" case "$AD5M_FIRMWARE" in forge_x) platform_hook="ad5m-forgex" ;; klipper_mod) platform_hook="ad5m-kmod" ;; esac # Pi and K1 platform hooks (pi32 shares Pi hooks) if [ "$platform" = "pi" ] || [ "$platform" = "pi32" ]; then platform_hook="pi" elif [ "$platform" = "k1" ]; then platform_hook="k1" fi if [ -n "$platform_hook" ]; then deploy_platform_hooks "$INSTALL_DIR" "$platform_hook" fi } # Main installation flow main() { update_mode=false uninstall_mode=false clean_mode=false version="" local_tarball="" # Parse arguments while [ $# -gt 0 ]; do case $1 in --update) update_mode=true shift ;; --uninstall) uninstall_mode=true shift ;; --clean) clean_mode=true shift ;; --version) if [ -z "${2:-}" ]; then log_error "--version requires a version argument" exit 1 fi version="$2" shift 2 ;; --local) if [ -z "${2:-}" ]; then log_error "--local requires a file path argument" exit 1 fi local_tarball="$2" shift 2 ;; --help|-h) usage exit 0 ;; *) log_error "Unknown option: $1" usage exit 1 ;; esac done echo "" echo "${BOLD}========================================${NC}" echo "${BOLD} HelixScreen Installer${NC}" echo "${BOLD}========================================${NC}" echo "" # Detect platform platform=$(detect_platform) log_info "Detected platform: ${BOLD}${platform}${NC}" if [ "$platform" = "unsupported" ]; then log_error "Unsupported platform: $(uname -m)" log_error "HelixScreen supports:" log_error " - Raspberry Pi (aarch64/armv7l)" log_error " - FlashForge Adventurer 5M (armv7l)" log_error " - Creality K1 series with Simple AF" exit 1 fi # For AD5M/K1, detect firmware variant and set appropriate paths local firmware="" if [ "$platform" = "ad5m" ]; then AD5M_FIRMWARE=$(detect_ad5m_firmware) firmware="$AD5M_FIRMWARE" elif [ "$platform" = "k1" ]; then K1_FIRMWARE=$(detect_k1_firmware) firmware="$K1_FIRMWARE" fi set_install_paths "$platform" "$firmware" # Check permissions check_permissions "$platform" # Handle uninstall (doesn't need all checks) if [ "$uninstall_mode" = true ]; then uninstall "$platform" exit 0 fi # Pre-flight checks log_info "Running pre-flight checks..." check_requirements install_runtime_deps "$platform" check_disk_space "$platform" detect_init_system # Get version (skip if using local tarball) if [ -n "$local_tarball" ]; then # Validate local file exists if [ ! -f "$local_tarball" ]; then log_error "Local tarball not found: $local_tarball" exit 1 fi # Extract version from filename if possible (helixscreen-platform-v1.2.3.tar.gz) version=$(echo "$local_tarball" | sed -n 's/.*helixscreen-[^-]*-\(v[0-9.]*\)\.tar\.gz/\1/p') if [ -z "$version" ]; then version="local" fi log_info "Installing from local file: ${BOLD}${local_tarball}${NC}" else if [ -z "$version" ]; then version=$(get_latest_version "$platform") fi fi log_info "Target version: ${BOLD}${version}${NC}" # Configure platform-specific settings before stopping UIs configure_platform # Stop competing UIs stop_competing_uis # Clean old installation if requested if [ "$clean_mode" = true ]; then clean_old_installation "$platform" fi # Stop existing service if updating if [ "$update_mode" = true ]; then if [ ! -d "$INSTALL_DIR" ]; then log_warn "No existing installation found. Performing fresh install." fi stop_service fi # Download and install (or use local tarball) if [ -n "$local_tarball" ]; then use_local_tarball "$local_tarball" else download_release "$version" "$platform" fi extract_release "$platform" fix_install_ownership install_service "$platform" install_platform_hooks # Configure Moonraker update_manager (Pi only - enables web UI updates) configure_moonraker_updates "$platform" # Start service start_service cleanup_old_install # Cleanup on success cleanup_on_success echo "" echo "${GREEN}${BOLD}========================================${NC}" echo "${GREEN}${BOLD} Installation Complete!${NC}" echo "${GREEN}${BOLD}========================================${NC}" echo "" echo "HelixScreen ${version} installed to ${INSTALL_DIR}" echo "" print_post_install_commands echo "" if [ "$platform" = "ad5m" ] || [ "$platform" = "k1" ]; then echo "Note: You may need to reboot for the display to update." fi } # Run main main "$@"