Saving and adjusting per-build-surface Z offsets

I have a couple of different build surfaces I use, and they require different Z offsets. On one side of my flex plate I have a textured PEI surface, and on the other a smooth PEI sheet. I found setting and storing the Z offset to be really tedious. Fortunately I was able to use the menu system to build a system for choosing a build surface from a preconfigured list, save the Z offset during live tuning, and apply the offset automatically at the start of a print. I’m using the save_variables config option; modification to these variables via the SAVE_VARIABLE command are applied immediately and accessible when referencing the printer.save_variables.variables dict.

Changing the bed surface and applying its stored Z offset is as simple as going to the Setup menu on your attached display and choosing from the Bed: list.



filename: ~/klipper_config/saved_vars.cfg


Populate bed_surfaces with a list of your print surfaces. These need to be variables, not friendly names; if you’ve got multiple smooth surfaces, name them something like smooth_01 and smooth_02. Note the format: the value needs to be an eval-able Python string that returns a list of strings.

Set the selected_bed_surface key to one of the entries in the bed_surfaces list, and note the quotes around the value.

For each item in the bed_surfaces list, create a saved variable prefixed with bed_surface_offsets.: these will store the per-surface offset values.

Below is a slightly-formatted version of my current vars file:

bed_surfaces = ['smooth', 'textured']

selected_bed_surface = 'smooth'

bed_surface_offsets.smooth = -0.185
bed_surface_offsets.textured = -0.17


;; choose a bed surface from a pre-configured list
[menu __main __setup __bed_surface]
type: input
name: Bed: { printer.save_variables.variables.bed_surfaces[menu.input | int] }
input: { printer.save_variables.variables.bed_surfaces.index(printer.save_variables.variables.selected_bed_surface) | float }
input_min: 0.0
input_max: { ((printer.save_variables.variables.bed_surfaces | length) - 1) | float }
input_step: 1.0
    SAVE_VARIABLE VARIABLE=selected_bed_surface VALUE="'{ printer.save_variables.variables.bed_surfaces[menu.input | int] }'"

;; tunes the Z offset from the setup menu, saves the offset
;; can be performed while not actively printing
[menu __main __setup __offsetz]
type: input
name: Offset Z:{ '%05.3f' % menu.input }
input: { printer.save_variables.variables["bed_surface_offsets." + printer.save_variables.variables.selected_bed_surface] | float }
input_min: -5
input_max: 5
input_step: 0.005
realtime: True
    RESPOND TYPE=command MSG="{ 'bed surface: %s, offset: %r' % (printer.save_variables.variables.selected_bed_surface, menu.input) }"
    SET_GCODE_OFFSET Z={ '%.3f' % menu.input } MOVE={ 1 if printer.toolhead.homed_axes == 'XYZ' else 0 }
    SAVE_VARIABLE VARIABLE=bed_surface_offsets.{ printer.save_variables.variables.selected_bed_surface } VALUE={ '%.3f' % menu.input }

;; tunes the Z offset from the tune menu, saves the offset
[menu __main __tune __offsetz]
type: input
name: Offset Z:{ '%05.3f' % menu.input }
input: { printer.gcode_move.homing_origin.z }
input_min: -5
input_max: 5
input_step: 0.005
realtime: True
    RESPOND TYPE=command MSG="{ 'bed surface: %s, offset: %r' % (printer.save_variables.variables.selected_bed_surface, menu.input) }"
    SET_GCODE_OFFSET Z={ '%.3f' % menu.input } MOVE=1
    SAVE_VARIABLE VARIABLE=bed_surface_offsets.{ printer.save_variables.variables.selected_bed_surface } VALUE={ '%.3f' % menu.input }


    {% set svv = printer.save_variables.variables %}


    ;; apply Z offset for configured bed surface
    RESPOND TYPE=command MSG="{ 'bed surface: %s, offset: %r' % (svv.selected_bed_surface, svv['bed_surface_offsets.' + svv.selected_bed_surface]) }"
    SET_GCODE_OFFSET MOVE=1 Z={ '%.3f' % (svv["bed_surface_offsets." + svv.selected_bed_surface] | float) }


Great to see save_variables being used in the real world. This was my first python project :smile:

1 Like

I love it! I find it super useful and it’s a much more flexible system than writing bytes to eeprom! I do kind of wish it was using json for storage, but this format is certainly consistent with the other config files. Thank you for your work!

Glad you like it. Could not have done it without Kevin’s help though.

So the way you have this set up, are the values (both negative) listed in the Variables being added to the existing Z offset value already set in printer.cfg?

Yes. The offset is fed directly to SET_GCODE_OFFSET: G-Codes - Klipper documentation

Will this work if you don’t have an attached display?
I have a v0.1 with no display, but use an old phone to access the Mainsail web interface while at the printer.

What’s posted here is kind of tied to the display, unfortunately. I did do some refactoring, however, after I had to yank the display off my printer.

[gcode_macro SET_BED_SURFACE]
description: stores and optionally activates the gcode offset for a given bed surface
    {% set svv = printer.save_variables.variables %}

    {% set BED_SURFACE = params.NAME | default(svv.selected_bed_surface) %}
    {% set Z_OFFSET    = params.OFFSET | default(printer.gcode_move.homing_origin.z) | float %}
    {% set MOVE        = params.MOVE | default(0) | int %}

    {% set OLD_Z_OFFSET = svv['bed_surface_offsets.' + BED_SURFACE] | default("unknown") %}

    SAVE_VARIABLE VARIABLE=selected_bed_surface VALUE="'{ BED_SURFACE }'"
    SAVE_VARIABLE VARIABLE=bed_surface_offsets.{ BED_SURFACE } VALUE={ '%.3f' % Z_OFFSET }

    RESPOND TYPE=command MSG="{ 'bed surface: %s, offset: %r -> %05.3f' % (BED_SURFACE, OLD_Z_OFFSET, Z_OFFSET) }"


With SET_BED_SURFACE you can just use the standard baby-stepping buttons/macros to set the offset, and then call SET_BED_SURFACE NAME=smooth to save the current offset value for the smooth surface, and it sets the smooth surface as the selected one. You can run that macro from your Klipper/Moonraker interface of choice.


It will be added to the existing offset? So it will not be set to this absolute value, whatever the current value is?

I made a version of this that works like the Prusa firmware. I missed the way my MK3S worked. Build Sheet Manager & "Adjust Live Z" - #2 by garethky
It’s similar to this macro but with some important differences:

  • you don’t have to edit the save variables file, everything, including creating a new sheet is through commands.
  • you can use human readable names for build sheets
  • a macro overrides SET_GCODE_OFFSET to capture baby stepping moves. This works with all of the front ends.
  • baby stepping moves are auto saved to the sheet. No need to remember to save or run an additional command. (This is the Prusa magic)
  • it knows the difference between baby stepping and regular z offset adjustments and it only records baby stepping.
  • it works with CALIBRATE_Z because it’s not an absolute offset. Think of it as storing the amount of “squish” you want per sheet.
  • I worked out the right JINJA incantation to store the values as JSON objects.