Skip to content

Installer

Developer guide for the HelixScreen installer infrastructure: modular shell scripts, platform detection, KIAUH integration, Moonraker updater, and the bats test suite.

User-facing install instructions: See the project README for quick-start installation commands.


The installer is a modular POSIX shell system with 10 library modules that get bundled into monolithic scripts for end-user distribution. All shell code targets /bin/sh for maximum compatibility, including BusyBox on embedded platforms (AD5M, K1).

scripts/
install-dev.sh # Development installer (sources modules at runtime)
install.sh # Bundled installer (auto-generated, committed)
uninstall.sh # Bundled uninstaller (auto-generated, committed)
bundle-installer.sh # Generates install.sh from modules
bundle-uninstaller.sh # Generates uninstall.sh from modules
helix-launcher.sh # Runtime launcher with watchdog supervision
lib/installer/
common.sh # Logging, colors, error handler, process killing
platform.sh # Platform/firmware detection, install paths, tmp dir
permissions.sh # Root/sudo checks
requirements.sh # Pre-flight: commands, deps, disk space, init system
forgex.sh # ForgeX-specific: display config, screen.sh patching
competing_uis.sh # Stop GuppyScreen, KlipperScreen, Xorg, stock UI
release.sh # Download from R2 CDN/GitHub, extract, validate arch
service.sh # systemd/SysV service install, platform hooks
moonraker.sh # Moonraker update_manager configuration
uninstall.sh # Uninstall, clean, re-enable previous UIs
kiauh.sh # KIAUH extension auto-detection and install
kiauh/helixscreen/
__init__.py # KIAUH extension constants and install dir detection
helixscreen_extension.py # KIAUH BaseExtension implementation
metadata.json # KIAUH extension metadata (display name, description)

Modules use source guards (_HELIX_*_SOURCED) to prevent double-sourcing. The load order in install-dev.sh matters — uninstall.sh must be last because it uses functions from other modules.

install-dev.shinstall.sh
UsageDevelopment, from repo checkoutEnd-user, via curl | sh
ModulesSources from lib/installer/ at runtimeAll modules inlined by bundle-installer.sh
GuardChecks _HELIX_BUNDLED_INSTALLER is unsetSets _HELIX_BUNDLED_INSTALLER=1
RegenerationN/A./scripts/bundle-installer.sh -o scripts/install.sh

When modifying any module in lib/installer/, you must regenerate the bundled scripts:

Terminal window
./scripts/bundle-installer.sh -o scripts/install.sh
./scripts/bundle-uninstaller.sh -o scripts/uninstall.sh

The bundler uses awk to strip shebangs, SPDX headers, and source guards from each module, then concatenates them with the main orchestration code.


The primary end-user method. Downloads and runs the bundled install.sh:

Terminal window
curl -sSL https://raw.githubusercontent.com/prestonbrown/helixscreen/main/scripts/install.sh | sh

Options:

FlagDescription
--updateUpdate existing installation (preserves config)
--uninstallRemove HelixScreen
--cleanRemove old installation completely, then fresh install
--version VERInstall specific version (e.g., --version v1.1.0)
--local FILEInstall from a local archive (.zip or .tar.gz, skip download)

For devices without HTTPS support (e.g., AD5M with BusyBox wget):

Terminal window
# On your computer: download the release
# On the device:
sh /data/install.sh --local /data/helixscreen-ad5m.zip

The installer auto-detects when HTTPS is unavailable and prints manual download instructions.

See the KIAUH Integration section below.

From a repo checkout, the modular installer sources modules directly:

Terminal window
./scripts/install-dev.sh
./scripts/install-dev.sh --update
./scripts/install-dev.sh --uninstall

