32 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
Oleksandr Shyshatskyi fc137ec59d Prepare beta release documentation 2026-09-02 13:41:27 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 65a7528e09 The manual assumed a printer that arrives broken. It arrives calibrated.
The pages written on top of the old README inherited its premise: that after
flashing you face a gauntlet -- go/no-go checks, a safety net, a mandatory
calibration -- before the machine can be trusted with a model. That is not
what happens. The factory numbers import themselves on the first boot, a bed
mesh loads at the start of every print, and the mod is built to run the files
the stock firmware ran. A printer that printed before the flash prints after
it. "Your first print" now says so in its first line, and everything under it
is optional, ordered by what you would actually reach for: check the offsets
are applied, and calibrate only if that disagrees with the machine.

Three audiences now, because owner and contributor was not the real split.
"For experienced users" sits between them: How a print runs, and How
calibration works. Someone who knows Klipper and wants to change what happens
around their file has a place to go that is neither a walkthrough nor the
build system.

What was cut is most of what changed. Pages that were cryptic to the reader
they were aimed at -- Printing, Upgrading -- are out of the nav and kept on
disk to be rewritten rather than deleted. Within the pages that stayed:
Expected output, Probe sampling, What is being measured, the symptom table,
the dark-screen diagnosis, TOOL_Z_ADJUST, PLATE_CHECK, the ff-*.cfg
inventory, [ff_legacy] and SAVE_CONFIG in a paragraph about factory
calibration. Filenames and internals earn their place on an owner page only
when the reader has to type them or look at them; the rest moved to the
experienced pages or to Internals, where the audience wants them.

Calibration became a section rather than a page, because tool offsets are not
the only calibration: bed mesh has its own page now, and its own gotcha
written down -- BED_MESH_CALIBRATE writes `default`, a print loads
`MESH_DATA`, so a mesh probed by hand is a mesh no print will use. Input
shaping is described but has no procedure page yet.

Several claims came out because they were not true. "Is this for you" listed
calibration and input shaping as things the mod gives you; both are the
touchscreen's own sequences, recovered and reimplemented, and the notes say
so plainly. What the mod actually gives you is that anything speaking
Moonraker can drive the machine -- on stock, FlashForge's Klipper does not
toolchange at all, it raises a flag and waits for an application that is not
watching a Mainsail print, and the print hangs at the first T2. The page now
opens by telling most owners not to flash it.

Contributors are still deferred. Their pages are regrouped, not rewritten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3XZh4SqPdXAowVndz6tMr
2026-08-28 23:33:29 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 a6098c7f64 The README was the manual. It is a front door now, and the manual is nine pages.
Fourteen sections, install through troubleshooting, in the one file that
existed when there was nowhere else to put them. The site gave the manual a
home; this moves it there and orders it by what the reader is doing rather
than by which subsystem owns the answer. Someone whose printer will not boot
should not have to know that the symptom table lives in a file called
hardware-testing.md.

Three topics were documented two and three times over, and each copy was free
to drift: install in README and toolchange.md, calibration in README,
calibration.md and toolchange.md, slicer setup in README and toolchange.md,
"before you start" in all three. Each is now in one place.

The owner path is Is this for you -> Installing -> After the flash -> Your
first print -> Nozzle-offset calibration -> Printing -> Upgrading -> When
something goes wrong, with error codes, versions and how-it-works behind a
Reference group. The README keeps what a front door keeps: what this is, the
status checkboxes, what you give up, which file to download, and where to go
next.

toolchange.md was four documents in one 38 KB file. The owner half -- slicer
setup, filament, runout, the offset check before a first print -- is in
Printing and Your first print; the design notes, input shaper, roadmap,
HelixScreen and reverse-engineering notes stay, and the page moves to
Internals. hardware-testing.md was split down the middle by audience the same
way: the go/no-go checks and the symptom table were the owner's, and only
keeping the two verification files honest as the macros change is ours.

Two things this fixes rather than moves. The error-code table was
notes/70-error-codes.md, an internals note the README sent owners to; it is
docs/error-codes.md now, a page. And toolchange.md still opened "Not yet run
on hardware" while the README's checkboxes described a Creator 5 Pro that has
been printing for weeks -- the two pages have disagreed since the first
hardware run.

Contributors are deferred, as agreed: those pages are regrouped, not
rewritten. The nav says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3XZh4SqPdXAowVndz6tMr
2026-08-28 22:20:19 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 421e9ddc08 Everything you set on the screen survives the update
HelixScreen keeps every user setting inside its own install tree:
firmwareExe exports HELIX_DATA_DIR=$MODDIR/helixscreen and the binary
resolves its settings as config/settings.json relative to that root. The
installer treated that whole tree as ours to replace, so brightness, theme,
log level, touch calibration and spool assignments went away on every
update -- and settings.json carries "wizard_completed", so a configured
printer came back asking to be set up from scratch.

Two independent things destroyed them, which is why fixing either alone
would not have helped: the rm -rf that makes the installed set exactly the
shipped set took the directory, and the release tarball ships a seeded
config/settings.json that would have landed on top anyway.

