Files
firmware/tools/replica
Oleksandr ShyshatskyiandClaude Opus 5 ebc3d60c58 Put back the one file a stock flash cannot
A printer flashed back to stock came up with the FlashForge screen and no
Klipper behind it, and stayed that way however many times the stock package
was flashed. The .cfg files were the obvious suspect and are not the fault:
stock's run.sh does `cp $WORK_DIR/klipper/config/* /usr/data/config/ -rf`, and
the printer's busybox unlinks a dangling symlink rather than writing through
it, so printer.base.cfg and the seven beside it come back on every stock
flash. The ff-*.cfg links left behind resolve to nothing and are included by
nothing. Even the SAVE_CONFIG block our Klipper wrote into printer.cfg is
harmless -- configfile.py marks every autosave section valid through
access_tracking, so `[ff_tool 0]` there is not a config error on a stock
klippy. All three were put to the printer's own binaries in the replica.

What survives is /usr/prog/klipper/klipperDaemon. Every release links it at
the mod, whose `start` does nothing on purpose -- klippy is an s6-rc longrun
and a second unsupervised one would fight it for /dev/ttyS4 -- and the stock
package has no klipperDaemon to copy over it: not in the software component,
not in its md5sum.list, no line in run.sh. Stock's start.sh ends in
`klipperDaemon start`, so the UI starts and klippy never does.

installer/recovery.sh is the repair, packed by bin/pack-recovery.sh into a
.tgz that carries no mod at all: FlashForge's own klipperDaemon, taken off a
factory /usr/prog and gated on its md5, and their Klipper configs out of the
stock package the build reads. It sweeps FlashForge's config directory by
shape rather than by a list of names -- a symlink into $MODDIR there can only
be ours -- refuses to run while the mod is still the firmware, and never
touches printer.cfg, which no package has a copy of to put back. Output goes
to work/recovery/: bin/pack.sh wipes work/out, and the replica lane installs
the newest .tgz there as the mod under test.

qa/replica/test_recovery.py holds the whole story against a real machine: the
link the mod leaves, the busybox cp, the refusal, `klipperDaemon start`
leaving no pid file, then the same command forking a real klippy once the file
is back.

Publishing lives in Klipper4FlashForge/stock-recovery, which checks this repo
out and releases there, so a repair for owners of old releases is not buried
among firmware releases they must not flash to get it.

Two things found on the way, both in the harness rather than the mod:

seed-prog.sh appended USER-CONFIG-MUST-SURVIVE as a plain comment after the
SAVE_CONFIG header, and Klipper drops the entire autosave block on the first
line there that does not start with '#*#'. Every replica ran with klippy
seeing none of the machine's saved PID, shaper, mesh or probe values, and
failing on an unrelated-looking missing option. It is written inside the block
now, an older image's copy is repaired in place, and test_install.py asserts
the invariant.

installed_image() keyed its cache on the package md5 alone, so a rebuilt
PRINTER_IMAGE silently handed back a bake from the old base -- which made the
fix above read as ineffective twice before the cache was the suspect. The tag
mixes in the base image id now, and build-printer-image.sh takes IMAGE_TAG so
a rebuilt image can be published without moving a tag ci.yml pins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hdzEqXuFcY4xXALGPbBFV
2026-09-07 20:47:04 +03:00
..

The printer replica

A container that runs the printer's own userland: the real rootfs.squashfs extracted from FlashForge's package, chrooted, with its MIPS binaries executing under qemu. uname -m says mips and busybox applets behave exactly as they do on the machine.

It lives under tools/ rather than test/ because it is not a test. Three different things drive it, and only one of them is a suite:

driver what it does with it
bin/payload.sh -> bin/build-payload.py the build path. The payload is assembled by running the PRINTER'S OWN opkg against /, inside here. Nothing on the host can do that
qa/lib/replica.py the replica lane's printer fixture -- holds one container open and probes it
make boot-screen-sim rendering the boot frames on the cross-built CPython
printer/                  the machine itself, SHELL on purpose: it is executed
                          by the printer's ash under qemu, and that it survives
                          that is a large part of what the suite proves
  Dockerfile              the container: qemu-user-static, rsync, openssl,
                          dosfstools. It contributes no userland worth speaking
                          of -- everything real comes from the chroot
  Dockerfile.full         the same, plus the firmware baked in: a prebuilt
                          PRINTER_IMAGE, so a run does not unpack 93MB
  binfmt.sh               registers a handler for the printer's binaries. The
                          stock qemu-mipsel registration does not match them:
                          its mask requires e_ident[EI_ABIVERSION] == 0 and
                          every binary the Ingenic toolchain built has 3 there
  assemble.sh             the mount layout -- read-only root, writable prog and
                          data partitions, optionally a real FAT USB stick
  seed-prog.sh            a /usr/prog that looks like a printer's
  entrypoint.sh           setup, then the case script it was given
  entrypoint-out.sh       the same, plus a writable /out inside the chroot
  bake.sh, bake-case.sh   install a package and commit the result as an image
ffsim/                    the host half. NOT a test framework -- it was one,
                          and 1,657 lines of it are gone; what is left starts
                          containers and reads config.env/test.env
build-printer-image.sh    bakes a prebuilt replica image (`make printer-image`)
sim-boot-screen.py        `make boot-screen-sim`

--privileged buys exactly two things: the binfmt registration, and the mount layout. Nothing here needs it for any other reason.

See docs/printer-replica.md for what the replica can and cannot prove, and docs/testing.md for the suites.