Full dual gantry support: IQEX and ITEX printers

If you happen to have a dual-gantry IQUEX (Independent Quad EXtruder) or ITEX (Independent Triple EXtruder) printer and want to test it under Klipper, I’d like to invite you to test a new experimental Klipper branch that adds a support of such printers under generic_cartesian kinematics. A general principle for configuring such printers is to define printer gantries as normal carriage and dual_carriage, e.g.

[carriage carriage_y0]
axis: y
...

[dual_carriage carriage_y1]
primary_carriage: carriage_y0
safe_distance: 50 # safe distance between y0 and y1
...

and then define carriages on these gantries as, e.g.

[carriage carriage_t0]
axis: x
gantry: carriage_y0
...

[carriage carriage_t1]
axis: x
gantry: carriage_y1
...

And then you can define dual carriages for them as needed, e.g.

[dual_carriage carriage_t2]
primary_carriage: carriage_t0
safe_distance: 70 # safe distance between t0 and t2
...

#If present:
#[dual_carriage carriage_t3]
#primary_carriage: carriage_t1
#safe_distance: 80  # safe distance between t1 and t3
...

Then these defined carriages can be enabled as primary or for copy and mirror modes via SET_DUAL_CARRIAGE as necessary, e.g.

SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=COPY
SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=MIRROR
SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 MODE=MIRROR

Also keep in mind that currently the first declared [carriage …] on each axis gets activated as primary by default when Klipper starts or after homing (e.g. in the example above, carriage t0 will be activated as primary because it was declared before t1). You may want to explicitly activate a specific carriage though, e.g. as a part of a homing procedure.

More complete configs with some useful macros can be found here for IQEX and here for ITEX printers.

Please note that I do not have such printers, so while I tested this branch to some degree by artificially adjusting my IDEX printer config, bugs are still quite possible, so exercise caution and be ready to hit emergency stop button if something goes wrong, especially in the beginning.

2 Likes

Hey, this looks very interesting! It might actually solve the blockage I’ve been stuck with on my Xplorer project.

This printer can be built as a Single Extruder, IDEX, Dual Gantry, or IQEX. For the IQEX configuration, I wasn’t able to create the necessary configs to make it work — the furthest I got was getting Dual Gantry working using standard Klipper with its generic_cartesian setup.

I’ll definitely give this branch a try because I have an IQEX Xplorer sitting here collecting dust.

By the way, the Xplorer project is on Discord, and the kits are sold by Formbot.

Check the links below:

So, I’m back again with an update on this topic. I’ve installed this branch on my Xplorer printer, and here’s what I can say so far:

  1. It works! The definition of the carriages and tools works correctly.I was able to print successfully.

If curiosity is killing you, check out this video from Discord:Xplorer IQEX -Formbot Discord

  1. Copy and Mirror modes on an IDEX-style printer also work — meaning one tool can copy or mirror the movements of the other. However, having all tools copy or mirror one tool does not work. I keep getting an “out of range” error on the X-axis, but only when I’m within the first half of the motion range (in my case, between 0 and 200). If I issue a command to move past the midpoint — for example, to 250 — the error no longer occurs.
    I do not have defined any ‘‘safe_distance’

  2. The tool calibration script no longer works. I’m using this one: https://github.com/viesturz/klipper-toolchanger/blob/main/klipper/extras/tools_calibrate.py, but it doesn’t understand generic_cartesian components like “gantry” and “carriage.”

    As toolchangers and multi-tool printers become more popular in the 3D-printing community, it would be great to have something in upstream Klipper that supports semi-automatic (or automatic) tool calibration out of the box.

    regards,
    Dan

Okay ,

I have to revise something: point 2 from my previous message. Actually, it works — I just had to switch T2 and T3.

Xplorer IQEX T1, T2, T3 Copy T0-Formbot Discord

Thanks for the testing! Actually, I realized that the kinematic range calculations were all wrong for this case. I pushed a fix to the branch now. If you could test it, that would be great! Especially if you could configure safe_distance values and position_min/position_max correctly for all carriages, move them to various positions on the build plate, and then manually enable different COPY/MIRROR modes for them and test that they move as expected and the safe_distance are respected for moves of all carriages, and also do it for several different positions and different combinations of COPY/MIRROR modes, that would be great! *You can do this of course without printing anything, just testing the carriage motion is sufficient.