The file list is HelixScreen's own -- HELIX_USER_CONFIG_FILES in the
install.sh the mod never runs, because it extracts the release tarball
directly. settings.json.backup rides along with it: Config::init falls back
to that backup when the live file has no config_version, so carrying the
settings forward without their backup would leave a rollback pointing at
factory defaults.

helixscreen.env is the one file where a release can carry something new --
the launcher sources it -- so the user's copy still wins, but the shipped
one is left beside it as .mod-new instead of vanishing.

Verified on the printer's own busybox against the real v0.99.115 config
files: with this change all of it survives, and the same harness on the
previous script reproduces the loss.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:32:41 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 a22f3b04ef Tool calibration speaks klipper-toolchanger's G-code
STATION_CALIBRATE is TOOL_LOCATE_SENSOR and TOOL_OFFSET_CALIBRATE is
TOOL_CALIBRATE_TOOL_OFFSET, with upstream's call shape as well as its
names: no TOOL= (the mounted tool is the one measured, so SELECT_TOOL
first, as upstream's macro does) and no PLATE_REMOVED=.

The renames are not just conformance. Upstream says "run
TOOL_LOCATE_SENSOR with tool 0" because tool 0's nozzle is the only
thing on their machine that can reach the pin -- their baseline is a
tool, so tool 0's own error rides into every other tool. Here the
carriage's probe element sits above the tool mount and reaches the
station directly with nothing installed, so the command means what it
says: locate the sensor, with no tool. That baseline cannot be spoiled
by a badly seated T0, which is what lets nozzle_x/y/z stay absolute and
per-tool independent.

PLATE_REMOVED=1 goes because the plate check that runs anyway is the
better guard: it parks the carriage and MEASURES whether the plate is
there, where the flag only asked the operator to promise. Dropping it
also removes the last non-upstream required parameter.

Losing TOOL= means losing TOOL=ALL, so CALIBRATE_TOOL_OFFSETS loops over
printer.toolchanger.tool_numbers -- the same shape as upstream's macro.
GRAB=/RELEASE= go with it: they cannot mean anything once the tool has
to be mounted already. The plate check still needs an empty carriage, so
the command parks the selected tool and picks it straight back up.

Probe sampling gets klipper-toolchanger's parameters -- SAMPLES,
SAMPLES_TOLERANCE, SAMPLES_TOLERANCE_RETRIES, SAMPLES_RESULT,
SAMPLE_RETRACT_DIST, PROBE_SPEED -- on both commands and in
[ff_tool_offset]. The fork's e_stop ALREADY samples (sub_cycle_cnt
touches, spread rejected above error_v, retried main_cycle_cnt times,
averaged); what was missing was reaching those numbers from a command
and a median. So _estop runs that loop itself over the fork's
single-touch _probe, every default is read off the [e_stop <axis>]
object, and an unparameterised command probes exactly as it did before.
Spread is judged after each touch, as the fork does, so a round already
known bad is not finished. Two things the fork does not bound are
bounded here: the 9999 move-failure sentinel it spins on forever, and an
e_stop with no _probe, which falls back to run_probe as one sample
rather than cubing the touch count.

Also fixes _plate_check reaching for z_trigger where it had bound
station_z -- a NameError left by the rename sweep in 4704ca0, on the one
path that runs before a nozzle is allowed to descend, and now the only
guard that the plate is off.

docs/calibration.md is the operator's side of all of it: the order, what
each command prints, and every refusal it can raise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 13:49:14 +03:00
Oleksandr ShyshatskyiandClaude Fable 5 37f39175c1 The README trims what nobody reads twice
The camera and the model gate were each explained in two places; the
calibration steps verified themselves twice; a handful of why-asides
duplicated what docs/ already says at length. Every command, port, filename
and warning stays -- the explanations now live once, where they land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:58:23 +03:00
Oleksandr ShyshatskyiandClaude Fable 5 ba1dfbf10c The Status checklist sorts by who is asking
Everything a printing day touches first -- flash, print, toolchanges,
calibration, Mainsail, camera, wifi, the screen, and the three empty boxes
-- then a "For the tinkerer" tail for ssh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:56:24 +03:00
Oleksandr ShyshatskyiandClaude Fable 5 4f5a5c0c55 The README knows where the people are
A Discord invite in the two places a user looks for one: the intro, and the
end of "If something goes wrong".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:54:37 +03:00
Oleksandr ShyshatskyiandClaude Fable 5 f86e995703 The README leads with a name: Reforge
Just the header -- the repo, the Makefile banner and the anvil-* artifacts
keep their names; what changed is that the project now has one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:54:05 +03:00
Oleksandr ShyshatskyiandClaude Fable 5 05dd2571d7 The Versions table links to what it names
Each component now points at its project, and each pin at the exact thing it
pins: the creator5 branch of the Klipper fork, Mainsail's and HelixScreen's
release tags, Moonraker's commit. The HelixScreen links follow the org
rename to Klipper4FlashForge -- the old Klipper4Creator5 URL was a redirect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:49:37 +03:00
Oleksandr ShyshatskyiandClaude Fable 5 0bfbda71d7 Status becomes the checklist it was trying to be
"Restores every file the mod replaced -- with one exception" contradicted
itself: Moonraker is a file the mod replaced. Both places now say what is
true -- the stock package restores everything it CARRIES, and Moonraker is
not in it (checked against the real package: no component ships it), so the
mod's build stays and keeps working.

