Atomic Motions of Multi-Gantry/Multi-Extruder/IDEX (4+) Setups

Basic Information:

Printer Model: custom (2 Y-gantries with IDEX on each)
MCU / Printerboard: 4x SKR MINI E3 V2.0
Host / SBC: BTT Pi
[-] klippy.log

I’m new to Klipper, but not new to 3DP.

Multi Gantry (2) with IDEX on each (4 extruders)

I have been experimenting with Klipper so far:

  • running 1 Klipper instance to control 4 extruders (X1,Y1,Z1,E1, X2,E2, X3,Y3,E3, X4,E4) but couldn’t really declare all the steppers, abandoned the approach quickly
  • running 4 Klipper instances (for each MCU controlling one extruder): works well, yet, the realtime requirements of my system I cannot really handle well yet, the virtual serial port /tmp/printer[1234] and the entire preprocessing before the low-level klipper commands are sent via USB to the MCU rises some challenges (to be resolved)
  • running 2 Klipper instances (two IDEX configs, two MCUs): works not well using T0-T3 macro switching extruders with SET_DUAL_CARRIAGE CARRIAGE=0 or 1 back and forth, even when doing SYNC_EXTRUDER_MOTION EXTRUDER=.. MOTION_QUEUE=.. I get slow/jittery motions (more debugging required I guess)

Atomicity

Essentially I require to declare multiple motions which should be execute at the same time, aka atomicity. In G-code context a single line is a command, X,Y and E states “move there and extrude accordingly”, the line represents an atomic execution.

When having multiple arms, which should operate in a controlled manner time-wise as well, we require to declare multiple motions executed at the same time.

Using single letters like U, V, W, and I, J, K, and H, P, Q, R, S we run out of letters for larger setups. Breaking G-code notion with multiple likes like XA, XB etc is perhaps also not wise.

Using macro system of Klipper to switch back and forth between extruders when using T0-3 notion, seemed not bear fruits yet (e.g. SYNC_EXTRUDER_MOTION).

True IDEX on Multi-Gantry

So, my use-case is having two extruders on a gantry perform independently (not in mirror or copy mode) simultaneously - and having multiple Y-gantries moving (non-colliding).

Has anyone done this already here?

Hi @Spiritdude ,

Not sure how much I can help with this, but I did recently find these videos (featured on Teaching Tech) showing a system similar to what you’re describing. The first video shows a two-head gantry, and the second video shows a three-head gantry. It looks like the main challenge is preventing the two heads from colliding.

Thanks - I’m aware of his work, I saw his posts on the forum.duet3d.com as he uses RRF as main firmware, the two videos:

  • dual gantry, they print independently (X and Y independent) - no need to realtime atomicity
  • dual gantry 3x extruders: only one extruder prints at a time, dito

He has more expanding plans as I saw, but he uses RRF and not yet the demands I have (might change).

FYI, the “generic cartesian” support was recently merged ( [kinematics] Generic Cartesian kinematics implementation by dmbutyugin · Pull Request #6815 · Klipper3d/klipper · GitHub ) and I hope to merge Support for additional G1 axes (6-axis support) by KevinOConnor · Pull Request #6888 · Klipper3d/klipper · GitHub in the next week or so.

Both of these PRs seem related to what you are working on, but I can’t say for sure if they will solve your issues.

Cheers,
-Kevin

1 Like

@koconnor - (bow) it took me a while to consider Klipper, to be honest, as I avoided to add another complexity and point of failure with an SBC; so it took me a while to arrive to consider Klipper - I’m deeply impressed of the work and clarity you put into; it shows, and the adoption shows; it brings value to otherwise “dumb” MCUs I just grew tired to recompile so often to change and test small things, so I was mainly using RRF for advanced stuff, and now thought to give Klipper a try to do something more sophisticated as well.

I will dive into those PR to see if it helps - I might take me a while to understand the full working of Klipper.

My considerations have been:

  • using other letters to address more axes (as I wrote) and stay in G-code semantics what is performed at a given time (one line = one task) – OR –
  • add more flow control like QUEUE/M2000 and EXEC/M2001 to mark & end of a or some motions (transaction like in SQL), then I would keep T0-T3 on separate lines with G[0123] statements afterwards, but having those custom code (macros) ensure stepper motor stage in the pipeline of Klipper knows what to start at the same time - it would be my task to make sure that involved F/speed and distances orchestrated two or more axes sharing the same Y gantry

This would mean, T0-T3 are declarative, and G[0123] following (even multiple) would a matter of “orchestration”:

  • let’s assume T0 goes G0 X0 Y0, G1 X100 Y100 E10
  • let’s assume T1 does G0 X200 (no Y as we share same Y), G1 X300 E10

The G0 I would not care to be atomic, but the beginning of the extrusion I am.

T0
G0 X0 Y0  ; done is motion queue 0
M400
T1
G0 X200   ; done is motion queue 1 => T0 & T1 move at the same time
M400
; at this point both T0 & T1 are done

M2000 ; start of atomicity
T0
G1 X100 Y10 E10
T1
G1 X300 E10      ; same Y
M2001 ; end of atomicity

The nice side-effect would be, M2001 would sync multiple tools (instead to run M400 for each one).

For my tests (prototype stage) I introduced “SYNC” (like M400) but takes arguments of T0, T1, etc, to state which tools are awaited; this way I sync multiple gantries and the IDEX to each other.

So, M2000 would be a start of atomicity, and M2001 internally sees the mentioned T0, T1, etc to await to finish - so those are two different semantics: a) declaring atomicity around lines of G-code, or SYNCing axes and have a clear defined state in time; I’m honestly not sure which is better.

