mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-19 03:49:50 -05:00
master
76
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
df57d2795e |
Remove the shared-stepper adapter
ff_extruder.py was taken from #29 but never enabled. The fork already accepts the four [extruderN] sections' repeated pins through duplicate_pin_override, each extruder keeps its own stepper and pressure advance, and no failure on a stock config has been shown. It can come back if one is. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
308d97ef21 |
Add optional Z-hop and in-dock retract to the return after a tool change
With restore_axis naming X or Y, a real tool change can now raise the old tool before it crosses the part to its dock, retract the new tool while it is still seated in its dock, travel back raised, descend, and give back part of the retract at the target. The options are restore_z_hop, restore_retract and restore_unretract with their feeds; all are zero as shipped. From Niels Sedat's #29 without the prime-tower registration, the model-skip rule, the shared-stepper sync or the stats hooks. Co-authored-by: Niels Sedat <nlssdt@gmail.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
7e8ea9dbf4 |
Drop the next-tool preheat from ADAPTIVE_MESH
ADAPTIVE_MESH no longer starts heating the file's second tool, and ff_print no longer reports next_tool and next_nozzle. Orca's own preheat M104 stays the only thing that heats a tool ahead of its change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
bb84772a75 |
Take the mesh, print-start and shared-stepper pieces of #29
From Niels Sedat's Creator 5 branch, the parts that stand without the rest of it: - ff_bed_mesh: mesh probing lifts 2 mm above the last trigger instead of to one fixed height. Active through [ff_bed_mesh] in ff-print-macros.cfg. - ADAPTIVE_MESH, ADAPTIVE_MESH_TOGGLE and DEFINE_PRIME_TOWER_OBJECT, with ff_print reading the active plate's prime tower. Used only by a profile that calls them and sets FF_BEFORE_PRINT_START.defer_mesh. - START_PRINT heats the bed before the first homing. ADAPTIVE_MESH preheats the second tool when its first M109 is near the start of the file. - ff_extruder, the shared-stepper adapter. Shipped inert: no [ff_extruder] section is configured. Left out on purpose: the used-tools list and the whole-file scans that fed it, the ff_stats hooks, the plate and material offsets, and the toolchange travel changes. The next-tool search reads only the head of the file, so a second colour far into a print is not heated from the start of the job. Co-authored-by: Niels Sedat <nlssdt@gmail.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
3d9f19391e |
Clear active bed mesh before parking with Z unhomed (#32)
* klipper: clear active mesh before parking with Z unhomed * Make the park status and mesh tests agree The unhomed-Z mesh guard reads printer and reactor, which the status test's park stub lacks, and the status handling reads `changing`, which the mesh test's stub lacks. Give each stub what the other change needs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Oleksandr Shyshatskyi <shalal545@gmail.com> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
01892ff6f1 | Fix toolchanger status while parking (#31) | ||
|
|
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. |
||
|
|
4f6bd1ee1c | Pin HelixScreen v0.99.118 for Creator 5 | ||
|
|
65a1d9dc28 |
Do not home XY twice when tool is mounted
Parks tool after homing XY, then homes only Z axis, this saves time on useless movements. Thanks to pappicio0000. |
||
|
|
bbc57db25c |
Ship Klipper examples and docs for Moonraker (#24)
Include config examples and reference documentation from the pinned Klipper source so Moonraker can register both file roots. Exclude upstream documentation website tooling, which requires Bash and does not belong on the printer. |
||
|
|
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>
|
||
|
|
5f237eff62 |
Version the pinned recipes on the release, not on memory (#21)
apk decides whether to upgrade from the version string alone. Three recipes
pin an upstream version -- anvil-moonraker on ${MOONRAKER_VERSION#v},
anvil-klipper on the pinned commit, anvil-helixscreen on its tag -- and each
ships a payload/ of ours beside that upstream tree. Our half changes without
the string moving, so the feed serves new bytes under the name every printer
already holds and `apk upgrade` finds nothing to do. #16 did exactly that to
moonraker.conf, the file that moves Moonraker's config to
/usr/data/anvil-data/config.
PKG_RELEASE is that string's other half, and it is the release date now:
pkg_release_stamp, monotone, with the same-day letter as a trailing digit so
20260827b sorts between 20260827 and 20260828. Nobody has to notice that a
payload changed, which is the only version of this that holds.
PKG_STAMP_EXTRA="$(pkg_payload_hash)" is not this and reads as though it were.
It hashes payload/ so the BUILD rebuilds; it says nothing to a printer. All
three recipes had it, and all three would have shipped stale.
test_recipe_layout.py makes it the rule for recipes added later: a pinned
recipe with a payload/ must take pkg_release_stamp, and the failure names the
recipe and the line to write.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
6bf7eef238 |
A TEMP= heat-up is put back on exit, like every other snapshot
FF_PA_CALIBRATE TEMP=245 heated the hotend for the sweep and then left it sitting at 245 forever: _heat only ran M104 + TEMPERATURE_WAIT and nothing restored the target. Now _heat snapshots the heater's previous target (usually 0) and both FF_PA_CALIBRATE and FF_PA_PROBE put it back in a finally, so the heater comes back down even when the run fails. A heater the operator set by hand -- no TEMP= given -- stays theirs, the same put-back-what-we-found rule as the rest of _guarded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016RLsVzXppRdXChrDCsMKkA |
||
|
|
b3710c37c4 |
Merge pull request #13 from Klipper4FlashForge/worktree-pa-sweep-chute
The PA sweep never moves the carriage, so park it over the purge chute |
||
|
|
9973019ec8 |
A printer ships knowing where its updates come from
Phase 3 of docs/notes/85-packaging.md was three things: signing, a URL the
printer asks, and a tree at that URL. The migration landed the first. This is
the second, and the tooling for the third.
THE POINTER HAS TO SHIP BEFORE THE HOST EXISTS, which is why this lands with
nothing serving yet. A feed cannot tell a printer where the feed is: a machine
installed without etc/apk/repositories needs a USB stick before it can ever
fetch anything, which is the thing packages were adopted to stop. So the file
travels in the .tgz, naming a host that does not answer yet, and every printer
installed from today starts fetching the day one does.
The cost of that is bounded, and was read out of apk's source rather than
assumed: an unreachable repository fails `apk upgrade` and nothing else --
apk_db_repository_check is called from app_upgrade.c and not from app_add.c,
so `apk add ./file.apk` does not consult a repository at all.
http://reforge.8941973.xyz/apk/mipsel_xburst2/anvil.adb
bin/common.sh's FEED_URL is the base, config.env can override it for a fork,
and pkgs/anvil-core generates the file from it beside the public key it
already shipped. Three things about that URL:
* IT NAMES THE INDEX FILE, not a directory. src/repoparser.c reads a line
ending .adb as a v3 index in place, with the packages as siblings named by
the pkgname-spec mkndx wrote into it; anything else sends apk looking for
<url>/<arch>/APKINDEX.tar.gz. That is the layout this repo already builds
and the stick already uses, so the network feed is the same shape --
exercised by every payload build rather than only in production. The v3
directory form would work too, but only with
`mkndx --pkgname-spec '${arch}/${name}-${version}.apk'`.
* THE ARCH IS A DIRECTORY. Package file names carry no architecture, so two
architectures served from one directory would be two files with one name.
* PLAIN HTTP. libfetch defaults to SSL_VERIFY_PEER and this printer has no
certificate bundle -- the same gap tools/python/README.md records for
CPython. Every package is signed, and the signature rather than the
transport is what decides whether a printer installs one.
bin/publish-feed.sh writes the matching tree and optionally rsyncs it.
Packages go first and the index last, because the index names every package by
sha256 and a mirror carrying a new index with old packages hands printers a
checksum for a file that 404s. Nothing is deleted at the far end. It refuses
an unsigned feed unless ALLOW_UNSIGNED=1: a printer verifies against the key
that shipped in its own anvil-core, so an unsigned feed is one no printer
installs from. The release workflow attaches the same tree as
reforge-apk-feed.tar.gz, so a host can be filled from any release -- including
ones tagged before the host existed -- without rebuilding.
THE REPLICA STOPPED WRITING THE FILE IT USED TO DELETE.
case-build-payload.sh wrote its own etc/apk/repositories naming /mnt and
removed it before tarring; it now passes --repositories-file with a path on
the stick, which apk reads INSTEAD of the default (database.c consults the
default file and repositories.d only when no file is named). So what ships is
whatever anvil-core installed and the build never reads it. The `apk info`
line takes the same flag: without it that one command reads the shipped file
and reaches for the network, which a build's output must not depend on.
Measured: 47 packages built from a cold cache, the payload assembled in the
replica twice, qa/static at 319 passed. Two of those are new -- the default
URL in common.sh (read from the file, not resolved, so a fork setting
FEED_URL in config.env still passes) and the built payload carrying a
package-owned entry that ends /mipsel_xburst2/anvil.adb.
What is left is operational: a host answering at $FEED_URL, and who runs
publish-feed.sh -- a release step with a deploy secret, or a person with the
tarball.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rgrDeu9YAUuMM2G7gKTpL
|
||
|
|
e5d6e5025a |
Merge pull request #12 from Klipper4FlashForge/relax-max-residual
Relax max_residual to 0.5 mm |
||
|
|
4e041a0897 |
A failed pre-flight check must not leak the PARK= override
_probe wrote the PARK= argument into self.park before running the pre-flight checks, but the restore sat in the finally inside _guarded -- so a probe refused for a cold extruder or unhomed axes kept the override. FF_PA_PROBE PARK=0 followed by any refusal would silently turn parking off for every later FF_PA_CALIBRATE, landing the gram on the plate instead of down the chute. The override now goes on inside its own try/finally that covers the checks too, with the rest of the probe split into _probe_inner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UBzgZQiUiARJcG1NupWgY2 |
||
|
|
8cd284d3ee |
A driver latch found already on is debris, not state to preserve
_guarded snapshots the pins on entry and _restore puts back what it found, which is right for every value except one. If a run ever ends with the latch still on, the next run snapshots 1.00, faithfully restores 1.00, and the drivers stay dead -- one aborted run turning into a printer that relatches itself on every subsequent run. Release the latch instead of reinstating it; only a non-latched value is worth restoring truthfully. Also correct an overstatement in the previous commit. A klippy or MCU shutdown does NOT leave the drivers latched: [output_pin] hands shutdown_value, which defaults to 0., to setup_start_value, so the MCU drives PH2/PH3 low by itself. The guard earns its keep on the ordinary failures -- a command_error, a cancel, an exception out of the sweep -- which unwind through _restore with the machine still running. The latched and released values are named rather than spelled 1.00 and 0.00 at four call sites, since the comparison in the snapshot now depends on them agreeing. |
||
|
|
2b92a76f2f |
The PA sweep never moves the carriage, so park it over the chute first
SET_PIN PIN=enable_pin_tmc_x VALUE=1.00 disables the X driver outright. It is not Klipper's stepper enable -- that is !PJ6 and !PJ2 -- but a second latch on PH2/PH3 that Klipper knows nothing about. So for the whole sweep every G1 X in the ladder is planned, step-generated and clocked out against dead drivers, and the gram of filament falls straight down from wherever the toolhead was standing. clearNozzleEddy settles it. Just before its jal to paTestMgr it builds the string resetTmcPa replays: X = 275 + tool_dx, Y = 254 + tool_dy, Z = probeZ + 2.8, from the constants at 0xdb6b2c/0xdb6b28/0xdb6b24 -- the purge chute, the same pair ff-filament.cfg carries as purge_x and purge_y, with a -8.0 that is literally our tool_dy[3]. The app parks over the chute, kills X and Y, dumps into the chute, and then tells Klipper the carriage is still at the chute, which is only sane because it never left. We inherited the latch and the height without the park, so the ladder piled onto the plate wherever the nozzle happened to be. Park in _FF_FILAMENT_PREP's own three moves before the latch goes on, replay SET_KINEMATIC_POSITION after, and shout when the pins will not unlatch rather than leaving a printer that cannot move. The replay lands after RESTORE_GCODE_STATE, not before: set_position fires toolhead:set_position and gcode_move recomputes last_position from base_position/homing_position, so it has to see the restored offsets. |
||
|
|
7b4ce414b5 |
max_residual guards against a mis-trigger, not against probe scatter
The 0.05 mm default was derived from what the guard can resolve rather than from what the machine actually measures: with four symmetric points one bad point of error e shows up as only ~e/4 in the residual while moving the fitted centre by e/2, so 0.05 trips at centre errors of about 0.1 mm. That is inside the ESTOP's own repeatability. A healthy run on a Creator 5 Pro fits with a worst residual of 0.23, which the guard rejected -- and rejecting means aborting without saving, so a good calibration was thrown away. Default is now 0.5, which trips at centre errors of about 1 mm. A probe that missed the bore edge or fired on the way in still lands far outside that; ordinary scatter of a couple of tenths passes. 0 still disables, and the option is still settable per printer. The other guards are unchanged: min_radius/max_radius are off by default, gap_min/gap_max is a 1.5..5 mm window around a nominal 3.2 mm rise, and plate_z_tolerance is one-sided at 0.8 mm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DYQe3sx9XDhz59PDHCDNU |
||
|
|
01e53aa3f6 |
Drop the PARK= opt-out from TOOL_LOCATE_SENSOR
The station pass needs an empty carriage, so with a toolchanger it always parks: PARK=0 only said "I docked it by hand", which the sensor check right below already decides for itself. Without [ff_toolchange] there is no TOOLCHANGE_PARK to call and no sensor to ask, so the pass just runs -- where it used to demand PARK=0 before it would. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfsqVZ3aXL3qxr3BFDS35x |
||
|
|
af16ea0cb5 |
Drop the HOME= opt-out from the offset commands
It served an operator who does not exist: anyone who wants the axes left alone simply does not run a calibration command, and nothing in the tree passes HOME=0. Both commands home when an axis is unhomed, full stop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfsqVZ3aXL3qxr3BFDS35x |
||
|
|
2923c2d921 |
XYZ calibration homes itself instead of refusing
Every move both offset commands make is in absolute machine coordinates, so an unhomed axis is a crash, not a warning -- but refusing with "home all axes first" just made the operator type the G28 we were about to need. Both commands now run it themselves; HOME=0 keeps the refusal for anyone driving the axes by hand. Homing docks a mounted tool (Z homes on the carriage's eddy sensor), so the tool pass no longer keys its re-grab off the plate check having run: it asks the toolchanger what is on the carriage, picks the tool back up if it is not there, and refuses to measure a carriage it cannot verify. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfsqVZ3aXL3qxr3BFDS35x |
||
|
|
074ffb9799 |
Merge PR #9: the chamber heater's fan follows the heater
Brought onto this branch rather than onto master so one CI run covers the combination. #9's own green run predates the apk migration: it was proved against an opkg feed, and the klipper-config payload it edits is now built and installed by a different package manager. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rbKEyLXrzSE6wm3iNApVp |
||
|
|
f11b63b6c1 |
Give apk a version it can parse in CI, and say so when it cannot
ci.yml writes MOD_VER=ci into config.env, and MOD_VER is anvil-core's PKG_VERSION. apk's version grammar wants a digit first, so mkpkg refused it -- with "info field 'version' has invalid value", naming neither the package nor the value nor where it came from. A free-form version was fine for .ipk, which is why the line survived the migration unread. MOD_VER=0 now. It is pinned rather than left to the date default so two runs of one tree produce the same bytes, and 0 says "not a release" as plainly as the word did while ordering below every real version. emit_apk checks the version before calling mkpkg and names the package, the offending value and the config.env setting to fix. The grammar test covers the recipes, and could not have caught this: the value comes from config.env, so anvil-core answers with whatever the machine running the test happens to say -- a date, locally, always valid. The new gate reads MOD_VER out of the workflow files themselves, which is the only place that value is written down, with a companion asserting a workflow still pins one at all so the check cannot pass by having nothing to check. busybox's pkg.conf expanded $BUSYBOX_BIN bare and let a failed sha256sum through a pipeline. Sourced under `set -euo pipefail` it therefore failed -- correctly skipping the recipe, since pkg_recipes evaluates PKG_WHEN in a subshell, but printing "BUSYBOX_BIN: unbound variable" twice into every CI log immediately above the first real failure, where it reads like the cause. With that fixed every recipe reads under the build's own flags, so the unsigned-recipe gate now covers all of them rather than only the ones a build would make: a recipe that cannot be read is dropped by that subshell rather than reported, so the failure mode is a package silently leaving the feed. Verified with the production key: 290 static, 135 replica, 47 signed packages and a payload. MOD_VER=ci reproduced locally against the new message, and MOD_VER=0 built and pruned clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rbKEyLXrzSE6wm3iNApVp |
||
|
|
734a04db45 |
Make the apk feed buildable by CI, and signable by a release
PR #10 was green on this machine and red on both CI jobs. Three separate faults, none of which a local signed build could show. apk-tools is cloned from gitlab.alpinelinux.org, which answers a GitHub runner with HTTP 418 or with nothing until the connection times out. It clones from the GitHub mirror now. Which host serves it cannot change what is fetched: the pin is a commit sha, and git names a commit by the hash of its content, so a mirror answering with this sha is serving this tree or is not answering. get_git also sets the remote url on every run, so a checkout made before this commit does not keep fetching from the host that refuses. anvil-core's cache key hashed the feed's public key as sha256sum "${APK_SIGN_KEY:-/dev/null}.pub" 2>/dev/null | cut ... With no key that file is absent, sha256sum is non-zero, pipefail makes the pipeline non-zero, and build-packages.sh -- which sources every pkg.conf under `set -euo pipefail` -- died before emitting a single package. The 2>/dev/null ate the reason, so `make packages` exited 1 having said only that it had sealed a cache entry. Every signed build passed. Every CI build is unsigned. It is now pkg_signkey_hash, which answers `unsigned` and succeeds. The build container mounts the checkout and nothing else, so a key at the documented ~/.anvil/anvil.rsa was visible in the developer's shell and invisible to apk. The Makefile reads APK_SIGN_KEY out of $(CONFIG_ENV) and bind-mounts its directory read-only. This survived review because the key used while writing the migration sat under work/, inside the one directory already mounted. Gates for all three, each verified against its own bug by reverting the fix under it. The recipe-reads-unsigned test runs pkg_conf under the same flags build-packages.sh uses -- the existing _conf helper does not, and would have stayed green throughout. Its companion asserts the recipe SET does not change with the key, because pkg_recipes evaluates PKG_WHEN in a subshell and a recipe that fails to source is dropped rather than reported: broken only when unsigned, a package would leave the feed with nothing turning red. Both are scoped to what pkg_recipes lists, so busybox -- gated off whenever there is no stock firmware to take one from -- is not asked a question it cannot answer. release.yml takes the private key PEM from secrets.APK_SIGN_KEY, writes it to $RUNNER_TEMP outside the checkout where no later step can archive it into a release, and derives the public half rather than storing a second secret that could drift out of step with the first. Unset, the release builds unsigned with a warning: a fork has no such secret, and firmware nobody else can build is worse than a feed untrusted on a stick the printer was handed by hand. Verified from a cold work/host and work/packages, three ways: no key (the CI case), a key inside the tree, and a key outside it at the documented location. Each gives 47 packages plus the index, a payload, 286 static and 135 replica tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rbKEyLXrzSE6wm3iNApVp |
||
|
|
f8b925e48f |
Replace opkg with apk, and sign the feed
docs/notes/85-packaging.md chose opkg in 2026-08 on one argument: every package
is cross-built here anyway, so the winner is whichever format is cheapest to
PRODUCE -- and .ipk had an upstream tool where .apk meant abuild, a keypair and
an Alpine host. Two lines of that comparison were wrong.
apk mkpkg is an applet of apk itself: -I key:value metadata, -F directory, no
keypair, no abuild, no Alpine host. The "making a package" row described
apk-tools 2. And the on-device client was counted a wash when it is better than
one -- openssl and zlib were already cross-built for CPython, and libarchive
existed in this tree only for opkg, so adopting apk REMOVES a cross-build.
What made it possible at all is that apk resolves its database relative to
--root and uses the same root as the file destination, while / here is a
read-only squashfs -- measured: neither /lib/apk nor /etc/apk can be created.
pkgs/3rdparty/apk-tools/prefix.patch splits the two. Upstream already carries
root_fd and dest_fd separately and merely aliases them in context.c, so the
patch defaults the database root to $MODDIR and leaves files extracting at /.
That is opkg's compile-time VARDIR plus `dest root /`, in apk's spelling.
THE FEED IS SIGNED NOW, which it never was: opkg signed the index and
bin/build-packages.sh signed nothing, so nothing was verified on a printer.
apk verifies with raw PEM public keys, not X.509 -- measured on the machine,
where every certificate directory is empty. ./bin/apk-keygen.sh makes an RSA
pair, APK_SIGN_KEY in config.env points at the private half, and the public
half both seeds the USB stick (so the first install can verify) and ships in
anvil-core (so the printer keeps trusting). An empty APK_SIGN_KEY still builds
an unsigned feed, deliberately: `make packages` has to run on a bare checkout
or the packaging lane stops being a CI gate.
WHAT IT COST is a carried patch, against an internal with one consumer, on a
branch mid v2->v3 transition, on an architecture with no upstream apk port to
share the rebasing. That is the standing risk and it does not go away.
Four bugs this shook out, all silent, all found by a gate rather than by
reading:
* The prune deleted seventeen packages it had just built. The all -> noarch
mapping went into the emitter and not into pkg_pkgfile, so the prune looked
for a name nothing wrote. `make packages` exited 0.
* Whitespace-separated Depends dropped nineteen packages. opkg's pkg_parse.c
splits that field on ',' and nothing else, so the whole list became one
dependency whose name contained spaces and resolved to nothing -- and opkg
neither warned nor failed. `make build` shipped a payload with no Tornado
in it. test_the_payload_is_dependency_closed is what catches that now.
* Ownership was recorded as `nobody`. apk resolves an owner to a NAME against
<--root>/etc/passwd and the build lane runs --user with no passwd entry, so
every file in every package would have installed as uid 65534, with no
error at build or install time. build-packages.sh writes a two-line
synthetic passwd naming the build user root and points --root at it.
* Four recipes carried versions apk cannot parse -- two spelled a commit
+git<sha> where the grammar's hash slot is ~, x264 had a snapshot name
whose -2245 reads as a malformed release, and helixscreen had
0.99.115.creator5.3 where apk allows one trailing letter and then only a
suffix from a closed set of nine words. All four were valid opkg versions,
which is why nothing had noticed. pkg_version_ok gates every recipe now.
A v3 .apk begins with ADBd magic and is not a tar stream, so there is no apk
equivalent of the `ar -p | gunzip | tar` that hand-unpacked anvil-opkg: you
cannot open a package without a package manager. bin/build-payload.py puts the
cross-built binary on the stick at /mnt/apk instead. It is the same build
anvil-apk-tools contains, that package installs normally, and the bootstrap
copy never lands.
Two gates retired rather than moved. apk's installed database is plain text
(F: sets a directory, R: names a file in it), so the payload-ownership check
reads one file where it used to glob info/*.list. And it carries no clock at
all -- apk records no install time and mkpkg sets no build-time -- so the
Installed-Time normalisation case-build-payload.sh did on every build is gone,
and the test asserts the ABSENCE of a t: line instead. Clock-free by
construction rather than by repair.
Also removed: PKG_SECTION, which 45 recipes declared and nothing read once
apk's schema had no field for it, and the `make apk-host` target -- the
packager is built on demand by bin/build-packages.sh and stamped on the pin,
the way pkg_toolchain and pkg_buildpython already work.
Verified from a cold work/host and work/packages: 47 packages built and signed,
`make build` installs 35 and verifies them against the shipped public key,
`make qa-static` 195 passed, `make qa-replica` 135 passed -- the printer
installing off a simulated USB stick and booting. The installed set was checked
against the declared closure of the eleven roots: 35 expected, 35 installed,
nothing extra and nothing missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rbKEyLXrzSE6wm3iNApVp
|
||
|
|
d1ef791745 |
The chamber heater's fan follows the heater, not the touchscreen
firmwareExe was the only thing that knew the chamber element needed air.
CommMgr::heatManager case 5 fired SET_FAN_SPEED FAN=chamber_heat_fan SPEED=0.9
and chamber_loop_fan SPEED=0.3 immediately before every SET_HEATER_TEMPERATURE
HEATER=chamber_heater, on a bare "target != 0" test evaluated once, at the
instant the target was written -- no state machine, no delay, no run-on, and on
the way down it stopped the fans a moment BEFORE dropping the target. Nothing on
the printer knew about the pairing: set a chamber target from a sliced M141,
from Mainsail or from Moonraker and the element ran with its fans stopped. This
mod replaces that binary, and it added M141/M191, so the coupling had to move
somewhere every client passes through.
chamber_heat_fan becomes a [heater_fan] bound to chamber_heater. That follows
the TARGET rather than the heater pin, which matters: PE13 is a PID output on
Klipper's default 0.1 s pwm_cycle_time, so a fan slaved to the pin would chop at
10 Hz and never reach speed. It also runs below G-code -- a pause, a cancel or a
klippy fault cannot strand a hot element behind a stopped fan -- and
fan.Fan(default_shutdown_speed=1.) drives PD12 to full on an MCU shutdown, the
one case where a macro is already dead. There is no tach on any chamber fan, so
nothing here detects a stall; this is not stall protection and does not claim to
be.
The section had to MOVE out of printer.base.cfg. [heater_fan] resolves its
heater at klippy:ready and lookup_heater() raises on the plain Creator 5, which
has no element -- and a pin may be claimed only once, so the plain model keeps
FlashForge's [fan_generic] and the Pro declares the [heater_fan], in the
per-model chamber/<model>.cfg that already exists for the heater itself.
chamber_loop_fan deliberately does NOT go the same way: HelixScreen publishes it
by object name as the user's aux fan ("aux": "fan_generic chamber_loop_fan"),
and a [heater_fan] is neither that object nor settable, so converting it would
take the aux slider away. It stays a [fan_generic] driven from the
SET_HEATER_TEMPERATURE wrapper -- where the app drove it, except this reaches
every client rather than only the touchscreen. ff-filament drops its
chamber_heat_fan blackout for the same reason it can no longer set it, and
because a filament change over a hot element is the last moment to stop
ventilating it.
SEPARATELY, A BUG THIS FOUND. SET_HEATER_TEMPERATURE is a mux command keyed on
HEATER: _cmd_mux raises "The value 'chamber_heater' is not valid for HEATER" for
a heater that was never registered, and a command error aborts a running print.
The gate in ff-chamber.cfg only blocked TARGET > 0, so TARGET=0 went straight to
the base command and threw -- and M141 S0 is what slicers put in their END
G-code. On a plain Creator 5 a chamber-aware profile ran the whole print and
died at the last line. The file's own header claimed "chamber off behaves
exactly as before"; it did not. The gate now covers every chamber command on a
model without the heater, warning only when a real request is dropped and
staying silent for an off that has nothing to turn off.
bin/unpack.sh's drift guard was already broken and is fixed here, because this
change would otherwise have deepened it. strip() dropped the moved sections'
HEADER lines but left their option lines on the stock side with nothing to match
them, and did not know about [include timelapse.cfg], so it printed "printer.
base.cfg DIFFERS" on every single unpack and had been silently useless. It
compares whole blocks now, and reports a match.
qa/static/test_klipper_config.py is new: there was no config coverage at all,
and test_ipk checks that the chamber configs are PACKAGED, never that they
PARSE. It asserts pin uniqueness, that every macro compiles, and -- the half
that earned its place -- it EXECUTES M141/M191 on both models, because reading
the macro did not show the mux bug and running it did. It reproduces klippy's
real parsers rather than approximating them: Environment('{%','%}','{','}') with
single-brace delimiters, RawConfigParser(inline_comment_prefixes=(';','#')) so a
';' inside a string literal survives as configparser leaves it, and both of
gcode.py's disagreeing parameter parsers -- traditional args_r.split(line.
upper()) for M141, shlex with keys-only uppercasing for the extended form, which
is what makes the macro's |lower load-bearing. Both fixes were mutation-tested:
re-adding the duplicate section and restoring the target > 0 gate each fail the
tests written for them.
qa-static green in the container: 171 passed, 3 skipped, including the ipk
packaging tests and shellcheck. make packages PKG=klipper-config builds clean
and the packaged tree is correct -- heater_fan on the Pro, fan_generic on the
plain model, and printer.base.cfg declaring chamber_heat_fan zero times.
qa-replica has NOT been run: this worktree has no work/ tree, so the lane needs
a full make build. Nothing has yet had the real klippy load this config, which
is the check that matters most here, since the failure mode is a printer that
will not boot klippy after a flash. The heater_temp 50 over-run and the 0.9 fan
speed also still want one real chamber heat-up; no test settles those.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDJmG2hGGqNEYwc6Hj5P12
|
||
|
|
28eb20f3da |
Package numpy, because klippy does not start without it
extras/stepper_resonance_tester.py opens with a bare `import numpy as np`; klippy.py:122 walks EVERY config section through a load_object whose importlib.import_module is not wrapped in except ImportError; printer.base.cfg includes FlashForge's printer.vibration.cfg, which declares [stepper_resonance_tester]. Since klippy moved to our own CPython 3.13 that has not been a printer missing resonance testing. It has been a klippy that never reaches ready. So this is not a scientific extra. anvil-klipper depends on anvil-python-numpy the way it depends on cffi, except that the others mean "klippy imports this" and this one means "klippy starts". It also gives back SHAPER_CALIBRATE and TEST_RESONANCES, dead since the same switch, and the VFA calibration behind STEPPER_RESONANCE_FACTORY_CALIBRATE that docs/notes/44-vfa-calibration.md traces end to end. WHAT WAS NOT OBVIOUS, in the order it bit. numpy 2.x builds with meson-python, and meson has never heard of _PYTHON_SYSCONFIGDATA_NAME -- the trick every other python-* recipe here leans on. pkg_mesoncross writes a real cross file instead, naming the $PKG_XW wrappers rather than the raw toolchain: the wrappers carry -EL -mnan=2008, and a cross file that reaches past them builds a tree that passes every gate which only reads symbols and is refused at exec() on the printer. pkg_pywheel grew a PKG_PY_PIP_ARGS branch to pass the file as --config-settings, which is the one thing PEP 517 does standardise. The cross file's `python =` is the BUILD python, and that is not a mistake: meson introspects an interpreter by running it and asking sysconfig, and pkg_pytarget has already made that sysconfig answer for mipsel. The old trick does reach meson, just not by its own mechanism. numpy's meson.build hard-errors below gcc 8.4 and ours is 7.2. The gate is the SciPy Toolchain Roadmap's support statement, not a requirement of the code: this toolchain compiles if constexpr, structured bindings, std::optional, std::string_view, and pocketfft_hdronly.h -- 3635 lines of the heaviest template C++ in the tree, and the half np.fft.rfft needs. Probed before the floor was lowered, and lowered rather than removed, behind a guard that fails loudly if upstream moves the check. 19 extension modules, every one at e_flags 0x70001405/0x70001407, 14MB shipped after pruning tests/, f2py/, stubs and static libs. meson-python is self-hosting, so pkg_buildpython runs meson and ninja just to install the backend -- hence ninja-build in the image and PYPKG_HOST_LIST in dependency order rather than alphabetical. And pip puts meson and cython in work/.py-host/bin, which nothing had ever looked in: without that on PATH the backend fails to install with `meson executable "meson" not found` while pip list shows it right there. THE GATE THAT WOULD HAVE CAUGHT ALL OF IT is qa/replica/test_klippy_extras_import.py: walk the real /usr/data/config include graph, map every section to its extra, import each on the printer's own interpreter. It needs no MCU, because config load happens before klippy:mcu_identify. Nothing asked that question before, which is how two comments in this repo came to assert the module guarded its own import. test_numpy.py asks for numeric answers rather than a module object -- linalg.lstsq specifically, because -Dallow-noblas selects the bundled lapack_lite and _r2_sinusoidal_fit calls it every calibration round. And test_install.py's include check read only ff-*.cfg, so FlashForge's four -- printer.vibration.cfg among them -- were never asked about at all. ff-toolchange.cfg wraps the two search commands in _FF_SHAPER_PREP, as SHAPER_CALIBRATE already was. STEPPER_RESONANCE_FACTORY_CALIBRATE is deliberately NOT wrapped: it is a gcode_macro, not a registered command, and same-named sections merge under RawConfigParser(strict=False) -- a wrapper would not wrap it, it would BE it, with rename_existing pointing at itself. It needs none, since its body is two calls to a command that is wrapped. No SAVE_CONFIG in any macro either; it restarts klippy and stays the operator's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr2x7aDaY889qwSxbdLt8 |
||
|
|
6a4067e863 |
VFA calibration is one import away, and that import stops klippy booting
The stock app's "magic" VFA calibration is not app-side math at all. Traced
from CommMgr::calibration_vfa (0x0075e694), it sends five G-code strings and
the last one is STEPPER_RESONANCE_FACTORY_CALIBRATE -- a macro in FlashForge's
printer.vibration.cfg over klippy/extras/stepper_resonance_tester.py, which
searches amplitude and phase for the motor torque-ripple compensation the
closed-loop driver applies at 1x/2x/4x the electrical frequency. Every piece is
already here: the module is byte-identical in our vendored fork, the mclib
sections are in our printer.base.cfg, and mclib_set_resonance_damp is in the
stock mainBoardGD dictionary we never rebuild.
WHAT THE TRACE TURNED UP ON THE WAY. Two comments in this repo said the numpy
gap was a small item because the module guards its own import. It does not:
line 1 of stepper_resonance_tester.py is a bare `import numpy as np`, and
klippy.py:122 loads EVERY config section through a load_object that does not
catch ImportError -- the default=None argument only covers a missing file.
printer.base.cfg:19 includes printer.vibration.cfg, which declares
[stepper_resonance_tester]. So on the CPython 3.13 klippy has run on since
|
||
|
|
2be2de2bc5 |
The compile does not drive s6-rc-update: on a printer it empties the scandir
Two commits ago this script called s6-rc-update so a recompiled database
reached a running machine without a reboot. On the printer it does this, from
a live state that had just booted cleanly:
s6-rc-update: fatal: unable to manage new service directories
in /run/s6-rc: Value too large for defined data type
and by then every symlink under $MODDIR/etc/s6 is already gone, while the
s6-supervise processes it started are still running. Nothing looks wrong --
until the next s6-svscan rescan, which finds an empty scandir and takes down
klipper, the UI and wifi together. On a printer reached over that wifi, the
recovery is a power cycle.
So the database is compiled, `current` is moved, and the live state is left
alone. The cost is stated where it is paid: `s6-rc` reports "unable to read
valid state" until the next boot, because /run/s6-rc/state is one byte per
service in the database that booted and /run/s6-rc/compiled resolves through
`current`. Supervision is a separate mechanism and keeps every service up, so
the printer runs normally in the window.
s6-rc-update still ships. It is the right tool for a hand at a prompt, and the
EOVERFLOW wants finding first -- it is not the obvious missing largefile flag,
which skalibs, s6, execline and s6-rc all already pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb
|
||
|
|
e3d5a62860 |
The ntp service execs ntpclient, the only name that sets the clock
sntpd and ntpclient are one binary that picks its argument parser from
argv[0], and only one of those parsers can step the clock. The step is behind
if (!dry && ntpc->set_clock) sntpd.c:485
and set_clock is raised in exactly one place: `case 's'` in the ntpclient
parser, sntpd.c:1459. Under the sntpd name it is initialised to zero with no
option to raise it -- that parser's -s means "log to syslog" -- and the daemon
disciplines the clock FREQUENCY through adjtimex instead. Right for a clock
that is roughly correct and drifting; useless on a printer with no RTC, which
boots in 1970. Parts per million do not close fifty-six years.
So the service has never worked. It resolved, connected, took valid stratum-1
readings, logged a reading line every poll, and left the clock in 1970 for as
long as the machine was up -- healthy by every signal except the one that
matters.
The recipe asks for --with-ntpclient, which is upstream's own install hook and
a symlink rather than a second copy of the binary (asserted, because a cp that
dereferenced it would ship 32KB twice). The run script execs that name with
-s -l, and its option block is rewritten against the ntpclient parser: the two
disagree about -p, and -n does not exist there at all, so the old arguments
would have been a usage error and a service that exited at once.
Measured on a printer: clock forced back two hours, service restarted, correct
again twenty seconds later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb
|
||
|
|
dda4eb4025 |
s6-rc-update ships, and the compile runs it before current moves
Recompiling on a printer is not enough on its own. s6-rc-init writes
/run/s6-rc/state as one byte per service in the database it booted, and
/run/s6-rc/compiled is a symlink to compiled/current -- so moving `current`
under a running system leaves a state file whose size no longer matches, and
every s6-rc command fails:
s6-rc: fatal: unable to read valid state in /run/s6-rc/state
Supervision survives it, which is what makes it quiet: s6-svscan and
s6-supervise are a separate mechanism, so every service stays up and
s6-svstat keeps answering while s6-rc itself is unusable until the next boot.
Seen on a printer, 2026-08-31, caused by the postinst added two commits ago.
s6-rc-update is the answer and was not shipped, on the reasoning that an
update is a reboot anyway -- true while updates arrived only as a .tgz, and
false now that a postinst compiles a database. It ships.
ORDER MATTERS, and the first version had it wrong: s6-rc-update reads the
database the live directory is on to know what it migrates FROM, and that is
`current`. Flipping first leaves it comparing the new database against
itself. The compile now migrates while the old database is still current,
and moves the symlink afterwards.
A migration failure is reported, not fatal: the database on disk is right
either way and a reboot applies it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb
|
||
|
|
9fcfaa43a4 |
klipper-config links the configs it ships, rather than hoping anvil-core did
printer.cfg includes the ff-*.cfg beside it, and those are symlinks that
anvil-link-prog.sh makes from whatever it finds in $MODDIR/config. The only
package calling that script was anvil-core -- which does not own those files,
and which opkg installs first. So an upgrade that ADDS an ff-*.cfg linked the
old set and left the new file with nothing pointing at it, and Klipper refused
to start:
Include file '/usr/data/config/ff-pa.cfg' does not exist
Seen on a printer, 2026-08-31. The package that ships the files now relinks
them. Re-running the script from two postinsts costs nothing: link_one leaves
a link that is already right alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb
|
||
|
|
15aa6abdda |
The boot database is compiled on the printer too, not only by the build
s6-rc's database is generated from anvil-core's etc/s6-rc/source, and the only thing that ran s6-rc-compile was the payload build. So `opkg upgrade anvil-core` on a printer wrote new service definitions and left the machine booting the database compiled from the old ones -- the upgrade landed and changed nothing. anvil-core ships bin/anvil-s6-rc-compile.sh and its postinst calls it. anvil-s6-rc calls it too, because s6-rc-compile writes its own execline shebang and oneshot runner into every database it produces, so one left by the previous s6-rc is not one this s6-rc should run. The payload build calls the same script rather than its own copy of the compile, passing the database name: given one the script prunes every other database, which is what keeps two builds of one commit byte-identical. Without a name it timestamps and keeps the database it replaced, so a printer that will not boot has one to point `current` back at. Nothing live is touched either way -- a running supervision tree keeps its database until the next boot, so an upgrade cannot restart services mid-print. anvil-s6-rc gains PKG_STAMP_EXTRA: its version comes from the tarball and cannot see control/postinst, and a stamp that cannot see an input reports "already current" and ships the previous .ipk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb |
||
|
|
a51b09884d |
klippy is exec'd directly; start-stop-daemon matched moonraker
klipper/run launched klippy with
exec start-stop-daemon -S --exec "$FF_PYTHON" -- "$KLIPPY" ...
-S starts the program unless a matching process is found, and --exec matches
on the interpreter rather than on the script. moonraker runs $FF_PYTHON too,
so start-stop-daemon found moonraker, reported the service already up, and
returned without ever exec'ing klippy; s6 saw a longrun exit and respawned it
for as long as the printer was on. One boot log carried 508 copies of
"/usr/data/anvil/bin/python3.13 is already running", with moonraker up,
klipper disconnected, ff-startup giving up with KLIPPER DID NOT FINISH
STARTING, and HelixScreen in front of a machine with no motion and no heaters.
Not intermittent: klipper waits on the mcu-bringup oneshot and moonraker
depends on nothing, so moonraker is always already running by then.
There was nothing for start-stop-daemon to do. s6-supervise is the parent --
it knows the pid, runs one copy of a servicedir at a time, and restarts what
dies -- so a duplicate check in a run script has no true positive left to find
and can only return false ones. The one case s6 does not cover, a klippy that
outlived its supervisor, was already swept by cmdline against klippy.py.
The guard is static rather than a replica test because the replica cannot
reproduce this. Printer binaries run there under qemu-mipsel-static, so a
moonraker process has cmdline[0] and /proc/PID/exe of qemu-mipsel-static, not
$FF_PYTHON; start-stop-daemon matches nothing and starts klippy correctly.
Measured: a replica baked from a package carrying the broken line passes a
behaviour test asserting klippy starts with moonraker up. A behaviour test
there would report this failure as green.
The rest of the diff is comment reduction, 119 lines to 66.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2a5e6832aa |
klippy is exec'd directly; start-stop-daemon matched moonraker
klipper/run launched klippy with
exec start-stop-daemon -S --exec "$FF_PYTHON" -- "$KLIPPY" ...
-S starts the program unless a matching process is found, and --exec matches
on the interpreter rather than on the script. moonraker runs $FF_PYTHON too,
so start-stop-daemon found moonraker, reported the service already up, and
returned without ever exec'ing klippy; s6 saw a longrun exit and respawned it
for as long as the printer was on. One boot log carried 508 copies of
"/usr/data/anvil/bin/python3.13 is already running", with moonraker up,
klipper disconnected, ff-startup giving up with KLIPPER DID NOT FINISH
STARTING, and HelixScreen in front of a machine with no motion and no heaters.
Not intermittent: klipper waits on the mcu-bringup oneshot and moonraker
depends on nothing, so moonraker is always already running by then.
There was nothing for start-stop-daemon to do. s6-supervise is the parent --
it knows the pid, runs one copy of a servicedir at a time, and restarts what
dies -- so a duplicate check in a run script has no true positive left to find
and can only return false ones. The one case s6 does not cover, a klippy that
outlived its supervisor, was already swept by cmdline against klippy.py.
The guard is static rather than a replica test because the replica cannot
reproduce this. Printer binaries run there under qemu-mipsel-static, so a
moonraker process has cmdline[0] and /proc/PID/exe of qemu-mipsel-static, not
$FF_PYTHON; start-stop-daemon matches nothing and starts klippy correctly.
Measured: a replica baked from a package carrying the broken line passes a
behaviour test asserting klippy starts with moonraker up. A behaviour test
there would report this failure as green.
The rest of the diff is comment reduction, 119 lines to 66.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
bd0df7c28d |
FF_PA_CALIBRATE runs FlashForge's PA sweep and reports the number
The host half of the touchscreen's automatic pressure advance, ported from CommMgr::paTestMgr. ff_pa.py drives the recovered sequence verbatim -- the same seven candidates in the same scrambled order, the same Y ladder, the same 5-sweep / 3-winner / mean-of-3 selection, the same "only verdict 9 passes" -- and prints the result. It saves nothing and applies nothing; the measurement itself stays where it already works, in the closed eBoard firmware we ship unchanged, reached through [pa_adjust]'s existing command wrappers. It borrows those wrappers rather than declaring its own. Declaring our own would be safe (lookup_query_command builds a fresh wrapper per call and SerialRetryCommand registers per send), but it would duplicate a closed MCU's protocol strings and would need a config-time callback on [mcu eboard], letting a report-only tool block startup on a machine whose eBoard is unplugged. Deliberate divergences, all commented at the point they matter: the extruder's own pressure advance is restored on exit, which the app never does because SET_PA_ADVANCE overrides it and we do not; a run where every line came back with the same verdict is refused, because "smallest passing wins" then yields the smallest candidate wearing a measurement's clothes; cold extruder, empty tool and out-of-range geometry are refused before anything moves rather than failing mid-line with the eBoard armed; SET_KINEMATIC_POSITION is not replayed, since our sweep is plain G1 under Klipper's own kinematics. What is NOT changed is the timing. pa_action is an immediate MCU command and there is no M400 between the last G1 and ACTION=0; the scorer was tuned against that, and Klipper's own input throttle is what bounds the window. FF_PA_PROBE draws one line and prints one verdict. It exists because the first real question on hardware is whether the eBoard discriminates between candidates at all, and answering that should not cost a full run. Checked: pyflakes clean; qa/static 143 passed; both packages build and both files land in the .ipk; the include set and the shipped ff-*.cfg set match the way the replica gate checks them. The selection logic was exercised against stubs -- it picks 0.0150/0.0200/0.0150 -> 0.016667 from a canned verdict table, refuses an all-9 run, and refuses fewer than three winners. Nothing in the repo can load klippy off-hardware, so behaviour on a machine is still unproven. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw2rehvQJ7LSc28hPikfqk |
||
|
|
9ef21c1fb8 |
wpa_supplicant is a supervised longrun, not a oneshot that forks
The wifi oneshot backgrounded a subshell and returned at once, so that the 15s wait for wlan0 and the 20s wait to associate did not stall the boot transition firmwareExe blocks on. Its comment was explicit that nothing could depend on it, which was true and was not the problem. The problem is the subshell it left behind: a child of s6rc-oneshot-runner with its stdout on a pipe s6-rc closes the moment the up script returns, so a sleep in the wlan0 wait followed by an echo is a write to a closed pipe. OBSERVED on a printer: a healthy supervision tree, s6-rc reporting wifi up, no wlan0 configured, no lease, and not one line anywhere saying why -- HelixScreen calling it "wifi system is not ready" because there was no supplicant holding a control socket. Getting in at all needed a repacked .tgz that brought wifi up ahead of s6. The subshell was never caught in the act and could not be: once its pipe is gone it cannot report its own death. Which is the argument, whatever the exact signal was. A oneshot that forks and returns cannot be relied on and cannot be debugged when it isn't. So: type longrun, no -B, exec wpa_supplicant in the foreground. Blocking IS the job now, s6-supervise holds the process, and a supplicant that dies is restarted rather than mourned. up/down are gone -- a longrun's down is silently dropped anyway. wifi-dhcp is new and is not optional. wpa_supplicant only associates; it never asks for an address, and running udhcpc was the oneshot's last act. bin/wifi-action.sh has shipped since the s6 migration for exactly this hook and was invoked by nothing at all -- `wpa_cli -a` is what invokes it. That also fixes what the oneshot never covered: it ran udhcpc ONCE after boot, so an owner joining a network from the HelixScreen panel an hour later was associated with no address, which the panel reports as a connection timeout. Its run script re-runs the action script when wpa_state is already COMPLETED. wpa_cli fires on CHANGES, and it is not established that it emits CONNECTED on attach to an already-connected supplicant, so without this the lease that finish drops would not come back on a restart. The action script replaces any previous client first, so it is idempotent with a real event. udhcpc keeps -b inside the action script. Different flag, different program: wpa_cli blocks until the script returns, so a foreground udhcpc would wedge event processing. ntp/run reasoned about "the wifi oneshot" and about nothing being allowed to depend on it; it still takes no wifi dependency, now because wifi being up means a supplicant is running rather than that anything is routable. The service tables in how-it-works.md and building.md, and test_s6rc.py -- where wifi sat in HW_SET as "the oneshot above" and test_execline_shipped_too used it as its example -- follow. Both wifi and wifi-dhcp stay out of BOOT_SET: they want a real wlan0, which the replica has not got. qa-static passes. The database is NOT compiled here -- s6-rc-compile is a MIPS binary that runs in the replica -- so make qa-replica is still owed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPQqLMitZzrRcaehiYLHdx |
||
|
|
6bb58a4091 |
The timelapse pin is main's head, and v0.0.1 is not a newer one
Two comments claimed upstream had never cut a tag. It has: v0.0.1, October 2021. The claim came from checking a --depth 1 clone, which fetches none. The pin does not move. v0.0.1 is two years OLDER than c7fff11e, which is still the head of main, so "update to the latest release" would drop hyperlapse, the configurable delayed_gcode interval and the webcam-database lookup getWebcamConfig depends on. Both comments now say which release exists and why the pin is a commit anyway, so the next reader does not re-derive it and reach the opposite conclusion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSmXJv47Wwq2J7FfgCgpCe |
||
|
|
3dc50ad187 |
moonraker-timelapse ships as a package
The Moonraker component and the Klipper macros behind Mainsail's Timelapse
tab, pinned by commit because upstream has never cut a tag.
Upstream's scripts/install.sh is not used: it clones into a home directory,
symlinks into a Moonraker checkout, appends to live config and registers an
update_manager entry that re-clones itself over the network. All of those are
things this repo does differently, so the tarball is a source like any other
and the two files it contributes are placed by hand. timelapse.py lands
inside anvil-moonraker's directory because Moonraker resolves a component
with import_module(".components.<name>", "moonraker") and looks nowhere else.
No BUILD_TIMELAPSE flag, unlike every other optional piece here.
moonraker.conf carries a [timelapse] section and printer.base.cfg an
[include timelapse.cfg]; Moonraker dies on a section with no component behind
it and Klipper on an include it cannot find, so the off position of that flag
would not be a printer without timelapses -- it would be a printer that does
not come up.
The dependency arrow runs one way on purpose. anvil-timelapse does NOT depend
on anvil-moonraker, though it plainly needs it: anvil-moonraker already
depends on this package for the config section above, and declaring both
would be a cycle. Only the direction that is a hard failure is written down.
frame_path is /usr/data/anvil-timelapse rather than the default
/tmp/timelapse/: /tmp here is tmpfs, so a tall print's frames would be
hundreds of megabytes of RAM on the machine running that print.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSmXJv47Wwq2J7FfgCgpCe
|
||
|
|
58cfd00415 |
FlashForge's ffmpeg cannot render a rotated webcam, so we build our own
moonraker-timelapse emits -vf 'transpose=1', 'hflip,vflip' and friends straight from the webcam's rotation and flip settings. The stock ffmpeg is configured --disable-everything with a narrow enable list, and asking the binary itself for -filters returns format, null, scale and trim -- so every one of those renders fails on a printer whose camera is not upright. So: ffmpeg 4.4.5 and x264, cross-built, static, one 1.8M binary at $MODDIR/bin/ffmpeg. The enable list is FlashForge's plus the filters they left out, since flash is scarce and nothing else is wanted. There is no hardware encoder to use instead, and it is written down in pkgs/3rdparty/ffmpeg/pkg.conf so nobody re-derives it: the X2600's device tree declares jpege (a JPEG encoder), jpegd and felix, and felix is an H.264 DECODER -- the die has no H.264 encode block at all. Only felix has a driver in the stock kernel; jpege and the rotate block have DT nodes and clock gates and nothing bound to them. Software x264 is the only route to an H.264 mp4 here, which is why moonraker.conf tunes it with -preset ultrafast rather than leaving the default. x264 is named anvil-x264-dev outright, as anvil-zlib-dev is: every file in it is a header or an archive, so a PKG_DEV_FILES split would leave an empty anvil-x264 beside it that no printer would ever install. bzip2 joins the build image because x264's only distributed form is a .tar.bz2 and tar could not unpack it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSmXJv47Wwq2J7FfgCgpCe |
||
|
|
d898d105e0 |
The Moonraker pin was frozen by a missing _sqlite3 we now build ourselves
MOONRAKER_VERSION was 9d0d09d, the last commit before 80c7620 moved the
database from lmdb to sqlite, because FlashForge's python 3.8.2 has no
_sqlite3 and every release after it died loading the database component.
This repo builds and ships its own CPython 3.13 now, and it has one. The pin
is v0.11.0 -- a release tag, not a sha, so PKG_VERSION is just 0.11.0 and no
longer the Debian-style 0.8.0+git<sha> snapshot string. GitHub strips the
leading "v" from the directory inside /archive/<tag>.tar.gz, which is what
${MOONRAKER_VERSION#v} in the recipe is for.
The dependency set is the part that actually moved. Read against 0.11.0's
CORE_COMPONENTS plus the sections moonraker.conf enables:
dbus-next became dbus-fast. dbus_manager is a core component and imports
it at module scope, so this is not optional whatever [machine] provider
says. Its sdist cythonises nine modules; SKIP_CYTHON=1 ships the python
they were generated from and keeps the package ARCH all. The wheel still
comes out platform-tagged -- poetry-core marks the distribution impure
for declaring a build script at all -- but it holds 29 .py and no .so,
and the content is what ships.
importlib-metadata is new, imported at module scope by
utils/source_info.py, and brings zipp. Both build through setuptools_scm,
so PYPKG_HOST_LIST grew packaging and setuptools-scm. That list is now
install order: pkg_buildpython pip-installs each with --no-deps against
no index, flit-core builds packaging, setuptools_scm imports it.
pyserial-asyncio is gone. Nothing in 0.11.0 imports serial_asyncio;
utils/async_serial.py drives pyserial over asyncio itself. pyserial stays
-- klippy needs it and declares it.
LMDB STAYS, for a reason that is not the old one. 0.11.0 converts an
existing data.mdb to sqlite on first start and imports lmdb to read it. With
the module missing it tries to pip install one, and a printer has no index
to install from -- the old database would be dropped in silence. It is the
upgrade path now, not the database, and the comment on WANTED_MODULES says
so.
inotify-simple STAYS AT 1.3.5 though 0.11.0 pins ==2.0.1: 2.0.1 declares its
licence the PEP 639 way and needs setuptools>=77 to build, against the 75.6.0
pinned here for lmdb's sake. It fails at metadata generation, not at import,
which is the kind of thing that reads as a broken recipe later. 1.3.5 carries
every name 0.11.0 asks of it, and moonraker.conf sets file_system_observer:
none, so the import is all that is asked. jinja2 goes to 3.1.6 to match.
Not taken: paho-mqtt, zeroconf, apprise, ldap3 and python-periphery, whose
components this printer does not enable, and msgspec and uvloop, which
Moonraker imports under contextlib.suppress(ImportError).
Verified: every changed sha256 downloads and matches at the URL
fetch-assets.sh builds; all five affected wheels build offline
(--no-index --no-build-isolation) on a real 3.13 with the host backends
installed in the declared order; qa/static 122 passed, including the gates
that cross-check PYPKG_LIST against the recipes and every PKG_DEPENDS
against the feed. The mipsel cross-build and `make qa-replica` have not run
yet, and versions.env still says a bump is gated on the latter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012oRdgKM7scrEsr6waAByaY
|
||
|
|
2e9f2214a0 |
Merge master into the comment trim
Master moved twelve commits while this branch ran: patch.sh became
payload.sh, run-pre.sh and run-append.sh became runFirmwareExe.sh, the
install manifest and anvil.conf are gone, and versions.env was trimmed
harder there than here (53 comment lines against 185).
Resolved so the merge contributes comments and nothing else. Verified
mechanically: every one of the 270 files in the tree now carries
exactly master's code, and no file this branch touched is resurrected.
- master's deletions accepted: bin/patch.sh, bin/verify.sh,
installer/run-{pre,append}.sh, anvil.conf.in
- versions.env: master's, which is the better trim
- 26 files master only renamed through: this branch's trim kept, with
patch.sh -> payload.sh applied
- 14 files master rewrote to describe the new installer: master's
text taken wholesale, to be re-trimmed separately
- stale references to the manifest and run-append.sh corrected in
pkgs/3rdparty/busybox/build.sh and pkgs/moonraker/build.sh
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMMpQTajN7RDJySrhQ2cTE
|
||
|
|
bf7d0f7e06 |
The printer has no clock and nothing was setting it
There is no RTC and no battery, so every boot starts at whatever the kernel was built with and stays there: job timestamps, log ordering and certificate validity are all wrong on a machine that has been unplugged. Nothing on the stock rootfs could fix it -- FlashForge's busybox is v1.31.1 built without the ntpd applet, and its rdate speaks RFC 868. anvil-sntpd is troglobit's sntpd, a fork of Doolittle's ntpclient: one small binary, servers and interval on the command line, so there is no config file, no privsep user and no chroot to place on a read-only rootfs. It ships $MODDIR/sbin/sntpd and nothing else. A LONGRUN, NOT A HOOK ON THE WIFI ONESHOT. That oneshot runs once, at boot, and returns before wlan0 has associated -- so a sync hung off its end would fire too early, never retry, and never fire at all for the owner who joins a network from the HelixScreen panel an hour later. And with no RTC a clock is not set once but maintained. sntpd keeps its schedule with the network down instead of exiting, which is what lets this service need no wifi dependency -- just as well, since nothing may depend on that oneshot. The poll interval is 300 rather than upstream's 600 because it is also the ceiling on the down-network retry backoff, and so bounds how long a printer that joined wifi late keeps the wrong time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0149y4iyLpEwYKh2PS4fmf4u |
||
|
|
5d7754285e |
Wipe $MODDIR and extract, instead of pruning by a manifest
The installer deletes /usr/data/anvil outright and extracts into the empty directory. The installed set is the shipped set by construction, so a renamed file cannot survive beside its replacement and no list has to be shipped, read or trusted for that to hold. What that costs is everything else under $MODDIR, and the audit of what was actually there is docs/notes/86-wipe-and-extract.md. backup/stock had no restore path in any script and omitted the one file a restore would need. .prev-root-hash was already dead state. config-installed was a twelve-file snapshot serving one md5, for a compare whose stated rationale -- "there is no include-and-override seam" -- stopped being true when moonraker-custom.conf became the last include, and which inverted its own purpose by keeping the webcam block and the API lockdown off any printer that tripped it. So moonraker.conf is overwritten like every ff-*.cfg. HelixScreen's /tmp stash is untouched and is now the only preservation the installer does, so the gate asserts it with a value that could only have come from the printer. Gone with it: both delete passes, the forged-path guard, the rmdir pass, the pre-manifest branch, the unconditional init.d/anvil-service/ anvil.conf removal, the etc/s6 scandir sweep and the $MODDIR __pycache__ sweep -- the last two existed only because a manifest cannot see paths made at runtime. firmwareExe already makes the scandir itself. test_upgrade.py rewritten: the negative control is inverted rather than deleted, and the legacy fixture goes -- there is one path now. qa/static green (112). The replica lane needs a package in work/out, which needs config.env with FF_KEY and STOCK_TGZ; not run here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXKk3zz8LAUPUDZkVEZ9fR |
||
|
|
7ffb39a730 |
The last ten files still named run-pre.sh and run-append.sh
Both were deleted three commits ago and the sweep that followed missed these: the recipes, the s6-rc service scripts, printer.base.cfg's header and three qa modules all still pointed at them by name. Renamed to runFirmwareExe.sh, which is the file that actually does each thing described. Two needed more than a rename. anvil-link-prog.sh's ORDERING note explained itself in terms of stock run.sh's line numbers -- "distributes the software component at its lines 107-179, before the payload is extracted at line 180" -- and none of that is the shape of an install any more. What the note is really for is why the links cannot ship as files, which is unchanged, and it now says that instead. The paragraph about the component no longer carrying firmwareExe is replaced by the reason that matters: no component ships at all, so the version directory under /usr/prog/PROGRAM/software stays FlashForge's and app_startup.sh's watchdog has their binary to fall back on. test_moddir_paths.py's ALLOWED set excused two paths that no longer exist. anvil.conf went when the settings it held moved into the services, and .prev-root-hash is not written any more -- this installer only deletes the stale ones. Both were dead weight in a list whose whole job is to say "not shipped, and correctly so": an entry for a file nobody guards on excuses nothing and hides the next real one. Gates: 112 static tests, shellcheck clean over the edited payload scripts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH7Yb4wuPhnppHAB25UX7e |