Raw Text Content QR
bat-pigeon-squid



# 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