Declaring atomicity would perhaps cleaner: “this is what needs to happen”, and it fails, e.g. axes colliding or some other mishap, it falls into a defined state back (before the M2000 state) and move back into a “safe” state (me just loud thinking).

So, let me see, having 6 axes in G1 as your PR states:

  • T0: X1, Y1, Z1, E1 (4 axes)
  • T1: X2, E2 (2 axes)
  • T2: X3, Y2, E3 (3 axes)
  • T3: X4, E4 (2 axes)

total 11 axes: XYZ(3) UVW(3) ABC(3) HI(2) using one letter per axis;

EDIT: but I could put T0 & T1 in one Klipper instance & MCU (6 axis) and the T2 & T3 on the 2nd Klipper/MCU (5 axes) - it would work/help indeed.

The question is, if something like multi_axes.py would help (alike dual_carriage.py) in which we switch from one “extruder” to another.

SET_MULTI_AXIS_EXTRUDER EXTRUDER=0,1,2,3

as the long(er) version of T0, T1 … that would tell, which tool/extruder does what, but the core problem I face is, I like to declare when those are executed (not sure what SYNC_EXTRUDER_MOTION does, haven’t studied the very details).

I care of the atomicity, two motions starting the same time.

I’m aware, that look-ahead like direction changes and [a/de]cceleration are lower level in Klipper and not knowable at the G-code level - but it’s something I do care as well, let me elaborate to give more context (for those who care):

Here the core challenge of my project:

  • I move two extruders in X while having the same Y (gantry)
  • and I like to plan so exact, that the T1 extruder might do 3 G1 statements, while T0 does one G1 E move, geometrically both do the same distances, something like:
T0
G0 X0 Y0
G1 X100 Y100 E10 F1000

T1
G0 X200
G0 X210 F1000       ; Step 1
G1 X280 E8 F1000    ; Step 2
G1 X300 F1000       ; Step 3

Both T0 and T1 move the same distance, but T1 has only a partial shorter extrusion (delayed start of extrusion and earlier stop).

I’m aware that is a hell of demand here, but I was hoping to be able to reverse calculate from the inner working of Klipper, and since the T1 have the same XY vector, that Klipper would time both motions nearly the same (T0: 2 lines, T1: 4 lines), and if not, I would find a way to anticipate and compensate (changing the speeds of Step 1, 2 and 3 or even change X positions to compensate de/acceleration of E motor).

:smiley:

Converting this into single tool space but supporting multiple extruders:

  • T0: X:X, Y:Y, E:E
  • T1: X:U, E:W

which gives:

;  +----T0----+  +--T1--+	+--common--+
G0 X0   Y0 		U200        F2000
G1 X10  Y10  E1	U210        F1000
G1 X80  Y80  E7	U280 W7
G1 X100 Y100 E2	U300

My guess is, when W7 kicks in that it has to conform the acceleration setting (trapezoid stage of planning in Klipper) will slow down the E7 as well a bit, even E1 has already performed and has reached proper F speed. The last U300 is just to keep the direction intact, so the speed/acceleration planning isn’t disturbed.

Thanks for the kind words.

It sounds to me like you have a large software development project ahead of you. I don’t know of any easy way to control multiple simultaneous toolheads like you are describing.

I guess the good news is, with Klipper, you shouldn’t have to worry about writing micro-controller code - everything should be possible from the host Python code. It does look like you’ve got a bit of code to write there though.

For what it is worth, if you are writing lots of new host code, you might want to consider using Klipper’s “extended g-code command” syntax. That is, avoid G1 and go with something like MYMOVE AXIS1=10.2,45.2,18.0 EXTRUDER1=94.3 AXIS2=33.245.2,18.0 EXTRUDER2=34.2 ... . At least that way you don’t have to worry about obscure g-code command limits.

Cheers,
-Kevin

1 Like

I managed to try the branch Support for additional G1 axes (6-axis support) by KevinOConnor · Pull Request #6888 · Klipper3d/klipper · GitHub you mentioned, and was able to get U working as 2nd X carriage (stepper_x1 vs stepper_x), I used following macro to home it:

[gcode_macro HOME_U]
gcode:
   MANUAL_STEPPER STEPPER=stepper_x1 GCODE_AXIS=
   MANUAL_STEPPER STEPPER=stepper_x1 SET_POSITION=0
   MANUAL_STEPPER STEPPER=stepper_x1 MOVE=400 STOP_ON_ENDSTOP=1 SPEED=40 ACCEL=500
   MANUAL_STEPPER STEPPER=stepper_x1 SET_POSITION=380
   MANUAL_STEPPER STEPPER=stepper_x1 MOVE=350 SPEED=80
   MANUAL_STEPPER STEPPER=stepper_x1 GCODE_AXIS=U
   ; G92 U350 ; doesn't work (yet) it resets all axes otherwise

and I was able to perform:

G0 X100 U200 F1000

and both X carriages moved accordingly together, achieving the “atomic” motion. (clap)

I post this so others find an example. Also note: you must declare [tmc2209 manual_stepper stepper_x1] before [stepper stepper_x1] when using sensorless homing :virtual_endstop.

You mentioned using “Extended G-code”, how can I enable the 2nd extruder (extruder1)? As I understood I need to use ACTIVATE_EXTRUDER before I could use it, and as far I saw, only one extruder can be active at a time? Do I need to declare my 2nd extruder (now [extruder1] & [tmc2209 extruder1]) as MANUAL_STEPPER instead, and do SYNC_EXTRUDER_MOTION my manual stepper?

Your MYMOVE example is achieved via [gcode_macro MYMOVE] and template code feeding the data to the underlying kinematics directly using {% .. %}, or direct klippy/extras/mymove.py and so maintaining atomicity? I’m not yet there to code this, need to study some examples how to do this. :slight_smile:

Edit: G92 U<number> won’t work, use SET_POSITION= instead (might work later).

I think I found a solution for 2nd extruder for 2nd X carriage:

  • declare [extruder1] like [extruder]
  • declare [tmc2209 manual_stepper extruder1]
  • declare [manual_stepper extruder1]
  • do MANUAL_STEPPER STEPPER=extruder1 GCODE_AXIS=W
  • do G92 W0 (don’t)

using macro to set temperature for extruder1 (2nd extruder):

[gcode_macro EXTRUDER_W_SET_TEMP]
gcode:
   {% set TEMP = params.TEMP|default(0)|float %}
   ACTIVATE_EXTRUDER EXTRUDER=extruder1
   M104 S{TEMP}
   ACTIVATE_EXTRUDER EXTRUDER=extruder
   
[gcode_macro EXTRUDER_W_SET_TEMP_WAIT]
gcode:
   {% set TEMP = params.TEMP|default(0)|float %}
   ACTIVATE_EXTRUDER EXTRUDER=extruder1
   M109 S{TEMP}
   ACTIVATE_EXTRUDER EXTRUDER=extruder

and then

M104 S200                    ; 1st extruder under 'E'
EXTRUDER_W                   ; 2nd extruder under 'W'
EXTRUDER_W_SET_TEMP_WAIT TEMP=200 

; two X-carriages on a common Y gantry (True IDEX):
G0 X100 Y100       U200
G1 X110 Y110 E5    U210 W5

Edit 1: it seems only absolute extrusion is possible via manual_stepper, any G91 W resets all axes into relative mode.

Edit 2: 'G92 W0` doesn’t work, it’s already set with SET_POSITION (G92 W might work later)

PS: @koconnor great you merged the 6-axis capability yesterday.

1 Like

200% 3DBenchy, first layer - mirror duplicated (horizontally & vertically) on G-code level (not on firmware level) - printed in “True IDEX” using new multi-axis (max 6 axes) capability of Klipper (merged 2025/05/13) as mentioned earlier.

It’s terrible print quality :slight_smile: (overextruded), but it works - all applied what was posted earlier in this thread.

Bed: 400x400, apprx. 380x390 printable.

The source is a 200% 3DBenchy sliced with Prusa-Slicer for 200x200 bed, and then G-code parsed, and offsetted and mirrored in X and Y respectively in absolute coords, and then merged back into:

;   Y Gantry  1st carriage/extruder  2nd carriage/extruder
G1  Y100      X100 E21               U380 W21 

4x BTT SKR MINI E3 V2.0, one MCU for each extruder, 2x Klipper instances handling each one gantry with two extruders; plus a multiplexer which coordinates the motions (like homing) in front of the two Klipper instances.

All is very very very experimental - so it’s result of apprx. 3 months preparation. The main aim is to print with two gantries with True IDEX on one piece collaboratively; this early test is to see if Klipper is capable of what I require as a base. :wink:

There are more requirements, like syncing gantries closer with each other, and I will see how well I can do this with Klipper at the G-code/serial port level, so far I noticed 0.5s precision to sync - I tried to delay G-code but since I can’t control the G-code buffer length it’s not that simple.

1 Like

Impressive effort :+1: