# 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)