diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 725a294c5..80bae72cc 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2104,6 +2104,89 @@ calibrate_y: 112.5 # be near the center of the bed ``` +### ⚠️ [z_calibration] + +Automatic Z offset calibration. One may define this section if the printer +is able to calibrate the nozzle's offset automatically. See +[Z-Calibration guide](Z_Calibration.md) and +[command reference](G-Codes.md#automatic-z-offset-calibration) for further +information. + +``` +[z_calibration] +nozzle_xy_position: +# A X, Y coordinate (e.g. 100,100) of the nozzle, clicking on the Z endstop. +switch_xy_position: +# A X, Y coordinate (e.g. 100,100) of the probe's switch body, clicking on +# the Z endstop. +bed_xy_position: default from relative_reference_index of bed_mesh +# a X, Y coordinate (e.g. 100,100) where the print surface (e.g. the center +# point) is probed. These coordinates will be adapted by the +# probe's X and Y offsets. The default is the relative_reference_index +# of the configured bed_mesh, if configured. It's possible to change the relative +# reference index at runtime or use the GCode argument BED_POSITION of CALIBRATE_Z. +switch_offset: +# The trigger point offset of the used mag-probe switch. +# Larger values will position the nozzle closer to the bed. +# This needs to be find out manually. More on this later +# in this section.. +max_deviation: 1.0 +# The maximum allowed deviation of the calculated offset. +# If the offset exceeds this value, it will stop! +# The default is 1.0 mm. +samples: default from "probe:samples" section +# The number of times to probe each point. The probed z-values +# will be averaged. The default is from the probe's configuration. +samples_tolerance: default from "probe:samples_tolerance" section +# The maximum Z distance (in mm) that a sample may differ from other +# samples. The default is from the probe's configuration. +samples_tolerance_retries: default from "probe:samples_tolerance_retries" section +# The number of times to retry if a sample is found that exceeds +# samples_tolerance. The default is from the probe's configuration. +samples_result: default from "probe:samples_result" section +# The calculation method when sampling more than once - either +# "median" or "average". The default is from the probe's configuration. +clearance: 2 * z_offset from the "probe:z_offset" section +# The distance in mm to move up before moving to the next +# position. The default is two times the z_offset from the probe's +# configuration. +position_min: default from "stepper_z:position_min" section. +# Minimum valid distance (in mm) used for probing move. The +# default is from the Z rail configuration. +speed: 50 +# The moving speed in X and Y. The default is 50 mm/s. +lift_speed: default from "probe:lift_speed" section +# Speed (in mm/s) of the Z axis when lifting the probe between +# samples and clearance moves. The default is from the probe's +# configuration. +probing_speed: default from "stepper_z:homing_speed" section. +# The fast probing speed (in mm/s) used, when probing_first_fast +# is activated. The default is from the Z rail configuration. +probing_second_speed: default from "stepper_z:second_homing_speed" section. +# The slower speed (in mm/s) for probing the recorded samples. +# The default is second_homing_speed of the Z rail configuration. +probing_retract_dist: default from "stepper_z:homing_retract_dist" section. +# Distance to retract (in mm) before probing the next sample. +# The default is homing_retract_dist from the Z rail configuration. +probing_first_fast: false +# If true, the first probing is done faster by the probing speed. +# This is to get faster down and the result is not recorded as a +# probing sample. The default is false. +start_gcode: +# A list of G-Code commands to execute prior to each calibration command. +# See docs/Command_Templates.md for G-Code format. This can be used to +# attach the probe. +before_switch_gcode: +# A list of G-Code commands to execute prior to each probing on the +# mag-probe. See docs/Command_Templates.md for G-Code format. This can be +# used to attach the probe after probing on the nozzle and before probing +# on the mag-probe. +end_gcode: +# A list of G-Code commands to execute after each calibration command. +# See docs/Command_Templates.md for G-Code format. This can be used to +# detach the probe afterwards. +``` + ## Additional stepper motors and extruders ### [stepper_z1] diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 8fb7108c7..3fbb9e2cb 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -1212,7 +1212,7 @@ profile matching the supplied name from persistent memory. Note that after SAVE or REMOVE operations have been run the SAVE_CONFIG gcode must be run to make the changes to persistent memory permanent. -### [smart_effector] +### ⚠️ [smart_effector] Several commands are available when a [smart_effector config section](Config_Reference.md#smart_effector) is enabled. @@ -1397,6 +1397,22 @@ the config. `REF_TEMP` manually overrides the reference temperature typically set during homing (for use in e.g. non-standard homing routines) - will be reset automatically upon homing. +### ⚠️ [z_calibration] + +The following commands are available when a +[z_calibration config section](Config_Reference.md#z_calibration) is enabled +(also see the [Z-Calibration guide](Z_Calibration.md)): +- `CALIBRATE_Z`: This calibrates the current offset between the nozzle and + the print surface. +- `PROBE_Z_ACCURACY [PROBE_SPEED=] [LIFT_SPEED=] [SAMPLES=] + [SAMPLE_RETRACT_DIST=]`: Calculate the maximum, minimum, + average, median, and standard deviation of multiple probe + samples. By default, 10 SAMPLES are taken. Otherwise the optional + parameters default to their equivalent setting in the z_calibration or probe + config section. +*Note* that appropriate macros and/or configurations are needed to attach +and detach a mag-probe for these commands! + ### [z_tilt] The following commands are available when the diff --git a/docs/Z_Calibration.md b/docs/Z_Calibration.md new file mode 100644 index 000000000..38e8e78a7 --- /dev/null +++ b/docs/Z_Calibration.md @@ -0,0 +1,289 @@ +# Automatic Z-Offset Calibration + +This document provides information on calibrating the nozzle's Z offset +automatically. With this enabled, manual Z offset or first layer +calibrations are needless. It computes always the correct offset independantly +of the current temperature, used nozzle or used print bed or flex plate. + +# Why This + +- The Z endstop used in Voron V1 or V2 printers is a clever one because the + nozzle clicks on a switch which is fixed to the print bed. This enables the + exchange of nozzles without changing the offset (between switch and bed): + ![endstop offset](img/z_calibrate-endstop.png) +- Or, by using a surface probing probe like a mag-probe as the Z endstop. + This enables the exchange of flex plates without adapting the offset: + ![probe offset](img/z_calibrate-probe.png) + An inductive probe would not work, since it does not probe the surface of + the bed directly! +- But, isn't it possible to get both of it? + +It is possible and this it is what this extension does! + +# Requirements + +But, there are some requirements to use it: + +- A Z endstop where the tip of the nozzle drives on a switch (like the stock + Voron V1/V2 enstop). It will not work with the virtual pin of the probe + configured as endstop! +- A (magnetic) switch based probe at the print head +- Both, the Z endstop and mag-probe are configured properly and homing and any + kind of bed leveling are working. +- Attach and detach macros of the mag-probe are needed for this configuration. + +# What It Does + +1. A normal homing of all axes using the Z endstop for Z (this is not part of + this plugin). After that, there is a defined zero point in Z. From now on, + everything is in relation to this point. So, a new homing would change + everything, since the homing is not that precise. +2. Determine the height of the nozzle by probing the tip of it on the Z endstop + (like the homing in step 1. But this one can result in a slightly different + value): + ![nozzle position](img/z_calibrate-nozzle.png) +3. Determine the height of the mag-probe by probing the body of the switch on + the z-endstop: + ![switch position](img/z_calibrate-switch.png) +4. Calculate the offset between the tip of the nozzle and the trigger point of + the mag-probe: + + `nozzle switch offset = mag probe height - nozzle height + switch offset` + + ![switch offset](img/z_calibrate-offset.png) + + The trigger point of the mag-probe cannot be probed directly. This is why + the body of the switch is clicked on the endstop indstead of the trigger + nob. This is why a small switch offset is used here to reflect the offset + between the nob and the body of the switch while it is triggerd. + This offset is fixed. +5. Determine the height of the print surface by probing one point with the + mag-probe on the bed (preferably the center or the + "bed_mesh:relative_reference_index" of a configured/used mesh). +6. Now, the final offset is calculated like this: + + `probe offset = probed height - calculated nozzle switch offset` + +7. Finally, the calculated offset is applied by using the `SET_GCODE_OFFSET` + command (a previous offset is resetted before!). + +## Interference + +Temperature or humindity changes are not a big deal since the switch is not +affected much by them and all values are probed in a small time period and only +the releations to each other are used. The nozzle height in step 2 can be +determined some time later and even many celsius higher in the printer's +chamber, compared to the homing in step 1. That is why the nozzle is probed +again and can vary a little to the first homing position. + +## Example Output + +The output of the calibration with all determined positions looks like this +(the offset is the one which is applied as GCode offset): + +``` +Z-CALIBRATION: ENDSTOP=-0.300 NOZZLE=-0.300 SWITCH=6.208 PROBE=7.013 --> OFFSET=-0.170 +``` + +The endstop value is the homed Z position which is always zero or the configure +"stepper_z:position_endstop" setting - and in this case, it's even the same as +the probed nozzle hight. + +# Configuration + +To activate the extension, a `[z_calibration]` section is needed in the printer +configuration. The configuration properties are described +[here](Config_Reference.md#z_calibration) in details. + +## Switch Offset + +The "z_calibration:switch_offset" is the already mentioned offset from the +switch body (which is the probed position) to the actual trigger point above +it. A starting point for this value can be taken from the datasheet like from +the Omron switch (D2F-5: 0.5mm and SSG-5H: 0.7mm). It's good to start with a +little less depending on the squishiness you prefer for the first layer (it's +about 0.45 for the D2F-5). So, with a smaller offset value, the nozzle is more +away from the bed! The value cannot be negative. + +For example, the datasheet of the D2F-5: + +![endstop offset](img/z_calibrate-d2f.png) + +And the calculation of the offset base: + +``` +offset base = OP (Operation Position) - switch body height + 0.5 mm = 5.5 mm - 5 mm +``` + +## Attaching and Detaching the Probe + +The attaching and detaching of the mag-probe can be done by creating a macro +for the `CALIBRATE_Z` command and surround it by the appropriate commands: + +``` +[gcode_macro CALIBRATE_Z] +description: Automatically calibrates the nozzles offset to the print surface and dock/undock MagProbe +rename_existing: CALIBRATE_Z_BASE +gcode: + ATTACH_PROBE # replace with the name of your specific attach macro + CALIBRATE_Z_BASE + DETACH_PROBE # replace with the name of your specific detach macro +``` + +It is also possible to use the `start_gcode` and `end_gcode` properties to +call the attach and detach commands instead: + +``` +[z_calibration] +... +start_gcode: ATTACH_PROBE # replace with the name of your specific attach macro +end_gcode: DETACH_PROBE # replace with the name of your specific detach macro +``` + +If there are any space restrictions and it is not possible to probe the nozzle +on the endstop with the probe attached, the `before_switch_gcode` property can +be used to attach the probe instead of the `start_gcode`. Then, the probe is +not attached until the probe is probed on the endstop: + +``` +[z_calibration] +... +before_switch_gcode: ATTACH_PROBE # replace with the name of your specific attach macro +end_gcode: DETACH_PROBE # replace with the name of your specific detach macro +``` + +## Bed Mesh + +If a bed mesh is used, the coordinates for probing on the print bed must be +exactly the relative reference index point of the mesh since this is the point +zero of the mesh! But, it is possible to omit these properties completely and +the relative reference index point of the mesh will be taken automatically (for +this, the "bed_mesh:relative_reference_index" setting is required and there is +no support for round bed/mesh so far)! + +# How To Test It + +Do not bother too much about absolute values of the calculated offsets. These +can vary a lot. Only the real position from the nozzle to the bed counts. To +test this, the result of the calibration can be queried by `GET_POSITION` +first: + +``` +> CALIBRATE_Z +> Z-CALIBRATION: ENDSTOP=-0.300 NOZZLE=-0.267 SWITCH=2.370 PROBE=3.093 --> OFFSET=-0.010000 +> GET_POSITION +> mcu: stepper_x:17085 stepper_y:15625 stepper_z:-51454 stepper_z1:-51454 stepper_z2:-51454 stepper_z3:-51454 +> stepper: stepper_x:552.500000 stepper_y:-47.500000 stepper_z:10.022500 stepper_z1:10.022500 stepper_z2:10.022500 stepper_z3:10.022500 +> kinematic: X:252.500000 Y:300.000000 Z:10.022500 +> toolhead: X:252.500000 Y:300.000000 Z:10.021472 E:0.000000 +> gcode: X:252.500000 Y:300.000000 Z:9.990000 E:0.000000 +> gcode base: X:0.000000 Y:0.000000 Z:-0.010000 E:0.000000 +> gcode homing: X:0.000000 Y:0.000000 Z:-0.010000 +``` + +Here, the Z position in "gcode base" reflects the calibrated Z offset. + +Then, the offset can be tested by moving the nozzle slowly down to zero by +moving it in multiple steps. It's good to do this by using GCodes, since +the offset is applied as GCode-Offset. For example like this: + +``` +> G90 +> G0 Z5 +> G0 Z3 +> G0 Z1 +> G0 Z0.5 +> G0 Z0.3 +> G0 Z0.1 +``` + +Check the distance to the print surface after every step. If there is a small +discrepancy (which should be smaller than the offset base from the switch's +datasheet), then adapt the "z_calibration:switch_offset" by that value. +Decreasing the "switch_offset" will move the nozzle more away from the bed. + +And finally, if you have double checked, that the calibrated offset is correct, +you can go for fine tuning the "z_calibration:switch_offset" by actually +printing first layer tests. This needs to be done only once! + +# How To Use It + +## Command CALIBRATE_Z + +The calibration is started by using the `CALIBRATE_Z` command. There are no +more parameters. A clean nozzle is needed for running this command. + +It does not matter when this calibration is called (and at what temperatures). +But, it is importaint to call it just before starting a print when the printer +is hot. So, it is good to add the `CALIBRATE_Z` command to the `PRINT_START` +macro (which is called from the slicers start gCode). The sequence of this +macro can look like this: + +1. Home all axes +2. Heat up the bed and nozzle (and chamber) +3. Get probe, make any bed leveling if needed (like QGL, Z-Tilt), park probe +4. Purge and clean the nozzle +5. Get probe, CALIBRATE_Z, park probe +6. (Adjust Z offset if needed) +7. Print intro line if used +8. Start printing... + +**:exclamation: And remove any old Z offset adjustments here +(like `SET_GCODE_OFFSET`)** + +For textured print surfaces, it might be necessary to go closer to the bed. +To adjust the offset from the slicers start GCode, the following command can be +added to the `PRINT_START` macro **after** calling the Z calibration: + +``` + # Adjust the G-Code Z offset if needed + SET_GCODE_OFFSET Z_ADJUST={params.Z_ADJUST|default(0.0)|float} MOVE=1 +``` + +Then, a `Z_ADJUST=0.0` can be added to the `PRINT_START` command in the Slicer. +This does **not** reset the offset to this value but adjusts it by the given +amount! + +>**NOTE:** Do not home Z again after running the Z calibration or it needs to +> be executed again! + +## Command PROBE_Z_ACCURACY + +There is also a `PROBE_Z_ACCURACY` command to test the accuracy of the Z +endstop (like the `PROBE_ACCURACY` command of the probe): + +``` +PROBE_Z_ACCURACY [PROBE_SPEED=] [LIFT_SPEED=] [SAMPLES=] [SAMPLE_RETRACT_DIST=] +``` + +It calculates the maximum, minimum, average, median and standard deviation of +multiple probe samles on the endstop by taking the configured nozzle position +on the endstop. The optional parameters default to their equivalent setting in +the z_calibration config section. + +## Ooze Mitigation + +Ooze with any nozzle probe endstop can cause inaccuracies, as the filament will continue to leak or +deform over the space of multiple probes. It is highly recommended to take some measures to prevent +ooze buildup before the nozzle probe portion of this plugin. + +A slow long retraction, of as much as 15mm at print end, can reduce the potential for ooze. If you do +this, consider adding a comparable extrude as the last command in your print start sequence to bring +the plastic back to the tip. (Retracts longer than 5mm have been linked to clogs in many hotends, +especially the Rapido. This may be best considered a last resort, depending on exact hardware and +filament.) + +Heating the nozzle about a minute before scrubbing - using a purge bucket - will allow all the +remaining plastic time to drain from the nozzle and be cleaned away by a simple wipe. If using a +purge and scrubbing bucket, do not purge filament at this stage. + +An endstop switch that requires a stronger activation force, such as sexbolt with a spring, or +unklicky z, can help squash any remaining ooze and improve consistency. + +Probing can be done with a hotend temperature below the full temperature for the print. If you print +at 250, you can preheat the nozzle to 180, and run this script before finishing the heat up to full +temperature. This may have varying effects depending on temperatures used. + +Also consider picking up your probe prior to your nozzle wipe, to allow this script to probe the +nozzle immediately after cleaning it. \ No newline at end of file diff --git a/docs/img/z_calibrate-d2f.png b/docs/img/z_calibrate-d2f.png new file mode 100644 index 000000000..0a073a2e3 Binary files /dev/null and b/docs/img/z_calibrate-d2f.png differ diff --git a/docs/img/z_calibrate-endstop.png b/docs/img/z_calibrate-endstop.png new file mode 100644 index 000000000..2114e9dae Binary files /dev/null and b/docs/img/z_calibrate-endstop.png differ diff --git a/docs/img/z_calibrate-nozzle.png b/docs/img/z_calibrate-nozzle.png new file mode 100644 index 000000000..67a292f4d Binary files /dev/null and b/docs/img/z_calibrate-nozzle.png differ diff --git a/docs/img/z_calibrate-offset.png b/docs/img/z_calibrate-offset.png new file mode 100644 index 000000000..23e1f961f Binary files /dev/null and b/docs/img/z_calibrate-offset.png differ diff --git a/docs/img/z_calibrate-probe.png b/docs/img/z_calibrate-probe.png new file mode 100644 index 000000000..fa20566a6 Binary files /dev/null and b/docs/img/z_calibrate-probe.png differ diff --git a/docs/img/z_calibrate-switch.png b/docs/img/z_calibrate-switch.png new file mode 100644 index 000000000..00b372a33 Binary files /dev/null and b/docs/img/z_calibrate-switch.png differ diff --git a/klippy/extras/z_calibration.py b/klippy/extras/z_calibration.py new file mode 100644 index 000000000..324c629bb --- /dev/null +++ b/klippy/extras/z_calibration.py @@ -0,0 +1,589 @@ +# Klipper plugin for a self-calibrating Z offset. +# +# Copyright (C) 2021-2023 Titus Meyer +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +from mcu import MCU_endstop + + +class ZCalibrationHelper: + def __init__(self, config): + self.state = None + self.z_endstop = None + self.z_homing = None + self.last_state = False + self.last_z_offset = 0.0 + self.position_z_endstop = None + self.config = config + self.printer = config.get_printer() + self.switch_offset = config.getfloat("switch_offset", 0.0, above=0.0) + # max_deviation is deprecated + self.max_deviation = config.getfloat("max_deviation", None, above=0.0) + config.deprecate("max_deviation") + self.offset_margins = self._get_offset_margins( + "offset_margins", "-1.0,1.0" + ) + self.speed = config.getfloat("speed", 50.0, above=0.0) + # clearance is deprecated + self.clearance = config.getfloat("clearance", None, above=0.0) + config.deprecate("clearance") + self.safe_z_height = config.getfloat("safe_z_height", None, above=0.0) + self.samples = config.getint("samples", None, minval=1) + self.tolerance = config.getfloat("samples_tolerance", None, above=0.0) + self.retries = config.getint( + "samples_tolerance_retries", None, minval=0 + ) + atypes = {"none": None, "median": "median", "average": "average"} + self.samples_result = config.getchoice("samples_result", atypes, "none") + self.lift_speed = config.getfloat("lift_speed", None, above=0.0) + self.probing_speed = config.getfloat("probing_speed", None, above=0.0) + self.second_speed = config.getfloat( + "probing_second_speed", None, above=0.0 + ) + self.retract_dist = config.getfloat( + "probing_retract_dist", None, above=0.0 + ) + self.position_min = config.getfloat("position_min", None) + self.first_fast = config.getboolean("probing_first_fast", False) + self.nozzle_site = self._get_xy("nozzle_xy_position", True) + self.switch_site = self._get_xy("switch_xy_position", True) + self.switch_xy_offsets = self._get_xy("switch_xy_offsets", True) + self.bed_site = self._get_xy("bed_xy_position", True) + self.wiggle_offsets = self._get_xy("wiggle_xy_offsets", True) + gcode_macro = self.printer.load_object(config, "gcode_macro") + self.start_gcode = gcode_macro.load_template(config, "start_gcode", "") + self.switch_gcode = gcode_macro.load_template( + config, "before_switch_gcode", "" + ) + self.end_gcode = gcode_macro.load_template(config, "end_gcode", "") + self.query_endstops = self.printer.load_object(config, "query_endstops") + self.printer.register_event_handler( + "klippy:connect", self.handle_connect + ) + self.printer.register_event_handler( + "homing:home_rails_end", self.handle_home_rails_end + ) + self.gcode = self.printer.lookup_object("gcode") + self.gcode.register_command( + "CALIBRATE_Z", self.cmd_CALIBRATE_Z, desc=self.cmd_CALIBRATE_Z_help + ) + self.gcode.register_command( + "PROBE_Z_ACCURACY", + self.cmd_PROBE_Z_ACCURACY, + desc=self.cmd_PROBE_Z_ACCURACY_help, + ) + self.gcode.register_command( + "CALCULATE_SWITCH_OFFSET", + self.cmd_CALCULATE_SWITCH_OFFSET, + desc=self.cmd_CALCULATE_SWITCH_OFFSET_help, + ) + + def get_status(self, eventtime): + return { + "last_query": self.last_state, + "last_z_offset": self.last_z_offset, + } + + def handle_connect(self): + # get z-endstop object + for endstop, name in self.query_endstops.endstops: + if name == "z": + # check for virtual endstops.. + if not isinstance(endstop, MCU_endstop): + raise self.printer.config_error( + "A virtual endstop for z" + " is not supported for %s" % (self.config.get_name()) + ) + self.z_endstop = EndstopWrapper(self.config, endstop) + # get z-endstop position from safe_z_home + if self.nozzle_site is None: + safe_z_home = self.printer.lookup_object( + "safe_z_home", default=None + ) + if safe_z_home is None: + raise self.printer.config_error( + "No nozzle position" + " configured for %s" % (self.config.get_name()) + ) + self.nozzle_site = [ + safe_z_home.home_x_pos, + safe_z_home.home_y_pos, + None, + ] + # check/calculate switch position by offsets + if self.switch_site is None: + if self.switch_xy_offsets is None: + raise self.printer.config_error( + "No switch position" + " configured for %s" % (self.config.get_name()) + ) + self.switch_site = [ + self.nozzle_site[0] + self.switch_xy_offsets[0], + self.nozzle_site[1] + self.switch_xy_offsets[1], + None, + ] + # get probing settings + probe = self.printer.lookup_object("probe", default=None) + if probe is None: + raise self.printer.config_error( + "A probe is needed for %s" % (self.config.get_name()) + ) + if self.samples is None: + self.samples = probe.sample_count + if self.tolerance is None: + self.tolerance = probe.samples_tolerance + if self.retries is None: + self.retries = probe.samples_retries + if self.lift_speed is None: + self.lift_speed = probe.lift_speed + # clearance is deprecated + if self.clearance is not None and self.clearance == 0: + self.clearance = 20 # defaults to 20mm + if self.safe_z_height is None: + self.safe_z_height = probe.z_offset * 2 + if self.safe_z_height < 3: + self.safe_z_height = 20 # defaults to 20mm + if self.samples_result is None: + self.samples_result = probe.samples_result + + def handle_home_rails_end(self, homing_state, rails): + # get z homing position + for rail in rails: + if rail.get_steppers()[0].is_active_axis("z"): + # get homing settings from z rail + self.z_homing = rail.position_endstop + if self.probing_speed is None: + self.probing_speed = rail.homing_speed + if self.second_speed is None: + self.second_speed = rail.second_homing_speed + if self.retract_dist is None: + self.retract_dist = rail.homing_retract_dist + if self.position_min is None: + self.position_min = rail.position_min + self.position_z_endstop = rail.position_endstop + + def _build_config(self): + pass + + cmd_CALIBRATE_Z_help = ( + "Automatically calibrates the nozzle offset" " to the print surface" + ) + + def cmd_CALIBRATE_Z(self, gcmd): + if self.z_homing is None: + raise gcmd.error("Must home axes first") + site_attr = gcmd.get("BED_POSITION", None) + if site_attr is not None: + # set bed site from BED_POSITION parameter + self.bed_site = self._parse_xy("BED_POSITION", site_attr) + elif self._get_xy("bed_xy_position", True) is not None: + # set bed site from configuration + self.bed_site = self._get_xy("bed_xy_position", False) + else: + # else get the mesh's zero reference position + try: + mesh = self.printer.lookup_object("bed_mesh", default=None) + if ( + hasattr(mesh.bmc, "zero_ref_pos") + and mesh.bmc.zero_ref_pos is not None + ): + self.bed_site = mesh.bmc.zero_ref_pos + else: + # trying to read the deprecated rri + rri = mesh.bmc.relative_reference_index + self.bed_site = mesh.bmc.points[rri] + logging.debug( + "Z-CALIBRATION probe bed_x=%.3f bed_y=%.3f" + % (self.bed_site[0], self.bed_site[1]) + ) + except: + raise gcmd.error( + "Either use the BED_POSITION parameter," + " configure a bed_xy_position or define" + " a mesh with a zero_reference_position" + " for %s" % (self.config.get_name()) + ) + self._log_config() + state = CalibrationState(self, gcmd) + state.calibrate_z() + + cmd_PROBE_Z_ACCURACY_help = ( + "Probe Z-Endstop accuracy at" " Nozzle-Endstop position" + ) + + def cmd_PROBE_Z_ACCURACY(self, gcmd): + if self.z_homing is None: + raise gcmd.error("Must home axes first") + speed = gcmd.get_float("PROBE_SPEED", self.second_speed, above=0.0) + lift_speed = gcmd.get_float("LIFT_SPEED", self.lift_speed, above=0.0) + sample_count = gcmd.get_int("SAMPLES", self.samples, minval=1) + sample_retract_dist = gcmd.get_float( + "SAMPLE_RETRACT_DIST", self.retract_dist, above=0.0 + ) + toolhead = self.printer.lookup_object("toolhead") + pos = toolhead.get_position() + self._move_safe_z(pos, lift_speed) + # move to z-endstop position + self._move(list(self.nozzle_site), self.speed) + pos = toolhead.get_position() + gcmd.respond_info( + "PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f" + " (samples=%d retract=%.3f" + " speed=%.1f lift_speed=%.1f)\n" + % ( + pos[0], + pos[1], + pos[2], + sample_count, + sample_retract_dist, + speed, + lift_speed, + ) + ) + # Probe bed sample_count times + positions = [] + while len(positions) < sample_count: + # Probe position + pos = self._probe(self.z_endstop, self.position_min, speed) + positions.append(pos) + # Retract + liftpos = [None, None, pos[2] + sample_retract_dist] + self._move(liftpos, lift_speed) + # Calculate maximum, minimum and average values + max_value = max([p[2] for p in positions]) + min_value = min([p[2] for p in positions]) + range_value = max_value - min_value + avg_value = self._calc_mean(positions)[2] + median = self._calc_median(positions)[2] + # calculate the standard deviation + deviation_sum = 0 + for i in range(len(positions)): + deviation_sum += pow(positions[i][2] - avg_value, 2.0) + sigma = (deviation_sum / len(positions)) ** 0.5 + # Show information + gcmd.respond_info( + "probe accuracy results: maximum %.6f, minimum %.6f, range %.6f," + " average %.6f, median %.6f, standard deviation %.6f" + % (max_value, min_value, range_value, avg_value, median, sigma) + ) + + cmd_CALCULATE_SWITCH_OFFSET_help = ( + "Calculates a switch_offset based on" " the current z position" + ) + + def cmd_CALCULATE_SWITCH_OFFSET(self, gcmd): + if self.last_z_offset is None: + raise gcmd.error("Must run CALIBRATE_Z first") + toolhead = self.printer.lookup_object("toolhead") + pos = toolhead.get_position() + new_switch_offset = self.switch_offset - (pos[2] - self.last_z_offset) + if new_switch_offset > 0.0: + gcmd.respond_info( + "switch_offset=%.3f - (current_z=%.3f - z_offset=%.3f" + ") --> new switch_offset=%.3f" + % ( + self.switch_offset, + pos[2], + self.last_z_offset, + new_switch_offset, + ) + ) + else: + gcmd.respond_info( + "The resulting switch offset is negative! Either" + " the nozzle is still too far away or something" + " else is wrong..." + ) + + def _get_xy(self, name, optional=False): + if optional and self.config.get(name, None) is None: + return None + else: + return self._parse_xy(name, self.config.get(name)) + + def _parse_xy(self, name, site): + try: + x_pos, y_pos = site.split(",") + return [float(x_pos), float(y_pos), None] + except: + raise self.config.error( + "Unable to parse %s in %s" % (name, self.config.get_name()) + ) + + def _get_offset_margins(self, name, default): + try: + margins = self.config.get(name, default).split(",") + for i, val in enumerate(margins): + margins[i] = float(val) + if len(margins) == 1: + val = abs(margins[0]) + margins[0] = -val + margins.append(val) + return margins + except: + raise self.config.error( + "Unable to parse %s in %s" % (name, self.config.get_name()) + ) + + def _probe(self, mcu_endstop, z_position, speed, wiggle=False): + toolhead = self.printer.lookup_object("toolhead") + pos = toolhead.get_position() + pos[2] = z_position + # probe + phoming = self.printer.lookup_object("homing") + curpos = phoming.probing_move(mcu_endstop, pos, speed) + # retract + self._move([None, None, curpos[2] + self.retract_dist], self.lift_speed) + if wiggle and self.wiggle_offsets is not None: + self._move( + [ + curpos[0] + self.wiggle_offsets[0], + curpos[1] + self.wiggle_offsets[1], + None, + ], + self.speed, + ) + self._move([curpos[0], curpos[1], None], self.speed) + self.gcode.respond_info( + "probe at %.3f,%.3f is z=%.6f" % (curpos[0], curpos[1], curpos[2]) + ) + return curpos + + def _move(self, coord, speed): + self.printer.lookup_object("toolhead").manual_move(coord, speed) + + def _move_safe_z(self, pos, lift_speed): + # clearance is deprecated + if self.clearance is not None: + if pos[2] < self.clearance: + # no clearance, better to move up (relative) + self._move([None, None, pos[2] + self.clearance], lift_speed) + else: + if pos[2] < self.safe_z_height: + # no safe z position, better to move up (absolute) + self._move([None, None, self.safe_z_height], lift_speed) + + def _calc_mean(self, positions): + count = float(len(positions)) + return [sum([pos[i] for pos in positions]) / count for i in range(3)] + + def _calc_median(self, positions): + z_sorted = sorted(positions, key=(lambda p: p[2])) + middle = len(positions) // 2 + if (len(positions) & 1) == 1: + # odd number of samples + return z_sorted[middle] + # even number of samples + return self._calc_mean(z_sorted[middle - 1 : middle + 1]) + + def _log_config(self): + logging.debug( + "Z-CALIBRATION: switch_offset=%.3f," + " offset_margins=%.3f,%.3f, speed=%.3f," + " samples=%i, tolerance=%.3f, retries=%i," + " samples_result=%s, lift_speed=%.3f," + " safe_z_height=%.3f, probing_speed=%.3f," + " second_speed=%.3f, retract_dist=%.3f," + " position_min=%.3f, probe_nozzle_x=%.3f," + " probe_nozzle_y=%.3f, probe_switch_x=%.3f," + " probe_switch_y=%.3f, probe_bed_x=%.3f," + " probe_bed_y=%.3f" + % ( + self.switch_offset, + self.offset_margins[0], + self.offset_margins[1], + self.speed, + self.samples, + self.tolerance, + self.retries, + self.samples_result, + self.lift_speed, + self.safe_z_height, + self.probing_speed, + self.second_speed, + self.retract_dist, + self.position_min, + self.nozzle_site[0], + self.nozzle_site[1], + self.switch_site[0], + self.switch_site[1], + self.bed_site[0], + self.bed_site[1], + ) + ) + + +class EndstopWrapper: + def __init__(self, config, endstop): + self.mcu_endstop = endstop + # Wrappers + self.get_mcu = self.mcu_endstop.get_mcu + self.add_stepper = self.mcu_endstop.add_stepper + self.get_steppers = self.mcu_endstop.get_steppers + self.home_start = self.mcu_endstop.home_start + self.home_wait = self.mcu_endstop.home_wait + self.query_endstop = self.mcu_endstop.query_endstop + + +class CalibrationState: + def __init__(self, helper, gcmd): + self.helper = helper + self.gcmd = gcmd + self.gcode = helper.gcode + self.z_endstop = helper.z_endstop + self.probe = helper.printer.lookup_object("probe") + self.toolhead = helper.printer.lookup_object("toolhead") + self.gcode_move = helper.printer.lookup_object("gcode_move") + self.max_deviation = helper.max_deviation + self.offset_margins = helper.offset_margins + + def _probe_on_site( + self, endstop, site, check_probe=False, split_xy=False, wiggle=False + ): + pos = self.toolhead.get_position() + self.helper._move_safe_z(pos, self.helper.lift_speed) + # move to position + if split_xy: + self.helper._move([site[0], pos[1], None], self.helper.speed) + self.helper._move([site[0], site[1], site[2]], self.helper.speed) + else: + self.helper._move(site, self.helper.speed) + if check_probe: + # check if probe is attached and switch is closed + time = self.toolhead.get_last_move_time() + if self.probe.mcu_probe.query_endstop(time): + raise self.helper.printer.command_error( + "Probe switch not" " closed - Probe not" " attached?" + ) + if self.helper.first_fast: + # first probe just to get down faster + self.helper._probe( + endstop, + self.helper.position_min, + self.helper.probing_speed, + wiggle=wiggle, + ) + retries = 0 + positions = [] + while len(positions) < self.helper.samples: + # probe with second probing speed + curpos = self.helper._probe( + endstop, + self.helper.position_min, + self.helper.second_speed, + wiggle=wiggle, + ) + positions.append(curpos[:3]) + # check tolerance + z_positions = [p[2] for p in positions] + if max(z_positions) - min(z_positions) > self.helper.tolerance: + if retries >= self.helper.retries: + self.helper.end_gcode.run_gcode_from_command() + raise self.gcmd.error("Probe samples exceed tolerance") + self.gcmd.respond_info( + "Probe samples exceed tolerance." " Retrying..." + ) + retries += 1 + positions = [] + # calculate result + if self.helper.samples_result == "median": + return self.helper._calc_median(positions)[2] + return self.helper._calc_mean(positions)[2] + + def _add_probe_offset(self, site): + # calculate bed position by using the probe's offsets + probe_offsets = self.probe.get_offsets() + probe_site = list(site) + probe_site[0] -= probe_offsets[0] + probe_site[1] -= probe_offsets[1] + return probe_site + + def _set_new_gcode_offset(self, offset): + # reset gcode z offset to 0 + gcmd_offset = self.gcode.create_gcode_command( + "SET_GCODE_OFFSET", "SET_GCODE_OFFSET", {"Z": 0.0} + ) + self.gcode_move.cmd_SET_GCODE_OFFSET(gcmd_offset) + # set new gcode z offset + gcmd_offset = self.gcode.create_gcode_command( + "SET_GCODE_OFFSET", "SET_GCODE_OFFSET", {"Z_ADJUST": offset} + ) + self.gcode_move.cmd_SET_GCODE_OFFSET(gcmd_offset) + + def calibrate_z(self): + self.helper.start_gcode.run_gcode_from_command() + # probe the nozzle + nozzle_zero = self._probe_on_site( + self.z_endstop, + self.helper.nozzle_site, + check_probe=False, + split_xy=True, + wiggle=True, + ) + # probe the probe-switch + self.helper.switch_gcode.run_gcode_from_command() + # probe the body of the switch + switch_zero = self._probe_on_site( + self.z_endstop, self.helper.switch_site, check_probe=True + ) + # probe position on bed + probe_site = self._add_probe_offset(self.helper.bed_site) + probe_zero = self._probe_on_site( + self.probe.mcu_probe, probe_site, check_probe=True + ) + # calculate the offset + offset = probe_zero - ( + switch_zero - nozzle_zero + self.helper.switch_offset + ) + # print result + self.gcmd.respond_info( + "Z-CALIBRATION: probe=%.3f - (switch=%.3f" + " - nozzle=%.3f + switch_offset=%.3f) -->" + " new offset=%.6f" + % ( + probe_zero, + switch_zero, + nozzle_zero, + self.helper.switch_offset, + offset, + ) + ) + self.gcmd.respond_info( + "HINT: z position_endstop=%.3f - offset=%.6f" + " --> possible z position_endstop=%.3f" + % ( + self.helper.position_z_endstop, + offset, + self.helper.position_z_endstop - offset, + ) + ) + # check offset margins + if ( + self.max_deviation is not None # deprecated + and abs(offset) > self.max_deviation + ): + self.helper.end_gcode.run_gcode_from_command() + raise self.helper.printer.command_error( + "Offset is greater than" + " allowed: offset=%.3f" + " > max_deviation=%.3f" % (offset, self.max_deviation) + ) + elif offset < self.offset_margins[0] or offset > self.offset_margins[1]: + self.helper.end_gcode.run_gcode_from_command() + raise self.helper.printer.command_error( + "Offset %.3f is outside" + " the configured range of" + " min=%.3f and max=%.3f" + % (offset, self.offset_margins[0], self.offset_margins[1]) + ) + # set new offset + self._set_new_gcode_offset(offset) + # set states + self.helper.last_state = True + self.helper.last_z_offset = offset + self.helper.end_gcode.run_gcode_from_command() + + +def load_config(config): + return ZCalibrationHelper(config)