# Support for a manual controlled stepper # # 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 class ManualStepper: def __init__(self, config): self.printer = config.get_printer() if config.get('endstop_pin', None) is not None: self.can_home = True self.rail = stepper.LookupRail( config, need_position_minmax=False, default_position_endstop=0.) self.steppers = self.rail.get_steppers() else: self.can_home = False self.rail = stepper.PrinterStepper(config) self.steppers = [self.rail] self.velocity = config.getfloat('velocity', 5., above=0.) self.accel = self.homing_accel = config.getfloat('accel', 0., minval=0.) self.next_cmd_time = 0. self.pos_min = config.getfloat('position_min', None) self.pos_max = config.getfloat('position_max', None) # Setup iterative solver ffi_main, ffi_lib = chelper.get_ffi() self.trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) self.trapq_append = ffi_lib.trapq_append 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 self.instant_corner_v = 0. self.gaxis_limit_velocity = self.gaxis_limit_accel = 0. # Register commands stepper_name = config.get_name().split()[1] gcode = self.printer.lookup_object('gcode') gcode.register_mux_command('MANUAL_STEPPER', "STEPPER", stepper_name, self.cmd_MANUAL_STEPPER, desc=self.cmd_MANUAL_STEPPER_help) def sync_print_time(self): toolhead = self.printer.lookup_object('toolhead') print_time = toolhead.get_last_move_time() if self.next_cmd_time > print_time: toolhead.dwell(self.next_cmd_time - print_time) else: self.next_cmd_time = print_time def do_enable(self, enable): self.sync_print_time() stepper_enable = self.printer.lookup_object('stepper_enable') if enable: for s in self.steppers: se = stepper_enable.lookup_enable(s.get_name()) se.motor_enable(self.next_cmd_time) else: for s in self.steppers: se = stepper_enable.lookup_enable(s.get_name()) se.motor_disable(self.next_cmd_time) self.sync_print_time() def do_set_position(self, setpos): self.rail.set_position([setpos, 0., 0.]) def _submit_move(self, movetime, movepos, speed, accel): cp = self.rail.get_commanded_position() dist = movepos - cp axis_r, accel_t, cruise_t, cruise_v = force_move.calc_move_time( dist, speed, accel) self.trapq_append(self.trapq, movetime, accel_t, cruise_t, accel_t, cp, 0., 0., axis_r, 0., 0., 0., cruise_v, accel) return movetime + accel_t + cruise_t + accel_t def do_move(self, movepos, speed, accel, sync=True): self.sync_print_time() self.next_cmd_time = self._submit_move(self.next_cmd_time, movepos, speed, accel) self.rail.generate_steps(self.next_cmd_time) self.trapq_finalize_moves(self.trapq, self.next_cmd_time + 99999.9, self.next_cmd_time + 99999.9) toolhead = self.printer.lookup_object('toolhead') toolhead.note_mcu_movequeue_activity(self.next_cmd_time) if sync: self.sync_print_time() def do_homing_move(self, movepos, speed, accel, triggered, check_trigger): if not self.can_home: raise self.printer.command_error( "No endstop for this manual stepper") self.homing_accel = accel pos = [movepos, 0., 0., 0.] endstops = self.rail.get_endstops() phoming = self.printer.lookup_object('homing') phoming.manual_home(self, endstops, pos, speed, 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) setpos = gcmd.get_float('SET_POSITION', None) if setpos is not None: self.do_set_position(setpos) speed = gcmd.get_float('SPEED', self.velocity, above=0.) accel = gcmd.get_float('ACCEL', self.accel, minval=0.) homing_move = gcmd.get_int('STOP_ON_ENDSTOP', 0) if homing_move: movepos = gcmd.get_float('MOVE') if ((self.pos_min is not None and movepos < self.pos_min) or (self.pos_max is not None and movepos > self.pos_max)): raise gcmd.error("Move out of range") self.do_homing_move(movepos, speed, accel, homing_move > 0, abs(homing_move) == 1) elif gcmd.get_float('MOVE', None) is not None: movepos = gcmd.get_float('MOVE') if ((self.pos_min is not None and movepos < self.pos_min) or (self.pos_max is not None and movepos > self.pos_max)): raise gcmd.error("Move out of range") sync = gcmd.get_int('SYNC', 1) 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() instant_corner_v = gcmd.get_float('INSTANTANEOUS_CORNER_VELOCITY', 1., minval=0.) limit_velocity = gcmd.get_float('LIMIT_VELOCITY', 999999.9, above=0.) limit_accel = gcmd.get_float('LIMIT_ACCEL', 999999.9, above=0.) 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 self.instant_corner_v = instant_corner_v self.gaxis_limit_velocity = limit_velocity self.gaxis_limit_accel = limit_accel 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): # Check move is in bounds movepos = move.end_pos[ea_index] if ((self.pos_min is not None and movepos < self.pos_min) or (self.pos_max is not None and movepos > self.pos_max)): raise move.move_error() # Check if need to limit maximum velocity and acceleration axis_ratio = move.move_d / abs(move.axes_d[ea_index]) limit_velocity = self.gaxis_limit_velocity * axis_ratio limit_accel = self.gaxis_limit_accel * axis_ratio if not move.is_kinematic_move and self.accel: limit_accel = min(limit_accel, self.accel * axis_ratio) move.limit_speed(limit_velocity, limit_accel) def calc_junction(self, prev_move, move, ea_index): diff_r = move.axes_r[ea_index] - prev_move.axes_r[ea_index] if diff_r: return (self.instant_corner_v / abs(diff_r))**2 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() def get_position(self): return [self.rail.get_commanded_position(), 0., 0., 0.] def set_position(self, newpos, homing_axes=""): self.do_set_position(newpos[0]) def get_last_move_time(self): self.sync_print_time() return self.next_cmd_time def dwell(self, delay): self.next_cmd_time += max(0., delay) def drip_move(self, newpos, speed, drip_completion): # Submit move to trapq self.sync_print_time() maxtime = self._submit_move(self.next_cmd_time, newpos[0], speed, self.homing_accel) # Drip updates to motors toolhead = self.printer.lookup_object('toolhead') toolhead.drip_update_time(maxtime, drip_completion, self.steppers) # Clear trapq of any remaining parts of movement reactor = self.printer.get_reactor() self.trapq_finalize_moves(self.trapq, reactor.NEVER, 0) self.rail.set_position([newpos[0], 0., 0.]) self.sync_print_time() def get_kinematics(self): return self def get_steppers(self): return self.steppers def calc_position(self, stepper_positions): return [stepper_positions[self.rail.get_name()], 0., 0.] def load_config_prefix(config): return ManualStepper(config)