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/config/sample-mixing-extruder.cfg b/config/sample-mixing-extruder.cfg new file mode 100644 index 000000000000..4668c2ce2abf --- /dev/null +++ b/config/sample-mixing-extruder.cfg @@ -0,0 +1,60 @@ +# 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 + +[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: + # 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} + 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 5c26a6a56a1e..1f38dee1063b 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. 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..b83748b1f9cf 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -325,6 +325,44 @@ 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=`: This command activates + the specified mixing extruder. Subsequent G1 commands use the mixing + defined for that mixing extruder. +- `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 + 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 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 + 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=] [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. + ### Extruder stepper Commands The following command is available when an 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. diff --git a/klippy/extras/mixingextruder.py b/klippy/extras/mixingextruder.py new file mode 100644 index 000000000000..5983c62018e4 --- /dev/null +++ b/klippy/extras/mixingextruder.py @@ -0,0 +1,453 @@ +# 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 +import logging + + +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, + 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, parent=None): + self.printer = config.get_printer() + self.activated = False + self.name = idx_to_extruder(idx) + self.extruder_names = [e.strip() + for e in + config.get('extruders', '').split(",") + if len(e)] + if not len(self.extruder_names): + raise self.printer.config_error( + "No extruders configured for mixing") + 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 = 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.retracted = 0.0 + self.printer.register_event_handler("klippy:connect", + self.handle_connect) + logging.info("MixingExtruder %d extruders=%s weights=%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) + if not idx: + gcode.register_command("SET_MIXING_EXTRUDER", + self.cmd_SET_MIXING_EXTRUDER, + desc=self.cmd_SET_MIXING_EXTRUDER_help) + 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, + 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 + (((p - x) % 3 + y) % 2)) / 3. + for p in range(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) + 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 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.extend(self.printer.lookup_object(extruder) + for extruder in self.extruder_names) + extruders.register_extruder(self.name, self) + except Exception as e: + self.extruders.clear() + logging.error("no extruders found: %s" % + (", ".join(self.extruder_names)), e) + + 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) + # 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 + + def _scale_move(self, move, idx, weights): + mixing = weights[idx] + if not mixing: + return None + # 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], + 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.) + return move + + def _check_move(self, scaled_move, move): + axis_r = scaled_move.axes_r[3] + axis_d = scaled_move.axes_d[3] + 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.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.extruders[0].max_e_dist)) + inv_extrude_r = 1. / abs(axis_r) + 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.extruders[0].filament_area + logging.debug("Overextrude: %s vs %s (area=%.3f dist=%.3f)", + 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.extruders[0].max_extrude_ratio + * self.extruders[0].filament_area)) + return move + + 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) + 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), + "/".join("%.1f" % x for x in mix))) + return mix + return default + + 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) + 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): + 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 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() + return status + + 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.extruders[0].get_heater() + + def stats(self, eventtime): + if self.name == 'mixingextruder': + 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.current_mixing)) + return False, self.name + ": mixing=%s" % ( + ",".join("%0.2f" % (m) for m in self.current_mixing)) + + cmd_SET_MIXING_EXTRUDER_help = "Set scale on motor/extruder" + + def cmd_SET_MIXING_EXTRUDER(self, gcmd): + 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_SAVE_MIXING_EXTRUDERS(self, gcmd): + mixingextruder = self + 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") + for i, v in enumerate(self.ratios): + 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 + 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') + return toolhead.get_extruder().get_name().lower() + + def _to_idx(self, name): + return extruder_to_idx(name, active=self._active_extruder) + + 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" + + def cmd_ACTIVATE_EXTRUDER(self, gcmd): + 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): + 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): + mixingextruder = None + for i in range(16): + pe = MixingExtruder(config.getsection('mixingextruder'), + i, parent=mixingextruder) + if i == 0: + mixingextruder = pe + logging.info("Started mixingextruder") + return mixingextruder diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 6775c5de061c..6d038a89c41c 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -160,15 +160,22 @@ 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 != 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') @@ -235,8 +242,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 +266,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)