Support of mapping dual_carriage(s) and extruders to custom GCode axes

Hi,

I have been working recently on a code to permit a user to map dual_carriage(s) to the GCode axes with generic_cartesian kinematics, and also allow mapping the extruders to custom axes besides E. The code now is in a working state (can be found here), and I wanted to raise awareness of this feature and invite for testing. So, how can it be used? Let’s assume you have

[dual_carriage u]
primary_carriage: x
...

and optionally

[dual_carriage v]
primary_carriage: y
...

Now, the command like

SET_DUAL_CARRIAGE CARRIAGE=u MODE=DIRECT

is supported, which adds GCode axis U, and now the carriage u can be controlled independently:

G1 X150 Y200 U250 F6000

Of course, you can move u carriage alone without x:

G1 U200 Y250 F6000

The only requirement is that the name of the dual_carriage is a single letter that is not already used by some axis, e.g. manual_carriage, extruder, and not a special letter (F, N).

If you want to disable the direct dual_carriage control, you can switch it to a different mode, e.g.

SET_DUAL_CARRIAGE CARRIAGE=x MODE=PRIMARY ; disables dual_carriage
SET_DUAL_CARRIAGE CARRIAGE=u MODE=PRIMARY ; maps dual_carriage to 'X' axis and disables primary

If you have two dual_carriages, then these commands can be executed independently or together for these carriages (u and v in my example), e.g.

SET_DUAL_CARRIAGE CARRIAGE=u MODE=DIRECT
SET_DUAL_CARRIAGE CARRIAGE=v MODE=DIRECT

will add two new GCode axes U and V, but it is possible to activate only one carriage in DIRECT mode too.

One small but important note for the users with two dual_carriages: in general, Klipper must track the trajectory of the toolhead to compute maximum speed to pass the corners. When a single dual_carriage is activated in DIRECT mode, e.g. u, Klipper will simply track the second “toolhead” that is mapped to (U, Y, Z) coordinates and will compute the cornering speeds for it, no action is needed. However, if there are two dual_carriages, and they are both running in DIRECT mode, Klipper will only track (X, Y, Z) “toolhead” and (U, V, Z) “toolhead” for cornering by default. But some printers may have up to 4 toolheads in such setups, so in order to let Klipper know about other toolheads, I added a command

SET_TRACK_CARRIAGES_JUNCTION CARRIAGES=<carriage_list> [ENABLE=[0|1]]

which can be used to enable or disable tracking of these extra “toolheads”, e.g.

SET_TRACK_CARRIAGES_JUNCTION CARRIAGES=U,Y,Z
SET_TRACK_CARRIAGES_JUNCTION CARRIAGES=X,V,Z
SET_TRACK_CARRIAGES_JUNCTION CARRIAGES=U,V,Z ENABLE=0

As for the extruders, you can now map an extruder to a GCode axis and activate it via, e.g.

ACTIVATE_EXTRUDER EXTRUDER=extruder1 GCODE_AXIS=G

and then run commands like

G1 X150 Y200 U250 E3.1 G2.5 F6000
G1 E-1 G-1 F1200
G1 E1 G-1 F1200

Unmapping the extruder from the GCode axis can be done via

ACTIVATE_EXTRUDER EXTRUDER=extruder1 GCODE_AXIS=

Note that extruder will retain the previously specified GCode axis, so a subsequent call with a simple

ACTIVATE_EXTRUDER EXTRUDER=extruder1

will map it back to the latest specified GCode axis (‘E’ by default). To map the exturder back to ‘E’, you need to activate it for ‘E’ axis: ACTIVATE_EXTRUDER EXTRUDER=extruder1 GCODE_AXIS=E.

