Files
firmware/docs/notes/40-offsets.md
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

4.7 KiB
Raw Permalink Blame History

Offsets & per-unit config: where the numbers live and how they are applied

There is exactly one config directory

Config::load reads /usr/data/firmwareRes/config/ and nothing else (initManagers @0x412efc; /usr/data/config/test.json is only a one-shot install hook — copied in, then deleted). Files: general.json filament.json extruder.json network.json time.json print.json test.json zoffset.json total.json userInfo.json.

They are not strict JSON — each has a trailing C-style comment after the closing brace, so use json.JSONDecoder().raw_decode().

Key files:

  • extruder.json (loadExtruderConfig @0x64f7f8): t0..t3_offset_x/y/z (per-tool nozzle calibration), x/y_check_pos..3 (per-dock coords), x/y/z_station_pos, now_extruder.
  • test.json (@0x654380): grabSpeed, grabSpeedSlow (mm/s, ×60; fallbacks 24000/6000 grab, 5400 release-slow), grabOffset (dock X correction), tempOffset (0.00045 — see below), generalFirmware.
  • zoffset.json (@0x65621c): z_offset_t1..t4 — the USER's per-tool Z tune from the UI menu, 1-based (z_offset_t1 is tool 0). Not calibration data.

Never write these files

Config::syncExtruderConfig @0x651388 rewrites the whole file from the app's in-memory struct on every toolchange — anything written externally is clobbered, and a partial write could revert unrelated per-unit calibration. On the klipper-vanilla branch these files are not read at runtime at all: ff_legacy.py's FF_IMPORT_FIRMWARE_CONFIG imports them once into [ff_tool n] / [ff_tool_offset] (SAVE_CONFIG block of printer.cfg), and calibration is redone from Klipper with TOOL_LOCATE_SENSOR / TOOL_CALIBRATE_TOOL_OFFSET (see 45-tool-offset-calibration.md).

Per-tool offsets — setGrabGcodeOffsetMgr @0x77f1dc

Applied on every grab (print start serialPrint and mid-print changeExtruderChannel both pass the enabling flag):

X = t<tool>_offset_x - t<base>_offset_x
Y = t<tool>_offset_y - t<base>_offset_y
Z = z_offset_t<tool+1> + m_zOffset + (t<tool>_offset_z - t<base>_offset_z)

emitted as SET_GCODE_OFFSET X=.. Y=.. MOVE=1 MOVE_SPEED=100 + SET_GCODE_OFFSET Z=.. MOVE=1 MOVE_SPEED=40 (3 decimals). They are differences against a base tool (default T0; the app rebases to the file's initial tool via setBaseExtruder — the base term cancels algebraically either way). m_zOffset is the once-per-print absolute base (below) plus any live babystep the user dialed in.

Example spread on one unit: T1 sits (−0.008, −0.599, −0.089) from T0 — with offsets omitted, T1 prints ~0.6 mm out in Y.

Babystep handling in the port: Klipper folds babysteps into the same G-code Z offset, so the port recovers it as current_offset − last_applied_tool_term and re-adds it, mirroring the app's separate register.

The absolute print Z offset — BuildPage::startPrint @0x9fc148

The single most dangerous fact of the whole project: the eddy probe's G28 Z is NOT nozzle-at-bed zero — after homing, the nozzle sits ~3.2 mm below where the coordinate claims, and nothing physical prevents driving it into the plate (position_min: -10).

Two sensors exist: the eddy coil on the head (used by G28 Z / bed mesh — triggers at a coil-to-bed distance) and a fixed nozzle-touch sensor below the bed plane. Factory calibration stores, in extruder.json:

  • z_station_pos — Z where the fixed sensor fired under the EMPTY carriage (≈ −1.68); it detects a carriage feature ~12.4 mm +X of the nozzle, not the eddy coil (corrected after the asm recovery, see 46-offset-calibration-recovered.md)
  • t0..t3_offset_z — Z where each NOZZLE touched the same sensor (≈ +1.5)

so t<n>_offset_z − z_station_pos ≈ the 3.19 mm gap between the station's empty-carriage trigger plane and tool n's nozzle plane. At print start the app computes:

m_zOffset = t<tool>_offset_z − z_station_pos
          + (nozzle_temp − 120) × tempOffset          # thermal expansion, 0.00045/°C
          + 0.08 if bed_temp ≥ 100
          − 0.06 if layer_height ≤ 0.10

and sends SET_GCODE_OFFSET X=0 Y=0 Z=<m_zOffset + z_offset_t<n+1>> MOVE=1 right before starting the print thread. Typical value: Z ≈ +3.24 for PLA 220/80/0.25. A Mainsail print that skips this runs the whole job ~3.2 mm low — this exact miss destroyed the first test print's first layer.

The port: TOOLCHANGE_SET_PRINT_OFFSET NOZZLE= BED= LAYER= [TOOL=] implements the formula from the same JSON; START_PRINT calls it after the first grab; the babystep recovery above carries the base through every subsequent toolchange (verified numerically against this unit's JSON: T0 220/80/0.25 → 3.240; later T2 grab → 3.197 = m_z + (t2−t0)). END/CANCEL reset Z=0.