mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-21 21:02:16 -05:00
cfaa7e4ff9dac2a2d2e4f0396eaec34c163fa352
156
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cfaa7e4ff9 | Document OrcaSlicer Moonraker upload setup | ||
|
|
fc137ec59d | Prepare beta release documentation | ||
|
|
7c74f025ae |
Document pressure-advance calibration (#15)
* Document automatic pressure-advance calibration FF_PA_CALIBRATE/FF_PA_PROBE and HelixScreen's calibration-screen integration shipped without owner/experienced-user documentation -- only the reverse-engineering notes covered it. Adds a table row and full section to how-calibration-works.md, and links back to it from the notes page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy * Add a simple pressure-advance calibration page Owner-facing procedure alongside XYZ tool calibration and bed mesh -- FF_PA_CALIBRATE with a temp and tool number, what the output means, and how to save it. how-calibration-works.md's mechanism section now links here instead of duplicating the command walkthrough. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy * Point pressure-advance results at the slicer, not printer.cfg PA is a filament property, not a printer one -- a value baked into printer.cfg is wrong the moment you switch material. Recommend the slicer's per-filament Pressure Advance field instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy * Correct pressure-advance page: there is no factory value Unlike tool offsets and bed mesh, PA calibration doesn't import a factory number on first boot -- FF_PA_CALIBRATE only measures and reports one. Say that instead of implying it's pre-populated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
c536a4f06e |
installing.md: show the first-boot screen instead of describing it (#14)
* Show the first-boot screen instead of describing it bin/preview-boot-screen.py already renders every ffscreen.py phase to PNG on the host, byte-identical to what the printer draws (make boot-screen-sim proves it against the real replica). Four of those frames -- waiting, importing, saving, complete -- now sit in installing.md's first-boot walkthrough instead of leaving the reader to picture a progress bar from prose alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMV6mRtJ4dCxAj2QAifT8v * Two big frames beat four small ones 24% width left the boot-screen text unreadable. Down to importing and complete at 48% -- the same sizing the README already uses for the HelixScreen pair -- and dropped the two frames no longer referenced. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMV6mRtJ4dCxAj2QAifT8v --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
53a9941660 | Add release notes for v20260827d-melitopol | ||
|
|
a29fd1913d |
HelixScreen's filament panel now acts on the tool you picked
v0.99.115-creator5.4. The screen was built for a printer with one hotend -- one nozzle reading, a Load button that never said which head it meant -- and on this machine that was not merely thin: preheat resolved "the nozzle" to the ACTIVE extruder, so selecting T2 and pressing preheat heated T0. Every nozzle target now goes to the selected tool's own heater, cooldown builds its heater name the same way, and where no heater resolves the command is dropped with a log line rather than sent to the wrong hotend. The panel above it is a row of tool chips, and the reading, the graph and the presets follow the selection. HELIX_PKG_VERSION MOVES WITH THE TAG, which is the whole reason the two lines are neighbours in versions.env: `_p4` for `-creator5.4`. The upstream release title calls this build 5; the tag's own revision is 4 and that is what a package version can be ordered by. Measured: `make packages PKG=helixscreen` builds anvil-helixscreen 0.99.115_p4-r1 against the new tarball, whose sha256 is the one the release's own SHA256SUMS publishes, and qa/static passes at 319. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017rgrDeu9YAUuMM2G7gKTpL |
||
|
|
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 |
||
|
|
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 |
||
|
|
b55461cd6b |
Cancelling an object leaves the SD reader with nothing to pace it
EXCLUDE_OBJECT installs the ExcludeObject move transform, and inside a cancelled object _ignore_move updates Python state without ever calling next_transform.move(). Nothing reaches the toolhead, so the lookahead buffer that normally blocks gcode.run_script never fills, and virtual_sdcard.work_handler runs on its own yields alone -- one reactor.pause per 8192-byte read, about 330 lines apart. The eboard's soft-PWM refresh misses its deadline and the MCU shuts down with "Timer too close". Measured on a Creator 5 Pro: 37 s from "Excluding object" to the shutdown, with 1 Hz status queries answered in 1.30-1.78 s throughout while the main MCU sat at mcu_awake=0.000 and gcodein=0. EXCLUDE_OBJECT_DEFINE is not the cost. The line costs 115 ms through the live klippy and nothing thereafter; three prints of the same file without a cancel complete with bytes_retransmit=0. Removing it removes the object there is to cancel, not the work. tools/gcode-bench holds the harnesses, including the shlex fast path that takes that DEFINE from 91.6 ms to 5.9 ms on the printer -- worth having for large plates, but not this bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb |
||
|
|
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
|
||
|
|
5ccfc85558 |
Make the import gate assert the module it exists for, and prove it bites
test_every_named_extra_imports skips a section whose module is absent -- deliberately, because that is what klippy's load_object does. But it means the test could go green on a machine where the klippy tree had lost stepper_resonance_tester, having never asked the question it was written for. So it now names that module and asserts it actually imported. Checked in both directions before being trusted. On the baked replica, with the real shipped tree under qemu: the module imports with numpy present, and with site-packages/numpy moved aside it raises ModuleNotFoundError at line 1 -- an ImportError, which is the one load_object does not catch. That is the original diagnosis reproduced on a machine rather than argued from source. All three lanes green: qa-static 148, qa-replica 135, mkdocs --strict. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr2x7aDaY889qwSxbdLt8 |
||
|
|
bf12558082 |
Document VFA where a Klipper-literate reader will look for it
how-calibration-works.md is the mechanism page under "For experienced users", and it already ends with input shaping -- which is exactly the section a reader would otherwise confuse this with. The touchscreen listed the two next to each other and they are not the same kind of thing: input shaping changes how moves are planned, VFA changes the current going into the motor. NOT docs/calibration.md, which is the XYZ tool offsets procedure and nothing else, and NOT "Using Reforge": the procedure is console work and needs SAVE_CONFIG and a macro name to describe at all, which is the line the docs map draws between the first section and the second. The section says the three things a reader can act on. That the correction is applied by the driver rather than by Klipper, so it is live on every move and is not something printing drifts -- which is why this is the calibration you are least likely to need. That SAVE_CONFIG is required and is deliberately not in the macro. And that the commands underneath are FlashForge's: the stock app ran this from its own flow, so it is not claimed as a gain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr2x7aDaY889qwSxbdLt8 |
||
|
|
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 |
||
|
|
a10930aa26 |
Name the thing that actually applies the VFA numbers: GigaDevice's mclib
The note said where the calibration is stored and left the consumer implied. It is two layers and only the first is ours. klippy/extras/mclib.py is fork-only and is not FlashForge's either -- its header is Copyright (C) 2024 Dongzhi Yu <dongzhi.yu@gigadevice.com>, so mclib is the GD32 vendor's motor-control library. On the host it is pure transport: _build_config emits the motor description through add_config_cmd, the block Klipper replays on every MCU connect, which is what makes SAVE_CONFIG's td*_amp/phase survive a reboot. The application is in the GD32H757ZG firmware at 600 MHz, and its command set says what it is: phase resistance, inductance, torque constant, bus voltage, current-loop kp/ki, motor identification. That is a closed-loop current- controlled driver synthesising phase currents, not a step/dir chip -- the 1x/ 2x/4x injection is added to the commutation waveform inside that loop. The fork ships upstream's src/ but no GD32 port and no mclib.c, so we cannot rebuild mainBoardGD.bin or read the injection maths. It does not matter: ff_mcu_bringup.py flashes nothing, the printer keeps FlashForge's firmware, and its dictionary already carries mclib_set_resonance_damp. Follows from that, and worth having written down: the compensation is applied by the driver and is live whether or not stepper_resonance_tester.py can import numpy. mclib.py imports only logging and stepper. A printer with saved td* values keeps them; only the calibration is blocked. Also checked while in there -- both phase results are normalised % 2pi before return, so nothing negative reaches the %u field on the wire. Documentation only. mkdocs build --strict passes. 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
|
||
|
|
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 |
||
|
|
5b291d0b46 |
The PA verdict comes off a serial sensor on the eBoard's second UART
eBoard.bin is a stock Klipper MCU build, so the handlers are reachable without symbols: eBoard.hex loads at 0x08010000, command_index sits at 0x0801a288 with 16-byte entries indexed by the dictionary's command id, and all 77 ids line up. pa_action is id 5 (0x080115ec), get_emcu_pa_value id 6 (0x080115d0). ACTION=11 arms a capture: it zeroes an 8000-byte buffer at 0x200001fc and the result word at 0x200000a0, then enables TIM1/TIM4/TIM8 and DMA1 ch5. The buffer is filled by the USART3 idle-line handler at 0x08016734, which validates a fixed 11-byte frame and stores the big-endian uint16 payload, up to 1990 samples -- and only while the capture flag is set, which is what makes it the PA trace. On stop, a background task at 0x08016c60 runs analyse(buffer, 2000) and parks the verdict where get_emcu_pa_value reads it. So the signal is a serial sample stream from a separate module, not the accelerometer, the eddy coil or an ADC channel -- the earlier guess in this file was wrong and is removed. Which transducer is on that UART is still open. The point that matters for the port: nothing ever looks at the printed lines. They exist only because you cannot make a real pressure transient without really extruding. The scoring is live, off a signal the toolhead already streams, so keeping FlashForge's eBoard firmware gets us the same verdicts for free. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw2rehvQJ7LSc28hPikfqk |
||
|
|
42d876d5eb |
The auto-PA sweep is host code; only the per-line verdict is in the eBoard
firmwareExe runs a real PA calibration at print start, inside the nozzle clean. CommMgr::paTestMgr @0x791640 prints one slow-fast-slow line per candidate at Y = 50 + 5*i for seven fixed PA values, asks the eBoard to score each line, keeps the smallest that scores 9, repeats until three sweeps have produced a winner and averages those three. All of that is host code and is recovered here in full. Only the verdict crosses into the closed toolhead MCU, over two commands that pa_adjust.py already forwards -- and pa_adjust.py is in our tree byte-identical to FlashForge's, with [pa_adjust] already in printer.base.cfg. So PA_ACTION and PA_GET should answer on a Reforge machine today; that is step one of the port. This supersedes "sensor unknown" in 60-background.md and "callable, not reproducible" in 25-app-vs-klipper-ownership.md. What stays unknown is narrower: which sensor the eBoard samples, what ACTION=11 and PC=666 select, and what the non-9 verdicts mean. Two findings that shape the port. PA_GET only respond_info()s its value, so the sweep cannot be a G-code macro -- it needs an extra that queries the eBoard directly. And PA_ACTION is an immediate MCU command, so FlashForge never synchronises the measurement window with motion; the port must copy that timing rather than improve it. 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 |
||
|
|
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
|
||
|
|
1b89782ac9 | Merge remote-tracking branch 'origin/master' | ||
|
|
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 |
||
|
|
c3a64e0394 |
The stock firmware is a pinned download like everything else
config.env.example opened with DESKTOP="$HOME/Desktop", and four settings hung off it. Nothing in bin/, pkgs/, qa/ or installer/ ever read it: three of the four were commented-out overrides for files `make vendor` already fetches, and the fourth, BUSYBOX_BIN, names a binary with no source at all. What DESKTOP really did was carry the stock FlashForge .tgz -- the one input a person had to go and find before they could build anything. It did not have to be. .github/workflows/ci.yml has been fetching that exact file for as long as printer-sim has run, from ghzserg/FF, pinned by sha256, with its own get(), its own cache and its own secrets -- a second spelling of "download the firmware and check the hash" that no human path shared. The pins here are those pins, verified byte-for-byte against a fresh download of both assets. So versions.env pins the stock package the way it pins everything else, bin/fetch-assets.sh downloads it into vendor/, and bin/common.sh defaults STOCK_TGZ_* there. A cold checkout is now `cp config.env.example config.env && make build`, with nothing to edit. --stock is a separate flag from --all, not folded into it. The packaging lane wants no FlashForge package -- that is most of the point of it being runnable in CI on a bare checkout -- so `--all` stays "every payload piece, no firmware", `make build` asks for the one model it is building, and `make vendor` (--all --stock) takes both. The one download that will not overwrite an override. HELIX_TGZ and friends name a public release anyone can re-fetch in a minute; STOCK_TGZ_* names 93MB of proprietary firmware someone downloaded by hand, and clobbering it on a hash mismatch would destroy the only copy on the machine. It is skipped instead, and unchecked -- their file, their problem. CI loses its duplicate fetcher, its stock/ cache and the STOCK_URL_* secrets. A fork testing its own firmware re-pins versions.env, which works locally too; the secrets only ever worked in CI. qa/lib/config.py reads versions.env for the pinned filenames so stock_for() looks where the download actually lands. An explicit STOCK_TGZ_* still wins. An existing config.env keeps working untouched: its paths are now overrides, so the build skips the download and uses the file it always used. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPU6RR4UJH2Qf9DR5hb2gg |
||
|
|
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 |
||
|
|
7abed5da91 |
Rebase the note onto the installer that exists
Master moved under this branch: patch.sh is payload.sh, run-pre.sh and run-append.sh are one runFirmwareExe.sh we own, and the install is no longer spliced into FlashForge's run.sh. Every path and line number in the note pointed at files that are not there. The argument survives intact -- the manifest, backup/stock and config-installed are all still on master, just moved -- so this is a re-derivation of the citations, not a rethink. One section did change: .prev-root-hash is resolved. Nothing records or restores the hash any more; runFirmwareExe.sh:169-171 just deletes it as a secret left by older installs. So the ordering risk against the password branch is gone, and the wipe subsumes those three lines too. backup/stock also reads better now: the script itself explains at :157-161 why firmwareExe is not in the backup, which is the point the note was making the long way round. mkdocs build --strict green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXKk3zz8LAUPUDZkVEZ9fR |
||
|
|
52afaad8e1 |
Say what the installer does now, not what it used to
Four claims in the new note described code or logic that is not there. The backup/stock section said make test-recovery "is still referenced" by hardware-testing.md; that page already says the gate is retired, so the sentence invented a staleness that does not exist. Property (a) was illustrated with a renamed init script starting nginx twice -- the failure that prompted the manifest, but init.d has not shipped since s6-rc took over, which is why test_upgrade.py deliberately avoids those names. The cancels section told a future reader to go and mark the two phase notes, which this branch had already done. And the allowlist paragraph still called it phase 2's to-do list after cancelling phase 2. The pointers in 80- and 85- also read as settled when nothing has landed; both now say proposal. The 80- one leaned on that note's gate item (c), which names anvil.conf -- removed outright some releases ago. mkdocs build --strict green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXKk3zz8LAUPUDZkVEZ9fR |
||
|
|
f2e0e5fcb2 |
The manifest protected three files, and none of them needed it
Proposal: on update, rm -rf $MODDIR and extract into the empty directory. No manifest, no selective delete, no compatibility branch. The audit is the note. Against a full wipe .install-manifest was protecting backup/stock, .prev-root-hash and config-installed, and each one falls: nothing restores from backup/stock and the documented recovery is a stock reflash; .prev-root-hash is removed by the password branch; config-installed is 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 got included last, and which inverts its own purpose by keeping the webcam block and API lockdown off any printer that trips it. So moonraker.conf joins ff-*.cfg and is simply overwritten, owner- dropped files under $MODDIR become an accepted loss, and phase 2's on-printer half in 85-packaging.md is cancelled rather than owed: a printer that never deletes selectively needs no file database for it. Phases 1 and 2 in 80-s6-migration.md and 85-packaging.md point here. No code has moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXKk3zz8LAUPUDZkVEZ9fR |
||
|
|
83a4600df8 |
Stop naming things that are not there any more
A sweep for comments and messages that describe machinery this tree no longer
has. Each one read as current and was not.
* bin/verify.sh, retired two commits before this branch, was still named as
the thing `make release` and docs/building.md were explaining the absence
of. The absence needs no explaining -- `make qa-replica` is the gate.
* `make test-recovery` does not exist. docs/hardware-testing.md told owners
it "used to" prove the stock rollback; the useful half of that -- nothing
tests the rollback today, treat it as unverified -- is kept and the dead
target name is not. runFirmwareExe.sh pointed at it too.
* run-append.sh in bin/build-payload.py, case-install.sh in
docs/printer-replica.md: both files are gone.
* `make test`/`make test-py` over test/, and the note that both suites run
in CI "until a case-*.sh has a green replacement" -- test/ was deleted and
there are no case scripts left to replace.
* docs/building.md still said a package carries the software component.
CI's shellcheck globs are the one real defect this turned up. Two of the four
-- pkgs/*/payload/init.d/S* and pkgs/*/prog/*.sh -- match nothing: the recipes
stopped using both paths, and payload/prog/ is where those scripts live now.
shellcheck given a pattern that expands to nothing writes usage to stderr and
exits 1, so that step has been failing for its globs rather than for the code.
Fixed to the paths that exist, with a pointer to
qa/static/test_shell_syntax.py, which addresses the same files and refuses a
glob that matches nothing -- the guard this step did not have.
Verified: the corrected invocation runs clean over the 21 files it now
matches, and 112 static tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CH7Yb4wuPhnppHAB25UX7e
|
||
|
|
b98552386b |
No FlashForge component ships; patch.sh is payload.sh
The software component existed to give the mod one exec hook. It is not needed for that any more, so a release now carries our installer and our payload and nothing else: 26MB smaller, 21MB of which was FlashForge's own firmwareExe travelling back to the partition it was already on, along with their klippy tree, their wifi module and their configs. WHAT IT GIVES BACK. /usr/prog/PROGRAM/software is no longer rewritten, so the version directory there stays FlashForge's with their genuine firmwareExe inside it -- and app_startup.sh's five-second watchdog, which copies one out of that directory when nothing by that name is running, has something to copy again. A printer whose payload fails to install now comes up on the stock UI instead of a blank screen. That recovery has been broken since we started deleting their binary out of a component we shipped; docs/how-it-works.md claimed it worked throughout. THE ROOT PASSWORD moves with it, because it used to ride in the component's shadow file. bin/pack.sh bakes ROOT_PW_HASH into the installer, single-quoted -- an unquoted $6$... would have the printer's shell expand $6 as a positional parameter and ship a truncated hash -- and the installer writes it to /usr/prog/etc/shadow. The random-password path asks one question instead of four: FlashForge ship the same root hash on every machine, so a live shadow still carrying it means nobody has set a password and it is ours to set. The .prev-root-hash recording, the shipped-vs-live comparison and the restore-it- afterwards step were all machinery for surviving a file that nothing overwrites any more, and are gone. pack.sh checks the hardcoded constant against the stock package it built from, because getting it wrong is silent and total: every printer would read as "already set" and none would ever get a password. bin/patch.sh is bin/payload.sh. It patched nothing any more -- no splice, no shadow edit, no firmwareExe deletion -- and what is left is the payload: the feed installed by the printer's own opkg, the seeded config, the manifest. Its sections are renumbered 1-5 with no gaps, and the five tombstone comments marking where removed sections used to be are gone with them. test_recipe_layout.py loses six of its nine tests. Each compared the tree against a description of the tree -- no fourth directory kind, each kind still used, 3rdparty carries no files of ours, installer/ holds exactly two named files -- and none could go red for a reason a printer would notice, while all six went red the moment the layout legitimately changed. The two that bite are kept: a payload/ path escaping $MODDIR, and a recipe name at both levels, which makes pkg_dir silently build the wrong package. SW_VER is deleted from config.env.example and both workflows: it stamped a component that is not built. Gates: 112 static tests pass. The generated installer is verified after substitution -- all four lines present, and `sh -n` over the result, because a hash with a stray character in it would otherwise reach a printer as a syntax error whose only symptom is a package that installs nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH7Yb4wuPhnppHAB25UX7e |
||
|
|
2a6331a831 |
We own runFirmwareExe.sh; the install is no longer spliced into run.sh
app_startup.sh runs whatever it finds under that name in the package it just decrypted. That is the entire contract -- plus the exit code, where 0 means "flashed, power-cycle me" and non-zero means "carry on booting" -- so owning the name is all it takes to own the install. We were not using it. We shipped FlashForge's copy verbatim and reached the printer through the one exec hook their file offers: the software component's run.sh, into which bin/patch.sh spliced two marked blocks with 40 lines of regex. installer/runFirmwareExe.sh is that file now. run-pre.sh and run-append.sh are folded into it unchanged in substance -- the manifest delete, the HelixScreen settings rescue, the s6 scandir sweep, the config merge, the password dance are all the same code -- and the two orderings that mattered are kept explicitly rather than by where a regex put them: the backups run BEFORE any component is installed, because the software component ships /usr/prog's shadow and passwd and its run.sh copies them over the live ones. The four component installers become one function. Their semantics are kept exactly, including that a component failing md5sum.list is discarded rather than half-installed, and that every previous version directory is wiped before the new one lands. What is NOT kept is the third of that file which never ran: create_key(), defined and never called, and built its paths without a slash so it would have written /usr/progprivate.pem; SSID_NAME/SSID_PASS, assigned and never read; the commented-out kernel version gate; the screwflag factory bypass of the model check, which we ship no screwflag for. bin/pack.sh stages it, rewriting MACHINE=, PID= and MOD_PW_AUTO= and then GREPPING for all three -- a package whose gate still said Creator5Pro because a sed missed would install on the wrong machine. MOD_PW_AUTO is 0 in the checkout, which is the safe default and not an arbitrary one: test_upgrade.py runs the unsubstituted file, and 1 would have it reroll the replica's root password and drop a password file on /mnt every run. The software component still ships, unchanged, and its run.sh is left exactly as FlashForge wrote it. Dropping it is the next commit; this one moves the vehicle without changing what reaches /usr/prog. Gates: qa/static/test_shell_syntax.py already covered installer/*.sh under both bash -n and the printer's ash plus shellcheck, so the new file was checked the day it landed -- 91 pass. test_upgrade.py runs the real installer over two synthetic payloads and is repointed at it. Two doc claims corrected while their sections were open, both false before this change rather than because of it: how-it-works.md said app_startup.sh's firmwareExe recovery "still works" -- it cannot, because it copies out of the version directory and ours has had firmwareExe deleted from it -- and it described a Moonraker preflight-and-rollback that went with the /usr/prog copy; moonraker-preflight.py exists nowhere in the tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH7Yb4wuPhnppHAB25UX7e |
||
|
|
56b5b6c334 |
The printer image is the only replica; make rootfs was a second way nothing took
`make rootfs` unsquashed rootfs.squashfs out of the stock package into
work/rootfs, and a thin container bind-mounted it with PROG_DUMP supplying
/usr/prog from the factory image. Every caller set PRINTER_IMAGE anyway --
both workflows wrote it into test.env BEFORE running `make rootfs`, so CI
unpacked a 93MB package, unsquashed a rootfs and downloaded a 182MB factory
image on every push and read none of it. build-printer-image.sh never used
work/rootfs either: Dockerfile.full does its own extraction from the same
public firmware and needs no stock package at all.
So the fallback went, and PROG_DUMP with it -- it was only ever read when the
replica was built locally, and both implementations already passed it empty
otherwise. Gone: the target, extract-rootfs.py, gates.extract_rootfs and its
two tar helpers, the thin printer/Dockerfile, the `prebuilt` flag threaded
through both Replica implementations, and Replica.start's want_output, whose
only message was that the local build was about to be slow.
PRINTER_IMAGE is pinned to 1.9.7-1.2.9-20260810 rather than :latest. It is
now the entire substrate the brick-safety gates run on and the only input
nothing checksums, so a floating tag would let it change under a green suite
-- the drift the sha256 pins in both workflows exist to prevent.
THREE THINGS THAT COULD NOT HAVE PASSED, found while doing it:
* Neither workflow built the feed. bin/patch.sh has refused to start
without one since
|
||
|
|
f289d6cc8d |
Two notes still said verify.sh checks things it no longer can
Follow-on to |
||
|
|
fbb3182e68 |
bin/verify.sh is retired; four of its forty checks move to qa/replica
verify.sh described itself as "simulate every check the printer performs,
against a built package". The word carrying that sentence is SIMULATE, and it
is why thirty-six of the forty are deleted rather than moved.
THE REPLICA LANE DOES NOT SIMULATE THE INSTALL. install-package.sh puts the
package on a genuine FAT filesystem at /dev/sda1 and runs the machine's own
/usr/prog/app_startup.sh over it, verbatim, under qemu. So these are
PRECONDITIONS of the lane now rather than assertions in it -- break any and
installed_image() raises with the install log and the whole lane goes red:
* the decrypt with the firmware key (the printer's unTar does it)
* runFirmwareExe.sh present, components plain tar not xz (bare `tar -xvf`)
* md5sum.list verifying (the installer aborts and deletes the payload)
* the MACHINE= gate (runFirmwareExe.sh refuses a foreign package)
* the /mnt/<Model>-*.tgz glob (app_startup.sh finds the file by it)
A host-side re-reading of those rules is a second implementation, and a second
implementation can agree with itself while disagreeing with the printer. That
is the defect qa/replica/conftest.py records this repo learning twice about
hand-placed payloads.
A second block was asking what the suite already answers, of an installed
filesystem rather than of a tar listing: the klippy tree and c_helper.so
(test_install, test_abi), the s6-rc database and the oneshot runner's execline
(test_s6rc), s6 and s6-ftrigrd (test_supervisor), the .install-manifest
(test_upgrade), libsodium's bare .so symlink and the chamber configs
(static/test_ipk), and every shipped script's syntax
(static/test_shell_syntax).
THE FOUR THAT MOVED are the ones nothing else asks, and each is stronger for
the move -- a `tar -t` listing cannot tell you that a shared object loads, or
what a name resolves to on a PATH:
* the interpreter IMPORTS sqlite3, lmdb and _cffi_backend, rather than a
grep finding three .so files. This is what catches a cross build that
resolved to an x86_64 manylinux wheel: present on disk, raises on import.
* nothing in $MODDIR/bin shadows FlashForge's python3 -- asked with
`command -v` after sourcing anvil-env.sh, which is the file that PREPENDS
$MODDIR/bin to PATH and so creates the hazard in the first place.
* CPython's dev half (headers, pkgconfig, config-3.13-*) did not ship.
* the ship boundary: no file on the installed printer is byte-identical to
one under bin/ or docker/. Asked of the machine rather than of the .tgz,
so it also sees whatever a maintainer script wrote as root at install
time. The sweep filters on exact size before hashing, which is what keeps
a walk of /usr/prog through qemu cheap.
WHAT GENUINELY LEFT, recorded in docs/qa-migration.md rather than only here:
there is no longer a check you can run on the build host, in seconds and
without docker, against a package you are about to flash. Nothing warns you
before you copy the file to a stick that it was built from the wrong model's
stock package. The printer still refuses it and `make qa-replica` still
catches it, but the fast host-side warning the hardware checklist used is
gone. `make verify` is removed and `make release` no longer calls it.
VERIFIED. All four ran against a real replica (the cached installed image,
PRINTER_IMAGE=monstrofil/creator5-printer:latest): 4 passed in 17s. Then each
was DRIVEN RED on purpose with a throwaway module, which is this repo's own
standard from
|
||
|
|
4850be432c |
Docs: say what is there now, not what the last three commits removed
Deleting test/ and anvil.conf left claims behind in nine files. Every one of
these was present tense about something that no longer exists.
THE CHELPER SYMBOL GATE, in three places that each described it as live:
notes/20-klipper-fork.md said pkgs/klipper gates the .so on "both the ELF
flags and test-chelper.py's symbol check". It
gates on neither now: the ABI is asked once of the
installed filesystem in qa/replica/test_abi.py,
and the single-source rule -- the .so is compiled
from the chelper sources of the tree it ships -- is
the whole defence. The page says that instead.
notes/85-packaging.md described the KLIPPER_FORK fallback the script no
longer had; the script itself is gone too.
qa-migration.md "What survives in test/" listed both the 28 unit
tests and test-chelper.py as surviving. It is
"What survived in test/, and then did not", and it
names what went uncovered rather than leaving that
to be discovered.
ANVIL.CONF, in the two notes that still read it at runtime:
notes/80-s6-migration.md is a dated plan, so it keeps its phases and gains
a "Where this ended up" note: anvil.conf and every
MOD_* switch were removed, so the phases that
source it describe an intermediate state, and the
case-*.sh gates they name are qa/replica/ modules.
notes/85-packaging.md predicted seed/ emptying via a postinst that seeds
anvil.conf.default. It emptied by anvil.conf being
deleted instead. Its payload allowlist said six
entries including anvil.conf and bin/busybox; the
real one in test_ipk.py has five and neither.
THE TWO STANDALONE TREES under tools/, stale before any of this and now fixed:
tools/python/README.md and tools/supervisor/README.md both told the reader to
run their gate with `test/integration/printer-exec.py` over a case-*.sh, and
one offered a `make test-python` target. None of the three has existed for
several commits. Both now point at qa/replica/, which installs the real package
and asks more than hand-feeding a tarball to a case script could. The four
places tools/python/README.md narrates what case-python.sh proves are past
tense, since that is now a record of how the measurements were taken.
versions.env's sqlite3 note pointed at the same dead target.
NAV: every notes/ page was listed except 85-packaging.md, and qa-migration.md
was reachable only by following a link out of Testing, Building or How it
works. Both are in the nav now, which also clears the two "not included in the
nav" lines from every build. .venv/ is gitignored -- docs/requirements.txt
tells you to make one right there in the tree.
Verified: `mkdocs build --strict` is clean and no longer reports pages outside
the nav. qa/static is 123 passed, its 110 errors the same missing
vendor/opkg-utils as before. No .md file is read by any lane, so the check that
matters here is the build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bLGYpVAeVzAX6NeAxcU42
|
||
|
|
57c776ee63 |
test/ is deleted; qa/ is the only suite
The eight files were already gone from the working tree. What this commit does
is everything that had to follow them, because nothing else had been updated
and three of the four things pointing at test/ were on a build or release path.
WHAT WAS STILL WIRED TO IT
pkgs/klipper/build.sh ran test/test-chelper.py under `|| pkg_die`. With the
file gone `make packages` would have died on every
klipper build.
bin/verify.sh ran the same script and read a non-zero exit as
"c_helper.so does not match the shipped klippy tree
(stale build)" -- a false red on every release verify
rather than an error about a missing file. Its
ship-boundary check also globbed `find bin test
docker`; that is `find bin docker` now.
pytest.ini testpaths = test, so a bare `pytest` collected
nothing at all. It is `qa` now, which is what the
comment above it was already describing.
qa/static/ PY_GLOBS listed test/*.py and test/integration/*.py,
test_shell_syntax.py and _files() makes an empty glob a collection error
on purpose -- so the whole static lane failed to
collect, taking 93 unrelated tests with it.
Also dropped: the `pytest -- the host-side tree` step in ci.yml, the `test-py`
target with its .PHONY entry and two help lines, and qa/conftest.py's opening
paragraph explaining why qa/ sat beside test/ rather than inside it.
THE CHELPER SYMBOL CHECK IS GONE, NOT MOVED
It compared the .so's dynamic symbols against what klippy cdefs, and it was the
only check for a c_helper.so built from older sources than the klippy tree
beside it -- the failure that shipped once and bricked klippy startup on
hardware, because cffi resolves lazily and a stale .so imports cleanly.
qa/replica/test_install.py covers the ELF arch and nan2008 and nothing else.
What stands in its place is structural rather than a gate: pkgs/klipper
compiles the .so from the chelper sources of the very tree it ships, so the
mismatch is not something the recipe can produce. The comments in
pkgs/klipper/build.sh and docs/building.md now say that is the whole defence.
DOCS
docs/testing.md's "what is left in test/" section is now "test/ is gone", and
it names what went uncovered rather than leaving it to be found: the tool-frame
arithmetic in ff_toolchange, the chamber macros' per-model branching, the stamp
discipline in ff-startup.py, and the verification G-code against the shipped
macros. docs/hardware-testing.md said `make test-py` keeps the two
gcode/creator5-*.gcode files honest; it now says nothing does, and to read them
by hand when a macro is renamed. Paths corrected in docs/building.md,
docs/printer-replica.md and docs/qa-migration.md's intro.
Verified: `pytest` collects 366; qa/static is 136 passed, up from 43 because
the lane could not collect before this. Its 110 errors are this checkout's
missing vendor/opkg-utils, the same cause before and after. bin/verify.sh,
pkgs/klipper/build.sh and the Makefile parse; ci.yml is valid YAML.
Not touched, and stale before this commit: tools/python/README.md and
tools/supervisor/README.md still document test/integration/printer/case-*.sh
and a printer-exec.py that earlier commits removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bLGYpVAeVzAX6NeAxcU42
|
||
|
|
c92b3c9e86 |
anvil.conf is gone; a component runs because it is installed
The file held three unrelated things behind one name: gates deciding which services boot, switches for the splash / the startup wait / the first-boot import, and tunables for the camera and the nice values. Every one of them defaulted to on, and what each offered was a half-installed printer -- a state nobody chose on purpose and everything downstream carried a second answer for. firmwareExe no longer expands the ok-all bundle and filters it, it asks for the bundle: BOOT_SET=ok-all. A printer that should not run a component does not have it -- `opkg remove`, or a stock FlashForge package. The settings worth keeping are each stated once now, in the service that uses them: the camera's resolution in its run script, nice 10 there and nice 5 in moonraker's (the values every install was getting from the template, not the 0 the inline fallback said). ff-startup's `up` stops passing --timeout and --klipper-tries at all, because 240 and 6 are already ff-startup.py's own defaults and two statements of a default can disagree. Nothing sources the file, so nothing reads it, so it must not survive an upgrade looking editable. run-append.sh stops preserving it and removes it unconditionally beside init.d and anvil-service.sh -- unconditionally because the pre-manifest sweep removes seven DIRECTORIES and would leave this file sitting at the top of $MODDIR. The two upgrade tests that asserted the edit survived now assert the file does not. seed/ leaves anvil-core entirely, which takes with it the recipe's one exception to "every sibling directory is shipped", the seed mount in the replica plumbing, and its exemption in the .ipk allowlist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015E7armHFDcnZFZwdAbqkbt |
||
|
|
7dda2fee0d |
ABI gate: one check, on the filesystem the kernel loads from
mips_abi_gate answered "did this recipe stage the right ABI", once per recipe, over a PKG_ROOT. Four implementations of that one rule had grown up around it -- the function in bin/common.sh, the call at the packaging boundary, two recipes that re-ran it on their own output, and a readelf|grep nan2008 in bin/verify.sh reading a single .so -- and between them they still could not see the largest binary we ship: bin/patch.sh unpacks HelixScreen into /usr/prog and no gate ever read that tree. pkgs/helixscreen/pkg.conf had carried a note saying exactly that. qa/replica/test_abi.py replaces all of it. It walks the installed filesystem on the printer's own CPython -- one qemu process, ~24000 files, 8s -- and reads the ELF header of everything it finds. That is the only place where what the recipes staged, what opkg unpacked, what patch.sh copied and what FlashForge shipped are all visible at once, and it is the thing the question is actually about: the kernel does not read a PKG_ROOT. MEASURED, not assumed: 603 ELF objects on a real install. 569 the loader handles and 568 of those conform. The exemptions are the other two, both narrow. ET_REL objects (33 stock .ko files, plus a stray python.o in FlashForge's CPython 3.8 tree) are not asked about the FP ABI, because insmod and the linker read them rather than the ELF loader and kernel code has no FP ABI to agree about -- by e_type, so the python.o needs no case of its own. And /usr/prog/PROGRAM/ffstartup-arm is an ARM binary FlashForge ships that nothing in the stock tree names; exempt by path, so an ARM binary anywhere else still fails. The architecture is settled before anything is classified, and that ordering came from planting a counterexample rather than from reasoning: a big-endian object decodes its own e_type big-endian, so ET_DYN reads back as 768, lands in neither bucket, and an earlier draft looked straight at it and passed. Every gate here was driven red on purpose before it was kept -- an x86-64 .so in $MODDIR, a real mipsel object with EF_MIPS_NAN2008 cleared, an ARM binary outside the exempt path, a big-endian object, an x86-64 object among the .ko files, and a truncated sweep. test_install.py's two chelper header tests go with it: they read one file out of the several hundred on the same fixture, a strict subset of what now runs. Whether the package ships a chelper at all stays test_install.py's question and is not asked twice. test_ipk.py's assertion that the gate exists becomes an assertion that a second one has not grown back beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NpH87TJJEjDyRhQPu18pgp |
||
|
|
965cf67d93 |
Compile the s6-rc database where the packages are installed
The replica already installs the feed with the printer's own opkg, and anvil-s6-rc already ships s6-rc-compile -- pkgs/3rdparty/s6-rc/build.sh keeps it deliberately, "the only way to recover a printer whose database is wrong without rebuilding a package on a laptop". So the compiler was sitting in the container the whole time, three lines after the install that puts the source tree there. bin/patch.sh section 5b-2 built a second, NATIVE skalibs+execline+s6+s6-rc instead -- four ./configure && make passes cached in work/.s6-native -- purely to get an s6-rc-compile that runs on x86. 110 lines, and its entire hazard was self-inflicted: s6-rc-compile bakes the #! of the execline the COMPILER was linked against into the oneshot runner, so a native stack configured with a different --prefix ships a database whose every oneshot ENOENTs on the printer. Compiling with the compiler we ship cannot get that wrong. The output is the same object. docs/notes/80-s6-migration.md records the measurement from 2026-08-28: one source tree, the host compiler and the target one under qemu, byte-identical db, n and resolve.cdb and identical servicedirs. That measurement was made to justify compiling on the host; it works just as well in the other direction. The rejected alternative in the old comment -- ship s6-rc-compile and compile at INSTALL time on the printer -- is a different thing and stays rejected. qa/static/test_s6rc_source.py is dropped. It was the only consumer of the native compiler, and it hard-failed on any checkout that had not run `make build`: 14 of the static suite's errors were it saying so. What goes with it is the timeout and edge assertions, which are now made nowhere; qa/replica/ test_s6rc.py still boots the real thing. bin/verify.sh keeps the half a host can still check, moved out of patch.sh: the oneshot runner's shebang, and that compiled/current resolves -- read as text straight out of the shipped payload, so it needs no mipsel binary. bin/patch.sh: 278 -> 207 lines. Static suite: 122 passed, errors 124 -> 110. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01La5MaDKqTW675k5fsXsP6S |
||
|
|
214a4e58b9 |
anvil-core owns firmwareExe and start.sh; kill the 3.8.2 myth
Three things, all downstream of klippy having moved to $MODDIR and our own
CPython 3.13.
SECTIONS 8 AND 9 ARE GONE. Both files install at $MODDIR/prog/ and
anvil-link-prog.sh points the stock paths at them -- from run-append.sh on a
flash and from anvil-core's postinst on `opkg upgrade anvil-core`. Nothing
execs either between run.sh distributing the component and link-prog running,
so the component copy bought no ordering. It cost, though: our firmwareExe
with no payload behind it logs "no anvil-env.sh", finds no s6-svscan and then
`while true; do sleep 3600; done` -- a printer that boots to a blank screen for
ever and never reaches app_startup.sh's own recovery. Absent, that recovery
restores a firmwareExe from a version directory. So patch.sh now REMOVES both
from the component: the stock component carries them, and deleting the two cp
lines alone would have shipped FlashForge's originals.
SECTION 5c IS GONE. It did no work -- two du prints and a comment whose first
line was the falsehood below.
THE FALSEHOOD. "klippy runs on the stock 3.8.2, started by
/usr/prog/klipper/start.sh" had spread well past patch.sh, and it had reached
things people follow:
- docs/toolchange.md told owners to scp the ff_*.py extras to
/usr/prog/klipper/klippy/extras/ -- the wrong partition -- and warned that
an OTA would delete them there. Same wrong path in three ff-*.cfg headers
and in ff_toolchange.py.
- versions.env labelled our CPython 3.13 and its eighteen packages
"SHIPPED BUT NOT USED".
- docs/how-it-works.md said Moonraker runs on python3.8, contradicting line
126 of the same file.
All corrected. Historical notes keep their history and get a [Superseded: ...]
marker instead -- including 80-s6-migration.md's "s6-rc-compile is not
shipped", which pkgs/3rdparty/s6-rc/build.sh contradicts deliberately.
Found while reviewing: every docs/ reference to the ff-*.cfg pointed at
pkgs/anvil-core/payload/config/, which does not exist -- they are
anvil-klipper-config's. 28 references across 9 files, several of them broken
markdown links on the published site. All 24 relative links under docs/ now
resolve.
Gates: verify.sh syntax-checks prog/start.sh and prog/firmwareExe out of the
payload (firmwareExe was never syntax-checked before) and fails if the
component carries either. A new replica test asserts both stock paths are
symlinks into $MODDIR/prog and do not dangle. One existing replica test stubbed
firmwareExe by redirecting into $FE, which now writes THROUGH the symlink into
the packaged wrapper; it moves the link aside instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La5MaDKqTW675k5fsXsP6S
|
||
|
|
53292b4d78 |
klippy runs from $MODDIR, so patch.sh stops staging a second copy
The klipper s6-rc service already execs klippy under $FF_PYTHON -- our own CPython 3.13 -- but it read the tree from /usr/prog/klipper/klippy: a copy bin/patch.sh staged into the software component out of the very anvil-klipper package that also installed it under $MODDIR. Two copies of one tree, and the one that ran was the one a stock flash could overwrite. One line moves it: KLIPPY=$MODDIR/klipper/klippy/klippy.py. That deletes sections 1 and 2 of bin/patch.sh -- the cp -a of the klippy tree, the chelper.tar wrapped around a .so the package already contained, the rm -rf of the stock extras/ and kinematics/, and the ff_*.py copy. The software component now carries firmwareExe, start.sh and run.sh and nothing else of ours, which is the phase 7 end state in docs/notes/80-s6-migration.md. It also lets pkgs/klipper declare the dependencies it has had since the interpreter moved. cffi and greenlet reached the printer only through a hardcoded list in patch.sh, so every build had them and no `opkg install anvil-klipper` did; pyserial and jinja2 came only as anvil-moonraker's Depends, so a BUILD_MOONRAKER=0 build shipped a klippy that dies the first time it opens an MCU. Both are real Depends now and MOD_ROOTS drops to ten. Also repointed: run-append.sh's __pycache__ sweep, which was clearing the tree nothing imports while klippy writes bytecode into the one it does -- and those runtime-created directories are in no .install-manifest, so the manifest delete walks past them. verify.sh reads the klippy tree and c_helper.so out of the payload (extracting ./klipper alone), and gains a check that the component does NOT carry a klippy tree. New replica tests assert the service execs the payload tree under $FF_PYTHON and that cffi/greenlet/serial/jinja2 import on it. Comments in anvil-env.sh, verify.sh and pkgs/klipper/pkg.conf that still said klippy runs on FlashForge's 3.8.2 are corrected; that stopped being true when FF_PYTHON moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01La5MaDKqTW675k5fsXsP6S |
||
|
|
c185ef3285 |
The replica is a build tool; test/ is 28 unit tests
Two changes that turned out to be one.
MOVED, NOT DELETED. `test/ffsim/` and `test/integration/printer/` are
`tools/replica/`. They were never tests: bin/patch.sh calls
bin/build-payload.py, which starts a replica and assembles the payload by
running the PRINTER'S OWN opkg against / inside it -- so the "test harness"
was on the build path, and deleting it as asked would have broken `make
build`. qa/lib/replica.py drives the same directory unmodified. No content
changed; what changed is qa/lib/replica.py's build directory, three launcher
sys.path lines, the Makefile targets and the CI shellcheck glob.
tools/replica/README.md says who drives it and why it is not under test/.
CUT TO 28. The five surviving modules were 136 tests; they are 28, chosen on
one question: would this failure be SILENT? What is kept is the mistakes that
run perfectly and are wrong --
test_startup 8 no stamp unless the values are verifiably saved (a
stamp written early is a printer that has lost its
factory calibration for good), and the handover order
test_tool_transform 7 every one is a move to the wrong place: a sign, a
frame left applied while probing, a stale gcode_move
cache, a config a calibrated printer cannot parse
test_ffscreen 5 the four ways it can do harm rather than look wrong
test_chamber 4 the chamber mistakes that cost a print before anyone
sees them, plus the parse gate they rest on
test_gcode 4 a renamed macro, and the safe file being cold
What went: the panel narration, the geometry probe and parse_geometry, the
z_adjust and per-job-Z bookkeeping, the config layout checks, the G-code
bounds checks. Each is either a wrong number shown to a person, or a failure
the printer reports loudly the first time -- and qa/replica/test_boot_screen.py
already renders the real frames on the real machine.
The rewrite is not a delete: assertions were folded so a kept test asks its
question in every direction it can be got wrong (both models, both rotations,
all four ways the migration can fail), and each file's header now says what it
dropped and why, where the next person will open it.
Also gone: conftest's `rootfs` fixture, orphaned two commits ago. Nothing under
test/ needs the firmware, docker, or a network any more.
28 host-side tests pass in 0.15s. qa/static unchanged at 218. `make rootfs`
re-verified end to end from its new path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzLebCrvc8ssWR5JuEsha4
|
||
|
|
465c9902f1 |
test/: drop the seven modules whose subject was the fake
Eight files, 46 tests and make-stock-fixture.sh. Each of the seven test modules built a stand-in -- a klippy stub, an argparse Namespace, a config tree -- elaborate enough that what they pinned down was the stand-in's behaviour rather than the printer's. What is left under test/ is the Klipper config gate and the three modules whose subject is our own Python: ff-startup, ffscreen, and the tool transform. make-stock-fixture.sh goes with them. docs/qa-migration.md argued for keeping it as the input a future build-path test would need, but it fed a gate that no longer exists, and a fixture generator with no consumer rots exactly as silently as a test that asserts nothing. What it had learnt is recorded in the doc so whoever ports the build path does not have to find it twice. The coverage this gives up is written down rather than glossed: a new section in docs/qa-migration.md names every dropped file, what it asserted, and what now asks the question instead -- for four of them, nothing. The one worth watching is the shebang-executable check, which qa/ makes only of qa/replica/actions/*.sh. Falling out of it: the rootfs fixture in test/conftest.py had no users left, and nothing under test/ needs the proprietary firmware any more. Comments in qa/static/test_probes.py, qa/replica/test_mcu_bringup.py, bin/patch.sh and ci.yml that pointed at the deleted files say what is true now; the tree listing in docs/building.md was stale from the run-tests.py removal and is rewritten. 136 host-side tests pass. qa/static is unchanged at 218. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzLebCrvc8ssWR5JuEsha4 |