Other notable implemented functionality and some potential gotchas:

  • printer.toolhead.axis_minimum and `printer.toolhead.axis_maximum` will export the minimum and maximum range of travel for DIRECT mode dual_carriage(s)
  • printer.toolhead.position will also report the components for DIRECT mode dual_carriage(s) and extruders that are mapped to other GCode axes
  • GET_POSITION and M114 will report all GCode axes, and the former will also correctly compute kinematic position of the toolhead for all kinematic axes
  • RESTORE_GCODE_STATE will now restore the base positions and offsets for DIRECT mode dual_carriage(s) and mapped extruders
    • However, these are restored on a by-name basis and only for the axis that still exist since SAVE_GCODE_STATE command invocation, so if the axes has changed, this may apply offsets to the ‘wrong’ axes, so ideally SAVE_GCODE_STATE/RESTORE_GCODE_STATE should only be called without changing axes in between or at least restoring the same axes.
  • G92 now supports setting a position for DIRECT mode dual_carriage(s) and mapped extruders
  • SET_GCODE_OFFSET can now set an offset also for DIRECT mode dual_carriage(s)
    • Note that normally (for XYZ axes) the set offsets are preserved through homing. However, for dual_carriage axes (like U and V in my example) the offsets are not going to be preserved that way after homing because DIRECT mode is disabled for them during homing (so in fact X and Y offsets will be applied if any). If you want to preserve offsets, you can do it via SAVE_GCODE_STATE, G28, RESTORE_GCODE_STATE.
  • M221 command now supports T argument. Called without this argument, e.g. M221 S98 still sets the extrude factor override percentage to 98% for the active extruder mapped to E axis. But M221 T0 S98 will set this factor specifically for extruder extruder, and M221 T1 S101 will set the factor to 101% for extruder extruder1, and so forth.
    • Note however that calling ACTIVATE_EXTRUDER resets the extrude factor of the newly activated extruder back to 100% (this is Klipper current behavior), so in practice M221 T{index} S{factor} will have an effect only for extruders that are already mapped to some GCode axis and are active.
  • printer.gcode_move.gcode_position and printer.gcode_move.position will report the position of all GCode axes
  • printer.gcode_move.extrude_factors will report on a per-axis extrude factors of mapped extruders, e.g. printer.gcode_move.extrude_factors.e, printer.gcode_move.extrude_factors.g (based on the example above), etc.

So, what can these features be used for? I think at the moment due to lack of slicer support, primarily in various macros and manually generated GCode, for example for fast tool swaps on IDEX printers. As always, any feedback is appreciated.

4 Likes

@koconnor FYI and I’d like to get your feedback on these features specifically, and I also had a few questions regarding some implementation details of the current Klipper code:

  • Current Klipper does not export custom GCode axes in any way anywhere, not via GET_POSITION / M114 nor via statuses. Was this intentional for some reason? Is it OK to change this behavior?
  • G92 command has some non-trivial behavior for additional axes when invoked without arguments that I did not understand. Could you elaborate what is this for?
  • SET_GCODE_OFFSET has an undocumented feature to set E axis offset. I deleted this in my branch, but if it has some purpose, I’d say it is best to document it (and then I can bring it back).
  • Extrude override factors (M221 command) are not saved in the extruders and are instead always reset to 100% on extruder change (via ACTIVATE_EXTRUDER). Is this intentional and why not to keep the extrude factors for each extruder and restore them? I can implement this support in my code, I just did not do it to maintain compatibility with the current Klipper code.

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.

2 Likes

Interesting. Thanks.

Sure. I haven’t reviewed your code - just your messages above. So, don’t take what I say here too seriously.

I guess I’d have two main comments/questions.

What’s the high-level “use case” for this change? That is, how do you envision most users will utilize this? Is it faster idex mode switching or, perhaps its more flexibility in boutique printers via elaborate macros, or something else.

I’d recommend against adding new functionality to the old “legacy” g-code commands (such as M114, M221, G1234, etc.). The issue is, various programs have expectations for these commands and if we change them we risk breaking those programs. It also leads to elaborate debates on the “correct” implementation of these commands (“what does M221 really really mean”). In these cases, I’d recommend introducing new commands using the newer verbose format (eg, SET_GCODE_EXTRUDER_RATIO). We get to fully define (and document) these commands and I think that’s a maintenance savings over the long haul.

