mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-23 05:42:17 -05:00
2b3772664aa9aef488b349615218e9c0c07b9fa7
105
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2b3772664a |
Close six gates that passed while checking nothing
Every one of these reported success on a condition it could not fail, which is
worse than having no gate at all -- this repo has been bitten by it three times
already (test-abi.sh, lint-danger, the old skip protocol) and the harness
docstrings cite those as the reason it exists.
run_pytest scored a run that collected NOTHING as a pass. pytest exits 5 and
writes tests="0"; passed = total - failures - skipped then gave 0/0/0 and the
gate went green, with proc.returncode never read. Renaming test/integration/,
breaking a conftest import or adding a collect_ignore would have stopped all 54
tests while make test and both CI jobs stayed green. Reproduced before fixing.
check_no_bashisms passed when shellcheck examined no files: with no targets it
writes usage to stderr and exits 1, leaving the SC3xxx filter empty.
check_shell_syntax has an empty-target guard; this one did not. Moving payload/
would have retired the ash-compatibility check in silence. Its exit code is now
checked too, so a usage or file error cannot read as "no findings".
case-install.sh's "a service owns Klipper startup" grepped for the string
start.sh anywhere under init.d/ -- and S70klipper and S60web both name it in
their header COMMENTS, so deleting the line that actually runs it still passed.
Now requires a non-comment line.
case-install.sh scored the genuine FlashForge binary at firmwareExe as ok
("no UI replacement requested"). patch.sh installs the wrapper unconditionally
and HelixScreen is the only UI, so that outcome means the install did not take
-- and it silently switched off four further assertions plus the whole boot-3
UI block, making "the install produced nothing" a passing run.
case-mcu-bringup.sh printed SKIP and exit 0 when the interpreter was not
executable, which Replica.run_case scores as a passing gate: the import test,
the resolution check, the bring-up run, the ttyS7 coverage and the
LD_LIBRARY_PATH control were all skipped and reported clean. seed-prog.sh
already hard-fails on a MISSING interpreter, so this window is a broken
replica, not an absent feature. It fails now.
verify.sh's ship-boundary check uses relative finds, so from any cwd but the
repo root host.md5 is empty, the join matches nothing, and it reported
"package carries nothing from bin/, test/ or docker/" having compared nothing
-- the only check keeping config.env off a USB stick.
Full suite with every gate tightened: 12 passed, 0 failed, 0 skipped. Nothing
was hiding behind them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
168020ab0d |
Close four holes in the print and calibration paths
_FF_PREFLIGHT now runs for every job origin, not only SDCARD_PRINT_FILE. It was gated on that origin, and prepare_on_m23 defaults to 0, so an M23-started print got neither the prepare nor the gate -- skipping the check that exists to stop the nozzle being driven ~3 mm into the plate. The gate only raises; it moves and heats nothing, so no origin is a reason to skip it. The prepare_on_m23 comment is corrected too: it was written for FlashForge's touchscreen preparing before M23, and that binary is replaced. _FF_JOB.ended is now cleared when a job starts rather than only when FF_AFTER_PRINT_END runs. It exists so a slicer footer's END_PRINT cannot make the exit block run twice, but only ff_print clears it, and only for a job it announced (self.active is armed after before_macro succeeds). So an END_PRINT or CANCEL_PRINT typed at the console while idle latched it -- and the NEXT print silently skipped its whole exit block: hotends and bed left at target, tool never docked, Z offset never reset, idle timeout left at 864000 s. CANCEL_PRINT calls END_PRINT, and both shipped gcode files call it directly, so this was reachable without touching a console at all. END_PRINT's SET_GCODE_OFFSET Z=0 MOVE=1 moved outside the homed_axes guard written two lines above it. MOVE=1 makes gcode_move issue a real Z move and cartesian.check_move refuses one on an unhomed Z, so a failed G28 Z earlier in the job made END_PRINT raise there -- skipping M18, the idle-timeout reset and the ended flag, which then let FF_AFTER_PRINT_END run the block a second time. Unhomed now clears the offset with MOVE=0. Calibration left the Z frame broken. _two_pass zeroes the G-code offsets to probe in raw coordinates and never restores them, and RELEASE defaults to 0, so TOOL_OFFSET_CALIBRATE ended with a tool mounted and Z=0 at the eddy plane, ~3.2 mm below the bed. Worse, ff_toolchange._z_tool_term still held the pre-calibration term, so the next grab computed its babystep against something no longer applied and propagated the zeroing instead of curing it. New ff_toolchange.reapply_offsets_after_external_zero() resets the term and re-applies the mounted tool's offsets; both calibration commands call it. GRAB=0 was silently defeated. It means the operator mounted the tool by hand, but the plate check parks whatever is mounted to measure an empty carriage -- so it undid the hand-mount and stored the bare carriage as that tool's nozzle position, out by the full gap in the crash direction, with the gap guard unable to catch it before a station_z exists. It now refuses with the two ways out rather than doing the wrong thing quietly. 54 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
68afe65485 |
Fix two features that silently did nothing, and give Moonraker a user seam
Both bugs were found by auditing |
||
|
|
b5982b09bb |
Fix eleven defects the first pass introduced
An adversarial re-read of master..HEAD, checking the new text rather than
trusting it. The substantive work held up -- all eight dead-code removals prove
out, SAFE-MODE->MOD_UI is complete, A3's reordering is correct -- but eleven
claims were wrong, and one made a document worse than it found it:
* docs/notes/10-hardware.md said `temperature_sensor motor_value` on
eboard:PA1 is "not in anything this repo ships". It is:
printer.motor.cfg:37 declares it and that file is in the package, which is
also why ff-toolchange.cfg can name MOTOR_STOP. The real nuance is that
printer.base.cfg does not include printer.motor.cfg. A correct claim in
25-app-vs-klipper-ownership.md was "corrected" into a false denial.
* docs/toolchange.md pointed at a worked example that reaches 0.125 mm. The
only example is NOZZLE=220 BED=80, which gives 0.045; the 0.08 bed term
needs a bed at 100 or above. Both numbers now stated, no forward reference.
* docs/testing.md said 48 tests; pytest collects 53.
* test_config_ownership.py cited bin/patch.sh:80/:88/:128 -- correct on
master, shifted by two when this branch deleted MARK_BEGIN/MARK_END. The
line numbers are dropped rather than re-pinned, so they cannot rot again.
* test_includes.py invented a reason for ff-legacy being last ("so its import
sees every [ff_tool] already declared"). ff_legacy resolves tools at
klippy:ready; order does not matter.
* fetch-assets.sh's replacement usage text omitted BUILD_MOONRAKER and was
less accurate than the vague line it replaced.
* 49-runout's sensor table still claimed `extruder: extruderN` after the A1
fix made that true only of our override, not of the stock file it describes.
* firmwareExe kept "newest version directory" after the same claim was
corrected in how-it-works.md.
* 25-app inverted the preflight call order and omitted that an M23 print gets
neither call.
* creator5-safe-moves.gcode called the prepare's G1 Z10 "the one move" below
Z50; the G28 and offset move before it also go below.
* DOC-DRIFT-AUDIT.md asserted both "Every finding has been applied" and
"Nothing in this file has been applied", and cited five commits after a
hash the rebase had rewritten.
53 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
63a2669577 |
Re-check the audit against the Moonraker work it was blind to
The audit branch was cut from origin/master and so was missing |
||
|
|
40d2e3bca2 |
Mark the audit resolved, and record the decisions
Every finding is applied. Adds the header that says so, with the eight decisions taken on the items that were not simply wrong prose -- otherwise the file reads as ~140 open problems and becomes exactly the kind of drift it was written to remove. Verified with the full suite, assets and stock package in place: 12 passed, 0 failed, 0 skipped -- including 53 pytest tests (the five that skip without an extracted rootfs run and pass there), the end-to-end install and the recovery gate. The recovery gate is worth calling out: its assertions are the evidence for the rewritten README paragraph -- firmwareExe, app_startup.sh, klipper/start.sh and printer.base.cfg all restored byte-for-byte, the payload left on the data partition and inert, the user's printer.cfg preserved through both flashes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
14672ffe7a |
Section H: delete the dead code the comments implied was live
* ff_print.py TAIL_BYTES -- nothing reads the tail; the module says outright that the slicer's config block is not a source. The rationale comment claiming a measured ~24 KB offset for it went with it. * ff_tool_offset.py Z_TARGET_APP and APP_ACCEL_RESTORE -- unreferenced, but sitting in a block of constants that ARE wired to config defaults. The numbers are kept as comments on the live constants beside them, since they record what the app did; they no longer look like code. * bin/patch.sh MARK_BEGIN / MARK_END -- the real markers are hardcoded inside the inline Python, in four different literals. Editing these did nothing. * bin/patch.sh -- the /etc bind-mount comment was written twice, the second a truncated copy of the first. * bin/common.sh WORK -- nothing reads it; bin/ hardcodes work/. * Makefile clean -- work/uninst and work/uninst-sw are never created. * payload/run-append.sh -- the ssh host-key block is guarded on bin/dropbearkey, which patch.sh never installs. We ship no dropbear at all: stock S50dropbear is already listening. * config.env.example STOCK_SOFTWARE -- referenced nowhere; the base comes from unpack.sh extracting software-*.tar.xz out of the stock package. * test/ffcfg.py -- sections() bound `name` and never used it. Checked: 48 passed / 5 skipped, bash -n over bin/, sh -n over the payload and the test shell, py_compile over every module, the HelixScreen JSON parses, and the Makefile still parses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d05565284d |
Sections E, F, G: wrong values, stale claims, overclaims
E -- values and counts. The START_PRINT prologue has no M400 and an idle timeout of 864000, not 1800000. The nozzle-clean temperature comes from the material table FIRST, so NOZZLE=240 with no per-tool temp cleans at 220 -- documented backwards before. TOOLS is the paired 0:220,2:240 form. The wipe Z is the raw eddy frame, not the homed one. The runout M117 strings are "T<n> clog" / "T<n> out of filament", not E-codes. Grab-verify raises E0051+tool, not E0143. Docks are at X~297; 250/280 are staging waypoints. The grab sequence is documented in the order _grab actually emits it. ff_print exposes seven status fields, not nine, and never emits TOOLS=. Package sizes reconciled against the real artifacts (62MB built, ~93MB stock, ~370MB of CI assets, ~60s not 90s). testing.md's sample output block matched no header the code prints, so it is replaced with the real ones; its pytest row listed two of six suites. F -- stale claims. _FF_PREFLIGHT is not "missing". SDCARD_SET_CHANNEL is not still issued ( |
||
|
|
ce06b7e88d |
Take initial_WHITE back out of printer.base.cfg; the plate check is one-sided
A7:
|
||
|
|
b2c651b11f |
Fix the runout insert path and TOOL=ALL staging; drop SAFE-MODE for MOD_UI
A2: _FF_INSERT's body was entirely inside {% if kind == 'switch' %}, yet
insert_gcode is wired on all four fm_ex* with KIND=motion. A motion insert --
which is what clearing a clog looks like, filament moving again -- produced no
output at all, on exactly the path where the operator most needs the next
step. Adds the motion branch the comment already described.
A3: TOOL_OFFSET_CALIBRATE staged each tool with set_nozzle() inside the loop,
so a gap-check failure on a later tool left earlier tools pending for
SAVE_CONFIG -- while raising an error that says "nothing saved". True for
TOOL=<n>, false for TOOL=ALL, and told to the operator at the moment they
decide whether to save. Staging now happens after every tool has passed.
A8: SAFE-MODE is removed rather than repaired. It never worked: the settle
check cleared the counter 90s after every boot on a condition
(`[ -s $CHOICE ]`) that cannot be false, since $CHOICE is written two lines
earlier and "none" is not empty. Nothing checked whether helix-screen was
alive, so the latch only fired on three reboots inside 90 seconds -- not the
crash-loop it was written for.
In its place, MOD_UI in anvil.conf, alongside MOD_WEB/MOD_CAM/MOD_SSH/MOD_WIFI
and templated by patch.sh the same way. It stops the UI being started; it does
not pretend to repair one. The docs said `touch SAFE-MODE` was how you recover
a blank screen. Nothing recovers a blank screen on the machine: HelixScreen is
the only UI, and getting one back means flashing a package. Said plainly now,
in the README, the recovery section and the troubleshooting table -- along with
what is genuinely unaffected, which is ssh, Mainsail, Klipper and printing.
48 passed, 5 skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e5bd171cf8 |
Correct the docs the code had drifted away from
Applies the audit's sections B, C and D, plus two decisions. Behaviour (A1): fm_ex1..3 carried stock's `extruder: extruder`, so a clog on T1..T3 was measured against T0's filament movement. Each motion sensor now watches its own extruder. Gate count (A6): "four gates" was the number of make test-* targets, not of gates run -- run-tests.py emits twelve. The number is dropped rather than restated, in docs/testing.md and the Makefile help. B, install instructions that failed as written: STOCK_TGZ_CREATOR5 missing its .tgz; a pointer to a ./build.sh that does not exist; klippy-extras/ paths that moved to payload/klipper/extras/ in 0ef9d6b; a calibration recipe both commands refuse without PLATE_REMOVED=1; a storage table that put dock_x/dock_y in an include, which SAVE_CONFIG refuses; four files telling the user to add includes printer.base.cfg already ships; ./pack.sh style paths that do not resolve from where the scripts leave you. The MAINSAIL_ZIP / HELIX_TGZ override was documented in three places as "used as-is, never checksummed". It is checksummed, and fetch-assets.sh overwrites the user's file with the pinned release -- SKIP re-downloads too. Corrected, with the way that actually works (match the hash in versions.env). C, files that contradicted themselves: the -5/-3 ESTOP target, the t<n>_offset_* storage names, the chamber heater's model split, the LED premise, the include order comment in test_includes.py, gear_stepper as a "feed hub" when it is the tool lock, an object list ff-toolchange.cfg says does not exist, the eddy probe vs the under-bed cylinder, "installs them without clobbering" against a gate that enforces the clobber, "keeps two versions" when the installer keeps one, "two changes from stock" when there are three, and ff_legacy's "nothing here runs during normal operation" when auto_import runs on every ready. D, dangling references: OKF/* and firmwareExe-decompiled/* repointed at docs/notes/*, plus case-boot.sh, sim-image.sh, printer-exec.sh, the test/printer build context, wait_for_banner, ff_toolchanger_compat and a nonexistent orca snippet. Not touched, pending a decision: sections E-H, and the printer.base.cfg "verbatim" claim (A7), which depends on how the unpack drift check is fixed. 48 passed, 5 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
39cc934e9b |
Audit every doc, comment and docstring against the code
Six read-only passes over the whole repo: build/install docs, testing docs, toolchange/offsets, architecture notes, print lifecycle/filament, and the in-code comments in both the build tooling and the on-printer payload. ~140 findings, none applied. Each carries a proposed fix. Section A is the subset that would change behaviour rather than prose and needs a decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d47e90e375 |
Moonraker: gate the swap on Moonraker's own component list
The pre-flight added with the last commit imported four modules chosen by
hand, and `authorization` was not one of them -- so it would have passed a
tree that cannot load it. That is not hypothetical: trying the pinned
build over ssh produced
OSError: Could not locate nacl lib, searched for libsodium.so, ...
from authorization.py, and a four-module check would have said nothing.
(That particular failure was the test script's fault, not the build's.
app_startup.sh exports a dozen /usr/prog/*/lib directories before it
launches anything, libsodium among them, and everything started at boot
inherits them. An ssh shell inherits none of it, so restarting Moonraker
by hand with a partial list starves a perfectly good build of a library
it would have had at boot. Both the check here and the ssh helper now
set the full list rather than the three that were being copied around.)
So the list is no longer written down by us. moonraker-preflight.py
reads CORE_COMPONENTS out of the tree it is about to install and imports
every one of them, plus every section in the printer's moonraker.conf,
so it keeps covering the right modules when the pin moves and when a
user configures a component we never considered. A section that is not a
component is skipped; a component whose DEPENDENCY is missing is not.
Verified against all three real cases on python 3.8: the pinned build
passes (17 components), v0.9.3 fails naming _sqlite3, and the pinned
build with libsodium removed fails naming authorization. The install
gate and its result are asserted on the printer replica.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4460488ea9 |
Moonraker: pin the last lmdb build -- this python has no sqlite3
v0.9.3 does not start on the printer. It was built, flashed by hand and
tried, and it died on:
ModuleNotFoundError: No module named '_sqlite3'
FlashForge built python 3.8.2 without that module -- there is no
_sqlite3*.so in lib-dynload and no libsqlite3 anywhere on the image,
only the pure-python sqlite3/ wrapper that cannot work without it.
Moonraker moved its database from lmdb to sqlite in v0.9.0, so every
release from there on gets as far as loading the database component and
exits. The last release still on lmdb is v0.8.0, which predates the
webcam "enabled" flag this whole change exists to get.
No release has both, so the pin is now the newest COMMIT that does:
9d0d09d, 2023-12-05, the one before "database: migrate from lmdb to
sqlite". It has the webcam field and reads the printer's existing lmdb
store in place, so nothing is converted and going back to stock is a
clean round trip. Verified on python 3.8 against the printer's exact
library versions with _sqlite3 removed from the interpreter to match.
Two things follow from having shipped a build that could not start.
run-append.sh now asks the printer's OWN python to import the tree
before it moves anything, and keeps the stock server if it cannot. A bad
pin refuses to install instead of leaving the machine with no web UI --
confirmed on the replica, where the v0.9.3 payload is now declined and
the stock Moonraker keeps running.
And the test that passed this build is fixed. It asked only whether a
moonraker.py process ever appeared, which a build that dies on an import
satisfies for a second or two on its way down. It now requires the
process to still be there after a settle, and the log to be free of
import errors. Re-pinned to v0.9.3 it fails, as it should have.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a40bc861dd |
Ship our own Moonraker: the stock 2022 build hides the camera
Mainsail has shown no webcam panel on this printer no matter how the stream is configured, and the stream was never the problem: nginx serves /webcam/ correctly and mjpg-streamer answers it. FlashForge's Moonraker is a 2022 build (API 1.0.5). Moonraker only grew the webcam "enabled" flag in April 2023, and Mainsail filters its webcam list on exactly that field -- so every [webcam] entry is discarded and the panel is removed from the dashboard entirely. Nothing on either side of that can be configured around; the server has to be newer. So the mod now ships Moonraker the way it ships Mainsail: pinned in versions.env, fetched to vendor/ against a sha256, installed over the stock tree. v0.9.3 is the newest release that runs on the printer's existing python environment untouched -- v0.10.0 wants dbus-fast, a compiled module that is not there and would have to be cross-compiled for mipsel. Only the python package is replaced; the interpreter, the moonraker-env beside it and moonrakerDaemon stay FlashForge's. It rides in the mod payload rather than the software component because the stock run.sh does not extract that component over /usr/prog -- it copies a hand-written list of paths out of it, and a moonraker/ dropped in beside them would never be looked at. run-append.sh installs it, moving the old tree aside and putting it back if the copy fails: this is the one thing the mod writes to /usr/prog that the stock installer does not, and a printer whose Moonraker did not survive has no web UI at all. The database migrates itself -- v0.9.x moved from lmdb to sqlite, lmdb 1.3.0 is installed, so Mainsail's settings survive the upgrade. Covered end to end on the printer replica: that the swap happened, that the installed webcam.py has the enabled field, and that the shipped tree really runs on the printer's own python3.8 under qemu-mipsel. With BUILD_MOONRAKER=0 those skip and the stock server is left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3e997e5a44 |
HelixScreen: bump to v0.99.115-creator5.1 (build 2)
Same upstream 0.99.115, rebuilt from creator5 @ 696c0d2: the setup wizard's Tool Offsets step, the connection step pre-filled with 127.0.0.1:7125, the Dock pill in the tool switcher, and Russian gone from the language list. Tarball name is unchanged, so the stale sha256 is what re-downloads it. Klipper fork is already at the tip of origin/creator5; Mainsail v2.18.2 is still the latest release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
401c2ecf35 |
The ff-*.cfg are ours: overwrite them, and say so at the top
Editing a shipped config was the wrong seam, and the installer was
letting people do it. An edited ff-*.cfg was KEPT and the new version
left beside it as .mod-new, so a machine could sit for releases on a
macro set nobody else was running -- the failure mode being a bug report
about code that had been replaced twice since.
Klipper already offers the better seam. It parses with
RawConfigParser(strict=False): same-named sections MERGE, the last value
of an option wins, and a redefined [gcode_macro] replaces the original.
So an override in printer.cfg restates only what it changes, keeps every
shipped value it does not mention, and survives every flash. printer.cfg
is the one config nothing ever writes.
So run-append.sh now overwrites ff-*.cfg unconditionally, logging a line
when it clobbered something so the log explains where an edit went, and
each of those files opens with a header saying it will be overwritten
and where to put changes instead.
printer.base.cfg needed no installer rule: FlashForge's own run.sh:135
does `cp $WORK_DIR/klipper/config/* /usr/data/config/ -rf` on every
flash, no comparison and no .mod-new, so it has always worked this way.
It gets the header anyway, because "why did my edit vanish" is the same
question there -- and the stock-drift comparison in bin/unpack.sh strips
comment lines, so the banner does not trip that warning.
moonraker.conf deliberately keeps the .mod-new dance. It is not a
Klipper config and has no include-and-override seam, so the argument
above does not apply to it, and a printer reached through a tuned
trusted_clients or cors_domains block would lose that access on an
update.
test_config_ownership.py holds the parts together: every mod-owned
config must carry the banner near the top, the banner must name
printer.cfg, moonraker.conf must NOT carry it, and run-append.sh must
still contain the overwrite branch the banner promises. A banner that
outlives the behaviour is worse than none -- the user trusts it, puts
nothing in printer.cfg, and their edit quietly wins.
Rebased onto
|
||
|
|
9b1f944293 |
Ship the ff-*.cfg includes in printer.base.cfg
The package installed every file the mod needs and then relied on the user
to hand-edit printer.cfg with seven [include] lines. That was the one
install step nobody could automate, because printer.cfg is the user's file
and run-append.sh may not write it -- so a freshly flashed printer had all
seven configs sitting in /usr/data/config/ and not one of them loaded, and
came up as a plain printer with no toolchanger and nothing saying so.
printer.base.cfg is ours, and the stock run.sh force-copies it to
/usr/data/config/ on every flash -- the same directory as the ff-*.cfg, so
relative includes resolve. Putting them at its end wires the mod up at
flash time and makes the undo button complete: flashing the stock
FlashForge package restores its own printer.base.cfg and the includes go
with it, leaving the ff-*.cfg as inert files rather than as a config
referencing extras that are no longer in /usr/prog.
It also retires the trap in the old instructions. "Append to printer.cfg"
is wrong for any printer that has run SAVE_CONFIG: an [include] below the
"#*#" marker makes Klipper read the whole autosave block as corruption
("modifications after header") and discard every calibrated value -- PID,
mesh, shaper, nozzle and dock positions.
Most of the documented ordering rules turn out not to exist. Nothing needs
to follow [virtual_sdcard]: virtual_sdcard registers SDCARD_PRINT_FILE and
M23 in __init__, ff_print takes them over at klippy:connect, and by then
every section is loaded. That was true of the old gcode_macro wrapper and
stayed in the docs after the move to ff_print.py. gcode_macro's
rename_existing defers to klippy:connect too, so the G28 and
SHAPER_CALIBRATE wrappers do not need their targets declared first either,
and ff-print-macros.cfg reads _FF_FILAMENT through a runtime template
lookup rather than at parse time. What survives is override order:
ff-runout.cfg after printer.filament.cfg's sensors, ff-chamber.cfg after
printer.chamber.cfg. End-of-file satisfies both.
ff_print.py's error message said "must be loaded after [virtual_sdcard]",
which pointed at the wrong problem; it can only fire when there is no
[virtual_sdcard] at all.
Both halves are checked on the replica now: case-install.sh asserts the
includes and files are present after a flash, case-recovery.sh asserts a
stock flash takes the includes away again. test_includes.py covers the
wiring in the repo -- every shipped ff-*.cfg included, every include
resolving, and the block after everything it overrides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f10d002cf6 |
Set the chamber light on in printer.base.cfg too
|
||
|
|
a70462b323 |
Light the chamber at startup; firmwareExe is not there to do it
printer.base.cfg declares [led chamber_led] on white_pin PB5 and stops
there. Klipper's led module defaults initial_WHITE to 0, and nothing in
the mod ever set it: switching the light was firmwareExe's job, done
from its own UI, and that binary is precisely what this mod replaces.
The result is a chamber that stays dark forever and reads as a dead
lamp rather than a missing command.
Fixed by overriding the one option instead of restating the section.
Klipper parses with RawConfigParser(strict=False), so a second
[led chamber_led] merges into the first and white_pin carries over.
That is what keeps FlashForge's printer.base.cfg byte-for-byte -- it is
shipped verbatim and bin/unpack.sh warns when it differs from the stock
package -- so the pin stays theirs and only the initial value is ours.
ff-chamber.cfg is included after printer.base.cfg, which is what makes
the override land at all.
Verified against the parser rather than assumed: reading the two files
in order with klippy/configfile.py's exact RawConfigParser settings
leaves white_pin PB5 and hardware_pwm False intact alongside
initial_white 1.0, and getfloat('initial_WHITE') finds it despite
configparser folding the key to lower case.
The file grew a second subject, so its title line says so now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
424ef059a7 |
Stop telling the operator to persist prepare=0
The feature print's header and docs/hardware-testing.md both said to set FF_BEFORE_PRINT_START.prepare to 0 and offered "persist it by editing variable_prepare" as the way to make it stick. Following that breaks every other print on this machine. The Orca profile here deliberately carries no START_PRINT: [ff_print] wraps SDCARD_PRINT_FILE and the implicit prepare is what homes and heats the bed. FlashForge's start block has no G28 and no M190 of its own, so with prepare persisted at 0 an ordinary file's first move -- G1 Z5 F2400 -- runs on unhomed axes, and nothing has heated the bed. The feature print is the one exception, because it carries its own START_PRINT with the TOOLS= list the automatic path cannot derive (nothing in a sliced file names the tool set without reading the slicer's config block). So it is a per-print toggle, and both files now say to put it back afterwards -- or just RESTART, which does it anyway. Comments and docs only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f70369155b |
Run the brick-safety gates in CI, and stop skipping quietly
The replica gates -- the only ones that decide whether a package bricks a
printer -- had never run in CI. They were gated on a STOCK_URL_CREATOR5PRO
secret that was never set, so printer-sim rendered as a grey dash beside
two green ticks and read as "CI passed".
The packages it needs are already public. ghzserg/FF publishes both stock
packages and the factory image, and this repo already points at that
release in docs/printer-replica.md. All three are now pinned in ci.yml by
sha256 and verified byte-for-byte against the copies used locally:
Creator5Pro-1.9.7-1.2.9-20260810.tgz 1f3252fe... 93MB
Creator5-1.9.7-1.2.9-20260810.tgz 72009be1... 93MB
Creator5Pro-factory.tar.xz 45657924... 190MB
Nothing proprietary is redistributed from here; the workflow fetches what
is published elsewhere, cached on their checksums so an unchanged pin
costs that archive nothing. Secrets still override for a fork with its own
firmware -- but a URL override without a matching checksum now refuses to
run rather than downloading something nobody verified. have-firmware is
gone with the `if:` it fed.
Rehearsing that job locally found the reason it would have failed anyway.
The replica gates call ./bin/patch.sh directly, and patch.sh expects
vendor/ already populated -- bin/build.sh is what fetches Mainsail and
HelixScreen, and the gates do not go through build.sh. No local run ever
noticed, because vendor/ was always already there. Both workflows now
fetch assets before the suite; in release.yml that step has to precede the
gates, since `make release` would otherwise fetch them one step too late.
Second half: a skip must be legible.
ALLOW_SKIP=1 silenced the explanation as well as the exit code -- the
summary printed "2 skipped" and then nothing about what or why. That is
the disease this harness exists to treat, reintroduced by the flag meant
to manage it. Now:
- Every skip is listed again in the summary with its reason and whether
it was accepted, whatever ALLOW_SKIP says, and becomes a GitHub
annotation. Nobody reads a green job's log.
- ALLOW_SKIP takes names: ALLOW_SKIP="pytest,the printer replica". `1`
still accepts everything, but it is a standing promise never to notice
a skip again, so CI uses the list and a third skip fails there.
- The pytest gate names the tests that skipped instead of counting them.
That immediately showed the exec-bit test skipping wherever .git is
absent -- a bare "6 skipped" would not have.
VERIFIED:
- printer-sim rehearsed end to end on the actual published bytes, real
git checkout, REQUIRE_PRINTER_SIM=1 and no ALLOW_SKIP: 12 passed,
0 failed, 0 skipped, exit 0, in about four minutes. Before the vendor
step it failed at patch.sh, which is what CI would have done.
- The four ALLOW_SKIP cases, in the build image with no firmware: none
set -> exit 1 naming both gaps; "pytest" only -> exit 1, replica still
NOT ACCEPTED; both named -> exit 0, both still printed; and under
GITHUB_ACTIONS the ::warning:: annotations appear.
- pytest 30 passed. Both workflows parse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
79ca564d36 |
ff-toolchange.cfg: drop the comment block that outlived its code
The "no persistent tool state" rationale is a third copy: it already sits next to the code that implements it (ff_toolchange.py:611) and in docs/notes/25-app-vs-klipper-ownership.md. This started as two blocks. The other one described the Moonraker print entry point as a wrapper living in this file, and carried a FIXME about four SDCARD_* calls that no longer exist -- but master has since replaced that block with the nine-line MOVED pointer to [ff_print], which is the same cleanup arrived at from the other side. Rebasing brought the two together as a conflict; the pointer is the better text and it stays. Comments only; no config value changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
30b21262ed |
Ship the two verification files, not the thing that wrote one
The generator was a build step for an artifact that is written once and then read by a person standing at a printer. Keeping it meant the file you send to the machine did not exist in the repo, and the header of the file you did get told you to re-run a script instead of telling you what to watch. gcode/ now holds the G-code itself and bin/gen-test-gcode.py is gone. Added alongside it: creator5-safe-moves.gcode, the feature print with the heat and the filament taken out. Once homed, nothing in it goes below Z50, nothing extrudes, no heater is ever given a target. It homes, laps the bed slow then fast, runs the diagonals a failing corexy motor turns into an L, changes tools, sweeps the fans and checks the chamber gate -- so that a dock that does not latch or a Z offset that is wrong is found 50 mm up rather than in the plate. It names its tools with SELECT_TOOL rather than a bare `Tn`, and carries no M104 or M140. ff_print scans a file's head for exactly those to decide what to prepare, so this one leaves it nothing to find and is safe whether FF_BEFORE_PRINT_START.prepare is 0 or 1. The feature print still wants it at 0. test_gcode.py reads the committed files now instead of running a generator, and covers both. The safe file gets the two checks that are its whole reason to exist -- never below Z50, and never a heater target or an E word -- because those are exactly the properties a later edit would break while leaving a file that still runs and still looks safe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
959fbd356a |
Generate the print that proves the macros, not just the machine
The on-hardware checklist ended at "a single-tool test print" and "a multi-tool test print" without saying which print. Any cube proves the motion system; none of them prove START_PRINT's preflight gate, the per-tool purge, the print Z offset, the M106 fan map or the M141 model gate, which are the parts of this firmware that have never run on a printer. bin/gen-test-gcode.py writes one file that drives all of them in the order a real print drives them, and lays down nested squares -- one ring per tool, a fixed gap apart -- so the tool offsets can be measured with calipers afterwards instead of eyeballed. It calibrates nothing: TOOL_OFFSET_CALIBRATE and STATION_CALIBRATE need the build plate off, which a print does not have, so the file only reads the geometry into the log. PAUSE is left out for the same kind of reason -- a resume that fails should not be buried inside the print the offsets are being measured from, so the header tells the operator to press Pause in Mainsail instead. Four things the first draft got wrong on the machine and not on paper: T<n> ships with restore_axis unset, so a change restores no position at all and every move after one has to re-state itself; descending to layer height before the cross-bed travel dragged the nozzle back over the work from the dock; START_PRINT's clean leaves each tool retracted by purge_retract and the start block primes only the tool it starts on, so a tool's first ring was the one recovering those 5 mm; and parked tools sat at full temperature oozing into their docks for the whole print. test_gcode.py is the gate that keeps the file honest: every command in it must be one the shipped configs or extras define, TOOLS= must list every tool the file uses, and every coordinate must be inside the configured axis limits. Renaming a macro now breaks the suite rather than the print. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cd36ff584c |
Restore the exec bit on 31 scripts, so CI can run any of them
CI has never once run this suite. Four runs, four failures, each about
twenty seconds long, each ending the same way:
./test/run-tests.sh: Permission denied exit 126
Not one tracked file in the repo was 100755. `core.filemode` is false on
my checkout and has to be -- the repo lives on a Windows drive where
every file reads back as 0777, so turning it on would mark the entire
tree executable. The consequence is that `chmod +x` here changes nothing
git can see. Locally everything ran anyway, because make invokes the
launchers through bash and because the working tree claims 0777, so the
only machine that could notice was the one nobody was reading.
The payload was never at risk: patch.sh chmods each file as it installs
it onto the printer. This is about the scripts the host executes.
The exec bit was only the first of two. With modes fixed the fixture job
still exits 1 -- honestly this time. It has no stock package by design,
so two gates cannot run: the tests that read the real rootfs, and the
replica. The suite refuses to call a gate it did not run a pass, which
is the rule the whole harness exists to enforce, so the fixture job is
exactly the case ALLOW_SKIP=1 is for. printer-sim and release.yml keep
REQUIRE_PRINTER_SIM=1 and no ALLOW_SKIP, because there a skip must fail.
test_harness.py gains the guard. It reads the mode from `git ls-files
-s`, not from the filesystem: asking this filesystem answers a question
about Windows, not about what was committed. It names each offender and
prints the fix. It passes `-c safe.directory=*`, because git will not
read a repository owned by another uid and every docker run here is root
over a checkout that is not -- without it the test skips inside the
build image, and release.yml runs the suite there with no ALLOW_SKIP, so
a skip would fail the release.
Writing that test turned up a flaw in the neighbouring one.
compileall.compile_file writes the .pyc beside the source and returns
False when it cannot, which under quiet=2 is indistinguishable from a
syntax error and carries an empty message -- a root-owned __pycache__
reported "python that does not compile:" and then nothing at all. It now
compiles through py_compile with an explicit cfile under tmp_path, so it
touches nothing outside the temp dir and reports the real exception.
VERIFIED:
- Reproduced the ci.yml test job against the committed tree: a fresh
clone on ext4, no config.env, no firmware, run in the build image.
Before, exit 126 on the first line. After: shellcheck ok, 7 passed
0 failed 2 skipped, exit 0 -- and without ALLOW_SKIP, exit 1, which
is the behaviour being kept deliberately.
- Full make test with the real firmware: 11 passed, 0 failed, all
three replica gates including recovery.
- The harness tests inside the build image, root over a uid-1000
checkout -- the release.yml shape: 4 passed, nothing skipped.
- Mutation: chmod -x a file and the new test fails naming it; append a
syntax error and the compile test fails with SyntaxError rather than
an empty string; restore and both pass.
- Checked every path, make target and shellcheck glob the workflows
reference: all present, no glob matching zero files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e0dad3513c |
Rewrite the host half of the harness in Python, so a broken gate fails
The shell suite decided "this gate did not run" by grepping the child's
stdout:
if "$@" >"$out" 2>&1; then
if grep -qE '^[[:space:]]*SKIP:|[0-9]+ skipped' "$out"
then skip "$name"; else pass "$name"; fi
Text on stdout cannot tell you what happened inside a process. A launcher that
had ALREADY failed reached that same line by another road: the file that
sources config.env failed to load, so STOCK_TGZ_* was never set, so the
launcher concluded there was nothing to test, printed those five characters
and exited 0 -- on a machine with docker and the firmware sitting right there.
That is how five broken launchers shipped and stayed broken.
So the host half is Python and a skip is an exception. Output cannot imitate
one, and a gate that fails cannot claim it was skipped.
test/ffsim/ config loading, docker plumbing, gate reporting
test/run-tests.py the orchestrator
test/integration/ sim-install.py, sim-roundtrip.py, printer-exec.py,
extract-rootfs.py
test/integration/printer/*.sh and the whole payload stay shell. They are
executed by the printer's own busybox ash under qemu, and surviving that is a
large part of what the suite proves; rewriting them would test a Python the
printer does not have. build-printer-image.sh and make-stock-fixture.sh stay
too: both are `set -euo pipefail`, neither is a gate, and one is a release
tool run by hand.
Four things that used to report as skips are now failures -- a config.env that
does not parse, a missing case script, a missing package, a missing baseline.
A genuinely absent precondition is still a skip, because that is still true.
Three more bug classes are gone by construction rather than by being tested
for. The repo root is found by searching upward for bin/common.sh instead of
counting `..` from $0, so a launcher can move to any depth. Sourcing a config
with a syntax error is checked -- `.` returns non-zero and does NOT stop the
shell, so the old code carried on with half a file applied, which is exactly
the broken-looks-absent confusion again. And pytest counts come from the JUnit
XML rather than from grepping "N skipped".
test_script_roots.py becomes test_harness.py: the `..` check now has two
remaining shell targets rather than seven, and it gains a byte-compile of the
whole harness. That last one matters -- sim-install.py and friends are only
executed by `make test-install`, which needs docker and the firmware, so
without it a typo would wait quietly until release day. It also pins the
property that replaced the `..` counting, from four different depths.
VERIFIED, and the verification is the point, because this machine has no
docker daemon and these are exactly the scripts that cannot run here:
- Stubbed the docker binary and diffed the recorded `docker run` argv,
shell against Python, across five paths: install with a prebuilt image,
install with a local build, local build plus PROG_DUMP, roundtrip, and
printer-exec. IDENTICAL in all five.
- Ran the old orchestrator and the new one in the same tree: both give
6 passed, 1 failed, 2 skipped, exit 1, with the same gates in the same
order. (The failure is shellcheck, which lives in the build image.)
- pytest: 24 passed / 5 skipped bare, 29 passed against a real extracted
rootfs.
- Compared failure modes directly. The four cases above go from "exit 0,
reported skipped" to "exit 1, named". Case 2 is the sharpest: with a
missing case script the shell launcher exited 0 in silence, having cp'd
nothing and mounted a directory over /case.sh.
Still unproven here: the gates that need a live daemon. `make test-install`,
`test-mcu` and `test-recovery` want one run on a docker-capable machine. The
argv diff says they will be handed the same command as before; it cannot say
what docker does with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5bf57cb75a |
ff_toolchange: RESTORE_AXIS, and three more upstream commands
Stage 3 -- RESTORE_AXIS on the change itself. We registered SELECT_TOOL and UNSELECT_TOOL but never read the parameter, and Klipper does not error on unknown parameters, so a config ported from klipper-toolchanger silently lost its position restore. Resolved inside _toolchange so T<n> and TOOLCHANGE -- what a file actually issues -- honour it too, defaulting to [ff_toolchange] restore_axis, itself empty: nothing is restored unless asked, which is how this machine has always behaved. A G-code position is captured and replayed, not a machine one, so it reads back through the offsets in force after the change and the new tool's nozzle lands where the old one was. X/Y first, Z last, so the nozzle is never dragged across the part. Z after a park is the sharp edge and is documented as such: parking zeroes the tool offsets, so the same G-code Z is ~3.2 mm of machine Z away. Stage 4 -- SET_TOOL_TEMPERATURE, VERIFY_TOOL_DETECTED and SELECT_TOOL_ERROR, which UIs send and we answered with "Unknown command". Upstream's docking-mode and tool-parameter commands stay absent: calibration here is TOOL_OFFSET_CALIBRATE / STATION_CALIBRATE / TOOL_Z_ADJUST, already tied to the factory numbers. Untested on hardware: RESTORE_AXIS moves the machine and is off by default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9008a9a00f |
Cut test_script_roots down to the one check that earns it
Four tests here was three too many. The path-existence check guarded the
PREBUILT=0 branch of sim-image.sh, which nobody executes -- with PRINTER_IMAGE
set to the published image the local docker build is never reached. Testing a
branch that is never run is an argument for deleting the branch, not for
guarding it. The two "were any of these actually checked" functions were test
scaffolding for tests; the vacuity concern behind them is real in this repo,
so it is now one assert inside the check it protects rather than two more test
functions.
What stays is the upward-walk check, and the reason it stays is worth writing
down, because the obvious objection to a static check is that the scripts run
and would fail on their own. These do not. Putting the ROOT= bug back into
sim-install.sh and running it gives:
line 22: .../test/test/integration/sim-image.sh: No such file or directory
SKIP: no stock package configured for fake.tgz -- set STOCK_TGZ_* ...
EXIT CODE: 0
The failure conceals itself. sim-image.sh is what sources config.env, so a
broken path to it means STOCK_TGZ_* is never set, the launcher decides it has
nothing to work on, and it skips -- on a machine with docker and the firmware
that could have run the gate. run-tests.sh greps for that exact string and
counts a skip. The suite still goes red, since a skip is not forgiven, but it
sends you to config.env instead of to line 12.
Mutation-tested both halves: a wrong `..` count reports "sim-install.sh:
climbs 1, is 2 deep", and a regex that matches nothing trips the vacuity
assert instead of passing green.
The build-context fixes from the previous commit stand; they were real, and
docker fails outright on a context that is not there.
Verified: 22 passed / 5 skipped without a rootfs, 27 with one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
71044d5867 |
Drop runout_distance and its watcher
The deferred pause was the one thing here Klipper cannot express natively, and it cost a delayed_gcode re-arming itself every second for the length of a print, three scratch variables living in the middle of the policy knobs, and a fourth branch in _FF_RUNOUT -- all to save filament the app also wasted, and only once someone measured their own switch-to-gear path and set the number. It ran on nobody's machine at the default of 0. _FF_RUNOUT_CFG is now two toggles that nothing writes to, so "edit this to configure" and "the macros scribble here" are no longer the same object. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1d8f3091dd |
Say what happened instead of quoting the app's error codes
E0162 / E0163 are FlashForge's numbers, meaningful to someone reading the stock firmware's tables and to nobody standing at the printer. The pause messages now say what happened and what to do, and the two cases stop sharing one string: a clog does not want "LOAD_FILAMENT and RESUME", it wants the jam cleared. The codes stay in the comments and notes that record what the app did -- that is provenance, not user-facing text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
14b8b1fe8f |
Drop the two macro bodies nothing calls
_FF_CHAMBER and _FF_FILAMENT are variable holders: everything reads them as printer["gcode_macro _FF_*"].<var>, and nothing invokes them. Their gcode: bodies ran only if typed by hand, and the leading underscore keeps them out of Mainsail's macro list, so they were hand-run diagnostics with no way to find them. _FF_CHAMBER's said whether a chamber heater exists -- which M141/M191 already gate on themselves -- and _FF_FILAMENT's printed a usage line the file header covers in more detail. Both holders keep an empty gcode:, as _FF_JOB and _FF_RUNOUT_CFG already do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
15ef06dcef |
Ship [ff_print], fold _FF_PRINT_END into END_PRINT, drop the origin flag
Three things the ff_print move left behind.
[ff_print] was declared nowhere. The module shipped (bin/patch.sh copies
ff_*.py) and the docs promised a stock Orca profile would work, but the
section that instantiates it existed only as a commented-out snippet in a
header and was missing from the required-includes list in docs/toolchange.md.
Following the docs got you the module on disk and nothing wrapping
SDCARD_PRINT_FILE -- so a stock profile printed from its own start block,
whose first motion is G1 Z5 F2400 on an unhomed machine. Declared in
ff-print-macros.cfg, which is already required and already included after
[virtual_sdcard].
_FF_PRINT_END existed only so END_PRINT and CANCEL_PRINT could share a body.
END_PRINT is the public exit macro, symmetric with START_PRINT, so it holds
the body and everything calls it.
_FF_JOB.mainsail told CANCEL_PRINT whether we started the job, because
FlashForge's touchscreen sends CANCEL_PRINT itself and then runs its own
motion cleanup that ours must not double. HelixScreen starts prints through
Moonraker exactly as Mainsail does, and the stock UI is not one this mod
ships, so the flag guarded a caller that no longer exists -- along with a
documented corner case where an abnormal end left it stuck at 1.
Plus the text those moves invalidated: the orphaned "WHY THIS WRAPPER EXISTS"
block in ff-toolchange.cfg (its FIXME described four SDCARD_* lines deleted in
|
||
|
|
9d33c8c591 |
Fix the two docker build contexts the move left behind
Moving the replica into test/integration/ rewrote the -f Dockerfile argument
on both `docker build` lines and left the build CONTEXT on the same line
pointing at test/printer, which no longer exists:
docker build -f test/integration/printer/Dockerfile test/printer
The search-and-replace matched the first path on the line and stopped. docker
fails outright on a context that is not there, so both launchers were dead --
but only with PRINTER_IMAGE unset. With the published prebuilt image PREBUILT
is 1 and neither line is reached, which is why nothing noticed.
This is the same accident as the ROOT= breakage and hid the same way: the code
that would have exercised it needs docker and the stock package, so on a
machine missing either it reported a clean skip.
So the guard grows a second half. test_script_roots.py already pins the `..`
count in the upward walks; it now also checks that every path under test/ or
bin/ that a launcher names actually exists. Scope matters in both directions:
- test/integration/printer/ is excluded. Those scripts run inside the
container and then inside a chroot of the printer's own filesystem, where
bin/ is the printer's /bin -- case-mcu-bringup.sh naming
bin/ff-mcu-bringup.py in an error message is correct and would otherwise
be flagged.
- Comments are skipped. They are prose and legitimately name paths that are
gone; run-tests.sh opens by explaining where test/unit went.
Mutation-tested by putting both stale contexts back, which produces
test/integration/build-printer-image.sh:78: names test/printer, ...
test/integration/sim-image.sh:77: names test/printer, ...
Verified: 30 passed against a real extracted rootfs, 25 passed / 5 skipped
without; bash -n clean on both edited scripts. The docker build itself still
has not run here -- there is no daemon on this machine -- so the fix is
verified as a path that now exists, not as a build that now succeeds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
43baf4054e |
Drop the unit lane; one suite, in test/integration
The split existed so a plain pull request had something to run. This repo has one maintainer who always has the firmware to hand, so it was a boundary kept in sync for a contributor who never arrives. One directory now. The tests moved rather than went away. test_chamber.py is the only check in the suite that can catch a printer heating something it has no element for, and the replica cannot cover it: case-install.sh touches .cfg files at exactly two lines, both a grep for a sentinel proving the user config survived, and it asserts klippy.py exists and c_helper.so has the right ABI without ever starting klippy. Declaring [heater_generic chamber_heater] on the plain Creator 5 fails four of these tests, including M191 waiting on a target the machine can never reach; that same package installs, boots, and passes every replica gate. make-stock-fixture.sh came too, since the packaging pipeline is built on it. Ordering changed, and that is the part to look at. With two lanes, pytest ran twice -- once per lane, each with its own -m expression or path. With one it runs once, so it has to run at the point where it can see the most: the stock package is now resolved and the rootfs extracted BEFORE pytest rather than after it. On a configured machine that is 28 tests in one pass; on a bare tree the five that read the rootfs skip. config.env is read in a subshell while doing that. It is the BUILD config, and it is now consulted upstream of the fixture build, where the entire point is that the fixture config wins -- sourcing it into this shell would put MOD_NAME and TARGET_MACHINE where that build could see them. Verified by pointing a config.env at MOD_NAME=LEAKED and confirming the fixture still builds Creator5Pro-anvil-ci.tgz. One thing gets worse, and it is inherent to having a single lane: on a tree with no rootfs, pytest reports 5 skips, so sub() marks the whole gate SKIP rather than ok, and the summary reads 6 passed instead of 7. Those 23 passes are real and the line above the SKIP says so. This is the existing policy working as designed -- a gate that did not fully run must not read as success -- but the summary is coarser than it was when a lane existed that never skipped. Verified: 28 passed with a real extracted rootfs, 23 passed / 5 skipped without; the full suite gives 7 passed / 1 failed / 1 skipped with a rootfs present and 6 / 1 / 2 without, the failure being shellcheck, which lives in the build image; no test/unit reference is left outside the passages that explain why it is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2ae9736e97 |
Name the two lanes unit and integration, and fix what the move broke
test/repo -> test/unit, test/replica -> test/integration. The old names said
where the input comes from; these say what the tests are, which is the thing
you already have a word for.
The rename is also how the bug below surfaced, because "integration" is one
character-count away from nothing and running the launchers by hand is not.
Every launcher that moved out of test/ into a subdirectory opened with
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
and one `..` was correct at test/ and wrong a directory deeper. $ROOT became
test/, so the first thing each of them did -- source $ROOT/test/integration/
sim-image.sh -- went looking for test/test/integration/. Five scripts:
printer-exec.sh, sim-install.sh, sim-roundtrip.sh, build-printer-image.sh and
extract-rootfs.sh, which reached bin/common.sh the same way.
The previous commit shipped this. The path-existence sweep that was supposed
to catch it only checked paths written as literals; these are assembled at
runtime from $0. Nothing else caught it either, and that is the part worth
naming: the integration half cannot run without docker and the stock package,
so on any machine missing either -- this one, and a fork's CI without the
secrets -- five broken scripts still reported a clean SKIP.
So test/unit/test_script_roots.py now checks it statically: for every .sh
under test/, count the `..` after `dirname "$0"` and compare it against how
deep the file actually sits. It needs no docker, no firmware and no shell, so
it runs in the lane that always runs, which is the whole point -- the failure
it pins only appears in the lane that often does not.
Verified: the guard fails with "climbs 1, is 2 deep" against a reintroduced
bug and passes once it is reverted; all four launchers now reach their own
preconditions (docker daemon, config.env) instead of dying on a missing file;
21 in test/unit and 7 in test/integration against a real extracted rootfs;
run-tests.sh reports 7 passed / 1 failed / 1 skipped, the same as before the
rename (shellcheck lives in the build image, and no stock package is
configured in this worktree); no test/repo or test/replica reference is left
anywhere in the tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8337359a68 |
Split test/ into the two lanes it already had
The suite has run in two halves for a long time, and the rest of the repo is
built around that: RUN vs RUNSIM in the Makefile, `test` vs `printer-sim` in
CI, two halves in run-tests.sh. Only test/ itself was flat, so which half a
file belonged to was something you learned by reading its header.
test/repo/ needs nothing but this checkout
test/replica/ needs the printer's real rootfs
test/ run-tests.sh, and the fixtures both lanes share
The axis is the precondition, not the runner. test/replica is everything that
cannot run without the proprietary package -- the qemu'd sim launchers and the
replica image, but also test_paths.py, which only reads the extracted rootfs
as files and needs no docker at all. Grouping by "does it use docker" would
have put that one in the wrong place; grouping by what has to exist first puts
every file where the question "can this run on a public checkout?" is answered
by the directory name.
The `rootfs` pytest marker goes with it. It existed to let run-tests.sh pass
`-m 'not rootfs'` in the PR lane and `-m rootfs` in the replica lane; the
directories say the same thing without anyone having to remember to tag a new
test, and a file in the wrong lane now shows up in `git status`. The two
lanes are `pytest ./test/repo` and `pytest ./test/replica`.
pytest.ini is new and is load-bearing: without a pinned rootdir, naming a lane
on the command line makes pytest take that lane as the root and never load
test/conftest.py, so the fixtures both lanes share would vanish depending on
which one you asked for.
Renames only, plus the path references: test/fixtures/make-stock-fixture.sh ->
test/repo/, the sim launchers and test/printer/ -> test/replica/, and
test_applets.py -> test/replica/test_paths.py, which is what it has actually
checked since the command-word half was dropped.
Verified: 19 in test/repo and 7 in test/replica against a real extracted
rootfs, the same 26 as before the split; run-tests.sh reports 7 passed /
1 failed / 1 skipped, identical to the pre-split run on this host (shellcheck
is absent here and lives in the build image, and no stock package is
configured in this worktree); every referenced path in the repo resolves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
801c772c21 |
ff_toolchange: align the compat views with klipper-toolchanger
Four field-level differences against viesturz/klipper-toolchanger:
- detect_state said 'present'; upstream's DETECT_PRESENT is "mounted".
A stock toolchanger UI read every tool as unavailable.
- toolchanger lacked name, detected_tool, detected_tool_number and
has_detection. We keep no commanded tool state, so detected_* is
the same derived value as tool* -- reported anyway, because a UI
that only reads detected_* must still see a tool.
- tool lacked toolchanger, heater and extruder_stepper.
- the header comment claimed detect_state was omitted on purpose;
it has been emitted from the grab sensor all along.
No behaviour change: the views are read-only and every other addition
is a new key.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a7e0b3fd3e |
Bring the MCU handshake in line with the decompiled stock routine
The level board never left its bootloader, so klippy died on
"mcu 'levelboard': Wait for identify_response". Four separate defects,
three of them in ff-mcu-bringup.py and all of them visible in the
printer's own output once you line the runs up.
The ack. ttyS7 answers 0x01 to 'A', every time, where ttyS4 answers 0x06.
The script sent 'A' once, saw a byte it did not recognise, printed "sent
the banner but acked b'\x01'" and gave up -- leaving the board in its
bootloader. So the three bootSerial* routines were disassembled out of
1.9.7's firmwareExe rather than guessed at any further, and stock does
this:
send = 'A'
for (j = 0; j < 0x32; j++) {
write(fd, &send, 1);
read(fd, &recv, 1);
if (recv == 6) { isAck = 1; break; }
}
Fifty attempts, and the reply is ignored except as a reason to stop
early: isAck is initialised to 1 and never assigned 0 anywhere in the
function, so the ack byte cannot fail the handshake at all. We now do
the same fifty writes and treat a non-0x06 byte as information, not a
verdict.
One port at a time. poke() opened, polled and closed each device in
turn, and a serial port nobody holds open is shut down in the driver --
so every banner ttyS7 sent while ttyS4 was being polled went in the bin.
That is the whole explanation for the two ports taking turns reporting a
banner across consecutive runs. Both are held open for the pass now.
A read count instead of a clock. MAX_READS=50 was documented as a ~5s
ceiling, which holds only when the board says nothing: VTIME=1 bounds an
*idle* read at 0.1s, and a board that is talking returns reads instantly.
Fifty of those can elapse in milliseconds. The pass is on a wall clock
now.
"No banner" reported as "already running". It is not the same thing, and
the printer proved it: ttyS4 was declared already-running on one pass and
sent a bootloader banner on the next. Stock's ready phase is only ~5s and
never waits for a second banner, so it only ever worked when it ran right
after the board powered up; the banner period is longer than that window.
An unknown is reported as an unknown now, and a board that keeps
bannering after fifty 'A' is reported as still in its bootloader.
Separately, start.sh tested -x on the script and printed "missing" while
it sat right there in /usr/data/anvil/bin. It is handed to the
interpreter by path, so the execute bit is irrelevant -- the payload
carries mode 644 in git and only patch.sh and run-append.sh ever chmod
it. Tests -f now.
Verified against ptys standing in for the boards: heat acking 0x06 and
level acking 0x01 both come up; a board that yields 0x06 only on the
eleventh 'A' is caught in 0.2s; a board that keeps bannering is reported
as stuck with a non-zero exit; a silent port reports the real deadline;
missing devices still name both ports and do not raise.
docs/notes/10-hardware.md carries the decompiled handshake, including the
two things the three routines do not share and stock's odd
tcsetattr(fd, TCSETS, ...).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
91604c2dfb |
Cut the suite to four gates, and rewrite the Python half as pytest
Twelve gates, most of them the weaker copy of a gate that survives. What is
left: pytest for the Klipper config, and three replica gates -- the end-to-end
install, MCU bring-up, and stock recovery.
Fix first, because the cut depends on it: a skip counted as a pass. Every
launcher exits 0 when it decides it cannot run, and sub() reported that as
`ok`. That is how test-abi.sh sat in the suite printing green while checking
nothing -- run-tests.sh deleted work/modpayload immediately before it ran and
CI set KLIPPER_FORK="", so it had no targets on any run, ever. Skips are now
counted apart, the summary says so, and the suite exits non-zero unless
ALLOW_SKIP=1 says the gap is deliberate. With four gates instead of twelve, a
gate that silently does not run is the failure mode that matters most.
Dropped, each because something else already does the job properly:
test-abi.sh bin/patch.sh refuses to build a non-nan2008
c_helper.so, bin/verify.sh re-checks the packaged
one, and case-install.sh reads the ELF header of
the file that actually landed on the machine.
test-ash-conformance.sh case-install.sh runs `sh -n` over every installed
script with the same qemu'd busybox and then greps
the boot log for "Syntax error".
test-model-gate.sh verify.sh SS8b/SS9 checks the same invariant and
pack.sh exits on MODEL MISMATCH before a package
exists. Its header claimed it proved the two models
ship different files; no such check was in it.
test-macros.py It used jinja2.Environment() -- default '{{ }}'.
The configs have no '{{' at all, only single-brace
expressions, so it checked '{% %}' block structure
and never looked inside an expression. Folded into
the chamber tests, which had the delimiters right.
test-base-cfg.py It read work/software/klipper/config/printer.base.cfg
as its stock reference -- the file bin/patch.sh
overwrites with ours. It was diffing our file
against itself: green on a cold tree, red on a
second run, blind to real drift either way. The
comparison moves to bin/unpack.sh, where a pristine
tree exists, as a warning rather than a gate.
lint-danger.sh Its structural greps were satisfiable by comments:
firmwareExe:44 and S80ui:16 *mention* helix and
SAFE-MODE, so they passed with the logic deleted.
case-ui.sh Goes with the helix-only decision.
applets.allow Ten entries, none of which the extractor had asked
about in a long time.
The command-word half of test-applets went too. It extracted only the first
word of a simple unprefixed command, so `if timeout 5 foo; then` yielded
['if','echo','fi'] -- the one failure its docstring named was invisible to it.
The absolute-path half survives: S50wifi's binaries sit behind a
[ -d /sys/class/net/wlan0 ] guard no simulation ever reaches, so nothing else
checks they exist.
The Python half is pytest now. Real assertions instead of hand-rolled
PASS/FAIL counters, underscored filenames so the modules import normally
(ffcfg keeps only the parser; the dash in test-macros.py was why one test used
to reach the other's parser through importlib), and a `rootfs` marker that
splits the tests needing the proprietary package from those that do not --
deselected in the PR lane, run in the replica half, so neither lane reports
skips it did not expect. test_default_delimiters_are_blind pins the jinja2 bug
so it cannot come back.
The config gate also moves out of the replica half into the half that runs on
a plain pull request. It needs python3, jinja2 and files in the repo, and it
is the only safety-relevant check in the suite -- a chamber heater declared on
a Creator 5 that has no element for it -- so it had no business being gated
behind the proprietary firmware.
Verified: pytest 21 passed / 5 rootfs-marked; the fixture pipeline
(unpack, patch, pack, verify) passes end to end; injecting a broken
single-brace expression fails test_every_macro_parses with the exact line, and
parses clean under the old defaults; the skip path reports SKIP and exits 1,
and ALLOW_SKIP=1 accepts it.
Also drops the CI `abi` job, which ran on a bare checkout with no binaries and
so was permanently green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3a28fdf65b |
Drop test-chelper and its build gate
test-chelper.py compared c_helper.so's exported symbols against the cdef blocks in the klippy tree shipped beside it, and bin/patch.sh ran it as a hard build gate with ALLOW_STALE_CHELPER=1 as the escape hatch. Both are gone, along with the Makefile target, the suite entry and the ALLOW_STALE_CHELPER passthrough that had nothing left to pass through. patch.sh still refuses a c_helper.so that is not MIPS32r2/nan2008/o32, so the ABI failure mode is still caught. The symbol failure mode is not: cffi resolves lazily, so a .so older than its klippy imports cleanly, passes the ABI check, installs, boots, and dies at connect with a traceback that names cffi rather than the stale build. That is a real thing that shipped once, so it is written down in docs/building.md and docs/notes/20-klipper-fork.md rather than left for someone to rediscover -- the fork path this guarded is winding down anyway, per the same note. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
71cea577c5 |
Drop test-printer-db and test-env
test-printer-db.py is gone entirely. It started as a re-implementation of PrinterDetector's scoring formula and was later narrowed to invariants that do not model anyone's arithmetic; neither version earned the place it took up. The Makefile target, the suite entry and the docs note go with it. test-env.sh was a file whose whole job was to load two other files. Its contents move into sim-image.sh, which every replica launcher already sources or is one exec away from, so the config and the docker plumbing now arrive together instead of as two sources the caller had to remember in order. sim-install.sh and sim-roundtrip.sh source sim-image.sh directly now, which also means a missing docker is reported before a package is staged rather than after. The environment still wins over both config files -- verified in both directions, an explicit PRINTER_IMAGE beats test.env and an unset one falls through to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7f30fc1132 |
Drop the dead weight in test/
Five things in test/ cost a reader attention and bought nothing back. test/applets.allow excused ten command words, and test-applets.py had stopped asking about every one of them. Running its own extractor over payload/ emits none: the payload reaches nginx, python3, helix-screen, klipperDaemon and the rest through an absolute path or a variable, and neither form is a bare command word. Ten entries that excused nothing read like ten commands under review. The file stays as the escape hatch -- empty, with a note saying that empty is the healthy state -- because the loader is what a future bare command would need. test-abi.sh searched assets/mips and assets/dropbear for binaries. Nothing in the repo has ever produced either: the MIPS binaries the mod ships are staged into work/modpayload, and dropbear comes from the stock rootfs. Two directory tests that could not be true, plus the .gitignore line that implied otherwise. sim-mcu-bringup.sh and sim-ui-fallback.sh were eight lines each and did one thing: source test-env.sh, then exec printer-exec.sh with a case script. That source belongs in printer-exec.sh, which needed it anyway -- the direct invocation docs/printer-replica.md documents never saw PRINTER_IMAGE and so rebuilt the local replica image on every run instead of using the prebuilt one. With it moved, both wrappers are pass-throughs, and the Makefile and run-tests.sh name the case scripts directly. Sourcing test-env.sh twice is harmless: the environment wins over both files, and the first pass exported them. Two comments named files that do not exist -- test/test-applets.sh and test/test-base-cfg.sh, both .py for a long time now. Also removes the checked-out test/__pycache__: bytecode for two Python versions, including a stale test-applets build. Nothing here changes what the suite tests. lint-danger, test-abi and test-model-gate were re-run and report exactly what they did before; every touched script still parses. Makefile, run-tests.sh and printer-exec.sh had uncommitted changes of their own, landed in the previous commit; this one edits them further. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3a968d4219 |
Land the ffcfg refactor that was sitting in the working tree
Carried in from the uncommitted working tree, not authored here. test/ffcfg.py gives the Python checks one parser and one set of PASS/FAIL helpers, so test-chamber, test-base-cfg and test-printer-db stop re-implementing the reporting and stop reaching test-macros' parser through importlib (the dash in that filename made a plain import impossible). test/sim-image.sh does the same for the docker plumbing the replica launchers duplicated. Both files were untracked, and run-tests.sh already imports them: the suite could not run from a clean clone until they were committed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2ad7304397 |
Drop assets/orca and the last four fork-only calls
The Orca profile is left stock, so there is nothing to ship for it: assets/orca
held copies of FlashForge's own start/end blocks, which a user already has and
is told not to touch. Removed, along with the two docs/toolchange.md references.
The four remaining fork-only calls in _FF_PRINT_END are gone too. Each undid a
line-rewriting override that only FlashForge's virtual_sdcard ever performed,
and that nothing in this tree switches on:
SET_FAN_M106[P2] ADJUSTED= FACTOR= added FACTOR*255/100 to every M106 the
fork passed through (fork lines 498-504)
SET_PA_ADVANCE T0..T3 ENABLE= replaced the file's own
SET_PRESSURE_ADVANCE with a per-tool
value (fork line 479)
MUTE_MODE_DISABLE never a command at all, here or in the
fork: mute was M204 S61166 (0xEEEE), a
sentinel accel read in toolhead.py that
clamped max_accel
Upstream rewrites nothing, so the file's own M106 and SET_PRESSURE_ADVANCE
apply as written and there is no override to clear. Nothing in payload/ ever
set ENABLE=1 or ADJUSTED=1, and HelixScreen does not send any of them, so no
behaviour is lost. Should a UI want live fan, PA or quiet overrides they are
new features against upstream Klipper, not end-of-print cleanup -- noted where
the calls used to be.
That is the last of them: no SDCARD_*, SET_PA_ADVANCE, SET_FAN_M106[P2] or
MUTE_MODE call remains in payload/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
35aa61fccb |
Keep the Orca profile stock; prepare from the file by default
The decision is that a FlashForge Orca profile is not edited at all: no START_PRINT pasted into Machine start G-code, and files sliced before the mod keep printing. So FF_BEFORE_PRINT_START.prepare is now 1, and [ff_print] runs the whole of START_PRINT before the file's first line. The stock block needs exactly that. It prepares nothing: M140 and M104 without waiting, fans, M191, then `G1 Z5 F2400` as its first motion on a machine nothing has homed, then T<n>, M109 and the prime line. No G28, so nothing homes; no M190, so nothing waits for the bed. The touchscreen app did both before handing the file over. Its end block is one move -- no heaters off, no M84, no park -- so FF_AFTER_PRINT_END is what ends the job. assets/orca/*.txt now hold the stock blocks themselves, as the reference for what the mod expects rather than something to paste. The previous contents were a modified start block carrying a START_PRINT call, which is no longer what a user should have. docs/toolchange.md said to paste both files into the profile; it now says to leave the profile alone, and documents prepare=0 plus an explicit START_PRINT as the way to drive the sequence from the slicer instead. Its TOOLS=/TEMPS= paragraph is updated for the paired TOOLS=0:220,2:240 form and for clean temperatures now coming from _FF_FILAMENT.tool_material. The two prepare=0 cases stay documented on the variable: a profile that calls START_PRINT itself, and one that homes for itself -- a generic Orca block runs G28 after M140/M104, and the G28 wrapper docks a mounted tool before homing Z. Untested on hardware. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
764af2aacd |
Drop SDCARD_SET_CHANNEL: upstream needs no channel
FlashForge's virtual_sdcard tracked a channel so it could rewrite bare
M104/M109 to " T<channel>" and SET_PRESSURE_ADVANCE to pa_value_t<channel>.
The upstream virtual_sdcard we ship does no such rewriting and needs none:
both apply to the ACTIVE extruder, which _grab (or the ACTIVATE_EXTRUDER on
the same-tool path) has just set to this tool. That is also why the stock
start block's bare `M104 S<t>` lands on the right hotend.
Like the four SDCARD_* calls in
|
||
|
|
848db028d0 |
Drop the four SDCARD_* calls: nothing left to clear
They cleared per-job state that FlashForge's virtual_sdcard fork carried over
from the last touchscreen job -- a stale `refuelling` flag, FFM, and the two
"print this job on another extruder" remap flags. We ship the upstream
virtual_sdcard (KLIPPER_FORK branch creator5), which has none of that state:
refuelling, change_filament, enable_ffm, need_check_ex, no_filament_check and
print_channel are all absent from its 308 lines, against the fork's 617. The
commands that cleared them are absent too.
They were never breaking anything. Klipper's cmd_default ends in
respond_info('Unknown command:"%s"'), not a raise, so all four were console
noise at the start of every Mainsail print and no more. Removing them rather
than shimming them: there is no state to shim.
The docs/notes references stay -- they describe FlashForge's fork, which is
still what they say it is.
SDCARD_SET_CHANNEL in ff_toolchange.py is the same case and still carries its
FIXME; left for a separate change since it sits on the toolchange path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
86be9462b4 |
Merge the published master: same tree, unrelated history
origin/master and this history have no common ancestor -- the repo was re-committed locally at some point, so the six published commits and the six they were made from carry the same messages and the same tree under different hashes. Their content was already identical, so this merge adds nothing; it exists so the import lands on top of what is published rather than replacing it, and the push stays a fast-forward. Resolved -X ours. That keeps this side's tree except for three files the import deleted, which the unrelated-history merge sees as added on the remote side and would resurrect; they are removed again here so the merged tree matches the branch exactly: profiles/default.env profiles/probe.env payload/report.sh |
||
|
|
8208fef216 |
ff_print: restore the SDCARD_* FIXME lost in the move
|