diff --git a/Documentation/ERCF_Manual.pdf b/Documentation/ERCF_Manual.pdf index 250d8ef7..da3fb7fe 100644 Binary files a/Documentation/ERCF_Manual.pdf and b/Documentation/ERCF_Manual.pdf differ diff --git a/Klipper_Files/Extra module/ercf.py b/Klipper_Files/Extra module/ercf.py index d8ecb373..3c848011 100644 --- a/Klipper_Files/Extra module/ercf.py +++ b/Klipper_Files/Extra module/ercf.py @@ -5,10 +5,11 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging import math +from random import randint from . import pulse_counter from . import force_move -import toolhead import copy +import time class EncoderCounter: @@ -43,12 +44,15 @@ def reset_counts(self): self._counts = 0. class Ercf: - LONG_MOVE_THRESHOLD = 70. - MACRO_SERVO_UP = "_ERCF_SERVO_UP" - MACRO_SERVO_DOWN = "_ERCF_SERVO_DOWN" - MACRO_UNSELECT_TOOL = "_ERCF_UNSELECT_TOOL" - MACRO_PAUSE = "_ERCF_PAUSE" + SERVO_DOWN_STATE = 1 + SERVO_UP_STATE = 0 + SERVO_UNKNOWN_STATE = -1 + + LOADED_STATUS_UNKNOWN = -1 + LOADED_STATUS_UNLOADED = 0 + LOADED_STATUS_PARTIAL = 1 + LOADED_STATUS_FULL = 2 def __init__(self, config): self.config = config @@ -75,14 +79,58 @@ def __init__(self, config): self.long_moves_accel = config.getfloat('long_moves_accel', 400.) self.short_moves_speed = config.getfloat('short_moves_speed', 25.) self.short_moves_accel = config.getfloat('short_moves_accel', 400.) + self.log_level = config.getint('log_level', 1) + self.gear_stepper_accel = config.getint('gear_stepper_accel', 0) + self.servo_down_angle = config.getfloat('servo_down_angle') + self.servo_up_angle = config.getfloat('servo_up_angle') + self.extra_servo_dwell_down = config.getint('extra_servo_dwell_down', 0) + self.extra_servo_dwell_up = config.getint('extra_servo_dwell_up', 0) + self.end_of_bowden_to_nozzle = config.getfloat('end_of_bowden_to_nozzle', above=30.) + self.num_moves = config.getint('num_moves', 2) + self.parking_distance = config.getfloat('parking_distance', 23., above=15., below=30.) + self.encoder_move_step_size = config.getfloat('encoder_move_step_size', 15., above=5., below=25.) + self.selector_offsets = config.getfloatlist('colorselector') + self.timeout_pause = config.getint('timeout_pause', 72000) + self.disable_heater = config.getint('disable_heater', 600) + self.log_statistics = config.getint('log_statistics', 0) + self.min_temp_extruder = config.getfloat('min_temp_extruder', 180.) + self.calibration_bowden_length = config.getfloat('calibration_bowden_length') + self.unload_buffer = config.getfloat('unload_buffer', 30.) + self.sync_unload_length = config.getfloat('sync_unload_length', 20.) + self.unload_parking_position = config.getfloat('unload_parking_position', 0.) + self.extruder_homing_max = config.getfloat('extruder_homing_max', 50.) + self.extruder_homing_step = config.getfloat('extruder_homing_step', 2.) + self.sensorless_selector = config.getint('sensorless_selector', 0) + self.enable_clog_detection = config.getint('enable_clog_detection', 1) + self.enable_endless_spool = config.getint('enable_endless_spool', 0) + self.endless_spool_groups = config.getintlist('endless_spool_groups') + + if self.enable_endless_spool == 1 and len(self.endless_spool_groups) != len(self.selector_offsets): + raise config.error( + "EndlessSpool mode requires that the endless_spool_groups parameter is set with the same number of values as the number of selectors") + + + + # State variables + self.is_paused = False + self.paused_extruder_temp = 0. + self.is_homed = False + self.tool_selected = -1 + self.servo_state = self.SERVO_UNKNOWN_STATE + self.loaded_status = self.LOADED_STATUS_UNKNOWN + + # Statistics + self.total_swaps = 0 + self.time_spent_loading = 0 + self.time_spent_unloading = 0 + self.time_spent_paused = 0 + self.total_pauses = 0 + # GCODE commands self.gcode = self.printer.lookup_object('gcode') self.gcode.register_command('ERCF_CALIBRATE_ENCODER', self.cmd_ERCF_CALIBRATE_ENCODER, desc=self.cmd_ERCF_CALIBRATE_ENCODER_help) - self.gcode.register_command('ERCF_RESET_ENCODER_COUNTS', - self.cmd_ERCF_RESET_ENCODER_COUNTS, - desc=self.cmd_ERCF_RESET_ENCODER_COUNTS_help) self.gcode.register_command('ERCF_LOAD', self.cmd_ERCF_LOAD, desc=self.cmd_ERCF_LOAD_help) @@ -92,24 +140,78 @@ def __init__(self, config): self.gcode.register_command('ERCF_BUZZ_GEAR_MOTOR', self.cmd_ERCF_BUZZ_GEAR_MOTOR, desc=self.cmd_ERCF_BUZZ_GEAR_MOTOR_help) - self.gcode.register_command('ERCF_HOME_EXTRUDER', - self.cmd_ERCF_HOME_EXTRUDER, - desc=self.cmd_ERCF_HOME_EXTRUDER_help) - self.gcode.register_command('ERCF_SET_STEPS', - self.cmd_ERCF_SET_STEPS, - desc=self.cmd_ERCF_SET_STEPS_help) - self.gcode.register_command('ERCF_GET_SELECTOR_POS', - self.cmd_ERCF_GET_SELECTOR_POS, - desc=self.cmd_ERCF_GET_SELECTOR_POS_help) - self.gcode.register_command('ERCF_MOVE_SELECTOR', - self.cmd_ERCF_MOVE_SELECTOR, - desc=self.cmd_ERCF_MOVE_SELECTOR_help) - self.gcode.register_command('ERCF_ENDLESSSPOOL_UNLOAD', - self.cmd_ERCF_ENDLESSSPOOL_UNLOAD, - desc=self.cmd_ERCF_ENDLESSSPOOL_UNLOAD_help) - self.gcode.register_command('ERCF_FINALIZE_LOAD', - self.cmd_ERCF_FINALIZE_LOAD, - desc=self.cmd_ERCF_FINALIZE_LOAD_help) + self.gcode.register_command('ERCF_SET_LOG_LEVEL', + self.cmd_ERCF_SET_LOG_LEVEL, + desc = self.cmd_ERCF_SET_LOG_LEVEL_help) + self.gcode.register_command('ERCF_SERVO_DOWN', + self.cmd_ERCF_SERVO_DOWN, + desc = self.cmd_ERCF_SERVO_DOWN_help) + self.gcode.register_command('ERCF_SERVO_UP', + self.cmd_ERCF_SERVO_UP, + desc = self.cmd_ERCF_SERVO_UP_help) + self.gcode.register_command('ERCF_TEST_SERVO', + self.cmd_ERCF_TEST_SERVO, + desc = self.cmd_ERCF_TEST_SERVO_help) + self.gcode.register_command('ERCF_MOTORS_OFF', + self.cmd_ERCF_MOTORS_OFF, + desc = self.cmd_ERCF_MOTORS_OFF_help) + self.gcode.register_command('ERCF_CHANGE_TOOL_SLICER', + self.cmd_ERCF_CHANGE_TOOL_SLICER, + desc = self.cmd_ERCF_CHANGE_TOOL_SLICER_help) + self.gcode.register_command('ERCF_CHANGE_TOOL', + self.cmd_ERCF_CHANGE_TOOL, + desc = self.cmd_ERCF_CHANGE_TOOL_help) + self.gcode.register_command('ERCF_CHANGE_TOOL_STANDALONE', + self.cmd_ERCF_CHANGE_TOOL_STANDALONE, + desc = self.cmd_ERCF_CHANGE_TOOL_STANDALONE_help) + self.gcode.register_command('ERCF_HOME', + self.cmd_ERCF_HOME, + desc = self.cmd_ERCF_HOME_help) + self.gcode.register_command('ERCF_PAUSE', + self.cmd_ERCF_PAUSE, + desc = self.cmd_ERCF_PAUSE_help) + self.gcode.register_command('ERCF_UNLOCK', + self.cmd_ERCF_UNLOCK, + desc = self.cmd_ERCF_HOME_help) + self.gcode.register_command('ERCF_EJECT', + self.cmd_ERCF_EJECT, + desc = self.cmd_ERCF_EJECT_help) + self.gcode.register_command('ERCF_RESET_STATS', + self.cmd_ERCF_RESET_STATS, + desc = self.cmd_ERCF_RESET_STATS_help) + self.gcode.register_command('ERCF_DUMP_STATS', + self.cmd_ERCF_DUMP_STATS, + desc = self.cmd_ERCF_DUMP_STATS_help) + self.gcode.register_command('ERCF_CALIBRATE', + self.cmd_ERCF_CALIBRATE, + desc = self.cmd_ERCF_CALIBRATE_help) + self.gcode.register_command('ERCF_CALIBRATE_SINGLE', + self.cmd_ERCF_CALIBRATE_SINGLE, + desc = self.cmd_ERCF_CALIBRATE_SINGLE_help) + self.gcode.register_command('ERCF_CALIB_SELECTOR', + self.cmd_ERCF_CALIB_SELECTOR, + desc = self.cmd_ERCF_CALIB_SELECTOR_help) + self.gcode.register_command('ERCF_ENCODER_RUNOUT', + self.cmd_ERCF_ENCODER_RUNOUT, + desc = self.cmd_ERCF_ENCODER_RUNOUT_help) + self.gcode.register_command('ERCF_DISPLAY_ENCODER_POS', + self.cmd_ERCF_DISPLAY_ENCODER_POS, + desc = self.cmd_ERCF_DISPLAY_ENCODER_POS_help) + self.gcode.register_command('ERCF_TEST_GRIP', + self.cmd_ERCF_TEST_GRIP, + desc = self.cmd_ERCF_TEST_GRIP_help) + self.gcode.register_command('ERCF_TEST_MOVE_GEAR', + self.cmd_ERCF_TEST_MOVE_GEAR, + desc = self.cmd_ERCF_TEST_MOVE_GEAR_help) + self.gcode.register_command('ERCF_TEST_LOAD_SEQUENCE', + self.cmd_ERCF_TEST_LOAD_SEQUENCE, + desc = self.cmd_ERCF_TEST_LOAD_SEQUENCE_help) + self.gcode.register_command('ERCF_SET_END_OF_BOWDEN_TO_NOZZLE', + self.cmd_ERCF_SET_END_OF_BOWDEN_TO_NOZZLE, + desc = self.cmd_ERCF_SET_END_OF_BOWDEN_TO_NOZZLE_help) + self.gcode.register_command('ERCF_SELECT_TOOL', + self.cmd_ERCF_SELECT_TOOL, + desc = self.cmd_ERCF_SELECT_TOOL_help) def handle_connect(self): self.toolhead = self.printer.lookup_object('toolhead') @@ -126,10 +228,235 @@ def handle_connect(self): raise config.error( "Manual_stepper gear_stepper must be specified") self.ref_step_dist=self.gear_stepper.rail.steppers[0].get_step_dist() + self.variables = self.printer.lookup_object('save_variables').allVariables + self.encoder_sensor = self.printer.lookup_object("filament_motion_sensor encoder_sensor") + self.printer.register_event_handler("klippy:ready", self._setup_heater_off_reactor) + self._reset_statistics() def get_status(self, eventtime): encoder_pos = float(self._counter.get_distance()) - return {'encoder_pos': encoder_pos} + return {'encoder_pos': encoder_pos, 'is_paused': self.is_paused, 'tool': self.tool_selected, 'clog_detection': self.enable_clog_detection} + +######################## +# STATISTICS FUNCTIONS # +######################## + def _reset_statistics(self): + self.total_swaps = 0 + self.time_spent_loading = 0 + self.time_spent_unloading = 0 + self.total_pauses = 0 + self.time_spent_paused = 0 + + self.tracked_start_time = 0 + self.pause_start_time = 0 + + def _track_swap_completed(self): + self.total_swaps += 1 + + def _track_load_start(self): + self.tracked_start_time = time.time() + + def _track_load_end(self): + self.time_spent_loading += time.time() - self.tracked_start_time + + def _track_unload_start(self): + self.tracked_start_time = time.time() + + def _track_unload_end(self): + self.time_spent_unloading += time.time() - self.tracked_start_time + + def _track_pause_start(self): + self.total_pauses += 1 + self.pause_start_time = time.time() + + def _track_pause_end(self): + self.time_spent_paused += time.time() - self.pause_start_time + + def _seconds_to_human_string(self, seconds): + result = "" + hours = int(math.floor(seconds / 3600.)) + if hours >= 1: + result += "%d hours " % hours + minutes = int(math.floor(seconds / 60.) % 60) + if hours >= 1 or minutes >= 1: + result += "%d minutes " % minutes + result += "%d seconds" % int((math.floor(seconds) % 60)) + return result + + def _dump_statistics(self): + if self.log_statistics: + self._log_info("ERCF Statistics:") + self._log_info("%d Swaps Completed" % self.total_swaps) + self._log_info("%s spent loading" % self._seconds_to_human_string(self.time_spent_loading)) + self._log_info("%s spent unloading" % self._seconds_to_human_string(self.time_spent_unloading)) + self._log_info("%s spent paused (%d pauses total)" % (self._seconds_to_human_string(self.time_spent_paused), self.total_pauses)) + + cmd_ERCF_RESET_STATS_help = "Reset the ERCF statistics" + def cmd_ERCF_RESET_STATS(self, gcmd): + self._reset_statistics() + + cmd_ERCF_DUMP_STATS_help = "Dump the ERCF statistics" + def cmd_ERCF_DUMP_STATS(self, gcmd): + self._dump_statistics() + +################### +# SERVO FUNCTIONS # +################### + def _servo_set_angle(self, angle): + # if we ever set the angle directly, we are in an unknown state + # the up/down functions will reset this properly + self.servo_state = self.SERVO_UNKNOWN_STATE + self.gcode.run_script_from_command("SET_SERVO SERVO=ercf_servo ANGLE=%1.f" % angle) + + def _servo_off(self): + self.gcode.run_script_from_command("SET_SERVO SERVO=ercf_servo WIDTH=0.0") + + def _servo_down(self): + if self.servo_state == self.SERVO_DOWN_STATE: + return + + self._log_debug("Setting servo to down angle: %d" % (self.servo_down_angle)) + self.gear_stepper.do_set_position(0.) + self.gear_stepper.do_move(0.5, 25, self.gear_stepper_accel, sync=0) + + self._servo_set_angle(self.servo_down_angle) + + self.toolhead.dwell(0.2) + self.gear_stepper.do_move(0., 25, self.gear_stepper_accel, sync=0) + self.toolhead.dwell(0.1) + self.gear_stepper.do_move(0.5, 25, self.gear_stepper_accel, sync=0) + self.toolhead.dwell(0.1 + self.extra_servo_dwell_down / 1000.) + self.gear_stepper.do_move(0., 25, self.gear_stepper_accel, sync=0) + + self._servo_off() + self.servo_state = self.SERVO_DOWN_STATE + + def _servo_up(self): + if self.servo_state == self.SERVO_UP_STATE: + return + + self._log_debug("Setting servo to up angle: %d" % (self.servo_up_angle)) + self._servo_set_angle(self.servo_up_angle) + self.toolhead.dwell(0.25 + self.extra_servo_dwell_up / 1000.) + self._servo_off() + self.servo_state = self.SERVO_UP_STATE + + cmd_ERCF_SERVO_UP_help = "Disengage the ERCF gear" + def cmd_ERCF_SERVO_UP(self, gcmd): + self._servo_up() + + cmd_ERCF_SERVO_DOWN_help = "Engage the ERCF gear" + def cmd_ERCF_SERVO_DOWN(self, gcmd): + self._servo_down() + + cmd_ERCF_TEST_SERVO_help = "Test the servo angle" + def cmd_ERCF_TEST_SERVO(self, gcmd): + angle = gcmd.get_float('VALUE') + + self._servo_set_angle(angle) + self.toolhead.dwell(0.25 + self.extra_servo_dwell_up / 1000.) + self._servo_off() + +######################### +# CALIBRATION FUNCTIONS # +######################### + def _get_calibration_ref(self): + return self.variables['ercf_calib_ref'] + + def _get_tool_ratio(self, tool): + if tool < 0: + return 1. + return self.variables['ercf_calib_%d' % tool] + + def _get_calibration_version(self): + return self.variables.get('ercf_calib_version', 1) + + def _do_calculate_calibration_ref(self, extruder_homing_length = 400, extruder_homing_step = 2): + # assume we are homed and unloaded + self._select_tool(0) + self._servo_down() + self._set_steps(1.) + self._set_above_min_temp() + self._log_info("Calibrating reference tool") + encoder_moved = self._load_into_encoder() + self._load_to_end_of_bowden(self.calibration_bowden_length-encoder_moved) + if self.is_paused: + return + + self._log_debug("Moved to calibration distance %.1f - encoder reads %.1f" % (self.calibration_bowden_length, self._counter.get_distance())) + + final_position = self._home_to_extruder(extruder_homing_length, extruder_homing_step) + if self.is_paused: + self._log_info("Calibration failed - unable to home to the extruder") + return + self._log_info("Calibration reference is %.1f" % final_position) + self.gcode.run_script_from_command("SAVE_VARIABLE VARIABLE=ercf_calib_ref VALUE=%.1f" % final_position) + self.gcode.run_script_from_command("SAVE_VARIABLE VARIABLE=ercf_calib_0 VALUE=1.0") + self.gcode.run_script_from_command("SAVE_VARIABLE VARIABLE=ercf_calib_version VALUE=2") + self._do_calibration_unload(final_position) + + def _do_calculate_calibration_ratio(self, tool): + if self.is_paused: + return + + load_length = self.calibration_bowden_length - 100. + + self._select_tool(tool) + self._servo_down() + self._set_steps(1.) + self._counter.reset_counts() + encoder_moved = self._load_into_encoder() + self._load_to_end_of_bowden(load_length-encoder_moved) + final_position = self._counter.get_distance() + ratio = load_length / final_position + self._log_info("Calibration move to %.1f read distance %.1f - Ratio is %.12f" % (load_length, final_position, ratio)) + self.gcode.run_script_from_command("SAVE_VARIABLE VARIABLE=ercf_calib_%d VALUE=%.12f" % (tool, ratio)) + self._do_calibration_unload(load_length) + + def _do_calibration_unload(self, length): + self._set_steps(1.) + self.toolhead.dwell(0.1) + self.toolhead.wait_moves() + self._unload_from_end_of_bowden(length-self.unload_buffer) + self._unload_encoder(10) + self._servo_up() + + cmd_ERCF_CALIBRATE_help = "Complete calibration of all ERCF Tools" + def cmd_ERCF_CALIBRATE(self, gcmd): + self._disable_encoder_sensor() + self._log_info("Start the complete auto calibration...") + self._do_home(0) + for i in range(len(self.selector_offsets)): + if i == 0: + self._do_calculate_calibration_ref() + else: + self._do_calculate_calibration_ratio(i) + self._log_info("End of the complete auto calibration!") + self._log_info("Please reload the firmware for the calibration to be active!") + + cmd_ERCF_CALIBRATE_SINGLE_help = "Calibration of a single ERCF Tool" + def cmd_ERCF_CALIBRATE_SINGLE(self, gcmd): + tool = gcmd.get_int('TOOL', 0) + if tool == 0: + self._do_home(0) + self._do_calculate_calibration_ref() + else: + self._do_home(tool) + self._do_calculate_calibration_ratio(tool) + + cmd_ERCF_CALIB_SELECTOR_help = "Calibration of the selector position for a defined Tool" + def cmd_ERCF_CALIB_SELECTOR(self, gcmd): + self._servo_up() + tool = gcmd.get_int('TOOL', 0, minval=0) + move_length = 20 + (tool + 1) * 21 + ((tool + 1) / 3) * 5 + self._log_info("Measuring the selector position for tool %d" % tool) + + self.selector_stepper.do_set_position(0.) + init_position = self.selector_stepper.steppers[0].get_mcu_position() + self.selector_stepper.do_homing_move(-move_length, 50, self.selector_stepper.accel, True, True) + current_position = self.selector_stepper.steppers[0].get_mcu_position() + traveled_position = abs(current_position - init_position) * self.selector_stepper.steppers[0].get_step_dist() + self._log_info("Selector position = %.1f" % traveled_position) def _sample_stats(self, values): mean = 0. @@ -143,33 +470,7 @@ def _sample_stats(self, values): vmin = min(values) vmax = max(values) return {'mean': mean, 'stdev': stdev, 'min': vmin, - 'max': vmax, 'range': vmax - vmin} - - def _gear_stepper_move_wait(self, dist, wait=True, speed=None, accel=None): - self.gear_stepper.do_set_position(0.) - is_long_move = abs(dist) > self.LONG_MOVE_THRESHOLD - if speed is None: - speed = self.long_moves_speed if is_long_move \ - else self.short_moves_speed - if accel is None: - accel = self.long_moves_accel if is_long_move \ - else self.short_moves_accel - self.gear_stepper.do_move(dist, speed, accel, True) - if wait : - self.toolhead.wait_moves() - - def _selector_stepper_move_wait(self, dist, home=0, wait=True, - speed=80., accel=1800): - homing_string = "" - wait_string = "" - if home != 0: - homing_string = (" STOP_ON_ENDSTOP=%s" % home) - if not wait: - wait_string = (" SYNC=0") - command_string = ("MANUAL_STEPPER STEPPER=selector_stepper" - " SPEED=%s ACCEL=%s MOVE=%s%s%s" - % (speed, accel, dist, homing_string, wait_string)) - self.gcode.run_script_from_command(command_string) + 'max': vmax, 'range': vmax - vmin} cmd_ERCF_CALIBRATE_ENCODER_help = "Calibration routine for the ERCF encoder" def cmd_ERCF_CALIBRATE_ENCODER(self, gcmd): @@ -219,235 +520,518 @@ def cmd_ERCF_CALIBRATE_ENCODER(self, gcmd): gcmd.respond_info("Resulting resolution for the encoder = %.6f" % resolution) gcmd.respond_info("After calibration measured length = %.6f" - % new_result) - - cmd_ERCF_HOME_EXTRUDER_help = "Home the filament tip on the toolhead sensor" - def cmd_ERCF_HOME_EXTRUDER(self, gcmd): - homing_length = gcmd.get_float('TOTAL_LENGTH', 100., above=0.) - step_length = gcmd.get_float('STEP_LENGTH', 1., above=0.) - both_in_sync = True - homing_speed = 25. - sensor = self.printer.lookup_object( - "filament_switch_sensor toolhead_sensor") - sensor_state = bool(sensor.runout_helper.filament_present) - if sensor_state : - step_length = -step_length - both_in_sync = False # Do not move the ERCF if move is an unload - for step in range( int( homing_length / abs(step_length) ) + 1 ): - if bool(sensor.runout_helper.filament_present) == sensor_state: - if step * abs(step_length) >= homing_length : - self.gcode.respond_info( - "Unable to reach the toolhead sensor") - self.gcode.run_script_from_command(self.MACRO_UNSELECT_TOOL) - self.gcode.run_script_from_command(self.MACRO_PAUSE) - break - if both_in_sync : - self.gear_stepper.do_set_position(0.) - self.gear_stepper.do_move(step_length, homing_speed, - self.short_moves_accel, False) - pos = self.toolhead.get_position() - pos[3] += step_length - self.toolhead.manual_move(pos, homing_speed) + % new_result) + + cmd_ERCF_TEST_GRIP_help = "Test the ERCF grip for a Tool" + def cmd_ERCF_TEST_GRIP(self, gcmd): + self._servo_down() + self.cmd_ERCF_MOTORS_OFF(gcmd) + + cmd_ERCF_TEST_MOVE_GEAR_help = "Move the ERCF gear" + def cmd_ERCF_TEST_MOVE_GEAR(self, gcmd): + length = gcmd.get_float('LENGTH', 200.) + speed = gcmd.get_float('SPEED', 50.) + accel = gcmd.get_float('ACCEL', 200.) + self.gear_stepper.do_set_position(0.) + self.gear_stepper.do_move(length, speed, accel) + + cmd_ERCF_TEST_LOAD_SEQUENCE_help = "Test sequence" + def cmd_ERCF_TEST_LOAD_SEQUENCE(self, gcmd): + loops = gcmd.get_int('LOOP', 10.) + random = gcmd.get_int('RAND', 0) + for l in range(loops): + self._log_info("testing loop %d / %d" % (l, loops)) + for t in range(len(self.selector_offsets)): + tool = t + if random == 1: + tool = randint(0, len(self.selector_offsets)-1) + self._log_info("testing tool %d / %d" % (tool, len(self.selector_offsets))) + self._select_tool(tool) + + self._do_load(100, True) + self.toolhead.dwell(0.05) + self._do_unload(100, skip_extruder=True) + self._unselect_tool() self.toolhead.dwell(0.2) - else: - self.toolhead.wait_moves() - self.gear_stepper.do_set_position(0.) - break - cmd_ERCF_RESET_ENCODER_COUNTS_help = "Reset the ERCF encoder counts" - def cmd_ERCF_RESET_ENCODER_COUNTS(self, gcmd): - self._counter.reset_counts() +################### +# STATE FUNCTIONS # +################### + def _setup_heater_off_reactor(self): + self.reactor = self.printer.get_reactor() + self.heater_off_handler = self.reactor.register_timer(self._handle_pause_timeout, self.reactor.NEVER) + + def _handle_pause_timeout(self, eventtime): + self._log_info("Disable extruder heater") + self.gcode.run_script_from_command("M104 S0") + return self.reactor.NEVER + + def _pause(self): + self._track_pause_start() + self.paused_extruder_temp = self.printer.lookup_object("extruder").heater.target_temp + self._servo_up() + self.is_paused = True + self.gcode.run_script_from_command("SET_IDLE_TIMEOUT TIMEOUT=%d" % self.timeout_pause) + self.reactor.update_timer(self.heater_off_handler, self.reactor.monotonic() + self.disable_heater) + self._log_info("An issue with the ERCF has been detected and the ERCF has been PAUSED") + self._log_info("When you intervene to fix the issue, first call the \"ERCF_UNLOCK\" Gcode") + self._log_info("Refer to the manual before resuming the print") + self.gcode.run_script_from_command("SAVE_GCODE_STATE NAME=ERCF_state") + self._disable_encoder_sensor() + self.gcode.run_script_from_command("PAUSE") + + def _unlock(self): + self.is_paused = False + self.reactor.update_timer(self.heater_off_handler, self.reactor.NEVER) + self.gcode.run_script_from_command("M104 S%.1f" % self.paused_extruder_temp) + self._unselect_tool() + self.gcode.run_script_from_command("RESTORE_GCODE_STATE NAME=ERCF_state") + self._counter.reset_counts() + self._track_pause_end() + + def _disable_encoder_sensor(self): + self._log_trace("Disable encoder sensor") + self.gcode.run_script_from_command("SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=0") + + def _enable_encoder_sensor(self): + self._log_trace("Enable encoder sensor") + self.gcode.run_script_from_command("SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=1") + + def _is_in_print(self): + return self.printer.lookup_object('idle_timeout').state == "Printing" + + def _set_above_min_temp(self): + if not self.printer.lookup_object("extruder").heater.can_extrude : + self._log_info("M118 Heating extruder above min extrusion temp (%.1f)" % self.min_temp_extruder) + self.gcode.run_script_from_command("M109 S%.1f" % self.min_temp_extruder) + + cmd_ERCF_MOTORS_OFF_help = "Turn off both ERCF motors" + def cmd_ERCF_MOTORS_OFF(self, gcmd): + self.gear_stepper.do_enable(False) + self.selector_stepper.do_enable(False) + self.is_homed = False + self.tool_selected = -1 + + def _do_buzz_gear_motor(self): + self._counter.reset_counts() + self._gear_stepper_move_wait(2., False) + self._gear_stepper_move_wait(-2.) cmd_ERCF_BUZZ_GEAR_MOTOR_help = "Buzz the ERCF gear motor" def cmd_ERCF_BUZZ_GEAR_MOTOR(self, gcmd): + self._do_buzz_gear_motor() + + cmd_ERCF_PAUSE_help = "Pause the current print and lock the ERCF operations" + def cmd_ERCF_PAUSE(self, gcmd): + self._pause() + + cmd_ERCF_UNLOCK_help = "Unlock ERCF operations" + def cmd_ERCF_UNLOCK(self, gcmd): + self._log_info("Unlock the ERCF") + self._unlock() + self._log_info("Refer to the manual before resuming the print") + + cmd_ERCF_DISPLAY_ENCODER_POS_help = "Display current value of the ERCF encoder" + def cmd_ERCF_DISPLAY_ENCODER_POS(self, gcmd): + self._log_info("Encoder value is %.2f" % self._counter.get_distance()) + + cmd_ERCF_SET_END_OF_BOWDEN_TO_NOZZLE_help = "Update the end_of_bowden_to_nozzle value" + def cmd_ERCF_SET_END_OF_BOWDEN_TO_NOZZLE(self, gcmd): + val = gcmd.get_float('VALUE', minval=30.) + self.end_of_bowden_to_nozzle = val + +######################### +# GENERAL MOTOR HELPERS # +######################### + def _gear_stepper_move_wait(self, dist, wait=True, speed=None, accel=None): + self.gear_stepper.do_set_position(0.) + is_long_move = abs(dist) > self.LONG_MOVE_THRESHOLD + if speed is None: + speed = self.long_moves_speed if is_long_move \ + else self.short_moves_speed + if accel is None: + accel = self.long_moves_accel if is_long_move \ + else self.short_moves_accel + self.gear_stepper.do_move(dist, speed, accel, True) + if wait : + self.toolhead.wait_moves() + + def _selector_stepper_move_wait(self, dist, home=0, wait=True, + speed=80., accel=1800): + homing_string = "" + wait_string = "" + if home != 0: + homing_string = (" STOP_ON_ENDSTOP=%s" % home) + if not wait: + wait_string = (" SYNC=0") + command_string = ("MANUAL_STEPPER STEPPER=selector_stepper" + " SPEED=%s ACCEL=%s MOVE=%s%s%s" + % (speed, accel, dist, homing_string, wait_string)) + self.gcode.run_script_from_command(command_string) + +#################### +# UNLOAD FUNCTIONS # +#################### + def _check_filament_in_extruder(self, move_size = 20, threshold = 1): + self._servo_up() + self._log_debug("Checking for filament in extruder") + # reset the counter and move the extruder backwards by move_size self._counter.reset_counts() - self._gear_stepper_move_wait(2., False) - self._gear_stepper_move_wait(-2.) + pos = self.toolhead.get_position() + pos[3] -= move_size + self.toolhead.manual_move(pos, 20) + self.toolhead.wait_moves() - cmd_ERCF_LOAD_help = "Load filament from ERCF to the toolhead" - def cmd_ERCF_LOAD(self, gcmd): - req_length = gcmd.get_float('LENGTH', 0.) - num_moves = gcmd.get_int('MOVES', 1) - iterate = False if req_length == 0. else True + #check the new encoder position + final_encoder_pos = self._counter.get_distance() + self._log_trace("Extruder Moved %.2f, Read %.2f, Threshold %.2f" % (move_size, final_encoder_pos, threshold)) + + #if the new encoder position is beyond the threshold, the filament moved + if final_encoder_pos > threshold: + self._log_debug("Filament found in extruder") + return True + else: + self._log_debug("No filament found in extruder") + return False + + def _check_filament_in_encoder(self): + self._log_debug("Checking for filament in encoder") + self._servo_down() + self._do_buzz_gear_motor() + final_encoder_pos = self._counter.get_distance() + self._log_trace("After buzzing gear motor, encoder read %.1f" % final_encoder_pos) + self._servo_up() + return final_encoder_pos > 0. + + def _unload_from_extruder(self): + if (self.is_paused): + self._log_info('ERCF is currently paused. Please use ERCF_UNLOCK') + return + # assume there is filament in the extruder, but a tip is already formed + # extract the filament using the provided values, and then test that the + # filament is actually out + + self._log_debug("Extracting filament from extruder") + pos = self.toolhead.get_position() + + # initial slow move + pos[3] -= 10 + self._log_trace("Extracting - slow move") + self.toolhead.manual_move(pos, 12) self.toolhead.wait_moves() - self._counter.reset_counts() - self._gear_stepper_move_wait(self.LONG_MOVE_THRESHOLD) - if self._counter.get_distance() <= 6.: - self.gcode.respond_info("Error loading filament to ERCF, no" - " filament detected during load sequence," - " retry loading...") - # Reengage gears and retry - self.gcode.run_script_from_command(self.MACRO_SERVO_UP) - self.toolhead.wait_moves() - self.gcode.run_script_from_command(self.MACRO_SERVO_DOWN) + # faster complete move + the previous 10mm to make sure we are out + pos[3] -= self.end_of_bowden_to_nozzle + self.unload_parking_position + self._log_trace("Extracting - full move") + self.toolhead.manual_move(pos, 20) + self.toolhead.wait_moves() + + # back up 15mm at a time until the encoder doesnt see any movement + # try this 3 times, and then give up + out_of_extruder = False + for i in range(3): + self._log_debug("Testing if filament is still in the extruder - #%d" % i) + self._counter.reset_counts() + pos[3] -= self.encoder_move_step_size + self.toolhead.manual_move(pos, 20) self.toolhead.wait_moves() - self._gear_stepper_move_wait(self.LONG_MOVE_THRESHOLD) - - if self._counter.get_distance() <= 6.: - self.gcode.respond_info("Still no filament loaded in the ERCF," - " calling %s..." - % self.MACRO_PAUSE) - self.gcode.run_script_from_command(self.MACRO_UNSELECT_TOOL) - self.gcode.run_script_from_command(self.MACRO_PAUSE) - return - else : - self.gcode.respond_info("Filament loaded in ERCF after retry") + moved_length = self._counter.get_distance() + self._log_trace("Test extruder move of %1.fmm recorded %1.fmm at the extruder" % (self.encoder_move_step_size, moved_length)) + if (moved_length <= 1.): + out_of_extruder = True + break + + if out_of_extruder: + self.loaded_status = self.LOADED_STATUS_PARTIAL + self._log_debug("Filament is out of extruder") + else: + self._log_info("Filament seems to be stuck in the extruder - PAUSING") + self._pause() + + def _unload_encoder(self, max_steps=5): + if (self.is_paused): + self._log_info('ERCF is currently paused. Please use ERCF_UNLOCK') + return + + self._servo_down() + + self._log_debug("Unloading from the encoder") + for step in range(max_steps): + self._counter.reset_counts() + self._gear_stepper_move_wait(-self.encoder_move_step_size) + dist_moved = self._counter.get_distance() + delta = self.encoder_move_step_size - dist_moved + self._log_trace("Unloading from encoder - step %d. Moved %.1f, encoder read %.1f (delta %.1f)" % (step, self.encoder_move_step_size, dist_moved, delta)) + if delta >= 3.0: + # if there is a large delta here, we are out of the encoder + self.loaded_status = self.LOADED_STATUS_UNLOADED + self._counter.reset_counts() + self._gear_stepper_move_wait(-(self.parking_distance - delta)) + if self._counter.get_distance() < 5.0: + self._servo_up() + return + self._log_info( + "Unable to get the filament out of the encoder cart," + " please check the ERCF, calling ERCF_PAUSE...") + self._pause() + + def _unload_from_end_of_bowden(self, length): + if (self.is_paused): + self._log_info('ERCF is currently paused. Please use ERCF_UNLOCK') + return + + # assume there is filament at the end of the bowden + self._log_debug("Unloading from end of bowden") - if req_length != 0: - counter_distance = self._counter.get_distance() - for i in range(num_moves): - self._gear_stepper_move_wait((req_length/num_moves) - (counter_distance/num_moves)) + self._servo_down() + self.toolhead.dwell(0.2) + self.toolhead.wait_moves() + + self._counter.reset_counts() + + # Initial unload in sync (ERCF + extruder) for xx mms + #self._log_trace("Moving the gear and extruder motors in sync %.1f" % self.sync_unload_length) + #pos = self.toolhead.get_position() + #pos[3] -= self.sync_unload_length + #self.gear_stepper.do_move(-self.sync_unload_length, 30, self.gear_stepper_accel, sync=0) + #self.toolhead.manual_move(pos, 30) + #self.toolhead.wait_moves() + #counter_distance = self._counter.get_distance() + #length -= counter_distance + #self._log_debug("Sync unload move done %.1f / %.1f (diff: %.1f)" % (counter_distance, self.sync_unload_length, counter_distance - self.sync_unload_length)) + + self._counter.reset_counts() + # initial attempt to unload the filament + for i in range(self.num_moves): + self._log_trace("Moving the gear motor %.1f" % (-length / self.num_moves)) + self._gear_stepper_move_wait(-length / self.num_moves) + counter_distance = self._counter.get_distance() + delta_length = length - counter_distance + self._log_info("Unload move done: %.1f / %.1f (diff: %.1f)" + % (counter_distance, length, delta_length) ) + + # correction attempt to unload the filament + if delta_length >= 3.0: + self._log_debug("Attempting a correction move of %.1f" % delta_length) + self._gear_stepper_move_wait(-delta_length) counter_distance = self._counter.get_distance() - - self.gcode.respond_info( - "Load move done, requested = %.1f, measured = %.1f" - %(req_length, counter_distance)) - diff_distance = req_length - counter_distance - - if diff_distance <= 6. or not iterate : - # Measured move is close enough or no iterations : load succeeds + delta_length = length - counter_distance + self._log_info("Correction unload move done: %.1f / %.1f (diff: %.1f)" + % (counter_distance, length, delta_length) ) + + # after correction, still too far away - abort + if delta_length > 15.0: + self._log_info("Too much slippage detected during the unload: %.1f / %.1f (diff: %.1f)" + % (counter_distance, length, delta_length)) + self._pause() + return + + def _do_unload(self, length, slow_extract=False, skip_extruder=False): + if (self.is_paused): + self._log_info('ERCF is currently paused. Please use ERCF_UNLOCK') + return + + self._track_unload_start() + + if slow_extract: + if self._check_filament_in_encoder(): + # if we are doing a slow extract, always form a tip before any moves + # this may be a waste if there is no filament in the extruder + # but checking for filament before tip forming will cause stringing + self._set_above_min_temp() + self.gcode.run_script_from_command("_ERCF_FORM_TIP_STANDALONE") + if self._check_filament_in_extruder(): + #if we are in a slow extract state, check to see if filament + #is in the extruder. If so, we can do a fast extract instead + slow_extract = False + else : + #if we are in the slow extract state, and there is no filament in + #the encoder, we are already ejected + self._log_debug("Filament already ejected!") + self._track_unload_end() return - # Do the correction moves - for retry_attempts in range(2): - self._gear_stepper_move_wait(diff_distance) - counter_distance = self._counter.get_distance() - self.gcode.respond_info("Correction load move done," - " requested = %.1f, measured = %.1f" - %(req_length, counter_distance)) - diff_distance = req_length - counter_distance - if diff_distance <= 6.: - # Measured move is close enough : load succeeds - return - if diff_distance > self.LONG_MOVE_THRESHOLD: - break - # Load failed - self.gcode.respond_info( - "Too much slippage detected during the load," - " requested = %.1f, measured = %.1f - calling %s..." - %(req_length, counter_distance, self.MACRO_PAUSE)) - self.gcode.run_script_from_command(self.MACRO_UNSELECT_TOOL) - self.gcode.run_script_from_command(self.MACRO_PAUSE) + if slow_extract: + # if we are still in slow extract mode (AKA, we know the filament + # is not in the extruder), do a slow extract from the encoder + self._unload_encoder(int(length / self.encoder_move_step_size) + 5) + else: + # fast extract means we know we are in the extruder + # we can now do an extruder extract follow by a full bowden + # unload and an encoder unload + if not skip_extruder: + self._unload_from_extruder() + self._unload_from_end_of_bowden(length-self.unload_buffer) + self._unload_encoder(10) + self.loaded_status = self.LOADED_STATUS_UNLOADED + self._track_unload_end() + cmd_ERCF_UNLOAD_help = "Unload filament and park it in the ERCF" def cmd_ERCF_UNLOAD(self, gcmd): - # Define unload move parameters - self.toolhead.dwell(0.2) - iterate = True - buffer_length = 30. - homing_move = gcmd.get_int('HOMING', 0, minval=0, maxval=1) + if (self.is_paused): + self._log_info('ERCF is currently paused. Please use ERCF_UNLOCK') + return + unknown_state = gcmd.get_int('UNKNOWN', 0, minval=0, maxval=1) - req_length = gcmd.get_float('LENGTH', 1200.) - no_th = gcmd.get_int('NOTH', 0) - num_moves = gcmd.get_int('MOVES', 1) + length = gcmd.get_float('LENGTH', self._get_calibration_ref()) + self._do_unload(length, unknown_state) + + cmd_ERCF_EJECT_help = "Eject filament from the ERCF" + def cmd_ERCF_EJECT(self, gcmd): + if (self.is_paused): + self._log_info('ERCF is currently paused. Please use ERCF_UNLOCK') + return + self._disable_encoder_sensor() + self._do_unload(self._get_calibration_ref(), True) + + def _do_unload_tool(self): + self._log_info('Unload tool %d' % self.tool_selected) + self._set_steps(self._get_tool_ratio(self.tool_selected)) + self.toolhead.dwell(0.1) self.toolhead.wait_moves() - # Do not unload if filament is still in the toolhead - if no_th == 0: - sensor = self.printer.lookup_object( - "filament_switch_sensor toolhead_sensor") - if bool(sensor.runout_helper.filament_present): - self.gcode.respond_info( - "Unable to unload filament while still in extruder") - return - # i.e. long move that will be fast and iterated using the encoder - if req_length > self.LONG_MOVE_THRESHOLD: - req_length = req_length - buffer_length - else: - iterate = False - if unknown_state : - iterate = False - self._counter.reset_counts() - for i in range(num_moves): - self._gear_stepper_move_wait(-req_length/num_moves) - homing_move = 1 - if homing_move : - iterate = False - for step in range( int(req_length / 15.) ): - self._counter.reset_counts() - self._gear_stepper_move_wait(-15.) - delta = 15. - self._counter.get_distance() - # Filament is now out of the encoder - if delta >= 3. : - self._counter.reset_counts() - self._gear_stepper_move_wait(-(23. - delta)) - if self._counter.get_distance() < 5. : - return - else: - self._counter.reset_counts() - for i in range(num_moves): - self._gear_stepper_move_wait(-req_length / num_moves) - if iterate: - counter_distance = self._counter.get_distance() - self.gcode.respond_info( - "Unload move done, requested = %.1f, measured = %.1f" - % (req_length, counter_distance) ) - delta_length = req_length - counter_distance - if delta_length >= 3.0: - self._gear_stepper_move_wait(-delta_length) - counter_distance = self._counter.get_distance() - self.gcode.respond_info("Correction unload move done," - " requested = %.1f, measured = %.1f" - %(req_length, counter_distance)) - if ( req_length - counter_distance ) >= 15. : - # Unload failed - self.gcode.respond_info( - "Too much slippage detected during the unload," - " requested = %.1f, measured = %.1f - calling %s..." - %(req_length, counter_distance, self.MACRO_PAUSE)) - self.gcode.run_script_from_command(self.MACRO_UNSELECT_TOOL) - self.gcode.run_script_from_command(self.MACRO_PAUSE) - return - # Final move to park position - for step in range( int(buffer_length / 15.) + 2 ): - self._counter.reset_counts() - self._gear_stepper_move_wait(-15.) - delta = 15. - self._counter.get_distance() - # Filament is now out of the encoder - if delta >= 3. : - self._counter.reset_counts() - self._gear_stepper_move_wait(-(23. - delta)) - if self._counter.get_distance() < 5. : - return - # Filament stuck in encoder - self.gcode.respond_info( - "Unable to get the filament out of the encoder cart," - " please check the ERCF, calling %s..." - % self.MACRO_PAUSE) - self.gcode.run_script_from_command(self.MACRO_UNSELECT_TOOL) - self.gcode.run_script_from_command(self.MACRO_PAUSE) - - cmd_ERCF_SET_STEPS_help = "Changes the steps/mm for the ERCF gear motor" - def cmd_ERCF_SET_STEPS(self, gcmd): - ratio = gcmd.get_float('RATIO', 1., above=0.) - new_step_dist = self.ref_step_dist / ratio - stepper = self.gear_stepper.rail.steppers[0] - if hasattr(stepper, "set_rotation_distance"): - new_rotation_dist = new_step_dist * stepper.get_rotation_distance()[1] - stepper.set_rotation_distance(new_rotation_dist) - else: - # bw compatibilty for old klipper versions - stepper.set_step_dist(new_step_dist) + self._do_unload(self._get_calibration_ref()) + self._servo_up() - cmd_ERCF_GET_SELECTOR_POS_help = "Report the selector motor position" - def cmd_ERCF_GET_SELECTOR_POS(self, gcmd): - ref_pos = gcmd.get_float('REF', 0.) - self.selector_stepper.do_set_position(0.) - init_position = self.selector_stepper.steppers[0].get_mcu_position() - self.command_string = ( - "MANUAL_STEPPER STEPPER=selector_stepper SPEED=50" - " MOVE=-" + str(ref_pos) + " STOP_ON_ENDSTOP=1") - self.gcode.run_script_from_command(self.command_string) +################## +# LOAD FUNCTIONS # +################## + def _load_into_encoder(self): + if (self.is_paused): + return 0 - current_position = self.selector_stepper.steppers[0].get_mcu_position() - traveled_position = abs(current_position - init_position) \ - * self.selector_stepper.steppers[0].get_step_dist() - self.gcode.respond_info("Selector position = %.1f " - %(traveled_position)) - - cmd_ERCF_MOVE_SELECTOR_help = "Move the ERCF selector" - def cmd_ERCF_MOVE_SELECTOR(self, gcmd): - target = gcmd.get_float('TARGET', 0.) + self._servo_down() + + self._counter.reset_counts() + self._gear_stepper_move_wait(self.LONG_MOVE_THRESHOLD) + self._log_trace("Initial load into encoder of %.1fmm - read %.1f on encoder" % (self.LONG_MOVE_THRESHOLD, self._counter.get_distance())) + if self._counter.get_distance() > 6.: + return self._counter.get_distance() + + self._log_info("Error loading filament - not enough detected at encoder") + + self._servo_up() + self.toolhead.wait_moves() + self._servo_down() + self.toolhead.wait_moves() + self._gear_stepper_move_wait(self.LONG_MOVE_THRESHOLD) + self._log_trace("Retry load into encoder of %.1fmm - read %.1f on encoder" % (self.LONG_MOVE_THRESHOLD, self._counter.get_distance())) + if self._counter.get_distance() <= 6.: + self._log_info("Error loading filament - not enough detected at encoder after retry") + self._pause() + return 0 + self.loaded_status = self.LOADED_STATUS_PARTIAL + return self._counter.get_distance() + + def _load_to_end_of_bowden(self, length): + if (self.is_paused): + return + + self._servo_down() + + start_distance = self._counter.get_distance() + for i in range(self.num_moves): + self._gear_stepper_move_wait(length/self.num_moves) + counter_distance = self._counter.get_distance() - start_distance + delta_length = length - counter_distance + self._log_info("Load move done: %.1f / %.1f (diff: %.1f)" + % (counter_distance, length, delta_length) ) + + # correction attempt to unload the filament + for i in range(2): + if delta_length >= 6.0: + self._log_debug("Attempting a correction move of %.1f" % delta_length) + self._gear_stepper_move_wait(delta_length) + counter_distance = self._counter.get_distance() - start_distance + delta_length = length - counter_distance + self._log_info("Correction load move done: %.1f / %.1f (diff: %.1f)" + % (counter_distance, length, delta_length) ) + else: + break + + self.loaded_status = self.LOADED_STATUS_PARTIAL + + # after correction, still too far away - abort + if delta_length > self.LONG_MOVE_THRESHOLD: + self._log_info("Too much slippage detected during the load: %.1f / %.1f (diff: %.1f)" + % (counter_distance, length, delta_length)) + self._pause() + return + + def _home_to_extruder(self, length, step): + self._servo_down() + + self._log_debug("Homing to extruder with %1.fmm moves" % (step)) + self.gear_stepper.do_set_position(0.) + for i in range (int(length / step)): + pre_move_position = self._counter.get_distance() + self.gear_stepper.do_move(step*(i+1), 5, self.gear_stepper_accel) + self.toolhead.dwell(0.2) + self.toolhead.wait_moves() + post_move_position = self._counter.get_distance() + + self._log_trace("Step #%d: pos: %.1f, last delta: %.1f" % (i, post_move_position, post_move_position - pre_move_position)) + if post_move_position - pre_move_position < step / 2.: + # not enough movements means we've hit the extruder + self._log_debug("Extruder reached after %d moves" % i) + return pre_move_position + + self._log_info("Failed to reach extruder after moving %.1fmm, pausing" % length) + self._pause() + + def _load_to_nozzle(self): + if (self.is_paused): + return + + self._log_debug("Loading to the nozzle") + + self._servo_up() + self._counter.reset_counts() + pos = self.toolhead.get_position() + pos[3] += self.end_of_bowden_to_nozzle + self.toolhead.manual_move(pos, 20) + self.toolhead.dwell(0.2) + self.toolhead.wait_moves() + moved_length = self._counter.get_distance() + self._log_debug("Extruder move to nozzle attempted %.1f - encoder read %.1f" % (self.end_of_bowden_to_nozzle, moved_length)) + + if moved_length < 5.: + self._log_info("Move to nozzle failed, pausing!") + self._pause() + return + + self.loaded_status = self.LOADED_STATUS_FULL + self._log_info('ERCF load successful') + + def _do_load(self, length, no_nozzle = False): + if self.is_paused: + self._log_info('ERCF is currently paused. Please use ERCF_UNLOCK') + return + + self._track_load_start() + self.toolhead.wait_moves() + already_loaded = self._load_into_encoder() + self._load_to_end_of_bowden(length-already_loaded) + self._home_to_extruder(self.extruder_homing_max, self.extruder_homing_step) + if not no_nozzle: + self._load_to_nozzle() + self._track_load_end() + + cmd_ERCF_LOAD_help = "Load filament from ERCF to the toolhead" + def cmd_ERCF_LOAD(self, gcmd): + length = gcmd.get_float('LENGTH', 100.) + self._do_load(length, True) + + def _do_load_tool(self, tool): + self._log_info('Loading tool %d' % tool) + self._set_steps(self._get_tool_ratio(tool)) + self._select_tool(tool) + self._do_load(self._get_calibration_ref()) + +############################ +# TOOL SELECTION FUNCTIONS # +############################ + def _do_move_selector_sensorless(self, target): selector_steps = self.selector_stepper.steppers[0].get_step_dist() init_position = self.selector_stepper.get_position()[0] init_mcu_pos = self.selector_stepper.steppers[0].get_mcu_position() @@ -472,12 +1056,12 @@ def cmd_ERCF_MOVE_SELECTOR(self, gcmd): self.toolhead.wait_moves() # Engage filament to the encoder - self.gcode.run_script_from_command(self.MACRO_SERVO_DOWN) + self._servo_down() self.gcode.run_script_from_command("ERCF_LOAD LENGTH=45") self.gcode.run_script_from_command("G4 P50") # Try to unload self.gcode.run_script_from_command("ERCF_UNLOAD LENGTH=68") - self.gcode.run_script_from_command(self.MACRO_SERVO_UP) + self._servo_up() # Check if selector can reach proper target self.gcode.run_script_from_command("ERCF_HOME_SELECTOR") init_position = self.selector_stepper.get_position()[0] @@ -495,55 +1079,243 @@ def cmd_ERCF_MOVE_SELECTOR(self, gcmd): # Selector path is still blocked self.gcode.respond_info( "Selector recovery failed, please check the ERCF," - " calling %s..." - % self.MACRO_PAUSE) - self.gcode.run_script_from_command(self.MACRO_UNSELECT_TOOL) - self.gcode.run_script_from_command(self.MACRO_PAUSE) + " calling ERCF_PAUSE...") + self._unselect_tool() + self._pause() else : # Selector path is blocked self.gcode.respond_info( "Selector path is blocked, " - " please check the ERCF, calling %s..." - % self.MACRO_PAUSE) - self.gcode.run_script_from_command(self.MACRO_UNSELECT_TOOL) - self.gcode.run_script_from_command(self.MACRO_PAUSE) - - cmd_ERCF_ENDLESSSPOOL_UNLOAD_help = "Unload the filament from the toolhead" - def cmd_ERCF_ENDLESSSPOOL_UNLOAD(self, gcmd): - self.gcode.respond_info("This is a placeholder") - - cmd_ERCF_FINALIZE_LOAD_help = "Finalize the load of a tool to the nozzle" - def cmd_ERCF_FINALIZE_LOAD(self, gcmd): - length = gcmd.get_float('LENGTH', 30.0, above=0.) - tune = gcmd.get_int('TUNE', 0) - threshold = gcmd.get_float('THRESHOLD', 10.0, above=0.) - if length is None : - self.gcode.respond_info("LENGTH has to be specified") + " please check the ERCF, calling ERCF_PAUSE...") + self._unselect_tool() + self._pause() + + def _set_steps(self, ratio=1.): + self._log_trace("Setting ERCF gear motor step ratio to %.1f" % ratio) + new_step_dist = self.ref_step_dist / ratio + stepper = self.gear_stepper.rail.steppers[0] + if hasattr(stepper, "set_rotation_distance"): + new_rotation_dist = new_step_dist * stepper.get_rotation_distance()[1] + stepper.set_rotation_distance(new_rotation_dist) + else: + # backwards compatibility for old klipper versions + stepper.set_step_dist(new_step_dist) + + def _unselect_tool(self): + if self.is_paused: + self._log_info("Could not unselect tool, ERCF is paused") return - self._counter.reset_counts() - pos = self.toolhead.get_position() - pos[3] += length - self.toolhead.manual_move(pos, 20) + + if not self.is_homed: + self._log_info("Could not unselect tool, ERCF is not homed") + return + + self._servo_up() + self._set_steps(1.) + + def _select_tool(self, tool): + if self.is_paused: + self._log_info("Could not select tool, ERCF is paused") + return + + if not self.is_homed: + self._log_info("Could not select tool, ERCF is not homed") + return + + self._log_info("Select Tool %d..." % tool) + self._servo_up() + if self.sensorless_selector == 1: + self._do_move_selector_sensorless(self.selector_offsets[tool]) + else: + self._selector_stepper_move_wait(self.selector_offsets[tool]) + self.tool_selected = tool + self._log_info("Tool %d Enabled" % tool) + + def _do_home_selector(self, tool = 0): + self._log_info("Homing selector") + self._servo_up() + num_channels = len(self.selector_offsets) + selector_length = 20. + num_channels*21. + (num_channels/3)*5. + self._log_debug("Moving up to %.1fmm to home a %d channel ERCF" % (selector_length, num_channels)) + + self.gcode.run_script_from_command("QUERY_ENDSTOPS") + + if self.sensorless_selector == 1: + self.selector_stepper.do_set_position(0.) + self.selector_stepper.do_homing_move(-selector_length, 60, self.selector_stepper.accel, True, True) + self.selector_stepper.do_set_position(0.) + else: + self.selector_stepper.do_set_position(0.) + self.selector_stepper.do_homing_move(-selector_length, 100, self.selector_stepper.accel, True, True) + self.selector_stepper.do_set_position(0.) + self.selector_stepper.do_move(5., 100, 0) + self.selector_stepper.do_homing_move(-10, 10, self.selector_stepper.accel, True, True) + self.selector_stepper.do_set_position(0.) + + self.gcode.run_script_from_command("QUERY_ENDSTOPS") self.toolhead.wait_moves() - final_encoder_pos = self._counter.get_distance() - if tune == 1 : - self.gcode.respond_info( - "Measured value from the encoder was %.1f" - % final_encoder_pos) - threshold_value = final_encoder_pos - 10.0 - self.gcode.respond_info( - "Check the manual to verify that this load was sucessful, and if so, use the following value for your threshold parameter : %.1f" - % threshold_value) + self.selector_stepper.do_move(self.selector_offsets[tool], 50, self.selector_stepper.accel) + self.toolhead.wait_moves() + endstop_lookup = 'manual_stepper selector_stepper' + if self.sensorless_selector: + endstop_lookup = 'manual_stepper gear_stepper' + selector_state = self.printer.lookup_object("query_endstops").get_status(0)['last_query'][endstop_lookup] + if selector_state != 1: + self._log_info("Homing failed - check what is blocking the selector") + self._pause() + + def _do_home(self, tool = 0): + if self._get_calibration_version() != 2: + self._log_info("You are running an old calibration version. It is strongly recommended that you rerun 'ERCF_CALIBRATE_SINGLE TOOL=0' to generate an updated calibration value") + self._disable_encoder_sensor() + self.is_homed = True + if self.is_paused: + self._unlock() + self._log_info("Homing ERCF...") + self._do_unload(self._get_calibration_ref(), True) + self._do_home_selector(tool) + self.tool_selected = -1 + self._log_info("Homing ERCF ended...") + + cmd_ERCF_CHANGE_TOOL_SLICER_help = "Perform a tool swap during a print" + def cmd_ERCF_CHANGE_TOOL_SLICER(self, gcmd): + if self.is_paused: return - if (final_encoder_pos < threshold) : - self.gcode.respond_info( - "Filament seems blocked between the extruder and the nozzle," - "threshold is %.1f while measured value was %.1f," - " calling %s..." - % (threshold, final_encoder_pos, self.MACRO_PAUSE)) - self.gcode.run_script_from_command(self.MACRO_PAUSE) + + tool = gcmd.get_int('TOOL') + if tool == self.tool_selected: + return + + self._disable_encoder_sensor() + self._do_unload_tool() + self._do_load_tool(tool) + if self.enable_clog_detection: + self._enable_encoder_sensor() + + cmd_ERCF_CHANGE_TOOL_STANDALONE_help = "Perform a tool swap out of a print" + def cmd_ERCF_CHANGE_TOOL_STANDALONE(self, gcmd): + if self.is_paused: + return + + tool = gcmd.get_int('TOOL') + if tool == self.tool_selected and self.loaded_status == self.LOADED_STATUS_FULL: + self._log_info('Tool %d is already selected' % tool) + return + + if not self.is_homed: + self._log_info('ERCF not homed, homing it...') + self._do_home(tool) + + self._disable_encoder_sensor() + self._do_unload(self._get_calibration_ref(), True) + self._set_above_min_temp() + self._do_load_tool(tool) + + cmd_ERCF_CHANGE_TOOL_help = "Perform a tool swap" + def cmd_ERCF_CHANGE_TOOL(self, gcmd): + tool = gcmd.get_int('TOOL') + initial_tool_string = 'unknown tool' if self.tool_selected else ("T%d" % self.tool_selected) + self._log_info("Tool swap requested, from %s to T%d" % (initial_tool_string, tool)) + self.gcode.run_script_from_command("M117 %s -> T%d" % (initial_tool_string, tool)) + if self._is_in_print(): + self.cmd_ERCF_CHANGE_TOOL_SLICER(gcmd) + else: + self.cmd_ERCF_CHANGE_TOOL_STANDALONE(gcmd) + self._track_swap_completed() + self._dump_statistics() + self._counter.reset_counts() + + cmd_ERCF_HOME_help = "Home the ERCF" + def cmd_ERCF_HOME(self, gcmd): + tool = gcmd.get_int('TOOL', 0) + + self._do_home(tool) + + cmd_ERCF_SELECT_TOOL_help = "Select the specified tool" + def cmd_ERCF_SELECT_TOOL(self, gcmd): + tool = gcmd.get_int('TOOL', 0) + + self._select_tool(tool) + +############################ +# RUNOUT AND ENDLESS SPOOL # +############################ + cmd_ERCF_ENCODER_RUNOUT_help = "Encoder runout handler" + def cmd_ERCF_ENCODER_RUNOUT(self, gcmd): + if self.tool_selected == -1: + self._log_info("Issue on an unknown tool - skipping clog/runout detection") return - self.gcode.respond_info("Filament loaded successfully") + + self._log_info("Issue on tool %d" % self.tool_selected) + self._log_info("Checking if this is a clog or a runout...") + + self._counter.reset_counts() + self._disable_encoder_sensor() + self._servo_down() + self._do_buzz_gear_motor() + self._servo_up() + moved = self._counter.get_distance() + if moved > 0.: + self._log_info("A clog has been detected and requires manual intervention") + else: + self._log_info("A runout has been detected") + # no movement means we are have a runout + if self.enable_endless_spool: + initial_pa = self.printer.lookup_object("extruder").get_status(0)['pressure_advance'] + self._log_info("EndlessSpool mode is ON!") + next_check = self.tool_selected + 1 + next_tool = -1 + while next_check != self.tool_selected: + if next_check > len(self.selector_offsets): + next_check = 0 + self._log_trace("Checking tool %d to see if it matches the right group" % next_check) + if self.endless_spool_groups[next_check] == self.endless_spool_groups[self.tool_selected]: + self._log_info("Found next tool in group: %d" % next_check) + next_tool = next_check + break + next_check += 1 + if next_tool == -1: + self._log_info("No more spools found in group %d - manual intervention is required" % self.endless_spool_groups[self.tool_selected]) + return + self.gcode.run_script_from_command("SAVE_GCODE_STATE NAME=ERCF_Pre_Brush_init") + self.gcode.run_script_from_command("_ERCF_ENDLESS_SPOOL_PRE_UNLOAD") + + self._log_info("Unloading filament...") + self.gcode.run_script_from_command("G91") + self.gcode.run_script_from_command("_ERCF_FORM_TIP_STANDALONE") + self._do_unload(self._get_calibration_ref(), False) + self._do_load_tool(next_tool) + self.gcode.run_script_from_command("SET_PRESSURE_ADVANCE ADVANCE=%.10f" % initial_pa) + + if self.is_paused: + self._log_info("ERCF is paused, cannot resume print") + return + + self.gcode.run_script_from_command("_ERCF_ENDLESS_SPOOL_POST_LOAD") + self.gcode.run_script_from_command("RESTORE_GCODE_STATE NAME=ERCF_Pre_Brush_init") + self.gcode.run_script_from_command("RESUME") + self._enable_encoder_sensor() + else: + self._log_info("EndlessSpool mode is off - manual intervention is required") + +##################### +# LOGGING FUNCTIONS # +##################### + cmd_ERCF_SET_LOG_LEVEL_help = "Set the log level for the ERCF" + def cmd_ERCF_SET_LOG_LEVEL(self, gcmd): + self.log_level = gcmd.get_int('LEVEL', 1) + + def _log_info(self, message): + if self.log_level > 0: + self.gcode.respond_info(message) + + def _log_debug(self, message): + if self.log_level > 1: + self.gcode.respond_info("DEBUG: %s" % message) + + def _log_trace(self, message): + if self.log_level > 2: + self.gcode.respond_info("TRACE: %s" % message) def load_config(config): return Ercf(config) diff --git a/Klipper_Files/client_macros.cfg b/Klipper_Files/client_macros.cfg index a837166a..654cd9cb 100644 --- a/Klipper_Files/client_macros.cfg +++ b/Klipper_Files/client_macros.cfg @@ -45,13 +45,13 @@ gcode: [gcode_macro RESUME] rename_existing: BASE_RESUME gcode: - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int != 0 %} + {% if printer.ercf.is_paused|int == 1 %} M118 You can't resume the print without unlocking the ERCF first. M118 Run ERCF_UNLOCK and solve any issue before hitting Resume again {% else %} RESTORE_GCODE_STATE NAME=PAUSE_state G90 - {% if printer["gcode_macro _ERCF_VAR"].clog_detection|int == 1 %} + {% if printer.ercf.clog_detection|int == 1 %} SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=1 {% endif %} BASE_RESUME diff --git a/Klipper_Files/ercf_hardware.cfg b/Klipper_Files/ercf_hardware.cfg index dc4ba299..ed098929 100644 --- a/Klipper_Files/ercf_hardware.cfg +++ b/Klipper_Files/ercf_hardware.cfg @@ -1,20 +1,22 @@ ## Enraged Rabbit : Carrot Feeder V1.1 hardware config file # Values are an example for voron 2.4 with 2 SKR1.4 +[mcu ercf] +serial: /dev/serial/by-id/usb-Klipper_samd21g18a_F6EE357D3432585020312E3545150FFF-if00 # Carrot Feeder 5mm D-cut shaft # Example for an SKR 1.4 Board (E1 on the XY mcu) [manual_stepper gear_stepper] -step_pin: P1.15 -dir_pin: P1.14 -enable_pin: !P1.16 -rotation_distance: 22.6789511 #Bondtech 5mm Drive Gears +step_pin: ercf:PA4 +dir_pin: !ercf:PA10 +enable_pin: !ercf:PA2 +rotation_distance: 22.554216869 #Bondtech 5mm Drive Gears gear_ratio: 80:20 microsteps: 16 # Please do not go higher than 16, this can cause 'MCU Timer too close' issues under Klipper full_steps_per_rotation: 200 #200 for 1.8 degree, 400 for 0.9 degree velocity: 35 accel: 150 #Right now no pin is used for the endstop, but we need to define one for klipper. So just use a random, not used pin -endstop_pin: P0.10 +endstop_pin: ^ercf:PA7 [tmc2209 manual_stepper gear_stepper] # Adapt accordingly to your setup and desires @@ -22,57 +24,59 @@ endstop_pin: P0.10 # Please adapt those values to the motor you are using # Example : for NEMA17 motors, you'll usually set the stealthchop_threshold to 0 # and use higher current -uart_pin: P1.1 +uart_pin: ercf:PA8 +uart_address: 0 interpolate: True run_current: 0.40 -hold_current: 0.1 -sense_resistor: 0.110 +hold_current: 0.3 +sense_resistor: 0.150 stealthchop_threshold: 500 -[tmc2209 manual_stepper selector_stepper] -uart_pin: P1.8 -run_current: 0.55 -interpolate: True -sense_resistor: 0.110 -stealthchop_threshold: 500 -# Uncomment the lines below if you want to use sensorless homing for the selector -#diag_pin: ^P1.27 # Set to MCU pin connected to TMC DIAG pin -#driver_SGTHRS: 75 # 255 is most sensitive value, 0 is least sensitive - # Carrot Feeder selector # Example for an SKR 1.4 Board (Z1 on the XY mcu) [manual_stepper selector_stepper] -step_pin: P0.22 -dir_pin: P2.11 -enable_pin: !P0.21 -microsteps: 16 # Please do not go higher than 16, this can cause 'MCU Timer too close' issues under Klipper +step_pin: ercf:PA9 +dir_pin: !ercf:PB8 +enable_pin: !ercf:PA11 +# step_pin: PE1 +# dir_pin: !PE0 +# enable_pin: PC5 rotation_distance: 40 +microsteps: 16 # Please do not go higher than 16, this can cause 'MCU Timer too close' issues under Klipper full_steps_per_rotation: 200 #200 for 1.8 degree, 400 for 0.9 degree velocity: 200 accel: 600 -# Select the endstop you want depending if you are using sensorless homing for the selector or not -endstop_pin: P1.25 -#endstop_pin: tmc2209_selector_stepper:virtual_endstop +endstop_pin: ^ercf:PB9 + +[tmc2209 manual_stepper selector_stepper] +uart_pin: ercf:PA8 +uart_address: 1 +# uart_pin: PD11 +run_current: 0.40 +hold_current: 0.2 +interpolate: True +# sense_resistor: 0.150 +sense_resistor: 0.110 +stealthchop_threshold: 500 +# Uncomment the lines below if you want to use sensorless homing for the selector +#diag_pin: ^P1.27 # Set to MCU pin connected to TMC DIAG pin +#driver_SGTHRS: 75 # 255 is most sensitive value, 0 is least sensitive # Values are for the MG90S servo [servo ercf_servo] -pin: P1.26 +pin: ercf:PA5 maximum_servo_angle: 180 minimum_pulse_width: 0.00085 maximum_pulse_width: 0.00215 [duplicate_pin_override] -pins: P1.0 +pins: ercf:PA6 # Put there the pin used by the encoder and the filament_motion_sensor # It has to be the same pin for those 3 [filament_motion_sensor encoder_sensor] -switch_pin: ^P1.0 +switch_pin: ^ercf:PA6 pause_on_runout: False detection_length: 10.0 extruder: extruder -# runout_gcode: _ERCF_ENCODER_MOTION_ISSUE - -[filament_switch_sensor toolhead_sensor] -pause_on_runout: False -switch_pin: ^P1.27 +runout_gcode: ERCF_ENCODER_RUNOUT diff --git a/Klipper_Files/ercf_parameters.cfg b/Klipper_Files/ercf_parameters.cfg new file mode 100644 index 00000000..b39acb81 --- /dev/null +++ b/Klipper_Files/ercf_parameters.cfg @@ -0,0 +1,72 @@ +[ercf] +# Encoder setup +# The encoder_pin must match the pin defined in ercf_hardware +# The encoder resolution is determined by running the ERCF_CALIBRATE_ENCODER +# See the manual for details on both settings +encoder_pin: ^ercf:PA6 +encoder_resolution: 1.333156 + +# Speeds and accels +# Long moves are faster than the small ones +# 100mm/s should be "quiet" with the NEMA14 motor or a NEMA17 pancake, but you can go lower if your really want it to be low noise +# Tested without any issue at 200mm/s, but it's noisy. I'm running mine at 170mm/s for long moves and 50mm/s for short moves. +long_moves_speed: 100 # mm/s. Default value is 100mm/s. +long_moves_accel: 400 # mm/s². Default value is 400mm/s² +short_moves_speed: 25 # mm/s. Default value is 25mm/s. +short_moves_accel: 400 # mm/s². Default value is 400mm/s² + +# Logging +# log_level can be set to one of (0 = off, 1 = info, 2 = debug, 3 = trace). +# Info is a good level for day to day usage, with debug and trace being useful for identifying problems +# or seeking help in discord. Setting this to off will supress all ERCF messages, including pauses and other important messages +log_level: 1 +# log_statistics set to 1 will log print statistics on each tool change +# These statistics include things like the number of swaps completed, as well as time spent swapping +log_statistics: 1 + +# Servo Configuration +# Default values: +# MG90S servo: Up=30, Down=140 +# SAVOX SH0255MG: Up=140, Down=30 +servo_up_angle: 140 +servo_down_angle: 65 +extra_servo_dwell_up: 0 # Additional dwell time in ms to apply to dwell prior to turning off the servo +extra_servo_dwell_down: 0 # Additional dwell time in ms to apply to dwell prior to turning off the servo + +# Base value for the loading length used by the auto-calibration macro +# Please use a value SMALLER than the real reverse bowden length (like 50mm less) +# This is only used during calibration, and has no impact on normal running +calibration_bowden_length: 880 + +# The position of each tool on the selector. See the manual for setting these values +colorselector: 2.3, 23.2, 44.0, 69.6, 91.2, 112.8, 138.4, 160.0, 180.0 + +# Distance between the end of the bowden tube and the nozzle +# This value can be determined by manually inserting filament at the very beginning of your +# extruder, and advancing it 1-2mm at a time until it starts to extrude from the nozzle. +# Subtract 1-2mm from that distance distance to get this value. If you have large gaps +# in your purge tower, increase this value. If you have blobs, reduce this value. +# This value will depend on your extruder, hotend and nozzle setup. +end_of_bowden_to_nozzle: 72 + +# if you are getting "timer too close" errors, increase the number of moves used during loading and unloading +num_moves: 2 # Number of moves to make when loading or unloading + +# Features +sensorless_selector: 0 # 0 = use an endstop, 1 = use sensorless homing +enable_clog_detection: 0 # 0 = do not use clog detection, 1 = use clog detection +enable_endless_spool: 0 # 0 = do not use endless spool, 1 = use endless spool +# if endless spool is turned on, you must define a list of EndlessSpool groups here, one entry for each tool in your ERCF +# when filament runs out on a tool, it will switch to the next tool with the same group number +# for example, if set to 1, 2, 3, 1, 2, 3, 1, 2, 3 on a 9 cart ERCF, and a runout occurs on tool #0 +# the ERCF will switch to using tool #3. +endless_spool_groups: 1, 2, 3, 1, 2, 3, 1, 2, 3 + +# configurable, but fairly fixed values +gear_stepper_accel: 0 # The acceleration value applied to the gear stepper on moves, standard is to use 0 +timeout_pause: 72000 # Time out used by the ERCF_PAUSE +disable_heater: 600 # Delay after which the hotend heater is disabled in the ERCF_PAUSE state +min_temp_extruder: 180 # Temp used during the auto-calibration macro, to ensure we can move the extruder (but not really extruding) +unload_buffer:50 # The amount, in mm, to reduce the unload so that the more accurate encoder unload has room to operate +extruder_homing_max: 50 # the maximum distance to advance in order to attempt to home the extruder +extruder_homing_step: 2 # the step size to use when homing the extruder diff --git a/Klipper_Files/ercf_software.cfg b/Klipper_Files/ercf_software.cfg index 70213747..a281deb9 100644 --- a/Klipper_Files/ercf_software.cfg +++ b/Klipper_Files/ercf_software.cfg @@ -1,607 +1,52 @@ -############################### -# ERCF module -############################### - -[ercf] -# Encoder -encoder_pin: ^P1.0 -encoder_resolution: 1.365188 # in mm -# Speeds and accels -# Long moves are faster than the small ones -# 100mm/s should be "quiet" with the NEMA14 motor or a NEMA17 pancake, but you can go lower if your really want it to be low noise -# Tested without any issue at 200mm/s, but it's noisy. I'm running mine at 170mm/s for long moves and 50mm/s for short moves. -long_moves_speed: 100 # mm/s. Default value is 100mm/s. -long_moves_accel: 400 # mm/s². Default value is 400mm/s² -short_moves_speed: 25 # mm/s. Default value is 25mm/s. -short_moves_accel: 400 # mm/s². Default value is 400mm/s² - -[gcode_macro _ERCF_VAR] -description: Empty macro to store ERCF variables -# ======================================================= -# ================== VALUES TO ADJUST =================== -# ======================================================= -# -# ==== Toolhead specific values ==== -# -# Distance between the end of the reverse bowden and the toolhead sensor. Value is toolhead specific. -# Tested values : -# Galileo Clockwork with ERCF V1.1 sensor (hall effect) : 27.0 -# LGX on AfterBurner with ERCF V1.1 sensor (hall effect) : 44.0 -# AfterBurner Clockwork with ERCF V1.1 sensor (hall effect) : 36.0 -variable_end_of_bowden_to_sensor: 27.0 -# Length from the sensor to the nozzle melt pool. -# Reduce this value if there are blobs of filament on each load, before the purge on the tower. -# Increase this value if there are big gaps on the purge tower (i.e. if it takes time for the filament to get pushed out after a swap) -# Tested values : -# Galileo Clockwork with ERCF 1.1 sensor (hall effect) & Dragon Normal Flow : 60.5 -# LGX on AfterBurner with ERCF 1.1 sensor (hall effect) & Dragon Normal Flow : 55.6 -# AfterBurner Clockwork with ERCF 1.1 sensor (hall effect) & Dragon Normal Flow : 54.0 -variable_sensor_to_nozzle: 60.5 -# -# ==== Values to tune ==== -# -# Tool position for the selector. This has to be tuned manually. Please scale this array to the number of tools you have -variable_colorselector = [2.4, 24.0, 44.8, 71.2, 92.0, 113.6, 139.2, 160.8, 181.6] -# Base value for the loading length used by the auto-calibration macro -# Please use a value SMALLER than the real reverse bowden length (like 50mm less) -variable_min_bowden_length: 750.0 -# Servo angle for the Up position (i.e. tool disengaged). Refer to the manual to know how to properly tune this value -# Default values: -# MG90S servo : 30 -# SAVOX SH0255MG : 140 -variable_servo_up_angle: 30 -# Servo angle for the Down position (i.e. tool engaged). Refer to the manual to know how to properly tune this value -# Default values: -# MG90S servo : 140 -# SAVOX SH0255MG : 30 -variable_servo_down_angle: 140 -# Threshold for the final load check (i.e. the move from the toolhead sensor to the nozzle) -# Check the _ERCF_GET_LOAD_THRESHOLD command to get the proper value for your setup -variable_final_load_check_threshold: 10.0 -# Options to use or not -# Beware that the clog detection and endless spool mode are in BETA mode for now -# Use at your own risk (beware of the involved macros and the pause(s) and resume ones) -# Put 0 to disable, 1 to enable -variable_clog_detection: 0 -variable_endless_spool_mode: 0 -variable_sensorless_selector: 0 -# The hair puller move is an extruder move that extrudes during an unload, -# right before the long unload move done by the ercf. The purpose is to pull off any -# potential long hair from the filament tip. This small extrusion move is done after -# the ercf has already unloaded the length defined by the unload_move_before_hair_pulling. -# Put 0 to disable, 1 to enable -variable_hair_puller_move: 0 -variable_unload_move_before_hair_pulling: 20.0 - -# ======================================================= -# ============ END OF VALUES TO ADJUST ================== -# ======================================================= -# You shouldn't have to change anything below this point - -# Things that you shouldn't have to change -variable_unload_modifier: 9.0 # Modifier to adjust the ERCF park position (where the filament ends when doing an unload) -variable_min_temp_extruder: 180 # Temp used during the auto-calibration macro, to ensure we can move the extruder (but not really extruding) -variable_extruder_eject_temp: 240 # Temp used during filament ejection (in the ERCF_HOME macro, if a filament is detected in the toolhead) -variable_timeout_pause: 72000 # Time out used by the _ERCF_PAUSE -variable_disable_heater: 600 # Delay after which the hotend heater is disabled in the _ERCF_PAUSE state -variable_gear_stepper_accel: 0 # The acceleration value applied to the gear stepper on moves, standard is to use 0 -variable_extra_servo_dwell_up: 0 # Additional dwell time in ms to apply to dwell prior to turning off the servo -variable_extra_servo_dwell_down: 0 # Additional dwell time in ms to apply to dwell prior to turning off the servo -variable_num_moves: 1 # Nunber of moves to use for load/unload. Increase to 2 or 3 to prevent 'Timer too close' error -gcode: - [save_variables] filename: /home/pi/klipper_config/ercf_vars.cfg -############################### -# ERCF Calibration macros -############################### -[gcode_macro _ERCF_CALIB_SELECTOR] -description: Calibration of the selector position for a defined Tool -gcode: - _ERCF_SERVO_UP - - {% set move_length=(20.0 + (params.TOOL|int + 1)*21.0 + ((params.TOOL|int + 1)/3)*5.0) %} - - M118 Measuring the selector position for tool {params.TOOL} - ERCF_GET_SELECTOR_POS REF={move_length|int} - - _ERCF_MOTORS_OFF - -[gcode_macro _ERCF_CALIBRATE] -description: Complete calibration of all ERCF Tools -gcode: - SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=0 - M118 Start the complete auto calibration... - M118 First home the ERCF - ERCF_HOME - - {% for chan in range(printer["gcode_macro _ERCF_VAR"].colorselector|length) %} - _ERCF_CALIBRATE_SINGLE TOOL={chan|int} - {% endfor %} - - M118 End of the complete auto calibration! - M118 Please reload the firmware for the calibration to be active! - -[gcode_macro _ERCF_CALIBRATE_SINGLE] -description: Calibration of a single ERCF Tool -gcode: - SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=0 - {% if params.TOOL|int >= 0 and params.TOOL|int < printer["gcode_macro _ERCF_VAR"].colorselector|length %} - {% if printer["gcode_macro ERCF_HOME"].home == -1 %} - M118 First home the ERCF - ERCF_HOME - {% endif %} - - _ERCF_SELECT_TOOL TOOL={params.TOOL} - ERCF_SET_STEPS RATIO=1.0 - - {% if params.TOOL|int == 0 %} - {% if printer['extruder'].temperature < printer["gcode_macro _ERCF_VAR"].min_temp_extruder %} - M109 S{printer["gcode_macro _ERCF_VAR"].min_temp_extruder|int} - {% endif %} - - M118 Calibrating reference tool {params.TOOL} - ERCF_LOAD LENGTH={printer["gcode_macro _ERCF_VAR"].min_bowden_length} MOVES={printer["gcode_macro _ERCF_VAR"].num_moves} - ERCF_HOME_EXTRUDER TOTAL_LENGTH={printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float + 400} STEP_LENGTH=0.5 - - _ERCF_CALIB_SAVE_VAR TOOL={params.TOOL} - - G91 - G92 E0 - MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=gear_stepper MOVE=-{printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float} SPEED=25 ACCEL={printer["gcode_macro _ERCF_VAR"].gear_stepper_accel|int} SYNC=0 - G1 E-{printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float} F1500.0 - - _ERCF_CALIB_UNLOAD - ERCF_BUZZ_GEAR_MOTOR - _ERCF_IS_FILAMENT_STUCK_IN_ERCF - - _ERCF_UNSELECT_TOOL - {% else %} - M118 Calibrating tool {params.TOOL} - - ERCF_LOAD LENGTH={printer["gcode_macro _ERCF_VAR"].min_bowden_length|float - 100.0} MOVES={printer["gcode_macro _ERCF_VAR"].num_moves} - _ERCF_CALIB_SAVE_VAR tool={params.TOOL} - - _ERCF_CALIB_UNLOAD LENGTH={printer["gcode_macro _ERCF_VAR"].min_bowden_length|float - 100.0 + 27.0} MOVES={printer["gcode_macro _ERCF_VAR"].num_moves} - {% endif %} - - {% else %} - M118 Tool out of range - {% endif %} - -[gcode_macro _ERCF_CALIB_SAVE_VAR] -description: Saving ERCF calibration values -gcode: - {% if params.TOOL|int == 0 %} - M118 Tool {params.TOOL} calibration value is {printer['ercf'].encoder_pos|float} - SAVE_VARIABLE VARIABLE=ercf_calib_ref VALUE={printer['ercf'].encoder_pos|float} # this is the reference value - SAVE_VARIABLE VARIABLE=ercf_calib_{params.TOOL|int} VALUE=1.0 - SET_GCODE_VARIABLE MACRO=_ERCF_CALIB_UNLOAD VARIABLE=ref VALUE={printer['ercf'].encoder_pos|float} - SET_GCODE_VARIABLE MACRO=_ERCF_CALIB_UNLOAD VARIABLE=ratio VALUE=1.0 - {% else %} - {% set ratio = (printer["gcode_macro _ERCF_VAR"].min_bowden_length|float - 100.0) / printer['ercf'].encoder_pos|float %} - M118 Tool {params.TOOL} ratio is {ratio|float} - SAVE_VARIABLE VARIABLE=ercf_calib_{params.TOOL|int} VALUE={ratio|float} - SET_GCODE_VARIABLE MACRO=_ERCF_CALIB_UNLOAD VARIABLE=ratio VALUE={ratio|float} - {% endif %} - -[gcode_macro _ERCF_CALIB_UNLOAD] -description: Filament unload during ERCF calibration -variable_ratio: 0.0 -variable_ref: 0.0 -gcode: - {% set unload_length = params.LENGTH|default(0.0)|float %} - {% if unload_length|float == 0.0 %} - ERCF_UNLOAD LENGTH={(ref|float)*(ratio|float) - printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float + 27.0} - {% else %} - {% if ratio|float < 1.0 %} # No correction move is done in case of "over targeting", hence the correction ratio is not applied during this unload sequence - ERCF_UNLOAD LENGTH={(unload_length|float)} - {% else %} - ERCF_UNLOAD LENGTH={(unload_length|float)*(ratio|float)} - {% endif %} - {% endif %} - -[gcode_macro _ERCF_GET_LOAD_THRESHOLD] -description: Measure the encoder threshold for the last part of the load -gcode: - _ERCF_CHANGE_TOOL_STANDALONE TOOL=0 TUNE=1 - -[respond] -default_type: command - -############################### -# ERCF servo -############################### -# Push the top hat down (enable the bondtech gears) while "buzzing" the gear motor to ensure proper BMG gear meshing -[gcode_macro _ERCF_SERVO_DOWN] -description: Engage the ERCF gear -gcode: - MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=gear_stepper MOVE=0.5 SPEED=25 ACCEL={printer["gcode_macro _ERCF_VAR"].gear_stepper_accel|int} SYNC=0 - SET_SERVO SERVO=ercf_servo ANGLE={printer["gcode_macro _ERCF_VAR"].servo_down_angle} - G4 P200 - MANUAL_STEPPER STEPPER=gear_stepper MOVE=0.0 SPEED=25 ACCEL={printer["gcode_macro _ERCF_VAR"].gear_stepper_accel|int} SYNC=0 - G4 P100 - MANUAL_STEPPER STEPPER=gear_stepper MOVE=-0.5 SPEED=25 ACCEL={printer["gcode_macro _ERCF_VAR"].gear_stepper_accel|int} SYNC=0 - G4 P{100 + printer["gcode_macro _ERCF_VAR"].extra_servo_dwell_down|int} - MANUAL_STEPPER STEPPER=gear_stepper MOVE=0.0 SPEED=25 ACCEL={printer["gcode_macro _ERCF_VAR"].gear_stepper_accel|int} - SET_SERVO SERVO=ercf_servo WIDTH=0.0 - -# Pull the top hat up (disengage the bondtech gears) -[gcode_macro _ERCF_SERVO_UP] -description: Disengage the ERCF gear -gcode: - SET_SERVO SERVO=ercf_servo ANGLE={printer["gcode_macro _ERCF_VAR"].servo_up_angle} - G4 P{250 + printer["gcode_macro _ERCF_VAR"].extra_servo_dwell_up|int} - SET_SERVO SERVO=ercf_servo WIDTH=0.0 - -############################### -# ERCF motors -############################### -[gcode_macro _ERCF_MOTORS_OFF] -description: Turn off both ERCF motors -gcode: - MANUAL_STEPPER STEPPER=gear_stepper ENABLE=0 - MANUAL_STEPPER STEPPER=selector_stepper ENABLE=0 - SET_GCODE_VARIABLE MACRO=ERCF_HOME VARIABLE=home VALUE=-1 - -############################### -# PAUSE MACROS -# _ERCF_PAUSE is called when an human intervention is needed -# use ERCF_UNLOCK to start the manual intervention -# and use RESUME when the intervention is over to resume the current print -############################### - -# Stop the delayed stop of the heater -[gcode_macro ERCF_UNLOCK] -description: Unlock ERCF operations -gcode: - M118 Unlock the ERCF - SET_GCODE_VARIABLE MACRO=_ERCF_PAUSE VARIABLE=is_paused VALUE=0 - UPDATE_DELAYED_GCODE ID=disable_heater DURATION=0 - {% if printer['filament_switch_sensor toolhead_sensor'].filament_detected == False %} - _ERCF_UNSELECT_TOOL - {% else %} - _ERCF_UNSELECT_TOOL FORCED=0 - {% endif %} - M104 S{printer["gcode_macro _ERCF_PAUSE"].extruder_temp} - RESTORE_GCODE_STATE NAME=ERCF_state - M118 Refer to the manual before resuming the print - -[delayed_gcode disable_heater] -gcode: - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int != 0 %} - M118 Disable extruder heater - M104 S0 - {% endif %} - -# Pause the ERCF, park the extruder at the parking position -# Save the current state and start the delayed stop of the heated -# modify the timeout of the printer accordingly to timeout_pause -[gcode_macro _ERCF_PAUSE] -description: Pause the current print and lock the ERCF operations -variable_is_paused: 0 -variable_extruder_temp: 0 -gcode: - SET_GCODE_VARIABLE MACRO=_ERCF_PAUSE VARIABLE=extruder_temp VALUE={printer.extruder.target} - SET_GCODE_VARIABLE MACRO=_ERCF_PAUSE VARIABLE=is_paused VALUE=1 - SET_IDLE_TIMEOUT TIMEOUT={printer["gcode_macro _ERCF_VAR"].timeout_pause} - UPDATE_DELAYED_GCODE ID=disable_heater DURATION={printer["gcode_macro _ERCF_VAR"].disable_heater} - M118 An issue with the ERCF has been detected and the ERCF has been PAUSED - M118 When you intervene to fix the issue, first call the "ERCF_UNLOCK" Gcode - M118 Refer to the manual before resuming the print - SAVE_GCODE_STATE NAME=ERCF_state - SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=0 - PAUSE - ############################################ # Changing tool macros -# _ERCF_CHANGE_TOOL_STANDALONE TOOL=XX to change filament outside of a print -# _ERCF_CHANGE_TOOL_SLICER TOOL=XX will be called automatically (from the ACTIVATE_EXTRUDER gcode from SuperSlicer), don't use that for "manual" filament swap # if the new extruder is different from the current extruder : # eject the filament if needed # load the new one ########################################### [gcode_macro T0] gcode: - _ERCF_CHANGE_TOOL TOOL=0 + ERCF_CHANGE_TOOL TOOL=0 [gcode_macro T1] gcode: - _ERCF_CHANGE_TOOL TOOL=1 + ERCF_CHANGE_TOOL TOOL=1 [gcode_macro T2] gcode: - _ERCF_CHANGE_TOOL TOOL=2 + ERCF_CHANGE_TOOL TOOL=2 [gcode_macro T3] gcode: - _ERCF_CHANGE_TOOL TOOL=3 + ERCF_CHANGE_TOOL TOOL=3 [gcode_macro T4] gcode: - _ERCF_CHANGE_TOOL TOOL=4 + ERCF_CHANGE_TOOL TOOL=4 [gcode_macro T5] gcode: - _ERCF_CHANGE_TOOL TOOL=5 + ERCF_CHANGE_TOOL TOOL=5 [gcode_macro T6] gcode: - _ERCF_CHANGE_TOOL TOOL=6 + ERCF_CHANGE_TOOL TOOL=6 [gcode_macro T7] gcode: - _ERCF_CHANGE_TOOL TOOL=7 + ERCF_CHANGE_TOOL TOOL=7 [gcode_macro T8] gcode: - _ERCF_CHANGE_TOOL TOOL=8 - -[gcode_macro _ERCF_CHANGE_TOOL] -description: Perform a tool swap -gcode: - {% set initial_tool_string = 'unknown tool' %} - {% if printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|int != -1 %} - {% set initial_tool_string = 'T' + printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|string %} - {% endif %} - M118 Tool swap request, from {initial_tool_string} to T{params.TOOL|int} - M117 {initial_tool_string} -> T{params.TOOL|int} - {% if printer.idle_timeout.state == "Printing" %} - _ERCF_CHANGE_TOOL_SLICER TOOL={params.TOOL|int} - {% else %} - _ERCF_CHANGE_TOOL_STANDALONE TOOL={params.TOOL|int} - {% endif %} - -[gcode_macro _ERCF_CHANGE_TOOL_SLICER] -description: Perform a tool swap during a print -gcode: - SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=0 - {% if printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|int != params.TOOL|int %} - # Add a variable 'SwapCounter' in your PRINT_START macro to display this info - # {% set newcounter = (printer["gcode_macro PRINT_START"].swapcounter|int + 1) %} - # SET_GCODE_VARIABLE MACRO=PRINT_START VARIABLE=swapcounter VALUE={newcounter} - # M118 Swap {newcounter|int} - _ERCF_UNLOAD_TOOL - _ERCF_LOAD_TOOL TOOL={params.TOOL|int} - {% endif %} - _ERCF_CHANGE_TOOL_SLICER_END - -[gcode_macro _ERCF_CHANGE_TOOL_SLICER_END] -gcode: - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int == 0 %} - {% if printer["gcode_macro _ERCF_VAR"].clog_detection|int == 1 %} - SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=1 - {% endif %} - {% endif %} - -[gcode_macro _ERCF_CHANGE_TOOL_STANDALONE] -description: Perform a tool swap out of a print -gcode: - SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=0 - {% set tune = params.TUNE|default(0)|int %} - {% if printer["gcode_macro ERCF_HOME"].home == -1 %} - M118 ERCF not homed, homing it... - ERCF_HOME - _ERCF_LOAD_TOOL TOOL={params.TOOL|int} TUNE={tune} - {% elif printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|int != params.TOOL|int %} - {% if printer['filament_switch_sensor toolhead_sensor'].filament_detected == True %} - M118 Unloading current filament - {% if printer['extruder'].temperature < 178 %} - M118 Preheat Nozzle - M109 S{printer["gcode_macro _ERCF_VAR"].extruder_eject_temp} - {% endif %} - ERCF_EJECT - {% endif %} - - _ERCF_LOAD_TOOL TOOL={params.TOOL|int} TUNE={tune} - {% endif %} - -############################################ -# Unloading/Loading Macros -############################################ - -# Load filament from ERCF to nozzle -[gcode_macro _ERCF_LOAD_TOOL] -description: Load the filament from the ERCF to the toolhead -gcode: - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int == 0 %} - {% if printer["gcode_macro ERCF_HOME"].home == -1 %} - M118 ERCF not homed, homing it... - ERCF_HOME - {% endif %} - {% set tune = params.TUNE|default(0)|int %} - M118 Loading tool {params.TOOL|int} ... - _ERCF_SELECT_TOOL TOOL={params.TOOL|int} - {% set ercf_params = printer.save_variables.variables %} - ERCF_SET_STEPS RATIO={ercf_params['ercf_calib_%s' % (params.TOOL|string)]} - M118 Loading filament from ERCF to extruder ... - {% set ercf_params = printer.save_variables.variables %} - ERCF_LOAD LENGTH={ercf_params.ercf_calib_ref|float - printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float} - _ERCF_LOAD_FILAMENT_IN_EXTRUDER TUNE={tune} - {% else %} - M118 ERCF is currently paused. Please use ERCF_UNLOCK - {% endif %} - -# Unload filament from nozzle to ERCF, using built-in tip forming macro -[gcode_macro ERCF_EJECT] -description: Eject the filament out of a print and park it into the ERCF -gcode: - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int == 0 %} - {% if printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|int != -1 %} - M118 Unloading tool {printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|int} ... - {% if printer['filament_switch_sensor toolhead_sensor'].filament_detected == True %} - _ERCF_UNLOAD_FILAMENT_IN_EXTRUDER_WITH_TIP_FORMING - {% set ercf_params = printer.save_variables.variables %} - ERCF_SET_STEPS RATIO={ercf_params['ercf_calib_%s' % (printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|string)]} - - ERCF_UNLOAD LENGTH={ercf_params.ercf_calib_ref|float - printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float + printer["gcode_macro _ERCF_VAR"].unload_modifier|float} - _ERCF_UNSELECT_TOOL - {% else %} - _ERCF_EJECT_UNKNOW_STATE - {% endif %} - {% else %} - _ERCF_EJECT_UNKNOW_STATE - {% endif %} - {% else %} - M118 ERCF is currently paused. Please use ERCF_UNLOCK - {% endif %} - -# Unload filament from nozzle to ERCF, using SuperSlicer ramming -[gcode_macro _ERCF_UNLOAD_TOOL] -description: Eject the filament during a print and park it into the ERCF -gcode: - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int == 0 %} - {% if printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|int != -1 %} - M118 Unload tool {printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|int} ... - G1 E-10.00 F1200.0 - G1 E-{printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float + 20.0} F2000 - _ERCF_SELECT_TOOL TOOL={printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|int} - {% set ercf_params = printer.save_variables.variables %} - ERCF_SET_STEPS RATIO={ercf_params['ercf_calib_%s' % (printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|string)]} - G4 P100 - M400 - _ERCF_EXTRACT_FROM_EXTRUDER - {% set ercf_params = printer.save_variables.variables %} - ERCF_UNLOAD LENGTH={ercf_params.ercf_calib_ref|float + printer["gcode_macro _ERCF_VAR"].unload_modifier|float - 60.0} - _ERCF_UNSELECT_TOOL - {% endif %} - {% else %} - M118 ERCF is currently paused. Please use ERCF_UNLOCK - {% endif %} - -# Unload filament from nozzle to ERCF, using SuperSlicer ramming -[gcode_macro _ERCF_EXTRACT_FROM_EXTRUDER] -description: Extract the tip at the parking position from the extruder -gcode: - {% if printer['filament_switch_sensor toolhead_sensor'].filament_detected == True %} - M118 Filament still below the extruder... Trying extraction again... - _ERCF_SERVO_UP - G1 E-5.00 F1200.0 - G1 E-{printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float + 25.0} F2000 - _ERCF_SERVO_DOWN - G4 P100 - M400 - _ERCF_RETRY_EXTRACT_FROM_EXTRUDER - {% else %} - _ERCF_HAIR_PULLING_MOVE - {% endif %} - -# Unload filament from nozzle to ERCF, using SuperSlicer ramming -[gcode_macro _ERCF_RETRY_EXTRACT_FROM_EXTRUDER] -description: Check if extraction retry went properly or not -gcode: - {% if printer['filament_switch_sensor toolhead_sensor'].filament_detected == True %} - M118 Filament is stuck below the extruder... - M118 Calling ERCF_PAUSE - _ERCF_PAUSE - {% else %} - _ERCF_HAIR_PULLING_MOVE - {% endif %} - -[gcode_macro _ERCF_HAIR_PULLING_MOVE] -description: Optionnal hair pulling move before the long unload -gcode: - G91 - G92 E0 - {% if printer["gcode_macro _ERCF_VAR"].hair_puller_move|int == 1 %} - MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=gear_stepper MOVE=-{printer["gcode_macro _ERCF_VAR"].unload_move_before_hair_pulling|float} SPEED=25 ACCEL=0 - MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=gear_stepper MOVE=-10 SPEED=25 ACCEL=0 SYNC=0 - G1 E10 F2000.0 - {% else %} - MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=gear_stepper MOVE=-30 SPEED=25 ACCEL=0 - {% endif %} - - -############################################ -# Select/Unselect a tool -# move the selector (if needed) to the requested tool -############################################ - -# Select a tool. move the idler and then move the color selector (if needed) -[gcode_macro _ERCF_SELECT_TOOL] -description: Move the selector to the Tool and select it -variable_tool_selected: -1 -variable_color_selected: -1 -gcode: - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int == 0 %} - {% if printer["gcode_macro ERCF_HOME"].home != -1 %} - M118 Select Tool {params.TOOL} ... - _ERCF_SERVO_UP - {% if printer["gcode_macro _ERCF_VAR"].sensorless_selector|int == 1 %} - ERCF_MOVE_SELECTOR TARGET={printer["gcode_macro _ERCF_VAR"].colorselector[params.TOOL|int]} - {% else %} - MANUAL_STEPPER STEPPER=selector_stepper MOVE={printer["gcode_macro _ERCF_VAR"].colorselector[params.TOOL|int]} - {% endif %} - SET_GCODE_VARIABLE MACRO=_ERCF_SELECT_TOOL VARIABLE=tool_selected VALUE={params.TOOL} - SET_GCODE_VARIABLE MACRO=_ERCF_SELECT_TOOL VARIABLE=color_selected VALUE={params.TOOL} - _ERCF_SERVO_DOWN - M118 Tool {params.TOOL} Enabled - {% else %} - M118 Could not select tool, ERCF is not homed - {% endif %} - {% else %} - M118 ERCF is currently paused. Please use ERCF_UNLOCK - {% endif %} - -# Unselect a tool -[gcode_macro _ERCF_UNSELECT_TOOL] -description: Unselect current Tool -gcode: - {% set unselect_color = params.FORCED|default(1)|int %} - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int == 0 %} - {% if printer["gcode_macro ERCF_HOME"].home != -1 %} - _ERCF_SERVO_UP - SET_GCODE_VARIABLE MACRO=_ERCF_SELECT_TOOL VARIABLE=tool_selected VALUE=-1 - {% if unselect_color == 1 %} - SET_GCODE_VARIABLE MACRO=_ERCF_SELECT_TOOL VARIABLE=color_selected VALUE=-1 - {% endif %} - ERCF_SET_STEPS RATIO=1.0 - {% else %} - M118 Could not unselect tool, ERCF is not homed - {% endif %} - {% else %} - M118 ERCF is currently paused. Please use ERCF_UNLOCK - {% endif %} + ERCF_CHANGE_TOOL TOOL=8 ############################################ # Loading/Unloading part FROM/TO EXTRUDER TO/FROM NOZZLE ############################################ -# Load the filament into the extruder -# Call _ERCF_PAUSE if the filament is not detected by the toolhead sensor -[gcode_macro _ERCF_LOAD_FILAMENT_IN_EXTRUDER] -description: Load filament from the toolhead entrance to the nozzle -gcode: - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int == 0 %} - {% set tune = params.TUNE|default(0)|int %} - {% if printer.extruder.temperature > printer["gcode_macro _ERCF_VAR"].min_temp_extruder %} - M118 Loading Filament... - G91 - G92 E0 - MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=gear_stepper MOVE={printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float - 7} SPEED=25 ACCEL={printer["gcode_macro _ERCF_VAR"].gear_stepper_accel|int} SYNC=0 - G1 E{printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float - 7} F1500.0 - G4 P100 - M400 - ERCF_HOME_EXTRUDER TOTAL_LENGTH=30.0 STEP_LENGTH=0.5 - _ERCF_UNSELECT_TOOL FORCED=0 - ERCF_FINALIZE_LOAD LENGTH={printer["gcode_macro _ERCF_VAR"].sensor_to_nozzle|float} THRESHOLD={printer["gcode_macro _ERCF_VAR"].final_load_check_threshold|float} TUNE={tune} - G92 E0 - G90 - {% else %} - M118 Extruder too cold - _ERCF_PAUSE - {% endif %} - {% else %} - M118 ERCF is currently paused. Please use ERCF_UNLOCK - {% endif %} - # StandAlone cooling moves to extract proper filament tip [gcode_macro _ERCF_FORM_TIP_STANDALONE] description: Generic tip forming macro @@ -701,292 +146,18 @@ gcode: {% endif %} G92 E0 - -# Unload from extruder with tip forming sequence -[gcode_macro _ERCF_UNLOAD_FILAMENT_IN_EXTRUDER_WITH_TIP_FORMING] -description: Unload filament from the nozzle to the toolhead entrance using generic tip forming macro -gcode: - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int == 0 %} - {% if printer.extruder.temperature > printer["gcode_macro _ERCF_VAR"].min_temp_extruder %} - {% if printer["gcode_macro _ERCF_SELECT_TOOL"].tool_selected|int == -1 %} - M118 Forming filament tip and Unloading Filament... - G91 - _ERCF_FORM_TIP_STANDALONE - G1 E-10.00 F1200.0 - G1 E-{printer["gcode_macro _ERCF_VAR"].sensor_to_nozzle|float - 10.00} F2000 - G1 E-{printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float + 20.00} F2000 - _ERCF_SERVO_DOWN - G4 P100 - _ERCF_EXTRACT_FROM_EXTRUDER - M118 Filament removed - {% else %} - M118 Tool selected, UNSELECT it - _ERCF_PAUSE - {% endif %} - {% else %} - M118 Extruder too cold - _ERCF_PAUSE - {% endif %} - {% else %} - M118 ERCF is currently paused. Please use ERCF_UNLOCK - {% endif %} - -############################################ -# Endstop check macros -############################################ -# Call _ERCF_PAUSE if the filament is stuck in the ERCF -[gcode_macro _ERCF_IS_FILAMENT_STUCK_IN_ERCF] -gcode: - {% if printer.ercf.encoder_pos|float != 0 %} - M118 Filament stuck in ERCF - _ERCF_PAUSE - {% else %} - M118 Filament not in ERCF - {% endif %} - -############################################ -# Macros called during homing to try to eject the filament if loaded -############################################ - -# Eject from extruder gear to the ERCF -[gcode_macro _ERCF_EJECT_UNKNOW_STATE] -description: Unload filament from an unknown position -gcode: - M118 Eject Filament if loaded ... - {% if printer['filament_switch_sensor toolhead_sensor'].filament_detected == True %} - M118 Filament in extruder, trying to eject it .. - {% if printer['extruder'].temperature < 178 %} - M118 Preheat Nozzle - M109 S{printer["gcode_macro _ERCF_VAR"].extruder_eject_temp} - {% endif %} - _ERCF_UNLOAD_FILAMENT_IN_EXTRUDER_WITH_TIP_FORMING - ERCF_UNLOAD LENGTH={printer["gcode_macro _ERCF_VAR"].min_bowden_length - 50} UNKNOWN=1 MOVES={printer["gcode_macro _ERCF_VAR"].num_moves} - {% else %} - _ERCF_SERVO_DOWN - ERCF_BUZZ_GEAR_MOTOR - _ERCF_EJECT_FROM_BOWDEN - {% endif %} - -# Eject from the bowden to the ERCF -[gcode_macro _ERCF_EJECT_FROM_BOWDEN] -description: Unload filament from the reverse bowden -gcode: - {% if printer.ercf.encoder_pos|float != 0 %} - ERCF_UNLOAD HOMING=1 - M118 Filament ejected ! - {% else %} - M118 Filament already ejected ! - {% endif %} - _ERCF_SERVO_UP - -############################################ -# Homing macros -# ERCF_HOME must be called before using the ERCF -############################################ - -# Home the ERCF -# eject filament if loaded with _ERCF_EJECT_UNKNOW_STATE -# next home the ERCF with _ERCF_HOME_ONLY -[gcode_macro ERCF_HOME] -description: Home the ERCF -variable_home: -1 -gcode: - SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=0 - SET_GCODE_VARIABLE MACRO=ERCF_HOME VARIABLE=home VALUE=1 - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int == 1 %} - ERCF_UNLOCK - {% endif %} - M118 Homing ERCF ... - QUERY_ENDSTOPS - _ERCF_EJECT_UNKNOW_STATE - _ERCF_HOME_SELECTOR - _ERCF_HOME_ONLY TOOL={params.TOOL|default(0)|int} - -[gcode_macro _ERCF_HOME_SELECTOR] -description: Home the ERCF selector -gcode: - M118 Homing selector - _ERCF_UNSELECT_TOOL - - {% set number_of_chan=printer["gcode_macro _ERCF_VAR"].colorselector|length %} - {% set selector_length=(20.0 + number_of_chan*21.0 + (number_of_chan/3)*5.0) %} - - {% if printer["gcode_macro _ERCF_VAR"].sensorless_selector|int == 1 %} - MANUAL_STEPPER STEPPER=selector_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=selector_stepper SPEED=60 MOVE=-{selector_length|float} STOP_ON_ENDSTOP=1 - MANUAL_STEPPER STEPPER=selector_stepper SET_POSITION=0 - {% else %} - MANUAL_STEPPER STEPPER=selector_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=selector_stepper SPEED=100 MOVE=-{selector_length|float} STOP_ON_ENDSTOP=1 - MANUAL_STEPPER STEPPER=selector_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=selector_stepper SPEED=100 MOVE=5.0 - MANUAL_STEPPER STEPPER=selector_stepper SPEED=10 MOVE=-10.0 STOP_ON_ENDSTOP=1 - MANUAL_STEPPER STEPPER=selector_stepper SET_POSITION=0 - {% endif %} - QUERY_ENDSTOPS - M400 - MANUAL_STEPPER STEPPER=selector_stepper SPEED=50 MOVE=3.0 - M400 - -# Home the ERCF (home the color selector if needed) -# if everything is ok, the ERCF is ready to be used -[gcode_macro _ERCF_HOME_ONLY] -gcode: - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int == 0 %} - {% set selector_homed = 0 %} - {% if printer["gcode_macro _ERCF_VAR"].sensorless_selector|int == 1 %} - {% if printer.query_endstops.last_query["manual_stepper gear_stepper"] == 1 %} - {% set selector_homed = 1 %} - {% endif %} - {% else %} - {% if printer.query_endstops.last_query["manual_stepper selector_stepper"] == 1 %} - {% set selector_homed = 1 %} - {% endif %} - {% endif %} - {% if selector_homed != 1 %} - M118 Homing ERCF selector failed, check what is blocking the selector - M118 Pausing the ERCF, run "ERCF_UNLOCK" to unlock it ... - _ERCF_PAUSE - {% else %} - M118 Homing ERCF ended ... - {% endif %} - {% else %} - M118 Homing ERCF failed, ERCF is paused, run "ERCF_UNLOCK" to unlock it ... - {% endif %} - -############################################### -# Test Macros -############################################### -[gcode_macro _ERCF_DISPLAY_ENCODER_POS] -description: Display current value of the ERCF encoder -gcode: - M118 Encoder value is {printer['ercf'].encoder_pos|float} - -[gcode_macro _ERCF_TEST_MOVE_GEAR] -description: Move the ERCF gear -gcode: - {% set move_length = params.LENGTH|default(200.0)|float %} - {% set move_speed = params.SPEED|default(50.0)|float %} - {% set move_accel = params.ACCEL|default(200.0)|float %} - MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=gear_stepper MOVE={move_length|float} SPEED={move_speed|float} ACCEL={move_accel|float} - -[gcode_macro _ERCF_TEST_SERVO] -description: Test the servo angle -gcode: - SET_SERVO SERVO=ercf_servo ANGLE={params.VALUE|float} - G4 P{250 + printer["gcode_macro _ERCF_VAR"].extra_servo_dwell_up|int} - SET_SERVO SERVO=ercf_servo WIDTH=0.0 - -[gcode_macro _ERCF_TEST_GRIP] -description: Test the ERCF grip for a Tool -gcode: - _ERCF_SERVO_DOWN - _ERCF_MOTORS_OFF - -[gcode_macro _ERCF_TEST_LOAD_SEQUENCE] -description: Test sequence -gcode: - {% set loop_number = params.LOOP|default(10)|int %} - {% set use_rand = params.RAND|default(0)|int %} - {% for iteration in range(loop_number|int) %} - {% for load in range((printer["gcode_macro _ERCF_VAR"].colorselector|length)|int) %} - {% if use_rand|int == 1 %} - _ERCF_SELECT_TOOL TOOL={range(0, printer["gcode_macro _ERCF_VAR"].colorselector|length)|random} - {% else %} - _ERCF_SELECT_TOOL TOOL={load|int} - {% endif %} - - ERCF_LOAD LENGTH=100 - G4 P50 - ERCF_UNLOAD LENGTH=100 - _ERCF_UNSELECT_TOOL - G4 P200 - {% endfor %} - {% endfor %} + G90 ############################################### # Endless spool mode and clog detection ############################################### - -[gcode_macro _ERCF_ENCODER_MOTION_ISSUE] -description: Perform a test when the encoder sense an issue (clog or runout) +[gcode_macro _ERCF_ENDLESS_SPOOL_PRE_UNLOAD] +description: Pre unload routine for EndlessSpool changes - modify for your setup gcode: - M118 Issue on tool {printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|int} - M118 Checking if this is a clog or a runout... + # BRUSH_CLEAN - SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=0 - - _ERCF_SERVO_DOWN - ERCF_BUZZ_GEAR_MOTOR - _ERCF_SERVO_UP - _ERCF_CLOG_OR_RUNOUT - -[gcode_macro _ERCF_CLOG_OR_RUNOUT] -description: Actions taken if a clog or a runout is detected by the ERCF encoder +[gcode_macro _ERCF_ENDLESS_SPOOL_POST_LOAD] +description: Post load routine for EndlessSpool changes - modify for your setup gcode: - {% if printer.ercf.encoder_pos|float != 0 %} - M118 Clog detected, please check the ERCF and the printer - {% else %} - M118 Runout detected ! - {% if printer["gcode_macro _ERCF_VAR"].endless_spool_mode|int == 1 %} - {% set pre_change_PA = printer.extruder.pressure_advance %} - M118 EndlessSpool mode is ON! - {% if printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|int >= (printer["gcode_macro _ERCF_VAR"].colorselector|length -1) %} - {% set nexttool = 0 %} - {% else %} - {% set nexttool = (printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|int + 1) %} - {% endif %} - - M118 Loading tool {nexttool|int} - SAVE_GCODE_STATE NAME=ERCF_Pre_Brush_init - # Adapt the example below to your own setup - # The goal is just to clean the nozzle after the change - # In my case I have a purge bucket with a brush - # G0 X45 Y300 F18000 - # G0 X45 Y310 Z1 F3000 - - # Custom unload sequence - M118 Unloading Filament... - G91 - _ERCF_FORM_TIP_STANDALONE - ERCF_HOME_EXTRUDER TOTAL_LENGTH={printer["gcode_macro _ERCF_VAR"].sensor_to_nozzle} STEP_LENGTH=2.0 - _ERCF_SERVO_DOWN - - G91 - G92 E0 - MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=gear_stepper MOVE=-{printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float} SPEED=25 ACCEL={printer["gcode_macro _ERCF_VAR"].gear_stepper_accel|int} SYNC=0 - G1 E-{printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float} F1500.0 - - {% set ercf_params = printer.save_variables.variables %} - ERCF_SET_STEPS RATIO={ercf_params['ercf_calib_%s' % (printer["gcode_macro _ERCF_SELECT_TOOL"].color_selected|string)]} - ERCF_UNLOAD LENGTH={ercf_params.ercf_calib_ref|float - printer["gcode_macro _ERCF_VAR"].end_of_bowden_to_sensor|float + printer["gcode_macro _ERCF_VAR"].unload_modifier|float} - _ERCF_UNSELECT_TOOL - - _ERCF_LOAD_TOOL TOOL={nexttool|int} - SET_PRESSURE_ADVANCE ADVANCE={pre_change_PA} - _ERCF_CHECK_IF_RESUME - {% else %} - M118 EndlessSpool mode not enabled, please do something - {% endif %} - {% endif %} - -[gcode_macro _ERCF_CHECK_IF_RESUME] -description: Safety checks before resuming the print after an encoder event -gcode: - {% if printer["gcode_macro _ERCF_PAUSE"].is_paused|int == 0 %} - # Adapt the example below to your own setup - # The goal is just to clean the nozzle after the change - # In my case I have a purge bucket with a brush - # BRUSH_PURGE LENGTH=50 - # BRUSH_CLEAN - RESTORE_GCODE_STATE NAME=ERCF_Pre_Brush_init - RESUME - {% if printer["gcode_macro _ERCF_VAR"].clog_detection|int == 1 %} - SET_FILAMENT_SENSOR SENSOR=encoder_sensor ENABLE=1 - {% endif %} - {% else %} - M118 ERCF is currently paused. Please use ERCF_UNLOCK - {% endif %} - + # BRUSH_PURGE LENGTH=50 + # BRUSH_CLEAN diff --git a/Filament_Sensor/CAD/AfterBurner_Clockwork_ERCF_Sensor.step b/Legacy_Filament_Sensor/CAD/AfterBurner_Clockwork_ERCF_Sensor.step similarity index 100% rename from Filament_Sensor/CAD/AfterBurner_Clockwork_ERCF_Sensor.step rename to Legacy_Filament_Sensor/CAD/AfterBurner_Clockwork_ERCF_Sensor.step diff --git a/Filament_Sensor/CAD/Galileo_Clockwork_ERCF_Sensor.step b/Legacy_Filament_Sensor/CAD/Galileo_Clockwork_ERCF_Sensor.step similarity index 100% rename from Filament_Sensor/CAD/Galileo_Clockwork_ERCF_Sensor.step rename to Legacy_Filament_Sensor/CAD/Galileo_Clockwork_ERCF_Sensor.step diff --git a/Filament_Sensor/CAD/LGX_on_AfterBurner_ERCF_Sensor.step b/Legacy_Filament_Sensor/CAD/LGX_on_AfterBurner_ERCF_Sensor.step similarity index 100% rename from Filament_Sensor/CAD/LGX_on_AfterBurner_ERCF_Sensor.step rename to Legacy_Filament_Sensor/CAD/LGX_on_AfterBurner_ERCF_Sensor.step diff --git a/Filament_Sensor/README.md b/Legacy_Filament_Sensor/README.md similarity index 100% rename from Filament_Sensor/README.md rename to Legacy_Filament_Sensor/README.md diff --git a/Filament_Sensor/Stls/AB/AB_ERCF_Sensor_Extruder_Body.stl b/Legacy_Filament_Sensor/Stls/AB/AB_ERCF_Sensor_Extruder_Body.stl similarity index 100% rename from Filament_Sensor/Stls/AB/AB_ERCF_Sensor_Extruder_Body.stl rename to Legacy_Filament_Sensor/Stls/AB/AB_ERCF_Sensor_Extruder_Body.stl diff --git a/Filament_Sensor/Stls/AB/[a]_AB_ERCF_Sensor_Cable_Cover.stl b/Legacy_Filament_Sensor/Stls/AB/[a]_AB_ERCF_Sensor_Cable_Cover.stl similarity index 100% rename from Filament_Sensor/Stls/AB/[a]_AB_ERCF_Sensor_Cable_Cover.stl rename to Legacy_Filament_Sensor/Stls/AB/[a]_AB_ERCF_Sensor_Cable_Cover.stl diff --git a/Filament_Sensor/Stls/AB/[a]_AB_ERCF_Sensor_Latch.stl b/Legacy_Filament_Sensor/Stls/AB/[a]_AB_ERCF_Sensor_Latch.stl similarity index 100% rename from Filament_Sensor/Stls/AB/[a]_AB_ERCF_Sensor_Latch.stl rename to Legacy_Filament_Sensor/Stls/AB/[a]_AB_ERCF_Sensor_Latch.stl diff --git a/Filament_Sensor/Stls/Galileo/Galileo_ERCF_Sensor_Main_Body.stl b/Legacy_Filament_Sensor/Stls/Galileo/Galileo_ERCF_Sensor_Main_Body.stl similarity index 100% rename from Filament_Sensor/Stls/Galileo/Galileo_ERCF_Sensor_Main_Body.stl rename to Legacy_Filament_Sensor/Stls/Galileo/Galileo_ERCF_Sensor_Main_Body.stl diff --git a/Filament_Sensor/Stls/Galileo/Galileo_JST_XH_Tool.stl b/Legacy_Filament_Sensor/Stls/Galileo/Galileo_JST_XH_Tool.stl similarity index 100% rename from Filament_Sensor/Stls/Galileo/Galileo_JST_XH_Tool.stl rename to Legacy_Filament_Sensor/Stls/Galileo/Galileo_JST_XH_Tool.stl diff --git a/Filament_Sensor/Stls/LGX/LGX_on_AfterBurner_Adapter_ERCF_Sensor.stl b/Legacy_Filament_Sensor/Stls/LGX/LGX_on_AfterBurner_Adapter_ERCF_Sensor.stl similarity index 100% rename from Filament_Sensor/Stls/LGX/LGX_on_AfterBurner_Adapter_ERCF_Sensor.stl rename to Legacy_Filament_Sensor/Stls/LGX/LGX_on_AfterBurner_Adapter_ERCF_Sensor.stl diff --git a/Toolhead/Stls/CW2/CW2_Main_Body_ERCF.stl b/Toolhead/Stls/CW2/CW2_Main_Body_ERCF.stl new file mode 100644 index 00000000..8b3b0089 Binary files /dev/null and b/Toolhead/Stls/CW2/CW2_Main_Body_ERCF.stl differ diff --git a/Toolhead/Stls/CW2/[a]_CW2_Latch_ERCF.stl b/Toolhead/Stls/CW2/[a]_CW2_Latch_ERCF.stl new file mode 100644 index 00000000..1916b66c Binary files /dev/null and b/Toolhead/Stls/CW2/[a]_CW2_Latch_ERCF.stl differ