diff --git a/docs/Axis_Twist_Compensation.md b/docs/Axis_Twist_Compensation.md index 0017a2279de7..b0c80b1864b8 100644 --- a/docs/Axis_Twist_Compensation.md +++ b/docs/Axis_Twist_Compensation.md @@ -24,19 +24,51 @@ try to probe the bed without attaching the probe if you use it. > **Tip:** Make sure the [probe X and Y offsets](Config_Reference.md#probe) are > correctly set as they greatly influence calibration. -1. After setting up the [axis_twist_compensation] module, -perform `AXIS_TWIST_COMPENSATION_CALIBRATE` -* The calibration wizard will prompt you to measure the probe Z offset at a few -points along the bed -* The calibration defaults to 3 points but you can use the option -`SAMPLE_COUNT=` to use a different number. -2. [Adjust your Z offset](Probe_Calibrate.md#calibrating-probe-z-offset) -3. Perform automatic/probe-based bed tramming operations, such as -[Screws Tilt Adjust](G-Codes.md#screws_tilt_adjust), -[Z Tilt Adjust](G-Codes.md#z_tilt_adjust) etc -4. Home all axis, then perform a [Bed Mesh](Bed_Mesh.md) if required -5. Perform a test print, followed by any -[fine-tuning](Axis_Twist_Compensation.md#fine-tuning) as desired +### Basic Usage: X-Axis Calibration +1. After setting up the ```[axis_twist_compensation]``` module, run: +``` +AXIS_TWIST_COMPENSATION_CALIBRATE +``` +This command will calibrate the X-axis by default. + - The calibration wizard will prompt you to measure the probe Z offset at + several points along the bed. + - By default, the calibration uses 3 points, but you can specify a different + number with the option: +`` +SAMPLE_COUNT= +`` + +2. **Adjust Your Z Offset:** +After completing the calibration, be sure to [adjust your Z offset] +(Probe_Calibrate.md#calibrating-probe-z-offset). + +3. **Perform Bed Leveling Operations:** +Use probe-based operations as needed, such as: + - [Screws Tilt Adjust](G-Codes.md#screws_tilt_adjust) + - [Z Tilt Adjust](G-Codes.md#z_tilt_adjust) + +4. **Finalize the Setup:** + - Home all axes, and perform a [Bed Mesh](Bed_Mesh.md) if necessary. + - Run a test print, followed by any + [fine-tuning](Axis_Twist_Compensation.md#fine-tuning) + if needed. + +### For Y-Axis Calibration +The calibration process for the Y-axis is similar to the X-axis. To calibrate +the Y-axis, use: +``` +AXIS_TWIST_COMPENSATION_CALIBRATE AXIS=Y +``` +This will guide you through the same measuring process as for the X-axis. + +### Automatic Calibration for Both Axes +To perform automatic calibration for both the X and Y axes without manual +intervention, use: +``` +AXIS_TWIST_COMPENSATION_CALIBRATE AUTO=True +``` +In this mode, the calibration process will run for both axes automatically. + > **Tip:** Bed temperature and nozzle temperature and size do not seem to have > an influence to the calibration process. diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 6b42fe48da91..c9b13ea8a0d0 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2035,9 +2035,9 @@ sensor_type: ldc1612 ### [axis_twist_compensation] -A tool to compensate for inaccurate probe readings due to twist in X gantry. See -the [Axis Twist Compensation Guide](Axis_Twist_Compensation.md) for more -detailed information regarding symptoms, configuration and setup. +A tool to compensate for inaccurate probe readings due to twist in X or Y +gantry. See the [Axis Twist Compensation Guide](Axis_Twist_Compensation.md) +for more detailed information regarding symptoms, configuration and setup. ``` [axis_twist_compensation] @@ -2050,16 +2050,33 @@ detailed information regarding symptoms, configuration and setup. calibrate_start_x: 20 # Defines the minimum X coordinate of the calibration # This should be the X coordinate that positions the nozzle at the starting -# calibration position. This parameter must be provided. +# calibration position. calibrate_end_x: 200 # Defines the maximum X coordinate of the calibration # This should be the X coordinate that positions the nozzle at the ending -# calibration position. This parameter must be provided. +# calibration position. calibrate_y: 112.5 # Defines the Y coordinate of the calibration # This should be the Y coordinate that positions the nozzle during the -# calibration process. This parameter must be provided and is recommended to +# calibration process. This parameter is recommended to # be near the center of the bed + +# For Y-axis twist compensation, specify the following parameters: +calibrate_start_y: ... +# Defines the minimum Y coordinate of the calibration +# This should be the Y coordinate that positions the nozzle at the starting +# calibration position for the Y axis. This parameter must be provided if +# compensating for Y axis twist. +calibrate_end_y: ... +# Defines the maximum Y coordinate of the calibration +# This should be the Y coordinate that positions the nozzle at the ending +# calibration position for the Y axis. This parameter must be provided if +# compensating for Y axis twist. +calibrate_x: ... +# Defines the X coordinate of the calibration for Y axis twist compensation +# This should be the X coordinate that positions the nozzle during the +# calibration process for Y axis twist compensation. This parameter must be +# provided and is recommended to be near the center of the bed. ``` ## Additional stepper motors and extruders diff --git a/docs/G-Codes.md b/docs/G-Codes.md index e55fba35db80..9a66bff6f23f 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -146,9 +146,19 @@ The following commands are available when the section](Config_Reference.md#axis_twist_compensation) is enabled. #### AXIS_TWIST_COMPENSATION_CALIBRATE -`AXIS_TWIST_COMPENSATION_CALIBRATE [SAMPLE_COUNT=]`: Initiates the X -twist calibration wizard. `SAMPLE_COUNT` specifies the number of points along -the X axis to calibrate at and defaults to 3. +`AXIS_TWIST_COMPENSATION_CALIBRATE [AXIS=] [AUTO=] +[SAMPLE_COUNT=]` + +Calibrates axis twist compensation by specifying the target axis or +enabling automatic calibration. + +- **AXIS:** Define the axis (`X` or `Y`) for which the twist compensation +will be calibrated. If not specified, the axis defaults to `'X'`. + +- **AUTO:** Enables automatic calibration mode. When `AUTO=True`, the +calibration will run for both the X and Y axes. In this mode, `AXIS` +cannot be specified. If both `AXIS` and `AUTO` are provided, an error +will be raised. ### [bed_mesh] diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index e7aad52c047f..31091e816157 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -23,18 +23,27 @@ def __init__(self, config): self.horizontal_move_z = config.getfloat('horizontal_move_z', DEFAULT_HORIZONTAL_MOVE_Z) self.speed = config.getfloat('speed', DEFAULT_SPEED) - self.calibrate_start_x = config.getfloat('calibrate_start_x') - self.calibrate_end_x = config.getfloat('calibrate_end_x') - self.calibrate_y = config.getfloat('calibrate_y') + self.calibrate_start_x = config.getfloat('calibrate_start_x', + default=None) + self.calibrate_end_x = config.getfloat('calibrate_end_x', default=None) + self.calibrate_y = config.getfloat('calibrate_y', default=None) self.z_compensations = config.getlists('z_compensations', default=[], parser=float) self.compensation_start_x = config.getfloat('compensation_start_x', default=None) - self.compensation_end_x = config.getfloat('compensation_start_y', + self.compensation_end_x = config.getfloat('compensation_end_x', default=None) - self.m = None - self.b = None + self.calibrate_start_y = config.getfloat('calibrate_start_y', + default=None) + self.calibrate_end_y = config.getfloat('calibrate_end_y', default=None) + self.calibrate_x = config.getfloat('calibrate_x', default=None) + self.compensation_start_y = config.getfloat('compensation_start_y', + default=None) + self.compensation_end_y = config.getfloat('compensation_end_y', + default=None) + self.zy_compensations = config.getlists('zy_compensations', + default=[], parser=float) # setup calibrater self.calibrater = Calibrater(self, config) @@ -43,28 +52,46 @@ def __init__(self, config): self._update_z_compensation_value) def _update_z_compensation_value(self, pos): - if not self.z_compensations: - return + if self.z_compensations: + pos[2] += self._get_interpolated_z_compensation( + pos[0], self.z_compensations, + self.compensation_start_x, + self.compensation_end_x + ) + + if self.zy_compensations: + pos[2] += self._get_interpolated_z_compensation( + pos[1], self.zy_compensations, + self.compensation_start_y, + self.compensation_end_y + ) + + def _get_interpolated_z_compensation( + self, coord, z_compensations, + comp_start, + comp_end + ): - x_coord = pos[0] - z_compensations = self.z_compensations sample_count = len(z_compensations) - spacing = ((self.calibrate_end_x - self.calibrate_start_x) + spacing = ((comp_end - comp_start) / (sample_count - 1)) - interpolate_t = (x_coord - self.calibrate_start_x) / spacing + interpolate_t = (coord - comp_start) / spacing interpolate_i = int(math.floor(interpolate_t)) interpolate_i = bed_mesh.constrain(interpolate_i, 0, sample_count - 2) interpolate_t -= interpolate_i interpolated_z_compensation = bed_mesh.lerp( interpolate_t, z_compensations[interpolate_i], z_compensations[interpolate_i + 1]) - pos[2] += interpolated_z_compensation - - def clear_compensations(self): - self.z_compensations = [] - self.m = None - self.b = None + return interpolated_z_compensation + def clear_compensations(self, axis=None): + if axis is None: + self.z_compensations = [] + self.zy_compensations = [] + elif axis == 'X': + self.z_compensations = [] + elif axis == 'Y': + self.zy_compensations = [] class Calibrater: def __init__(self, compensation, config): @@ -80,10 +107,14 @@ def __init__(self, compensation, config): self._handle_connect) self.speed = compensation.speed self.horizontal_move_z = compensation.horizontal_move_z - self.start_point = (compensation.calibrate_start_x, + self.x_start_point = (compensation.calibrate_start_x, compensation.calibrate_y) - self.end_point = (compensation.calibrate_end_x, + self.x_end_point = (compensation.calibrate_end_x, compensation.calibrate_y) + self.y_start_point = (compensation.calibrate_x, + compensation.calibrate_start_y) + self.y_end_point = (compensation.calibrate_x, + compensation.calibrate_end_y) self.results = None self.current_point_index = None self.gcmd = None @@ -119,20 +150,88 @@ def _register_gcode_handlers(self): def cmd_AXIS_TWIST_COMPENSATION_CALIBRATE(self, gcmd): self.gcmd = gcmd sample_count = gcmd.get_int('SAMPLE_COUNT', DEFAULT_SAMPLE_COUNT) + axis = gcmd.get('AXIS', None) + auto = gcmd.get('AUTO', False) + + if axis is not None and auto: + raise self.gcmd.error( + "Cannot use both 'AXIS' and 'AUTO' at the same time." + ) + + if auto: + self._start_autocalibration(sample_count) + return + + if axis is None and not auto: + axis = 'X' # check for valid sample_count - if sample_count is None or sample_count < 2: + if sample_count < 2: raise self.gcmd.error( "SAMPLE_COUNT to probe must be at least 2") - # clear the current config - self.compensation.clear_compensations() + # calculate the points to put the probe at, returned as a list of tuples + nozzle_points = [] + + if axis == 'X': + + self.compensation.clear_compensations('X') + + if not all([ + self.x_start_point[0], + self.x_end_point[0], + self.x_start_point[1] + ]): + raise self.gcmd.error( + """AXIS_TWIST_COMPENSATION for X axis requires + calibrate_start_x, calibrate_end_x and calibrate_y + to be defined + """ + ) + + start_point = self.x_start_point + end_point = self.x_end_point + + x_axis_range = end_point[0] - start_point[0] + interval_dist = x_axis_range / (sample_count - 1) + + for i in range(sample_count): + x = start_point[0] + i * interval_dist + y = start_point[1] + nozzle_points.append((x, y)) + + elif axis == 'Y': + + self.compensation.clear_compensations('Y') + + if not all([ + self.y_start_point[0], + self.y_end_point[0], + self.y_start_point[1] + ]): + raise self.gcmd.error( + """AXIS_TWIST_COMPENSATION for Y axis requires + calibrate_start_y, calibrate_end_y and calibrate_x + to be defined + """ + ) + + start_point = self.y_start_point + end_point = self.y_end_point + + y_axis_range = end_point[1] - start_point[1] + interval_dist = y_axis_range / (sample_count - 1) + + for i in range(sample_count): + x = start_point[0] + y = start_point[1] + i * interval_dist + nozzle_points.append((x, y)) + + else: + raise self.gcmd.error( + "AXIS_TWIST_COMPENSATION_CALIBRATE: " + "Invalid axis.") - # calculate some values - x_range = self.end_point[0] - self.start_point[0] - interval_dist = x_range / (sample_count - 1) - nozzle_points = self._calculate_nozzle_points(sample_count, - interval_dist) probe_points = self._calculate_probe_points( nozzle_points, self.probe_x_offset, self.probe_y_offset) @@ -142,16 +241,155 @@ def cmd_AXIS_TWIST_COMPENSATION_CALIBRATE(self, gcmd): # begin calibration self.current_point_index = 0 self.results = [] + self.current_axis = axis self._calibration(probe_points, nozzle_points, interval_dist) - def _calculate_nozzle_points(self, sample_count, interval_dist): - # calculate the points to put the probe at, returned as a list of tuples - nozzle_points = [] + def _calculate_corrections(self, coordinates): + # Extracting x, y, and z values from coordinates + x_coords = [coord[0] for coord in coordinates] + y_coords = [coord[1] for coord in coordinates] + z_coords = [coord[2] for coord in coordinates] + + # Calculate the desired point (average of all corner points in z) + # For a general case, we should extract the unique + # combinations of corner points + z_corners = [z_coords[i] for i, coord in enumerate(coordinates) + if (coord[0] in [x_coords[0], x_coords[-1]]) + and (coord[1] in [y_coords[0], y_coords[-1]])] + z_desired = sum(z_corners) / len(z_corners) + + + # Calculate average deformation per axis + unique_x_coords = sorted(set(x_coords)) + unique_y_coords = sorted(set(y_coords)) + + avg_z_x = [] + for x in unique_x_coords: + indices = [i for i, coord in enumerate(coordinates) + if coord[0] == x] + avg_z = sum(z_coords[i] for i in indices) / len(indices) + avg_z_x.append(avg_z) + + avg_z_y = [] + for y in unique_y_coords: + indices = [i for i, coord in enumerate(coordinates) + if coord[1] == y] + avg_z = sum(z_coords[i] for i in indices) / len(indices) + avg_z_y.append(avg_z) + + # Calculate corrections to reach the desired point + x_corrections = [z_desired - avg for avg in avg_z_x] + y_corrections = [z_desired - avg for avg in avg_z_y] + + return x_corrections, y_corrections + + def _start_autocalibration(self, sample_count): + + if not all([ + self.x_start_point[0], + self.x_end_point[0], + self.y_start_point[0], + self.y_end_point[0] + ]): + raise self.gcmd.error( + """AXIS_TWIST_COMPENSATION_AUTOCALIBRATE requires + calibrate_start_x, calibrate_end_x, calibrate_start_y + and calibrate_end_y to be defined + """ + ) + + # check for valid sample_count + if sample_count is None or sample_count < 2: + raise self.gcmd.error( + "SAMPLE_COUNT to probe must be at least 2") + + # verify no other manual probe is in progress + manual_probe.verify_no_manual_probe(self.printer) + + # clear the current config + self.compensation.clear_compensations() + + min_x = self.x_start_point[0] + max_x = self.x_end_point[0] + min_y = self.y_start_point[1] + max_y = self.y_end_point[1] + + # calculate x positions + interval_x = (max_x - min_x) / (sample_count - 1) + xps = [min_x + interval_x * i for i in range(sample_count)] + + # Calculate points array + interval_y = (max_y - min_y) / (sample_count - 1) + flip = False + + points = [] for i in range(sample_count): - x = self.start_point[0] + i * interval_dist - y = self.start_point[1] - nozzle_points.append((x, y)) - return nozzle_points + for j in range(sample_count): + if(not flip): + idx = j + else: + idx = sample_count -1 - j + points.append([xps[i], min_y + interval_y * idx ]) + flip = not flip + + + # calculate the points to put the nozzle at, and probe + probe_points = [] + + for i in range(len(points)): + x = points[i][0] - self.probe_x_offset + y = points[i][1] - self.probe_y_offset + probe_points.append([x, y, self._auto_calibration((x,y))[2]]) + + # calculate corrections + x_corr, y_corr = self._calculate_corrections(probe_points) + + x_corr_str = ', '.join(["{:.6f}".format(x) + for x in x_corr]) + + y_corr_str = ', '.join(["{:.6f}".format(x) + for x in y_corr]) + + # finalize + configfile = self.printer.lookup_object('configfile') + configfile.set(self.configname, 'z_compensations', x_corr_str) + configfile.set(self.configname, 'compensation_start_x', + self.x_start_point[0]) + configfile.set(self.configname, 'compensation_end_x', + self.x_end_point[0]) + + + configfile.set(self.configname, 'zy_compensations', y_corr_str) + configfile.set(self.configname, 'compensation_start_y', + self.y_start_point[1]) + configfile.set(self.configname, 'compensation_end_y', + self.y_end_point[1]) + + self.gcode.respond_info( + "AXIS_TWIST_COMPENSATION state has been saved " + "for the current session. The SAVE_CONFIG command will " + "update the printer config file and restart the printer.") + # output result + self.gcmd.respond_info( + "AXIS_TWIST_COMPENSATION_AUTOCALIBRATE: Calibration complete: ") + self.gcmd.respond_info("\n".join(map(str, [x_corr, y_corr])), log=False) + + def _auto_calibration(self, probe_point): + + # horizontal_move_z (to prevent probe trigger or hitting bed) + self._move_helper((None, None, self.horizontal_move_z)) + + # move to point to probe + self._move_helper((probe_point[0], + probe_point[1], None)) + + # probe the point + pos = probe.run_single_probe(self.probe, self.gcmd) + + # horizontal_move_z (to prevent probe trigger or hitting bed) + self._move_helper((None, None, self.horizontal_move_z)) + + return pos def _calculate_probe_points(self, nozzle_points, probe_x_offset, probe_y_offset): @@ -238,14 +476,31 @@ def _finalize_calibration(self): configfile = self.printer.lookup_object('configfile') values_as_str = ', '.join(["{:.6f}".format(x) for x in self.results]) - configfile.set(self.configname, 'z_compensations', values_as_str) - configfile.set(self.configname, 'compensation_start_x', - self.start_point[0]) - configfile.set(self.configname, 'compensation_end_x', - self.end_point[0]) - self.compensation.z_compensations = self.results - self.compensation.compensation_start_x = self.start_point[0] - self.compensation.compensation_end_x = self.end_point[0] + + if(self.current_axis == 'X'): + + configfile.set(self.configname, 'z_compensations', values_as_str) + configfile.set(self.configname, 'compensation_start_x', + self.x_start_point[0]) + configfile.set(self.configname, 'compensation_end_x', + self.x_end_point[0]) + + self.compensation.z_compensations = self.results + self.compensation.compensation_start_x = self.x_start_point[0] + self.compensation.compensation_end_x = self.x_end_point[0] + + elif(self.current_axis == 'Y'): + + configfile.set(self.configname, 'zy_compensations', values_as_str) + configfile.set(self.configname, 'compensation_start_y', + self.y_start_point[1]) + configfile.set(self.configname, 'compensation_end_y', + self.y_end_point[1]) + + self.compensation.zy_compensations = self.results + self.compensation.compensation_start_y = self.y_start_point[1] + self.compensation.compensation_end_y = self.y_end_point[1] + self.gcode.respond_info( "AXIS_TWIST_COMPENSATION state has been saved " "for the current session. The SAVE_CONFIG command will "