And then perhaps you could also try to test Tri-DEX mode, for example by commenting out the second carriage on the second gantry out of configuration, manually move it to the side, and artificially limit position_max of the other carriage to prevent it colliding with the disabled carriage, and then also try various motions in various COPY/MIRROR modes, that would also be very useful! Cause I don’t have means to test neither of these myself, and it is important to validate that the new code actually works as expected.

okay, I will do that.

please explain me what ‘safe_distance’ actually does? If i set it to 50 for example, to what refers this 50? 50mm from where to where?

This is configured for a dual_carriage, and it is a distance how close you can move it towards the primary_carriage via G0/G1 commands. In my example, for instance

[dual_carriage carriage_t2]
primary_carriage: carriage_t0
safe_distance: 70 # safe distance between t0 and t2 in mm
...

it means that you can do

SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY
G1 X100
SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=PRIMARY
G1 X170  ; this is the closes t2 can get to t0
G1 X169  ; this is too close and will fail

or for example

SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY
G1 X0
SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=PRIMARY
G1 X200
SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=MIRROR
G1 X65  ; this moves t0 to 65, t2 to 135, leaving 70 mm between them, so it's OK
G1 X70  ; this triggers an error, since this commands 60 mm distance between t0 and t2

This also works for dual gantries, for example

[carriage carriage_y0]
axis: y
...

[dual_carriage carriage_y1]
primary_carriage: carriage_y0
safe_distance: 50 # safe distance between y0 and y1
...

will work in exactly the same way for carriage_y0 and carriage_y1, with the minimal distance commanded on Y axis being 50 mm, e.g.

SET_DUAL_CARRIAGE CARRIAGE=carriage_y0 MODE=PRIMARY
G1 Y100
SET_DUAL_CARRIAGE CARRIAGE=carriage_y1 MODE=PRIMARY
G1 Y150  ; this is the closes y1 can get to y0

Note that these safety checks only work between two gantries or the dual carriages on the same gantry, which means that unrelated carriages can move however you want to, e.g.

SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY
G1 X10
SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=PRIMARY
G1 X20   ; no collisions with t0 since they are on different gantries
SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY
G1 X200
SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 MODE=PRIMARY
G1 X100  ; no collisions with t0 since t3 they are on different gantries (t3 on the same gantry with t1)
SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=PRIMARY
G1 X40   ; depending on t3's safe_distance, this may trigger an error
1 Like

Just wanted to jump in and say that this works really well so far! Any plans to submit a PR?

So, I tested the latest version of the branch.

  • safe_distance works as expected
  • Tri-idex (one gantry with 2 Tools as an IDEX and the second gantry with only 1 Tool) also works

I haven’t detect any issue that I can report

That’s good to hear, thanks for testing.

@koconnor could you take a look at this feature and provide your feedback? If it looks OK I could clean it up, add the documentation, and open a PR. At high level, this brings the following:

  • Let a user define more than a single [carriage …] per axis (and corresponding [dual_carriage …]); proximity checks are enabled only between a dual_carriage and its corresponding primary carriage, but not between different carriage(s) and dual_carriage(s).
  • Let a user set any of the carriages as a PRIMARY and enable COPY or MIRROR mode for any of the other carriages (previously only a dual_carriage could be set to COPY or MIRROR mode).

Then, a bit more of an implementation detail, a new parameter gantry was added for the primary carriages:

[carriage carriage_t0]
axis: x
gantry: carriage_y0
...

[carriage carriage_t1]
axis: x
gantry: carriage_y1
...

This parameters will play two roles: act as a sanity check to prevent a user accidentally declaring a primary and a dual carriage as primary carriages by making the intent very explicit, and also determine the primary activated carriage after the printer startup and homing (the primary carriage on primary gantry will be activated). However, this may be a bit too hardware-specific, so I’m open to other suggestions and alternatives.

One other alternative that I considered was not specify gantry at all, and just define

[carriage carriage_t0]
axis: x
...

[carriage carriage_t1]
axis: x
...

from the example below. That would work too (without added safety), and one just needs to figure out which carriage to activate at startup. This could be the first one declared (though I do not like that too much either as this becomes fragile and e.g. dependent on the includes in the config and such), or a dedicated parameter could be added (but this may look a bit weird too). So, while this is not my option of choice, I could also do that, so let me know what you think of it.

Interesting, thanks. I took a look through your examples and some of the test cases. I have not reviewed your code.

