Commit Graph
7 Commits
Author SHA1 Message Date
Oleksandr ShyshatskyiandClaude Opus 5 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
2026-08-30 23:03:37 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 a22f3b04ef Tool calibration speaks klipper-toolchanger's G-code
STATION_CALIBRATE is TOOL_LOCATE_SENSOR and TOOL_OFFSET_CALIBRATE is
TOOL_CALIBRATE_TOOL_OFFSET, with upstream's call shape as well as its
names: no TOOL= (the mounted tool is the one measured, so SELECT_TOOL
first, as upstream's macro does) and no PLATE_REMOVED=.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 13:49:14 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 5798606e48 The pre-HelixScreen program waits for the boards, and says which one
It only ever ran on a first boot, and only to migrate. Every other boot it
exited at its stamp in microseconds and HelixScreen started immediately --
against a klippy that was very often still hunting for its MCUs. What the
owner saw then was a UI reporting a disconnected printer, with nothing
anywhere naming the board that had not answered.

So ff-firstboot-import.py becomes ff-startup.py, with the migration demoted to
its second job. Every boot it now waits: first for ff-mcu-bringup.py, which
start.sh runs alongside it, to finish handing the toolhead boards over from
their bootloaders, then for klipper and moonraker to be ready. Only after that
does it look at the stamp and decide whether there is a migration to do.

Naming the board needed a source. ff-mcu-bringup.py now publishes what it is
still working on to /tmp/ff-mcu-bringup.status -- rewritten only when the
outstanding set actually shrinks, renamed into place so a reader never sees
half a file, and on a tmpfs so it cannot survive a reboot and be mistaken for
this boot's. The panel turns the serial port into something an owner can act
on: WAITING FOR THE HEATER BOARD, not /dev/ttyS4. When klippy fails outright
its own state_message is the only place the guilty board is named, so that is
parsed too and becomes THE HEATER BOARD DID NOT ANSWER.

The MCU wait is one we are happy to abandon. A bring-up that never reports --
because it has not started yet, or wrote something malformed, or is simply
absent -- times out into the klipper wait rather than holding the boot: klippy
is the real authority on whether a board answered, and it has a far better
message than we could write. Tests pin that, including a parametrised sweep of
malformed status files.

The two halves of that file format are checked against each other rather than
against a copy of it written down twice.

MOD_STARTUP=0 skips the whole thing; MOD_IMPORT=0 keeps the wait and skips
only the migration. 20 frames, still byte-identical host to replica.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 21:04:07 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 97caf27e1c The factory-calibration import moves out of klippy
Importing this unit's nozzle/dock/station numbers out of firmwareExe's JSON
is a Creator-5 chore that happens once per INSTALL, and it lived inside a
general-purpose Klipper extra on a klippy:ready hook -- the one moment klippy
cannot know whether the rest of the machine is up. It also had no memory of
having run: auto_import re-decided itself on every ready by guessing from
"has anything been calibrated yet", so the lifetime was wrong in both
directions.

It is now bin/ff-firstboot-import.py, its own program with one job, which the
firmwareExe wrapper runs ahead of HelixScreen. It exits on a stat() of its
stamp (every boot but the first) or when there is no firmwareRes JSON at all;
otherwise it waits until klipper, moonraker AND Mainsail are all answering,
sends FF_IMPORT_FIRMWARE_CONFIG and SAVE_CONFIG over the moonraker API, rides
out the klippy restart that save causes, verifies the values came back saved,
and only then stamps. It stands down if a tool is already calibrated or if
someone else's SAVE_CONFIG is pending -- neither is ours to overwrite.

Nothing short of a verified save writes the stamp. The heater board on this
machine routinely needs several klippy restarts before it answers, so "the
stack was not up in time" is a normal event to retry on the next boot, not a
failure to record forever. It runs in the foreground before the UI because
the save restarts klippy, and doing that behind a HelixScreen already showing
a printer is worse than making the splash wait; MOD_IMPORT_TIMEOUT bounds it
and MOD_IMPORT=0 turns it off.

BREAKING: [ff_legacy] auto_import and auto_save are gone, and klipper refuses
to start on an unknown option -- anyone who set either one in printer.cfg has
to remove it. FF_IMPORT_FIRMWARE_CONFIG itself is unchanged.

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

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

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

48 passed, 5 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 19:48:43 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 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>
2026-08-24 19:48:42 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 0ef9d6b86b Move the imported toolchange files into this repo's layout
Pure renames, no content changes, so `git log --follow` walks straight
back into the toolchange history imported by the previous commit.

  klippy-extras/ff_*.py -> payload/klipper/extras/
  config/ff-*.cfg       -> payload/klipper/config/
  helixscreen/*.json    -> payload/helixscreen/printer_database.d/
  orca/                 -> assets/orca/
  docs/notes/           -> docs/notes/
  README.md             -> docs/toolchange.md

payload/ is the right home for the first three: they are files that
ship to the printer, which is what everything else under payload/ is.
The Orca snippets go to assets/ instead -- they are pasted into a
slicer on the desktop and never reach the machine.

bin/patch.sh still reads them from $TOOLCHANGE; the next commit points
it here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgNhSa1Qvwvxb2nQXT6b1Y
2026-08-23 16:48:25 +03:00