-
Automatic Tool Alignment in XYZ
05/12/2026 at 08:56 • 0 commentsLast weekend I spent ~30 minutes calibrating three pens manually. Annoyed, I decided to automate this process as well. For complete pen alignment we need to offset the XYZ coordinates -- the offsets are stored in a RFID tag (more details).
A Resistive Touch Panel was used to build a new plotter bed. By probing a tool over the panel at several location we can know the three spatial coordinates.
![]()
![]()
I used a Cheap Yellow Display (CYD) 2.8" which contains an XPT2046 resistive touch controller and panel. I removed the display entirely and soldered pigtails to four touch panel pins. Then I used another larger resistive panel that I reclaimed from an old car GPS. Magnetic Pogo Pins allow connecting the panel when needed.
The new bed is 2.5mm thick and magnetic alignement is highly repeatable. I posted a demo video of it in action.
Hardware
To connect the touch panel to klipper, I'm currently keeping the CYD in use.
- The CYD outputs a 3.3V 10ms pulse whenever a touch event is detected.
- An OR gate built with two NPN transistors combines the BLTouch Probe
Signal and the touch signal, output of which is used as PROBE_ENDSTOP on klipper. - Now, when running Bed Mesh or Probe calibration, the toolhead stops when
EITHER resistive touch event or BLTouch trigger is detected. - This essentially means that basic Z offset calculation work using BLTouch with no changes to klipper.
- CYD is connected to Klipper Pi over USB.
- There's a screen protector layer over the panel and the software will implement wear leveling to use the entire panel evenly.
Software
The CYD runs a very basic ESPHome firmware. It logs out the XY and Pressure values via serial port and can be read by a Python script. Klipper macros can use CLI command to run the script when probing to read the coordinates.
A PROBE_TOOLS macro will automate the whole process:
- Partial BLTouch bed mesh over the touch panel.
- Docks `reference tool` (T0) and probe at 4 locations. Stores the coordinates and scaling factor.
- Following steps are repeated from tool T1 to T4:
- Dock Tool
- Slow Probe to find tool Z offset
- Fast Probe at 4 locations
- Calculate XYZ offset from reference pen
- Write the offset to pen's RFID tag
- Undock Tool
- Partial bed mesh on paper area (A6 size).
- Repeat from T0 to T4:
- Dock Tool (also reads the tag)
- Draw alignment pattern
- Undock
- User/Camera confirmation
Roughly 10 units of displacement on the touchscreen corresponds to 1-2mm toolhead movement, in my observation. I'll work on alignment script next, given that I seem to have all the required parameters about tools available.
Github repo will be updated soon containing the config changes.
PS: The screen that comes with the CYD is also sufficient, I just had some extra available. It was slightly thicker as well since I couldn't easily remove the display layer.
![]()
-
Applying Tool Parameters, Tethered tools
05/01/2026 at 20:53 • 0 commentsI finally got around to implementing the initial version of tool alignment. As I mentioned in a previous post, it'd require extra effort to align each pen manually at true zero. Instead we opted for an RFID tag affixed to the tools. The tag currently contains XYZ offsets and a tool identifier. In future for tools such as paint markers we can also store attributes to inform the plotter to purge the pen at a given interval, or for cutting tools, the blade angle and cutting depth.
To read the tag and apply tool parameters during print run on Klipper, I came up with a method as described below:
1. Reading Tool Data when docking
A klipper module is created that allows us to execute arbitrary shell commands. The output of this command would contain the "result" enclosed in a known heredoc style pattern. The gcode can read this result using the `printer` variable. Then we can apply the XYZ offset using `SET_GCODE_OFFSET` command. Of course this is a potential security vulnerability if we run unknown gcode, but something to be improved later (quite easily).
[gcode_macro _RFID_HOME] gcode: G1 X130 G1 Y3 G1 Z7 M400 [gcode_macro RFID_READ] gcode: # Home to Reading Position _RFID_HOME EXECUTE_AND_STORE COMMAND="/home/pi/.local/bin/uv run --directory /home/pi/limn rfid.py read-tag" M400 APPLY_RFID_DATA [gcode_macro APPLY_RFID_DATA] gcode: {% set output_buff = printer["shell_output sh_output_buffer"].output %} {% if "||" not in output_buff %} # Do nothing M118 No tag found. {% else %} {% set comps = output_buff.split('||') %} SAVE_VARIABLE VARIABLE=tool_offset_x VALUE={comps[0]} SAVE_VARIABLE VARIABLE=tool_offset_y VALUE={comps[1]} SAVE_VARIABLE VARIABLE=tool_offset_z VALUE={comps[2]} SAVE_VARIABLE VARIABLE=tool_name VALUE='"{comps[3]}"' M118 Stored offsets for tool. {% endif %}In short, we use the saved variables to pass the tool information between macros.
The potential security issue stems from the fact that if an attacker is aware of `EXECUTE_AND_STORE` command, they are able to add it within their own crafted Gcode. So caution should be taken (as always) when plotting unknown gcode.
2. Applying tool offsets
We override move commands (G1, G2, G3) so that when instructed, we apply the tool offsets before moving. Any moves not containing `ALIGN1` parameter would not be affected by gcode offset -- this is a feature, as we can choose to only align certain moves.
[gcode_macro _APPLY_OFFSETS] gcode: {% set align = params.ALIGN|int %} {% set svv = printer.save_variables.variables %} {% set offset_x = svv.tool_offset_x|default(0)|int %} {% set offset_y = svv.tool_offset_y|default(0)|int %} {% set offset_z = svv.tool_offset_z|default(0)|int %} {% if align == 1 %} SET_GCODE_OFFSET X={offset_x} Y={offset_y} Z={offset_z} {% else %} SET_GCODE_OFFSET X=0 Y=0 Z=0 {% endif %} [gcode_macro G1] rename_existing: G1.1 # Rename the existing G1 command to G1.1 gcode: {% set p_x = ' X' ~ params.X if 'X' in params else '' %} # Extract only the move commands we care about. {% set p_y = ' Y' ~ params.Y if 'Y' in params else '' %} {% set p_z = ' Z' ~ params.Z if 'Z' in params else '' %} {% set p_f = ' F' ~ params.F if 'F' in params else '' %} {% set act = params.ACT|int if 'ACT' in params else 0 %} {% set align = params.ALIGN if 'ALIGN' in params else 0 %} _APPLY_OFFSETS ALIGN={align} {% if act == 1 %} # PEN DOWN {% set p_z = " Z0.2" %} {% elif act == 2 %} # PEN UP {% set p_z = " Z5" %} {% elif act == 3 %} # TRAVEL {% set p_z = " Z7" %} {% endif %} {% set ps = p_x + p_y + p_z + p_f %} G1.1 {ps}3. Applying tool offsets only when drawing (and other moves)
Since the toolhead interacts with the machine physically (eg. docking, rfid read) we need to selectively apply the offsets only when it's needed. Currently the only place it's needed is when drawing. Therefore I modified slicer config to append parameter `ALIGN` on each travel and draw moves. If a tool doesn't specify any parameter, there is no effective offset.
Additionally I wanted the printer to control pen-up/down movements, but still see preview in the slicer. Fun fact: the slicer preview is 1:1 reconstruction of the gcode. Therefore I added another parameter `ACT` to G* commands. So, eg. ACT1 would be pen_down, and any Z specified in the slicer is ignored. This is illustrated in above code example. The modification in the slicer is shown below:
4. Writing/updating tool parameters
The rfid script is a typer cli, so we can simply call it from a Gcode macro to update tool info. The macro auto homes to correct location where the tag can be read, and echoes the written data back to the user.
I discovered a neat trick in klipper here. If we use dot notation (params.dx) to access the parameter (instead of index notation, eg. params['dx']), fluidd would auto generate a mini UI to input the values:![]()
It's quite handy!---
In a sort of a gimmick (for now) I tried docking a tethered tool (tool with a physical connection elsewhere) and it works as expected. I put a little UVC camera on it for the demo on Youtube. Normally this would not be gimmicky since a tool-changer 3D Printer would obviously need tethered tools, but I don't think Limn is gonna be able to dock a tool with PTFE tube (such tool would also be heavy) on it yet!--
So this is how I got the alignment to work. I can finally take a small break and plot some multi-ink designs! I'll update the github repo shortly with new printer config -- I just need to first do some more actual plotting to see if things are stable.
-
New Z axis, Plotter Bed, STEP Files
04/21/2026 at 14:36 • 0 commentsI spent some time reflecting and then decided to actually fix the backlash issue in the Z axis. My initial tests showed that the NEMA 8 motor would be good enough for a lead-screw driven axis. A 35mm M3 bolt was attached to a bracket with a nut and fixed with thread-locker. Then the "lead screw" is fixed to the motor using a coupler.
![]()
The lead-screw, which drives the Z axis, is placed on one side. This posed a challenge because the "rider" would tilt up/down and jam. I looked around at slide mechanisms, and noticed that many optical drives a common layout -- the rider is kept level through triangular geometry. The following shows the current slider layout:
![]()
This works surprisingly well, although is a bit slower than belted axis. I'd need to recalibrate the tool dock and also find the correct rotation-distance.
Update: I'm reasonably close to correct value: 0.03 mm/rotation (the value is explained by the fact that I'm using no microstepping on a driver hard-wired for 16 microsteps. I can't use microstepping because the AVR MCU can't generate steps fast enough.) The probe accuracy is reasonably better:![]()
----
To keep the paper flat I designed a magnetic bed system adapted for A5 paper size. I believe it'd be good enough to cut/score paper as well.![]()
![]()
----
Checkpoint 1: I've published all the current design files in the GitHub repository. The Fusion360 file barely fits under 100MB (zipped) and is fully articulated. Also uploaded the STEP file.
The printer config is no longer valid and I'll update them later.
This took about four months of effort and I'm pleased with the results so far. I think the toolhead could be easily adapted to fit on other CoreXY frames as well, and hence I've grouped it (and others) together in the design files. Majority of the components were sourced from Bambu store and AliExpress.
I'll also post the files to Printables next month along with some instructions.
Link to Files: https://github.com/prashnts/limn/tree/master/step
-
Interactive Plotting
04/15/2026 at 21:15 • 0 commentsI posted a quick video showing how I'm using the plotter currently. It's far from ideal but also quite efficient. Basically just dragging SVGs from Sketch to the slicer. As long as the paper doesn't move we can keep plotting at different areas.
Apart from pen alignment, the core toolchanging part seems to work well now. Here are some plots I've done lately:
![]()
> An image vectorized in Inkscape, 0.05mm
> Drawings from Wikimedia Commons, 0.7mm![]()
> Pioneer Plaque, Paris Metro map (using toolchanger, misaligned). 0.05mm
The paper curled at certain points but overall I'm happy with the results.
![]()
> Paris Metro, Pioneer Plaque, Telescope. 0.7mm
-
RFID Read, Source available on GitHub
04/11/2026 at 15:42 • 0 commentsI posted a screencast where the plotter scans RFID tags on the tools. Still need to find out how to use the values read.
Additionally, I was finally able to clean up a bit the klipper side of things. In the Limn repo I've published the current printer/toolchanger/rfid config. It's under MIT license. The README explains the contents.
This is my first time configuring klipper, so I'd be keen to know what could be done better!
-
Under the hood
04/10/2026 at 19:33 • 0 commentsThe plotter is designed to be scalable. Four components in each corners mate with variable lengths of rods/belts/base.
A hard MDF board and the four steel rods on the edges together form a sufficiently rigid and square frame. I got the rods and bearings from a Tronxy 3D Printer Kit, as well as the driver PCB and XY Stepper Motors.
The driver board uses 12V, and has 4 A4982 driver chips. Z axis is configured to drive dual Z axis motors so we have a different pinout here for the motors.
I'd noticed the small SRM1509 and Linear motors get too hot on 12V regardless of proper VREF, so I cut the 12V trace and supplied external 5V to Z and K (the toolhead lock) axis drivers.
![]()
![]()
When the belts are tensioned the frame is pulled inwards along the belt direction, which reduces the wobble. However it will likely deform when lifted in the current setup. There is not much to do unless we switch to a metal frame.
The tool dock however needed special consideration as we need it to not move and survive the toolhead crashing into it. To keep it positioned the brackets (shown below) mate with a steel rod, and rest on the base. The middle one also acts as a hinge for the LCD.![]()
--
I'm quite close to finishing the design. I've built a 1:1 model in Fusion360, and would just like to give a shout to amazing people on GrabCAD and Printables (and elsewhere) for sharing their models. I'll be posting the STEP files on Printables (about a week or so). There are many parts, with some parts such as the K axis requiring special considerations (and it's pretty specific to the hardware I had at hand) so there's a bit of work left to do to documenting them. Almost all parts print without supports.![]()
![]()
![]()
-
Detecting Tools, Tool Parameters, "Slicing"
04/09/2026 at 12:22 • 0 commentsThe tool docking mechanism relies on Maxwell Coupling for repeatability, however this repeatability only holds per tool. As such, different pens will land at varying positions on the paper for the same coordinate.
My current understanding is that it will be too much effort to precisely align each tool, and rather it'd be easier to calibrate each tool against a reference tool (ideally the sharpest pen). Hence we need a way to configure the tool parameters (`{offset_x, offset_y, offset_z, depth...}`).
For the sake of simplicity it'd be good that the printer handles this at runtime. Any print file could be printed by any tool/tools.
I really did not want to reinvent any more wheels, so I stuck to PrusaSlicer. A new printer config, with custom gcode templates, multi-extruder toolhead, and some regex susbtitution later I can now drag an SVG file in the slicer, and generate the exact GCODE that klipper is expecting.
![]()
Pen Up and Down motion is achieved by replacing `retraction` events. I chose to keep raw Z values here, and change Z-offset on klipper instead, so that the travel moves reflect actual pen moves in the preview. Similarly extrusions are kept only to have something in the preview, but are ignored by G1 macro.
The slicer emits T0..T4 commands for each tool change. On klipper side custom macros translate these commands to actual movement.
![]()
I still haven't figured out varying nozzle sizes while slicing but it should be possible.
Back to detecting tools: A basic detection can be done by physical contact with the coupling surface -- on the tool's side we can bridge two screws with a wire. I'm aware of 1Wire EEPROM being a thing but did not find any to buy. There seems to be several Spool detecting projects, such as OpenSpools, which can use RFID readers to detect the spools. We could use something like that.
To set the parameters there's now a spring-loaded-retractable RFID sensor mounted near the first tool. It can be slid back by the Y axis.
![]()
![]()
(I did not realize that I wired the PN532 with I2C, but OpenSpools needs SPI connection. I'm too lazy to take it out so I'll likely have to figure out an alternative on this.)
On the tools we stick a tag and the docking macro will eventually use the sensor for tool parameters.
-
NEMA 8: No Go & Backlash Correction
04/03/2026 at 15:46 • 0 commentsI received the AliExpress NEMA 8 motors yesterday and replaced the SRM1509 for a test. After tightening the belts I can turn the axis manually but the stepper just buzzes. I tried upping the stepper current till 0.8A but I could stop the shaft with my fingers. Oh well. I went back to the previous motor.
![]()
I wanted to make a note about the pulleys. Normally even if you make the shaft hole good size the pulley would not engage and the little flex it has will allow it to turn. I figured a good way to prevent this is by embedded nuts. Essentially a nut on the flat surface of the shaft, and a screw.
![]()
Since for now we have to live with the gear play I attempted correcting the backlash in software. Klipper allows you to override any macros, including G1, and with a macro variable one can keep track of the direction the motor was going in previously. If we are about to go in opposite direction then extra motor movement can correct for backlash IF it is stable (it is in my case). Only problem is that BED_CALIBRATE does not seem to be using a G1 command so the bed mesh is not corrected.
For plotting without a bed mesh the backlash is not really a big deal. With bed mesh we need to just be careful to not overshoot the end positions on the axis. I haven't found a clean way to do this yet, but I'm looking at /extras dir in klipper right now.
gcode_macro G1] rename_existing: G1.1 # Rename the existing G1 command to G1.1 gcode: {% set is_abs = printer.gcode_move.absolute_coordinates %} {% set curr_z = printer.toolhead.position.z %} {% set param_z = params.Z|default(-42)|float %} {% set new_z = 0 %} {% if 'Z' in params %} {% set next_z = param_z if is_abs else (curr_z + param_z) %} {% set delta = next_z - curr_z %} {% set direction = (delta / delta|abs) if delta != 0 else last_dir %} {% set backlash = 1.8 if direction != last_dir else 0 %} {% set correction = backlash * direction %} SET_GCODE_VARIABLE MACRO=G1 VARIABLE=last_dir VALUE={direction} {% if correction %} {% set step_to = correction if not is_abs else (curr_z + correction) %} # SET_KINEMATIC_POSITION Z={curr_z - correction} SET_HOMED='' G92 Z{curr_z - correction} G1.1 Z{step_to} # SET_KINEMATIC_POSITION Z={curr_z} SET_HOMED='' G92 Z{curr_z} M118 Corrected: Backlash: {backlash} Direction: {direction} {% endif %} {% endif %} {% set p_x = ' X' ~ params.X if 'X' in params else '' %} {% set p_y = ' Y' ~ params.Y if 'Y' in params else '' %} {% set p_z = ' Z' ~ params.Z if 'Z' in params else '' %} {% set p_f = ' F' ~ params.F if 'F' in params else '' %} {% set ps = p_x + p_y + p_z + p_f %} G1.1 {ps} variable_last_dir: 1PS: The 3-point coupling seems to be good enough to pass some electricity, finally!
![]()
-
Z Axis & Toolhead
04/01/2026 at 18:50 • 0 commentsFor a plotter we do not really need large Z axis movement. Initial iterations used a MG14 servo motor for Pen Up/Pen Down motion. It worked but was loud, imprecise, and limited to flat bed.
I found that SRM1509 stepper motors were just strong enough while being small and are what in use currently. These use a gearbox so some trial and error yielded a close but incorrect Z scaling. Additionally there is significant play due to gears and I had to account for the backlash.
The motor module is designed separately so can be changed for a different system later. I plan to switch to NEMA8/NEMA11 motors soon. Currently the belt tension is achieved through three bolts that lift the entire motor and GT2 pulley assembly.
For the motion system 3mm/5cm dowels and bushings are used. Belt rides on 603ZZ bearings. When tensioned, the motor module and Y axis "rider" are rigid. Weighs about 250g!
Attached some pictures.
![]()
![]()
![]()
![]()
-
First Steps
03/31/2026 at 09:03 • 0 commentsMagnets was the answer, as it turns out. For locking the tool, good alignment is necessary. I had planned for, but forgotten to put, magnets on the coupling screws to help the tool positioning. It does a "click" now when taken from the rack and then the lock can be closed.
I posted a video here:
Apologies for vertical orientation, I was too excited!
Next steps: Working on the software stack to go from layered svg to mutli-tool gcode.
Prashant Sinha


4. Writing/updating tool parameters






> Drawings from Wikimedia Commons, 0.7mm

















