And an example of the fast tool swaps that become possible with this code (FWIW, RatOS also supports similar swap, but through quite a bit more complex compound motion, involving partial COPY/MIRROR mode moves):
And the resulting cube (the print time of the cube itself without bed and initial tool heating was ~8 minutes):
In case you want to reproduce it on your IDEX setup and test it, here’s how I did it. Note that it is fairly complex (including a need to override G1 command), but this is primarily because of lack of a proper slicer support (e.g. inability to get the next GCode position in slicing). Also the code not only swaps the tools, but also gradually cools down inactive tool to prevent the filament from cooking there, and as a result the tool swapping code supports both fast swaps (when the other tool is not cooled down too much yet) and slow code swap (waiting the other tool to heat up).
Slicer GCodes and custom Klipper macros
Slicer custom G-Codes (for PrusaSlicer, tested on 2.9.2), only relevant excerpts:
Tool Change GCode
{if previous_extruder >= 0 }
M104 S{first_layer_temperature[previous_extruder]} T{previous_extruder}
DELAY_HEATER_OFF T={previous_extruder} LT={first_layer_temperature[previous_extruder]*0.75}
{endif}
{if previous_extruder >= 0 or layer_num > 0}
SET_HEATER_TEMPERATURE HEATER=extruder{if next_extruder > 0}{next_extruder}{endif} TARGET={if layer_num > 0}{temperature[next_extruder]}{else}{first_layer_temperature[next_extruder]}{endif}
INITIATE_TOOL_SWAP NEXT_TOOL=[next_extruder] MIN_TEMP={first_layer_temperature[next_extruder]*0.99}
{endif}
Below slicer GCodes may or may not be necessary depending on your slicer configuration settings. They primarily prevent unnecessary heat-up and cooking of inactive tool.
Start GCode:
{if is_extruder_used[1-current_extruder]}
DELAY_HEATER_OFF T={1-current_extruder} LT={first_layer_temperature[1-current_extruder]*0.75}
UPDATE_DELAYED_GCODE ID=DELAYED_HEATER_OFF DURATION=1
{endif}
End GCode:
{if is_extruder_used[1-current_extruder]}
DELAY_HEATER_OFF T={1-current_extruder} LT=0
UPDATE_DELAYED_GCODE ID=DELAYED_HEATER_OFF DURATION=1
{endif}
Layer change GCode:
{if layer_num == 1 and is_extruder_used[1-current_extruder]}
DELAY_HEATER_OFF T={1-current_extruder} LT={first_layer_temperature[1-current_extruder]*0.75}
UPDATE_DELAYED_GCODE ID=DELAYED_HEATER_OFF DURATION=5
{endif}
Now the relevant Klipper macros:
[gcode_macro MOVE_TO_MAX_X]
variable_x_offset: 0.5
gcode:
G1 X{printer.toolhead.axis_maximum.x - printer["gcode_macro MOVE_TO_MAX_X"].x_offset} F60000
[gcode_macro MOVE_TO_MIN_X]
variable_x_offset: 0.5
gcode:
G1 X{printer.toolhead.axis_minimum.x + printer["gcode_macro MOVE_TO_MIN_X"].x_offset} F60000
[gcode_macro INITIATE_TOOL_SWAP]
variable_next_tool: -1
variable_swap_speed: 1000.0
variable_swap_accel: 10000.0
variable_z_hop: 0.4
variable_restore_accel: 0
gcode:
{% if 'NEXT_TOOL' not in params %}
{action_raise_error("Must specify NEXT_TOOL parameter")}
{% endif %}
{% set next_tool = params.NEXT_TOOL|int %}
{% if next_tool not in (0, 1) %}
{action_raise_error("NEXT_TOOL must be 0 or 1")}
{% endif %}
{% if next_tool == 0 %}
{% set extruder = 'extruder' %}
{% else %}
{% set extruder = 'extruder1' %}
{% endif %}
{% set vars = printer["gcode_macro INITIATE_TOOL_SWAP"] %}
{% set accel = params.ACCEL|default(vars.swap_accel)|float %}
SAVE_GCODE_STATE NAME=tool_change
{% if accel > 0 %}
M204 S{accel}
{% endif %}
{% set min_temp = params.MIN_TEMP|default(0)|float %}
{% if printer[extruder].temperature < min_temp %}
; Slow path
G91
G0 Z{vars.z_hop} F600
T{next_tool}
TEMPERATURE_WAIT SENSOR={extruder} MINIMUM={min_temp}
M83
G0 E10 F600
G0 E-1 F1200
WIPE_EXTRUDER T={next_tool}
RESTORE_GCODE_STATE NAME=tool_change MOVE=0
{% endif %}
T{next_tool} P0 ; Switch tool but do not park the other one
SAVE_DUAL_CARRIAGE_STATE NAME=tool_change
SET_GCODE_VARIABLE MACRO=INITIATE_TOOL_SWAP VARIABLE=next_tool VALUE={next_tool}
SET_GCODE_VARIABLE MACRO=INITIATE_TOOL_SWAP VARIABLE=restore_accel VALUE={printer.toolhead.max_accel}
SET_DUAL_CARRIAGE CARRIAGE=u MODE=DIRECT
[gcode_macro G1]
rename_existing: G1.1
gcode:
{% set vars = printer["gcode_macro INITIATE_TOOL_SWAP"] %}
{% set t1_vars = printer["gcode_macro T1"] %}
{% if vars.next_tool >= 0 and ('X' in params or 'Y' in params) %}
{% set z_pos = printer.gcode_move.gcode_position.z+vars.z_hop %}
SET_GCODE_OFFSET X=0 Y=0 U=0
G90
{% if vars.next_tool == 0 %}
{% set x_pos = params.X|default(printer.gcode_move.position.x+t1_vars.tool_x_offset)|float %}
{% set y_pos = params.Y|default(printer.gcode_move.position.y+t1_vars.tool_y_offset)|float %}
{% set park_pos = printer.toolhead.axis_maximum.u - printer["gcode_macro MOVE_TO_MAX_X"].x_offset} %}
G1.1 X{x_pos} Y{y_pos} U{park_pos} Z{z_pos} F{vars.swap_speed*60.0}
RESTORE_DUAL_CARRIAGE_STATE NAME=tool_change MOVE=0
RESTORE_GCODE_STATE NAME=tool_change MOVE=0
SET_GCODE_OFFSET X=0 Y=0
{% else %}
{% set x_pos = params.X|default(printer.gcode_move.position.u)|float %}
{% set y_pos = params.Y|default(printer.gcode_move.position.y)|float %}
{% set park_pos = printer.toolhead.axis_minimum.x + printer["gcode_macro MOVE_TO_MIN_X"].x_offset %}
G1.1 U{x_pos-t1_vars.tool_x_offset} Y{y_pos-t1_vars.tool_y_offset} X{park_pos} Z{z_pos} F{vars.swap_speed*60.0}
RESTORE_DUAL_CARRIAGE_STATE NAME=tool_change MOVE=0
RESTORE_GCODE_STATE NAME=tool_change MOVE=0
SET_GCODE_OFFSET X={0.0-(t1_vars.tool_x_offset|float)} Y={0.0-(t1_vars.tool_y_offset|float)}
{% endif %}
{% if vars.restore_accel > 0 %}
M204 S{vars.restore_accel}
SET_GCODE_VARIABLE MACRO=INITIATE_TOOL_SWAP VARIABLE=restore_accel VALUE=0
{% endif %}
SET_GCODE_VARIABLE MACRO=INITIATE_TOOL_SWAP VARIABLE=next_tool VALUE=-1
{% endif %}
G1.1 {rawparams}
[gcode_macro G0]
rename_existing: G0.1
gcode:
G1 {rawparams}
[gcode_macro ACTIVE_TOOL]
variable_tool: 0
gcode:
[gcode_macro T0]
gcode:
{% set active_tool = printer["gcode_macro ACTIVE_TOOL"].tool %}
{% if active_tool != 0 %}
SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder1
ACTIVATE_EXTRUDER EXTRUDER=extruder
{% if (params.P|default(1)|int) %}
PARK_EXTRUDER1
{% endif %}
SET_DUAL_CARRIAGE CARRIAGE=x
SET_GCODE_OFFSET X=0 Y=0
SET_GCODE_VARIABLE MACRO=ACTIVE_TOOL VARIABLE=tool VALUE=0
{% endif %}
[gcode_macro T1]
variable_tool_x_offset: 0.05
variable_tool_y_offset: -0.8
gcode:
{% set active_tool = printer["gcode_macro ACTIVE_TOOL"].tool %}
{% set vars = printer["gcode_macro T1"] %}
{% if active_tool != 1 %}
SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder1
ACTIVATE_EXTRUDER EXTRUDER=extruder1
{% if (params.P|default(1)|int) %}
PARK_EXTRUDER
{% endif %}
SET_DUAL_CARRIAGE CARRIAGE=u
SET_GCODE_OFFSET X={0.0-(vars.tool_x_offset|float)} Y={0.0-(vars.tool_y_offset|float)}
SET_GCODE_VARIABLE MACRO=ACTIVE_TOOL VARIABLE=tool VALUE=1
{% endif %}
[delayed_gcode DELAYED_HEATER_OFF]
gcode:
{% set vars = printer["gcode_macro DELAY_HEATER_OFF"] %}
{% if vars.tool >= 0 %}
{% if vars.tool > 0 %}
{% set heater = "extruder" + vars.tool|string %}
{% else %}
{% set heater = "extruder" %}
{% endif %}
{% if vars.turn_off or vars.low_temp <= 0 %}
SET_HEATER_TEMPERATURE HEATER={heater} TARGET=0
{% else %}
SET_HEATER_TEMPERATURE HEATER={heater} TARGET={vars.low_temp}
SET_GCODE_VARIABLE MACRO=DELAY_HEATER_OFF VARIABLE=turn_off VALUE=True
UPDATE_DELAYED_GCODE ID=DELAYED_HEATER_OFF DURATION={vars.heater_off_timeout}
{% endif %}
{% endif %}
[gcode_macro DELAY_HEATER_OFF]
variable_low_temp_timeout: 60
variable_heater_off_timeout: 300
variable_tool: -1
variable_low_temp: 0
variable_turn_off: False
gcode:
{% set LT = params.LT|default(0)|float %}
{% if 'T' not in params %}
{action_raise_error("Must specify T parameter")}
{% endif %}
{% set T = params.T|int %}
SET_GCODE_VARIABLE MACRO=DELAY_HEATER_OFF VARIABLE=tool VALUE={T}
SET_GCODE_VARIABLE MACRO=DELAY_HEATER_OFF VARIABLE=low_temp VALUE={LT}
SET_GCODE_VARIABLE MACRO=DELAY_HEATER_OFF VARIABLE=turn_off VALUE=False
UPDATE_DELAYED_GCODE ID=DELAYED_HEATER_OFF DURATION={printer["gcode_macro DELAY_HEATER_OFF"].low_temp_timeout}
[gcode_macro PARK_EXTRUDER]
gcode:
SAVE_GCODE_STATE NAME=park0
SET_DUAL_CARRIAGE CARRIAGE=x
SET_GCODE_OFFSET X=0
G90
MOVE_TO_MIN_X
RESTORE_GCODE_STATE NAME=park0
[gcode_macro PARK_EXTRUDER1]
gcode:
SAVE_GCODE_STATE NAME=park1
SET_DUAL_CARRIAGE CARRIAGE=u
SET_GCODE_OFFSET X=0
G90
MOVE_TO_MAX_X
RESTORE_GCODE_STATE NAME=park1
[gcode_macro WIPE_EXTRUDER]
variable_wipe_length: 25
gcode:
{% if 'T' not in params %}
{action_raise_error("Must specify T parameter")}
{% endif %}
{% set T = params.T|int %}
SAVE_GCODE_STATE NAME=wipe_tool
SAVE_DUAL_CARRIAGE_STATE NAME=wipe_tool
{% if T <= 1 %}
SET_DUAL_CARRIAGE CARRIAGE={"xu"[T]}
{% else %}
SET_DUAL_CARRIAGE CARRIAGE=u MODE=MIRROR
{% endif %}
SET_GCODE_OFFSET X=0
G90
{% if T == 1 %}
MOVE_TO_MAX_X
G91
G1 X-{printer["gcode_macro WIPE_EXTRUDER"].wipe_length}
G1 X{printer["gcode_macro WIPE_EXTRUDER"].wipe_length}
{% else %}
MOVE_TO_MIN_X
G91
G1 X{printer["gcode_macro WIPE_EXTRUDER"].wipe_length}
G1 X-{printer["gcode_macro WIPE_EXTRUDER"].wipe_length}
{% endif %}
RESTORE_DUAL_CARRIAGE_STATE NAME=wipe_tool MOVE=0
RESTORE_GCODE_STATE NAME=wipe_tool MOVE=0
Note that due to the way the nozzle wipers are organized on my printer, I need only minimal wiping routine, and on fast tool swaps just the swap itself suffices:
However, if you have different wiping mechanism (e.g. in a dedicated place), you will need to adjust the tool swap macros accordingly. However, even in this case you could benefit from the simultaneous carriage control, e.g. by simultaneously parking one tool and moving the other tool to the wiping location.