I’m not all that familiar with IDEX systems, and certainly not with their quad/tri brethren. So, take what I say here “with a grain of salt”.

If I understand correctly, current generic_cartesian defines three types of carriages:

  • [carriage x] which defines the “primary” carriage for one of x, y, and z. Today, there must be exactly three of these in the config and they must be named x, y, z.
  • [dual_carriage some_name] defines optional carriages that may be activated in place of one of the “primary carriages” mentioned above (or in parallel in mirror/dup mode).
  • [extra_carriage my_name] defines a carriage that homes separately from a “primary carriage”, but is otherwise tied to it.

This proposal adds to the above:

  • It is on top of PR #7073. That is, it allows a [carriage some_name] with axis: x, instead of requiring the name to be one of “x”, “y”, “z”.
  • It allows for more than one [carriage my_tool] for a given axis. That is, one could have [carriage x] with axis: x along with [carriage my_tool_on_x] with axis: x.
  • A new “gantry” parameter is added to a [carriage some_name] config. It can be set to the name of some other carriage or dual_carriage.

Did I get that correct? Did I miss anything?

I guess my initial feedback is that the multiple uses of [carriage somename] seems a little confusing to me. It no longer means the “primary carriage” for an axis.

Out of curiosity, if a carriage is not the main “primary carriage” activated on startup, why not declare it with [dual_carriage somename]? For example:

# The "primary carriage" for x activated at startup
[carriage carriage_t0]
axis: x
...

# An activatable "carriage" paired with t3
[dual_carriage carriage_t1]
axis: x
...

# An activatable carriage paired with t0
[dual_carriage carriage t2]
primary_carriage: carriage_t0
safe_distance: 70 # safe distance between t0 and t2

# Carriage paired with t1
[dual_carriage carriage_t3]
primary_carriage: carriage_t1
safe_distance: 80  # safe distance between t1 and t3

Otherwise, my high-level feedback is that the functionality seems useful.

Let me know if I’ve missed something,
-Kevin

1 Like

Thanks Kevin. For now, I wanted to get your high-level feedback on the user-facing side of a feature, so that I could make some adjustments before writing the documentation and opening a PR.

Yes, all of that is correct and pretty much summarizes the current primary proposal.

Well, FWIW, the name of a section says [carriage …], but not [primary_carriage …], so I guess the configuration does not explicitly call out it as ‘primary’?

Yes, it is possible to go that path, and the proposed approach is fairly flexible and is not tied to ‘gantry’ concept. It would also theoretically not require PR #7073 as a prerequisite. So, it would be a third proposal, I suppose. For completeness, I see a couple of downsides with this proposal:

First, a fairly minor one, is that

[dual_carriage carriage_t1]
axis: x
...

[dual_carriage carriage_t3]
primary_carriage: carriage_t1
...

may look a bit odd, that a dual carriage t1 is declared as a primary carriage for t3. Similar to two [carriage …] sections for the same axis, this may be confusing to the users, as to “What exactly does that mean, dual_carriage carriage_t1 is a primary_carriage for a dual_carriage carriage_t3? In what sense is it a primary carriage?”

The second issue, a bigger one, in my opinion, is that it may be easy for a non-informed user to write something like

[carriage carriage_t0]
axis: x
...

[dual_carriage carriage_t1]
axis: x
...

in the config for an actual IDEX printer. This configuration is technically valid and will kind of work, but does not match the printer and the user will not get any carriage proximity checks (even if they configured position_min/position_max correctly), and they’ll get no errors or warnings about it. FWIW, generic_cartesian kinematics may appear complicated to the users, but at least today it has a lot of built-in checks that prevent users from specifying incorrect configuration, e.g. ‘forgetting’ to set some parameters in the configuration. So, this proposal, similarly to my alternative proposal, deviates from that philosophy in that it now becomes possible to create an invalid configuration by a mistake or an overlook and Klipper will silently accept it. Of course, at the end of the day it is always possible to configure something incorrectly, but in this instance the bar for an accidental mistake is rather low.

Please let me know what your thoughts are. I’m not against this proposal per se, but I am worried about the potential user confusion and mistakes.

Okay, thanks. Just to be clear, I’m not pushing for any particular implementation. I really don’t have enough experience with IDEX (and similar) to provide strong feedback.

