From 6f3e534d0bfda0aa2634e50fd6492d22eb0144a8 Mon Sep 17 00:00:00 2001 From: ogland Date: Fri, 9 Oct 2026 10:59:04 +0200 Subject: [PATCH 1/6] Check every pickup and park with the load cell --- macros/indx-tc-macros.cfg | 217 ++++++++++++++++++++++++++++++++++++++ macros/indx.cfg | 27 +++++ 2 files changed, 244 insertions(+) diff --git a/macros/indx-tc-macros.cfg b/macros/indx-tc-macros.cfg index 90db5cb..3ada2d7 100644 --- a/macros/indx-tc-macros.cfg +++ b/macros/indx-tc-macros.cfg @@ -577,12 +577,17 @@ gcode: [gcode_macro _PARK_TOOL_APPLY] description: Inner - the park itself. Split from PARK_TOOL so active_tool is read after any homing has run. +# With seat_check_enabled the load cell is read with the tool still on the +# head and again after the exit: if it has not changed by park_min_change_g, +# the tool did not come off and the park stops (_INDX_PARK_CHECK). CHECK=0 +# skips that, for the pickup retry, which parks a tool that may not be there. gcode: {% set st = printer["gcode_macro _TOOLCHANGE_VARS"].toolhead_state|int %} {% set act = printer.save_variables.variables.active_tool|default(-1)|int %} {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} {% set c = printer["gcode_macro _INDX_CONSTANTS"] %} {% set zhop = params.ZHOP|default(0.0)|float %} + {% set check = params.CHECK|default(1)|int and tp.seat_check_enabled|default(True) %} {% if act != -1 %} # Resolve this tool's dock geometry {% set tool = tp.tools[act] %} @@ -610,6 +615,12 @@ gcode: # High-speed sprint to trigger line _TC_G0 Y={"%.3f"|format(trigger_y)} + {% if check %} + # Load cell with the tool still seated, for the park check below + M400 + G4 P250 + _INDX_LC_STORE VAR=seated + {% endif %} {% if st == 0 %} _SYNC_UNLOCK_DANCE DOCK_Y={dock_y} @@ -621,10 +632,34 @@ gcode: M400 _LINEAR_EXIT + {% if check %} + # Did the tool really leave the head? If not, this raises with the + # tool kept as active_tool, before the head drives anywhere else. + M400 + G4 P250 + _INDX_LC_STORE VAR=now + _INDX_PARK_CHECK TOOL={act} + {% endif %} _INDX_SET_ACTIVE_TOOL TOOL=-1 {% endif %} [gcode_macro _PICKUP_TOOL] +description: Inner - pick up TOOL, check with the load cell that it is seated (one re-home and retry), then offsets, record and heat +# Every step is its own macro so each renders when it runs and sees what the +# step before it found. A failed seat check re-homes X/Y and tries once more. +# After that a print pauses and anything else stops (_PICKUP_SEAT_FAIL). +# With seat_check_enabled False, or no calibrated load cell, the check passes +# and this is the same pickup as before. +gcode: + {% set rz = (" RESTORE_Z=" ~ params.RESTORE_Z) if params.RESTORE_Z|default("")|string|length else "" %} + _PICKUP_GRAB TOOL={params.TOOL} ZHOP={params.ZHOP|default(0.0)}{rz} + _INDX_SEAT_CHECK TOOL={params.TOOL} + _PICKUP_SEAT_RETRY TOOL={params.TOOL}{rz} + _PICKUP_SEAT_FAIL TOOL={params.TOOL} TARGET={params.TARGET|default(0)} SKIP_Z_CORRECTION={params.SKIP_Z_CORRECTION|default(0)} + _PICKUP_FINISH TOOL={params.TOOL} TARGET={params.TARGET|default(0)} SKIP_Z_CORRECTION={params.SKIP_Z_CORRECTION|default(0)} + +[gcode_macro _PICKUP_GRAB] +description: Inner - the pickup moves (to the dock, open, engage, lock, exit). Only called by _PICKUP_TOOL and its retry, which check the seat and record the tool. gcode: {% set t = params.TOOL|int %} {% set st = printer["gcode_macro _TOOLCHANGE_VARS"].toolhead_state|int %} @@ -632,6 +667,7 @@ gcode: {% set c = printer["gcode_macro _INDX_CONSTANTS"] %} {% set zhop = params.ZHOP|default(0.0)|float %} {% set restore_z = params.RESTORE_Z|default("")|string %} + {% set check = tp.seat_check_enabled|default(True) %} # Resolve this tool's dock geometry {% set tool = tp.tools[t] %} @@ -653,6 +689,14 @@ gcode: # Sprint to trigger line _TC_G0 Y={"%.3f"|format(trigger_y)} + {% if check %} + # Empty-head load cell reading, seconds before the pickup. The seat + # check measures against it, so load cell drift (about 40 g per degree + # C) drops out. + M400 + G4 P250 + _INDX_LC_STORE VAR=empty + {% endif %} # Latch moves run through INDX_EXTRUDER_MOVE so pickup works on a cold tool # without heating it. INDX_EXTRUDER_MOVE moves are inherently relative. @@ -679,6 +723,94 @@ gcode: # Z hop-down is folded into _LINEAR_EXIT's Y-clearance sprint (non-blocking) _LINEAR_EXIT RESTORE_Z={restore_z} M400 + {% if check %} + # Let the load cell settle, then read it for the seat check + G4 P300 + M400 + _INDX_LC_STORE VAR=now + {% endif %} + +[gcode_macro _PICKUP_SEAT_RETRY] +description: Inner - seat check failed: re-home X/Y, park the tool in case it is on the head, pick it up again +# A misread seat check must not drop a tool that is really on the head, so +# the retry parks it first: a tool on the head goes back into its dock, and +# with an empty head the park is the dock visit that resets the latch. +gcode: + {% if not printer["gcode_macro _INDX_SEAT_CHECK"].ok|int %} + {% set t = params.TOOL|int %} + {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} + RESPOND TYPE=error MSG="T{t} did not seat (load cell {printer['gcode_macro _INDX_SEAT_CHECK'].force_g|round(0)|int} g against the empty head). Re-homing X/Y and trying once more." + M104 S0 + # Over a print the X/Y home sweeps across it: lift z_hop first + {% if "z" in printer.toolhead.homed_axes %} + {% set z = [printer.toolhead.position.z|float + tp.z_hop|float, printer.toolhead.axis_maximum.z|float - 1]|min %} + G90 + G1 Z{"%.3f"|format(z)} F900 + {% endif %} + _INDX_REHOME_XY + _INDX_SET_ACTIVE_TOOL TOOL={t} + _PARK_TOOL_APPLY CHECK=0 + _PICKUP_GRAB TOOL={t}{(" RESTORE_Z=" ~ params.RESTORE_Z) if params.RESTORE_Z|default("")|string|length else ""} + _INDX_SEAT_CHECK TOOL={t} + {% endif %} + +[gcode_macro _INDX_REHOME_XY] +description: Inner - home X and Y again, from inside G28 too +# Inside the homing override G28 is Klipper's own homing, so the INDX steps +# around it (Y before X, X at the clearance line) are done here. +gcode: + {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} + {% if printer["gcode_macro _TOOLCHANGE_VARS"].homing_busy|default(0)|int %} + G28 Y + G90 + G0 Y{"%.3f"|format(tp.clearance_y|float)} F{tp.home_travel_feedrate|default(3000)|int} + G28 X + {% else %} + G28 X Y + {% endif %} + +[gcode_macro _PICKUP_SEAT_FAIL] +description: Inner - still not seated after the retry: pause a print, stop anything else, heater off and no tool recorded +# The print pauses with the base PAUSE, so nothing moves and nothing is +# retracted from a head without a tool. The new tool's offsets are set first +# (MOVE=0), so the position the pause saves is in the coordinates the print +# continues in once the tool change has been redone by hand. Inside G28 (the +# T0 fetch of a print start) it stops instead: _INDX_RAISE also undoes the +# provisional Z home. +gcode: + {% if not printer["gcode_macro _INDX_SEAT_CHECK"].ok|int %} + {% set t = params.TOOL|int %} + {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} + {% set f = printer["gcode_macro _INDX_SEAT_CHECK"].force_g|round(0)|int %} + {% set min_g = tp.seat_min_force_g|default(800.0)|float %} + {% set why = "T%d did not seat after a re-home and a second try (load cell %d g against the empty head, a seated tool shows at least %d g). Check the T%d dock and the head. If T%d is on the head, run MANUAL_TOOL_SEAT TOOL=%d to claim it, or MANUAL_TOOL_REMOVE and take it off" % (t, f, min_g, t, t, t) %} + M104 S0 + _INDX_SET_ACTIVE_TOOL TOOL=-1 + {% set busy = printer["gcode_macro _TOOLCHANGE_VARS"].homing_busy|default(0)|int %} + {% if printer.print_stats.state == "printing" and not busy %} + {% set svv = printer.save_variables.variables %} + {% set tool_z = svv["t" ~ t ~ "_offset_z"]|default(0.0)|float %} + {% set global_z = svv.global_z_offset|default(0.0)|float %} + {% set skip_z = params.SKIP_Z_CORRECTION|default(0)|int %} + {% set apply_z = global_z if skip_z else (tool_z + global_z) %} + SET_GCODE_OFFSET X={svv["t"~t~"_offset_x"]|default(0.0)} Y={svv["t"~t~"_offset_y"]|default(0.0)} Z='{"%.3f"|format(apply_z)}' MOVE=0 + {% set cmd = "CHANGE_TOOL TOOL=%d TEMP=%s%s" % (t, params.TARGET|default(0), " SKIP_Z_CORRECTION=1" if skip_z else "") %} + _INDX_LCD LEVEL=action MSG="T{t} pickup failed: check dock" + RESPOND TYPE=error MSG="{why}. Print paused with the heater off. When the head is right, run {cmd}, then RESUME." + {% set pm = printer.configfile.settings["gcode_macro pause"] %} + {pm.rename_existing if pm is defined and pm.rename_existing is defined else "PAUSE"} + {% else %} + _INDX_LCD LEVEL=action MSG="T{t} pickup failed: check dock" + RESPOND TYPE=error MSG="{why}. Stopped with the heater off. Leave T{t} in its dock and the head empty, then try again." + _INDX_RAISE MSG="T{t} pickup failed: not seated, see above" + {% endif %} + {% endif %} + +[gcode_macro _PICKUP_FINISH] +description: Inner - seated: apply offsets and filament, record the tool, heat +gcode: + {% if printer["gcode_macro _INDX_SEAT_CHECK"].ok|int %} + {% set t = params.TOOL|int %} # Apply new tool offsets. # Z = tool_z + global_z so the toolhead lands at the correct first-layer height. @@ -700,6 +832,7 @@ gcode: # Record the tool now that the pickup is physically complete. Before the # heating below: M104/M109 only heat the tool recorded as on the head. _RECORD_TOOLCHANGE TOOL={t} + _INDX_LCD_CLEAR # Start heating only after tool is fully locked, exited dock, and offsets applied. # M109 rather than M104: the tool was parked cold and the heater was off for the @@ -709,6 +842,90 @@ gcode: {% if params.TARGET|default(0)|float > 0 %} M109 S{params.TARGET|default(0)|float} {% endif %} + {% endif %} + +[gcode_macro _INDX_LC_STORE] +description: Inner - store the load cell reading as VAR=empty (head empty, before a pickup), seated (before a park) or now (the reading a check compares) +# The reading counts from the empty-head calibration in printer.cfg +# (reference_tare_counts), not from the last tare. The checks only compare +# two readings taken standing still seconds apart, so the load cell's thermal +# drift (about 40 g per degree C, hundreds of grams over a day) drops out. +# No calibrated load cell: the reading is marked invalid and the checks are +# skipped, with one console line per restart. +variable_empty_g: 0.0 +variable_empty_valid: 0 +variable_seated_g: 0.0 +variable_seated_valid: 0 +variable_now_g: 0.0 +variable_now_valid: 0 +variable_skip_said: 0 +gcode: + {% set var = params.VAR|lower %} + {% set lc = printer.load_cell_probe if "load_cell_probe" in printer else {} %} + {% set cfgl = printer.configfile.settings.load_cell_probe|default({}) %} + {% set ref = cfgl.reference_tare_counts|default(none) %} + {% if ref is not none and lc.is_calibrated|default(false) + and lc.counts_per_gram|default(0)|float > 0 + and lc.tare_counts is defined and lc.force_g|default(none) is not none %} + {% if ref is not number %}{% set ref = ref|sum %}{% endif %} + {% set inv = -1.0 if cfgl.sensor_orientation|default("normal") == "inverted" else 1.0 %} + {% set g = lc.force_g|float + inv * (lc.tare_counts|float - ref|float) / lc.counts_per_gram|float %} + SET_GCODE_VARIABLE MACRO=_INDX_LC_STORE VARIABLE={var}_g VALUE={g} + SET_GCODE_VARIABLE MACRO=_INDX_LC_STORE VARIABLE={var}_valid VALUE=1 + {% else %} + SET_GCODE_VARIABLE MACRO=_INDX_LC_STORE VARIABLE={var}_valid VALUE=0 + {% if not printer["gcode_macro _INDX_LC_STORE"].skip_said|int %} + SET_GCODE_VARIABLE MACRO=_INDX_LC_STORE VARIABLE=skip_said VALUE=1 + RESPOND TYPE=error MSG="INDX: pickup and park checks skipped, the load cell is not calibrated. Run CALIBRATE_LOAD_CELL (see Load cell calibration in the README), or set seat_check_enabled: False in indx.cfg." + {% endif %} + {% endif %} + +[gcode_macro _INDX_SEAT_CHECK] +description: Inner - end of a pickup: does the load cell see a tool on the head? Stores ok and force_g +# Compares the reading after the pickup with the empty-head reading taken at +# the trigger line just before it. A seated tool adds its latch spring +# preload (1100 to 1900 g on the test machine), an empty head stays near +# zero. Without this a missed pickup leaves active_tool set on an empty head. +# Threshold: seat_min_force_g in indx.cfg. Checks off or no load cell: ok. +variable_ok: 1 +variable_force_g: 0.0 +gcode: + {% set e = printer["gcode_macro _INDX_LC_STORE"] %} + {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} + {% set ns = namespace(ok=1, force=0.0) %} + {% if tp.seat_check_enabled|default(True) and e.empty_valid|int and e.now_valid|int %} + {% set ns.force = e.now_g|float - e.empty_g|float %} + {% set ns.ok = 1 if ns.force|abs >= tp.seat_min_force_g|default(800.0)|float else 0 %} + {% if ns.ok %} + RESPOND MSG="T{params.TOOL} seated: load cell {ns.force|round(0)|int} g against the empty head" + {% endif %} + {% endif %} + SET_GCODE_VARIABLE MACRO=_INDX_LC_STORE VARIABLE=empty_valid VALUE=0 + SET_GCODE_VARIABLE MACRO=_INDX_LC_STORE VARIABLE=now_valid VALUE=0 + SET_GCODE_VARIABLE MACRO=_INDX_SEAT_CHECK VARIABLE=ok VALUE={ns.ok} + SET_GCODE_VARIABLE MACRO=_INDX_SEAT_CHECK VARIABLE=force_g VALUE={ns.force} + +[gcode_macro _INDX_PARK_CHECK] +description: Inner - after a park: did the load cell change by about the tool's preload? If not the tool may still be on the head: stop, heater off, tool kept as active +# Seated tools read 1100 to 1900 g away from an empty head on the test +# machine, so a park that really released the tool changes the reading by +# that much. Both readings are taken standing still, seconds apart. +# Threshold: park_min_change_g in indx.cfg. Checks off or no load cell: no check. +gcode: + {% set t = params.TOOL|int %} + {% set e = printer["gcode_macro _INDX_LC_STORE"] %} + {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} + SET_GCODE_VARIABLE MACRO=_INDX_LC_STORE VARIABLE=seated_valid VALUE=0 + SET_GCODE_VARIABLE MACRO=_INDX_LC_STORE VARIABLE=now_valid VALUE=0 + {% if tp.seat_check_enabled|default(True) and e.seated_valid|int and e.now_valid|int %} + {% set change = e.now_g|float - e.seated_g|float %} + {% if change|abs < tp.park_min_change_g|default(500.0)|float %} + M104 S0 + _INDX_LCD LEVEL=warn MSG="T{t} may still be on the head" + RESPOND TYPE=error MSG="T{t} may still be on the head: the load cell changed only {change|round(0)|int} g at the park (a released tool changes it by its preload, about 1100 to 1900 g). Stopped with the heater off and T{t} kept as the tool on the head. Check the head. If the heater also reported a model divergence, a blob from a detached print may be stuck on the nozzle." + _INDX_RAISE MSG="T{t} may still be on the head after the park, see above" + {% endif %} + {% endif %} ##################################################################### # 6. HELPER MACROS diff --git a/macros/indx.cfg b/macros/indx.cfg index cf2b60e..5128d4b 100644 --- a/macros/indx.cfg +++ b/macros/indx.cfg @@ -100,6 +100,33 @@ variable_home_check_seat: False # lower only if a seated tool is refused. variable_home_min_force_g: 800.0 +# ── Pickup and park checks ─────────────────────────────────────────────────── +# seat_check_enabled = check every pickup and every park with the load cell. +# On by default. Each check compares two readings taken +# standing still seconds apart, so the tare and the +# load cell's drift do not matter, unlike home_check_seat. +# With no calibrated load cell the checks are skipped +# with one console line. Set False to turn them off. +# Write True or False, capitalised. +# +# Pickup: the reading after the pickup must differ from +# the empty head before it by seat_min_force_g. If not, +# X/Y is re-homed and the tool picked up once more. If +# that fails too, a print pauses and anything else stops, +# with the heater off and no tool recorded. +# +# Park: the reading after the drop-off must differ from +# the seated one by park_min_change_g. If not, the tool +# is still on the head: INDX stops with the heater off +# and keeps the tool recorded as on the head. +variable_seat_check_enabled: True + +# seat_min_force_g = grams a seated tool must show against the empty head. +# Seated tools show 1100 to 1900 g on the test machine. +# park_min_change_g = grams the reading must change by when a tool is parked. +variable_seat_min_force_g: 800.0 +variable_park_min_change_g: 500.0 + # ── Dock approach direction ────────────────────────────────────────────────── # dock_dir = Y sign into the dock: -1 front (min Y), +1 rear (max Y). # Flips trigger line, unlock depths, peel, and the homing From 51b95b5aed35601b03902386f9d51d838e7432bb Mon Sep 17 00:00:00 2001 From: ogland Date: Fri, 9 Oct 2026 11:01:39 +0200 Subject: [PATCH 2/6] Document the pickup and park checks; clearer failure messages --- README.md | 11 +++++++++++ macros/indx-tc-macros.cfg | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d61336e..c3ab620 100644 --- a/README.md +++ b/README.md @@ -1069,6 +1069,17 @@ INDX's own `M104` and `M109` (in `indx-tc-macros.cfg`) enforce this: with no too Warnings that are dangerous to ignore (`! ...`, for example a failed homing or `clearance_y` too close to a dock) and moments when you need to act (`> ...`, for example docks not calibrated yet) are also shown with `M117` on the printer display. The console keeps the full message. An INDX alert clears itself after the next successful `G28`, and never clears your own `M117` text. +#### Pickup and park checks + +INDX checks every pickup and every park with the load cell. This is on by default (`seat_check_enabled: True` in `indx.cfg`) and is separate from the homing [seat check](#seat-check) above. Each check compares two readings taken standing still a few seconds apart, so the tare and the load cell's drift with temperature do not affect it. + +- **Pickup.** The head reads the load cell at the trigger line with the head empty, picks up the tool, and reads it again. A seated tool adds its latch spring preload, so the two readings must differ by at least `seat_min_force_g` (default 800 g). If they don't, the console says so, X and Y are homed again, and the tool is parked and picked up once more. If the second try fails too, the heater is turned off and no tool is recorded. During a print, the print pauses, with nothing moved or retracted: put the tool back in its dock and leave the head empty (`MANUAL_TOOL_REMOVE` opens the latch), run the `CHANGE_TOOL` command the console gives you, then `RESUME`. Outside a print, and during the T0 fetch of a `G28`, INDX stops with an error. +- **Park.** The head reads the load cell with the tool still seated, parks it, and reads it again after leaving the dock. The readings must differ by at least `park_min_change_g` (default 500 g). If they don't, the tool is probably still on the head: INDX stops with an error before driving anywhere else, turns the heater off, and keeps the tool recorded as on the head. Check the head. If the heater also reported a model divergence, a blob from a detached print may be stuck on the nozzle. + +Successful pickups print the reading in the console, for example `T1 seated: load cell -1650 g against the empty head`, which is useful when tuning `seat_min_force_g`. A failed check also shows a short alert on the printer display. + +Without a calibrated load cell the checks are skipped, with one line in the console after each restart. To turn them off, set `variable_seat_check_enabled: False` in `indx.cfg`. Tool changes then work as they did before the checks existed. If you keep your own `indx.cfg`, the checks are on with the defaults above until you add these settings. + #### RRF (RepRapFirmware) Configure the Bondtech INDX PCB in RRF following the [Duet INDX Toolboard documentation](https://docs.duet3d.com/en/Duet3D_hardware/Duet_3_family/INDX_Toolboard). diff --git a/macros/indx-tc-macros.cfg b/macros/indx-tc-macros.cfg index 3ada2d7..21da65b 100644 --- a/macros/indx-tc-macros.cfg +++ b/macros/indx-tc-macros.cfg @@ -783,7 +783,7 @@ gcode: {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} {% set f = printer["gcode_macro _INDX_SEAT_CHECK"].force_g|round(0)|int %} {% set min_g = tp.seat_min_force_g|default(800.0)|float %} - {% set why = "T%d did not seat after a re-home and a second try (load cell %d g against the empty head, a seated tool shows at least %d g). Check the T%d dock and the head. If T%d is on the head, run MANUAL_TOOL_SEAT TOOL=%d to claim it, or MANUAL_TOOL_REMOVE and take it off" % (t, f, min_g, t, t, t) %} + {% set why = "T%d did not seat after a re-home and a second try (load cell %d g against the empty head, a seated tool shows at least %d g). Check the T%d dock and the head" % (t, f, min_g, t) %} M104 S0 _INDX_SET_ACTIVE_TOOL TOOL=-1 {% set busy = printer["gcode_macro _TOOLCHANGE_VARS"].homing_busy|default(0)|int %} @@ -796,12 +796,12 @@ gcode: SET_GCODE_OFFSET X={svv["t"~t~"_offset_x"]|default(0.0)} Y={svv["t"~t~"_offset_y"]|default(0.0)} Z='{"%.3f"|format(apply_z)}' MOVE=0 {% set cmd = "CHANGE_TOOL TOOL=%d TEMP=%s%s" % (t, params.TARGET|default(0), " SKIP_Z_CORRECTION=1" if skip_z else "") %} _INDX_LCD LEVEL=action MSG="T{t} pickup failed: check dock" - RESPOND TYPE=error MSG="{why}. Print paused with the heater off. When the head is right, run {cmd}, then RESUME." + RESPOND TYPE=error MSG="{why}. Print paused with the heater off. Put T{t} back in its dock and leave the head empty (MANUAL_TOOL_REMOVE opens the latch), then run {cmd} and RESUME." {% set pm = printer.configfile.settings["gcode_macro pause"] %} {pm.rename_existing if pm is defined and pm.rename_existing is defined else "PAUSE"} {% else %} _INDX_LCD LEVEL=action MSG="T{t} pickup failed: check dock" - RESPOND TYPE=error MSG="{why}. Stopped with the heater off. Leave T{t} in its dock and the head empty, then try again." + RESPOND TYPE=error MSG="{why}. Stopped with the heater off. If T{t} is on the head, run MANUAL_TOOL_SEAT TOOL={t} to claim it, or MANUAL_TOOL_REMOVE and put it back in its dock, then try again." _INDX_RAISE MSG="T{t} pickup failed: not seated, see above" {% endif %} {% endif %} From ae8546868779cefe837d4229e20498a0121c6063 Mon Sep 17 00:00:00 2001 From: ogland Date: Fri, 9 Oct 2026 11:05:45 +0200 Subject: [PATCH 3/6] Keep a possibly half seated tool recorded after a failed pickup --- README.md | 2 +- macros/indx-tc-macros.cfg | 63 ++++++++++++++++++++++++++++----------- macros/indx.cfg | 9 +++++- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c3ab620..2355340 100644 --- a/README.md +++ b/README.md @@ -1073,7 +1073,7 @@ Warnings that are dangerous to ignore (`! ...`, for example a failed homing or ` INDX checks every pickup and every park with the load cell. This is on by default (`seat_check_enabled: True` in `indx.cfg`) and is separate from the homing [seat check](#seat-check) above. Each check compares two readings taken standing still a few seconds apart, so the tare and the load cell's drift with temperature do not affect it. -- **Pickup.** The head reads the load cell at the trigger line with the head empty, picks up the tool, and reads it again. A seated tool adds its latch spring preload, so the two readings must differ by at least `seat_min_force_g` (default 800 g). If they don't, the console says so, X and Y are homed again, and the tool is parked and picked up once more. If the second try fails too, the heater is turned off and no tool is recorded. During a print, the print pauses, with nothing moved or retracted: put the tool back in its dock and leave the head empty (`MANUAL_TOOL_REMOVE` opens the latch), run the `CHANGE_TOOL` command the console gives you, then `RESUME`. Outside a print, and during the T0 fetch of a `G28`, INDX stops with an error. +- **Pickup.** The head reads the load cell at the trigger line with the head empty, picks up the tool, and reads it again. A seated tool adds its latch spring preload, so the two readings must differ by at least `seat_min_force_g` (default 800 g). If they don't and the difference is below `seat_empty_max_g` (default 200 g), the head is empty: the console says so, X and Y are homed again, and the tool is parked and picked up once more. If the second try fails too, the heater is turned off and no tool is recorded. A difference between `seat_empty_max_g` and `seat_min_force_g` means the tool may be half seated on the head. INDX never assumes the head is empty, so there is no retry: the heater is turned off, the tool stays recorded as on the head, the latch is left as it is, and the display shows `T may be on the head`. Check the head by hand, then run `MANUAL_TOOL_REMOVE` (take the tool off and put it in its dock) or `PARK_TOOL`. Either way, during a print the print pauses, with nothing moved or retracted: get the tool back in its dock with the head empty, run the `CHANGE_TOOL` command the console gives you, then `RESUME`. Outside a print, and during the T0 fetch of a `G28`, INDX stops with an error. - **Park.** The head reads the load cell with the tool still seated, parks it, and reads it again after leaving the dock. The readings must differ by at least `park_min_change_g` (default 500 g). If they don't, the tool is probably still on the head: INDX stops with an error before driving anywhere else, turns the heater off, and keeps the tool recorded as on the head. Check the head. If the heater also reported a model divergence, a blob from a detached print may be stuck on the nozzle. Successful pickups print the reading in the console, for example `T1 seated: load cell -1650 g against the empty head`, which is useful when tuning `seat_min_force_g`. A failed check also shows a short alert on the printer display. diff --git a/macros/indx-tc-macros.cfg b/macros/indx-tc-macros.cfg index 21da65b..4e8dd1a 100644 --- a/macros/indx-tc-macros.cfg +++ b/macros/indx-tc-macros.cfg @@ -735,8 +735,12 @@ description: Inner - seat check failed: re-home X/Y, park the tool in case it is # A misread seat check must not drop a tool that is really on the head, so # the retry parks it first: a tool on the head goes back into its dock, and # with an empty head the park is the dock visit that resets the latch. +# An ambiguous reading (something may be on the head) gets no retry: the +# retry re-homes X/Y and parks without a check, with a tool hanging off the +# head. _PICKUP_SEAT_FAIL stops and asks instead. gcode: - {% if not printer["gcode_macro _INDX_SEAT_CHECK"].ok|int %} + {% set sc = printer["gcode_macro _INDX_SEAT_CHECK"] %} + {% if not sc.ok|int and not sc.ambiguous|int %} {% set t = params.TOOL|int %} {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} RESPOND TYPE=error MSG="T{t} did not seat (load cell {printer['gcode_macro _INDX_SEAT_CHECK'].force_g|round(0)|int} g against the empty head). Re-homing X/Y and trying once more." @@ -770,22 +774,34 @@ gcode: {% endif %} [gcode_macro _PICKUP_SEAT_FAIL] -description: Inner - still not seated after the retry: pause a print, stop anything else, heater off and no tool recorded +description: Inner - the pickup did not seat: pause a print, stop anything else, heater off. Clearly empty head: no tool recorded. Anything in between: the tool stays recorded +# Never assume the head is empty. A reading below seat_empty_max_g is an +# empty head and no tool is recorded. A reading between that and +# seat_min_force_g means something may be on the head, a half seated tool: +# the tool stays recorded, so the next move parks it instead of driving a +# loaded head into another dock. The latch is left as it is. # The print pauses with the base PAUSE, so nothing moves and nothing is -# retracted from a head without a tool. The new tool's offsets are set first -# (MOVE=0), so the position the pause saves is in the coordinates the print -# continues in once the tool change has been redone by hand. Inside G28 (the -# T0 fetch of a print start) it stops instead: _INDX_RAISE also undoes the -# provisional Z home. +# retracted. The new tool's offsets are set first (MOVE=0), so the position +# the pause saves is in the coordinates the print continues in once the tool +# change has been redone by hand. Inside G28 (the T0 fetch of a print start) +# it stops instead: _INDX_RAISE also undoes the provisional Z home. gcode: - {% if not printer["gcode_macro _INDX_SEAT_CHECK"].ok|int %} + {% set sc = printer["gcode_macro _INDX_SEAT_CHECK"] %} + {% if not sc.ok|int %} {% set t = params.TOOL|int %} {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} - {% set f = printer["gcode_macro _INDX_SEAT_CHECK"].force_g|round(0)|int %} + {% set f = sc.force_g|round(0)|int %} {% set min_g = tp.seat_min_force_g|default(800.0)|float %} - {% set why = "T%d did not seat after a re-home and a second try (load cell %d g against the empty head, a seated tool shows at least %d g). Check the T%d dock and the head" % (t, f, min_g, t) %} + {% set ambiguous = sc.ambiguous|int %} + {% if ambiguous %} + {% set why = "T%d may be half seated on the head: the load cell reads %d g against the empty head, more than an empty head (under %d g) but less than a seated tool (%d g). T%d is kept as the tool on the head. Check it by hand" % (t, f, tp.seat_empty_max_g|default(200.0)|float, min_g, t) %} + {% set lcd = "T%d may be on the head" % t %} + {% else %} + {% set why = "T%d did not seat after a re-home and a second try (load cell %d g against the empty head, a seated tool shows at least %d g). Check the T%d dock and the head" % (t, f, min_g, t) %} + {% set lcd = "T%d pickup failed: check dock" % t %} + {% endif %} M104 S0 - _INDX_SET_ACTIVE_TOOL TOOL=-1 + _INDX_SET_ACTIVE_TOOL TOOL={t if ambiguous else -1} {% set busy = printer["gcode_macro _TOOLCHANGE_VARS"].homing_busy|default(0)|int %} {% if printer.print_stats.state == "printing" and not busy %} {% set svv = printer.save_variables.variables %} @@ -795,14 +811,22 @@ gcode: {% set apply_z = global_z if skip_z else (tool_z + global_z) %} SET_GCODE_OFFSET X={svv["t"~t~"_offset_x"]|default(0.0)} Y={svv["t"~t~"_offset_y"]|default(0.0)} Z='{"%.3f"|format(apply_z)}' MOVE=0 {% set cmd = "CHANGE_TOOL TOOL=%d TEMP=%s%s" % (t, params.TARGET|default(0), " SKIP_Z_CORRECTION=1" if skip_z else "") %} - _INDX_LCD LEVEL=action MSG="T{t} pickup failed: check dock" - RESPOND TYPE=error MSG="{why}. Print paused with the heater off. Put T{t} back in its dock and leave the head empty (MANUAL_TOOL_REMOVE opens the latch), then run {cmd} and RESUME." + _INDX_LCD LEVEL={'warn' if ambiguous else 'action'} MSG="{lcd}" + {% if ambiguous %} + RESPOND TYPE=error MSG="{why}, then MANUAL_TOOL_REMOVE (opens the latch: take the tool off and put it in its dock) or PARK_TOOL. Print paused with the heater off. With the head empty, run {cmd}, then RESUME." + {% else %} + RESPOND TYPE=error MSG="{why}. Print paused with the heater off. Put T{t} back in its dock and leave the head empty (MANUAL_TOOL_REMOVE opens the latch), then run {cmd} and RESUME." + {% endif %} {% set pm = printer.configfile.settings["gcode_macro pause"] %} {pm.rename_existing if pm is defined and pm.rename_existing is defined else "PAUSE"} {% else %} - _INDX_LCD LEVEL=action MSG="T{t} pickup failed: check dock" - RESPOND TYPE=error MSG="{why}. Stopped with the heater off. If T{t} is on the head, run MANUAL_TOOL_SEAT TOOL={t} to claim it, or MANUAL_TOOL_REMOVE and put it back in its dock, then try again." - _INDX_RAISE MSG="T{t} pickup failed: not seated, see above" + _INDX_LCD LEVEL={'warn' if ambiguous else 'action'} MSG="{lcd}" + {% if ambiguous %} + RESPOND TYPE=error MSG="{why}, then MANUAL_TOOL_REMOVE (opens the latch: take the tool off and put it in its dock) or PARK_TOOL. Stopped with the heater off." + {% else %} + RESPOND TYPE=error MSG="{why}. Stopped with the heater off. If T{t} is on the head, run MANUAL_TOOL_SEAT TOOL={t} to claim it, or MANUAL_TOOL_REMOVE and put it back in its dock, then try again." + {% endif %} + _INDX_RAISE MSG="T{t} pickup failed: {'may be on the head' if ambiguous else 'not seated'}, see above" {% endif %} {% endif %} @@ -887,15 +911,19 @@ description: Inner - end of a pickup: does the load cell see a tool on the head? # preload (1100 to 1900 g on the test machine), an empty head stays near # zero. Without this a missed pickup leaves active_tool set on an empty head. # Threshold: seat_min_force_g in indx.cfg. Checks off or no load cell: ok. +# A failed reading at or above seat_empty_max_g is ambiguous: something may +# be on the head (a half seated tool), so it is not treated as empty. variable_ok: 1 variable_force_g: 0.0 +variable_ambiguous: 0 gcode: {% set e = printer["gcode_macro _INDX_LC_STORE"] %} {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} - {% set ns = namespace(ok=1, force=0.0) %} + {% set ns = namespace(ok=1, force=0.0, ambiguous=0) %} {% if tp.seat_check_enabled|default(True) and e.empty_valid|int and e.now_valid|int %} {% set ns.force = e.now_g|float - e.empty_g|float %} {% set ns.ok = 1 if ns.force|abs >= tp.seat_min_force_g|default(800.0)|float else 0 %} + {% set ns.ambiguous = 1 if not ns.ok and ns.force|abs >= tp.seat_empty_max_g|default(200.0)|float else 0 %} {% if ns.ok %} RESPOND MSG="T{params.TOOL} seated: load cell {ns.force|round(0)|int} g against the empty head" {% endif %} @@ -904,6 +932,7 @@ gcode: SET_GCODE_VARIABLE MACRO=_INDX_LC_STORE VARIABLE=now_valid VALUE=0 SET_GCODE_VARIABLE MACRO=_INDX_SEAT_CHECK VARIABLE=ok VALUE={ns.ok} SET_GCODE_VARIABLE MACRO=_INDX_SEAT_CHECK VARIABLE=force_g VALUE={ns.force} + SET_GCODE_VARIABLE MACRO=_INDX_SEAT_CHECK VARIABLE=ambiguous VALUE={ns.ambiguous} [gcode_macro _INDX_PARK_CHECK] description: Inner - after a park: did the load cell change by about the tool's preload? If not the tool may still be on the head: stop, heater off, tool kept as active diff --git a/macros/indx.cfg b/macros/indx.cfg index 5128d4b..4b28228 100644 --- a/macros/indx.cfg +++ b/macros/indx.cfg @@ -113,7 +113,10 @@ variable_home_min_force_g: 800.0 # the empty head before it by seat_min_force_g. If not, # X/Y is re-homed and the tool picked up once more. If # that fails too, a print pauses and anything else stops, -# with the heater off and no tool recorded. +# with the heater off and no tool recorded. A reading +# between seat_empty_max_g and seat_min_force_g means +# something may be half seated on the head: no retry, +# the tool stays recorded, and it pauses or stops. # # Park: the reading after the drop-off must differ from # the seated one by park_min_change_g. If not, the tool @@ -123,8 +126,12 @@ variable_seat_check_enabled: True # seat_min_force_g = grams a seated tool must show against the empty head. # Seated tools show 1100 to 1900 g on the test machine. +# seat_empty_max_g = below this a failed pickup counts as an empty head. +# From here up to seat_min_force_g it counts as a tool +# that may be half seated, and INDX keeps it recorded. # park_min_change_g = grams the reading must change by when a tool is parked. variable_seat_min_force_g: 800.0 +variable_seat_empty_max_g: 200.0 variable_park_min_change_g: 500.0 # ── Dock approach direction ────────────────────────────────────────────────── From 4ae919356fc35f12074ff2204ad70a7fa325c8cd Mon Sep 17 00:00:00 2001 From: ogland Date: Fri, 9 Oct 2026 11:08:49 +0200 Subject: [PATCH 4/6] Refuse homing and tool changes until a possibly half seated tool is checked --- README.md | 2 +- macros/homing.cfg | 5 +++++ macros/indx-cal.cfg | 3 +++ macros/indx-tc-macros.cfg | 34 ++++++++++++++++++++++++++++++---- macros/indx.cfg | 5 ++++- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2355340..b73748d 100644 --- a/README.md +++ b/README.md @@ -1073,7 +1073,7 @@ Warnings that are dangerous to ignore (`! ...`, for example a failed homing or ` INDX checks every pickup and every park with the load cell. This is on by default (`seat_check_enabled: True` in `indx.cfg`) and is separate from the homing [seat check](#seat-check) above. Each check compares two readings taken standing still a few seconds apart, so the tare and the load cell's drift with temperature do not affect it. -- **Pickup.** The head reads the load cell at the trigger line with the head empty, picks up the tool, and reads it again. A seated tool adds its latch spring preload, so the two readings must differ by at least `seat_min_force_g` (default 800 g). If they don't and the difference is below `seat_empty_max_g` (default 200 g), the head is empty: the console says so, X and Y are homed again, and the tool is parked and picked up once more. If the second try fails too, the heater is turned off and no tool is recorded. A difference between `seat_empty_max_g` and `seat_min_force_g` means the tool may be half seated on the head. INDX never assumes the head is empty, so there is no retry: the heater is turned off, the tool stays recorded as on the head, the latch is left as it is, and the display shows `T may be on the head`. Check the head by hand, then run `MANUAL_TOOL_REMOVE` (take the tool off and put it in its dock) or `PARK_TOOL`. Either way, during a print the print pauses, with nothing moved or retracted: get the tool back in its dock with the head empty, run the `CHANGE_TOOL` command the console gives you, then `RESUME`. Outside a print, and during the T0 fetch of a `G28`, INDX stops with an error. +- **Pickup.** The head reads the load cell at the trigger line with the head empty, picks up the tool, and reads it again. A seated tool adds its latch spring preload, so the two readings must differ by at least `seat_min_force_g` (default 800 g). If they don't and the difference is below `seat_empty_max_g` (default 200 g), the head is empty: the console says so, X and Y are homed again, and the tool is parked and picked up once more. If the second try fails too, the heater is turned off and no tool is recorded. A difference between `seat_empty_max_g` and `seat_min_force_g` means the tool may be half seated on the head. INDX never assumes the head is empty, so there is no retry: the heater is turned off, the tool stays recorded as on the head, the latch is left as it is, and the display shows `T may be on the head`. Until the head has been checked, `G28` (when it homes Z), `CHANGE_TOOL` and `PARK_TOOL` refuse to run, also after a restart. Check the head by hand, then run `MANUAL_TOOL_REMOVE` (take the tool off and put it in its dock) or `MANUAL_TOOL_SEAT TOOL=` (it is fully on). Either one, or `MANUAL_TOOLHEAD_RESET`, clears the refusal. Either way, during a print the print pauses, with nothing moved or retracted: get the tool back in its dock with the head empty, run the `CHANGE_TOOL` command the console gives you, then `RESUME`. Outside a print, and during the T0 fetch of a `G28`, INDX stops with an error. - **Park.** The head reads the load cell with the tool still seated, parks it, and reads it again after leaving the dock. The readings must differ by at least `park_min_change_g` (default 500 g). If they don't, the tool is probably still on the head: INDX stops with an error before driving anywhere else, turns the heater off, and keeps the tool recorded as on the head. Check the head. If the heater also reported a model divergence, a blob from a detached print may be stuck on the nozzle. Successful pickups print the reading in the console, for example `T1 seated: load cell -1650 g against the empty head`, which is useful when tuning `seat_min_force_g`. A failed check also shows a short alert on the printer display. diff --git a/macros/homing.cfg b/macros/homing.cfg index e30a8a7..17ab086 100644 --- a/macros/homing.cfg +++ b/macros/homing.cfg @@ -72,6 +72,11 @@ gcode: or (home_x and "y" not in printer.toolhead.homed_axes) %} {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} _INDX_CHECK_CLEARANCE + # A failed pickup may have left a tool half seated on the head: no Z home + # until the head has been checked by hand + {% if home_z %} + _INDX_CHECK_SUSPECT + {% endif %} # In the first seconds after a restart INIT_STATE has not built the tools # list yet; the T0 fetch below needs it {% if tp.tools|length == 0 %} diff --git a/macros/indx-cal.cfg b/macros/indx-cal.cfg index 04abe10..a2285ac 100644 --- a/macros/indx-cal.cfg +++ b/macros/indx-cal.cfg @@ -1107,6 +1107,7 @@ gcode: M400 SET_GCODE_VARIABLE MACRO=_TOOLCHANGE_VARS VARIABLE=toolhead_state VALUE=2 SAVE_VARIABLE VARIABLE=toolhead_state VALUE=2 + SAVE_VARIABLE VARIABLE=seat_suspect VALUE=-1 _INDX_SET_ACTIVE_TOOL TOOL=-1 RESPOND MSG="⚠️ WARNING! Toolhead latch OPEN! Be sure to REMOVE any tools by hand!!! The toolhead status & active tool have been RESET to OPEN & EMPTY!!" @@ -1149,6 +1150,7 @@ gcode: SET_GCODE_VARIABLE MACRO=_TOOLCHANGE_VARS VARIABLE=toolhead_state VALUE=0 SAVE_VARIABLE VARIABLE=toolhead_state VALUE=0 + SAVE_VARIABLE VARIABLE=seat_suspect VALUE=-1 _INDX_SET_ACTIVE_TOOL TOOL={t} RESPOND MSG="Latch locked, T{t} claimed. G28 Z will now probe with it. If this is not the tool that is physically on the head, run MANUAL_TOOL_REMOVE and start again." @@ -1157,6 +1159,7 @@ description: ⚠️ Resets toolhead macro status to OPEN & EMPTY - USE WITH CAUT gcode: SET_GCODE_VARIABLE MACRO=_TOOLCHANGE_VARS VARIABLE=toolhead_state VALUE=2 SAVE_VARIABLE VARIABLE=toolhead_state VALUE=2 + SAVE_VARIABLE VARIABLE=seat_suspect VALUE=-1 _INDX_SET_ACTIVE_TOOL TOOL=-1 M400 TOOL_STATUS diff --git a/macros/indx-tc-macros.cfg b/macros/indx-tc-macros.cfg index 4e8dd1a..46c45a7 100644 --- a/macros/indx-tc-macros.cfg +++ b/macros/indx-tc-macros.cfg @@ -443,6 +443,21 @@ gcode: _INDX_RAISE MSG="{msg}" {% endif %} +[gcode_macro _INDX_CHECK_SUSPECT] +description: Inner - refuse to move while a failed pickup may have left a tool half seated on the head (saved variable seat_suspect) +# _PICKUP_SEAT_FAIL sets seat_suspect to the tool number when the load cell +# read more than an empty head but less than a seated tool. Until someone has +# looked at the head, any move could drive a loose tool into a dock or probe +# Z with it, so G28 Z, CHANGE_TOOL and PARK_TOOL refuse. It is a saved +# variable, so it survives a restart. MANUAL_TOOL_REMOVE, MANUAL_TOOL_SEAT +# and MANUAL_TOOLHEAD_RESET clear it. +gcode: + {% set t = printer.save_variables.variables.seat_suspect|default(-1)|int %} + {% if t >= 0 %} + _INDX_LCD LEVEL=warn MSG="T{t} may be on the head" + _INDX_RAISE MSG="T{t} may be half seated on the head. Check the head by hand, then run MANUAL_TOOL_REMOVE (take it off) or MANUAL_TOOL_SEAT TOOL={t} (it is fully on)." + {% endif %} + [gcode_macro CHANGE_TOOL] gcode: {% if 'TOOL' not in params %} @@ -450,6 +465,7 @@ gcode: {% endif %} _INDX_CHECK_DOCKS _INDX_CHECK_CLEARANCE + _INDX_CHECK_SUSPECT {% set t = params.TOOL|int %} {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} @@ -563,6 +579,7 @@ description: Park the tool currently on the head back in its dock. gcode: _INDX_CHECK_DOCKS _INDX_CHECK_CLEARANCE + _INDX_CHECK_SUSPECT {% if printer["gcode_macro TOOL_POSITIONS"].tools|length == 0 %} _INDX_BUILD_TOOLS {% endif %} @@ -778,8 +795,9 @@ description: Inner - the pickup did not seat: pause a print, stop anything else, # Never assume the head is empty. A reading below seat_empty_max_g is an # empty head and no tool is recorded. A reading between that and # seat_min_force_g means something may be on the head, a half seated tool: -# the tool stays recorded, so the next move parks it instead of driving a -# loaded head into another dock. The latch is left as it is. +# the tool stays recorded and seat_suspect is saved, so G28 Z, CHANGE_TOOL +# and PARK_TOOL refuse until the head has been checked by hand +# (_INDX_CHECK_SUSPECT). The latch is left as it is. # The print pauses with the base PAUSE, so nothing moves and nothing is # retracted. The new tool's offsets are set first (MOVE=0), so the position # the pause saves is in the coordinates the print continues in once the tool @@ -813,19 +831,27 @@ gcode: {% set cmd = "CHANGE_TOOL TOOL=%d TEMP=%s%s" % (t, params.TARGET|default(0), " SKIP_Z_CORRECTION=1" if skip_z else "") %} _INDX_LCD LEVEL={'warn' if ambiguous else 'action'} MSG="{lcd}" {% if ambiguous %} - RESPOND TYPE=error MSG="{why}, then MANUAL_TOOL_REMOVE (opens the latch: take the tool off and put it in its dock) or PARK_TOOL. Print paused with the heater off. With the head empty, run {cmd}, then RESUME." + RESPOND TYPE=error MSG="{why}, then MANUAL_TOOL_REMOVE (opens the latch: take the tool off and put it in its dock) or MANUAL_TOOL_SEAT TOOL={t} (it is fully on). Tool changes and G28 are refused until then. Print paused with the heater off. With the head empty, run {cmd}, then RESUME." {% else %} RESPOND TYPE=error MSG="{why}. Print paused with the heater off. Put T{t} back in its dock and leave the head empty (MANUAL_TOOL_REMOVE opens the latch), then run {cmd} and RESUME." {% endif %} + {% if ambiguous %} + # Last, so nothing in the retry or fail path runs into it + SAVE_VARIABLE VARIABLE=seat_suspect VALUE={t} + {% endif %} {% set pm = printer.configfile.settings["gcode_macro pause"] %} {pm.rename_existing if pm is defined and pm.rename_existing is defined else "PAUSE"} {% else %} _INDX_LCD LEVEL={'warn' if ambiguous else 'action'} MSG="{lcd}" {% if ambiguous %} - RESPOND TYPE=error MSG="{why}, then MANUAL_TOOL_REMOVE (opens the latch: take the tool off and put it in its dock) or PARK_TOOL. Stopped with the heater off." + RESPOND TYPE=error MSG="{why}, then MANUAL_TOOL_REMOVE (opens the latch: take the tool off and put it in its dock) or MANUAL_TOOL_SEAT TOOL={t} (it is fully on). Tool changes and G28 are refused until then. Stopped with the heater off." {% else %} RESPOND TYPE=error MSG="{why}. Stopped with the heater off. If T{t} is on the head, run MANUAL_TOOL_SEAT TOOL={t} to claim it, or MANUAL_TOOL_REMOVE and put it back in its dock, then try again." {% endif %} + {% if ambiguous %} + # Last, so nothing in the retry or fail path runs into it + SAVE_VARIABLE VARIABLE=seat_suspect VALUE={t} + {% endif %} _INDX_RAISE MSG="T{t} pickup failed: {'may be on the head' if ambiguous else 'not seated'}, see above" {% endif %} {% endif %} diff --git a/macros/indx.cfg b/macros/indx.cfg index 4b28228..e0de754 100644 --- a/macros/indx.cfg +++ b/macros/indx.cfg @@ -116,7 +116,10 @@ variable_home_min_force_g: 800.0 # with the heater off and no tool recorded. A reading # between seat_empty_max_g and seat_min_force_g means # something may be half seated on the head: no retry, -# the tool stays recorded, and it pauses or stops. +# the tool stays recorded, and it pauses or stops. G28 Z, +# CHANGE_TOOL and PARK_TOOL then refuse, also after a +# restart, until the head is checked by hand and +# MANUAL_TOOL_REMOVE or MANUAL_TOOL_SEAT TOOL= is run. # # Park: the reading after the drop-off must differ from # the seated one by park_min_change_g. If not, the tool From bd08a243ae9402834f6263a55d5caeacf83b747d Mon Sep 17 00:00:00 2001 From: ogland Date: Fri, 9 Oct 2026 11:25:32 +0200 Subject: [PATCH 5/6] Seat and park check thresholds 15 % more lenient: 680 g seated, 230 g empty, 425 g park change --- README.md | 4 ++-- macros/indx-tc-macros.cfg | 10 +++++----- macros/indx.cfg | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b73748d..70553fe 100644 --- a/README.md +++ b/README.md @@ -1073,8 +1073,8 @@ Warnings that are dangerous to ignore (`! ...`, for example a failed homing or ` INDX checks every pickup and every park with the load cell. This is on by default (`seat_check_enabled: True` in `indx.cfg`) and is separate from the homing [seat check](#seat-check) above. Each check compares two readings taken standing still a few seconds apart, so the tare and the load cell's drift with temperature do not affect it. -- **Pickup.** The head reads the load cell at the trigger line with the head empty, picks up the tool, and reads it again. A seated tool adds its latch spring preload, so the two readings must differ by at least `seat_min_force_g` (default 800 g). If they don't and the difference is below `seat_empty_max_g` (default 200 g), the head is empty: the console says so, X and Y are homed again, and the tool is parked and picked up once more. If the second try fails too, the heater is turned off and no tool is recorded. A difference between `seat_empty_max_g` and `seat_min_force_g` means the tool may be half seated on the head. INDX never assumes the head is empty, so there is no retry: the heater is turned off, the tool stays recorded as on the head, the latch is left as it is, and the display shows `T may be on the head`. Until the head has been checked, `G28` (when it homes Z), `CHANGE_TOOL` and `PARK_TOOL` refuse to run, also after a restart. Check the head by hand, then run `MANUAL_TOOL_REMOVE` (take the tool off and put it in its dock) or `MANUAL_TOOL_SEAT TOOL=` (it is fully on). Either one, or `MANUAL_TOOLHEAD_RESET`, clears the refusal. Either way, during a print the print pauses, with nothing moved or retracted: get the tool back in its dock with the head empty, run the `CHANGE_TOOL` command the console gives you, then `RESUME`. Outside a print, and during the T0 fetch of a `G28`, INDX stops with an error. -- **Park.** The head reads the load cell with the tool still seated, parks it, and reads it again after leaving the dock. The readings must differ by at least `park_min_change_g` (default 500 g). If they don't, the tool is probably still on the head: INDX stops with an error before driving anywhere else, turns the heater off, and keeps the tool recorded as on the head. Check the head. If the heater also reported a model divergence, a blob from a detached print may be stuck on the nozzle. +- **Pickup.** The head reads the load cell at the trigger line with the head empty, picks up the tool, and reads it again. A seated tool adds its latch spring preload, so the two readings must differ by at least `seat_min_force_g` (default 680 g). If they don't and the difference is below `seat_empty_max_g` (default 230 g), the head is empty: the console says so, X and Y are homed again, and the tool is parked and picked up once more. If the second try fails too, the heater is turned off and no tool is recorded. A difference between `seat_empty_max_g` and `seat_min_force_g` means the tool may be half seated on the head. INDX never assumes the head is empty, so there is no retry: the heater is turned off, the tool stays recorded as on the head, the latch is left as it is, and the display shows `T may be on the head`. Until the head has been checked, `G28` (when it homes Z), `CHANGE_TOOL` and `PARK_TOOL` refuse to run, also after a restart. Check the head by hand, then run `MANUAL_TOOL_REMOVE` (take the tool off and put it in its dock) or `MANUAL_TOOL_SEAT TOOL=` (it is fully on). Either one, or `MANUAL_TOOLHEAD_RESET`, clears the refusal. Either way, during a print the print pauses, with nothing moved or retracted: get the tool back in its dock with the head empty, run the `CHANGE_TOOL` command the console gives you, then `RESUME`. Outside a print, and during the T0 fetch of a `G28`, INDX stops with an error. +- **Park.** The head reads the load cell with the tool still seated, parks it, and reads it again after leaving the dock. The readings must differ by at least `park_min_change_g` (default 425 g). If they don't, the tool is probably still on the head: INDX stops with an error before driving anywhere else, turns the heater off, and keeps the tool recorded as on the head. Check the head. If the heater also reported a model divergence, a blob from a detached print may be stuck on the nozzle. Successful pickups print the reading in the console, for example `T1 seated: load cell -1650 g against the empty head`, which is useful when tuning `seat_min_force_g`. A failed check also shows a short alert on the printer display. diff --git a/macros/indx-tc-macros.cfg b/macros/indx-tc-macros.cfg index 46c45a7..c3ed0a0 100644 --- a/macros/indx-tc-macros.cfg +++ b/macros/indx-tc-macros.cfg @@ -809,10 +809,10 @@ gcode: {% set t = params.TOOL|int %} {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} {% set f = sc.force_g|round(0)|int %} - {% set min_g = tp.seat_min_force_g|default(800.0)|float %} + {% set min_g = tp.seat_min_force_g|default(680.0)|float %} {% set ambiguous = sc.ambiguous|int %} {% if ambiguous %} - {% set why = "T%d may be half seated on the head: the load cell reads %d g against the empty head, more than an empty head (under %d g) but less than a seated tool (%d g). T%d is kept as the tool on the head. Check it by hand" % (t, f, tp.seat_empty_max_g|default(200.0)|float, min_g, t) %} + {% set why = "T%d may be half seated on the head: the load cell reads %d g against the empty head, more than an empty head (under %d g) but less than a seated tool (%d g). T%d is kept as the tool on the head. Check it by hand" % (t, f, tp.seat_empty_max_g|default(230.0)|float, min_g, t) %} {% set lcd = "T%d may be on the head" % t %} {% else %} {% set why = "T%d did not seat after a re-home and a second try (load cell %d g against the empty head, a seated tool shows at least %d g). Check the T%d dock and the head" % (t, f, min_g, t) %} @@ -948,8 +948,8 @@ gcode: {% set ns = namespace(ok=1, force=0.0, ambiguous=0) %} {% if tp.seat_check_enabled|default(True) and e.empty_valid|int and e.now_valid|int %} {% set ns.force = e.now_g|float - e.empty_g|float %} - {% set ns.ok = 1 if ns.force|abs >= tp.seat_min_force_g|default(800.0)|float else 0 %} - {% set ns.ambiguous = 1 if not ns.ok and ns.force|abs >= tp.seat_empty_max_g|default(200.0)|float else 0 %} + {% set ns.ok = 1 if ns.force|abs >= tp.seat_min_force_g|default(680.0)|float else 0 %} + {% set ns.ambiguous = 1 if not ns.ok and ns.force|abs >= tp.seat_empty_max_g|default(230.0)|float else 0 %} {% if ns.ok %} RESPOND MSG="T{params.TOOL} seated: load cell {ns.force|round(0)|int} g against the empty head" {% endif %} @@ -974,7 +974,7 @@ gcode: SET_GCODE_VARIABLE MACRO=_INDX_LC_STORE VARIABLE=now_valid VALUE=0 {% if tp.seat_check_enabled|default(True) and e.seated_valid|int and e.now_valid|int %} {% set change = e.now_g|float - e.seated_g|float %} - {% if change|abs < tp.park_min_change_g|default(500.0)|float %} + {% if change|abs < tp.park_min_change_g|default(425.0)|float %} M104 S0 _INDX_LCD LEVEL=warn MSG="T{t} may still be on the head" RESPOND TYPE=error MSG="T{t} may still be on the head: the load cell changed only {change|round(0)|int} g at the park (a released tool changes it by its preload, about 1100 to 1900 g). Stopped with the heater off and T{t} kept as the tool on the head. Check the head. If the heater also reported a model divergence, a blob from a detached print may be stuck on the nozzle." diff --git a/macros/indx.cfg b/macros/indx.cfg index e0de754..a56753c 100644 --- a/macros/indx.cfg +++ b/macros/indx.cfg @@ -133,9 +133,9 @@ variable_seat_check_enabled: True # From here up to seat_min_force_g it counts as a tool # that may be half seated, and INDX keeps it recorded. # park_min_change_g = grams the reading must change by when a tool is parked. -variable_seat_min_force_g: 800.0 -variable_seat_empty_max_g: 200.0 -variable_park_min_change_g: 500.0 +variable_seat_min_force_g: 680.0 +variable_seat_empty_max_g: 230.0 +variable_park_min_change_g: 425.0 # ── Dock approach direction ────────────────────────────────────────────────── # dock_dir = Y sign into the dock: -1 front (min Y), +1 rear (max Y). From 7387f6bc97d179a6f8bcfa129b8a5cf50454baf1 Mon Sep 17 00:00:00 2001 From: ogland Date: Fri, 9 Oct 2026 11:27:45 +0200 Subject: [PATCH 6/6] A pickup retry in sport mode drops to normal speed and says so until MODE_SPORT is chosen again --- README.md | 2 +- macros/indx-tc-macros.cfg | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 70553fe..7adeb08 100644 --- a/README.md +++ b/README.md @@ -1073,7 +1073,7 @@ Warnings that are dangerous to ignore (`! ...`, for example a failed homing or ` INDX checks every pickup and every park with the load cell. This is on by default (`seat_check_enabled: True` in `indx.cfg`) and is separate from the homing [seat check](#seat-check) above. Each check compares two readings taken standing still a few seconds apart, so the tare and the load cell's drift with temperature do not affect it. -- **Pickup.** The head reads the load cell at the trigger line with the head empty, picks up the tool, and reads it again. A seated tool adds its latch spring preload, so the two readings must differ by at least `seat_min_force_g` (default 680 g). If they don't and the difference is below `seat_empty_max_g` (default 230 g), the head is empty: the console says so, X and Y are homed again, and the tool is parked and picked up once more. If the second try fails too, the heater is turned off and no tool is recorded. A difference between `seat_empty_max_g` and `seat_min_force_g` means the tool may be half seated on the head. INDX never assumes the head is empty, so there is no retry: the heater is turned off, the tool stays recorded as on the head, the latch is left as it is, and the display shows `T may be on the head`. Until the head has been checked, `G28` (when it homes Z), `CHANGE_TOOL` and `PARK_TOOL` refuse to run, also after a restart. Check the head by hand, then run `MANUAL_TOOL_REMOVE` (take the tool off and put it in its dock) or `MANUAL_TOOL_SEAT TOOL=` (it is fully on). Either one, or `MANUAL_TOOLHEAD_RESET`, clears the refusal. Either way, during a print the print pauses, with nothing moved or retracted: get the tool back in its dock with the head empty, run the `CHANGE_TOOL` command the console gives you, then `RESUME`. Outside a print, and during the T0 fetch of a `G28`, INDX stops with an error. +- **Pickup.** The head reads the load cell at the trigger line with the head empty, picks up the tool, and reads it again. A seated tool adds its latch spring preload, so the two readings must differ by at least `seat_min_force_g` (default 680 g). If they don't and the difference is below `seat_empty_max_g` (default 230 g), the head is empty: the console says so, X and Y are homed again, and the tool is parked and picked up once more. In sport mode INDX first switches to normal tool change speed, says so on the console and the display, and stays in normal (also after a restart) until you run `MODE_SPORT` again. If the second try fails too, the heater is turned off and no tool is recorded. A difference between `seat_empty_max_g` and `seat_min_force_g` means the tool may be half seated on the head. INDX never assumes the head is empty, so there is no retry: the heater is turned off, the tool stays recorded as on the head, the latch is left as it is, and the display shows `T may be on the head`. Until the head has been checked, `G28` (when it homes Z), `CHANGE_TOOL` and `PARK_TOOL` refuse to run, also after a restart. Check the head by hand, then run `MANUAL_TOOL_REMOVE` (take the tool off and put it in its dock) or `MANUAL_TOOL_SEAT TOOL=` (it is fully on). Either one, or `MANUAL_TOOLHEAD_RESET`, clears the refusal. Either way, during a print the print pauses, with nothing moved or retracted: get the tool back in its dock with the head empty, run the `CHANGE_TOOL` command the console gives you, then `RESUME`. Outside a print, and during the T0 fetch of a `G28`, INDX stops with an error. - **Park.** The head reads the load cell with the tool still seated, parks it, and reads it again after leaving the dock. The readings must differ by at least `park_min_change_g` (default 425 g). If they don't, the tool is probably still on the head: INDX stops with an error before driving anywhere else, turns the heater off, and keeps the tool recorded as on the head. Check the head. If the heater also reported a model divergence, a blob from a detached print may be stuck on the nozzle. Successful pickups print the reading in the console, for example `T1 seated: load cell -1650 g against the empty head`, which is useful when tuning `seat_min_force_g`. A failed check also shows a short alert on the printer display. diff --git a/macros/indx-tc-macros.cfg b/macros/indx-tc-macros.cfg index c3ed0a0..22c1aad 100644 --- a/macros/indx-tc-macros.cfg +++ b/macros/indx-tc-macros.cfg @@ -749,6 +749,9 @@ gcode: [gcode_macro _PICKUP_SEAT_RETRY] description: Inner - seat check failed: re-home X/Y, park the tool in case it is on the head, pick it up again +# sport_dropped: this retry switched sport to normal; _PICKUP_FINISH shows +# the display alert after its own clear, so a good retry doesn't wipe it +variable_sport_dropped: 0 # A misread seat check must not drop a tool that is really on the head, so # the retry parks it first: a tool on the head goes back into its dock, and # with an empty head the park is the dock visit that resets the latch. @@ -762,6 +765,15 @@ gcode: {% set tp = printer["gcode_macro TOOL_POSITIONS"] %} RESPOND TYPE=error MSG="T{t} did not seat (load cell {printer['gcode_macro _INDX_SEAT_CHECK'].force_g|round(0)|int} g against the empty head). Re-homing X/Y and trying once more." M104 S0 + # A pickup that needed a retry in sport mode drops to normal speed for + # the retry and from then on, until the user picks sport again. + {% if printer["gcode_macro _TOOLCHANGE_VARS"].mode|lower == "sport" %} + MODE_NORMAL + _TC_APPLY_MODE_LIMITS + RESPOND TYPE=error MSG="INDX: switched from sport to normal tool change speed because T{t} needed a pickup retry. It stays normal, also after a restart, until you run MODE_SPORT again." + _INDX_LCD LEVEL=action MSG="Sport mode off: MODE_SPORT to restore" + SET_GCODE_VARIABLE MACRO=_PICKUP_SEAT_RETRY VARIABLE=sport_dropped VALUE=1 + {% endif %} # Over a print the X/Y home sweeps across it: lift z_hop first {% if "z" in printer.toolhead.homed_axes %} {% set z = [printer.toolhead.position.z|float + tp.z_hop|float, printer.toolhead.axis_maximum.z|float - 1]|min %} @@ -775,6 +787,15 @@ gcode: _INDX_SEAT_CHECK TOOL={t} {% endif %} +[gcode_macro _TC_APPLY_MODE_LIMITS] +description: Inner - set the tool change velocity and acceleration limits from the current speed mode (after a mode change in the middle of a tool change) +# _CHANGE_TOOL_APPLY sets these limits once, when the change starts, and puts +# the slicer's back when it ends. Its own macro, so it reads the mode after +# the switch. +gcode: + {% set v = printer["gcode_macro _TOOLCHANGE_VARS"] %} + SET_VELOCITY_LIMIT VELOCITY={printer.configfile.settings.printer.max_velocity * v.speed_mult} ACCEL={printer.configfile.settings.printer.max_accel * v.accel_mult} + [gcode_macro _INDX_REHOME_XY] description: Inner - home X and Y again, from inside G28 too # Inside the homing override G28 is Klipper's own homing, so the INDX steps @@ -883,6 +904,10 @@ gcode: # heating below: M104/M109 only heat the tool recorded as on the head. _RECORD_TOOLCHANGE TOOL={t} _INDX_LCD_CLEAR + {% if printer["gcode_macro _PICKUP_SEAT_RETRY"].sport_dropped|int %} + _INDX_LCD LEVEL=action MSG="Sport mode off: MODE_SPORT to restore" + SET_GCODE_VARIABLE MACRO=_PICKUP_SEAT_RETRY VARIABLE=sport_dropped VALUE=0 + {% endif %} # Start heating only after tool is fully locked, exited dock, and offsets applied. # M109 rather than M104: the tool was parked cold and the heater was off for the