Skip to content
Ch 13 › Part L — Add-on: nozzle scrubber with sheet stops52 / 54

Step 13.52 — Add the scrubber macros

Step 52 of 54 · Chapter 13 — Initial startup

(no image — see text)

Parts: none.

Do: Paste the block below under [gcode_macro PRINT_END]. Replace the four placeholder values with the numbers you wrote down, then SAVE & RESTART.

#####################################################################
#   Nozzle scrubber  — ADDED at Ch 13 (add-on). Geometry and macro logic
#   adapted from jinetix, "Voron 2.4 Silicone Nozzle Scrubber w/ Sheet
#   Stops", Printables 796563, macro revision 2025-12-22, CC BY-NC-SA 4.0.
#   Changes: one variable block instead of two copies; bucket on the LEFT
#   (mirrored fit); the circle radius and centre are clamped to this
#   machine's Y overtravel; XY moves happen at park height and Z drops only
#   over the bucket (the author's Z-then-XY order drags the nozzle at scrub
#   height); refuses to run until the values are measured.
#####################################################################

[gcode_arcs]
resolution: 0.1

[gcode_macro _SCRUB]
description: Scrubber geometry. Values only; read by NOZZLE_PARK_BUCKET and NOZZLE_CLEAN.
##  Every value marked (verify on bench) is measured on YOUR machine.
##  The placeholders below make both macros refuse to run.
variable_brush_x_min: -1.0     # (verify on bench) X, nozzle over the OUTBOARD (left) bristle edge
variable_brush_x_max: -1.0     # (verify on bench) X, nozzle over the INBOARD (right) bristle edge
variable_edge_y: -1.0          # (verify on bench) Y, nozzle tip exactly over the plate's rear edge
variable_z_scrub: 99.0         # (verify on bench) Z of the bristle tops minus 0.5
variable_bucket_offset: 10.0   # park this far outboard of brush_x_min, over the bucket
variable_park_z: 5.0
variable_circle_r: 2.0         # largest scrub circle; shrunk to fit the Y overtravel
variable_passes: 5
variable_circles: 2
variable_flicks: 5
variable_flick_depth: 3.0
variable_travel_speed: 9000
variable_flick_speed: 9000
variable_scrub_speed: 3000
variable_z_speed: 1500
gcode:
    {action_respond_info("_SCRUB only holds the scrubber's measured values")}

[gcode_macro NOZZLE_PARK_BUCKET]
description: Lift, then park the nozzle over the purge bucket.
gcode:
    {% set s = printer["gcode_macro _SCRUB"] %}
    {% if s.brush_x_min < 0 or s.brush_x_max <= s.brush_x_min or s.edge_y < 0 or s.z_scrub > 10 %}
      {action_raise_error("Scrubber not measured yet: fill in [gcode_macro _SCRUB]")}
    {% endif %}
    {% if printer.toolhead.homed_axes != "xyz" %}
      {action_raise_error("Scrubber: home all three axes first")}
    {% endif %}
    {% set ymax = printer.toolhead.axis_maximum.y - 0.5 %}
    {% set ylo = s.edge_y + 1.5 %}
    {% set r = [s.circle_r, (ymax - ylo) / 2.0]|min %}
    {% set cy = ylo + r %}
    G90
    G1 Z{[printer.toolhead.position.z, s.park_z]|max} F{s.z_speed}
    G1 X{s.brush_x_min - s.bucket_offset} Y{cy} F{s.travel_speed}
    G1 Z{s.park_z} F{s.z_speed}

[gcode_macro NOZZLE_CLEAN]
description: Flick ooze into the bucket, then scrub the nozzle on the silicone brush.
gcode:
    {% set s = printer["gcode_macro _SCRUB"] %}
    {% set ymax = printer.toolhead.axis_maximum.y - 0.5 %}
    {% set ylo = s.edge_y + 1.5 %}
    {% set r = [s.circle_r, (ymax - ylo) / 2.0]|min %}
    {% if r < 0.75 %}
      {action_raise_error("Scrubber: Y overtravel past the plate is too short for the brush")}
    {% endif %}
    {% set cy = ylo + r %}
    {% set bucket_x = s.brush_x_min - s.bucket_offset %}
    {% set flick_x = s.brush_x_min + s.flick_depth %}
    {% set x0 = s.brush_x_min + 0.5 %}
    {% set x1 = s.brush_x_max - 2 * r - 0.5 %}
    {% set dx = (x1 - x0) / (s.passes - 1) if s.passes > 1 else 0 %}
    NOZZLE_PARK_BUCKET
    G90
    G1 Z{s.z_scrub} F{s.z_speed}                ; down over the bucket, never over the brush
    {% for i in range(s.flicks) %}
      G1 X{flick_x} F{s.flick_speed}            ; into the outboard bristles
      G4 P20
      G1 X{bucket_x} F{s.flick_speed * 1.5}     ; snap back: the string drops in the bucket
    {% endfor %}
    {% for i in range(s.passes) %}
      {% set x = x0 + i * dx %}
      G1 X{x} Y{cy} F{s.travel_speed}
      {% for j in range(s.circles) %}
        {% if (i + j) % 2 == 1 %}
          G3 X{x} Y{cy} I{r} J0 F{s.scrub_speed}
        {% else %}
          G2 X{x} Y{cy} I{r} J0 F{s.scrub_speed}
        {% endif %}
      {% endfor %}
    {% endfor %}
    G1 Z{s.park_z} F{s.z_speed}                 ; lift straight up off the brush

check Check

Klipper restarts without an error, and NOZZLE_PARK_BUCKET parks the nozzle over the bucket at Z5.

What you're looking at

[gcode_arcs] lets Klipper run the small G2/G3 circles of the scrub. _SCRUB holds your numbers; NOZZLE_PARK_BUCKET and NOZZLE_CLEAN read them and refuse to run while any is still a placeholder.

tip Tip

The macro sizes its scrub circles to fit your reach past the plate, from 2 mm radius down to 0.75 mm.

Source: Printables 796563 — macros, revision 2025-12-22 · Klipper docs § gcode_arcs · Klipper docs § Command templates