test/ has two test frameworks. run-tests.py + ffsim is 1,657 lines reimplementing what pytest already does -- skip accounting, gating, reporting, then parsing pytest's own JUnit XML back out -- and it calls pytest as gate #5 of about twenty. Underneath it, 6,280 lines of case-*.sh execute inside the qemu chroot and report 13 booleans between them; case-moonraker.sh is 1,059 lines that collapse to one pass/fail. Nothing runs in parallel and nothing can be selected: each replica gate is its own `docker run --privileged`, re-registering binfmt and re-assembling the mount layout, thirteen of them in sequence. All three follow from one decision -- THE REPLICA CONTAINER IS TREATED AS A TEST RUNNER INSTEAD OF AS A FIXTURE. That is what pushes assertions into ash, which yields one bit per container, which then needs a bespoke framework above pytest to aggregate the bits. qa/ is the replacement, built beside test/ rather than inside it, with two lanes and one framework. Both suites run in CI; test/ is still the release gate; nothing has been deleted. docs/qa-migration.md has the order the rest moves in, and the rule: a case script is deleted only once its replacement here has been green, so there is never a window where coverage drops. THE MACHINE IS UNCHANGED. qa/ reuses test/integration/printer/'s Dockerfile, entrypoint.sh, assemble.sh, binfmt.sh and seed-prog.sh as they are, and its `docker run` argv is deliberately ffsim's, minus --rm and plus -d. While both suites run they have to be testing the same machine or neither proves anything about the other. The only new trick is how the container is held open: entrypoint.sh ends by running the case script it was given, and the one it is given here touches a marker and sleeps, so setup completes exactly as it always has and the container simply does not exit. The marker is also the readiness signal, because setup ranges from under a second to over a minute and a constant would either waste the fast case or fail the slow one. TESTS RUN AGAINST A MACHINE THE REAL INSTALLER PRODUCED. The `printer` fixture is a replica with the package installed by the printer's own app_startup.sh, off a genuine FAT filesystem on /dev/sda1. The first version of this tree did what case-services.sh does -- copy payload/*.sh and payload/init.d/S* into $MODDIR by hand -- and two things were wrong with it. It is a second implementation of the install, so everything downstream asserted against a layout the harness built rather than one the installer produced, and the real installer could have broken without a single test noticing. And it can only place what is in payload/, so everything the BUILD produces was missing: s6 is a build output that bin/patch.sh stages into the package, so a hand-placed payload has no supervisor at all, which is why the tests had to carry a stand-in scanner and gate one assertion on whether it was in use. Installing for real fixes both and deletes the stand-in. Hand-placed, /usr/data/anvil held anvil-env.sh, anvil-service.sh, anvil.conf, init.d and etc/s6. Really installed it holds VERSION, backup, bin, config, config-installed, helixscreen, lib, libexec, moonraker, nginx and www as well -- bin/lib/libexec being the cross-built s6 and CPython 3.13. The file is SHORTER than the case script it replaces while reporting 44 results instead of one. case-install.sh's header records this same lesson from the first time somebody learned it: "An earlier version of this file replayed app_startup.sh by hand -- which meant a bug in our reading of it could never be caught." The install is the machine's own shell under qemu, so it is baked into an image once per package and cached under a tag derived from the package's md5 -- build-printer-image.sh's trick for the stock baseline. Rebuild the package and the tag changes, so the next run bakes again instead of testing yesterday's build; deliberately not a hand-written staleness check, because those are the ones that eventually get it wrong. A MISSING TOOL OR RESOURCE FAILS, and there is no flag. This tree briefly had --strict-skip=<lane|all>, which was ALLOW_SKIP inverted -- and ALLOW_SKIP's defect was never the direction it named things in, it was that the decision lived in the CI invocation. An accept-list has to be edited whenever a gate moves; a strict-list has to be remembered whenever a job is added; both degrade silently when the command line is wrong, which is the same disease as the printer-sim job that was gated on an unset secret and never ran once in its existence. So the verdict lives with the knowledge: no shellcheck, no pyflakes, no docker, no daemon, no replica image, no built package -- each fails where it is needed, with the fix in the message. There are no skips in this suite. MEASURED, on monstrofil/creator5-printer:latest, both suites agreeing on the same machine: 131 tests in 40s, all green; qa/static alone 1.3s with no docker; one assertion selected by node id 2.9s, against the whole 360-line case being the smallest unit test/ can run. Granularity checked by injecting the regression case-services.sh exists to catch -- deleting SVC_EXTRA_VERBS="|force-start" from S70klipper -- which failed one named test, printed the usage line that came back, left the other 41 reporting, and took 4s to re-run alone. Two bugs the port found in its own harness, both only visible by running it. bake.sh shipped without its executable bit, so the lane died at container init with 43 errors all saying "could not install the package" -- a file mode wearing an installer failure's clothes; test_action_scripts_are_executable now guards it, the same check the payload's own s6 run scripts already have. And an ELF magic assertion used `od -An`, which the printer's busybox od does not have -- the replica catching host-vs-printer drift in the test code itself, which is the entire reason it exists. Also here: qa/static/test_probes.py, which has no counterpart in test/. Printer.ps() and Printer.listening() are parsers, and a parser is where a harness goes quietly wrong -- a listening() that never matches makes every readiness assertion pass by the absence of contradiction. Neither failure looks like a failure; they look like a green suite. So they are tested against captured output, in the static lane, with no docker involved. Two things found while surveying, neither fixed here. case-pyext.sh is 567 lines with no gate in gates.py, no Makefile target and no CI step: it has never run. And case-mcu-bringup.sh:4 says the handshake is "tested elsewhere against a pty" -- there is no pty anywhere in the repo, and every documented past bug in ff_mcu_bringup.py lived in the code that comment excuses from testing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reforge
Custom firmware for the FlashForge Creator 5 and Creator 5 Pro.
It installs the way a normal FlashForge update does: copy one file to a USB stick, plug it in, power the printer on. No jailbreak, no soldering, no opening the case, no ssh needed to get started — the printer's own updater does the work.
Packages are published on the Releases page — two files per release, one per model, marked pre-release while the mod is young. Help and support: the Discord.
Unofficial and unaffiliated with FlashForge. It voids your warranty and you are responsible for your machine. See Status for what has actually been tried on hardware before you flash anything.
Status
It runs on a real printer — a Creator 5 Pro, flashed and printing:
A checked box happened on that machine; an empty one has not, yet.
For everyone:
- Install and migration from stock firmware — the printer's own updater does it; your unit's factory calibration imports itself on the first boot
- Printing — gcode straight from the slicer, nothing edited
- Toolchanges — a current Klipper with real toolchanger support, in place of the 0.12-era tree FlashForge ships
- Nozzle-offset calibration
- Mainsail at
http://<printer-ip>/, with a current Moonraker behind it — anything that speaks the Klipper API works - Camera — mjpg-streamer, already on the printer; the mod just starts it
- Wifi
- HelixScreen on the printer's own screen
- Chamber heater
- Creator 5 (non-Pro) — its package builds and passes the same replica test suite, but has not touched hardware
- Going back to stock — proven on the printer replica, not yet needed on the machine. The stock package restores everything it carries; Moonraker is not in it, so the mod's build stays and keeps working — see If something goes wrong
For the tinkerer:
- ssh as root — a random password is chosen on the first install
and written to
anvil-password.txton your stick; updates keep it (make passwdbakes in your own)
Have the stock package for your model on a spare stick before you begin.
How the mod hooks in — one file, firmwareExe, with the stock boot scripts,
updater and recovery path left untouched — is
docs/how-it-works.md.
What you give up
That one file is FlashForge's whole application, so anything that lived inside
it rather than in a separate service goes with it: the FlashForge network
API on :8898, and with it FlashPrint, Orca's FlashForge profile and the
mobile app. Moonraker's API takes over — upload from OrcaSlicer or Mainsail
instead. Flashing the stock package brings the API back.
Which file do I need
Creator 5 and Creator 5 Pro are not interchangeable. The printer refuses a package built for the other model — the most common reason a flash "does nothing" — so pick the file whose name matches your machine on the Releases page:
| Your printer | The file |
|---|---|
| Creator 5 | Creator5-anvil-<date>.tgz |
| Creator 5 Pro | Creator5Pro-anvil-<date>.tgz |
Not sure which you have? On the printer: Settings → About.
How to flash
Before you start
- Put the stock FlashForge package for your model on a second stick. That stick is the undo button — see If something goes wrong.
- Read docs/hardware-testing.md first: the go/no-go checks after the flash, in order, and what means stop.
The flash
- Format a USB stick as FAT32. Not exFAT, not NTFS.
- Copy the file for your model to the root of the stick. Not in a folder. Leave the name exactly as it is — the printer looks for that pattern.
- Power the printer off.
- Plug the stick in and power the printer on. It finds the package by itself, shows the update screen and installs. This takes a few minutes.
- Wait for it to reboot on its own, then pull the stick — your root
password is on it, in
anvil-password.txt.
After the flash
Everything is on the printer's own address:
http://<printer-ip>/ |
Mainsail |
http://<printer-ip>:7125 |
Moonraker's API — what slicers upload to |
http://<printer-ip>/webcam/ |
the camera stream (mjpg-streamer on :8080) |
ssh root@<printer-ip> |
the shell — password from anvil-password.txt |
Logs worth reading, all on the data partition and all surviving a reboot:
/usr/data/anvil-install.log what the installer did
/usr/data/logs/anvil-boot.log services + UI choice at each boot
/usr/data/logs/printer.log klipper
/usr/data/logs/helixscreen.log helixscreen
Then run the go/no-go checks in docs/hardware-testing.md, in order, with the emergency stop within reach.
Before your first print: calibrate
Prints refuse to start on an uncalibrated toolchanger — by design. The sequence is short, and the first part happens without you.
-
Your unit's factory calibration imports and saves itself on the first boot (Klipper restarts once, before the screen is up).
-
Take the PEI sheet off — the calibration station sits below the bed plane — then home and calibrate:
G28 CALIBRATE_TOOL_OFFSETS SAVE_CONFIGStep by step, with every message it can refuse with: docs/calibration.md.
-
TOOLCHANGE_STATUSandTOOL_OFFSET_STATUS— every tool should show a nozzle triple and a dock position, nothing should sayNOT CALIBRATED. Then check the Z offset against a sheet of paper before trusting a first layer — Verify shows the exact moves. If the nozzle presses into the plate, stop.
The full procedure is docs/toolchange.md. Two files
under gcode/ are there for the first runs:
creator5-safe-moves.gcode (cold, nothing below Z50), then
creator5-feature-test.gcode (the real thing, two tools).
Slicer setup
OrcaSlicer, with the stock FlashForge profile left alone. The one field to
touch is Change filament G-code: clear it down to a bare Tn — the
toolchanger takes it from there. Upload through Orca's Klipper/Moonraker
target or drop the file into Mainsail. Details:
docs/toolchange.md.
Your files and the mod's files
| File | Whose |
|---|---|
/usr/data/config/printer.cfg |
Yours — never overwritten. Overrides go here, after the includes: restate only what you change, the last value wins. |
moonraker-custom.conf |
Yours — created once, never rewritten, included last so your settings win. Do not delete it. |
ff-*.cfg, printer.base.cfg, moonraker.conf |
The mod's — overwritten on every update; do not edit. |
anvil/helixscreen/config/settings.json |
Yours — everything you set on the screen. Written by HelixScreen itself; carried across updates along with helixscreen.env and its spool map. |
Upgrading
A newer release installs exactly like the first flash: stick in, power on.
Your printer.cfg with its saved calibration, moonraker-custom.conf, your
root password and everything you set on the screen survive; the ff-*.cfg
family is replaced. If a release changes helixscreen.env, yours is kept and
the new one is left beside it as helixscreen.env.mod-new.
If something goes wrong
Flash the stock FlashForge package for your model. It restores everything it carries and installs the same way — stick in, power on. Keep one on a spare stick before you start; the stock packages are published at ghzserg/FF.
The one thing it cannot bring back is FlashForge's Moonraker: the stock package does not carry it, so the mod's build stays — it works, and Mainsail is happy with it (details). The symptom table, the logs worth reading, and the factory-restore last resort are in docs/hardware-testing.md — and there are people to ask on the Discord.
Versions
| Component | Pinned at |
|---|---|
| Stock FlashForge base | 1.9.7-1.2.9-20260810, fetched from ghzserg/FF at build time |
| Klipper | a current fork (branch creator5) with the ff_* toolchanger extras |
| Mainsail | v2.18.2 |
| Moonraker | commit 9d0d09d — a commit, not a release, for a hard reason: docs/how-it-works.md |
| HelixScreen | v0.99.115-creator5.1 |
Documentation
| docs/hardware-testing.md | The on-hardware procedure, with the checks that say go or stop |
| docs/how-it-works.md | How the mod hooks into the stock firmware, and what the stock firmware does that surprises people |
| docs/toolchange.md | The toolchanger mod: the ff_* Klipper extras, the ff-*.cfg configs, and the Z offsets to get right before you print |
| docs/calibration.md | XYZ tool calibration from the operator's side: the order of commands, what each prints, and what every error means |
| docs/notes/70-error-codes.md | Every error code the firmware can raise, by subsystem |
| docs/building.md | Building your own packages from a stock FlashForge one |
| docs/testing.md | The test suite, and how we know a package does not brick a printer |
| docs/printer-replica.md | The printer replica: what is authentic, what is not |
| docs/notes/ | Reverse-engineering notes on the stock firmware, behind the toolchanger work |
| gcode/ | The two verification files for the first runs on hardware |
Credits
Built on the work of others: ghzserg/FF publishes the stock packages and factory image everything here starts from; Mainsail, Moonraker and HelixScreen are shipped as released; the toolchanger status API follows viesturz/klipper-toolchanger so tool-aware UIs work unchanged.

