This early video shows the motion and introduces the core challenge: two toolheads moving in a shared space. I didn't know if it work would at all when this was made!
Early video showing motion in a shared space
There are a few ways to handle the "two toolheads, one workspace" challenge, for a single print.
Here are the different cases, visualized, with more details explanations below.
It all makes me want to play some Tetris on a 1989 Game Boy, and play long enough to see a Buran take off again at the credits screen.
Anyway, for the calculations that drive the usable-travel numbers above, as well as V0 and V2 sizes, check this spreadsheet.
At smaller bed sizes, the simple options drastically reduce the usable bed size, motivating the use of interference detection and avoidance. At larger sizes, there's less loss.
Sure, you can always go "one size up" on the extrusions to get 100% usable bed fraction, by going with ~50mm wider X and Y extrusions and wider panels, and doing something simple, like Park in Opposite Corners. But that means a larger, heavier printer, with more air to heat up, longer belt runs, and potentially more challenges with tuning.
If you can get 100% of the usable bed with software... why not use that?
Ahhh yes. Where it gets interesting! Read on.
For every travel move, some code:
- slicer
- G-code post-processor
- firmware
... must detect toolhead interferences and proactively avoid them in the full workspace.
Consider any G-code move command (G0
, G1
, G28
, etc.), then ask this question:
Would one toolhead (a ~40 mm x 53 mm rectangle in XY, plus some clearance) intersect with the other toolhead's rectangle, at any point in the motion for that move command?
There are four distinct cases of motion here, at least when the toolheads are small relative to the travel.
An American football analogy will help us here.
We'll walk through the four distinct cases.
Endzone dancing adds print time and the potential for print artifacts (from retractions), but it’s necessary to use the full travel. When does it happen?
The worst case for motion is when every travel move triggers an endzone dance; for example, this condition occurs when printing an end of a square and when using a square-aligned infill direction (0 degrees or 90 degrees).
TBD: pics of motions matching this description
More generally, endzone dancing is needed for each outer perimeter of a full-size square.
This is the downside of the Dual Gantry approach. It is what it is.
However, some straightforward techiques can help us avoid it.
Don’t hang out in the endzone if it’s not necessary! Long, skinny parts can be reoriented to use the full Y height with park-in-the-back mode, or on a diagonal with the park-in-opposite-corners mode.
Rotate the infill direction to 45 degrees, so that the inactive extruder is only in the way twice for each square.
Avoid outer perimeters on large parts to avoid endzone dancing. For example, go from 4 to 3 outer perimeters and increase the outer perimeter width to compensate.
Instead of doing a full-Y-travel move for each flip, what about minimizing the shuffle distance to only what’s necessary?
The inactive extruder could get out of the way, while the extruder is on the other side, to hide some of the time cost of the dancing.
Simultaneous motion can be triggered with the two-Klipper control option, where each gantry is independently controlled. Doing this with RepRapFirmware would require at least minor changes to the generated G-code to drive XY and UV (second-gantry) axes simultaneously. Any implementation that assumes the use of T0
and T1
commands to share XY move commands between gantries cannot enable this.
The G-code processor could be smarter with positioning to avoid the need for retractions for interesting shapes, like a C-shaped part where one toolhead preemptively darts to the correct spot to get out of the way, or even just parks in the crook of the C shape and never moves.
A side benefit of Dual Gantry - like IDEX - is the option of simultaneous multi-part modes that split up the workspace. Here, we assume that collision detection and avoidance enables full workspace access.
-
Duplication Mode: like an IDEX, split the workspace in two:
- Usable bed space: divide the bed in two.
- 2x 85 x 170.
- Usable bed space: divide the bed in two.
-
Mirror mode: similar, but you leave space for the toolheads to never touch, so less space is usable.
- Usable bed space: divide the bed in two and leave an interior gutter.
- 2x 60 x 170.
- Usable bed space: divide the bed in two and leave an interior gutter.
However, you could get more flexibility to use the space with a slight bit of slicer awareness. Imagine offsetting the start Y positions for two mirrored parts so that you get effectively higher overlap than a simple static partitioning (two circles, each larger than half the bed width or height). You could print two half-bed-size triangles at the same time, for example.
Or, imagine having two streams of independent G-code, where the streams are sync’ed at Z moves.
Apparently this will be a thing for RRF for version 3.5!
You can then print completely different parts at the same time. How cool is that? IDEX can never do that.
The software and firmware side for Dual Gantry printers will be an evolving space - to implement the optimizations described above, but also to reduce the complexity of configuration and tuning for new builds.
Watch out for new stuff here. Join in!
At minimum, any Dual Gantry-capable firmware needs to control both gantries. If you want full travel, you also need something to ensure safety and insert moves with added G-codes.
You’ll need to align the toolheads in XYZ too. Z alignment can be easily automated with a shared Tri-Zero nozzle endstop. XY alignment is the same process as with an IDEX, of either:
- measuring with Vernier-style or other test prints or macros
- jogging printer motion with an upward-facing nozzle camera to measure the temp offset
- connecting up to a camera and using machine vision a la TAMV
Below are your software options.. at least, the ones that come from Zruncho with ❤️.
RRF has built-in support for CoreXYUV built-in, which makes it stupid simple here to at least get the axes in motion. You select this printer type with a single G-code command, where you map the X, Y, U, and V axes to your printer control board’s stepper outputs. Then you configure a homing macro to home all axes, and then you add macros to execute on each toolchange, specifically, to park the toolheads in opposing corners.
As long as the slicer spits out T0
and T1
commands, like for an IDEX, everything should then “just work”, for the Dual Toolhead, No Avoidance Needed
case.
This was a big surprise!
In fact, the main Duet/RRF developer, David Crocker, made it easy and general for all kinds of “linear axis combination” machines to flexibly use the same firmware. See this forum post:
"...after I implemented CoreXY, CoreXZ, Core XYU and CoreXYUV kinematics, I got fed up with having to add a new kinematics class every time someone wanted another variant of CoreXY. That is why RRF now has a universal linear kinematics class, which supports any kinematics for which the motion of every axis is a linear combination of the motion of each axis motor, up to the maximum number of axes supported."
That’s what we have here - a 2 simple linear combinations for motion.
Nice going, David! Impressive foresight.
RRF “just works” here. There is a known-good configuration for a Duet 2 Wifi board in the Configs/Duet2RRF
folder.
NOTE: This config is gantry-only. You’ll need to add your Z and E config.
For the curious, here are the most relevant RepRapFirmware documentation pages:
- Configuring RepRapFirmware for CoreXY Printer
- M669: Set type to K8 (CoreXYUV)
- M584: Set drive mapping (add U & V axes)
- M563: Define tools
To be clear, this config does not handle the Dual Toolhead, With Avoidance Needed
case.
To support that case, you would need to make at least these changes:
- Change printer config type to not map XY and UV axes to XY motion commands: in the M669 command, change away from type K8, probably to K1 and with a custom movement matrix
- Generate separate XY and UV commands in the G-code post-processor
- Replace
T0
andT1
macros with G-code equivalents
Klipper supports a massive ecosystem of control boards, sees new features added frequently (especially Input Shaper), and supports many toolhead boards that work well in a printer with tiny toolheads like Dueling Zero.
As of 2023-07-03, Klipper mainline code does not directly support a Dual Gantry printer. Which is fine... Kevin (the main Klipper dev) has a high bar for Klipper additions - ideally having value to many people - and there aren't many Dueling Zeros.
No problem, though. Thanks to a collaboration between Zruncho and Tircown (a developer of the highly-related Klipper IDEX code), code is avialable here:
You'll have to change your local Klipper code to use the dual_gantry_main branch, plus you'll have to restart Klipper so that the new code takes effect.
Since the changes live entirely within a single file (klippy/kinematics/dualgantry_corexy.py
), there's a good chance that if you rebase this code to the latest Klipper, it will "just work". There's a chicken-and-egg situtation here toward a mainline release that can only be resolved when enough people have these, share them, and show the code running. Then it justifies a merge, and it's even easier for the next people to get them.
NOTE: If you do not rebase onto the latest Klipper code, you'll be missing out on recent bugfixes and feature additions, plus, your existing toolhead and driver boards may not connect if they have been previously flashed with a more recent version. You definitely want to rebase this branch onto whatever is the latest code.
NOTE: HERE BE DRAGONS. There's a lot of config that is not yet documented here. If you get stuck, ask on the #dueling-zero-dev` on the
#tri-zero` channel on the DoomCube Discord. But this can be fun, too.
The docs are currently just about the bare minimum; they certainly will get more prescriptive and complete over time, especially if you share your build on the Voron Forum or ``#dueling-zero-devthread on the
#tri-zero` channel on the DoomCube Discord.
For example, if you're reading this... you should get your entire printer working first without the custom branch, with a successful single-extruder print. THEN proceed to make the two work together.
Configuration:
To use Dual Gantry support from the branch above, the minimal Klipper configuration must:
- Add [stepper_u] and [stepper_v] sections for the second gantry, along with corresponding stepper-driver sections
- Specify
kinematics: dualgantry_corexy
in the printer section - Define T0 and T1 macros with SET_DUAL_CARRIAGE inside
You'll have to modify an existing sample config.
However, all kinds of other stuff must evolve when going from one to two extruders. PRINT_START
, CANCEL
, even HOME
, are all are macros that really need dual-extruder awareness to prevent collisions in all cases. Take a look at these.
This repo includes a G-code processing code called duel.py that receives a G-Code file to safely handle all movement cases. It has to assume a starting position and needs the specific toolhead-size and XY motion bounds.
duel.py
uses a few Python modules to simplify the implementation:
- gcodeparser: a simple parser to turn ASCII lines into modifiable python objects
- requests: the usual Pythonic way to do clean REST interactions
- cmd: a simple way in Python to do interactive programs. “Battle mode” is a little easter egg.
- shapely: a geometry library
- nose: test-running library
The polygon intersect from Shapely enables interference detection.
That code looks like this, with a function get_toolhead_bounds()
to get toolhead bounds as a shapely.geometry.Polygon instance:
poly1 = get_toolhead_bounds(p1)
poly2 = get_toolhead_bounds(p2)
overlap = poly1.intersects(poly2)
One mildly interesting bit is the move split needed for the Segmented Avoidance case, which can be handled with basic y = mx + b math.
Aside from these bits, the code is pretty straightforward... surprisingly so.
duel.py
is also capable of driving two independent Klipper instances in a "Ships in the Night"-style setup. This code enabled me to validate the avoidance algorithms on a real printer, before modifying Klipper, but for all kinds of reasons, the integrated Klipper version is better: no static port partitioning, no easy ability to use a shared nozzle endstop, and no ability to use a single web interface for the whole printer, amongst other issues.