Commit Graph
7 Commits
Author SHA1 Message Date
Oleksandr Shyshatskyi 80a4d64ffd Prepare next firmware release
Add Fluidd and custom boot scripts, migrate DC24V control, update the web UIs and filament macros, improve startup status reporting, and expand upgrade and replica coverage.
2026-09-24 11:37:28 +03:00
MonstrofilandClaude Opus 5 f095075985 Put FlashForge's chelper back where our Klipper replaced it (#22)
chelper/__init__.py and c_helper.so are one unit: the first declares the cffi
cdefs, the second is what they describe. FlashForge's 1.9.7 declares

    void extruder_set_pressure_advance(struct stepper_kinematics *sk
        , double pressure_advance, double smooth_time);

and ours takes a fourth argument. A stock flash restores c_helper.so -- it is
in the chelper.tar their software component carries -- and restores nothing
else in that directory. Their klippy.py, toolhead.py and mcu.py come back too;
gcode.py, bed_mesh.py and chelper/__init__.py do not, because their overlay is
thirty files against the three hundred and twelve in the factory tree.

So a printer that took v20260827 or v20260827b, whose software component
carried our klippy, comes back from a stock flash with our cdefs against their
library and their extruder.py calling it their way: "expects 4 arguments, got
3", at the first extruder, on a machine that has already been flashed back to
stock and has nothing left to flash.

$MODDIR/prog/stock-chelper is FlashForge's directory, off a 1.9.7 factory
/usr/prog, and anvil-link-prog.sh restores it over ours. The whole directory
rather than the two files that disagree: their __init__.py is upstream's,
check_build_code and all, so a .c newer than c_helper.so sends klippy to
mips-linux-gnu-gcc, which no printer has. c_helper.so is touched last for the
same reason.

ONLY UP TO 1.9.7. That is the firmware this copy came off; a newer FlashForge
release may pair a different __init__.py with a different library, and
overwriting it would be this bug pointed the other way. An unreadable version
is left alone too. The version is theirs to state -- no package of ours ships
a software component, so /usr/prog/PROGRAM/software still names FlashForge's.

It rides in anvil-klipper rather than anvil-core, which qa/static caught: a
MIPS c_helper.so in a package declaring Architecture: all installs on anything.
That recipe gets a postinst of its own for the same reason anvil-klipper-config
has one -- anvil-core's pass runs before this package's payload has landed.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 21:07:25 +03:00
MonstrofilandClaude Opus 5 5316694795 Put FlashForge's klipperDaemon back on a printer that lost it (#20)
A stock package restores every path the mod points at itself except
/usr/prog/klipper/klipperDaemon, which it does not carry: not in the software
component, not in its md5sum.list, no line in run.sh. Whatever sits there
survives every stock flash and stock's start.sh calls it on each boot, so a
printer whose copy was replaced cannot be returned to stock at all -- the file
FlashForge shipped it with is gone, and no package of theirs will put one back.

anvil-core ships that copy now, and anvil-link-prog.sh restores it whenever it
finds a link into $MODDIR at that path -- on a flash and on an `apk upgrade`
alike. One update is enough, and it costs 845 bytes.

The population this is for is the one that keeps up to date: they took a
release that owned the file, they upgraded, and the link is still there --
dangling, since the payload no longer carries what it pointed at. Under the
mod nothing calls it and nothing looks wrong; the bill arrives when they flash
stock and get a UI with no Klipper. Owners who have already rolled back cannot
be reached from here at all and are served by the recovery package.

The shipped copy is FlashForge's, byte for byte, so it is in the parse list
and out of the shellcheck list: they write `mkdir -p $(dirname $KLIPPER_LOG)`
and a lint fix would put a script on printers that no FlashForge machine has
ever run. The replica asserts its md5 after the restore for the same reason.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 12:53:54 +03:00
MonstrofilandClaude Opus 5 1afd5dd2d2 Leave FlashForge's klipperDaemon alone (#19)
It is the one file a stock flash cannot undo, and replacing it is what left
owners with a printer that would not print. A stock package restores the other
two paths the mod points at itself -- its run.sh copies its own firmwareExe and
start.sh over them, and busybox `cp -f` unlinks a symlink rather than writing
through it. It carries no klipperDaemon at all: not in the software component,
not in its md5sum.list, no line in run.sh. So the link outlived the mod, and
stock's start.sh, whose last line is

    /usr/prog/klipper/klipperDaemon start

ran our shim, whose `start` does nothing on purpose. The UI came up, klippy did
not, and flashing stock again changed nothing because there was nothing in it
to change.

