I wanted to be able to use my Z probe to tram the bed and tell me how to adjust the leveling screws all from the LCD without having to use the computer. I finally got it working, and here’s a video of it in action.
As of 2/3/2023 (commit fa78e6b), this solution works on mainline Klipper without the need for any code patches. To get started, create a new screws_display.cfg
containing the following:
#############################################
### DISPLAY GROUP FOR "MEASURING" MESSAGE ###
#############################################
[display_data screws_running line1]
position: 1, 0
text: { " Measuring Bed." }
[display_data screws_running line2]
position: 2, 0
text: { " Please wait..." }
#########################################
### DISPLAY GROUP FOR "ERROR" MESSAGE ###
#########################################
[display_data screws_error line1]
position: 0, 0
text: Tilt exceeds
[display_data screws_error line2]
position: 1, 0
text: max deviation.
[display_data screws_error line3]
position: 2, 0
text: Adjust screws
[display_data screws_error line4]
position: 3, 0
text: and try again.
########################################
### DISPLAY GROUP FOR RESULTS SCREEN ###
########################################
[display_template _results_template_text]
param_adjust: None
param_base: None
text:
{% if param_base %}
Base
{% else %}
{ '{} '.format(param_adjust) }
{% endif %}
[display_template _results_template_sign]
param_sign: None
text:
{% if param_sign == 'CW' %}
~clockwise_1~~clockwise_2~
{% elif param_sign == 'CCW' %}
~counter_clockwise_1~~counter_clockwise_2~
{% endif %}
# Back left screw
[display_data screws_results back_left_text]
position: 1, 0
text:
{% set bed_width_fifth = printer.configfile.settings.stepper_x.position_max / 5 %}
{% set bed_depth_fifth = printer.configfile.settings.stepper_y.position_max / 5 %}
{% for screw in printer.screws_tilt_adjust.results %}
{% set coords = printer.configfile.settings.screws_tilt_adjust.get(screw) %}
{% set results = printer.screws_tilt_adjust.results.get(screw) %}
{% set y_back = coords[1] >= bed_depth_fifth * 3 %}
{% set x_left = coords[0] <= bed_width_fifth * 2 %}
{% if y_back and x_left %}
{% if results.get('is_base') %}
{ render("_results_template_text", param_base=True) }
{% else %}
{ render("_results_template_text", param_adjust=results.get('adjust')) }
{% endif %}
{% endif %}
{% endfor %}
[display_data screws_results back_left_sign]
position: 0, 1
text:
{% set bed_width_fifth = printer.configfile.settings.stepper_x.position_max / 5 %}
{% set bed_depth_fifth = printer.configfile.settings.stepper_y.position_max / 5 %}
{% for screw in printer.screws_tilt_adjust.results %}
{% set coords = printer.configfile.settings.screws_tilt_adjust.get(screw) %}
{% set results = printer.screws_tilt_adjust.results.get(screw) %}
{% set y_back = coords[1] >= bed_depth_fifth * 3 %}
{% set x_left = coords[0] <= bed_width_fifth * 2 %}
{% if y_back and x_left %}
{% if not results.get('is_base') %}
{ render("_results_template_sign", param_sign=results.get('sign')) }
{% endif %}
{% endif %}
{% endfor %}
# Front left screw
[display_data screws_results front_left_text]
position: 3, 0
text:
{% set bed_width_fifth = printer.configfile.settings.stepper_x.position_max / 5 %}
{% set bed_depth_fifth = printer.configfile.settings.stepper_y.position_max / 5 %}
{% for screw in printer.screws_tilt_adjust.results %}
{% set coords = printer.configfile.settings.screws_tilt_adjust.get(screw) %}
{% set results = printer.screws_tilt_adjust.results.get(screw) %}
{% set y_front = coords[1] <= bed_depth_fifth * 2 %}
{% set x_left = coords[0] <= bed_width_fifth * 2 %}
{% if y_front and x_left %}
{% if results.get('is_base') %}
{ render("_results_template_text", param_base=True) }
{% else %}
{ render("_results_template_text", param_adjust=results.get('adjust')) }
{% endif %}
{% endif %}
{% endfor %}
[display_data screws_results front_left_sign]
position: 2, 1
text:
{% set bed_width_fifth = printer.configfile.settings.stepper_x.position_max / 5 %}
{% set bed_depth_fifth = printer.configfile.settings.stepper_y.position_max / 5 %}
{% for screw in printer.screws_tilt_adjust.results %}
{% set coords = printer.configfile.settings.screws_tilt_adjust.get(screw) %}
{% set results = printer.screws_tilt_adjust.results.get(screw) %}
{% set y_front = coords[1] <= bed_depth_fifth * 2 %}
{% set x_left = coords[0] <= bed_width_fifth * 2 %}
{% if y_front and x_left %}
{% if not results.get('is_base') %}
{ render("_results_template_sign", param_sign=results.get('sign')) }
{% endif %}
{% endif %}
{% endfor %}
# Back right screw
[display_data screws_results back_right_text]
position: 0, 11
text:
{% set bed_width_fifth = printer.configfile.settings.stepper_x.position_max / 5 %}
{% set bed_depth_fifth = printer.configfile.settings.stepper_y.position_max / 5 %}
{% for screw in printer.screws_tilt_adjust.results %}
{% set coords = printer.configfile.settings.screws_tilt_adjust.get(screw) %}
{% set results = printer.screws_tilt_adjust.results.get(screw) %}
{% set y_back = coords[1] >= bed_depth_fifth * 3 %}
{% set x_right = coords[0] >= bed_width_fifth * 3 %}
{% if y_back and x_right %}
{% if results.get('is_base') %}
{ render("_results_template_text", param_base=True) }
{% else %}
{ render("_results_template_text", param_adjust=results.get('adjust')) }
{% endif %}
{% endif %}
{% endfor %}
[display_data screws_results back_right_sign]
position: 1, 12
text:
{% set bed_width_fifth = printer.configfile.settings.stepper_x.position_max / 5 %}
{% set bed_depth_fifth = printer.configfile.settings.stepper_y.position_max / 5 %}
{% for screw in printer.screws_tilt_adjust.results %}
{% set coords = printer.configfile.settings.screws_tilt_adjust.get(screw) %}
{% set results = printer.screws_tilt_adjust.results.get(screw) %}
{% set y_back = coords[1] >= bed_depth_fifth * 3 %}
{% set x_right = coords[0] >= bed_width_fifth * 3 %}
{% if y_back and x_right %}
{% if not results.get('is_base') %}
{ render("_results_template_sign", param_sign=results.get('sign')) }
{% endif %}
{% endif %}
{% endfor %}
# Middle right screw
[display_data screws_results middle_right_text]
position: 1, 11
text:
{% set bed_width_fifth = printer.configfile.settings.stepper_x.position_max / 5 %}
{% set bed_depth_fifth = printer.configfile.settings.stepper_y.position_max / 5 %}
{% for screw in printer.screws_tilt_adjust.results %}
{% set coords = printer.configfile.settings.screws_tilt_adjust.get(screw) %}
{% set results = printer.screws_tilt_adjust.results.get(screw) %}
{% set y_middle = (bed_depth_fifth * 2) < coords[1] < (bed_depth_fifth * 3) %}
{% set x_right = coords[0] >= bed_width_fifth * 3 %}
{% if y_middle and x_right %}
{% if results.get('is_base') %}
{ render("_results_template_text", param_base=True) }
{% else %}
{ render("_results_template_text", param_adjust=results.get('adjust')) }
{% endif %}
{% endif %}
{% endfor %}
[display_data screws_results middle_right_sign]
position: 2, 12
text:
{% set bed_width_fifth = printer.configfile.settings.stepper_x.position_max / 5 %}
{% set bed_depth_fifth = printer.configfile.settings.stepper_y.position_max / 5 %}
{% for screw in printer.screws_tilt_adjust.results %}
{% set coords = printer.configfile.settings.screws_tilt_adjust.get(screw) %}
{% set results = printer.screws_tilt_adjust.results.get(screw) %}
{% set y_middle = (bed_depth_fifth * 2) < coords[1] < (bed_depth_fifth * 3) %}
{% set x_right = coords[0] >= bed_width_fifth * 3 %}
{% if y_middle and x_right %}
{% if not results.get('is_base') %}
{ render("_results_template_sign", param_sign=results.get('sign')) }
{% endif %}
{% endif %}
{% endfor %}
# Front right screw
[display_data screws_results front_right_text]
position: 3, 11
text:
{% set bed_width_fifth = printer.configfile.settings.stepper_x.position_max / 5 %}
{% set bed_depth_fifth = printer.configfile.settings.stepper_y.position_max / 5 %}
{% for screw in printer.screws_tilt_adjust.results %}
{% set coords = printer.configfile.settings.screws_tilt_adjust.get(screw) %}
{% set results = printer.screws_tilt_adjust.results.get(screw) %}
{% set y_front = coords[1] <= bed_depth_fifth * 2 %}
{% set x_right = coords[0] >= bed_width_fifth * 3 %}
{% if y_front and x_right %}
{% if results.get('is_base') %}
{ render("_results_template_text", param_base=True) }
{% else %}
{ render("_results_template_text", param_adjust=results.get('adjust')) }
{% endif %}
{% endif %}
{% endfor %}
[display_data screws_results front_right_sign]
position: 2, 12
text:
{% set bed_width_fifth = printer.configfile.settings.stepper_x.position_max / 5 %}
{% set bed_depth_fifth = printer.configfile.settings.stepper_y.position_max / 5 %}
{% for screw in printer.screws_tilt_adjust.results %}
{% set coords = printer.configfile.settings.screws_tilt_adjust.get(screw) %}
{% set results = printer.screws_tilt_adjust.results.get(screw) %}
{% set y_front = coords[1] <= bed_depth_fifth * 2 %}
{% set x_right = coords[0] >= bed_width_fifth * 3 %}
{% if y_front and x_right %}
{% if not results.get('is_base') %}
{ render("_results_template_sign", param_sign=results.get('sign')) }
{% endif %}
{% endif %}
{% endfor %}
#########################################
### EXTRA GLYPHS FOR SCREW DIRECTIONS ###
#########################################
[display_glyph clockwise_1]
data:
................
.........******.
........***..***
.......**......*
......**........
......*........*
......*.......**
................
................
....******......
....*****.......
....****........
....*****......*
....**..***..***
....*....******.
................
[display_glyph clockwise_2]
data:
................
...*............
..**............
****............
****............
****............
****............
................
................
.*..............
.*..............
**..............
*...............
................
................
................
[display_glyph counter_clockwise_1]
data:
................
....*....******.
....**..***..***
....*****......*
....****........
....*****.......
....******......
................
................
......*.......**
......*........*
......**........
.......**......*
........***..***
.........******.
................
[display_glyph counter_clockwise_2]
data:
................
................
................
*...............
**..............
.*..............
.*..............
................
................
****............
****............
****............
****............
..**............
...*............
................
This also requires a custom menu.cfg
to override defaults. I was already using a custom menu from this thread. Here’s my new menu.cfg
with the bed-tramming additions:
[menu __main]
type: list
name: Main
### TOP-LEVEL MENUS ###
[menu __main __tune]
type: list
enable:
{% set printing = printer.idle_timeout.state == "Printing" %}
{% set mainmenu = printer['gcode_macro GLOBALS'].display_group == printer.configfile.settings.display.display_group %}
{printing and mainmenu}
name: Tune
index: 1
[menu __main __octoprint]
type: list
name: OctoPrint
enable: false
[menu __main __settings]
type: list
name: Settings
enable:
{% set mainmenu = printer['gcode_macro GLOBALS'].display_group == printer.configfile.settings.display.display_group %}
{mainmenu}
index: 2
[menu __main __calibrate]
type: list
enable:
{% set printing = printer.idle_timeout.state == "Printing" %}
{% set mainmenu = printer['gcode_macro GLOBALS'].display_group == printer.configfile.settings.display.display_group %}
{not printing and mainmenu}
name: Calibrate
index: 3
[menu __main __control]
type: list
enable:
{% set mainmenu = printer['gcode_macro GLOBALS'].display_group == printer.configfile.settings.display.display_group %}
{mainmenu}
name: Control
[menu __main __temp]
type: list
enable:
{% set mainmenu = printer['gcode_macro GLOBALS'].display_group == printer.configfile.settings.display.display_group %}
{mainmenu}
name: Temperature
[menu __main __filament]
type: list
enable:
{% set mainmenu = printer['gcode_macro GLOBALS'].display_group == printer.configfile.settings.display.display_group %}
{mainmenu}
name: Filament
[menu __main __sdcard]
type: vsdlist
enable: false
name: SD Card
[menu __main __setup]
type: list
enable:
{% set printing = printer.idle_timeout.state == "Printing" %}
{% set mainmenu = printer['gcode_macro GLOBALS'].display_group == printer.configfile.settings.display.display_group %}
{not printing and mainmenu}
name: Setup
### SETTINGS SUB-MENU ###
[menu __main __settings __pause]
type: command
enable: {printer.idle_timeout.state == "Printing"}
name: Pause printing
gcode: PAUSE
[menu __main __settings __resume]
type: command
enable: {not printer.idle_timeout.state == "Printing"}
name: Resume printing
gcode: RESUME
[menu __main __settings __abort]
type: command
enable: {printer.idle_timeout.state == "Printing"}
name: Abort printing
gcode: CANCEL_PRINT
### CONTROL SUB-MENU ###
[menu __main __control __home]
type: command
enable: false
name: Home All
[menu __main __control __smart_home]
type: command
name: Smart Home All
index: 1
gcode: SMART_HOME
### CALIBRATE SUB-MENU ###
[menu __main __calibrate __calibration_mesh_calibrate]
type: command
name: Measure Bed Mesh
gcode: MEASURE_BED_MESH
[menu __main __calibrate __calibration_bed_tram]
type: command
name: Tram Bed
gcode:
{(menu.exit(true))}
TRAM_BED
[menu __main __calibrate __calibration_probe_calibrate]
type: list
name: Probe Calibrate
[menu __main __calibrate __calibration_probe_calibrate __calibrate]
type: command
name: Probe Calib.
gcode:
SMART_HOME
PROBE_CALIBRATE
[menu __main __calibrate __calibration_probe_calibrate __adjust_Z+1]
type: command
name: Z+1: {'%05.1f' % printer.gcode_move.position.z}
gcode: TESTZ Z=+1
[menu __main __calibrate __calibration_probe_calibrate __adjust_Z-1]
type: command
name: Z-1: {'%05.1f' % printer.gcode_move.position.z}
gcode: TESTZ Z=-1
[menu __main __calibrate __calibration_probe_calibrate __adjust_Z+.1]
type: command
name: Z+.1: {'%05.1f' % printer.gcode_move.position.z}
gcode: TESTZ Z=+.1
[menu __main __calibrate __calibration_probe_calibrate __adjust_Z-.1]
type: command
name: Z-.1: {'%05.1f' % printer.gcode_move.position.z}
gcode: TESTZ Z=-.1
[menu __main __calibrate __calibration_probe_calibrate __adjust_Zpp]
type: command
name: Z+: {'%05.1f' % printer.gcode_move.position.z}
gcode: TESTZ Z=+
[menu __main __calibrate __calibration_probe_calibrate __adjust_Zmm]
type: command
name: Z-: {'%05.1f' % printer.gcode_move.position.z}
gcode: TESTZ Z=-
[menu __main __calibrate __calibration_probe_calibrate __calibration_accept]
type: command
name: Accept Adj.
gcode:
ACCEPT
SAVE_CONFIG
{(menu.back.True)}
[menu __main __calibrate __calibration_probe_calibrate __calibration_abort]
type: command
name: Abort
gcode:
ABORT
{(menu.back.True)}
### SECONDARY MENU FOR BED TRAMMING RESULTS SCREEN ###
[menu __main __redo_tramming]
type: command
enable:
{printer['gcode_macro GLOBALS'].display_group == 'screws_results'}
name: Measure Again
gcode:
{(menu.exit(true))}
TRAM_BED
index: 0
[menu __main __finished]
type: command
enable:
{printer['gcode_macro GLOBALS'].display_group == 'screws_results'}
name: Done
gcode:
SET_DISPLAY_GROUP GROUP={printer.configfile.settings.display.display_group}
SET_GCODE_VARIABLE MACRO=GLOBALS VARIABLE=display_group VALUE="'_default_16x4'"
{(menu.exit(true))}
index: 1
Finally, some macros need to be added to printer.cfg
:
This one is a hack to store a variable that is used to keep track of what menu structure to display.
[gcode_macro GLOBALS]
variable_display_group:"_default_16x4"
gcode:
M115 ; must provide something
This one will home all axes that aren’t already homed. This isn’t strictly necessary but it saves time when repeating the measurements.
[gcode_macro SMART_HOME]
gcode:
{% set toHome = [] %}
{% if 'x' not in printer.toolhead.homed_axes %}
{ toHome.append('x') or "" }
{% endif %}
{% if 'y' not in printer.toolhead.homed_axes %}
{ toHome.append('y') or "" }
{% endif %}
{% if 'z' not in printer.toolhead.homed_axes %}
{ toHome.append('z') or "" }
{% endif %}
{% if toHome|length == 0 %}
{ action_respond_info('All axes homed!') }
{% else %}
G28 { toHome|join(' ') }
{% endif %}
This one is the magic.
[gcode_macro TRAM_BED]
gcode:
SET_DISPLAY_GROUP GROUP=screws_running
SET_GCODE_VARIABLE MACRO=GLOBALS VARIABLE=display_group VALUE="'screws_running'"
SMART_HOME
screws_tilt_calculate
{% if printer.screws_tilt_adjust.error %}
SET_DISPLAY_GROUP GROUP=screws_error
{% else %}
SET_DISPLAY_GROUP GROUP=screws_results
SET_GCODE_VARIABLE MACRO=GLOBALS VARIABLE=display_group VALUE="'screws_results'"
{% endif %}
This should work with any bed that has 4 leveling screws. It should also work with 3 screws as long as the third screw (the “middle” one) is on the side of the bed opposite the X endstop and has a Y location roughly half way between the other two screws. In other words, on an otherwise stock Ender 3 that’s been converted to three bed screws, if there are two screws on the left and one on the right, this should work.