From 7201f41664ffa658e90146187ce57ad70b71d11c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 21 Mar 2025 23:04:46 -0400 Subject: [PATCH] manual_stepper: Support registering as an additional axis Add a new G-Code command that can register a manual_stepper as an additional axis on standard G-Code G1 commands. Signed-off-by: Kevin O'Connor --- docs/G-Codes.md | 12 +++++++ klippy/extras/manual_stepper.py | 57 ++++++++++++++++++++++++++++++++- test/klippy/manual_stepper.test | 15 +++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/docs/G-Codes.md b/docs/G-Codes.md index b8a0ce69..6d69dd9c 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -968,6 +968,18 @@ scheduled to run after the stepper move completes, however if a manual stepper move uses SYNC=0 then future G-Code movement commands may run in parallel with the stepper movement. +`MANUAL_STEPPER STEPPER=config_name GCODE_AXIS=[A-Z]`: If the +`GCODE_AXIS` parameter is specified then it configures the stepper +motor as an extra axis on `G1` move commands. For example, if one +were to issue a `MANUAL_STEPPER ... GCODE_AXIS=R` command then one +could issue commands like `G1 X10 Y20 R30` to move the stepper motor. +The resulting moves will occur synchronously with the associated +toolhead xyz movements. If the motor is associated with a +`GCODE_AXIS` then one may no longer issue movements using the above +`MANUAL_STEPPER` command - one may unregister the stepper with a +`MANUAL_STEPPER ... GCODE_AXIS=` command to resume manual control of +the motor. + ### [mcp4018] The following command is available when a diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index c15ce235..4c24ff75 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -1,8 +1,9 @@ # Support for a manual controlled stepper # -# Copyright (C) 2019-2021 Kevin O'Connor +# Copyright (C) 2019-2025 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. +import logging import stepper, chelper from . import force_move @@ -28,6 +29,8 @@ class ManualStepper: self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves self.rail.setup_itersolve('cartesian_stepper_alloc', b'x') self.rail.set_trapq(self.trapq) + # Registered with toolhead as an axtra axis + self.axis_gcode_id = None # Register commands stepper_name = config.get_name().split()[1] gcode = self.printer.lookup_object('gcode') @@ -88,6 +91,10 @@ class ManualStepper: triggered, check_trigger) cmd_MANUAL_STEPPER_help = "Command a manually configured stepper" def cmd_MANUAL_STEPPER(self, gcmd): + if gcmd.get('GCODE_AXIS', None) is not None: + return self.command_with_gcode_axis(gcmd) + if self.axis_gcode_id is not None: + raise gcmd.error("Must unregister from gcode axis first") enable = gcmd.get_int('ENABLE', None) if enable is not None: self.do_enable(enable) @@ -107,6 +114,54 @@ class ManualStepper: self.do_move(movepos, speed, accel, sync) elif gcmd.get_int('SYNC', 0): self.sync_print_time() + # Register as a gcode axis + def command_with_gcode_axis(self, gcmd): + gcode_move = self.printer.lookup_object("gcode_move") + toolhead = self.printer.lookup_object('toolhead') + gcode_axis = gcmd.get('GCODE_AXIS').upper() + if self.axis_gcode_id is not None: + if gcode_axis: + raise gcmd.error("Must unregister axis first") + # Unregister + toolhead.remove_extra_axis(self) + toolhead.unregister_step_generator(self.rail.generate_steps) + self.axis_gcode_id = None + return + if (len(gcode_axis) != 1 or not gcode_axis.isupper() + or gcode_axis in "XYZEFN"): + if not gcode_axis: + # Request to unregister already unregistered axis + return + raise gcmd.error("Not a valid GCODE_AXIS") + for ea in toolhead.get_extra_axes(): + if ea is not None and ea.get_axis_gcode_id() == gcode_axis: + raise gcmd.error("Axis '%s' already registered" % (gcode_axis,)) + self.axis_gcode_id = gcode_axis + toolhead.add_extra_axis(self, self.get_position()[0]) + toolhead.register_step_generator(self.rail.generate_steps) + def process_move(self, print_time, move, ea_index): + axis_r = move.axes_r[ea_index] + start_pos = move.start_pos[ea_index] + accel = move.accel * axis_r + start_v = move.start_v * axis_r + cruise_v = move.cruise_v * axis_r + self.trapq_append(self.trapq, print_time, + move.accel_t, move.cruise_t, move.decel_t, + start_pos, 0., 0., + 1., 0., 0., + start_v, cruise_v, accel) + def check_move(self, move, ea_index): + # XXX - support out of bounds checks + # XXX - support max accel/velocity + # XXX - support non-kinematic max accel/velocity + pass + def calc_junction(self, prev_move, move, ea_index): + # XXX - support max instantaneous velocity change + return move.max_cruise_v2 + def get_axis_gcode_id(self): + return self.axis_gcode_id + def get_trapq(self): + return self.trapq # Toolhead wrappers to support homing def flush_step_generation(self): self.sync_print_time() diff --git a/test/klippy/manual_stepper.test b/test/klippy/manual_stepper.test index af0d406a..d19c39c5 100644 --- a/test/klippy/manual_stepper.test +++ b/test/klippy/manual_stepper.test @@ -22,3 +22,18 @@ M84 # Verify stepper_buzz STEPPER_BUZZ STEPPER="manual_stepper basic_stepper" STEPPER_BUZZ STEPPER="manual_stepper homing_stepper" + +# Register with g-code +MANUAL_STEPPER STEPPER=basic_stepper GCODE_AXIS=A +G28 +G1 X20 Y20 Z10 +G1 A10 X22 + +# Test unregistering +MANUAL_STEPPER STEPPER=basic_stepper GCODE_AXIS= +G1 X15 + +# Test registering again +G28 +MANUAL_STEPPER STEPPER=basic_stepper GCODE_AXIS=A +G1 X20 Y20 Z10 A20