######################################### ########## ADAPTIVE BED MESH ############ ######################################### # Written by Frix_x#0161 # # @version: 4.0 # CHANGELOG: # v4.0: - patched and re-simplified the macro to work with latest Klipper changes: zero_reference_position is much more constrained and all the fancy stuff # that was used in the past for homing over the RRI with virtual probe or Z calibration plugin is not possible anymore. Now homing must be done at # fixed zero_reference_poisition and can't be dynamic anymore (but it's also simpler) # - this change also allow the mesh to have even probe point now for more flexibility # v3.0: - added the use of [exclude_object] tags to extract the first layer bounding box (many thanks to Kyleisah for the excellent idea and inspiration) # the macro is still fully compatible with the old way using the SIZE parameter: it will use it if specified, or else # fallback to the [exclude_object] method and if both are not available, it will do a full and normal bed mesh as usual. # - also added a FORCE_MESH parameter to mesh even for very small parts # - removed the RRI that was always added put in the BED_MESH_CALIBRATE call. Now it's added only when there is one defined in the [bed_mesh] section # v2.3: moved the install notes into a proper markdown file in: docs > features > adaptive_bed_mesh.md # v2.2: removed the requirement to set mesh_pps in the [bed_mesh] section. It's now again optional as it should be # v2.1: fix for the nominal mesh (when no SIZE parameter is used or SIZE=0_0_0_0) # v2.0: split in multple macros to be able to use the center point in the z calibration bed probing position before doing the mesh # v1.1: fix for a bug when parsing string when using uppercase letters in the [bed_mesh] section # v1.0: first adaptive bed mesh macro # ------------------------------------------------------------------------------------------------------------------------- # If you want to use it into your own config, please install it as a standalone macro as described in the # installation section of this file: docs > features > adaptive_bed_mesh.md # ------------------------------------------------------------------------------------------------------------------------- ### What is it ? ### # The adaptive bed mesh is simple: it's a normal bed mesh, but only "where" and "when" it's necessary. # Sometime I print small parts, sometime I print full plates and I like to get a precise bed_mesh (like 9x9 or more). However, it take a # lot of time and it's useless to probe all the plate for only a 5cm² part. So this is where the adaptive bed mesh is helping: # 1. It get the corners coordinates of the fisrt layer surface either from the slicer or the [exclude_object] tags # 2. It compute a new set of points to probe on this new zone to get at least the same precision as your standard bed mesh. For example, if # a normal bed mesh is set to 9x9 for 300mm², it will then compute 3x3 for a 100mm² surface. Also if for whatever reason your parts are in # the corner of the build plate (like for a damaged PEI in the center), it will follow them to probe this exact area. # 3. To go further, it will not do any bed_mesh if there is less than 3x3 points to probe (very small part alone) and choose/change the # algorithm (bicubic/lagrange) depending of the size and shape of the mesh computed (like 3x3 vs 3x9) # Feel free to ping me on Discord (Frix_x#0161) if you need help or have any comments to improve it :) # =========================================================================================================== # DO NOT MODIFY THOSE VARIABLES (they are used internaly by the adaptive bed mesh macro) [gcode_macro _ADAPTIVE_MESH_VARIABLES] variable_ready: False variable_do_mesh: False variable_do_nominal: False variable_mesh_min: 0,0 variable_mesh_max: 0,0 variable_probe_count: 0,0 variable_algo: "bicubic" gcode: [gcode_macro COMPUTE_MESH_PARAMETERS] description: Compute the mesh parameters and store them for later use gcode: # 1 ----- GET ORIGINAL BEDMESH PARAMS FROM CONFIG ---------------------- {% set xMinConf, yMinConf = printer["configfile"].config["bed_mesh"]["mesh_min"].split(',')|map('trim')|map('int') %} {% set xMaxConf, yMaxConf = printer["configfile"].config["bed_mesh"]["mesh_max"].split(',')|map('trim')|map('int') %} {% set xProbeCntConf, yProbeCntConf = printer["configfile"].config["bed_mesh"]["probe_count"].split(',')|map('trim')|map('int') %} {% set algo = printer["configfile"].config["bed_mesh"]["algorithm"]|lower %} {% set xMeshPPS, yMeshPPS = (printer["configfile"].config["bed_mesh"]["mesh_pps"]|default('2,2')).split(',')|map('trim')|map('int') %} {% set margin = params.MARGIN|default(5)|int %} # additional margin to mesh around the first layer {% set force_mesh = params.FORCE_MESH|default(False) %} # force the mesh even if it's a small part (ie. computed less than 3x3) # 2 ----- GET FIRST LAYER COORDINATES and SIZE ------------------------------------- # If the SIZE parameter is defined and not a dummy placeholder, we use it to do the adaptive bed mesh logic {% set coordinatesFound = false %} {% if params.SIZE is defined and params.SIZE != "0_0_0_0" %} RESPOND MSG="Got a SIZE parameter for the adaptive bed mesh" {% set xMinSpec, yMinSpec, xMaxSpec, yMaxSpec = params.SIZE.split('_')|map('trim')|map('int') %} {% set coordinatesFound = true %} {% elif printer.exclude_object is defined %} {% if printer.exclude_object.objects %} # Else if SIZE is not defined, we fallback to use the [exclude_object] tags # This method is derived from Kyleisah KAMP repository: https://github.com/kyleisah/Klipper-Adaptive-Meshing-Purging) RESPOND MSG="No SIZE parameter, using the [exclude_object] tags for the adaptive bed mesh" {% set eo_points = printer.exclude_object.objects|map(attribute='polygon')|sum(start=[]) %} {% set xMinSpec = eo_points|map(attribute=0)|min %} {% set yMinSpec = eo_points|map(attribute=1)|min %} {% set xMaxSpec = eo_points|map(attribute=0)|max %} {% set yMaxSpec = eo_points|map(attribute=1)|max %} {% set coordinatesFound = true %} {% endif %} {% endif %} {% if not coordinatesFound %} # If no SIZE parameter and no [exclude_object] tags, then we want to do a nominal bed mesh # so nothing to do here... RESPOND MSG="No info about the first layer coordinates, doing a nominal bed mesh instead of adaptive" {% endif %} # If the first layer size was correctly retrieved, we can do the adaptive bed mesh logic, else we # fallback to the original and nominal BED_MESH_CALIBRATE function (full bed probing) {% if xMinSpec and yMinSpec and xMaxSpec and yMaxSpec %} # 3 ----- APPLY MARGINS ---------------------------------------------- # We use min/max function as we want it to be constrained by the original # bedmesh size. This will avoid going outside the machine limits {% set xMin = [xMinConf, (xMinSpec - margin)]|max %} {% set xMax = [xMaxConf, (xMaxSpec + margin)]|min %} {% set yMin = [yMinConf, (yMinSpec - margin)]|max %} {% set yMax = [yMaxConf, (yMaxSpec + margin)]|min %} # 4 ----- COMPUTE A NEW PROBE COUNT ---------------------------------- # The goal is to have at least the same precision as from the config. So we compute an equivalent number # of probe points on each X/Y dimensions (distance between two points should be the same as in the config) {% set xProbeCnt = ((xMax - xMin) * xProbeCntConf / (xMaxConf - xMinConf))|round(0, 'ceil')|int %} {% set yProbeCnt = ((yMax - yMin) * yProbeCntConf / (yMaxConf - yMinConf))|round(0, 'ceil')|int %} # Then, three possibilities : # a) Both dimensions have less than 3 probe points : the bed_mesh is not needed as it's a small print (if not forced). # b) If one of the dimension is less than 3 and the other is greater. The print looks to be elongated and # need the adaptive bed_mesh : we add probing points to the small direction to reach 3 and be able to do it. # c) If both direction are greater than 3, we need the adaptive bed_mesh and it's ok. # At the end we control (according to Klipper bed_mesh method: "_verify_algorithm") that the computed probe_count is # valid according to the choosen algorithm or change it if needed. {% if xProbeCnt < 3 and yProbeCnt < 3 %} {% if force_mesh %} RESPOND MSG="Bed mesh forced (small part detected): meshing 3x3..." {% set xProbeCnt = 3 %} {% set yProbeCnt = 3 %} {% set algo = "lagrange" %} {% set mesh_min = "%d,%d"|format(xMin, yMin) %} {% set mesh_max = "%d,%d"|format(xMax, yMax) %} {% set probe_count = "%d,%d"|format(xProbeCnt, yProbeCnt) %} RESPOND MSG="Computed mesh parameters: MESH_MIN={mesh_min} MESH_MAX={mesh_max} PROBE_COUNT={probe_count} ALGORITHM={algo}" SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_mesh VALUE={True} SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_nominal VALUE={False} SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_min VALUE='"{mesh_min}"' SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_max VALUE='"{mesh_max}"' SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=probe_count VALUE='"{probe_count}"' SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=algo VALUE='"{algo}"' {% else %} RESPOND MSG="Computed mesh parameters: none, bed mesh not needed for very small parts" SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_mesh VALUE={False} {% endif %} {% else %} {% set xProbeCnt = [3, xProbeCnt]|max %} {% set yProbeCnt = [3, yProbeCnt]|max %} # Check of the probe points and interpolation algorithms according to Klipper code {% if xMeshPPS != 0 or yMeshPPS != 0 %} {% set probeCntMin = [xProbeCnt, yProbeCnt]|min %} {% set probeCntMax = [xProbeCnt, yProbeCnt]|max %} {% if algo == "lagrange" and probeCntMax > 6 %} # Lagrange interpolation tends to oscillate when using more than 6 samples: swith to bicubic {% set algo = "bicubic" %} {% endif %} {% if algo == "bicubic" and probeCntMin < 4 %} {% if probeCntMax > 6 %} # Impossible case: need to add probe point on the small axis to be >= 4 (we want 5 to keep it odd) {% if xProbeCnt > yProbeCnt %} {% set yProbeCnt = 5 %} {% else %} {% set xProbeCnt = 5 %} {% endif %} {% else %} # In this case bicubic is not adapted (less than 4 points): switch to lagrange {% set algo = "lagrange" %} {% endif %} {% endif %} {% endif %} # 5 ----- FORMAT THE PARAMETERS AND SAVE THEM --------------------------- {% set mesh_min = "%d,%d"|format(xMin, yMin) %} {% set mesh_max = "%d,%d"|format(xMax, yMax) %} {% set probe_count = "%d,%d"|format(xProbeCnt, yProbeCnt) %} RESPOND MSG="Computed mesh parameters: MESH_MIN={mesh_min} MESH_MAX={mesh_max} PROBE_COUNT={probe_count} ALGORITHM={algo}" SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_mesh VALUE={True} SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_nominal VALUE={False} SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_min VALUE='"{mesh_min}"' SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_max VALUE='"{mesh_max}"' SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=probe_count VALUE='"{probe_count}"' SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=algo VALUE='"{algo}"' {% endif %} {% else %} SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_mesh VALUE={True} SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_nominal VALUE={True} {% endif %} # Finaly save in the variables that we already computed the values SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=ready VALUE={True} [gcode_macro ADAPTIVE_BED_MESH] description: Perform a bed mesh, but only where and when it's needed gcode: {% set ready = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].ready %} {% if not 'xyz' in printer.toolhead.homed_axes %} { action_raise_error("Must Home printer first!") } {% endif %} # If the parameters where computed, we can do the mesh by calling the _DO_ADAPTIVE_MESH {% if ready %} _DO_ADAPTIVE_MESH # If the parameters where not computed prior to the ADAPTIVE_BED_MESH call, we call the COMPUTE_MESH_PARAMETERS # macro first and then call the _DO_ADAPTIVE_MESH macro after it {% else %} RESPOND MSG="Adaptive bed mesh: parameters not already computed, automatically calling the COMPUTE_MESH_PARAMETERS macro prior to the mesh" COMPUTE_MESH_PARAMETERS {rawparams} M400 # mandatory to flush the gcode buffer and be sure to use the last computed parameters _DO_ADAPTIVE_MESH {% endif %} [gcode_macro _DO_ADAPTIVE_MESH] gcode: # 1 ----- POPULATE BEDMESH PARAMS FROM SAVED VARIABLES ---------------------- {% set do_mesh = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].do_mesh %} {% set do_nominal = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].do_nominal %} {% set mesh_min = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].mesh_min %} {% set mesh_max = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].mesh_max %} {% set probe_count = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].probe_count %} {% set algo = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].algo %} # 2 --------- ADAPTIVE_BED_MESH LOGIC -------------------------------------- # If it's necessary to do a mesh {% if do_mesh %} # If it's a standard bed_mesh to be done {% if do_nominal %} RESPOND MSG="Adaptive bed mesh: nominal bed mesh" BED_MESH_CALIBRATE {% else %} RESPOND MSG="Adaptive bed mesh: MESH_MIN={mesh_min} MESH_MAX={mesh_max} PROBE_COUNT={probe_count} ALGORITHM={algo}" BED_MESH_CALIBRATE MESH_MIN={mesh_min} MESH_MAX={mesh_max} PROBE_COUNT={probe_count} ALGORITHM={algo} {% endif %} {% else %} RESPOND MSG="Adaptive bed mesh: no mesh to be done" {% endif %} # Set back the 'ready' parameter to false SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=ready VALUE={False}