The shim guarded that same line: klippy is an s6-rc longrun here, and stock's
klipperDaemon forks a second, unsupervised one that fights it for /dev/ttyS4
and /tmp/uds. But no caller is left on a modded machine. Our own start.sh
replaces stock's and asks s6-rc; FlashForge's firmwareExe execs
/usr/prog/klipper/start.sh and names no other script (`strings` on their
binary); Moonraker runs with `provider: none`, so a restart from Mainsail goes
over the API rather than through a service script; ff-startup and S70klipper
drove it once and neither exists now. What remains is a person typing
`klipperDaemon start` over ssh, who gets FlashForge's behaviour and a second
klippy -- s6-rc and s6-svc are the supported way, and that trade buys back a
printer that can be returned to stock.

test_install.py asks the machine the inverted question now: klipperDaemon is
not a link into $MODDIR, it exists, and it is FlashForge's own script.

Printers that already took an earlier release still carry the link and cannot
be repaired from here -- nothing of ours runs during a stock flash. The
package at Klipper4FlashForge/stock-recovery puts their copy back.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 11:33:55 +03:00
MonstrofilandClaude Opus 5 e20ed98dac Refuse to install on FlashForge firmware older than 1.9.6 (#17)
A printer still on 1.9.4 does not come up on this package: the board firmware
that release pairs with differs from the one klippy is built to talk to, the
MCU never connects, and the failure is at the bottom of the stack -- no
Klipper, so no screen and no Mainsail. From the outside it is indistinguishable
from a bad flash, and the only way out is flashing stock again.

So it is refused before anything is written, with the fix in the message:
install FlashForge's own current firmware first, then this. That costs one
more flash and leaves a working printer either way.

WHAT IS READ is /usr/prog/PROGRAM/software, which holds one directory named
for the version of FlashForge's software component -- install_component wipes
the previous one before renaming the new one in. No package of ours ever ships
a software component, so it goes on describing the printer rather than the
mod. The highest is taken when a machine somehow holds two, so a leftover from
an interrupted install cannot refuse a printer that should go ahead, and the
comparison is field-by-field numeric because a string compare makes 1.9.10
older than 1.9.6.

AN UNREADABLE VERSION IS INSTALLED ON. The two failures are not the same size:
letting one through costs a printer that has to be flashed back to stock,
while refusing on one would break the install path on every machine whose
layout is not the one being read.

THE REFUSAL HAS TO BE VISIBLE. It happens before the panel is switched to
start.img and app_startup.sh goes on booting the printer, so from the outside
the flash would just have done nothing. installer/stock-too-old.img is the
frame it paints instead -- the same raw 480x800@32 framebuffer dump start.img
is, rendered by the same ffscreen.py, so it needs nothing on the printer but
`cat > /dev/fb0`. The version in it is DRAWN, so bin/pack.sh carries the
command to re-render it when MIN_STOCK_VER changes. anvil-NOT-INSTALLED.txt on
the USB stick is the copy that survives the power cycle.

Three tests, on the replica. The machine there is a real one in the way that
matters -- its /usr/prog/PROGRAM/software holds FlashForge's own 1.9.7 -- so
"current firmware installs" needs no setup, the refusal swaps that directory
for one holding 1.9.4 and restores it, and the third asserts that a stale
1.9.4 left BESIDE the real version does not refuse. The control matters more
than the refusal: a gate that refused everything would pass the refusal test
and brick every flash.

qa/replica/test_upgrade.py is 19/19; the lane's 10 other failures are the ones
master has on this bake.


Claude-Session: https://claude.ai/code/session_01Fn1k6kaHWhUrsmBfpxjUcG

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 10:39:37 +03:00
MonstrofilandClaude Opus 5 0747bc500b Keep the config in a directory of our own (#16)
* Keep the config in a directory of our own

Flashing a stock FlashForge package back over the mod left a printer that
would not start. /usr/data is the DATA partition: a stock flash rewrites
/usr/prog and does not touch it, so the symlinked printer.base.cfg and the
ff-*.cfg beside it outlived the mod, pointing into a /usr/data/anvil that was
gone -- and Klipper treats an [include] it cannot resolve as fatal. Nothing of
ours runs during a stock flash, so there is no undo to write from inside the
mod. Not writing there is the only version of this that works.

The live config is /usr/data/anvil-data/config now, seeded once by
anvil-link-prog.sh with a copy of everything in /usr/data/config: printer.cfg
with the SAVE_CONFIG block of calibrated values at the end of it, and the stock
includes the mod neither ships nor parses. The seed test is printer.cfg, which
is copied last and through a temp name, so a machine that never finished seeds
again on the next boot rather than staying half-configured, and no update ever
re-seeds over a tuned file. It is outside $MODDIR, so the installer's
wipe-and-extract cannot reach it and nothing has to be stashed on /tmp.

Moonraker moved with it, and the lever is the data path rather than -c:
moonraker registers its config root as <data path>/config and nothing
configures that separately, so -c alone would move the file it READS while
file_manager kept registering FlashForge's directory. With -d
/usr/data/anvil-data its config directory is klippy's, which is also what
Mainsail's config editor lists. gcodes and logs stay where the machine keeps
them -- /usr/data/gcodes is klippy's [virtual_sdcard] path -- as symlinks
inside the data path, which works because register_directory() realpaths a
root that is a link. The database is carried across once, by the service,
below the stray-killer: the one point where moonraker is provably not running.

What an older release left in /usr/data/config STAYS there. A sweep could
delete the links but could not put stock's own printer.base.cfg back, because
the release that replaced it kept no copy -- and stock's run.sh force-copies
its own onto every flash, which repairs that directory whether or not anything
was swept.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fn1k6kaHWhUrsmBfpxjUcG

* Ask the config-directory test about this installer, not the bake

test_flashforges_config_directory_is_never_written asserted that the mod's
filenames are absent from /usr/data/config. On a replica baked from a release
that still WROTE them, that is a question about the bake rather than about the
installer under test, and it fails for a reason the change cannot fix.

Remove them, run the installer over the top, then ask. The property is "an
install writes nothing there", and that is now what is measured.

Verified on the replica: qa/replica/test_upgrade.py is 18/18. The five
test_install.py assertions about the seeded directory still need a machine
baked from a package built with this branch -- work/out holds one from
2026-08-30, which symlinks into /usr/data/config as the old code did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fn1k6kaHWhUrsmBfpxjUcG

* Ask the config directory of a machine that has one

The five assertions about /usr/data/anvil-data/config could only pass on a
replica baked from a package built with this branch, and the bake installs
work/out/*.tgz -- whatever was built last. So they failed for a reason the
change cannot fix, and the gate could only go green after a full build, which
is exactly when nobody runs it.

Two different fixes, because the questions are two different kinds.

WHAT THE INSTALLED PACKAGE PRODUCED is still asked of the bake, but the
directory is now read off the machine instead of written down: PRINTER_CFG in
the installed etc/s6-rc/source/klipper/run is the command line klippy is
exec'd with, and therefore the only thing that decides which directory is
live. test_install.py and test_klippy_extras_import.py follow it, so the
include set, the graph walk and the user's printer.cfg are asked about
whichever side of the move the installed package was built on.

WHAT THIS CODE DOES is a new module, test_config_directory.py, which stages
anvil-link-prog.sh from the checkout and runs it -- the way test_upgrade.py
stages the installer. Only the timing is mocked: it seeds from a real
printer.cfg with FlashForge's real includes beside it, links the ff-*.cfg the
installed package really shipped, and reads MACHINE= out of the real
app_startup.sh. It asserts the seeding carries the machine's own marker, the
links point into $MODDIR, the chamber config matches the model, every include
resolves, a second run does not re-seed over an owner's edit, and the run adds
NOTHING to /usr/data/config.

That last one is a DELTA, not an absolute. A machine that has been through a
release which symlinked into FlashForge's directory still holds those links
and nothing can restore what they replaced. What this code owes is that it
adds nothing.

Verified on the replica: 135 passed, and the 10 failures are the same ones
master has on this bake -- ffmpeg, numpy, the timelapse component and the
hardware services, all missing from the 2026-08-30 package in work/out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fn1k6kaHWhUrsmBfpxjUcG

* Link every .cfg the packages ship, not a list of names

test_every_include_resolves_in_the_new_directory failed on CI:
printer.base.cfg has [include timelapse.cfg] and no timelapse.cfg was in
/usr/data/anvil-data/config, which Klipper treats as fatal.

anvil-link-prog.sh linked printer.base.cfg and the ff-*.cfg by name.
timelapse.cfg is anvil-timelapse's and matched neither, so it reached the
live directory only through the installer's copy of $MODDIR/config -- and
never at all on the `apk upgrade` path, where the link script is the only
thing that runs. A named list is a file that resolves on a fresh install
and goes stale the moment a package adds one.

So the rule is the glob: every .cfg in $MODDIR/config is linked, and the
installer skips every .cfg rather than the four names it knew. The .conf
beside them stay the installer's -- moonraker-custom.conf is the owner's
file to edit, so it is copied once and never linked to a package's copy.

test_the_mods_configs_are_links_into_moddir asks the same way now, so a
package whose config nothing includes is covered too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YULWHVHyxKm2HE9kiPfEjP

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 10:05:36 +03:00
Oleksandr Shyshatskyi fc137ec59d Prepare beta release documentation 2026-09-02 13:41:27 +03:00