Of course, a careful observer will recognize that I “added new functionality to old gcode” when I recently added extra axis support to the G1 command. In truth, I was hesitant to make that change. I felt G1 was one of the few timing sensitive commands with deeply embedded behaviour in many applications. Few other old-style g-code commands are like that. It may have been a mistake on my part though.

As above, I would advise against changing M114. We can certainly change GET_POSITION and add status variables. I didn’t add them because I didn’t have a use case and didn’t want to add stuff that I may have to deprecate later if a future use-case had other requirements. The other, subtle, issue with status variables is that we currently export positions over the api server using lists (eg, position: [1.0, 2.0, 3.0, 4.0]) and I felt in the future we should be exporting them using a dict (eg, new_position: {'x': 1.0, 'y': 2.0, 'z': 3.0, 'e': 4.0, 'r': 99.2}.

For whatever strange reason, a bare G92 command means set the current position as the origin for XYZE. So, G92 X0 means set the X origin, but G92 means set them all. (These types of weird expectations is why I don’t like messing with these old commands.) The two lines that you’ve highlighted above were intended to replicate that behaviour for XYZE and to make as little change in behaviour aside from that.

I think when I implemented SET_GCODE_OFFSET I intended it to be a superset of G92 and so I probably added in the E support at that time. (Years back, using the rationale above, I needed more than what G92 could do but didn’t want to change G92 so added a new command.) Since it’s never been documented, if it’s painful to continue that support then I think we can remove it with a note in Config_Changes.md.

Well, as above, I’d advise against changing the M221 command. We could certainly add a new command (eg, SET_GCODE_EXTRUDER_RATIO), and in that case we can define what it does (persistent storage or not). That said, if this is part of the “gcode transform” then I’d recommend implementing it in gcode_move.py and not touch the low-level code, and vice-versa, if it’s implemented in the low-level code then gcode_move.py shouldn’t need to know about it. Note, though, that there is already a SET_EXTRUDER_ROTATION_DISTANCE command (in the low-level code).

Separate from the above, I didn’t understand your description of SET_TRACK_CARRIAGES_JUNCTION. Are you implementing a new “junction deviation” system and if so, why/how?

Also, is the new ACTIVATE_EXTRUDER stuff separate from the new SET_DUAL_CARRIAGE stuff, or are the code for these two things tied together somehow?

Also, for what it is worth, I’d recommend against requiring one letter carriage names in the config. I think we want to “move past” this old legacy g-code stuff where we can. So, you might want to consider something like SET_DUAL_CARRIAGE CARRIAGE=my_carraige MODE=DIRECT GCODE_AXIS=u.

Maybe that helps a little,
-Kevin

1 Like

Thanks Kevin,

Yep, thanks, that’s exactly what I was hoping to get for now - just the high-level feedback.

With this new functionality it is immediately possible to optimize and improve tool switching for IDEX printers, either with just through macros (making them faster), or with a preprocessing script, which could allow to do even more powerful things (like preheating, purging and cleaning the nozzle of the next toolhead while the other one is still printing).

Then a user could programmatically generate a code to print with two toolheads simultaneously two somewhat different objects, or print different parts of a single object. Admittedly, on a printer with two toolheads on a single Y rail that’s somewhat difficult, because Y movement is not independent, but on CoreXYUV this is doable and could be interesting, and perhaps the slicers could pick up and support this in the future.

These are two different and in principle code-wise independent features. However, since they both could be useful for faster tool swaps (e.g. it makes it possible to print and purge two extruders independently, or retract one and unretract another one, etc.) and independent printing, I published them both together right now to see what kind of feedback they’d both get from the interested testers, and what they could enable. But we could merge them separately, or not merge one of them at all.

Thanks for the feedback, I’ll keep that in mind, and perhaps revert my changes to some of them. However, G92 would need to work with at least remapped extruders, as this is a very frequent occurrence to issue commands like G92 E0/G92 G0 to reset a position of extruder. And thenG1 will need to support the new axes, since let’s say a gcode around tool swap like

...
G1 X150 Y150 E-1 F6000 ; wipe extruder
T1
G1 X155 Y123 F18000 ; travel to the next printing position
G1 Z5.2 F600
...

for the fast tool swap gets transformed in my macros into roughly (in reality, it is a bit more complicated to enable PA changes and such).

G1 X150 Y150 E-1 F6000 ; wipe extruder
SET_DUAL_CARRIAGE CARRIAGE=u MODE=DIRECT
G1 X{printer.toolhead.axis_minimum.x+0.5} Y123 U155 Z5.6 F18000
T1
G1 Z5.2 F600
...

thus requiring a coordinated move of all kinematic axes involved. As such, I find it would be quite confusing to add another extended GCode command that has the same semantics as G1, but is not G1. Arguably, on some kinematics it could be beneficial to have an ability to issue commands for U axis in this case that run parallel to regular G1 commands (similar to MANUAL_STEPPER SYNC=0), but I find that to be too difficult to implement for what it gives. That said, I did not update G2/G3 commands to support dual_carriage and extruder custom GCode axes yet (and I’d probably not do that for now).

Well, I certainly used status variables in macros. And having GET_POSITION report all positions is extremely helpful for debugging the macros you write (and FWIW, I think at least one command should report all positions). As for exporting positions over API, well, it seems mainsail handles this at the moment because it looks at [:4] components for now. But I agree, exporting position as a map would make a lot of sense.

Well, it is not painful, I just do not see a point for extruders. You typically need to set an extruder to be at specific position, not at specific offset. And while it is technically possible with SET_GCODE_OFFSET, you cannot do it purely from a terminal, you need a macro first that does, e.g. to zero out an extruder position, something like SET_GCODE_OFFSET E={0.0-printer.gcode_move.gcode_postion.e} (or maybe it needs to be position, I’d need to think). So, what I’m really interested in is your opinion, should extruder support in SET_GCODE_OFFSET continue, or is now a good time to deprecate it? I’m fine either way.

Interesting idea, I’ll think about it. FWIW, if we could allow the user to remap x carriage, e.g. along the lines of

[carriage xc]
axis: x
...
[dual_carriage dc]
primary_carriage: xc

and then

SET_DUAL_CARRIAGE CARRIAGE=dc MODE=PRIMARY  ;  maps dc to X
SET_DUAL_CARRIAGE CARRIAGE=xc MODE=DIRECT GCODE_AXIS=U  ; maps xc to U

that could simplify some macros for tool changes (and slicer gcode preprocessing), so I will look into that.

Well, it is not a “new” junction deviation system, but rather an extension of an existing one. Let’s consider an IQEX printer (e.g. Formbot Xplorer) that has 4 toolheads. Such printers are supported by Klipper to a limited extend, and can be configured with 2 dual_carriage(s). Putting limited support aside, let’s say we enable DIRECT mode for the four carriages - X, Y, U, V. Then let’s consider the following moves: (10, 10), (10, 10); (10, 10), (-10, -10) => (10, 10), (10, -10); (10, 10), (-10, 10)

SET_KINEMATIC_POSITION X=50 Y=50 U=150 V=150
G1 X60 Y60 U160 V160
G1 X70 Y50 U150 V170

Here the (X, Y) (say, T0) moves as (+10, +10), (+10, -10), making a 90 degrees turn clockwise, and (U, V) (say, T3) moves as (+10, +10), (-10, +10), making a 90 degrees turn counter-clockwise. But if we consider the other 2 toolheads, (X, V) (say, T1) moves as (+10, +10), (+10, +10), essentially a straight line, but (U, Y) (say, T2) moves as (+10, +10), (-10, -10), making a 180 degrees turn. So, making Klipper just computing the junction velocities for T0 is insufficient (unlike mirror and copy modes). But in general, if we have 4 carriages for X, Y, U, V, we really don’t know how many toolheads we have - it may be just 2, could be 3 or 4. So, we need a way for the user to tell Klipper, which ones to track, and that’s what that command is for. By default, Klipper will track (X, Y, Z) and (U, V, Z) in this case. But a user can enable (X, V, Z) and (U, Y, Z) too, and also disable (U, V, Z) - if they have 3 toolheads and configured them this way. If you are curious how the calculation of junction velocities is implemented under the hood, here’s how (kinematics, move).

I will consider that. FWIW, the current way of handling extrusion multipliers in Klipper is a bit hectic. Indeed right now there is a low-level SET_EXTRUDER_ROTATION_DISTANCE command, but it is actually per-stepper, does not work with percentages (so must be wrapped into a macro to adjust the RD from the config), and flushes the move queue, so it is not really suitable to adjust flow in real-time (especially for mixing extruders). And in general I see a rotation distance as a “physical property of a given extruder stepper”, that is, how much of a filament length it pushes per “rotation”; this could be calibrated and adjusted, but should not be used to regulate or tune the extruder flow to account for different filaments. Then we have a concept of ‘filament extrusion’ in GCode. That is, E component of G1 command instructs how much filament should be extruded. Ideally, we’d have 1 or more extruders that track E axis and extrude based on it, each having its own extrusion multiplier that can be adjusted during printing (e.g. for mixing extruders the sum of the multipliers should normally be 100%, but can be changed smoothly to create gradients, for IDEX they may be set arbitrarily to account for different materials loaded into different extruders and tuned by the user during printing to compensate for under- and over-extrusions). Alas, we don’t have that. SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder despite the name of the argument EXTRUDER actually syncs the extruder stepper to the specified ‘extruder’, and the synced stepper starts to copy the motion of the synced extruder exactly, thus ignoring any extrusion multiplier of the extruder1, if it had any. So in practice we never have two extruders ‘extruding simultaneously’, but rather “the one extruder” active, and some extruder steppers additionally copying its motion. Similarly, right nowM221 changes essentially a GCode multiplier, rather than extruder multiplier. I agree that introducing SET_GCODE_EXTRUDER_RATIO (though it will conflict with M221 command) and reworking extruder synchronization would be nice and would solve this (e.g. changing SYNC_EXTRUDER_MOTION to SYNC_EXTRUDER_STEPPER STEPPER=extruder1 MOTION_QUEUE=extruder, and adding a capability to run multiple extruders on the same GCode axis or actually syncing extruders to one another). I’ll consider this, but it is a rather considerable change, so it would need to be developed and reviewed separately outside of the improvements for IDEX.

im not quite sure if this gonna help you “or not”, but this is how i manage 4 extruder in 1 carriage, with no cutter (only retracting filament, the splitter in 4 ways direct to carriage)

#Driver1 extruder 1 / T0
[extruder]
max_extrude_only_distance: 1000.0
step_pin: PA4
dir_pin: !PA6
enable_pin: !PA2
microsteps: 16
rotation_distance: 22.5
nozzle_diameter: 0.300
filament_diameter: 1.750
heater_pin: PL4
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PK5
#control = pid
#pid_kp = 31.452
#pid_ki = 2.496
#pid_kd = 99.074
min_temp: -270
max_temp: 3000
pressure_advance: 0.68
min_extrude_temp: 170
max_extrude_cross_section: 70

#Driver2 extruder 2 / T1
[extruder_stepper extruder1]
extruder:
step_pin: external:PD7
dir_pin: !external:PC5
enable_pin: !external:PD6
microsteps: 16
rotation_distance: 22.5

..and so on, until T2 and T3 (3th and 4th extruder respectivly)

and the macros to activate using sintetic variable to store wich exdruder is in use:

#sync the extruder, ex: for T0, the same way is in T1, T2, T3, changin “extruder0, 1, 2, 3”

[gcode_macro T0]
gcode:
Zsalto #macro to increase z
{% set svv = printer.save_variables.variables %}
{% if active_tool !=‘“T0”’ %}
{ action_respond_info("Active Tool: " + svv.active_tool) }
G90
G1 E-2 F1400#
G1 Z+5 F900
G28 X
G1 Y35 F9000
G1 E-150 F1400
SYNC_EXTRUDER_MOTION EXTRUDER=extruder MOTION_QUEUE=extruder
SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=
SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=
SYNC_EXTRUDER_MOTION EXTRUDER=extruder3 MOTION_QUEUE=
SAVE_VARIABLE VARIABLE=active_tool VALUE='"T0"'
SET_PRESSURE_ADVANCE ADVANCE=0.68 SMOOTH_TIME=0.080 EXTRUDER=extruder

tempcambiador #macro to increase temp to 240 during extruder and filament change
G1 E160 F400  #first extrusion for reach nozzle and clean
limpia_color #small macro to clean, using a brush add to the side of bed)
{% endif %}
G28 X
G1 Y35
G1 E20 F600  #2nd extrusion of cleaning
templaminador # regular temp for extrusion 
limpia_color #extrude an small amount
CLEAN_NOZZLE #clean nozzle rapidly using brush
G1 Y22 F9000
G1 X215 F9000
#G1 Z-2
G90

i understand what are you doing, but i think you need to do it in printer, and if i undersand you do it in gcode… forget that, you only need to “activate carriage” and activate extruder in printer.cfg

The pre-heat should be handled by the slicer, unless we are going to start doing look ahead on the gcode. We would need to calculate the time the active tool head is going to be printing. Then compare that with heat up times of the hotend and trigger an M104 when the estimated time is hit. I would still throw an M109 in the tool change macro/gcode to verify the tool is heated before changing the tool.

If I understand this correctly, you want to purge and move a non-printing tool head while the other is currently printing. Sign me up! I have been trying to figure out how to mesh the extruder and manual_stepper classes to try and be able to an extruder move while the other tool is printing for a while now. But the best I have been able to do is crash Klipper. (Not good at python). If I can purge filament and do a wipe while the other tool head is printing (without waiting on the tool change) then I am all for that.

Is this really how it works? Because I have used very different extruders in copy and mirror modes on my Tridex without any noticeable loss in print quality between the parts. I used a Stealthburner witth a Clockwork2 extruder on T0 and an Orbiter 1.5 on a Dragon Burner for T1. Which have very different rotation distances. I currently use a BoxTurtle on T1 (the same rotation distance as a CW2) that is syncronized with the tool head extruder1 which is a Galileo 2. Unless I am missing something, the AFC add-on sync’s the extruder for the BoxTurtle lane to the extruder with the same calls as “SYNC_EXTRUDER_MOTION EXTRUDER=xxx” and the buffer only adjusts the rotation distance by +/-5%. With the gear ratio taken out of the equation there is a 12% difference between the two extruders for rotation distance, and I can watch the buffer move in and out during a print as the BoxTurtles lane extruders extrusion multiplier is adjusted.

If the extrusion multiplier was ignored for the sync’d stepper, I would expect the buffer to eventually get forced to one side or the other. Or am I misunderstanding the issue here?

As far as the M221, you could always just extend the command with a macro, add a tag to identify the command is coming from the console, M221 S98 T=Console, and if the gcode tries to set it back to 100% (like all slicers seem to do for the wipe tower) then ignore the command. I do the same with M106 commands with a modified version of this: klipper-voron2.4-config/printer_data/config/part-cooling.cfg at mainline · garethky/klipper-voron2.4-config · GitHub for some of my printers.

Hi Dmitry. Thanks for the detailed response.

Okay.

Okay.

I didn’t understand your examples - they did not contain a G92 command.

Nothing today should be issuing G92 G0 so I’m not sure why we couldn’t tell users to issue SET_GCODE_OFFSET G=xxx instead (or perhaps something like SET_GCODE_OFFSET G_RELATIVE=0.0). Similarly, nothing should be issuing a M221 T1 S1.0 so I’d think we could use something like SET_GCODE_AXIS_SCALE G=1.0 instead. Certainly, we may need to alter the internal cmd_G1() code to reflect these new translations, but I’m not sure that should be perceived as a change to G1.

Okay - I think it’s fine to add that.

Okay. I’ll try to start a thread on best way to handle the API Server dict vs list issue.

Okay, I think I understand. Currently the junction deviation code always uses XYZ (eg, move.axes_r[:3]) and this change would allow junction deviation to use any 3 axes (eg, (axes_r[0], axes_r[4], axes_r[7])).

For what it is worth, that seems confusing to me. I think it would be simpler if we told users that junction speeds are always determined using XYZ and if some toolhead should be the source of junction speeds then the user should switch to that toolhead so it is the XYZ. Then allow users to map the other carriages/toolheads to arbitrary gcode axis identifiers (eg, G1 U13)

Well, if you want my “2 cents” I’d say that is the role of a “gcode transform” module. That is, I think it is important to maintain internal code modularity, and the low-level kinematic code should not have to deal with arbitrary coordinate transforms. Just as we wouldn’t want klippy/kinematics/delta.py to have to deal with bed mesh Z adjustments, I think it would be preferable if klippy/kinematics/extruder.py did not have to directly manage a “mixing extruder” changing filament ratios..

I don’t have a strong opinion on that. (That is, I could be convinced either way; and/or I may have a new opinion next week..)

I do have some “indirect” thoughts, that may shape a decision though.

  • I think it would be preferable if the low-level kinematic code used absolute coordinates that easily correlate with coordinates found in the printer.cfg file. I think it would be preferable to avoid coordinate transforms in the low-level code - preferring user transforms in gcode_move.py and/or g-code transforms registered in gcode_move.py.
  • I think it would be preferable if gcode_move.py didn’t need to know what each type of axis is. Currently, an axis can be a core kinematic stepper (XYZ), the main extruder (E), or a manual_stepper. It seems like we’ll be adding additional extruders and kinematic carriages to that list. I think it would be nice if gcode_move.py didn’t need to care if some axis (eg, A) is an extruder, carriage, or manual_stepper.
  • I’d prefer to avoid “adding new knobs to old g-code”. I prefer the more verbose command format (eg, SET_KINEMATIC_POSITION) for new functionality.

Just “thinking out loud”, if the concern is that an additional extruder axis requires more gcode transforms, perhaps we could add that capability to the existing SET_GCODE_OFFSET command. For example, SET_GCODE_OFFSET E_RELATIVE=123.0 could do something more similar to what G92 E123 does today. And maybe SET_GCODE_OFFSET E_SCALE=1.2 could do something like what M221 S1.2 does today. (And, it could do this translation independent of the “type” of axis.)

Alternatively, we could do something like the above but use new commands (eg, SET_GCODE_AXIS_SCALE). As before, I don’t have a strong opinion on this - what are your thoughts?

Cheers,
-Kevin

Hi, @dmbutyugin, @koconnor
take this as “my view”, when I have printed over 1000 hours on an IDEX printer

  • because Klipper does not support T0 and T1 commands directly, a gcode macro must be created, I don’t see much benefit

  • synchronous exchange of T0/T1 can be elegantly solved with a macro

  • I also solve various settings for M221 T0/T1 with a macro

info: GitHub - DrumClock/Leapfrog-BOLT
video: https://www.youtube.com/watch?v=vFLhHAStguo

A different situation would occur if Slicers supported independent printing of two different objects.

But I’m not sure if it will be so widely used, after all, even the classic IDEX is only a tiny percentage of printers.

However, if the slicer would allow printing “two different” parts, then only on the Dual Gantry X kinematics with dual carriage (Core XY + Core UV )

info: GitHub - DrumClock/Generic-cartesian: tested
video: https://youtu.be/CByJuaerTZ0

Here I would see this advantage of independent control of additional carriages as a benefi.

Petr

Thank you for your feedback!

I am aware that synchronous exchange can be implemented with a macro, for example, RatRig implemented that for their RatRig v4 IDEX. However, I would note that it is not very elegant:

  • It requires fairly elaborate math and pretty complicated macro, that must be replicated by every IDEX user.
  • It requires splitting a swap into 2 gcode moves, creating micro stutter between them (because one of the toolhead comes to a complete stop there, so the other toolhead must also stop and then accelerate again). This might be just a quality-of-life defect, but still, I do not like it.

Then another useful thing that you can implement already today (though with a gcode postprocessing script) for some printers is a combined purge and wipe of the next extruder (if extruder wiper is located near the toolhead parking position, as is on my printer).

Let’s say you have a gcode near a tool change like this:

G1 X148.347 Y140.642 E.01567
G1 X148.895 Y140.546 E.01655
............................
G1 X152.165 Y179.257 E.02065
G1 X151.405 Y179.427 E.0231
G1 X150.711 Y179.517 E.02066
G1 X150.016 Y179.553 E.02058

;WIPE_START
G1 F14400
G1 X149.996 Y179.554 E-.00277
G1 X149.975 Y179.555 E-.00291
G1 X149.17 Y179.511 E-.11169
G1 X148.347 Y179.372 E-.11563
G1 X147.564 Y179.179 E-.11172
G1 X146.842 Y178.906 E-.10694
G1 X146.11 Y178.567 E-.11176
G1 X145.408 Y178.181 E-.11099
G1 X144.761 Y177.743 E-.10824
G1 X144.11 Y177.23 E-.11483
G1 X143.562 Y176.733 E-.10252
;WIPE_END

M104 S200 T0
M104 S220 T1

T1

set_pressure_advance advance=0.03

G1 X113.620 Y160.719 F18000
G1 X113.120 Y159.469
G1 Z7.1 F1200
G1 E1 F2100

Then with a script we can transform it into

; Preheat T1
M104 S220 T1
............................
G1 X148.347 Y140.642 E.01567
; Start T1 purge so that it purges at least {min_purge_len} mm of filament
SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder
G1 X148.895 Y140.546 E.01655
G1 X149.449 Y140.487 E.01657
............................
G1 X150.711 Y179.517 E.02066
G1 X150.016 Y179.553 E.02058

;WIPE_START
; Wipe and retract both tools simultaneously
SET_DUAL_CARRIAGE CARRIAGE=carriage_u MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=carriage_x MODE=DIRECT GCODE_AXIS=U
G91
G1 F60000
G1 U-0.02  Y0.001          E-.00277
G1 U-0.021 Y0.001          E-.00291
G1 U-0.805 Y-0.044 X-8.062 E-.11169
G1 U-0.823 Y-0.139 X-8.347 E-.11563
G1 U-0.783 Y-0.193 X-8.064 E-.11172
G1 U-0.722 Y-0.273 X-7.719 E-.10694
G1 U-0.732 Y-0.339 X8.067  E-.11176
G1 U-0.702 Y-0.386 X8.011  E-.11099
G1 U-0.647 Y-0.438 X7.813  E-.10824
G1 U-0.651 Y-0.513 X8.288  E-.11483
G1 U-0.548 Y-0.497 X-7.398 E-.10252
; Synchronized tool swap
G90
M104 S200 T0
G1 X113.620 Y160.719 U{tool0_park_position}
;WIPE_END

; Activate T1
SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder1
T1
set_pressure_advance advance=0.03

G1 X113.120 Y159.469 F18000
G1 Z7.1 F1200
G1 E1 F2100

This is something that is simply not possible with COPY/MIRROR macros, because the two carriages must travel different distances during the wipe. And this can reduce the tool swap time even even further - pretty much down to the time needed for a toolhead to travel to the new position.

Hi @dmbutyugin

I agree that for synchronous T0/T1 exchange it is an ideal solution.
The macro with COPY/PRIMARE has a small pause.

But with G-code it is worse. The “End-user” would have to write G-code for “exchange with cleaning”, as you mention in the example directly into Slicer or Slicer would have to already support it. Please note that the vast majority of “End-users” do not even know what G-code is and if someone does not write it into their slicer then… They just want to load STL into slicer and just print and not worry about T0/T1 exchange or their cleaning in G-code.
Here I do not see much benefit of supporting X,Y,U because Slicers are not yet ready for this without user intervention.

  • Petr
1 Like

For 2, I think there is some misunderstanding. Of course, I do not propose the users to manually edit GCode (this is not even feasible, typically prints have hundreds of tool changes). But this can be achieved by a post-processing script that you simply add to the slicer configuration (PrusaSlicer supports them, I believe Orca Slicer too). This is something that I plan to create for myself, and then it can be shared and used by others too (at least, by the the owners of the same IDEX printer as mine). Of course, it’ll take time for the slicers to catch up, but we do not have to wait until then.