The main() function orchestrates this sequence:

  1. Platform detectiondetect_platform() returns ad5m, k1, pi, pi32, or unsupported
  2. Firmware detection — AD5M: klipper_mod or forge_x; K1: simple_af or stock_klipper
  3. Path configurationset_install_paths() sets INSTALL_DIR, INIT_SCRIPT_DEST, PREVIOUS_UI_SCRIPT, TMP_DIR
  4. Permission check — Root required on AD5M/K1; sudo on Pi
  5. Pre-flight checks — Required commands, runtime deps (libdrm2/libinput10 on Pi), disk space, init system detection
  6. Klipper ecosystem check — Verifies Klipper/Moonraker running (AD5M/K1 only, warns if missing)
  7. Platform configuration — ForgeX: display mode, screen.sh patching, logged wrapper
  8. Stop competing UIs — GuppyScreen, KlipperScreen, Xorg, stock FlashForge UI
  9. Download release — R2 CDN primary (releases.helixscreen.org), GitHub Releases fallback
  10. Extract with atomic swap — Validates ELF architecture, backs up config, mv old to .old, rollback on failure
  11. Platform hooks — Deploys hooks-{platform}.sh to $INSTALL_DIR/platform/hooks.sh
  12. Install service — systemd unit or SysV init script (templated with @@HELIX_USER@@, etc.)
  13. Moonraker integration — Adds [update_manager helixscreen] section, writes release_info.json
  14. KIAUH extension — Auto-installs if KIAUH detected. Note: The kiauh.sh module exists but is not currently integrated into the installer flow. KIAUH extension files are installed manually or via the KIAUH UI.
  15. Install-time printer detection — Tier-1 model fingerprint, falling back to Tier-2 Moonraker detection with a B/C confidence gate. Seeds device defaults (and, when confident, a full preset) into settings.json before first launch. See Install-Time Printer Detection below.
  16. Config symlinkprinter_data/config/helixscreen symlink for Mainsail/Fluidd access
  17. Start service — Waits up to 5 seconds for startup confirmation
  18. Cleanup — Remove temp files, remove .old backup

After the release is in place but before the service starts, the installer tries to recognize the printer and pre-seed settings.json so the first launch lands on (or near) the right configuration without the user driving the whole wizard. The logic lives in scripts/lib/installer/printer_seed.sh, orchestrated from main.sh:

Terminal window
seed_pid=$(detect_printer_model) # Tier-1
if [ -n "$seed_pid" ]; then
seed_settings_for_printer "$seed_pid" # device-level seed
install_klipper_include_for_printer "$seed_pid"
else
seed_from_moonraker_detection || true # Tier-2
fi

It runs in two tiers, Tier-1 first and Tier-2 only as a fallback:

Tier-1 — model fingerprint (detect_printer_model). A filesystem-based binary fingerprint: it looks for a stock-firmware artifact at a known path that uniquely identifies a model. Currently the only fingerprint shipped is the Sovol SV06 Ace, keyed on the presence of the stock mksclient binary (e.g. /home/sovol/printer_data/build/mksclient). On a match it seeds the printer’s device-level blocks (display/input) directly and installs any Klipper include for that printer. This tier is intentionally narrow — it only fires for signals strong enough to avoid false positives — so most installs fall through to Tier-2.

Tier-2 — Moonraker detection (seed_from_moonraker_detection). When Tier-1 finds nothing, the installer shells out to the freshly-installed binary:

Terminal window
helix-screen --detect-printer --host 127.0.0.1 --port 7125

That one-shot queries the local Moonraker over REST and prints a JSON verdict (see --detect-printer in DEVELOPMENT.md for the exact shape). Tier-2 parses preset, confidence, and runner_up_confidence from that verdict. It is a no-op (returns success without seeding) when Moonraker is unreachable, the verdict carries no preset, or the JSON is malformed.

Tier-2 decides what to seed using two numeric thresholds (overridable via environment):

VariableDefaultMeaning
HELIX_DETECT_MIN_CONFIDENCE85Minimum top-match confidence to auto-apply a full preset
HELIX_DETECT_MIN_MARGIN10Minimum lead over the runner-up (confidence - runner_up_confidence)
Terminal window
margin=$(( conf - rconf ))
if [ "$conf" -ge "$HELIX_DETECT_MIN_CONFIDENCE" ] && [ "$margin" -ge "$HELIX_DETECT_MIN_MARGIN" ]; then
# Detection B -> full preset
seed_full_preset_for_printer "$preset"
else
# Detection C -> device-level seed + localhost host
seed_settings_for_printer "$preset"
_seed_moonraker_host_localhost
fi

Note these are not lettered confidence levels (there is no A/D). Confidence is a continuous 0-100 score; “B” and “C” are simply the two seeding paths the gate selects:

  • Path B (confident: confidence >= 85 AND margin >= 10). Auto-apply the full preset via seed_full_preset_for_printer. This writes the preset’s display block, merges its printer block (heaters, fans, LEDs, filament sensors) into printers["default"], sets the top-level "preset" marker so the app knows a preset is already applied, and sets printers["default"]["wizard_completed"] = false so the wizard still runs once for the user to verify rather than silently trusting the seed.

  • Path C (ambiguous: confidence < 85 OR margin < 10). Seed only the safe device-level blocks (input, display) via seed_settings_for_printer, then pre-fill printers["default"]["moonraker_host"] = "127.0.0.1" so the app can reach Moonraker on first launch. Crucially it does not write the "preset" marker — the app re-runs its own detection at startup and asks the user to confirm, rather than committing to an uncertain guess.

  • Skip (no-op). Moonraker unreachable, no preset in the verdict, or malformed JSON: nothing is seeded and the installer continues.

Seeded printer ids are recorded to ${INSTALL_DIR}/config/.seeded_settings (idempotently) so uninstall can be seed-aware.

Because Path B’s seed can be wrong on an ambiguous-but-just-over-threshold match, it is recoverable: re-running the app with --wizard (see DEVELOPMENT.md) clears the "preset" marker and host, turning the next launch back into a full wizard.

The installer uses trap 'error_handler $LINENO' ERR to catch failures. On error:

  • Reports the failing line number and exit code
  • Cleans up temp files
  • Restores backed-up configuration if the install was partially complete
  • Prints help resources

The extract_release() function in release.sh implements a safe upgrade path:

  1. Extract archive to a temp directory
  2. Validate the helix-screen binary exists and has correct ELF architecture
  3. Move existing $INSTALL_DIR to $INSTALL_DIR.old
  4. Move extracted content to $INSTALL_DIR
  5. Restore user config from backup
  6. If step 4 fails, automatically roll back from .old

NoNewPrivileges and Self-Update on Pi (systemd)

Section titled “NoNewPrivileges and Self-Update on Pi (systemd)”

When helix-screen performs a self-update (user presses “Check for Updates”, the binary downloads a new archive and spawns install.sh), the installer runs as a subprocess of the helix-screen systemd service.

The Pi systemd unit includes:

[Service]
NoNewPrivileges=true

This is a hardening flag that prevents any process in the service’s cgroup — including child processes — from gaining new privileges. Concretely: sudo is completely non-functional inside install.sh when spawned by helix-screen.

Affected operations and how they are handled:

OperationOld behaviorFixed behavior
fix_install_ownership() — chown files to klipper usersudo chown → fatal exitWarns and continues; not critical for self-update
Remove stale $INSTALL_DIR.oldsudo rm -rfTry plain rm -rf first; fall back to timestamped name (*.old.TIMESTAMP)
Cleanup .old* dirs post-installsudo rm -rfTry plain rm -rf first; warn and skip if blocked

Root-owned .old directory — a common scenario after a manual root-level install leaves behind a root-owned helixscreen.old/. The pi user cannot remove it even with sudo blocked by NoNewPrivileges. The installer detects this and creates a timestamped fallback (helixscreen.old.1234567890). A one-time manual cleanup is needed on the Pi:

Terminal window
# Diagnose: find files not owned by the current user
find ~/helixscreen* ! -user "$(id -un)" 2>/dev/null
# Fix: remove root-owned stale backup
sudo rm -rf ~/helixscreen.old

Design principle: Under NoNewPrivileges, the installer must complete the core swap (mv old → .old, mv new → INSTALL_DIR, restore config) without sudo. Anything that requires sudo must be either non-fatal or deferred to a manual step.


SettingValue
Detection/etc/os-release contains Debian/Raspbian, or /home/pi, /home/biqu, /home/mks exists
32/64-bitgetconf LONG_BIT determines userspace bitness (64-bit kernel with 32-bit userspace is common)
Install dirAuto-detected based on Klipper ecosystem: ~/helixscreen if klipper/moonraker/printer_data found, else /opt/helixscreen
Klipper userDetected via systemd service owner, process table, printer_data scan, or well-known users (biqu, pi, mks)
Init systemsystemd (service template with @@HELIX_USER@@ substitution)
Runtime depslibdrm2, libinput10 installed via apt
Config symlink~/printer_data/config/helixscreen -> $INSTALL_DIR/config for web UI access

FlashForge Adventurer 5M — Forge-X Firmware (ad5m, forge_x)

Section titled “FlashForge Adventurer 5M — Forge-X Firmware (ad5m, forge_x)”
SettingValue
Detectionarmv7l + kernel contains ad5m or 5.4.61
FirmwareForge-X detected by /opt/config/mod/.root directory
Install dir/opt/helixscreen
Init script/etc/init.d/S90helixscreen
Previous UI/opt/config/mod/.root/S80guppyscreen

ForgeX-specific patches (all reversible on uninstall):

  • Display mode: Sets variables.cfg display to GUPPY mode (required for backlight)
  • GuppyScreen disable: chmod -x on /opt/config/mod/.root/S80guppyscreen
  • tslib disable: chmod -x on /opt/config/mod/.root/S35tslib
  • Stock UI disable: Comments out ffstartup-arm in /opt/auto_run.sh
  • screen.sh backlight patch: Blocks non-100 backlight changes when HelixScreen active (allows S99root init cycle)
  • screen.sh drawing patch: Skips draw_splash, draw_loading, boot_message when HelixScreen active
  • logged wrapper: Wraps /opt/config/mod/.bin/exec/logged to strip --send-to-screen flag (prevents direct framebuffer writes)

FlashForge Adventurer 5M — Klipper Mod (ad5m, klipper_mod)

Section titled “FlashForge Adventurer 5M — Klipper Mod (ad5m, klipper_mod)”
SettingValue
FirmwareDetected by /root/printer_software or /mnt/data/.klipper_mod
Install dir/root/printer_software/helixscreen
Init script/etc/init.d/S80helixscreen
Previous UI/etc/init.d/S80klipperscreen
XorgStopped and disabled (S40xorg) since HelixScreen uses fbdev directly

Creality K1 Series — Simple AF (k1, simple_af)

Section titled “Creality K1 Series — Simple AF (k1, simple_af)”
SettingValue
DetectionBuildroot OS + /usr/data + 2+ K1 indicators (pellcorp, printer_data, get_sn_mac.sh, etc.)
Install dir/usr/data/helixscreen
Init script/etc/init.d/S99helixscreen
Previous UI/etc/init.d/S99guppyscreen

K2 support exists in the release_info.json asset naming (helixscreen-k2.zip) but platform detection is not yet implemented in detect_platform().


KIAUH (Klipper Installation And Update Helper) is the standard tool for managing Klipper ecosystem components.

The KIAUH extension lives in scripts/kiauh/helixscreen/ and consists of three files:

metadata.json — Extension metadata for KIAUH’s menu system:

{
"metadata": {
"index": 14,
"module": "helixscreen_extension",
"maintained_by": "prestonbrown",
"display_name": "HelixScreen",
"description": ["Modern touchscreen interface for Klipper..."],
"repo": "https://github.com/prestonbrown/helixscreen",
"updates": true
}
}

__init__.py — Constants and install directory detection:

  • HELIXSCREEN_INSTALLER_URL — URL to the bundled install.sh
  • find_install_dir() — Scans platform-dependent paths for existing installation

helixscreen_extension.pyBaseExtension subclass with three operations:

  • install_extension() — Downloads and runs install.sh
  • update_extension() — Runs install.sh --update
  • remove_extension() — Runs install.sh --uninstall