Sure. I guess what I was trying to say though was that previously I could form a one or two sentence description of what [carriage], [dual_carriage], and [extra_carriage] objects are at a high-level. However, with the proposal I’m not sure I could do that. What would you say would be a few sentence description of the objects? To a regular end user what’s the fundamental distinction between a dual_carriage and a carriage?

For what it is worth, it took me a few passes reading through the example configs to figure out what they did. In particular the connection from [carriage t2] to carriage_gantry1_left to [dual_carriage] wasn’t obvious to me. It seemed almost like important details of “t2” were defined in some other config object. YMMV.

Yeah. I guess “primary” could be read as “group leader for this rail”. But, I agree with your observation.

Well, I guess they’d get an error if they configured a safe_distance, but I also guess they wouldn’t get the current default for it. Otherwise is it really an “incorrect” configuration..

For what it is worth, at a somewhat philosophical level I think it’s really really hard for the software to automatically infer the “right” parameters for heavily bespoke printers like this. I guess I’d put “IDEX”, “IQEX”, and “ITEX” in the “it can work” category and not in the “it’ll automatically do what you want” category. Again, YMMV.

Cheers,
-Kevin

Oh, thinking about it a little more, I guess I made a leap that going forward a “dual_carriage” object would be: “an optional carriage that is not enabled at startup, but can be enabled by users at run-time as a replacement for the primary, a dup, a mirror, or similar. It is called “dual” because way back in the day printers only ever had 2 of these carriages on just 1 rail, but now one can have as many as they want on as many rails as they want and enable/disable them whenever they need”.

I guess if one was looking at a “dual_carriage” object as “one of two carriages on a single rail” then my comments would indeed seem odd.

Again, I’m not pushing for any particular implementation.
-Kevin

@dmbutyugin can I help anywhere to keep the momentum moving forward? Haven’t seen anything new to test on fork and the last commit was 3 weeks ago. Here to help! This is such an exciting feature. My iqex build is pumping electrons and extruding all of the plastic. Lemme know where or how I can support the branch! I’m here for it.

Well, I was busy with other things and could not dedicate time to rework the branch per suggestions. But now this was done, I modified the code in the test branch to follow this configuration:

# The "primary carriage" for x activated at startup
[carriage carriage_t0]
axis: x
...

# An activatable "carriage" paired with t3
[dual_carriage carriage_t1]
axis: x
...

# An activatable carriage paired with t0
[dual_carriage carriage t2]
primary_carriage: carriage_t0
safe_distance: 70 # safe distance between t0 and t2
...

# Carriage paired with t1
[dual_carriage carriage_t3]
primary_carriage: carriage_t1
safe_distance: 80  # safe distance between t1 and t3
...

instead of defining carriage_t1 as

[carriage carriage_t1]
axis: x
gantry: carriage_y1

Please test the changes and let me know how it works now.

Alright, just pulled in the changes and testing now. Changed all of the bits I needed in my X configs. Looks like Y configs have no changes. Klipper starts happy. Things of notable observation, expected I’m sure but calling them out as extra data points:

  • I had left over configs in t1 concerning safe distance and klipper complained, so I moved all safe distance lines to t2 and t3 definitions.
  • I had left over configs for axis designation on t2 and klipper complained, so I removed it and made sure it was only declared on t0 and t1.

After reading and implementing on my end, I like this a bit more than the previous way. I like that we aren’t declaring the Y gantry configs on X definitions. For the dual_carriage designation on t2 and t3, I wish there was different language there to signify that this is in fact another gantry. Perhaps for the extra gantry carriages could get a special object designation like aux_carriage or opt_carriage signifying auxiliary or optional? Just spit balling.

Regardless, I’ll run through a few test prints:

  1. Primary, t0 only
  2. Primary, t0 and t1 only
  3. Copy, t0 and t1 only
  4. Copy, all
  5. Mirror, t0 and t1 only
  6. Mirror, all

Alright, I was off to a rough start :sweat_smile:. That whole everything but t0 being designated as a dual_carriage is really confusing, as mentioned above. So my t0 and t1 share an x rail at the back and my t2 and t3 share a gantry in front. The designation of a carriage where there is a primary == it’s on the same rail as the primary.

I’m straightened out how. The notices I received from klipper before now make more sense:

  • safe distances are defined only once per axis, not twice
  • Only 1 axis can be defined on a gantry, not twice…which makes perfect sense.

All that said, I’ve managed to make it through most of the prints and they are all working with only having to make adjustments to my x motor configs as defined above in the sample configuration. Looking good from end.