Status and What-you-get said overlapping things in two formats; they are now
one checklist under Status, a box per feature, checked when it has happened
on the real Creator 5 Pro and empty when it has not -- the chamber heater,
the non-Pro model, and going back to stock (replica-proven, not yet needed
on the machine).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:47:53 +03:00
Oleksandr ShyshatskyiandClaude Fable 5 20a0748b6a The README says it: this runs on a real printer now
Status rewritten from the first session on a Creator 5 Pro -- flash and
migration from stock, printing with unedited slicer gcode, toolchanges,
nozzle-offset calibration, wifi, Mainsail and the camera all checked on the
machine; the chamber heater and the non-Pro model still have not touched
hardware. Two photos of HelixScreen doing real work sit where the disclaimer
used to insist nothing had.

Trimmed while there: the release-naming note stays on the releases
themselves, anvil.conf leaves the README (ssh users find it in the docs),
and "If something goes wrong" is now three sentences and two links -- the
stock packages at ghzserg/FF are the whole answer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 00:09:12 +03:00
Oleksandr ShyshatskyiandClaude Fable 5 64ea5a84a9 The first boot imports the factory calibration and keeps it
auto_import already loaded firmwareExe's per-unit numbers whenever no tool
had a saved nozzle position -- but only live, so every boot re-imported,
TOOL_OFFSET_STATUS nagged about an unsaved calibration, and the SAVE_CONFIG
was homework for the user. Now the first successful import persists itself:
ff_legacy schedules SAVE_CONFIG one reactor callback after klippy:ready, the
restart that save causes lands seconds into the first boot -- before
HelixScreen is anywhere near its setup wizard -- and the saved nozzle
positions are exactly what stops the import from ever running again.

Guarded on three sides: nothing is saved when something else was already
staged before the import (not ours to commit), when the JSON put no tool on
its feet (a restart with nothing to keep), or when auto_save is off in
[ff_legacy], which restores the old live-but-unsaved contract. A failed save
never restarted klippy, so there is no loop behind any of it.

The extras finally get a klippy-free unit lane:
test/integration/test_ff_legacy.py stubs the four objects ff_legacy touches
and pins the once-ness, both guards, and the failure path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 23:06:03 +03:00
Oleksandr ShyshatskyiandClaude Fable 5 e36219cf40 The README now reads in the order a user flashes
Status and the download link first, the flash steps next, and then the three
sections that were missing entirely: what is where after the flash, the
calibration that must happen before a first print, and which files are the
user's versus the mod's. The build internals the top of the file used to
carry -- the firmwareExe integration point, the Moonraker irreversibility
story -- were already told in full in docs/how-it-works.md, so the README
links instead of repeating them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:58:43 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 e62b300f68 Say what the release actually does about the root password
No ROOT_PW_HASH secret is set, on purpose: a hash baked into a public
package is one anyone can extract and brute-force offline, and the
random-per-install path already exists and is what a local build does.
README claimed the opposite for official releases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 20:54:22 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 ac558f6686 Document the Moonraker swap honestly, and finish its doc coverage
The reversibility claim was the strongest promise in the README and a40bc86
quietly invalidated it. Moonraker is not in the update package -- it ships only
on the factory image -- and run-append.sh deletes the stock tree it moved aside
once the copy succeeds. So a stock reflash cannot restore it and nothing local
can either. README and how-it-works now say so plainly, with BUILD_MOONRAKER=0
as the way to opt out and the ssh one-liner that repairs an interrupted swap.
Per the decision taken, the swap itself is left as it is.

Two claims that rested on it are corrected rather than repeated:
bin/common.sh no longer offers a stock reflash as a whole uninstall, and
testing.md's test-recovery row says which three files it actually compares
instead of "back to stock byte-for-byte".

Moonraker was also missing from eight doc surfaces that still described a
two-asset world: the pipeline box, the third-party section and its bump recipe,
MOONRAKER_TGZ as a local-copy override, the versions.env line in the layout
tree, `make vendor`'s help and the comment above it, config.env.example, the CI
step name, and printer-replica's "genuine from the factory image" list.

Other cross-document contradictions found this pass:

* versions.env carried the same wrong bump recipe that was fixed in
  building.md last pass -- it needs the sha256 set to SKIP first.
* docs/building.md said `make build` is "all four" while listing five scripts;
  build.sh does not run verify.sh.
* toolchange.md's image resolved to docs/docs/molten-toolhead.jpg -- broken on
  GitHub -- and still said ff-legacy.cfg is included "only for the import step,
  then remove", which printer.base.cfg contradicts.
* hardware-testing.md stated the random root password unconditionally, in the
  one document a person reads while standing at the machine.
* notes 30 and 00 kept claims their siblings now contradict (servo_min/max,
  motor_value), and 47 pointed at a note that does not exist.
* "nozzle-to-eddy-trigger gap" survived in two places after being corrected in
  a third; the station is not the eddy coil.
* ff-toolchange.cfg's restore_axis note had the hazard backwards -- parking
  zeroes X and Y, not Z -- and called `xy` the default when the option is unset.

54 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 19:48:45 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 68afe65485 Fix two features that silently did nothing, and give Moonraker a user seam
Both bugs were found by auditing a40bc86, which the first pass never saw.

1. The plain Creator 5 could not print at all. ff-filament.cfg subscripted
   printer["heater_generic chamber_heater"] unguarded, and that object exists
   only on the Pro. Klipper renders a macro's whole template before executing
   any of it, so _FF_FILAMENT_PREP/_FF_FILAMENT_FINISH raised on a non-Pro --
   taking LOAD_FILAMENT, UNLOAD_FILAMENT, PURGE, _FF_NOZZLE_CLEAN and
   therefore START_PRINT and every Moonraker print with them. Guarded with the
   `in printer` idiom ff-chamber.cfg already used.

   test_chamber.py gains a static gate over every ff-*.cfg macro. Proven to
   catch it: reverted the guards and the test fails naming both macros.

2. The shipped moonraker.conf could never install. /usr/data/config/
   moonraker.conf is ON the factory image -- verified by probing the replica --
   so the compare-and-.mod-new rule saw an existing file with no snapshot to
   compare it against and parked ours as .mod-new, on the first install and on
   every one after. The [webcam] entry therefore never reached the printer and
   Mainsail still showed no camera, which is the entire reason a40bc86 exists.
   The API lockdown never applied either.

   /usr/prog/klipper/runConfig/moonraker.conf is FlashForge's pristine
   template and is byte-identical to the live copy on an untouched machine
   (both 371 bytes, md5 a49bd5ea...), so that is now the test for "nobody has
   edited this": matching it means the file is ours to write.

   case-install.sh asserts the LIVE file has a [webcam] block, not that a
   .mod-new appeared. It reproduced the bug before the fix.

3. moonraker.conf is mod-owned and overwritten every update, so it needed the
   seam printer.cfg gives the Klipper configs. New moonraker-custom.conf:
   created once, never written again, and included at the END of
   moonraker.conf -- Moonraker applies options in read order, so the user's
   win. Moonraker treats an include matching no file as fatal, so the file is
   guaranteed to exist and both it and its header say not to delete it.

   case-install.sh writes a trusted_clients block into it before boot 2 and
   asserts it is byte-identical afterwards, so "survives an update" is tested
   rather than asserted.

Verified on the printer replica through all three boots: the config lands, the
seam is created and included, a user edit survives the re-install, and
Moonraker still starts -- which is what proves the include parses.

53 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 19:48:44 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 d05565284d Sections E, F, G: wrong values, stale claims, overclaims
E -- values and counts. The START_PRINT prologue has no M400 and an idle
timeout of 864000, not 1800000. The nozzle-clean temperature comes from the
material table FIRST, so NOZZLE=240 with no per-tool temp cleans at 220 --
documented backwards before. TOOLS is the paired 0:220,2:240 form. The wipe Z
is the raw eddy frame, not the homed one. The runout M117 strings are
"T<n> clog" / "T<n> out of filament", not E-codes. Grab-verify raises
E0051+tool, not E0143. Docks are at X~297; 250/280 are staging waypoints. The
grab sequence is documented in the order _grab actually emits it. ff_print
exposes seven status fields, not nine, and never emits TOOLS=. Package sizes
reconciled against the real artifacts (62MB built, ~93MB stock, ~370MB of CI
assets, ~60s not 90s). testing.md's sample output block matched no header the
code prints, so it is replaced with the real ones; its pytest row listed two
of six suites.

F -- stale claims. _FF_PREFLIGHT is not "missing". SDCARD_SET_CHANNEL is not
still issued (764af2a). The app and Moonraker do not coexist; the app is gone.
TOOLCHANGE_STATUS has nothing to compare against since the JSON reader was
removed. The JSON is re-imported every boot until a nozzle is saved, not once.
The 23-check render harness was deleted in 91604c2. PURGE now wipes by
default. 50-print-lifecycle.md gained the section on how the sequences are
actually entered now -- [ff_print], not the slicer. The dead "profiles"
concept is gone from three files, one of which printed it on every replica run.

G -- overclaims. The README no longer says a stock reflash puts the printer
back "exactly as it was": it says what is restored and what survives inert,
which is what test-recovery actually asserts. The random ssh password is
conditional on ROOT_PW_HASH being unset. verify.sh no longer warns "ssh will
not start" on every build for a dropbear we deliberately do not ship -- the
check now reads the way round it means. case-install.sh's read-only block says
it asserts nothing. An uncalibrated tool contributes z_adjust, not zero, in
three operator-facing places. offset_base does not cancel out in X/Y. The
safe-moves file's Z50 promise is scoped to its own lines, with the implicit
prepare's G1 Z10 spelled out.

48 passed, 5 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 19:48:43 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 b2c651b11f Fix the runout insert path and TOOL=ALL staging; drop SAFE-MODE for MOD_UI
A2: _FF_INSERT's body was entirely inside {% if kind == 'switch' %}, yet
insert_gcode is wired on all four fm_ex* with KIND=motion. A motion insert --
which is what clearing a clog looks like, filament moving again -- produced no
output at all, on exactly the path where the operator most needs the next
step. Adds the motion branch the comment already described.

A3: TOOL_OFFSET_CALIBRATE staged each tool with set_nozzle() inside the loop,
so a gap-check failure on a later tool left earlier tools pending for
SAVE_CONFIG -- while raising an error that says "nothing saved". True for
TOOL=<n>, false for TOOL=ALL, and told to the operator at the moment they
decide whether to save. Staging now happens after every tool has passed.

A8: SAFE-MODE is removed rather than repaired. It never worked: the settle
check cleared the counter 90s after every boot on a condition
(`[ -s $CHOICE ]`) that cannot be false, since $CHOICE is written two lines
earlier and "none" is not empty. Nothing checked whether helix-screen was
alive, so the latch only fired on three reboots inside 90 seconds -- not the
crash-loop it was written for.

In its place, MOD_UI in anvil.conf, alongside MOD_WEB/MOD_CAM/MOD_SSH/MOD_WIFI
and templated by patch.sh the same way. It stops the UI being started; it does
not pretend to repair one. The docs said `touch SAFE-MODE` was how you recover
a blank screen. Nothing recovers a blank screen on the machine: HelixScreen is
the only UI, and getting one back means flashing a package. Said plainly now,
in the README, the recovery section and the troubleshooting table -- along with
what is genuinely unaffected, which is ssh, Mainsail, Klipper and printing.

48 passed, 5 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 19:48:42 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 a40bc861dd Ship our own Moonraker: the stock 2022 build hides the camera
Mainsail has shown no webcam panel on this printer no matter how the
stream is configured, and the stream was never the problem: nginx serves
/webcam/ correctly and mjpg-streamer answers it.

FlashForge's Moonraker is a 2022 build (API 1.0.5). Moonraker only grew
the webcam "enabled" flag in April 2023, and Mainsail filters its webcam
list on exactly that field -- so every [webcam] entry is discarded and
the panel is removed from the dashboard entirely. Nothing on either side
of that can be configured around; the server has to be newer.

So the mod now ships Moonraker the way it ships Mainsail: pinned in
versions.env, fetched to vendor/ against a sha256, installed over the
stock tree. v0.9.3 is the newest release that runs on the printer's
existing python environment untouched -- v0.10.0 wants dbus-fast, a
compiled module that is not there and would have to be cross-compiled
for mipsel. Only the python package is replaced; the interpreter, the
moonraker-env beside it and moonrakerDaemon stay FlashForge's.

It rides in the mod payload rather than the software component because
the stock run.sh does not extract that component over /usr/prog -- it
copies a hand-written list of paths out of it, and a moonraker/ dropped
in beside them would never be looked at. run-append.sh installs it,
moving the old tree aside and putting it back if the copy fails: this is
the one thing the mod writes to /usr/prog that the stock installer does
not, and a printer whose Moonraker did not survive has no web UI at all.

The database migrates itself -- v0.9.x moved from lmdb to sqlite, lmdb
1.3.0 is installed, so Mainsail's settings survive the upgrade.

Covered end to end on the printer replica: that the swap happened, that
the installed webcam.py has the enabled field, and that the shipped tree
really runs on the printer's own python3.8 under qemu-mipsel. With
BUILD_MOONRAKER=0 those skip and the stock server is left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 17:42:34 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 401c2ecf35 The ff-*.cfg are ours: overwrite them, and say so at the top
Editing a shipped config was the wrong seam, and the installer was
letting people do it. An edited ff-*.cfg was KEPT and the new version
left beside it as .mod-new, so a machine could sit for releases on a
macro set nobody else was running -- the failure mode being a bug report
about code that had been replaced twice since.

Klipper already offers the better seam. It parses with
RawConfigParser(strict=False): same-named sections MERGE, the last value
of an option wins, and a redefined [gcode_macro] replaces the original.
So an override in printer.cfg restates only what it changes, keeps every
shipped value it does not mention, and survives every flash. printer.cfg
is the one config nothing ever writes.

So run-append.sh now overwrites ff-*.cfg unconditionally, logging a line
when it clobbered something so the log explains where an edit went, and
each of those files opens with a header saying it will be overwritten
and where to put changes instead.

printer.base.cfg needed no installer rule: FlashForge's own run.sh:135
does `cp $WORK_DIR/klipper/config/* /usr/data/config/ -rf` on every
flash, no comparison and no .mod-new, so it has always worked this way.
It gets the header anyway, because "why did my edit vanish" is the same
question there -- and the stock-drift comparison in bin/unpack.sh strips
comment lines, so the banner does not trip that warning.

moonraker.conf deliberately keeps the .mod-new dance. It is not a
Klipper config and has no include-and-override seam, so the argument
above does not apply to it, and a printer reached through a tuned
trusted_clients or cors_domains block would lose that access on an
update.

test_config_ownership.py holds the parts together: every mod-owned
config must carry the banner near the top, the banner must name
printer.cfg, moonraker.conf must NOT carry it, and run-append.sh must
still contain the overwrite branch the banner promises. A banner that
outlives the behaviour is worse than none -- the user trusts it, puts
nothing in printer.cfg, and their edit quietly wins.

Rebased onto 9b1f944, which ships the includes inside printer.base.cfg.
Two things followed from it: the banner there now cites run.sh rather
than the software component, since that commit establishes the live copy
in /usr/data/config as the one that matters; and the paragraph in
docs/hardware-testing.md telling the operator to add seven [include]
lines by hand was left stale by it and is rewritten here.

