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