Files
firmware/README.md
T
Oleksandr Shyshatskyi ff7204f7d5 Run everything in Docker; test against the printer's real busybox
The stock kernel-*.tar.xz turned out to contain rootfs.squashfs -- the
printer's genuine root filesystem. Unpacking it answered several questions
that were previously guesswork:

- init is busybox init -> /etc/init.d/rcS -> S99factory_test_shell ->
  /usr/prog/app_startup.sh. There is no systemd, and rcS runs from the
  read-only squashfs before /usr/prog/etc is bind-mounted over /etc, so a
  new S-script cannot be injected into the stock boot sequence.
- /usr/sbin/dropbear and an ENABLED /etc/init.d/S50dropbear already ship.
  Port 22 is open on a stock printer; only the root password is missing.
  The dropbear cross-build is therefore deleted -- it was never needed.

New: test/test-ash-conformance.sh parses every on-printer script with the
printer's own busybox 1.31.1 ash under qemu-mipsel. A parse error in the
firmwareExe wrapper would mean a blank screen, and neither bash -n nor dash
-n proves busybox accepts it.

Makefile now runs every target inside the pinned build image, with the docker
socket mounted so test targets can start sibling containers. The repo is
mounted at its real host path rather than /src, because sibling mounts are
resolved by the host daemon.

78 checks pass.
2026-08-22 22:02:11 +03:00

7.8 KiB

creator5-custom-firmware

Build custom firmware for the FlashForge Creator 5 / Creator 5 Pro: take the stock update package, modify it, pack it back up, copy it to a USB stick. The printer installs it by itself on the next power-on — no jailbreak, no ssh, no opening the case.

cp config.env.example config.env     # point it at your stock package
make probe                           # stage 0: changes nothing, reports back
make test                            # full brick-safety suite

Nothing runs on your machine except Docker. Every make target executes inside a pinned build image; the docker socket is mounted through so the test targets can start sibling containers. LOCAL=1 make <target> opts out.

Unofficial and unaffiliated with FlashForge. It voids your warranty and you are responsible for your machine. Nothing here has been run on hardware yet — see Status.


How it works

We reuse FlashForge's own updater rather than writing a parallel one. The stock chain, recovered from app_startup.sh, the unTar binary and the rootfs image:

busybox init → /etc/init.d/rcS → S99factory_test_shell
             → /usr/prog/app_startup.sh
                 ├─ mounts /dev/sda* on /mnt, globs Creator5Pro-*.tgz
                 ├─ /usr/prog/bin/unTar  (des3 decrypt → /usr/data/update)
                 └─ runFirmwareExe.sh Creator5Pro 0029
                        └─ per component: extract → md5 gate → run.sh
             → firmwareExe  ← the UI, and what starts Klipper

The mod's entire integration point is one file: it replaces /usr/prog/PROGRAM/software/firmwareExe with a shell script. Because everything funnels through that binary, app_startup.sh, rcS and the whole init chain stay completely stock and unpatched. The genuine binary is kept beside it as firmwareExe.stock and remains the default UI.

Services are ordinary init.d-style scripts in /usr/data/mod/init.d/, run in filename order and individually restartable over ssh:

S60web       nginx (Mainsail) + moonraker
S70klipper   Klipper
S80ui        decides which UI runs; owns SAFE-MODE

Everything the mod installs lives under /usr/data/mod on the data partition, which a FlashForge OTA cannot delete.


Five things the stock firmware does that will surprise you

Each of these silently breaks a package or a printer, and each is enforced by a check in make test:

Reality
*.tar.xz is not xz The components are plain tar archives carrying an .xz name. The installer runs a bare tar -xvf, so a genuinely xz-compressed file does not install.
.tgz is not gzip DES3-encrypted plain tar. The key is hardcoded in the printer's own unTar binary. Modern OpenSSL needs -md md5 to match its OpenSSL 1.0.2.
Nothing in init starts Klipper firmwareExe runs /usr/prog/klipper/start.sh. Replace the UI without accounting for this and the printer boots to a working screen that cannot move or heat.
The installer wipes the software dir first ls | grep -v temp | xargs rm -rf runs before run.sh, so the previous firmwareExe is already gone. An uninstall package must ship the genuine binary; it cannot back one up.
Binaries must be nan2008 The Ingenic X2000 kernel returns ENOEXEC for legacy-NaN MIPS. Debian's gcc-mipsel-linux-gnu cannot produce a usable binary — its libc is legacy-NaN.

And one pleasant surprise, found by unpacking rootfs.squashfs out of the kernel component: ssh needs no work at all. The stock rootfs already ships /usr/sbin/dropbear and an enabled /etc/init.d/S50dropbear, so port 22 is already open on a stock printer. There is simply no published root password. Setting ROOT_PW_HASH is the whole feature.


Profiles: a ladder, not a menu

Each profile is a rung. Flash them in order — every rung makes the next one recoverable. Full procedure with go/no-go checks: docs/hardware-testing.md.

Profile What it changes Why this rung
probe nothing Proves the chain works on your unit and reports partition sizes, the init layout and the stock nginx config back onto the USB stick
ssh root password Gives you a recovery channel before anything risky
web + Mainsail/moonraker Uncomments what stock already ships; Klipper untouched
full + forked Klipper, toolchanger First rung that changes how the machine moves
helix + HelixScreen as the UI First rung where the stock UI stops driving the screen
make probe            # or: make build PROFILE=probe
make all-profiles
make uninstall-pkg    # build this FIRST and keep it on a spare stick

Testing: how we know it does not brick

make test needs no printer and no proprietary firmware — a synthetic fixture (test/fixtures/make-stock-fixture.sh) reproduces the package structure, so the whole pipeline runs in CI on a clean machine.

Test What it does
test-lint Scans on-printer scripts for brick patterns: raw block-device writes, rm -rf of top-level paths, unguarded rm -rf $VAR/, missing backups
test-install Runs the installer for real in a container with a fake printer rootfs, then asserts the printer would still boot: UI present and executable, touchscreen driver intact, boot scripts unmodified, user printer.cfg preserved, re-install idempotent
test-roundtrip Installs the mod, then the uninstall package, and asserts the machine is back to stock
test-ui Drives the UI selection logic: helix missing, crash-loop, SAFE-MODE latch and release, and the no-UI-at-all case
test-ash Parses every on-printer script with the printer's own busybox 1.31.1 ash, extracted from the stock rootfs.squashfs and run under qemu-mipsel — a parse error in firmwareExe means a blank screen
test-abi Every shipped MIPS binary is nan2008/mips32r2/o32, and executes under qemu-mipsel

These are not theoretical. Writing them caught four real bugs before any hardware was involved:

  • backups were taken after the stock run.sh had already overwritten the files, so the uninstall package would have restored the modified versions
  • the uninstall package shipped no firmwareExe, so uninstalling would have left a blank screen
  • rm -rf $MODDIR/bin with an unset MODDIR expands to rm -rf /bin
  • a crashed UI is a zombie, and kill -0 succeeds on a zombie — the crash detector called a dead UI healthy

Requirements

Docker. That is the whole list — the build image carries openssl, tar, xz, unzip, python3, binutils, squashfs-tools and qemu-user-static, and every target runs inside it. make shell drops you into it.

make rootfs extracts the printer's genuine root filesystem from the stock package's kernel-*.tar.xz (it contains rootfs.squashfs) and enables make test-ash. It is never committed — it is FlashForge's firmware.


Layout

bin/            unpack → patch → pack, plus verify / make-uninstall
payload/        what runs ON the printer (POSIX sh, busybox ash)
  firmwareExe     the wrapper that replaces the stock binary
  init.d/         S60web, S70klipper, S80ui
  run-pre.sh      backups, injected at the TOP of the stock run.sh
  run-append.sh   payload install, injected before its exit
  report.sh       the stage-0 diagnostic report
profiles/       the ladder
assets/         nginx.conf, moonraker.conf
test/           the suite above + the synthetic fixture
docs/           hardware-testing.md

Status

The build pipeline and the full test suite pass on a development machine (75 checks). No package has been installed on a printer yet. The Klipper creator5 branch this builds on is itself documented as not yet run on hardware.

Start at stage 0, and build the uninstall stick before you start.