# Code for handling the kinematics of cable winch robots # # Copyright (C) 2018-2021 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import stepper, mathutil class WinchKinematics: def __init__(self, toolhead, config): # Setup steppers at each anchor self.steppers = [] self.anchors = [] for i in range(26): name = 'stepper_' + chr(ord('a') + i) if i >= 3 and not config.has_section(name): break stepper_config = config.getsection(name) s = stepper.LookupMultiRail(stepper_config) s.get_homing_info().position_endstop self.steppers.append(s) a = tuple([stepper_config.getfloat('anchor_' + n) for n in 'xyz']) self.anchors.append(a) s.setup_itersolve('winch_stepper_alloc', *a) s.set_trapq(toolhead.get_trapq()) toolhead.register_step_generator(s.generate_steps) config.get_printer().register_event_handler("stepper_enable:motor_off", self._motor_off) # Setup max velocity self.max_velocity, self.max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( 'max_z_velocity', self.max_velocity, above=0., maxval=self.max_velocity) self.max_z_accel = config.getfloat('max_z_accel', self.max_accel, above=0., maxval=self.max_accel) # Setup boundary checks acoords = list(zip(*self.anchors)) self.limits = [(1.0, -1.0)] * 3 self.axes_min = toolhead.Coord(*[min(a) for a in acoords], e=0.) self.axes_max = toolhead.Coord(*[max(a) for a in acoords], e=0.) self.set_position([0., 0., 0.], ()) def get_steppers(self): return list(self.steppers) def calc_position(self, stepper_positions): # Use only first three steppers to calculate cartesian position pos = [stepper_positions[rail.get_name()] for rail in self.steppers[:3]] return mathutil.trilateration(self.anchors[:3], [sp*sp for sp in pos]) def set_position(self, newpos, homing_axes): for i, s in enumerate(self.steppers): s.set_position(newpos) if i in homing_axes: self.limits[i] = s.get_range() def note_z_not_homed(self): # Helper for Safe Z Home self.limits[2] = (1.0, -1.0) def home(self, homing_state): # Each axis is homed independently and in order for axis in homing_state.get_axes(): s = self.steppers[axis] # Determine movement position_min, position_max = s.get_range() hi = s.get_homing_info() homepos = [None, None, None, None] homepos[axis] = hi.position_endstop forcepos = list(homepos) if hi.positive_dir: forcepos[axis] -= 1.5 * (position_min) else: forcepos[axis] += 1.5 * (position_max) # Perform homing homing_state.home_rails([s], forcepos, homepos) homing_state.set_axes([0, 1, 2]) homing_state.set_homed_position([0., 0., 0.]) def _check_endstops(self, move): end_pos = move.end_pos for i in (0, 1, 2): if (move.axes_d[i] and (end_pos[i] < self.limits[i][0] or end_pos[i] > self.limits[i][1])): if self.limits[i][0] > self.limits[i][1]: raise move.move_error("Must home axis first") raise move.move_error() def check_move(self, move): limits = self.limits xpos, ypos = move.end_pos[:2] if (xpos < limits[0][0] or xpos > limits[0][1] or ypos < limits[1][0] or ypos > limits[1][1]): self._check_endstops(move) if not move.axes_d[2]: # Normal XY move - use defaults return # Move with Z - update velocity and accel for slower Z axis self._check_endstops(move) z_ratio = move.move_d / abs(move.axes_d[2]) move.limit_speed( self.max_z_velocity * z_ratio, self.max_z_accel * z_ratio) def _motor_off(self, print_time): self.limits = [(1.0, -1.0)] * 3 def get_status(self, eventtime): axes = [a for a, (l, h) in zip("xyz", self.limits) if l <= h] return { 'homed_axes': "".join(axes), 'axis_minimum': self.axes_min, 'axis_maximum': self.axes_max, } def load_kinematics(toolhead, config): return WinchKinematics(toolhead, config)