During installation, install_kiauh_extension() in lib/installer/kiauh.sh:

  1. Calls detect_kiauh_dir() to find ~/kiauh/kiauh/extensions/ or /home/*/kiauh/kiauh/extensions/
  2. If KIAUH is found and extension source files exist in the release package ($INSTALL_DIR/scripts/kiauh/helixscreen/)
  3. Copies __init__.py, helixscreen_extension.py, and metadata.json to the KIAUH extensions directory
  4. Installs by default when KIAUH is detected; --skip-kiauh-registration opts out
  5. On updates, silently updates the extension files

When modifying the extension:

  1. Edit files in scripts/kiauh/helixscreen/
  2. The extension files are included in release archives and auto-updated during --update
  3. Run the KIAUH extension bats tests to verify structural correctness

The metadata top-level key is required (GitHub issue #3 was caused by this being missing). The bats tests validate this structure to prevent regressions.


The installer configures Moonraker to enable one-click updates from Mainsail/Fluidd web UIs.

  1. [update_manager helixscreen] section appended to moonraker.conf:

    [update_manager helixscreen]
    type: web
    channel: stable
    repo: prestonbrown/helixscreen
    path: /home/biqu/helixscreen
    persistent_files:
    config/settings.json
    config/helixscreen.env
    config/.disabled_services
  2. release_info.json written to $INSTALL_DIR/ — Moonraker type:web needs this to detect the installed version

  3. moonraker.asvc — HelixScreen added to Moonraker’s service allowlist so it can restart the service after updates

Moonraker’s type: web updater wipes the install directory (shutil.rmtree) before extracting each update. Config is preserved via three layers:

  1. persistent_files in moonraker.conf — Moonraker backs up listed files before rmtree and restores them after extraction
  2. Rolling backupsConfig::save() maintains backups in /var/lib/helixscreen/ (systemd StateDirectory) and $HOME/.helixscreen/ (fallback). Config::init() auto-restores from these if the config file is missing after an update.
  3. SysV init script — On BusyBox systems (K1, AD5M) where systemd isn’t available, the init script exports HOME=/root and creates /var/lib/helixscreen/ so backup paths are persistent (not volatile /tmp/).

The installer also runs ensure_persistent_files() on every upgrade to add persistent_files to existing Moonraker configs that predate this feature.

The installer detects old type: git_repo and type: zip configurations and auto-migrates them to type: web, cleaning up the sparse clone directory.

The find_moonraker_conf() function searches in this order:

  1. $KLIPPER_HOME/printer_data/config/moonraker.conf (detected user)
  2. Static fallbacks: /home/pi/..., /home/biqu/..., /home/mks/..., /root/..., /opt/config/..., /usr/data/...

Moonraker update_manager is skipped on AD5M (typically no Mainsail/Fluidd web UI).


The uninstaller (scripts/uninstall.sh) reverses the installation:

  1. Stop service — systemd or SysV, plus kill remaining processes (watchdog first to prevent crash dialog)
  2. Remove service — Delete systemd unit or init script
  3. Re-enable disabled services — Reads config/.disabled_services state file and re-enables each recorded entry
  4. Remove installation — Checks all known paths: /opt/helixscreen, /root/printer_software/helixscreen, /usr/data/helixscreen
  5. Restore previous UI — Platform-specific:
    • Klipper Mod: Re-enable Xorg and KlipperScreen
    • K1: Re-enable GuppyScreen
    • ForgeX: Full cleanup via uninstall_forgex() (restore display mode, unpatch screen.sh, remove logged wrapper, re-enable GuppyScreen/tslib)
  6. Remove caches — Thumbnail caches, temp files, PID files, log files
  7. Remove Moonraker section — Strips [update_manager helixscreen] from moonraker.conf

The installer tracks what it disabled in $INSTALL_DIR/config/.disabled_services:

systemd:KlipperScreen
sysv-chmod:/etc/init.d/S80klipperscreen
sysv-chmod:/etc/init.d/S40xorg

The uninstaller reads this file and reverses each action (systemd enable, chmod +x). This is listed in persistent_files in the Moonraker config so it survives zip updates.


Downloads go through releases.helixscreen.org (Cloudflare R2 bucket):

  1. Fetch stable/manifest.json for latest version and per-platform download URLs
  2. Download the platform-specific archive from R2

If R2 is unavailable or returns a corrupt file:

  1. Query api.github.com/repos/.../releases/latest for the tag name
  2. Download from github.com/.../releases/download/{version}/{filename}

On embedded platforms (AD5M), BusyBox wget does not support HTTPS. The installer:

  1. Tests curl HTTPS, then wget HTTPS
  2. If neither works, prints step-by-step manual install instructions with scp commands

After extraction, validate_binary_architecture() reads the ELF header (first 20 bytes) to verify:

  • ELF magic bytes
  • ELF class (32-bit vs 64-bit)
  • Machine type (ARM vs AARCH64)

This prevents installing a Pi binary on AD5M or vice versa.


The installer has 543 test cases across 30 bats files (~6700 lines of test code), making it one of the most thoroughly tested shell installer systems for 3D printer firmware.

Terminal window
# Run all shell tests
bats tests/shell/
# Run a specific test file
bats tests/shell/test_platform_detection.bats
# Run with verbose output
bats --verbose-run tests/shell/test_platform_detection.bats
Test FileCoverage
test_platform_detection.batsPi 32/64-bit detection, AD5M/K1 identification
test_platform_hooks.batsPlatform hook deployment
test_pi_install_path.batsPi install directory auto-detection cascade
test_user_detection.batsKlipper user detection (systemd, process, printer_data, well-known)
test_forgex_boot.batsForgeX boot patches, screen.sh, logged wrapper
test_arch_validation.batsELF header parsing, architecture mismatch detection
test_download_validation.batsArchive validation, HTTPS capability
test_r2_installer.batsR2 CDN manifest parsing, fallback to GitHub
test_extract_release.batsExtraction, atomic swap, rollback
test_release_packaging.batsRelease archive structure
test_service_install.batssystemd/SysV service installation
test_service_template.batsService template placeholder substitution
test_moonraker_config.batsupdate_manager section add/remove/migrate
test_moonraker_paths.batsmoonraker.conf discovery across platforms
test_config_symlink.batsprinter_data config symlink creation
test_uninstall.batsFull uninstall flow, cache cleanup, UI restore
test_disabled_services.batsService disable/re-enable state tracking
test_requirements.batsCommand checking, disk space, init system detection
test_detect_tmp_dir.batsTemp directory selection with space checking
test_kiauh_extension.batsKIAUH metadata.json structure, Python syntax
test_kiauh_installer.batsKIAUH extension install/update logic
test_klipper_check.batsKlipper/Moonraker ecosystem pre-flight
test_monolithic_installer.batsBundled install.sh/uninstall.sh structural checks
test_helix_launcher.batsLauncher script, env file sourcing, watchdog
test_generate_manifest.batsRelease manifest generation
test_no_echo_ansi.batsNo raw ANSI in echo (BusyBox compat)
test_code_lint.batsShell code quality checks
test_symbol_extraction.batsDebug symbol extraction for crash reporting
test_telemetry_pull.batsTelemetry data pull scripts
test_resolve_backtrace.batsBacktrace symbol resolution

tests/shell/helpers.bash provides shared utilities:

  • mock_command — Create a mock executable that outputs specific text
  • mock_command_fail — Create a mock that exits non-zero
  • mock_command_script — Create a mock with custom shell logic
  • setup_mock_pi — Create temp directory structure mimicking a Pi system
  • create_fake_elf / create_fake_arm32_elf / create_fake_aarch64_elf — Generate minimal ELF headers for architecture validation tests
  • SUDO="" — Exported no-op for tests that call $SUDO
  • Logging stubs (log_info, log_warn, etc.) suppressed during tests

Pattern for a new test file:

#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-3.0-or-later
WORKTREE_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
setup() {
load helpers
# Reset globals
unset _HELIX_MYMODULE_SOURCED
. "$WORKTREE_ROOT/scripts/lib/installer/mymodule.sh"
}
@test "my function does the right thing" {
result=$(my_function "arg")
[ "$result" = "expected" ]
}
@test "my function handles errors" {
run my_function "bad_arg"
[ "$status" -ne 0 ]
}

Key patterns:

  • Use WORKTREE_ROOT (not hardcoded paths) so tests work in git worktrees
  • unset _HELIX_*_SOURCED before sourcing modules to reset source guards
  • Use $BATS_TEST_TMPDIR for temp files (auto-created, auto-cleaned)
  • Mock system commands by prepending $BATS_TEST_TMPDIR/bin to $PATH

Add a new case in detect_platform() in scripts/lib/installer/platform.sh. Detection must be reliable and specific — avoid false positives on other ARM devices.

Terminal window
# In detect_platform():
if [ "$arch" = "aarch64" ] && is_my_platform; then
echo "myplatform"
return
fi

Add a case in set_install_paths():

Terminal window
elif [ "$platform" = "myplatform" ]; then
INSTALL_DIR="/path/to/helixscreen"
INIT_SCRIPT_DEST="/etc/init.d/S90helixscreen"
PREVIOUS_UI_SCRIPT="/path/to/previous/ui"

If the platform needs runtime hooks (pre-start/post-start behavior), create config/platform/hooks-myplatform.sh in the release package and add the mapping in install_platform_hooks() in the bundled installer.

Step 4: Firmware-Specific Module (Optional)

Section titled “Step 4: Firmware-Specific Module (Optional)”

For platforms with complex setup (like ForgeX), create a dedicated module lib/installer/myplatform.sh:

  • Add source guard
  • Implement install-time and uninstall-time functions
  • Source it in install-dev.sh and add to the bundle-installer.sh module list

Create tests/shell/test_myplatform.bats covering:

  • Platform detection (positive and negative cases)
  • Install path configuration
  • Any firmware-specific patching
  • Uninstall/restore behavior
Terminal window
./scripts/bundle-installer.sh -o scripts/install.sh
./scripts/bundle-uninstaller.sh -o scripts/uninstall.sh

Add the platform to the CI/CD build matrix so release archives are generated. Update the write_release_info() case statement in moonraker.sh with the asset name.


Cause: BusyBox wget on AD5M/K1 doesn’t support HTTPS.

Fix: Download the archive on another computer and use --local:

Terminal window
scp -O helixscreen-ad5m.zip root@printer-ip:/data/
ssh root@printer-ip "sh /data/install.sh --local /data/helixscreen-ad5m.zip"

Cause: Wrong release archive for the platform (e.g., Pi binary on AD5M).

Fix: Ensure you download the correct platform variant. The installer validates ELF headers before proceeding.

Cause: The target filesystem needs at least 50MB free, plus temp space for extraction (~3x archive size).

Fix: Free space, or override the temp directory: TMP_DIR=/path/with/space sh install.sh

”Failed to extract archive: no space left on device”

Section titled “”Failed to extract archive: no space left on device””

Cause: Temp directory ran out of space during extraction.

Fix: The installer tries multiple temp locations (/data/, /mnt/data/, /var/tmp/, /tmp/), picking the first with 100MB+ free. Override with TMP_DIR= env var.

ForgeX: Screen flickers or goes blank after install

Section titled “ForgeX: Screen flickers or goes blank after install”

Cause: ForgeX display mode not set correctly, or screen.sh patches didn’t apply.

Fix: Check display mode: grep display /opt/config/mod_data/variables.cfg — should be GUPPY. Verify patches: grep helixscreen_active /opt/config/mod/.shell/screen.sh.

Cause: Missing release_info.json, wrong section type, or service not in moonraker.asvc.

Fix:

  1. Check release_info.json exists in install dir
  2. Verify section is type: zip (not git_repo)
  3. Ensure helixscreen is in printer_data/moonraker.asvc
  4. Restart Moonraker: systemctl restart moonraker

Cause: The previous UI init script wasn’t found or config/.disabled_services was deleted.

Fix: Manually re-enable the previous UI:

Terminal window
# ForgeX
chmod +x /opt/config/mod/.root/S80guppyscreen
# K1
chmod +x /etc/init.d/S99guppyscreen
# Klipper Mod
chmod +x /etc/init.d/S40xorg
chmod +x /etc/init.d/S80klipperscreen

Cause: Extension files not copied to KIAUH’s extensions directory.

Fix: Manually copy:

Terminal window
cp -r /opt/helixscreen/scripts/kiauh/helixscreen ~/kiauh/kiauh/extensions/

“sudo: The ‘no new privileges’ flag is set, which prevents sudo from running as root”

Section titled ““sudo: The ‘no new privileges’ flag is set, which prevents sudo from running as root””

Cause: The helix-screen systemd service has NoNewPrivileges=true. When a self-update is triggered from the UI, install.sh runs as a child of the service and inherits this restriction — sudo is fully non-functional.

What this affects:

  • Chowning files to the klipper user (non-fatal, a warning is logged)
  • Removing a root-owned stale helixscreen.old backup from a prior manual install

Fix for root-owned stale backup: Manually clean it up on the Pi before or after an update:

Terminal window
# Check what's root-owned
find ~/helixscreen* ! -user "$(id -un)" 2>/dev/null
# Remove it
sudo rm -rf ~/helixscreen.old

The installer handles this gracefully: if it cannot remove the stale .old directory, it renames the new backup to helixscreen.old.TIMESTAMP so the atomic swap can still proceed.