# Locate the X/Y endstops and home the printer
#
# Copyright (C) 2026 the Klipper contributors
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import discovery_util, homing
AXES = "xy"
class DiscoveryEndstop:
# Wrapper around an MCU endstop that records whether the endstop
# triggered during a homing move
def __init__(self, mcu_endstop, name):
self.mcu_endstop = mcu_endstop
self.name = name
self.did_trigger = False
def get_steppers(self):
return self.mcu_endstop.get_steppers()
def home_start(self, print_time, sample_time, sample_count, rest_time,
triggered=True):
self.did_trigger = False
return self.mcu_endstop.home_start(
print_time, sample_time, sample_count, rest_time,
triggered=triggered)
def home_wait(self, home_end_time):
trigger_time = self.mcu_endstop.home_wait(home_end_time)
self.did_trigger = trigger_time > 0.
return trigger_time
class HomingDiscovery:
def __init__(self, config):
self.printer = printer = config.get_printer()
self.gcode = printer.lookup_object('gcode')
self.max_home_distance = config.getfloat(
'max_home_distance', above=0.)
self.max_endstop_difference = config.getfloat(
'max_endstop_difference', above=0.)
self.speed = config.getfloat('speed', None, above=0., maxval=200.)
self.gcode.register_command(
"HOMING_DISCOVERY", self.cmd_HOMING_DISCOVERY,
desc=self.cmd_HOMING_DISCOVERY_help)
def _check_endstops_open(self, rails, toolhead):
print_time = toolhead.get_last_move_time()
triggered = []
for axis, _, rail in rails:
for mcu_endstop, name in rail.get_endstops():
if mcu_endstop.query_endstop(print_time):
triggered.append("%s (%s)" % (name, axis.upper()))
if triggered:
raise self.printer.command_error(
"Homing discovery: endstop(s) %s already triggered, all"
" X/Y endstops must be open before homing discovery"
% (", ".join(triggered),))
def _query_endstops(self, endstops):
# The trigger state recorded during the homing moves is used (a
# plain endstop query would not detect an endstop that has
# already opened again after its trigger)
return [name for des, name in endstops if des.did_trigger]
def _homing_move(self, endstops, movepos, speed):
hmove = homing.HomingMove(self.printer, endstops)
hmove.homing_move(movepos, speed, probe_pos=True,
check_triggered=False)
return hmove
def _measured_travel(self, hmove, kin):
# Commanded rail travel of the last homing move (all steppers of
# a rail advance identically)
steppers = {s.get_name(): s for s in kin.get_steppers()}
for sp in hmove.stepper_positions:
stepper = steppers.get(sp.stepper_name)
if stepper is not None:
return (sp.halt_pos - sp.start_pos) * stepper.get_step_dist()
return 0.
# Discovery of one axis
def _discover_axis(self, toolhead, kin, axis, axis_index, rail, endstops,
max_home_distance, max_endstop_difference, speed):
# Establish a discovery reference frame with the axis zeroed at
# the current location, and temporarily widen the axis limits so
# that the bounded homing moves may travel beyond the configured
# range
startpos = toolhead.get_position()
startpos[axis_index] = 0.
toolhead.set_position(startpos, homing_axes=axis)
orig_limits = kin.limits[axis_index]
margin = 2. * max_home_distance + max_endstop_difference + 10.
kin.limits[axis_index] = (min(orig_limits[0], -margin),
max(orig_limits[1], margin))
try:
endstop_pos = self._search_endstop(
toolhead, kin, axis, endstops, axis_index, startpos,
max_home_distance, max_endstop_difference, speed)
finally:
kin.limits[axis_index] = orig_limits
# Mark the axis as homed with the endstop at the configured
# position_endstop reference
pos = toolhead.get_position()
pos[axis_index] = rail.get_homing_info().position_endstop
toolhead.set_position(pos, homing_axes=axis)
return endstop_pos
def _search_endstop(self, toolhead, kin, axis, endstops, axis_index,
startpos, max_home_distance, max_endstop_difference,
speed):
axis_name = axis.upper()
# Bounded homing move in the forward (positive) direction
direction = 1.
self.gcode.respond_info(
"Homing discovery: searching for the %s endstop(s), moving"
" forward up to %.3fmm" % (axis_name, max_home_distance))
movepos = list(toolhead.get_position())
movepos[axis_index] = (startpos[axis_index]
+ direction * max_home_distance)
self._homing_move(endstops, movepos, speed)
found = self._query_endstops(endstops)
if not found:
# Nothing found going forward: move backward through the start
# position and out to -max_home_distance (2x the forward move)
direction = -1.
self.gcode.respond_info(
"Homing discovery: %s endstop not found, moving backward"
" up to %.3fmm" % (axis_name, 2. * max_home_distance))
movepos = list(toolhead.get_position())
movepos[axis_index] = (startpos[axis_index]
+ direction * max_home_distance)
self._homing_move(endstops, movepos, speed)
found = self._query_endstops(endstops)
if not found:
raise self.printer.command_error(
"Homing discovery: no %s endstop triggered within"
" %.3fmm forward and %.3fmm backward of the starting"
" position" % (axis_name, max_home_distance,
max_home_distance))
# On rails with multiple endstops (eg, dual X motors) every
# endstop must trigger. Endstops that did not trigger yet get
# one additional move of max_endstop_difference; if one still
# does not trigger then it must be considered defective
missing = [es for es in endstops if es[1] not in found]
while missing:
self.gcode.respond_info(
"Homing discovery: %s endstop(s) %s not triggered,"
" checking within %.3fmm more"
% (axis_name, ", ".join([name for _, name in missing]),
max_endstop_difference))
pre_move_pos = toolhead.get_position()
movepos = list(pre_move_pos)
movepos[axis_index] += direction * max_endstop_difference
hmove = self._homing_move(missing, movepos, speed)
# The probe_pos position reconstruction tracks only the armed
# endstop's steppers while cartesian.calc_position() reads the
# primary rail stepper, so re-apply the commanded travel
# measured from the armed endstop
pos = toolhead.get_position()
pos[axis_index] = (pre_move_pos[axis_index]
+ self._measured_travel(hmove, kin))
toolhead.set_position(pos)
still_found = self._query_endstops(missing)
missing = [es for es in missing if es[1] not in still_found]
if missing:
raise self.printer.command_error(
"Homing discovery: %s endstop(s) %s did not trigger"
" within %.3fmm of the other %s endstop(s), assuming"
" the endstop is defective"
% (axis_name, ", ".join([name for _, name in missing]),
max_endstop_difference, axis_name))
# All endstops triggered; the toolhead is at the discovered
# endstop position (relative to the zeroed discovery start)
endstop_pos = toolhead.get_position()[axis_index]
self.gcode.respond_info(
"Homing discovery: %s endstop(s) triggered at %s=%.3f"
" (relative to the search start)"
% (axis_name, axis, endstop_pos))
return endstop_pos
def _save_results(self, gcmd, results):
pconfig = self.printer.lookup_object('configfile')
touched = [('homing_discovery', 'endstop_position_' + axis)
for axis in results]
for axis, endstop_pos in results.items():
pconfig.set('homing_discovery', 'endstop_position_' + axis,
"%.6f" % (endstop_pos,))
gcmd.respond_info(
"Homing discovery complete\n"
"endstop_position_x = %.6f\n"
"endstop_position_y = %.6f\n"
"The endstop positions are saved to the SAVE_CONFIG block"
" at the end of the config file, restarting..."
% (results['x'], results['y']))
try:
self.gcode.run_script_from_command("SAVE_CONFIG")
except self.printer.command_error as e:
# Do not leave pending entries behind (they would otherwise be
# applied by any later SAVE_CONFIG command)
discovery_util.discard_autosave(self.printer, touched)
raise gcmd.error("Unable to save via SAVE_CONFIG: %s" % (e,))
# G-code command
cmd_HOMING_DISCOVERY_help = (
"Discover the X and Y endstops, home the printer, and save the"
" endstop positions to the config file")
def cmd_HOMING_DISCOVERY(self, gcmd):
discovery_util.check_not_printing(self.printer)
max_home_distance = gcmd.get_float(
'MAX_HOME_DISTANCE', self.max_home_distance, above=0.,
maxval=self.max_home_distance)
max_endstop_difference = gcmd.get_float(
'MAX_ENDSTOP_DIFFERENCE', self.max_endstop_difference, above=0.,
maxval=self.max_endstop_difference)
speed = gcmd.get_float('SPEED', self.speed, above=0., maxval=200.)
save = gcmd.get_int('SAVE', 1, minval=0, maxval=1)
toolhead = self.printer.lookup_object('toolhead')
kin = discovery_util.lookup_cartesian_kinematics(
self.printer, "HOMING_DISCOVERY")
rails = [("x", 0, kin.rails[0]), ("y", 1, kin.rails[1])]
# Every X and Y endstop (of every motor) must be open before
# starting
self._check_endstops_open(rails, toolhead)
results = {}
try:
for axis, axis_index, rail in rails:
move_speed = speed
if move_speed is None:
move_speed = rail.get_homing_info().speed
endstops = [(DiscoveryEndstop(mcu_endstop, name), name)
for mcu_endstop, name in rail.get_endstops()]
results[axis] = self._discover_axis(
toolhead, kin, axis, axis_index, rail, endstops,
max_home_distance, max_endstop_difference, move_speed)
except self.printer.command_error:
# The axis positions may be in a discovery relative frame -
# do not leave partially valid homing state behind
toolhead.get_kinematics().clear_homing_state(AXES)
raise
if not save:
gcmd.respond_info(
"Homing discovery complete (SAVE=0: endstop positions not"
" saved, endstop_position_x = %.6f, endstop_position_y ="
" %.6f)" % (results['x'], results['y']))
return
self._save_results(gcmd, results)
def load_config(config):
return HomingDiscovery(config)
Read 8 times, last 4 days ago