diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 906cfb8fd24f..84bf39a3405e 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1722,6 +1722,89 @@ control_pin: # See the "probe" section for information on these parameters. ``` +### [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 switchbody, 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. It will raise an error if there is no +# probe_bed site and no bed_mesh with a relative_reference_index +# configured. +switch_offset: +# The trigger point offset of the used mag-probe switch. +# This needs to be fined 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" +# 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 or 20mm if it is still zero. +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: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. +fast_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_retract_dist: default from "stepper_z:homing_retract_dist" section. +# Distance to backoff (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 745c63dbd4e8..ab1d743e057b 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -383,6 +383,22 @@ see the [BL-Touch guide](BLTouch.md)): - `BLTOUCH_STORE MODE=`: This stores an output mode in the EEPROM of a BLTouch V3.1 Available output_modes are: `5V`, `OD` +### Automatic Z Offset 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! + ### Delta Calibration The following commands are available when the diff --git a/docs/Z_Calibration.md b/docs/Z_Calibration.md new file mode 100644 index 000000000000..a43f49ad9981 --- /dev/null +++ b/docs/Z_Calibration.md @@ -0,0 +1,263 @@ +# 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: DETACH_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: DETACH_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 ommit 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. diff --git a/docs/img/z_calibrate-d2f.png b/docs/img/z_calibrate-d2f.png new file mode 100644 index 000000000000..0a073a2e30fb 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 000000000000..2114e9daed89 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 000000000000..67a292f4da59 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 000000000000..23e1f961f7a1 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 000000000000..fa20566a6414 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 000000000000..00b372a334f8 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 000000000000..9347165868d0 --- /dev/null +++ b/klippy/extras/z_calibration.py @@ -0,0 +1,332 @@ +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. + self.config = config + self.printer = config.get_printer() + self.switch_offset = config.getfloat('switch_offset', 0.0, above=0.) + self.max_deviation = config.getfloat('max_deviation', 1.0, above=0.) + self.speed = config.getfloat('speed', 50.0, above=0.) + self.clearance = config.getfloat('clearance', None, above=0.) + self.samples = config.getint('samples', None, minval=1) + self.tolerance = config.getfloat('samples_tolerance', None, above=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.) + self.probing_speed = config.getfloat('probing_speed', + None, above=0.) + self.fast_probing_speed = config.getfloat('fast_probing_speed', + None, above=0.) + self.retract_dist = config.getfloat('probing_retract_dist', + None, above=0.) + self.position_min = config.getfloat('position_min', None) + self.first_fast = config.getboolean('probing_first_fast', False) + self.nozzle_site = self._parse_site("nozzle_xy_position") + self.switch_site = self._parse_site("switch_xy_position") + self.bed_site = self._parse_site("bed_xy_position", 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) + 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 + 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 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 + if self.clearance is None: + self.clearance = probe.z_offset * 2 + if self.clearance == 0: + self.clearance = 20 # defaults to 20mm + if self.samples_result is None: + self.samples_result = probe.samples_result + # get the mesh's relative reference point + # a round mesh/bed would not work here so far... + if self.bed_site is None: + mesh = self.printer.lookup_object('bed_mesh', default=None) + if mesh is None or mesh.bmc.relative_reference_index is None: + raise self.printer.config_error("Either configure" + " bed_xy_position or configure" + " a mesh with a" + " relative_reference_index" + " for %s" + % (self.config.get_name())) + rri = mesh.bmc.relative_reference_index + self.bed_site = [mesh.bmc.points[rri][0], mesh.bmc.points[rri][1]] + logging.debug("Z-CALIBRATION probe_bed_x=%.3f probe_bed_y=%.3f" + % (self.bed_site[0], self.bed_site[1])) + 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'): + # used for result logging + self.z_homing = rail.position_endstop + # should we take these values here too? + if self.fast_probing_speed is None: + self.fast_probing_speed = rail.homing_speed + if self.probing_speed is None: + self.probing_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 + 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.state is not None: + raise self.printer.command_error("Already performing CALIBRATE_Z") + return + 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): + speed = gcmd.get_float("PROBE_SPEED", self.probing_speed, above=0.) + lift_speed = gcmd.get_float("LIFT_SPEED", self.lift_speed, above=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.) + toolhead = self.printer.lookup_object('toolhead') + pos = toolhead.get_position() + if pos[2] < self.clearance: + # no clearance, better to move up + self._move([None, None, pos[2] + self.clearance], 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.) + 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)) + def _parse_site(self, name, optional=False): + try: + if optional and self.config.get(name, None) is None: + return None + x_pos, y_pos = self.config.get(name).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 _probe(self, mcu_endstop, z_position, speed): + 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) + 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 _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]) +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') + def _probe_on_z_endstop(self, site): + pos = self.toolhead.get_position() + if pos[2] < self.helper.clearance: + # no clearance, better to move up + self.helper._move([None, None, pos[2] + self.helper.clearance], + self.helper.lift_speed) + # move to position + self.helper._move(list(site), self.helper.speed) + if self.helper.first_fast: + # first probe just to get down faster + self.helper._probe(self.z_endstop, self.helper.position_min, + self.helper.fast_probing_speed) + retries = 0 + positions = [] + while len(positions) < self.helper.samples: + # probe with second probing speed + curpos = self.helper._probe(self.z_endstop, + self.helper.position_min, + self.helper.probing_speed) + 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: + 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 _probe_on_bed(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] + # move to probing position + pos = self.toolhead.get_position() + self.helper._move([None, None, pos[2] + self.helper.clearance], + self.helper.lift_speed) + self.helper._move(probe_site, self.helper.speed) + if self.helper.first_fast: + # fast probe to get down first + self.helper._probe(self.probe.mcu_probe, self.probe.z_position, + self.helper.fast_probing_speed) + # probe it + return self.probe.run_probe(self.gcmd)[2] + 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_z_endstop(self.helper.nozzle_site) + # probe the probe-switch + self.helper.switch_gcode.run_gcode_from_command() + # check if probe is attached and the switch is closing it + #time = self.helper.printer.lookup_object('toolhead') + # .get_last_move_time() + #probe = self.helper.printer.lookup_object('probe') + #if probe.mcu_probe.query_endstop(time): + # raise self.helper.printer.command_error(ERROR_NOT_ATTACHED) + # return + switch_zero = self._probe_on_z_endstop(self.helper.switch_site) + # probe position on bed + probe_zero = self._probe_on_bed(self.helper.bed_site) + # move up by retract_dist + self.helper._move([None, None, + probe_zero + self.helper.retract_dist], + self.helper.lift_speed) + # calculate the offset + offset = probe_zero - (switch_zero - nozzle_zero + + self.helper.switch_offset) + # print result + self.gcmd.respond_info("Z-CALIBRATION: ENDSTOP=%.3f NOZZLE=%.3f" + " SWITCH=%.3f PROBE=%.3f --> OFFSET=%.6f" + % (self.helper.z_homing, nozzle_zero, + switch_zero, probe_zero, offset)) + # check max deviation + if abs(offset) > self.helper.max_deviation: + raise self.helper.printer.command_error("Offset is larger as" + " allowed: OFFSET=%.3f" + " MAX_DEVIATION=%.3f" + % (offset, + self.helper.max_deviation)) + return + # 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)