make test: 12 passed, 0 failed, 0 skipped, replica gates included --
boot -> install -> re-install -> boot exercises the overwrite path, and
both "user printer.cfg preserved through both flashes" and
"printer.base.cfg restored" still pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 15:59:41 +03:00
Oleksandr Shyshatskyi 12bbd1e7dc Retire the probe profile: there is one package now
The probe profile was a bring-up aid. It set BUILD_APPLY=0, reinstalled
the stock component byte-for-byte and wrote a report to the stick, so you
could prove the update chain worked on your own machine -- the filename
glob, the DES3 key, the md5 gate, run.sh execution -- before flashing
anything that changed a file. That rehearsal has served its purpose.

With it gone there was exactly one profile left, so the whole PROFILE
indirection goes too: profiles/ is deleted and its six BUILD_* flags are
plain defaults in bin/common.sh, which also means config.env now
overrides them instead of being silently overridden. make default folds
into make build; all-profiles and RECOVERY_PROFILE are gone with it.

What is NOT gone is the undo button: flashing the stock FlashForge
package still restores every file the mod touches, and case-recovery.sh
still proves it. What is gone is the dry run.

Note the loss: PROG_MB/DATA_MB used to come out of the probe report.
The docs now point at df -h over ssh, which you only have after a
successful flash.
2026-08-23 22:15:26 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 607e6595fb Start the camera ourselves: mjpg-streamer was on the disk all along
Correcting the previous commit, which said the :8080 stream could not be
restored without cross-compiling a streamer. Both halves are actually true:

  * firmwareExe really did serve the stream itself -- its own accept loop,
    `GET /?action=stream`, boundarydonotcross, even announcing
    `Server: MJPG-Streamer/0.2` -- so killing it does take the camera down.
  * ...but /usr/prog/mjpg-streamer/ ships the genuine article: mjpg_streamer,
    input_uvc.so, output_http.so, its own libjpeg.so.9 and www/.
    app_startup.sh:159 exports LD_LIBRARY_PATH for it and then never launches
    it. Nothing needs building; the binary just needs starting.

S65camera does that, on the S50wifi pattern -- the wait for /dev/video0 and the
respawn loop run in a background subshell, so a printer with no camera plugged
in never holds up Klipper or the UI. stop() kills the supervisor before the
streamer, or it would come back three seconds later.

Two details are load-bearing and easy to lose: libjpeg.so.9 lives in the
mjpg-streamer directory, so without LD_LIBRARY_PATH the binary dies on a
missing shared object (the same trap moonrakerDaemon has in S60web); and the
plugin .so names resolve against the working directory, hence the cd.

nginx proxies it at /webcam/ with buffering off and no read timeout, since
MJPEG is one endless response, and moonraker.conf's [webcam] entry uses
relative URLs through that proxy -- so one forwarded port carries the stream,
tunnels included. MOD_CAM=0 turns it off; MOD_CAM_RES/FPS/PORT tune it.

The earlier claim is corrected in docs/notes/25 and in the README, which no
longer lists the camera as something you give up.

Verified: parses under the printer's own busybox ash, lint-danger clean. The
full suite was not re-run to completion -- the working tree is being edited
concurrently and a build lane raced it (tar: file changed as we read it).
bin/patch.sh's MOD_CAM line is deliberately left out of this commit for the
same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 22:06:25 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 5bdf78109a Retire firmwareExe: own the web stack, and say what goes with it
The mod replaces FlashForge's application outright, so the reasoning that
still framed the web stack as coexisting with it was wrong on its own terms.

* nginx and moonraker are documented as the only web stack on the printer.
  MOD_WEB=0 is a debugging switch that leaves ssh and the touchscreen, not a
  way back to stock -- that is a stock FlashForge package. The own-prefix rule
  stays, for the reason that is actually true: /usr/prog is the firmware
  partition and every FlashForge package reinstalls it, so a config written
  there is undone by the next stock flash.

* Configs the mod ships now update when you have not edited them. A live file
  still byte-identical to the one the last package wrote is replaced; one you
  changed is kept and the new version lands beside it as .mod-new.
  $MODDIR/config-installed holds that last-written copy. Without this a config
  we own could only ever be written once: it exists on the second flash, so it
  would land as .mod-new forever and updates would never reach the printer.

* Dropped the three dead /server/files/ alias blocks from nginx.conf. The regex
  location above them matches /server/ too and nginx gives regex locations
  precedence, so they never fired -- and had they fired, serving those paths
  from disk would 405 the DELETE and POST that Mainsail sends to the very same
  URLs.

* Recorded what dies with firmwareExe, in docs/notes/25 and in the README: the
  :8898 REST API (FlashPrint, Orca's FlashForge profile, the mobile app) and
  the :8080 camera stream. The camera is the one that looks restorable and is
  not -- the MJPEG server is compiled into firmwareExe (its own accept loop,
  `GET /?action=stream`, boundarydonotcross) rather than being mjpg-streamer,
  so there is no daemon left to point a [webcam] at. assets/moonraker.conf
  carries the block, commented, for when a streamer is built for this board.

Also in this change: the MOD_UI switch and the stock-UI path are gone, the
chamber-heater model gate arrives with ff-model.cfg/ff-chamber.cfg, and the
tests follow.

make test: 32 passed, 1 failed. The failure is the probe end-to-end, which
dies at "mount: failed to setup loop device for /stick.img" -- leaked loop
devices on this build host, not the package. Probe installs nothing, and the
default profile's install -> re-install -> boot passes in the same run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 21:50:32 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 a9299397f4 Build the toolchanger from this repo instead of a TOOLCHANGE= path
bin/patch.sh now copies the ff_*.py extras, the ff-*.cfg configs and the
HelixScreen printer-database entry out of payload/, the way it already
handled report.sh and the nginx/moonraker assets.

Both call sites also lose a silent-skip guard. They tested for the files
under $TOOLCHANGE and quietly shipped a package without the toolchanger
if the path was wrong or unset -- worth having when the source was a
checkout that might not be there, wrong now that the files are committed
next to the script. A missing file here is a broken checkout, and cp
should say so.

CI and the test fixture had TOOLCHANGE="" set precisely to trigger that
skip, so drop the line; both build the probe profile, which never
reaches this step anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgNhSa1Qvwvxb2nQXT6b1Y
2026-08-23 16:50:44 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 22d16f984e Run the printer's own boot script for the update test, and cut the suite to 5m
The install test used to re-implement app_startup.sh: it pulled the glob,
MACHINE and PID out of it with sed, then called the installer itself. A
mistake in our reading of the boot script could never be caught that way.

The package now sits on a real FAT filesystem on a loop device exposed as
/dev/sda1, and app_startup.sh runs verbatim through three boots -- stick in,
stick still in, stick pulled. The last one asserts what actually matters:
nginx and moonraker up, klipper launched, the UI in the foreground where
app_startup.sh's own 5-second ps watchdog can see it.

One substitution, declared in .SIMULATED: the printer's kernel has nls_cp936
and a container kernel does not, so /bin/mount retries without codepage= if
the real options are rejected.

Speed. The image carried the partitions but not the stock install, and
test-env.sh sourced the config files after the environment -- so PRINTER_IMAGE
on the command line was overwritten by the empty value in test.env and the
published image was never used. Measured, per replica case: 22s to unpack the
factory image, 37s to install the stock package, 0.7s with the image. The
stock install is now baked in at image build time (bake.sh -- it needs binfmt
and chroot, so it runs privileged and the result is committed) and recorded in
/usr/prog/.BASELINE; a run asking for a different package reinstalls.

make test: ~20m -> 5m26s. Every section header now carries elapsed seconds.

Dropped four checks that could not fail:

  * test-abi's execution half was `qemu "$f" -h || [ $? -lt 126 ]`, which
    accepts nearly every exit status
  * test-ash's md5sum -s probe printed "inconclusive" on every path
  * run-tests.sh's per-file `sh -n` "POSIX" loop ran bash, and its bashism
    grep flagged `local`, which busybox ash supports
  * CI's shellcheck ended in `|| true`

And one that mattered more: the release workflow's final sim-install loop
ignored every exit status, and sim-install.sh exits 0 on a skip -- so the last
gate before publishing could not fail. It now stops on the first failure and
runs with REQUIRE_PRINTER_SIM=1, which turns skips into failures throughout.

Also: the profile lists in run-tests.sh, RECOVERY_PROFILE and release.yml
still said ssh/web/full/helix and would have failed the release job; work/.sim
was shared, so two suites at once truncated each other's packages; and the
fixture half left a 380KB package in work/out where make test-install picks it
up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgNhSa1Qvwvxb2nQXT6b1Y
2026-08-23 16:36:45 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 a5300bde12 Split the repo into ship / build / test lanes, and add the printer replica
Two strands, both landing here because they had grown into each other in the
working tree.

The printer replica: the printer's real rootfs.squashfs chrooted under
qemu-mipsel, with /usr/prog and /usr/data from the factory image, so a package
is installed by FlashForge's own updater running on the printer's own busybox,
tar, md5sum and unTar. There are no stubs -- if the real prog partition is
missing the harness refuses to run rather than test against mocks. A release
workflow is blocked on it.

The lanes: a file's directory now says whether it can ever reach a printer.

  payload/, assets/   ship
  bin/, profiles/     build the package
  test/, docs/        neither, ever

  * the replica's own infrastructure moved out of bin/ and docker/ and in
    beside the harness it belongs to (test/extract-rootfs.sh,
    test/build-printer-image.sh, test/printer/Dockerfile{,.full})
  * the Makefile has two runners: only RUNSIM gets the docker socket and the
    replica's variables, so a build cannot reach the docker daemon at all
  * replica settings moved to test.env, which nothing in a package reads
  * build-time flags renamed BUILD_* (APPLY, REPORT, KLIPPER, TOOLCHANGE,
    MAINSAIL, HELIX). MOD_* now means only the runtime switches written into
    anvil.conf and re-read by the printer at every boot
  * verify.sh fails if any file in a package is byte-identical to one in
    bin/, test/, docker/ or profiles/ -- by content, because a name blacklist
    would trip over whatever Mainsail happens to call common.sh

Fixed while mapping it:

  * BUILD_REPORT was read only inside the BUILD_APPLY=0 branch, so four
    profiles declared it and got nothing. It is honoured on both paths now,
    and off outside probe: report.sh copies /etc, passwd and shadow onto the
    USB stick, which is a diagnostic, not something to hand out.
  * three checks in the brick lint had been quietly dead. Two were guarded on
    a payload/boot.sh replaced by the firmwareExe wrapper long ago; the third
    was the file glob itself, which matched neither firmwareExe nor the
    init.d/S* scripts -- the UI wrapper and every service went unscanned. A
    missing file is now a failure rather than a silent pass.
  * run-tests.sh overwrote config.env and copied it back, leaving the config
    you edited one crashed run away from being a fixture. It uses CONFIG_ENV.
  * bin/make-uninstall.sh does not exist and never did; README, run-pre.sh and
    full.env all pointed at it. Recovery is the stock package, which is what
    test-recovery actually proves.

README is now for someone who wants to flash their printer: what it is, which
file, how to flash, what to do when it goes wrong. The rest moved to
docs/how-it-works.md, docs/building.md and docs/testing.md.

make test: 89 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:30:09 +03:00
Oleksandr Shyshatskyi 855bb52573 Build a separate package per model; add the model-gate test
The Creator5Pro stock package turned up, and comparing it with the non-Pro
one settled the question: they are different files carrying DIFFERENT
firmwareExe binaries (f666ff60... vs bf505905...), each with its own
MACHINE=/PID= gate.

That exposed a real bug: pack.sh emitted BOTH filenames from a single build,
copying one file to the other model's name. It was modelled on FlashForge's
factory package, where doing so is legitimate because that package is an
installer-only wrapper with no model-specific binaries. For a full firmware
package it would hand one model the other's firmware.

pack.sh now names the output from the gate inside the package it was built
from, and refuses if that disagrees with TARGET_MACHINE. config.env takes one
stock package per model (STOCK_TGZ_CREATOR5 / STOCK_TGZ_CREATOR5PRO) and
MODEL=/TARGET_MACHINE selects between them. `make release` builds both into
dist/.

test/test-model-gate.sh locks it in: filename prefix matches the gate, the PID
matches the model, and the two packages do not share a firmwareExe.

Also fixed: the CI fixture now carries a real model gate (and is generated
into work/ rather than /tmp, because sibling-container mounts are resolved by
the host daemon, so a build-container /tmp path mounts empty).

77 checks pass. Both models build and verify from the real firmware.
2026-08-22 22:27:55 +03:00
Oleksandr Shyshatskyi 3a70b678f4 Recovery is the stock package; add the model gate check
Drop bin/make-uninstall.sh. Diffing its output against FlashForge's own
software component showed it differed in exactly two files -- run.sh (22
appended lines) and the md5sum.list that changed because of it. Everything
doing the actual reverting was FlashForge's. So recovery is simply "flash the
stock package you already have"; the leftover /usr/data/mod payload is inert
once firmwareExe is the genuine binary again.

test/sim-roundtrip.sh now proves that path instead: install the mod, flash the
stock package, assert firmwareExe matches the stock binary byte-for-byte, the
boot scripts are restored, and no stock script references the payload.

That test immediately surfaced a real problem. Packages carry a model gate --
runFirmwareExe.sh has MACHINE=/PID= baked in and refuses to install when they
do not match what app_startup.sh passes from the installed firmware:

    Creator5    / 0028
    Creator5Pro / 0029

The stock package in use here is the non-Pro build (MACHINE=Creator5), while
the target printer reports "Creator 5 Pro" in general.json. Anything built
from it would be refused by the printer, and it is not a valid recovery image
for a Pro either.

unpack.sh now reports the gate, verify.sh fails on a mismatch against
TARGET_MACHINE in config.env, and the ladder doc leads with checking it.
2026-08-22 22:11:53 +03:00
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
Oleksandr Shyshatskyi 4415905fb3 Initial commit: USB-installable custom firmware builder for Creator 5
Builds a modified FlashForge update package that the printer installs by
itself from a USB stick, reusing FlashForge's own updater rather than a
parallel one.

Integration is a single file: /usr/prog/PROGRAM/software/firmwareExe is
replaced with a shell script. Everything funnels through that binary, so
app_startup.sh, rcS and the init chain stay completely stock. The genuine
binary is kept as firmwareExe.stock and remains the default UI.

Key findings from the stock firmware, each enforced by a test:
- components named *.tar.xz are PLAIN tar; the installer runs bare `tar -xvf`
- the .tgz is DES3-encrypted plain tar, not gzip
- nothing in init starts Klipper -- firmwareExe runs start.sh, so replacing
  the UI without S70klipper leaves a printer that cannot move
- the installer wipes the software dir before run.sh, so an uninstall package
  must ship the genuine firmwareExe rather than back one up
- ssh already works: stock ships dropbear + an enabled S50dropbear, only the
  root password is missing

Brick-safety is designed in, not bolted on: a five-rung profile ladder
starting with a probe that changes nothing, a container-based simulation that
actually runs the installer and asserts the printer would still boot, an
uninstall round-trip test, and a SAFE-MODE latch for UI crash loops.

75 checks pass. Not yet run on hardware.
2026-08-22 21:48:56 +03:00