From d9ea2564bc8bdc81b4ea7d2ec935eddbf1188a93 Mon Sep 17 00:00:00 2001 From: Peter Gruber Date: Fri, 12 Feb 2021 11:46:06 +0100 Subject: [PATCH 01/17] extruder: Add mixing extruder (with example usage in a printer.cfg) Supported G-codes are: M163 Sx Pa:b:..:h, M164 Sx - set ratios for virtual mixing extruders M567 Px Ea:b:..:h - set mixing for virtual mixing extruders G1 Ea:b:..:h - extrude with mixing (allowed when mixingextruder is active) ACTIVATE_EXTRUDER EXTRUDER=mixingextruder MIXING_STATUS EXTRUDER=mixingextruder Signed-off-by: Peter Gruber --- config/printer-geeetech-301.cfg | 158 +++++++++++++++++++++ klippy/extras/mixingextruder.py | 238 ++++++++++++++++++++++++++++++++ 2 files changed, 396 insertions(+) create mode 100644 config/printer-geeetech-301.cfg create mode 100644 klippy/extras/mixingextruder.py diff --git a/config/printer-geeetech-301.cfg b/config/printer-geeetech-301.cfg new file mode 100644 index 000000000000..a8f6aa239d33 --- /dev/null +++ b/config/printer-geeetech-301.cfg @@ -0,0 +1,158 @@ +# This file contains common pin mappings for the GTM32 PRO board in the Geeetech 301 printer +# To use this config, the firmware should be compiled for the +# STM32F103 with "No bootloader" and with "Use USB for communication" +# disabled. + +# The "make flash" command does not work on the GTM32 PRO. Instead, +# after running "make", run the following command to flash the board: +# stm32flash -w out/klipper.bin -v -i rts,-dtr,dtr /dev/ttyUSB0 + +# See docs/Config_Reference.md for a description of parameters. + +[board_pins gtm32_pro_vb] +aliases_probes: X_MIN=PE5,X_MAX=PE4,Y_MIN=PE3,Y_MAX=PE2,Z_MIN=PE1,Z_MAX=PE0 +aliases_temp: E0_TEMP=PC0,T0_PWM=PB4,E1_TEMP=PC1,T1_PWM=PB5,E2_TEMP=PC2,T2_PWM=PB0,BED_TEMP=PC3,BED_PWM=PB1 +aliases_other: FAN0_PWM=PB7,FAN1_PWM=PB8,FAN2_PWM=PB9,BEEP=PB10,LED_PWM=PD12 +aliases_motors: X_EN=PA8,X_DIR=PD13,X_STEP=PC6,Y_EN=PA15,Y_DIR=PA11,Y_STEP=PA12,Z_EN=PB3,Z_DIR=PD3,Z_STEP=PD6,E0_EN=PC15,E0_DIR=PC13,E0_STEP=PC14,E1_EN=PA1,E1_DIR=PB6,E1_STEP=PA0,E2_EN=PC4,E2_DIR=PB11,E2_STEP=PB2 +aliases_lcd: LCD_ENC1=PE9,LCD_ENC2=PE8,LCD_PUSH=PE13,LCD_BEEP=PE12,LCD_RS=PE6,LCD_E=PE14,LCD_D4=PD8,LCD_D5=PD9,LCD_D6=PD10,LCD_D7=PE15 + +[multi_pin heater] +pins: T0_PWM,T1_PWM,T2_PWM + +[multi_pin printhead_fans] +pins: FAN1_PWM,FAN2_PWM + +[thermistor bed_thermistor] +temperature1: 24 +resistance1: 104600 +temperature2: 40 +resistance2: 47700 +temperature3: 67 +resistance3: 13000 + +[stepper_a] +step_pin: X_STEP +dir_pin: !X_DIR +enable_pin: !X_EN +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^X_MAX +homing_speed: 50 +position_endstop: 216 +arm_length: 201 + +[stepper_b] +step_pin: Y_STEP +dir_pin: !Y_DIR +enable_pin: !Y_EN +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^Y_MAX + +[stepper_c] +step_pin: Z_STEP +dir_pin: !Z_DIR +enable_pin: !Z_EN +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^Z_MAX + +[extruder] +step_pin: E0_STEP +dir_pin: !E0_DIR +enable_pin: !E0_EN +microsteps: 16 +rotation_distance: 32 +nozzle_diameter: 0.4 +filament_diameter: 1.75 +heater_pin: multi_pin:heater +sensor_type: EPCOS 100K B57560G104F +sensor_pin: E2_TEMP +min_extrude_temp: 160 +min_temp: 0 +max_temp: 250 + +[extruder1] +step_pin: E1_STEP +dir_pin: !E1_DIR +enable_pin: !E1_EN +microsteps: 16 +rotation_distance: 32 +nozzle_diameter: 0.4 +filament_diameter: 1.75 +shared_heater: extruder + +[extruder2] +step_pin: E2_STEP +dir_pin: !E2_DIR +enable_pin: !E2_EN +microsteps: 16 +rotation_distance: 32 +nozzle_diameter: 0.4 +filament_diameter: 1.75 +shared_heater: extruder + +[heater_bed] +heater_pin: BED_PWM +sensor_type: bed_thermistor +sensor_pin: BED_TEMP +min_temp: 0 +max_temp: 150 + +[temperature_sensor board] +sensor_type: temperature_mcu +gcode_id: MCU + +[temperature_sensor secondary] +sensor_pin: E1_TEMP +sensor_type: EPCOS 100K B57560G104F +gcode_id: SEC + +[temperature_sensor ambient] +sensor_pin: E0_TEMP +sensor_type: EPCOS 100K B57560G104F +gcode_id: AMB + +[homing_heaters] +heaters: extruder + +[fan] +pin: FAN0_PWM + +[heater_fan printhead] +pin: multi_pin:printhead_fans +heater: extruder +max_power: 0.6 +off_below: 0.2 +shutdown_speed: 0 + +[mixingextruder] +extruders: extruder,extruder1,extruder2 + +[mcu] +serial: /dev/serial/by-id/usb-FTDI_FT232R_USB_UART_AH06IBBC-if00-port0 +restart_method: cheetah + +[printer] +kinematics: delta +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 150 +delta_radius: 94 + +[output_pin beep] +pin: BEEP + +[output_pin lcd_beep] +pin: LCD_BEEP + +[display] +lcd_type: hd44780 +rs_pin: LCD_RS +e_pin: LCD_E +d4_pin: LCD_D4 +d5_pin: LCD_D5 +d6_pin: LCD_D6 +d7_pin: LCD_D7 +encoder_pins: ^LCD_ENC1,^LCD_ENC2 +click_pin: ^LCD_PUSH diff --git a/klippy/extras/mixingextruder.py b/klippy/extras/mixingextruder.py new file mode 100644 index 000000000000..9ebd072ce822 --- /dev/null +++ b/klippy/extras/mixingextruder.py @@ -0,0 +1,238 @@ +# Code for supporting mixing extruders. +# +# Copyright (C) 2021 Peter Gruber +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import math, logging +import stepper, chelper + +from toolhead import Move +from gcode import GCodeCommand + +class MixingMove: + def __init__(self, x, y, z, e, + dist_x, dist_y, dist_z, dist_e, + ratio_x, ratio_y, ratio_z, ratio_e, + distance, acceleration, + velocity_start, velocity_cruise, + time_accel, time_cruise, time_decel): + self.axes_d = (dist_x, dist_y, dist_z, dist_e) + self.axes_r = (ratio_x, ratio_y, ratio_z, ratio_e) + self.move_d = distance + self.accel = acceleration + self.start_v, self.cruise_v = velocity_start, velocity_cruise + self.accel_t, self.cruise_t, self.decel_t = time_accel, time_cruise, time_decel + self.start_pos = (x, y, z, e) + self.end_pos = tuple(sum(s) for s in zip(self.start_pos, self.axes_d)) + +class MixingExtruder: + def __init__(self, config, idx): + self.printer = config.get_printer() + self.activated = False + self.name = config.get_name() if idx == 0 else "%s%d" % (config.get_name(), idx) + self.extruder_names = [e.strip() for e in config.get('extruders', None).split(",")] + if not len(self.extruder_names): + raise self._mcu.get_printer().config_error( + "No extruders configured for mixing") + self.main_extruder = None + self.extruders = [] + self.heater = None + self.mixing_extruders = {} if idx == 0 else self.printer.lookup_object("mixingextruder").mixing_extruders + self.mixing_extruders[idx] = self + self.mixing = [0 if p else 1 for p in range(len(self.extruder_names))] + self.commanded_pos = 0 + self.positions = [0. for p in range(len(self.extruder_names))] if idx == 0 else self.mixing_extruders[0].positions + self.ratios = [0 for p in range(len(self.extruder_names))] + gcode = self.printer.lookup_object('gcode') + logging.info("MixingExtruder extruders=%s", ", ".join(self.extruder_names)) + # Register commands + gcode.register_mux_command("ACTIVATE_EXTRUDER", "EXTRUDER", + self.name, self.cmd_ACTIVATE_EXTRUDER, + desc=self.cmd_ACTIVATE_EXTRUDER_help) + gcode.register_mux_command("MIXING_STATUS", "EXTRUDER", + self.name, self.cmd_MIXING_STATUS, + desc=self.cmd_MIXING_STATUS_help) + def _activate(self): + if self.activated: + return + self.activated = True + if self.mixing_extruders[0] != self: + return + try: + self.extruders = [self.printer.lookup_object(extruder) + for extruder in self.extruder_names] + self.main_extruder = self.extruders[0] + self.heater = self.main_extruder.get_heater() + except Exception as e: + self.extruders = [] + self.heater = None + logging.error("no extruders found: %s" % (", ".join(self.extruder_names)), e) + gcode = self.printer.lookup_object('gcode') + # Register commands + gcode.register_command("M163", self.cmd_M163) + gcode.register_command("M164", self.cmd_M164) + gcode.register_command("M567", self.cmd_M567) + self.orig_G1 = gcode.register_command("G1", None) + gcode.register_command("G1", self.cmd_G1) + def update_move_time(self, flush_time): + for extruder in self.extruders: + extruder.update_move_time(flush_time) + def calc_junction(self, prev_move, move): + diff_r = move.axes_r[3] - prev_move.axes_r[3] + if diff_r: + m = max(self.mixing) + return (self.main_extruder.instant_corner_v / abs(m * diff_r))**2 + return move.max_cruise_v2 + def _scale_move(self, move, idx): + mixing = self.mixing[idx] + if not mixing: + return None + return MixingMove(move.start_pos[0], move.start_pos[1], move.start_pos[2], self.positions[idx], + move.axes_d[0], move.axes_d[1], move.axes_d[2], mixing * move.axes_d[3], + move.axes_r[0], move.axes_r[1], move.axes_r[2], mixing * move.axes_r[3], + move.move_d, move.accel, + move.start_v if hasattr(move, "start_v") else 0., + move.cruise_v if hasattr(move, "cruise_v") else math.sqrt(move.max_cruise_v2), + move.accel_t if hasattr(move, "accel_t") else 0., + move.cruise_t if hasattr(move, "cruise_t") else move.min_move_t, + move.decel_t if hasattr(move, "decel_t") else 0.) + def _check_move(self, scaled_move, move): + axis_r = scaled_move.axes_r[3] + axis_d = scaled_move.axes_d[3] + if not self.heater.can_extrude: + raise self.printer.command_error( + "Extrude below minimum temp\n" + "See the 'min_extrude_temp' config option for details") + if (not move.axes_d[0] and not move.axes_d[1]) or axis_r < 0.: + # Extrude only move (or retraction move) - limit accel and velocity + if abs(axis_d) > self.main_extruder.max_e_dist: + raise self.printer.command_error( + "Extrude only move too long (%.3fmm vs %.3fmm)\n" + "See the 'max_extrude_only_distance' config" + " option for details" % (axis_d, self.main_extruder.max_e_dist)) + inv_extrude_r = 1. / abs(axis_r) + move.limit_speed(self.main_extruder.max_e_velocity * inv_extrude_r, + self.main_extruder.max_e_accel * inv_extrude_r) + elif axis_r > self.main_extruder.max_extrude_ratio: + if axis_d <= self.main_extruder.nozzle_diameter * self.main_extruder.max_extrude_ratio: + # Permit extrusion if amount extruded is tiny + return + area = axis_r * self.main_extruder.filament_area + logging.debug("Overextrude: %s vs %s (area=%.3f dist=%.3f)", + axis_r, self.main_extruder.max_extrude_ratio, area, move.move_d) + raise self.printer.command_error( + "Move exceeds maximum extrusion (%.3fmm^2 vs %.3fmm^2)\n" + "See the 'max_extrude_cross_section' config option for details" + % (area, self.main_extruder.max_extrude_ratio * self.main_extruder.filament_area)) + def check_move(self, move): + for idx, extruder in enumerate(self.extruders): + scaled_move = self._scale_move(move, idx) + if scaled_move: + self._check_move(scaled_move, move) + def move(self, print_time, move): + for idx, extruder in enumerate(self.extruders): + scaled_move = self._scale_move(move, idx) + if scaled_move: + extruder.move(print_time, scaled_move) + self.positions[idx] = scaled_move.end_pos[3] + self.commanded_pos = move.end_pos[3] + def get_status(self, eventtime): + return dict(mixing=",".join("%0.1f%%" % (m * 100.) for m in self.mixing), + positions=",".join("%0.2fmm" % (m) for m in self.positions), + ticks=",".join("%0.2f" % (extruder.stepper.get_mcu_position()) for extruder in self.extruders), + extruders=",".join(extruder.name for extruder in self.extruders)) + def _reset_positions(self): + pos = [extruder.stepper.get_commanded_position() for extruder in self.extruders] + for i, p in enumerate(pos): + self.positions[i] = p + def get_commanded_position(self): + return self.commanded_pos + def get_name(self): + return self.name + def get_heater(self): + return self.heater + def stats(self, eventtime): + if self.name == 'mixingextruder': + return False, "mixingextruder: positions=%s mixing=%s" % ( + ",".join("%0.2f" % (m) for m in self.positions), + ",".join("%0.2f" % (m) for m in self.mixing)) + return False, "mixingextruder: mixing=%s" % (",".join("%0.2f" % (m) for m in self.mixing)) + def cmd_M163(self, gcmd): + index = gcmd.get_int('S', None, minval=0, maxval=len(self.extruders)) + weight = gcmd.get_float('P', 0., minval=0.) + self.ratios[index] = weight + def cmd_M164(self, gcmd): + mixingextruder = self + index = gcmd.get_int('S', 0, minval=0, maxval=len(self.extruders)) + if index: + mixingextruder = self.printer.lookup_object( + "mixingextruder%d" % (index)) + if not mixingextruder: + raise gcmd.error("Invalid extruder index") + s = sum(self.ratios) + if s <= 0: + raise gcmd.error("Could not save ratio: its empty") + for i, v in enumerate(self.ratios): + mixingextruder.mixing[i] = v/s + def cmd_M567(self, gcmd): + mixingextruder = self + index = gcmd.get_int('P', 0, minval=0, maxval=len(self.extruders)) + if index: + mixingextruder = self.printer.lookup_object( + "mixingextruder%d" % (index)) + if not mixingextruder: + raise gcmd.error("Invalid extruder index") + weighting = gcmd.get('E', None) + if not weighting: + raise gcmd.error("No weighting in M567") + weights = [float(w) for w in weighting.split(":")] + if min(weights) < 0: + raise gcmd.error("Negative weight not allowed") + s = sum(weights) + if s > 1.01 or s < 0.99: + raise gcmd.error("Could not save ratio: out of bounds %0.2f" % (s)) + for i, v in enumerate(weights): + mixingextruder.mixing[i] = v/s + def cmd_G1(self, gcmd): + gcode = self.printer.lookup_object('gcode') + weighting = gcmd.get('E', None) + if not weighting or ":" not in weighting: + self.orig_G1(gcmd) + return + weights = [float(w) for w in weighting.split(":")] + extrude = sum(weights) + weighting = ":".join("%0.2f" % (w / extrude) for w in weights) + self.cmd_M567(GCodeCommand( + gcode, "M567", "M567 E%s" % (weighting), + dict(E=weighting), gcmd._need_ack)) + self.orig_G1(GCodeCommand( + gcode, "G1", gcmd.get_commandline(), + dict(gcmd.get_command_parameters(), E="%f" % (extrude)), + gcmd._need_ack)) + cmd_ACTIVATE_EXTRUDER_help = "Change the active extruder" + def cmd_ACTIVATE_EXTRUDER(self, gcmd): + self._activate() + toolhead = self.printer.lookup_object('toolhead') + if toolhead.get_extruder() is self: + gcmd.respond_info("Extruder %s already active" % (self.name,)) + return + gcmd.respond_info("Activating extruder %s" % (self.name,)) + toolhead.flush_step_generation() + toolhead.set_extruder(self, self.get_commanded_position()) + self._reset_positions() + self.printer.send_event("extruder:activate_extruder") + cmd_MIXING_STATUS_help = "Display the status of the given MixingExtruder" + def cmd_MIXING_STATUS(self, gcmd): + self._activate() + eventtime = self.printer.get_reactor().monotonic() + status = self.get_status(eventtime) + gcmd.respond_info(", ".join("%s=%s" % (k, v) for k, v in status.items())) + +def load_config(config): + printer = config.get_printer() + for i in range(16): + section = 'mixingextruder' + if i: + section = 'mixingextruder%d' % (i,) + pe = MixingExtruder(config.getsection('mixingextruder'), i) + printer.add_object(section, pe) From b1f9c01fec86c292fbf99d1448518454e137082f Mon Sep 17 00:00:00 2001 From: Peter Gruber Date: Fri, 12 Feb 2021 14:07:33 +0100 Subject: [PATCH 02/17] mixingextruder: reformat code Signed-off-by: Peter Gruber --- klippy/extras/mixingextruder.py | 99 ++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 26 deletions(-) diff --git a/klippy/extras/mixingextruder.py b/klippy/extras/mixingextruder.py index 9ebd072ce822..989f66ffa71f 100644 --- a/klippy/extras/mixingextruder.py +++ b/klippy/extras/mixingextruder.py @@ -3,12 +3,12 @@ # Copyright (C) 2021 Peter Gruber # # This file may be distributed under the terms of the GNU GPLv3 license. -import math, logging -import stepper, chelper +import math +import logging -from toolhead import Move from gcode import GCodeCommand + class MixingMove: def __init__(self, x, y, z, e, dist_x, dist_y, dist_z, dist_e, @@ -21,30 +21,38 @@ def __init__(self, x, y, z, e, self.move_d = distance self.accel = acceleration self.start_v, self.cruise_v = velocity_start, velocity_cruise - self.accel_t, self.cruise_t, self.decel_t = time_accel, time_cruise, time_decel + self.accel_t, self.cruise_t, self.decel_t = \ + time_accel, time_cruise, time_decel self.start_pos = (x, y, z, e) self.end_pos = tuple(sum(s) for s in zip(self.start_pos, self.axes_d)) + class MixingExtruder: def __init__(self, config, idx): self.printer = config.get_printer() self.activated = False - self.name = config.get_name() if idx == 0 else "%s%d" % (config.get_name(), idx) - self.extruder_names = [e.strip() for e in config.get('extruders', None).split(",")] + self.name = config.get_name() if idx == 0 else "%s%d" % ( + config.get_name(), idx) + self.extruder_names = [e.strip() + for e in + config.get('extruders', None).split(",")] if not len(self.extruder_names): raise self._mcu.get_printer().config_error( "No extruders configured for mixing") self.main_extruder = None self.extruders = [] self.heater = None - self.mixing_extruders = {} if idx == 0 else self.printer.lookup_object("mixingextruder").mixing_extruders + self.mixing_extruders = {} if idx == 0 else self.printer.lookup_object( + "mixingextruder").mixing_extruders self.mixing_extruders[idx] = self self.mixing = [0 if p else 1 for p in range(len(self.extruder_names))] self.commanded_pos = 0 - self.positions = [0. for p in range(len(self.extruder_names))] if idx == 0 else self.mixing_extruders[0].positions + self.positions = [0. for p in range(len(self.extruder_names)) + ] if idx == 0 else self.mixing_extruders[0].positions self.ratios = [0 for p in range(len(self.extruder_names))] gcode = self.printer.lookup_object('gcode') - logging.info("MixingExtruder extruders=%s", ", ".join(self.extruder_names)) + logging.info("MixingExtruder extruders=%s", + ", ".join(self.extruder_names)) # Register commands gcode.register_mux_command("ACTIVATE_EXTRUDER", "EXTRUDER", self.name, self.cmd_ACTIVATE_EXTRUDER, @@ -52,6 +60,7 @@ def __init__(self, config, idx): gcode.register_mux_command("MIXING_STATUS", "EXTRUDER", self.name, self.cmd_MIXING_STATUS, desc=self.cmd_MIXING_STATUS_help) + def _activate(self): if self.activated: return @@ -66,7 +75,8 @@ def _activate(self): except Exception as e: self.extruders = [] self.heater = None - logging.error("no extruders found: %s" % (", ".join(self.extruder_names)), e) + logging.error("no extruders found: %s" % + (", ".join(self.extruder_names)), e) gcode = self.printer.lookup_object('gcode') # Register commands gcode.register_command("M163", self.cmd_M163) @@ -74,28 +84,38 @@ def _activate(self): gcode.register_command("M567", self.cmd_M567) self.orig_G1 = gcode.register_command("G1", None) gcode.register_command("G1", self.cmd_G1) + def update_move_time(self, flush_time): for extruder in self.extruders: extruder.update_move_time(flush_time) + def calc_junction(self, prev_move, move): diff_r = move.axes_r[3] - prev_move.axes_r[3] if diff_r: m = max(self.mixing) return (self.main_extruder.instant_corner_v / abs(m * diff_r))**2 return move.max_cruise_v2 + def _scale_move(self, move, idx): mixing = self.mixing[idx] if not mixing: return None - return MixingMove(move.start_pos[0], move.start_pos[1], move.start_pos[2], self.positions[idx], - move.axes_d[0], move.axes_d[1], move.axes_d[2], mixing * move.axes_d[3], - move.axes_r[0], move.axes_r[1], move.axes_r[2], mixing * move.axes_r[3], + return MixingMove(move.start_pos[0], move.start_pos[1], + move.start_pos[2], self.positions[idx], + move.axes_d[0], move.axes_d[1], move.axes_d[2], + mixing * move.axes_d[3], + move.axes_r[0], move.axes_r[1], move.axes_r[2], + mixing * move.axes_r[3], move.move_d, move.accel, move.start_v if hasattr(move, "start_v") else 0., - move.cruise_v if hasattr(move, "cruise_v") else math.sqrt(move.max_cruise_v2), + move.cruise_v if hasattr( + move, "cruise_v" + ) else math.sqrt(move.max_cruise_v2), move.accel_t if hasattr(move, "accel_t") else 0., - move.cruise_t if hasattr(move, "cruise_t") else move.min_move_t, + move.cruise_t if hasattr( + move, "cruise_t") else move.min_move_t, move.decel_t if hasattr(move, "decel_t") else 0.) + def _check_move(self, scaled_move, move): axis_r = scaled_move.axes_r[3] axis_d = scaled_move.axes_d[3] @@ -109,26 +129,32 @@ def _check_move(self, scaled_move, move): raise self.printer.command_error( "Extrude only move too long (%.3fmm vs %.3fmm)\n" "See the 'max_extrude_only_distance' config" - " option for details" % (axis_d, self.main_extruder.max_e_dist)) + " option for details" % (axis_d, + self.main_extruder.max_e_dist)) inv_extrude_r = 1. / abs(axis_r) move.limit_speed(self.main_extruder.max_e_velocity * inv_extrude_r, self.main_extruder.max_e_accel * inv_extrude_r) elif axis_r > self.main_extruder.max_extrude_ratio: - if axis_d <= self.main_extruder.nozzle_diameter * self.main_extruder.max_extrude_ratio: + if axis_d <= self.main_extruder.nozzle_diameter * \ + self.main_extruder.max_extrude_ratio: # Permit extrusion if amount extruded is tiny return area = axis_r * self.main_extruder.filament_area logging.debug("Overextrude: %s vs %s (area=%.3f dist=%.3f)", - axis_r, self.main_extruder.max_extrude_ratio, area, move.move_d) + axis_r, self.main_extruder.max_extrude_ratio, area, + move.move_d) raise self.printer.command_error( "Move exceeds maximum extrusion (%.3fmm^2 vs %.3fmm^2)\n" "See the 'max_extrude_cross_section' config option for details" - % (area, self.main_extruder.max_extrude_ratio * self.main_extruder.filament_area)) + % (area, self.main_extruder.max_extrude_ratio + * self.main_extruder.filament_area)) + def check_move(self, move): for idx, extruder in enumerate(self.extruders): scaled_move = self._scale_move(move, idx) if scaled_move: self._check_move(scaled_move, move) + def move(self, print_time, move): for idx, extruder in enumerate(self.extruders): scaled_move = self._scale_move(move, idx) @@ -136,31 +162,46 @@ def move(self, print_time, move): extruder.move(print_time, scaled_move) self.positions[idx] = scaled_move.end_pos[3] self.commanded_pos = move.end_pos[3] + def get_status(self, eventtime): - return dict(mixing=",".join("%0.1f%%" % (m * 100.) for m in self.mixing), - positions=",".join("%0.2fmm" % (m) for m in self.positions), - ticks=",".join("%0.2f" % (extruder.stepper.get_mcu_position()) for extruder in self.extruders), - extruders=",".join(extruder.name for extruder in self.extruders)) + return dict(mixing=",".join("%0.1f%%" % ( + m * 100.) for m in self.mixing), + positions=",".join("%0.2fmm" % (m) + for m in self.positions), + ticks=",".join("%0.2f" % ( + extruder.stepper.get_mcu_position()) + for extruder in self.extruders), + extruders=",".join(extruder.name + for extruder in self.extruders)) + def _reset_positions(self): - pos = [extruder.stepper.get_commanded_position() for extruder in self.extruders] + pos = [extruder.stepper.get_commanded_position() + for extruder in self.extruders] for i, p in enumerate(pos): self.positions[i] = p + def get_commanded_position(self): return self.commanded_pos + def get_name(self): return self.name + def get_heater(self): return self.heater + def stats(self, eventtime): if self.name == 'mixingextruder': return False, "mixingextruder: positions=%s mixing=%s" % ( ",".join("%0.2f" % (m) for m in self.positions), ",".join("%0.2f" % (m) for m in self.mixing)) - return False, "mixingextruder: mixing=%s" % (",".join("%0.2f" % (m) for m in self.mixing)) + return False, "mixingextruder: mixing=%s" % ( + ",".join("%0.2f" % (m) for m in self.mixing)) + def cmd_M163(self, gcmd): index = gcmd.get_int('S', None, minval=0, maxval=len(self.extruders)) weight = gcmd.get_float('P', 0., minval=0.) self.ratios[index] = weight + def cmd_M164(self, gcmd): mixingextruder = self index = gcmd.get_int('S', 0, minval=0, maxval=len(self.extruders)) @@ -174,6 +215,7 @@ def cmd_M164(self, gcmd): raise gcmd.error("Could not save ratio: its empty") for i, v in enumerate(self.ratios): mixingextruder.mixing[i] = v/s + def cmd_M567(self, gcmd): mixingextruder = self index = gcmd.get_int('P', 0, minval=0, maxval=len(self.extruders)) @@ -193,6 +235,7 @@ def cmd_M567(self, gcmd): raise gcmd.error("Could not save ratio: out of bounds %0.2f" % (s)) for i, v in enumerate(weights): mixingextruder.mixing[i] = v/s + def cmd_G1(self, gcmd): gcode = self.printer.lookup_object('gcode') weighting = gcmd.get('E', None) @@ -210,6 +253,7 @@ def cmd_G1(self, gcmd): dict(gcmd.get_command_parameters(), E="%f" % (extrude)), gcmd._need_ack)) cmd_ACTIVATE_EXTRUDER_help = "Change the active extruder" + def cmd_ACTIVATE_EXTRUDER(self, gcmd): self._activate() toolhead = self.printer.lookup_object('toolhead') @@ -222,11 +266,14 @@ def cmd_ACTIVATE_EXTRUDER(self, gcmd): self._reset_positions() self.printer.send_event("extruder:activate_extruder") cmd_MIXING_STATUS_help = "Display the status of the given MixingExtruder" + def cmd_MIXING_STATUS(self, gcmd): self._activate() eventtime = self.printer.get_reactor().monotonic() status = self.get_status(eventtime) - gcmd.respond_info(", ".join("%s=%s" % (k, v) for k, v in status.items())) + gcmd.respond_info(", ".join("%s=%s" % (k, v) + for k, v in status.items())) + def load_config(config): printer = config.get_printer() From 1df0bb54bddb59a5637ad8babe255c035c5018da Mon Sep 17 00:00:00 2001 From: Peter Gruber Date: Sun, 14 Feb 2021 19:14:15 +0100 Subject: [PATCH 03/17] mixingextruder: Fix limit checks Signed-off-by: Peter Gruber --- klippy/extras/mixingextruder.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/klippy/extras/mixingextruder.py b/klippy/extras/mixingextruder.py index 989f66ffa71f..c322ed686c21 100644 --- a/klippy/extras/mixingextruder.py +++ b/klippy/extras/mixingextruder.py @@ -204,7 +204,7 @@ def cmd_M163(self, gcmd): def cmd_M164(self, gcmd): mixingextruder = self - index = gcmd.get_int('S', 0, minval=0, maxval=len(self.extruders)) + index = gcmd.get_int('S', 0, minval=0, maxval=16) if index: mixingextruder = self.printer.lookup_object( "mixingextruder%d" % (index)) @@ -215,10 +215,11 @@ def cmd_M164(self, gcmd): raise gcmd.error("Could not save ratio: its empty") for i, v in enumerate(self.ratios): mixingextruder.mixing[i] = v/s + self.ratios[i] = 0.0 def cmd_M567(self, gcmd): mixingextruder = self - index = gcmd.get_int('P', 0, minval=0, maxval=len(self.extruders)) + index = gcmd.get_int('P', 0, minval=0, maxval=16) if index: mixingextruder = self.printer.lookup_object( "mixingextruder%d" % (index)) @@ -227,7 +228,9 @@ def cmd_M567(self, gcmd): weighting = gcmd.get('E', None) if not weighting: raise gcmd.error("No weighting in M567") - weights = [float(w) for w in weighting.split(":")] + weights = [float(w) + for i, w in enumerate(weighting.split(":")) + if i < len(self.extruders)] if min(weights) < 0: raise gcmd.error("Negative weight not allowed") s = sum(weights) @@ -242,7 +245,9 @@ def cmd_G1(self, gcmd): if not weighting or ":" not in weighting: self.orig_G1(gcmd) return - weights = [float(w) for w in weighting.split(":")] + weights = [float(w) + for i, w in enumerate(weighting.split(":")) + if i < len(self.extruders)] extrude = sum(weights) weighting = ":".join("%0.2f" % (w / extrude) for w in weights) self.cmd_M567(GCodeCommand( @@ -252,6 +257,7 @@ def cmd_G1(self, gcmd): gcode, "G1", gcmd.get_commandline(), dict(gcmd.get_command_parameters(), E="%f" % (extrude)), gcmd._need_ack)) + cmd_ACTIVATE_EXTRUDER_help = "Change the active extruder" def cmd_ACTIVATE_EXTRUDER(self, gcmd): @@ -265,6 +271,7 @@ def cmd_ACTIVATE_EXTRUDER(self, gcmd): toolhead.set_extruder(self, self.get_commanded_position()) self._reset_positions() self.printer.send_event("extruder:activate_extruder") + cmd_MIXING_STATUS_help = "Display the status of the given MixingExtruder" def cmd_MIXING_STATUS(self, gcmd): From a2cf769b52987d16117632c0673ba4bd3bc7863a Mon Sep 17 00:00:00 2001 From: Peter Gruber Date: Sun, 14 Feb 2021 19:16:27 +0100 Subject: [PATCH 04/17] docs: Add documentation for mixingextruder Initial documentation for the mixingextruder section. Also documents the available additional g-codes. Signed-off-by: Peter Gruber --- docs/Config_Reference.md | 19 +++++++++++++++++++ docs/G-Codes.md | 41 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 5c26a6a56a1e..5539d3422d5a 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1825,6 +1825,25 @@ more information. # parameters. ``` +### [mixingextruder] + +A mixing printhead which has in-1out mixing nozzle. When specified +16 virtual mixingextruders are created ("mixingextruder", +"mixingextruder1", ... "mixingextruder15"). They can be activated like +the standard extruders with "ACTIVATE_EXTRUDER EXTRUDER=mixingextruder" and +"MIXING_STATUS EXTRUDER=mixingextruder" provides some statistics. +When activated additional g-code are available: M163, M164, M567 and a +extended G1 command. See [G-Codes](G-Codes.md#mixing-commands) for +a detailed description of the additional commands. + +``` +[mixingextruder] +#extruders: +# Which extruders feed into the hotend/nozzle. provide a comma +# separated list, eg. "extruder,extruder1,extruder2". +# This configuration is required. +``` + ### [manual_stepper] Manual steppers (one may define any number of sections with a diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 745c63dbd4e8..58259659f338 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -325,6 +325,47 @@ enabled: future G-Code movement commands may run in parallel with the stepper movement. +### Mixingextruder Commands + +The following commands are available when a +[mixingextruder config section](Config_Reference.md#mixingextruder) is +enabled: +- `ACTIVATE_EXTRUDER EXTRUDER=mixingextruder`: This command activates the + specified mixing extruder. Subsequent G1 command use the mixing definded + for that mixing extruder. +- `M163 Sx Pa.a`: Set a ratio for the given extruder. The range of x are the indices of + the extruders in the + [mixingextruder config section](Config_Reference.md#mixingextruder). The + ratio a.a can be any positive number. The ratios are saved to a given + mixingextruder with the M164 command. +- `M164 [Sx]`: Save the ratios to the given mixing extruder. The range of x are + the 16 generated mixing extruders which can be activated with the + ACTIVATE_EXTRUDER command. If S is not specified it defaults to the + "mixingextruder" extruder. +- `M567 [Px] Ea.a:b.b:...:h.h`: Directly set the weighting on one of the + mixingextruders. The range of x are the 16 generated mixing extruders which + can be activated with the ACTIVATE_EXTRUDER command. The weighting + a.a, ... h.h are the weightings for the extruders in the + [mixingextruder config section](Config_Reference.md#mixingextruder) and + should all be positive numbers adding up to a total of 1. + If P is not specified it defaults to the "mixingextruder" extruder. If less weights than + extruders are provided the remaining weights are + assummed 0.0, additional beyond the available + extruders are ignored. +- `G1 [X] [Y] [Z] [E::...:] [F]`: + This extended G1 command is available if a mixing extruder is active. + The pos1, ... posn parameters define the amounts to extrude for the + extruders defined in the + [mixingextruder config section](Config_Reference.md#mixingextruder). + If less weights than + extruders are provided the remaining weights are + assummed 0.0, additional beyond the available + extruders are ignored. + Additionally it also sets the mixing for that extruder for subsequent + standard G1 commands, eg. "G1 ... E1:2:7 G1 ... E1" first extrudes + 1mm, 2mm and 7mm with the respective extruders and then 0.1mm, 0.2mm and + 0.7mm with the second G1. + ### Extruder stepper Commands The following command is available when an From 987933ab3e11afe31b8b8f23d8f7ba271166665f Mon Sep 17 00:00:00 2001 From: Peter Gruber Date: Mon, 15 Feb 2021 15:40:20 +0100 Subject: [PATCH 05/17] mixingextruder: Add support for M165 g-code command Signed-off-by: Peter Gruber --- klippy/extras/mixingextruder.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/klippy/extras/mixingextruder.py b/klippy/extras/mixingextruder.py index c322ed686c21..c8357d804be9 100644 --- a/klippy/extras/mixingextruder.py +++ b/klippy/extras/mixingextruder.py @@ -81,6 +81,7 @@ def _activate(self): # Register commands gcode.register_command("M163", self.cmd_M163) gcode.register_command("M164", self.cmd_M164) + gcode.register_command("M165", self.cmd_M165) gcode.register_command("M567", self.cmd_M567) self.orig_G1 = gcode.register_command("G1", None) gcode.register_command("G1", self.cmd_G1) @@ -217,6 +218,29 @@ def cmd_M164(self, gcmd): mixingextruder.mixing[i] = v/s self.ratios[i] = 0.0 + def cmd_M165(self, gcmd): + mixingextruder = self + toolhead = self.printer.lookup_object('toolhead') + activeextruder = toolhead.get_extruder() + if activeextruder is not self: + if activeextruder not in mixingextruder.mixing_extruders.values(): + raise gcmd.error("Active extruder is not mixing") + mixingextruder = activeextruder + a = gcmd.get_float('A', 0., minval=0, maxval=1) + b = gcmd.get_float('B', 0., minval=0, maxval=1) + c = gcmd.get_float('C', 0., minval=0, maxval=1) + d = gcmd.get_float('D', 0., minval=0, maxval=1) + h = gcmd.get_float('H', 0., minval=0, maxval=1) + i = gcmd.get_float('I', 0., minval=0, maxval=1) + weights = [float(w) + for i, w in enumerate((a, b, c, d, h, i)) + if i < len(self.extruders)] + s = sum(weights) + if s > 1.01 or s < 0.99: + raise gcmd.error("Could not save ratio: out of bounds %0.2f" % (s)) + for i, v in enumerate(weights): + mixingextruder.mixing[i] = v/s + def cmd_M567(self, gcmd): mixingextruder = self index = gcmd.get_int('P', 0, minval=0, maxval=16) From b2353fbcb4a7eeeae73e8add27de164589929b55 Mon Sep 17 00:00:00 2001 From: Peter Gruber Date: Mon, 15 Feb 2021 16:13:15 +0100 Subject: [PATCH 06/17] docs: Add documentation for the M165 g-code command Signed-off-by: Peter Gruber --- docs/G-Codes.md | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 58259659f338..c3cf94e34332 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -331,27 +331,35 @@ The following commands are available when a [mixingextruder config section](Config_Reference.md#mixingextruder) is enabled: - `ACTIVATE_EXTRUDER EXTRUDER=mixingextruder`: This command activates the - specified mixing extruder. Subsequent G1 command use the mixing definded + specified mixing extruder. Subsequent G1 command use the mixing defined for that mixing extruder. -- `M163 Sx Pa.a`: Set a ratio for the given extruder. The range of x are the indices of - the extruders in the +- `M163 S P`: Set a ratio for the given extruder. The + range of base_extruder are the indices of the extruders in the [mixingextruder config section](Config_Reference.md#mixingextruder). The - ratio a.a can be any positive number. The ratios are saved to a given + ratio can be any positive number. The ratios are saved to a given mixingextruder with the M164 command. -- `M164 [Sx]`: Save the ratios to the given mixing extruder. The range of x are - the 16 generated mixing extruders which can be activated with the - ACTIVATE_EXTRUDER command. If S is not specified it defaults to the - "mixingextruder" extruder. -- `M567 [Px] Ea.a:b.b:...:h.h`: Directly set the weighting on one of the - mixingextruders. The range of x are the 16 generated mixing extruders which - can be activated with the ACTIVATE_EXTRUDER command. The weighting - a.a, ... h.h are the weightings for the extruders in the +- `M164 [S]`: Save the ratios to the given mixing extruder. + The range of mixing_extruder are the 16 generated mixing extruders which + can be activated with the ACTIVATE_EXTRUDER command. If S is not + specified it defaults to the "mixingextruder" extruder. +- `M165 [A] [B] [C] [D] [H] [I]`: + Directly set the weighting on the current mixingextruder. The weightings + mix_1, ... mix_6 are the weightings for the extruders in the [mixingextruder config section](Config_Reference.md#mixingextruder) and should all be positive numbers adding up to a total of 1. - If P is not specified it defaults to the "mixingextruder" extruder. If less weights than - extruders are provided the remaining weights are - assummed 0.0, additional beyond the available - extruders are ignored. + If less weights than extruders are provided the remaining weights are + assumed 0.0, additional weights beyond the available extruders are ignored. +- `M567 [P] E::...:`: Directly set + the weighting on one of the mixingextruders. The range of mixing_extruder + are the 16 generated mixing extruders which can be activated with the + ACTIVATE_EXTRUDER command. The weightings mix_1, ... mix_n are the weightings + for the extruders in the + [mixingextruder config section](Config_Reference.md#mixingextruder) and + should all be positive numbers adding up to a total of 1. + If P is not specified it defaults to the first mixing extruder + ("mixingextruder"" extruder. + If less weights than extruders are provided the remaining weights are + assumed 0.0, additional weights beyond the available extruders are ignored. - `G1 [X] [Y] [Z] [E::...:] [F]`: This extended G1 command is available if a mixing extruder is active. The pos1, ... posn parameters define the amounts to extrude for the @@ -359,7 +367,7 @@ enabled: [mixingextruder config section](Config_Reference.md#mixingextruder). If less weights than extruders are provided the remaining weights are - assummed 0.0, additional beyond the available + assumed 0.0, additional beyond the available extruders are ignored. Additionally it also sets the mixing for that extruder for subsequent standard G1 commands, eg. "G1 ... E1:2:7 G1 ... E1" first extrudes From 8617cba8ce1aa0c3a05ecd3c6345656d549dfb35 Mon Sep 17 00:00:00 2001 From: Peter Gruber Date: Mon, 1 Mar 2021 23:58:31 +0100 Subject: [PATCH 07/17] mixingextruder: remove all M* g-codes and add comprehensive commands Add some basic g-code to control mixing and gradients. These commands have comprehensive names and can be used to implement all flavors (marlin, reprap) of the standard mixing g-codes M163-M166, M567 via g-code macros. Samples for that are provided. Signed-off-by: Peter Gruber --- config/sample-mixing-extruder.cfg | 61 +++++ docs/Config_Reference.md | 4 +- docs/G-Codes.md | 63 ++--- klippy/extras/mixingextruder.py | 407 ++++++++++++++++++++---------- 4 files changed, 374 insertions(+), 161 deletions(-) create mode 100644 config/sample-mixing-extruder.cfg diff --git a/config/sample-mixing-extruder.cfg b/config/sample-mixing-extruder.cfg new file mode 100644 index 000000000000..0b19b0d5b5dc --- /dev/null +++ b/config/sample-mixing-extruder.cfg @@ -0,0 +1,61 @@ +# This file contains a configuration snippet for a printer using a +# mixing extruder (3 in - 1 out). The 3 extruder motors should be defined +# as extruder, extruder1 and extruder2 + +# See docs/Config_Reference.md for a description of parameters. + +[mixingextruder] +extruders: extruder,extruder1,extruder2 +extended_g1: true + + +[gcode_macro M163] +gcode: + SET_MIXING_EXTRUDER EXTRUDER=mixingextruder MIXING_MOTOR={S} SCALE={P} + +[gcode_macro M164] +default_parameter_S: ACTIVE +gcode: + SAVE_MIXING_EXTRUDERS EXTRUDER=mixingextruder MIXING_EXTRUDER={S} + +[gcode_macro M165] +default_parameter_A: 0. +default_parameter_B: 0. +default_parameter_C: 0. +gcode: + SET_MIXING_EXTRUDER EXTRUDER=mixingextruder MIXING_MOTOR=0 SCALE={A} + SET_MIXING_EXTRUDER EXTRUDER=mixingextruder MIXING_MOTOR=1 SCALE={B} + SET_MIXING_EXTRUDER EXTRUDER=mixingextruder MIXING_MOTOR=2 SCALE={C} + SAVE_MIXING_EXTRUDERS EXTRUDER=mixingextruder MIXING_EXTRUDER=ACTIVE + +[gcode_macro M166] +default_parameter_T: ACTIVE +default_parameter_S: RESET +default_parameter_A: -1 +default_parameter_Z: -1 +default_parameter_I: -1 +default_parameter_J: -1 +gcode: + {% set extruder = printer['mixingextruder'].find_mixing_extruder(T) %} + {% if extruder >= 0 %} + {% if params.A|float >= 0 and params.Z|float >= 0 and params.I|int >= 0 and params.J|int >= 0 %} + ADD_MIXING_GRADIENT EXTRUDER={extruder} START_HEIGHT={A} END_HEIGHT={Z} START={I} END={J} + SET_MIXING_GRADIENT EXTRUDER={extruder} ENABLE={S} + {% elif S != "RESET" %} + SET_MIXING_GRADIENT EXTRUDER={extruder} ENABLE={S} + {% else %} + RESET_MIXING_GRADIENT EXTRUDER={extruder} + {% endif %} + {% else %} + {action_raise_error("Could not find mixingextruder")} + {% endif %} + +[gcode_macro M567] +default_parameter_P: ACTIVE +default_parameter_E: 1:0:0 +gcode: + {% set weights = (E+":0:0").split(":") %} + SET_MIXING_EXTRUDER EXTRUDER=mixingextruder MIXING_MOTOR=0 SCALE={weights[0]} + SET_MIXING_EXTRUDER EXTRUDER=mixingextruder MIXING_MOTOR=1 SCALE={weights[1]} + SET_MIXING_EXTRUDER EXTRUDER=mixingextruder MIXING_MOTOR=2 SCALE={weights[2]} + SAVE_MIXING_EXTRUDERS EXTRUDER=mixingextruder MIXING_EXTRUDER={P} diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 5539d3422d5a..1f38dee1063b 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1832,8 +1832,8 @@ A mixing printhead which has in-1out mixing nozzle. When specified "mixingextruder1", ... "mixingextruder15"). They can be activated like the standard extruders with "ACTIVATE_EXTRUDER EXTRUDER=mixingextruder" and "MIXING_STATUS EXTRUDER=mixingextruder" provides some statistics. -When activated additional g-code are available: M163, M164, M567 and a -extended G1 command. See [G-Codes](G-Codes.md#mixing-commands) for +When activated additional g-code are available. See +[G-Codes](G-Codes.md#mixing-commands) for a detailed description of the additional commands. ``` diff --git a/docs/G-Codes.md b/docs/G-Codes.md index c3cf94e34332..865f0bc61968 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -330,36 +330,39 @@ enabled: The following commands are available when a [mixingextruder config section](Config_Reference.md#mixingextruder) is enabled: -- `ACTIVATE_EXTRUDER EXTRUDER=mixingextruder`: This command activates the - specified mixing extruder. Subsequent G1 command use the mixing defined - for that mixing extruder. -- `M163 S P`: Set a ratio for the given extruder. The - range of base_extruder are the indices of the extruders in the - [mixingextruder config section](Config_Reference.md#mixingextruder). The - ratio can be any positive number. The ratios are saved to a given - mixingextruder with the M164 command. -- `M164 [S]`: Save the ratios to the given mixing extruder. - The range of mixing_extruder are the 16 generated mixing extruders which - can be activated with the ACTIVATE_EXTRUDER command. If S is not - specified it defaults to the "mixingextruder" extruder. -- `M165 [A] [B] [C] [D] [H] [I]`: - Directly set the weighting on the current mixingextruder. The weightings - mix_1, ... mix_6 are the weightings for the extruders in the - [mixingextruder config section](Config_Reference.md#mixingextruder) and - should all be positive numbers adding up to a total of 1. - If less weights than extruders are provided the remaining weights are - assumed 0.0, additional weights beyond the available extruders are ignored. -- `M567 [P] E::...:`: Directly set - the weighting on one of the mixingextruders. The range of mixing_extruder - are the 16 generated mixing extruders which can be activated with the - ACTIVATE_EXTRUDER command. The weightings mix_1, ... mix_n are the weightings - for the extruders in the - [mixingextruder config section](Config_Reference.md#mixingextruder) and - should all be positive numbers adding up to a total of 1. - If P is not specified it defaults to the first mixing extruder - ("mixingextruder"" extruder. - If less weights than extruders are provided the remaining weights are - assumed 0.0, additional weights beyond the available extruders are ignored. +- `ACTIVATE_EXTRUDER EXTRUDER=`: This command activates + the specified mixing extruder. Subsequent G1 commands use the mixing + defined for that mixing extruder. +- `SET_MIXING_EXTRUDER EXTRUDER= MIXING_MOTOR= + SCALE=`: Set a scale for a given extruder at the mixing extruder. +- `SAVE_MIXING_EXTRUDERS EXTRUDER= + [MIXING_EXTRUDER=]`: Saves the previous set scales to a given + target mixing extruder. If the target is is not given, save it at the + source mixing extruder. For example with a 2in-iout extruder to set + the mix for "mixingextruder3" to 75%/25% one would use: + `SET_MIXING_EXTRUDER EXTRUDER=mixingextruder MIXING_MOTOR=0 SCALE=75` + `SET_MIXING_EXTRUDER EXTRUDER=mixingextruder MIXING_MOTOR=1 SCALE=25` + `SAVE_MIXING_EXTRUDERS EXTRUDER=mixingextruder MIXING_EXTRUDER=3` +- `ADD_MIXING_GRADIENT EXTRUDER= START= + END= START_HEIGHT= END_HEIGHT=`: Configures + (adds) a gradient for the given mixing extruder. The sources are + references to (other) mixing extruders. For example if with a 2in-1out + extruder "mixingextruder2" is set mix 100%/0% and "mixingextruder3" + is set to 0%/100% + `ADD_MIXING_GRADIENT EXTRUDER=mixingextruder START=2 END=3 + START_HEIGHT=10 END_HEIGHT=20` + `ADD_MIXING_GRADIENT EXTRUDER=mixingextruder START=3 END=2 + START_HEIGHT=20 END_HEIGHT=30` + would setup a gradient for "mixingextruder" which is constant 100%/0% + between heights 0mm and 10mm, then linearly interpolates to 0%/100% at + height 20mm and back to 100%/0% at height 30mm and stays that for all + heights above. +- `RESET_MIXING_GRADIENT EXTRUDER=`: Reset/remove all + gradients for the given mixing extruder. +- `SET_MIXING_GRADIENT EXTRUDER= [ENABLE=]`: + Enable/disable the gradient at the given mixing extruder. +- `MIXING_STATUS EXTRUDER=`: Returns the configuration + and status of the given mixing extruder. - `G1 [X] [Y] [Z] [E::...:] [F]`: This extended G1 command is available if a mixing extruder is active. The pos1, ... posn parameters define the amounts to extrude for the diff --git a/klippy/extras/mixingextruder.py b/klippy/extras/mixingextruder.py index c8357d804be9..9e97e15a084c 100644 --- a/klippy/extras/mixingextruder.py +++ b/klippy/extras/mixingextruder.py @@ -9,6 +9,32 @@ from gcode import GCodeCommand +def extruder_to_idx(name, active=None): + name = name.lower() + if name == "active": + name = active() if active else '' + if name.startswith('mixingextruder'): + if name[14:] == '': + return 0 + try: + return int(name[14:]) + except Exception: + pass + try: + return int(name) + except Exception: + return -1 + + +def idx_to_extruder(idx): + return "mixingextruder%d" % (idx) if idx else "mixingextruder" + + +def find_mixing_extruder(self, name, active=''): + idx = extruder_to_idx(name, lambda: active) + return idx_to_extruder(0 if idx < 0 else idx) + + class MixingMove: def __init__(self, x, y, z, e, dist_x, dist_y, dist_z, dist_e, @@ -28,39 +54,84 @@ def __init__(self, x, y, z, e, class MixingExtruder: - def __init__(self, config, idx): + def __init__(self, config, idx, parent=None): self.printer = config.get_printer() self.activated = False - self.name = config.get_name() if idx == 0 else "%s%d" % ( - config.get_name(), idx) + self.name = idx_to_extruder(idx) self.extruder_names = [e.strip() for e in - config.get('extruders', None).split(",")] + config.get('extruders', '').split(",") + if len(e)] if not len(self.extruder_names): raise self._mcu.get_printer().config_error( "No extruders configured for mixing") - self.main_extruder = None - self.extruders = [] - self.heater = None - self.mixing_extruders = {} if idx == 0 else self.printer.lookup_object( - "mixingextruder").mixing_extruders + self.extended_g1 = config.get('extended_g1', 'false').lower() == 'true' + self.extruders = parent.extruders if parent else [] + self.mixing_extruders = parent.mixing_extruders if parent else {} self.mixing_extruders[idx] = self - self.mixing = [0 if p else 1 for p in range(len(self.extruder_names))] + self.mixing = self._init_mixings(idx, len(self.extruder_names)) self.commanded_pos = 0 self.positions = [0. for p in range(len(self.extruder_names)) ] if idx == 0 else self.mixing_extruders[0].positions self.ratios = [0 for p in range(len(self.extruder_names))] - gcode = self.printer.lookup_object('gcode') - logging.info("MixingExtruder extruders=%s", - ", ".join(self.extruder_names)) + self.current_mixing = tuple(self.ratios) + self.gradient_enabled = False + # assumed to be sorted list of ((start, middle, end), (ref1, ref2)) + self.gradients = [] + self.gradient_method = 'linear' + logging.info("MixingExtruder %d extruders=%s", idx, + ",".join(self.extruder_names), + ",".join("%.1f" % (x) for x in self.mixing)) # Register commands + gcode = self.printer.lookup_object('gcode') gcode.register_mux_command("ACTIVATE_EXTRUDER", "EXTRUDER", self.name, self.cmd_ACTIVATE_EXTRUDER, desc=self.cmd_ACTIVATE_EXTRUDER_help) + gcode.register_mux_command("SET_MIXING_EXTRUDER", "EXTRUDER", + self.name, self.cmd_SET_MIXING_EXTRUDER, + desc=self.cmd_SET_MIXING_EXTRUDER_help) + gcode.register_mux_command("SAVE_MIXING_EXTRUDERS", "EXTRUDER", + self.name, self.cmd_SAVE_MIXING_EXTRUDERS, + desc=self.cmd_SAVE_MIXING_EXTRUDERS_help) + gcode.register_mux_command("ADD_MIXING_GRADIENT", "EXTRUDER", + self.name, self.cmd_ADD_MIXING_GRADIENT, + desc=self.cmd_ADD_MIXING_GRADIENT_help) + gcode.register_mux_command("RESET_MIXING_GRADIENT", "EXTRUDER", + self.name, self.cmd_RESET_MIXING_GRADIENT, + desc=self.cmd_RESET_MIXING_GRADIENT_help) + gcode.register_mux_command("SET_MIXING_GRADIENT", "EXTRUDER", + self.name, self.cmd_SET_MIXING_GRADIENT, + desc=self.cmd_SET_MIXING_GRADIENT_help) gcode.register_mux_command("MIXING_STATUS", "EXTRUDER", self.name, self.cmd_MIXING_STATUS, desc=self.cmd_MIXING_STATUS_help) + def _init_mixings(self, idx, extruders): + if idx == 0: + return [1./extruders for p in range(extruders)] + idx = idx-1 + if idx < extruders: + return [1. if p == idx else 0. for p in range(extruders)] + idx = idx-extruders + if idx < extruders: + return [0. if p == idx else 1./(extruders-1) + for p in range(extruders)] + idx = idx-extruders + if extruders == 3: + if idx < 2*extruders: + return [[0. if p == x else (1+((x+y) % 2))/3. + for p in range(extruders)] + for x in range(extruders) for y in (1, 2)][idx] + idx = idx-2*extruders + elif extruders > 3: + if idx < (extruders*(extruders-1)/2): + return [[0. if p == x or p == y else 1./(extruders-2) + for p in range(extruders)] + for x in range(extruders) + for y in range(x+1, extruders)][idx] + idx = idx-(extruders*(extruders-1)/2) + return [1./extruders for p in range(extruders)] + def _activate(self): if self.activated: return @@ -70,21 +141,18 @@ def _activate(self): try: self.extruders = [self.printer.lookup_object(extruder) for extruder in self.extruder_names] - self.main_extruder = self.extruders[0] - self.heater = self.main_extruder.get_heater() except Exception as e: self.extruders = [] - self.heater = None logging.error("no extruders found: %s" % (", ".join(self.extruder_names)), e) gcode = self.printer.lookup_object('gcode') # Register commands - gcode.register_command("M163", self.cmd_M163) - gcode.register_command("M164", self.cmd_M164) - gcode.register_command("M165", self.cmd_M165) - gcode.register_command("M567", self.cmd_M567) - self.orig_G1 = gcode.register_command("G1", None) - gcode.register_command("G1", self.cmd_G1) + if self.extended_g1: + for cmd in ("G1", "G2", "G3"): + o = gcode.register_command(cmd, None) + if o: + gcode.register_command(cmd, + lambda g: self.cmd_G(cmd, o, g)) def update_move_time(self, flush_time): for extruder in self.extruders: @@ -94,11 +162,11 @@ def calc_junction(self, prev_move, move): diff_r = move.axes_r[3] - prev_move.axes_r[3] if diff_r: m = max(self.mixing) - return (self.main_extruder.instant_corner_v / abs(m * diff_r))**2 + return (self.extruders[0].instant_corner_v / abs(m * diff_r))**2 return move.max_cruise_v2 - def _scale_move(self, move, idx): - mixing = self.mixing[idx] + def _scale_move(self, move, idx, weights): + mixing = weights[idx] if not mixing: return None return MixingMove(move.start_pos[0], move.start_pos[1], @@ -120,60 +188,120 @@ def _scale_move(self, move, idx): def _check_move(self, scaled_move, move): axis_r = scaled_move.axes_r[3] axis_d = scaled_move.axes_d[3] - if not self.heater.can_extrude: + if not self.get_heater().can_extrude: raise self.printer.command_error( "Extrude below minimum temp\n" "See the 'min_extrude_temp' config option for details") if (not move.axes_d[0] and not move.axes_d[1]) or axis_r < 0.: # Extrude only move (or retraction move) - limit accel and velocity - if abs(axis_d) > self.main_extruder.max_e_dist: + if abs(axis_d) > self.extruders[0].max_e_dist: raise self.printer.command_error( "Extrude only move too long (%.3fmm vs %.3fmm)\n" "See the 'max_extrude_only_distance' config" " option for details" % (axis_d, - self.main_extruder.max_e_dist)) + self.extruders[0].max_e_dist)) inv_extrude_r = 1. / abs(axis_r) - move.limit_speed(self.main_extruder.max_e_velocity * inv_extrude_r, - self.main_extruder.max_e_accel * inv_extrude_r) - elif axis_r > self.main_extruder.max_extrude_ratio: - if axis_d <= self.main_extruder.nozzle_diameter * \ - self.main_extruder.max_extrude_ratio: + move.limit_speed(self.extruders[0].max_e_velocity * inv_extrude_r, + self.extruders[0].max_e_accel * inv_extrude_r) + elif axis_r > self.extruders[0].max_extrude_ratio: + if axis_d <= self.extruders[0].nozzle_diameter * \ + self.extruders[0].max_extrude_ratio: # Permit extrusion if amount extruded is tiny return - area = axis_r * self.main_extruder.filament_area + area = axis_r * self.extruders[0].filament_area logging.debug("Overextrude: %s vs %s (area=%.3f dist=%.3f)", - axis_r, self.main_extruder.max_extrude_ratio, area, + axis_r, self.extruders[0].max_extrude_ratio, area, move.move_d) raise self.printer.command_error( "Move exceeds maximum extrusion (%.3fmm^2 vs %.3fmm^2)\n" "See the 'max_extrude_cross_section' config option for details" - % (area, self.main_extruder.max_extrude_ratio - * self.main_extruder.filament_area)) + % (area, self.extruders[0].max_extrude_ratio + * self.extruders[0].filament_area)) + + def _get_gradient(self, start_pos, end_pos): + default = self.mixing + for heights, refs in self.gradients: + start, _, end = heights + start_mix, end_mix = (self.mixing_extruders[i].mixing + for i in refs) + if self.gradient_method == 'linear': + zpos = start_pos[2] + if zpos <= start: + return start_mix + if zpos >= end: + default = end_mix + continue + w = (zpos - start) / (end - start) + logging.info("linear gradient @%.1f(%.1f-%.1f) [%s-%s]" % + (zpos, start, end, + "/".join("%.1f" % x for x in start_mix), + "/".join("%.1f" % x for x in end_mix))) + return list(((1. - w) * s + w * e) + for s, e in zip(start_mix, end_mix)) + if self.gradient_method == 'spherical': + pos = [(x+y)/2. for x, y in zip(start_pos, end_pos)] + dist = math.sqrt(sum(x**2 for x in pos)) + if dist <= start: + return start_mix + if dist >= end: + default = end_mix + continue + w = (dist - start) / (end - start) + logging.info("spherical gradient @%.1f(%.1f-%.1f) [%s-%s]" % + (zpos, start, end, + "/".join("%.1f" % x for x in start_mix), + "/".join("%.1f" % x for x in end_mix))) + return list(((1. - w) * s + w * e) + for s, e in zip(start_mix, end_mix)) + return default def check_move(self, move): + mixing = self.mixing if not self.gradient_enabled \ + else self._get_gradient(move.start_pos[:3], move.end_pos[:3]) for idx, extruder in enumerate(self.extruders): - scaled_move = self._scale_move(move, idx) + scaled_move = self._scale_move(move, idx, mixing) if scaled_move: self._check_move(scaled_move, move) def move(self, print_time, move): + mixing = self.mixing if self.gradient_enabled \ + else self._get_gradient(move.start_pos[:3], move.end_pos[:3]) + self.current_mixing = tuple(mixing) for idx, extruder in enumerate(self.extruders): - scaled_move = self._scale_move(move, idx) + scaled_move = self._scale_move(move, idx, mixing) if scaled_move: extruder.move(print_time, scaled_move) self.positions[idx] = scaled_move.end_pos[3] self.commanded_pos = move.end_pos[3] def get_status(self, eventtime): - return dict(mixing=",".join("%0.1f%%" % ( - m * 100.) for m in self.mixing), - positions=",".join("%0.2fmm" % (m) - for m in self.positions), - ticks=",".join("%0.2f" % ( + status = {} + status = dict(mixing=",".join("%0.1f%%" % (m * 100.) + for m in self.mixing), + current=",".join("%0.1f%%" % (m * 100.) + for m in self.current_mixing), + positions=",".join("%0.2fmm" % (p) + for p in self.positions), + ticks=",".join("%0.2f" % ( extruder.stepper.get_mcu_position()) - for extruder in self.extruders), - extruders=",".join(extruder.name - for extruder in self.extruders)) + for extruder in self.extruders), + extruders=",".join(extruder.name + for extruder in self.extruders)) + for i, gradient in enumerate(self.gradients): + status.update({"gradient%d" % (i): ",".join( + "%s:%s" % (k, v) + for k, v in dict( + heights="%.1f-(%.1f)-%.1f" % gradient[0], + mixings="%s-%s" % tuple( + "/".join("%.1f" % (x) + for x in self.mixing_extruders[i].mixing) + for i in gradient[1]), + method=self.gradient_method, + enabled=str(self.gradient_enabled)).items())}) + active = self._active_extruder() + status['find_mixing_extruder'] = \ + lambda name: find_mixing_extruder(name, active) + return status def _reset_positions(self): pos = [extruder.stepper.get_commanded_position() @@ -188,29 +316,65 @@ def get_name(self): return self.name def get_heater(self): - return self.heater + return self.extruders[0].get_heater() def stats(self, eventtime): if self.name == 'mixingextruder': - return False, "mixingextruder: positions=%s mixing=%s" % ( + return False, self.name + ": positions=%s mixing=%s" % ( ",".join("%0.2f" % (m) for m in self.positions), - ",".join("%0.2f" % (m) for m in self.mixing)) - return False, "mixingextruder: mixing=%s" % ( - ",".join("%0.2f" % (m) for m in self.mixing)) + ",".join("%0.2f" % (m) for m in self.current_mixing)) + return False, self.name + ": mixing=%s" % ( + ",".join("%0.2f" % (m) for m in self.current_mixing)) - def cmd_M163(self, gcmd): - index = gcmd.get_int('S', None, minval=0, maxval=len(self.extruders)) - weight = gcmd.get_float('P', 0., minval=0.) - self.ratios[index] = weight + def cmd_G(self, name, orig, gcmd): + weighting = gcmd.get('E', None) + if not weighting or ":" not in weighting: + return orig(gcmd) + toolhead = self.printer.lookup_object('toolhead') + extruder = toolhead.get_extruder() + if not extruder.get_name().startswith("mixingextruder"): + raise gcmd.error("No mixing extruder active") + gcode = self.printer.lookup_object('gcode') + weights = [float(w) + for i, w in enumerate(weighting.split(":")) + if i < len(self.extruders)] + extrude = sum(weights) + if min(weights) < 0: + raise gcmd.error("Invalid extrude: %s" % (weighting)) + for i, w in enumerate(weights): + extruder.mixing[i] = w/extrude + orig(GCodeCommand( + gcode, name, gcmd.get_commandline(), + dict(gcmd.get_command_parameters(), E="%f" % (extrude)), + gcmd._need_ack)) + + cmd_SET_MIXING_EXTRUDER_help = "Set scale on motor/extruder" + + def cmd_SET_MIXING_EXTRUDER(self, gcmd): + self._activate() + extruder = gcmd.get('MIXING_MOTOR') + scale = gcmd.get_float('SCALE', minval=0.) + if extruder not in self.extruder_names: + try: + index = int(extruder) + if not 0 <= index < len(self.extruder_names): + raise Exception("Invalid index") + except Exception as e: + raise gcmd.error("Invalid extruder/motor: %s" % (e.message)) + else: + index = self.extruder_names.index(extruder) + self.ratios[index] = scale + + cmd_SAVE_MIXING_EXTRUDERS_help = "Save the scales on motors" - def cmd_M164(self, gcmd): + def cmd_SAVE_MIXING_EXTRUDERS(self, gcmd): + self._activate() mixingextruder = self - index = gcmd.get_int('S', 0, minval=0, maxval=16) - if index: - mixingextruder = self.printer.lookup_object( - "mixingextruder%d" % (index)) - if not mixingextruder: - raise gcmd.error("Invalid extruder index") + extruder = gcmd.get('MIXING_EXTRUDER', None) + if extruder: + idx = self._to_idx(extruder) + if idx >= 0: + mixingextruder = self.mixing_extruders[idx] s = sum(self.ratios) if s <= 0: raise gcmd.error("Could not save ratio: its empty") @@ -218,69 +382,53 @@ def cmd_M164(self, gcmd): mixingextruder.mixing[i] = v/s self.ratios[i] = 0.0 - def cmd_M165(self, gcmd): - mixingextruder = self + cmd_SET_MIXING_GRADIENT_help = "Turn no/off grdient mixing" + + def cmd_SET_MIXING_GRADIENT(self, gcmd): + try: + enable = gcmd.get_int('ENABLE', 1, minval=0, maxval=1) + self.gradient_enabled = enable == 1 + except Exception: + enable = gcmd.get('ENABLE', '') + self.gradient_enabled = enable.lower() == 'true' + + cmd_ADD_MIXING_GRADIENT_help = "Add mixing gradient" + + def _active_extruder(self): toolhead = self.printer.lookup_object('toolhead') - activeextruder = toolhead.get_extruder() - if activeextruder is not self: - if activeextruder not in mixingextruder.mixing_extruders.values(): - raise gcmd.error("Active extruder is not mixing") - mixingextruder = activeextruder - a = gcmd.get_float('A', 0., minval=0, maxval=1) - b = gcmd.get_float('B', 0., minval=0, maxval=1) - c = gcmd.get_float('C', 0., minval=0, maxval=1) - d = gcmd.get_float('D', 0., minval=0, maxval=1) - h = gcmd.get_float('H', 0., minval=0, maxval=1) - i = gcmd.get_float('I', 0., minval=0, maxval=1) - weights = [float(w) - for i, w in enumerate((a, b, c, d, h, i)) - if i < len(self.extruders)] - s = sum(weights) - if s > 1.01 or s < 0.99: - raise gcmd.error("Could not save ratio: out of bounds %0.2f" % (s)) - for i, v in enumerate(weights): - mixingextruder.mixing[i] = v/s + return toolhead.get_extruder().get_name().lower() - def cmd_M567(self, gcmd): - mixingextruder = self - index = gcmd.get_int('P', 0, minval=0, maxval=16) - if index: - mixingextruder = self.printer.lookup_object( - "mixingextruder%d" % (index)) - if not mixingextruder: - raise gcmd.error("Invalid extruder index") - weighting = gcmd.get('E', None) - if not weighting: - raise gcmd.error("No weighting in M567") - weights = [float(w) - for i, w in enumerate(weighting.split(":")) - if i < len(self.extruders)] - if min(weights) < 0: - raise gcmd.error("Negative weight not allowed") - s = sum(weights) - if s > 1.01 or s < 0.99: - raise gcmd.error("Could not save ratio: out of bounds %0.2f" % (s)) - for i, v in enumerate(weights): - mixingextruder.mixing[i] = v/s + def _to_idx(self, name): + return extruder_to_idx(name, active=self._active_extruder) - def cmd_G1(self, gcmd): - gcode = self.printer.lookup_object('gcode') - weighting = gcmd.get('E', None) - if not weighting or ":" not in weighting: - self.orig_G1(gcmd) - return - weights = [float(w) - for i, w in enumerate(weighting.split(":")) - if i < len(self.extruders)] - extrude = sum(weights) - weighting = ":".join("%0.2f" % (w / extrude) for w in weights) - self.cmd_M567(GCodeCommand( - gcode, "M567", "M567 E%s" % (weighting), - dict(E=weighting), gcmd._need_ack)) - self.orig_G1(GCodeCommand( - gcode, "G1", gcmd.get_commandline(), - dict(gcmd.get_command_parameters(), E="%f" % (extrude)), - gcmd._need_ack)) + def cmd_ADD_MIXING_GRADIENT(self, gcmd): + start_extruder = self._to_idx(gcmd.get('START')) + end_extruder = self._to_idx(gcmd.get('END')) + if start_extruder not in self.mixing_extruders.keys() or \ + end_extruder not in self.mixing_extruders.keys(): + raise gcmd.error("Invalid start/end value") + start_height = gcmd.get_float('START_HEIGHT', minval=0.) + end_height = gcmd.get_float('END_HEIGHT', minval=0.) + if start_height > end_height: + start_height, end_height = end_height, start_height + start_extruder, end_extruder = end_extruder, start_extruder + for gradient in self.gradients: + s, _, e = gradient[0] + if e > start_height and end_height > s: + raise gcmd.error( + "Could not configure gradient: overlapping starts/ends") + self.gradients.append(( + (start_height, + (start_height + end_height) / 2, + end_height), + (start_extruder, end_extruder))) + self.gradients.sort(key=lambda x: x[0][0]) + + cmd_RESET_MIXING_GRADIENT_help = "Clear mixing gradient info" + + def cmd_RESET_MIXING_GRADIENT(self, gcmd): + self.gradient_enabled, self.gradients, self.gradient_method = \ + False, [], 'linear' cmd_ACTIVATE_EXTRUDER_help = "Change the active extruder" @@ -307,10 +455,11 @@ def cmd_MIXING_STATUS(self, gcmd): def load_config(config): - printer = config.get_printer() + mixingextruder = None for i in range(16): - section = 'mixingextruder' - if i: - section = 'mixingextruder%d' % (i,) - pe = MixingExtruder(config.getsection('mixingextruder'), i) - printer.add_object(section, pe) + pe = MixingExtruder(config.getsection('mixingextruder'), + i, parent=mixingextruder) + if i == 0: + mixingextruder = pe + logging.info("Started mixingextruder") + return mixingextruder From b5e279a683bf9db0e772e6757b17cb8d7f8d44e0 Mon Sep 17 00:00:00 2001 From: Peter Gruber Date: Tue, 16 Mar 2021 00:20:47 +0100 Subject: [PATCH 08/17] extruder: add extruder registry and check registry at M104/M109 Slicers usually produce g-codes to preheat tools/extruders before usage. This will have undesire effects when multiple extruders share a heater. Signed-off-by: Peter Gruber --- klippy/kinematics/extruder.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 6775c5de061c..890e0a442483 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -160,15 +160,20 @@ def cmd_M104(self, gcmd, wait=False): # Set Extruder Temperature temp = gcmd.get_float('S', 0.) index = gcmd.get_int('T', None, minval=0) + current_extruder = self.printer.lookup_object('toolhead').get_extruder() if index is not None: - section = 'extruder' - if index: - section = 'extruder%d' % (index,) - extruder = self.printer.lookup_object(section, None) - if extruder is None: + extruders = self.printer.lookup_object("extruders", None) + logging.info("extruders", extruders.extruder_names) + printer_extruders = extruders.get_extruders() + if index < len(printer_extruders): + extruder = printer_extruders[index] + else: if temp <= 0.: return raise gcmd.error("Extruder not configured") + if current_extruder.get_heater() == extruder.get_heater(): + gcmd.respond_info("not changing temperature of current heater") + return else: extruder = self.printer.lookup_object('toolhead').get_extruder() pheaters = self.printer.lookup_object('heaters') @@ -235,8 +240,23 @@ def get_heater(self): def get_trapq(self): raise self.printer.command_error("Extruder not configured") +class Extruders: + def __init__(self, config): + self.name = "extruders" + self.extruders = {} + self.extruder_names = [] + def register_extruder(self, name, extruder): + self.extruders[name] = extruder + self.extruder_names.append(name) + def get_extruders(self): + return [self.extruders[name] for name in self.extruder_names] + def get_extruder(self, name): + return self.extruders[name] + def add_printer_objects(config): printer = config.get_printer() + extruders = Extruders(config.getsection('extruder')) + printer.add_object("extruders", extruders) for i in range(99): section = 'extruder' if i: @@ -244,4 +264,5 @@ def add_printer_objects(config): if not config.has_section(section): break pe = PrinterExtruder(config.getsection(section), i) + extruders.register_extruder(section, pe) printer.add_object(section, pe) From 8c02e139178ad5bfd3503f7a0581701d74fc80f4 Mon Sep 17 00:00:00 2001 From: Peter Gruber Date: Tue, 16 Mar 2021 00:26:45 +0100 Subject: [PATCH 09/17] mixingextruder: remove enhanced G1 command and use extruder registry Also make some small improvements and code cleanups. Signed-off-by: Peter Gruber --- docs/G-Codes.md | 32 +++----- klippy/extras/mixingextruder.py | 131 +++++++++++++------------------- 2 files changed, 60 insertions(+), 103 deletions(-) diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 865f0bc61968..b83748b1f9cf 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -333,16 +333,15 @@ enabled: - `ACTIVATE_EXTRUDER EXTRUDER=`: This command activates the specified mixing extruder. Subsequent G1 commands use the mixing defined for that mixing extruder. -- `SET_MIXING_EXTRUDER EXTRUDER= MIXING_MOTOR= - SCALE=`: Set a scale for a given extruder at the mixing extruder. -- `SAVE_MIXING_EXTRUDERS EXTRUDER= - [MIXING_EXTRUDER=]`: Saves the previous set scales to a given +- `SET_MIXING_EXTRUDER MIXING_MOTOR= + SCALE=`: Set a scale for a given extruder. +- `SAVE_MIXING_EXTRUDERS [MIXING_EXTRUDER=]`: Saves the previous set scales to a given target mixing extruder. If the target is is not given, save it at the - source mixing extruder. For example with a 2in-iout extruder to set + currently active mixing extruder. For example with a 2in-iout extruder to set the mix for "mixingextruder3" to 75%/25% one would use: - `SET_MIXING_EXTRUDER EXTRUDER=mixingextruder MIXING_MOTOR=0 SCALE=75` - `SET_MIXING_EXTRUDER EXTRUDER=mixingextruder MIXING_MOTOR=1 SCALE=25` - `SAVE_MIXING_EXTRUDERS EXTRUDER=mixingextruder MIXING_EXTRUDER=3` + `SET_MIXING_EXTRUDER MIXING_MOTOR=0 SCALE=75` + `SET_MIXING_EXTRUDER MIXING_MOTOR=1 SCALE=25` + `SAVE_MIXING_EXTRUDERS MIXING_EXTRUDER=3` - `ADD_MIXING_GRADIENT EXTRUDER= START= END= START_HEIGHT= END_HEIGHT=`: Configures (adds) a gradient for the given mixing extruder. The sources are @@ -359,23 +358,10 @@ enabled: heights above. - `RESET_MIXING_GRADIENT EXTRUDER=`: Reset/remove all gradients for the given mixing extruder. -- `SET_MIXING_GRADIENT EXTRUDER= [ENABLE=]`: - Enable/disable the gradient at the given mixing extruder. +- `SET_MIXING_GRADIENT EXTRUDER= [ENABLE=] [METHOD=]`: + Enable/disable the gradient at the given mixing extruder and set the gradient method. - `MIXING_STATUS EXTRUDER=`: Returns the configuration and status of the given mixing extruder. -- `G1 [X] [Y] [Z] [E::...:] [F]`: - This extended G1 command is available if a mixing extruder is active. - The pos1, ... posn parameters define the amounts to extrude for the - extruders defined in the - [mixingextruder config section](Config_Reference.md#mixingextruder). - If less weights than - extruders are provided the remaining weights are - assumed 0.0, additional beyond the available - extruders are ignored. - Additionally it also sets the mixing for that extruder for subsequent - standard G1 commands, eg. "G1 ... E1:2:7 G1 ... E1" first extrudes - 1mm, 2mm and 7mm with the respective extruders and then 0.1mm, 0.2mm and - 0.7mm with the second G1. ### Extruder stepper Commands diff --git a/klippy/extras/mixingextruder.py b/klippy/extras/mixingextruder.py index 9e97e15a084c..5a666280ab8a 100644 --- a/klippy/extras/mixingextruder.py +++ b/klippy/extras/mixingextruder.py @@ -6,8 +6,6 @@ import math import logging -from gcode import GCodeCommand - def extruder_to_idx(name, active=None): name = name.lower() @@ -63,22 +61,23 @@ def __init__(self, config, idx, parent=None): config.get('extruders', '').split(",") if len(e)] if not len(self.extruder_names): - raise self._mcu.get_printer().config_error( + raise self.printer.config_error( "No extruders configured for mixing") - self.extended_g1 = config.get('extended_g1', 'false').lower() == 'true' self.extruders = parent.extruders if parent else [] self.mixing_extruders = parent.mixing_extruders if parent else {} self.mixing_extruders[idx] = self self.mixing = self._init_mixings(idx, len(self.extruder_names)) self.commanded_pos = 0 - self.positions = [0. for p in range(len(self.extruder_names)) - ] if idx == 0 else self.mixing_extruders[0].positions + self.positions = parent.positions if parent else \ + [0. for p in range(len(self.extruder_names))] self.ratios = [0 for p in range(len(self.extruder_names))] self.current_mixing = tuple(self.ratios) self.gradient_enabled = False # assumed to be sorted list of ((start, middle, end), (ref1, ref2)) self.gradients = [] self.gradient_method = 'linear' + self.printer.register_event_handler("klippy:connect", + self.handle_connect) logging.info("MixingExtruder %d extruders=%s", idx, ",".join(self.extruder_names), ",".join("%.1f" % (x) for x in self.mixing)) @@ -87,11 +86,12 @@ def __init__(self, config, idx, parent=None): gcode.register_mux_command("ACTIVATE_EXTRUDER", "EXTRUDER", self.name, self.cmd_ACTIVATE_EXTRUDER, desc=self.cmd_ACTIVATE_EXTRUDER_help) - gcode.register_mux_command("SET_MIXING_EXTRUDER", "EXTRUDER", - self.name, self.cmd_SET_MIXING_EXTRUDER, + if not idx: + gcode.register_command("SET_MIXING_EXTRUDER", + self.cmd_SET_MIXING_EXTRUDER, desc=self.cmd_SET_MIXING_EXTRUDER_help) - gcode.register_mux_command("SAVE_MIXING_EXTRUDERS", "EXTRUDER", - self.name, self.cmd_SAVE_MIXING_EXTRUDERS, + gcode.register_command("SAVE_MIXING_EXTRUDERS", + self.cmd_SAVE_MIXING_EXTRUDERS, desc=self.cmd_SAVE_MIXING_EXTRUDERS_help) gcode.register_mux_command("ADD_MIXING_GRADIENT", "EXTRUDER", self.name, self.cmd_ADD_MIXING_GRADIENT, @@ -108,51 +108,46 @@ def __init__(self, config, idx, parent=None): def _init_mixings(self, idx, extruders): if idx == 0: - return [1./extruders for p in range(extruders)] - idx = idx-1 + return [1. / extruders for p in range(extruders)] + idx = idx - 1 if idx < extruders: return [1. if p == idx else 0. for p in range(extruders)] - idx = idx-extruders + idx = idx - extruders if idx < extruders: - return [0. if p == idx else 1./(extruders-1) + return [0. if p == idx else 1. / (extruders - 1) for p in range(extruders)] - idx = idx-extruders + idx = idx - extruders if extruders == 3: - if idx < 2*extruders: - return [[0. if p == x else (1+((x+y) % 2))/3. + if idx < 2 * extruders: + return [[0. if p == x else (1 + (((p - x) % 3 + y) % 2)) / 3. for p in range(extruders)] - for x in range(extruders) for y in (1, 2)][idx] - idx = idx-2*extruders + for x in range(extruders) for y in (0, 1)][idx] + idx = idx - 2 * extruders elif extruders > 3: - if idx < (extruders*(extruders-1)/2): - return [[0. if p == x or p == y else 1./(extruders-2) + if idx < (extruders * (extruders - 1) / 2): + return [[0. if p == x or p == y else 1. / (extruders - 2) for p in range(extruders)] for x in range(extruders) - for y in range(x+1, extruders)][idx] - idx = idx-(extruders*(extruders-1)/2) - return [1./extruders for p in range(extruders)] + for y in range(x + 1, extruders)][idx] + idx = idx - (extruders * (extruders - 1) / 2) + return [1. / extruders for p in range(extruders)] - def _activate(self): + def handle_connect(self): if self.activated: return self.activated = True + extruders = self.printer.lookup_object("extruders", None) if self.mixing_extruders[0] != self: + extruders.register_extruder(self.name, self) return try: - self.extruders = [self.printer.lookup_object(extruder) - for extruder in self.extruder_names] + self.extruders.extend(self.printer.lookup_object(extruder) + for extruder in self.extruder_names) + extruders.register_extruder(self.name, self) except Exception as e: - self.extruders = [] + self.extruders.clear() logging.error("no extruders found: %s" % (", ".join(self.extruder_names)), e) - gcode = self.printer.lookup_object('gcode') - # Register commands - if self.extended_g1: - for cmd in ("G1", "G2", "G3"): - o = gcode.register_command(cmd, None) - if o: - gcode.register_command(cmd, - lambda g: self.cmd_G(cmd, o, g)) def update_move_time(self, flush_time): for extruder in self.extruders: @@ -162,6 +157,7 @@ def calc_junction(self, prev_move, move): diff_r = move.axes_r[3] - prev_move.axes_r[3] if diff_r: m = max(self.mixing) + # TODO: don't use extruder property instant_corner_v return (self.extruders[0].instant_corner_v / abs(m * diff_r))**2 return move.max_cruise_v2 @@ -169,7 +165,8 @@ def _scale_move(self, move, idx, weights): mixing = weights[idx] if not mixing: return None - return MixingMove(move.start_pos[0], move.start_pos[1], + # TODO: don't use move properties + move = MixingMove(move.start_pos[0], move.start_pos[1], move.start_pos[2], self.positions[idx], move.axes_d[0], move.axes_d[1], move.axes_d[2], mixing * move.axes_d[3], @@ -217,6 +214,7 @@ def _check_move(self, scaled_move, move): "See the 'max_extrude_cross_section' config option for details" % (area, self.extruders[0].max_extrude_ratio * self.extruders[0].filament_area)) + return move def _get_gradient(self, start_pos, end_pos): default = self.mixing @@ -239,7 +237,7 @@ def _get_gradient(self, start_pos, end_pos): return list(((1. - w) * s + w * e) for s, e in zip(start_mix, end_mix)) if self.gradient_method == 'spherical': - pos = [(x+y)/2. for x, y in zip(start_pos, end_pos)] + pos = [(x + y) / 2. for x, y in zip(start_pos, end_pos)] dist = math.sqrt(sum(x**2 for x in pos)) if dist <= start: return start_mix @@ -247,24 +245,21 @@ def _get_gradient(self, start_pos, end_pos): default = end_mix continue w = (dist - start) / (end - start) - logging.info("spherical gradient @%.1f(%.1f-%.1f) [%s-%s]" % - (zpos, start, end, + mix = list(((1. - w) * s + w * e) + for s, e in zip(start_mix, end_mix)) + logging.info("spherical gradient @%.1f(%.1f-%.1f) [%s-%s]=%s" % + (dist, start, end, "/".join("%.1f" % x for x in start_mix), - "/".join("%.1f" % x for x in end_mix))) - return list(((1. - w) * s + w * e) - for s, e in zip(start_mix, end_mix)) + "/".join("%.1f" % x for x in end_mix), + "/".join("%.1f" % x for x in mix))) + return mix return default def check_move(self, move): - mixing = self.mixing if not self.gradient_enabled \ - else self._get_gradient(move.start_pos[:3], move.end_pos[:3]) - for idx, extruder in enumerate(self.extruders): - scaled_move = self._scale_move(move, idx, mixing) - if scaled_move: - self._check_move(scaled_move, move) + self.extruders[0].check_move(move) def move(self, print_time, move): - mixing = self.mixing if self.gradient_enabled \ + mixing = self.mixing if not self.gradient_enabled \ else self._get_gradient(move.start_pos[:3], move.end_pos[:3]) self.current_mixing = tuple(mixing) for idx, extruder in enumerate(self.extruders): @@ -275,7 +270,6 @@ def move(self, print_time, move): self.commanded_pos = move.end_pos[3] def get_status(self, eventtime): - status = {} status = dict(mixing=",".join("%0.1f%%" % (m * 100.) for m in self.mixing), current=",".join("%0.1f%%" % (m * 100.) @@ -283,8 +277,8 @@ def get_status(self, eventtime): positions=",".join("%0.2fmm" % (p) for p in self.positions), ticks=",".join("%0.2f" % ( - extruder.stepper.get_mcu_position()) - for extruder in self.extruders), + extruder.stepper.get_mcu_position()) + for extruder in self.extruders), extruders=",".join(extruder.name for extruder in self.extruders)) for i, gradient in enumerate(self.gradients): @@ -326,32 +320,9 @@ def stats(self, eventtime): return False, self.name + ": mixing=%s" % ( ",".join("%0.2f" % (m) for m in self.current_mixing)) - def cmd_G(self, name, orig, gcmd): - weighting = gcmd.get('E', None) - if not weighting or ":" not in weighting: - return orig(gcmd) - toolhead = self.printer.lookup_object('toolhead') - extruder = toolhead.get_extruder() - if not extruder.get_name().startswith("mixingextruder"): - raise gcmd.error("No mixing extruder active") - gcode = self.printer.lookup_object('gcode') - weights = [float(w) - for i, w in enumerate(weighting.split(":")) - if i < len(self.extruders)] - extrude = sum(weights) - if min(weights) < 0: - raise gcmd.error("Invalid extrude: %s" % (weighting)) - for i, w in enumerate(weights): - extruder.mixing[i] = w/extrude - orig(GCodeCommand( - gcode, name, gcmd.get_commandline(), - dict(gcmd.get_command_parameters(), E="%f" % (extrude)), - gcmd._need_ack)) - cmd_SET_MIXING_EXTRUDER_help = "Set scale on motor/extruder" def cmd_SET_MIXING_EXTRUDER(self, gcmd): - self._activate() extruder = gcmd.get('MIXING_MOTOR') scale = gcmd.get_float('SCALE', minval=0.) if extruder not in self.extruder_names: @@ -368,7 +339,6 @@ def cmd_SET_MIXING_EXTRUDER(self, gcmd): cmd_SAVE_MIXING_EXTRUDERS_help = "Save the scales on motors" def cmd_SAVE_MIXING_EXTRUDERS(self, gcmd): - self._activate() mixingextruder = self extruder = gcmd.get('MIXING_EXTRUDER', None) if extruder: @@ -379,12 +349,15 @@ def cmd_SAVE_MIXING_EXTRUDERS(self, gcmd): if s <= 0: raise gcmd.error("Could not save ratio: its empty") for i, v in enumerate(self.ratios): - mixingextruder.mixing[i] = v/s + mixingextruder.mixing[i] = v / s self.ratios[i] = 0.0 cmd_SET_MIXING_GRADIENT_help = "Turn no/off grdient mixing" def cmd_SET_MIXING_GRADIENT(self, gcmd): + method = gcmd.get('METHOD') + if method in ["linear", "spherical"]: + self.gradient_method = method try: enable = gcmd.get_int('ENABLE', 1, minval=0, maxval=1) self.gradient_enabled = enable == 1 @@ -433,7 +406,6 @@ def cmd_RESET_MIXING_GRADIENT(self, gcmd): cmd_ACTIVATE_EXTRUDER_help = "Change the active extruder" def cmd_ACTIVATE_EXTRUDER(self, gcmd): - self._activate() toolhead = self.printer.lookup_object('toolhead') if toolhead.get_extruder() is self: gcmd.respond_info("Extruder %s already active" % (self.name,)) @@ -447,7 +419,6 @@ def cmd_ACTIVATE_EXTRUDER(self, gcmd): cmd_MIXING_STATUS_help = "Display the status of the given MixingExtruder" def cmd_MIXING_STATUS(self, gcmd): - self._activate() eventtime = self.printer.get_reactor().monotonic() status = self.get_status(eventtime) gcmd.respond_info(", ".join("%s=%s" % (k, v) From 16224e7161d46a5e6d3793e704030b6a00de1dac Mon Sep 17 00:00:00 2001 From: Peter Gruber Date: Tue, 16 Mar 2021 08:55:30 +0100 Subject: [PATCH 10/17] extruder: improve shared heater handling Signed-off-by: Peter Gruber --- klippy/kinematics/extruder.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 890e0a442483..f4da5a5cbd30 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -160,7 +160,8 @@ def cmd_M104(self, gcmd, wait=False): # Set Extruder Temperature temp = gcmd.get_float('S', 0.) index = gcmd.get_int('T', None, minval=0) - current_extruder = self.printer.lookup_object('toolhead').get_extruder() + current_extruder = \ + self.printer.lookup_object('toolhead').get_extruder() if index is not None: extruders = self.printer.lookup_object("extruders", None) logging.info("extruders", extruders.extruder_names) @@ -171,7 +172,8 @@ def cmd_M104(self, gcmd, wait=False): if temp <= 0.: return raise gcmd.error("Extruder not configured") - if current_extruder.get_heater() == extruder.get_heater(): + if current_extruder != extruder and \ + current_extruder.get_heater() == extruder.get_heater(): gcmd.respond_info("not changing temperature of current heater") return else: From 7ac21d98ba85a198f1fb63d013febb0ba04a3ac9 Mon Sep 17 00:00:00 2001 From: Maciej Kurc Date: Sat, 19 Jun 2021 21:58:13 +0200 Subject: [PATCH 11/17] Fixed bugs preventing mixing extruders from working Signed-off-by: Maciej Kurc --- klippy/extras/mixingextruder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/klippy/extras/mixingextruder.py b/klippy/extras/mixingextruder.py index 5a666280ab8a..a1e48aae5e07 100644 --- a/klippy/extras/mixingextruder.py +++ b/klippy/extras/mixingextruder.py @@ -78,7 +78,7 @@ def __init__(self, config, idx, parent=None): self.gradient_method = 'linear' self.printer.register_event_handler("klippy:connect", self.handle_connect) - logging.info("MixingExtruder %d extruders=%s", idx, + logging.info("MixingExtruder %d extruders=%s weights=%s", idx, ",".join(self.extruder_names), ",".join("%.1f" % (x) for x in self.mixing)) # Register commands @@ -181,6 +181,7 @@ def _scale_move(self, move, idx, weights): move.cruise_t if hasattr( move, "cruise_t") else move.min_move_t, move.decel_t if hasattr(move, "decel_t") else 0.) + return move def _check_move(self, scaled_move, move): axis_r = scaled_move.axes_r[3] From c76c3f53e52d1898f05a254c651477bf7f32da22 Mon Sep 17 00:00:00 2001 From: Maciej Kurc Date: Thu, 1 Jul 2021 14:36:16 +0200 Subject: [PATCH 12/17] Allowed chainging heater temperature of a not currently selected mixing extruder Signed-off-by: Maciej Kurc --- klippy/kinematics/extruder.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index f4da5a5cbd30..6d038a89c41c 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -172,10 +172,10 @@ def cmd_M104(self, gcmd, wait=False): if temp <= 0.: return raise gcmd.error("Extruder not configured") - if current_extruder != extruder and \ - current_extruder.get_heater() == extruder.get_heater(): - gcmd.respond_info("not changing temperature of current heater") - return +# if current_extruder != extruder and \ +# current_extruder.get_heater() == extruder.get_heater(): +# gcmd.respond_info("not changing temperature of current heater") +# return else: extruder = self.printer.lookup_object('toolhead').get_extruder() pheaters = self.printer.lookup_object('heaters') From ffbac77be7cd7e33b0327cec4e25de3a6f5012c8 Mon Sep 17 00:00:00 2001 From: Maciej Kurc Date: Thu, 1 Jul 2021 14:43:41 +0200 Subject: [PATCH 13/17] A crude automatic retraction handling for a mixing extruder. Works well. Signed-off-by: Maciej Kurc --- klippy/extras/mixingextruder.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/klippy/extras/mixingextruder.py b/klippy/extras/mixingextruder.py index a1e48aae5e07..9b25eeedd5e9 100644 --- a/klippy/extras/mixingextruder.py +++ b/klippy/extras/mixingextruder.py @@ -76,6 +76,7 @@ def __init__(self, config, idx, parent=None): # assumed to be sorted list of ((start, middle, end), (ref1, ref2)) self.gradients = [] self.gradient_method = 'linear' + self.retracted = 0.0 self.printer.register_event_handler("klippy:connect", self.handle_connect) logging.info("MixingExtruder %d extruders=%s weights=%s", idx, @@ -260,8 +261,24 @@ def check_move(self, move): self.extruders[0].check_move(move) def move(self, print_time, move): + # Track extrusion to handle retractions. + # FIXME: May need to split the move! + delta_e = move.end_pos[3] - move.start_pos[3] + if delta_e < 0.0: + if self.retracted == 0.0: + logging.debug("%s is retracting / unretracting" % self.name) + retracting = True + self.retracted += -delta_e + else: + retracting = self.retracted > 0.0 + self.retracted -= delta_e + self.retracted = max(0.0, self.retracted) + if self.retracted == 0.0: + logging.debug("%s is mixing" % self.name) mixing = self.mixing if not self.gradient_enabled \ else self._get_gradient(move.start_pos[:3], move.end_pos[:3]) + if retracting: + mixing = [1. / len(self.extruders) for p in range(len(self.extruders))] self.current_mixing = tuple(mixing) for idx, extruder in enumerate(self.extruders): scaled_move = self._scale_move(move, idx, mixing) From 3233ea19d49d37f5d88732b6de534590b090d02b Mon Sep 17 00:00:00 2001 From: Maciej Kurc Date: Sat, 7 Aug 2021 19:57:56 +0200 Subject: [PATCH 14/17] Added documentation for some mixing extruder nuances. Signed-off-by: Maciej Kurc --- docs/Mixing_Extruder.md | 95 +++++++++++++++++++++++++++++++++++++++++ docs/Overview.md | 1 + 2 files changed, 96 insertions(+) create mode 100644 docs/Mixing_Extruder.md diff --git a/docs/Mixing_Extruder.md b/docs/Mixing_Extruder.md new file mode 100644 index 000000000000..3f03196536fe --- /dev/null +++ b/docs/Mixing_Extruder.md @@ -0,0 +1,95 @@ +# Mixing extruder + +This document describes how to configure and use a filament mixing extruder. +Please also refer to the [config reference](Config_Reference.md) and supported +[G-Codes](G-Codes.md). + +## Configuring a mixing extruder. + +A mixing extruder has N-filament inputs and a single nozzle. This requires +N independent filament drives. They have to be defined in the config as regular +extruders. + +Since they all drive filament to the same hotend they must have the +`shared_heater` property set to point to one of them. For this reason all +heater-related parameters should be set only for that one. + +Following individual filament drivers definitions there should be the +`[mixingextruder]` section which groups all of them to tell Klipper that +they in fact drive the same mixing hotend. + +Here is an example of configuration snippet for 3-to-1 mixing extruder: +``` +# "Alpha" stepper +[extruder] +step_pin: PC7 +dir_pin: !PC8 +enable_pin: !PA18 +microsteps: 16 +rotation_distance: 7.15 +nozzle_diameter: 0.400 +filament_diameter: 1.75 +heater_pin: PC22 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PA6 +control: pid +pid_kp: 37.919 +pid_ki: 0.950 +pid_kd: 378.241 +min_temp: 0 +max_temp: 275 +pressure_advance: 0.1 +min_extrude_temp: 0 + +# "Beta" stepper +[extruder1] +step_pin: PB3 +dir_pin: !PC10 +enable_pin: !PB4 +microsteps: 16 +rotation_distance: 7.15 +nozzle_diameter: 0.400 +filament_diameter: 1.75 +pressure_advance: 0.1 +shared_heater: extruder + +# "Gamma" stepper +[extruder2] +step_pin: PB1 +dir_pin: !PB0 +enable_pin: !PB2 +microsteps: 16 +rotation_distance: 7.15 +nozzle_diameter: 0.400 +filament_diameter: 1.75 +pressure_advance: 0.1 +shared_heater: extruder + +[mixingextruder] +extruders: extruder,extruder1,extruder2 +``` + +Stepper parameters may vary among all the drivers but in most systems they are +identical. + +## Retractions + +For correct operation of a mixing extruder all N input filaments should be +retracted by the same amount regardless of the mixing ratio used. Most +3D printer firmware implement that via firmware retractions. + +The current implementation of retractions in Klipper is different and is based +on automatic tracking of extrusion moves. When Klipper detects a retraction +(which is backward extrusion move) it sets internally the mixing ratio for +each filament driver to 1/N ensuring that all filaments are moved by the same +distance. The retracted distance is accumulated and used to detect the end +of subsequent unretraction move. When that happens the mixing ratio is restored. + +Because retractions happen with mixing ratio of 1/N for each stepper the amount +the filament actually moves and the move speed is also divided by N. This +currently has to be accounted for in a slicer firmware. For example whe one +wants to retract by 3mm with the speed of 50mm/s a 3-to-1 mixing extruder then +the values in the slicer need to be 3 * 3mm = 9mm and 3 * 50mm/s = 150mm/s. + +A mixing extruder should be thought of as if it was driving a "virtual filament" +at the point where it enters the nozzle. diff --git a/docs/Overview.md b/docs/Overview.md index 7d712433e971..03901c512602 100644 --- a/docs/Overview.md +++ b/docs/Overview.md @@ -41,6 +41,7 @@ communication with the Klipper developers. using adxl345 accelerometer hardware to measure resonance. - [Pressure advance](Pressure_Advance.md): Calibrate extruder pressure. +- [Mixing extruder](Mixing_Extruder.md): Information about setting-up and using filament mixing extruders. - [Slicers](Slicers.md): Configure "slicer" software for Klipper. - [Command Templates](Command_Templates.md): G-Code macros and conditional evaluation. From 3dda29f1ea303300124984de88b94845f7c0ace2 Mon Sep 17 00:00:00 2001 From: Maciej Kurc Date: Sat, 7 Aug 2021 20:09:09 +0200 Subject: [PATCH 15/17] Fixed too long line Signed-off-by: Maciej Kurc --- klippy/extras/mixingextruder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/klippy/extras/mixingextruder.py b/klippy/extras/mixingextruder.py index 9b25eeedd5e9..9ca9ad721573 100644 --- a/klippy/extras/mixingextruder.py +++ b/klippy/extras/mixingextruder.py @@ -278,7 +278,8 @@ def move(self, print_time, move): mixing = self.mixing if not self.gradient_enabled \ else self._get_gradient(move.start_pos[:3], move.end_pos[:3]) if retracting: - mixing = [1. / len(self.extruders) for p in range(len(self.extruders))] + mixing = [1. / len(self.extruders) \ + for p in range(len(self.extruders))] self.current_mixing = tuple(mixing) for idx, extruder in enumerate(self.extruders): scaled_move = self._scale_move(move, idx, mixing) From 41be0f3e1c3da51d56d860abe0e281b620da8724 Mon Sep 17 00:00:00 2001 From: Maciej Kurc Date: Tue, 10 Aug 2021 09:04:07 +0200 Subject: [PATCH 16/17] Removed providing a Python function object as a part of mixing extruder status Signed-off-by: Maciej Kurc --- config/sample-mixing-extruder.cfg | 3 ++- klippy/extras/mixingextruder.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/config/sample-mixing-extruder.cfg b/config/sample-mixing-extruder.cfg index 0b19b0d5b5dc..9b6bcf992baf 100644 --- a/config/sample-mixing-extruder.cfg +++ b/config/sample-mixing-extruder.cfg @@ -36,7 +36,8 @@ default_parameter_Z: -1 default_parameter_I: -1 default_parameter_J: -1 gcode: - {% set extruder = printer['mixingextruder'].find_mixing_extruder(T) %} + # Assume there is only one mixing extruder and it is named "mixingextruder" + {% set extruder = 'mixingextruder' %} {% if extruder >= 0 %} {% if params.A|float >= 0 and params.Z|float >= 0 and params.I|int >= 0 and params.J|int >= 0 %} ADD_MIXING_GRADIENT EXTRUDER={extruder} START_HEIGHT={A} END_HEIGHT={Z} START={I} END={J} diff --git a/klippy/extras/mixingextruder.py b/klippy/extras/mixingextruder.py index 9ca9ad721573..5983c62018e4 100644 --- a/klippy/extras/mixingextruder.py +++ b/klippy/extras/mixingextruder.py @@ -312,8 +312,6 @@ def get_status(self, eventtime): method=self.gradient_method, enabled=str(self.gradient_enabled)).items())}) active = self._active_extruder() - status['find_mixing_extruder'] = \ - lambda name: find_mixing_extruder(name, active) return status def _reset_positions(self): From 79f8d912157db05349dd1517ca0f3048c87f8f1a Mon Sep 17 00:00:00 2001 From: Maciej Kurc Date: Tue, 10 Aug 2021 20:25:20 +0200 Subject: [PATCH 17/17] Removed the unsupported "extended_g1" option from the mixing extruder example config Signed-off-by: Maciej Kurc --- config/sample-mixing-extruder.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/sample-mixing-extruder.cfg b/config/sample-mixing-extruder.cfg index 9b6bcf992baf..4668c2ce2abf 100644 --- a/config/sample-mixing-extruder.cfg +++ b/config/sample-mixing-extruder.cfg @@ -6,8 +6,6 @@ [mixingextruder] extruders: extruder,extruder1,extruder2 -extended_g1: true - [gcode_macro M163] gcode: