Files
firmware/docs/hardware-testing.md
T
Oleksandr ShyshatskyiandClaude Opus 5 6aa622d2ff 6,280 lines of case scripts reported 13 bits. They are 118 tests now.
test/run-tests.py is gone, and with it the bespoke harness that wrapped
pytest, re-parsed its JUnit XML, and drove thirteen replica case scripts to
get one boolean back from each. Every gate it ran is either a module under
qa/replica/ or deliberately retired.

PORTED. case-install.sh (579 lines, 1 bit) is test_install.py, 22 tests, asked
of the machine the printer's own app_startup.sh built rather than one the
harness assembled. case-upgrade.sh is 21, case-supervisor.sh 31,
case-boot-screen.sh 17, case-mcu-bringup.sh 8, and case-nginx.sh plus
case-moonraker313-s6.sh are test_web.py's 7.

RETIRED, subject deleted by the s6-rc switch: case-services.sh, case-nginx.sh,
case-moonraker.sh, case-moonraker313-s6.sh, case-camera.sh and
case-priority.sh drove $MODDIR/init.d/S* or sourced anvil-service.sh, neither
of which exists. Decided against real output -- `make test-services` failed
with "the payload ships no anvil-service.sh". case-pyext.sh and
case-moonraker313.sh went too: no gate, no target, no CI step, never run once.

RETIRED as a coverage decision: case-python.sh (test_web.py runs real Moonraker
on that interpreter, which is harder), case-libpath.sh, and case-recovery.sh.
The last is the brick-safety rollback and has no replacement; it is recorded in
docs/qa-migration.md rather than left to be discovered.

THREE ASSERTIONS HAD STOPPED MEANING ANYTHING, and each had been green for
months:

  * case-mcu-bringup.sh's negative control had INVERTED. It asserted the
    bring-up fails without LD_LIBRARY_PATH; CPython is --disable-shared now and
    needs no library path, so the case's own skip branch was the one firing.
  * case-upgrade.sh's headline assertion was VACUOUS. run-append.sh:150 does
    `rm -rf $MODDIR/init.d` unconditionally, outside the manifest branch, so
    "the update removed the stale S60web" passed regardless of the manifest
    logic it was written to test.
  * case-boot-screen.sh CONFIRMED ITS OWN ARITHMETIC, computing expected pixel
    packing by calling ffscreen._pixel inside the replica -- a swapped R/B
    matched itself.

case-install.sh's boot-log grep is not reproduced for the same reason: the log
line names every service in the boot set including the ones that then fail, so
grepping it cannot go red. The compiled database is asked instead.

TWO REAL BUGS FOUND ON THE WAY.

assemble.sh attached /stick.img with `losetup` and wrote the device to
/stick.loop, which NOTHING HAS EVER READ. Loop devices are the host's, not the
container's, so every USB_STICK=1 run leaked one permanently and a WSL2 kernel
has thirteen. Once they were gone every replica run failed with "failed to
setup loop device", which looks like a docker fault and is not. Detached now in
entrypoint.sh on the way out, and in qa's stop() for the held-open containers
that never take that path.

test_startup.py was 43 tests red: it built its argparse.Namespace by hand and
had never been taught --only-bringup / --no-bringup, and its FakeKlipper still
modelled start.sh plus a separate stop when klipper is restarted with one
`s6-svc -wr -t`.

CI AND RELEASE. The replica job was getting its package as a side effect of
run-tests.py running unpack/patch/pack; that is an explicit step now, as the
comment there predicted it would have to be. release.yml tested the exact files
it ships through sim-install.py, whose exit status the loop was discarding --
it is `REAL_PKG=<pkg> pytest ./qa/replica` per model now, 22 named results each.

WHAT IS NOT TESTED ANY MORE: bin/unpack.sh, patch.sh, pack.sh and verify.sh.
The packaging build on a synthetic stock package went with the runner and has
no counterpart in qa/. That is the path producing the .tgz a user flashes.
make-stock-fixture.sh is kept as the input a replacement needs.

Measured: qa/static 246 passed, qa/replica 110 passed and 8 blocked on hardware
the replica has not got (no /dev/ttyS*, no /dev/video*, so ok-all is
unreachable), test/ pytest 185 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BTAXgjtF8GdhqSxvdXCFW
2026-08-29 22:51:50 +03:00

4.2 KiB

Hardware testing

CI installs the package into a replica of the printer -- the real rootfs.squashfs running under qemu-mipsel, on the printer's own busybox and unTar (see printer-replica.md) -- and proves the machine would still boot. What it cannot do is drive the screen, the MCUs, the toolchanger or a print. This is the on-hardware procedure.

There is one flash: the firmware package for your model.

Rule: have the stock FlashForge package for your model on a spare stick before you flash anything. Flashing it back is the uninstall, and it is the only recovery step that needs nothing but a USB port — no ssh, no screen.

It used to be proven by make test-recovery, which installed the mod into the replica and flashed the stock package over it. That gate has been retired and nothing has replaced it yet — see qa-migration.md. It passed on its last run, but treat the rollback as unverified against the current build.


Step zero: check the model gate

Every package carries a gate. Its runFirmwareExe.sh has MACHINE= / PID= baked in, and compares them against the values app_startup.sh passes from the firmware already on the printer. A mismatch is refused with "Firmware does not match machine type" — it will not install, and it is not a valid recovery image either.

Model MACHINE PID
Creator 5 Creator5 0028
Creator 5 Pro Creator5Pro 0029

The gate is not cosmetic: the two models ship different firmwareExe binaries, so a package must be built from the stock package for its own model. make release builds both.

Find yours in Settings → About, or in /usr/data/firmwareRes/config/general.json (machineName), and set TARGET_MACHINE in config.env. Then:

make build && make verify          # one model
make release                       # both, into dist/

verify.sh fails loudly on a mismatch. You must start from a stock package built for your own model — the mod inherits the gate from whatever package you unpack.

Before the first flash

  • Put a copy of the stock FlashForge package for your model on a spare USB stick and keep it physically separate. That is your recovery image: flashing it restores every file the mod touches. Check it is the right model first — see above.
  • Note your printer's serial number (Settings → About). The factory image restores a placeholder serial and you may need to put yours back.
  • Confirm you can reach the printer's IP.
  • make qa passes.
  • make rootfs && make qa-replica passes — this parses every script that will run on the printer using the printer's own busybox ash.

Two USB sticks, FAT32, both packages at the root of the stick (not in a folder). Ship both filenames (Creator5-* and Creator5Pro-*): the Pro's boot script globs Creator5Pro-*.tgz, the non-Pro globs Creator5-*.tgz.


The flash

One flash brings up everything: a root password you know, Mainsail and moonraker, the forked Klipper with the toolchanger extras, and HelixScreen on the touchscreen in place of FlashForge's UI.

From here the procedure is the one an owner follows, and it is written for them rather than repeated here:

Installing the flash itself, the first boot, the root password
Your first print the go/no-go checks, calibration, and the two files under gcode/
Support the logs, and the way back to stock

What follows is the part that is ours rather than theirs: keeping those two verification files honest as the macros change.

Keeping the verification files honest

make test-py checks both files against the shipped macros and the configured axis limits: every command in them must be one the mod's Klipper config or pkgs/klipper/payload/klipper/klippy/extras/ defines (bar an explicit allowlist of Klipper built-ins, currently just SET_PRESSURE_ADVANCE), TOOLS= must list every tool the feature print uses, and the safe file's own lines must stay cold and above Z50 — the check reads the file, so it does not see what an implicit START_PRINT does before it. A renamed macro breaks the suite rather than the print.