2017-12-04 00:54:34 +01:00
|
|
|
# Delta calibration support
|
|
|
|
#
|
2019-12-05 15:03:37 +01:00
|
|
|
# Copyright (C) 2017-2019 Kevin O'Connor <kevin@koconnor.net>
|
2017-12-04 00:54:34 +01:00
|
|
|
#
|
|
|
|
# This file may be distributed under the terms of the GNU GPLv3 license.
|
2018-09-03 19:34:27 +02:00
|
|
|
import math, logging, collections
|
2020-06-12 15:55:57 +02:00
|
|
|
import mathutil
|
|
|
|
from . import probe
|
2017-12-04 00:54:34 +01:00
|
|
|
|
2018-09-03 19:34:27 +02:00
|
|
|
# A "stable position" is a 3-tuple containing the number of steps
|
|
|
|
# taken since hitting the endstop on each delta tower. Delta
|
|
|
|
# calibration uses this coordinate system because it allows a position
|
|
|
|
# to be described independent of the software parameters.
|
|
|
|
|
2018-09-04 22:48:36 +02:00
|
|
|
# Load a stable position from a config entry
|
|
|
|
def load_config_stable(config, option):
|
2021-08-19 20:54:10 +02:00
|
|
|
return config.getfloatlist(option, count=3)
|
2018-09-04 22:48:36 +02:00
|
|
|
|
2018-09-03 19:34:27 +02:00
|
|
|
|
2018-09-05 16:38:19 +02:00
|
|
|
######################################################################
|
|
|
|
# Delta calibration object
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
# The angles and distances of the calibration object found in
|
|
|
|
# docs/prints/calibrate_size.stl
|
|
|
|
MeasureAngles = [210., 270., 330., 30., 90., 150.]
|
|
|
|
MeasureOuterRadius = 65
|
|
|
|
MeasureRidgeRadius = 5. - .5
|
|
|
|
|
|
|
|
# How much to prefer a distance measurement over a height measurement
|
|
|
|
MEASURE_WEIGHT = 0.5
|
|
|
|
|
|
|
|
# Convert distance measurements made on the calibration object to
|
|
|
|
# 3-tuples of (actual_distance, stable_position1, stable_position2)
|
|
|
|
def measurements_to_distances(measured_params, delta_params):
|
|
|
|
# Extract params
|
|
|
|
mp = measured_params
|
|
|
|
dp = delta_params
|
|
|
|
scale = mp['SCALE'][0]
|
|
|
|
cpw = mp['CENTER_PILLAR_WIDTHS']
|
|
|
|
center_widths = [cpw[0], cpw[2], cpw[1], cpw[0], cpw[2], cpw[1]]
|
|
|
|
center_dists = [od - cw
|
|
|
|
for od, cw in zip(mp['CENTER_DISTS'], center_widths)]
|
|
|
|
outer_dists = [
|
|
|
|
od - opw
|
|
|
|
for od, opw in zip(mp['OUTER_DISTS'], mp['OUTER_PILLAR_WIDTHS']) ]
|
|
|
|
# Convert angles in degrees to an XY multiplier
|
2022-06-13 19:51:07 +02:00
|
|
|
obj_angles = list(map(math.radians, MeasureAngles))
|
2020-06-12 16:04:13 +02:00
|
|
|
xy_angles = list(zip(map(math.cos, obj_angles), map(math.sin, obj_angles)))
|
2018-09-05 16:38:19 +02:00
|
|
|
# Calculate stable positions for center measurements
|
|
|
|
inner_ridge = MeasureRidgeRadius * scale
|
|
|
|
inner_pos = [(ax * inner_ridge, ay * inner_ridge, 0.)
|
|
|
|
for ax, ay in xy_angles]
|
|
|
|
outer_ridge = (MeasureOuterRadius + MeasureRidgeRadius) * scale
|
|
|
|
outer_pos = [(ax * outer_ridge, ay * outer_ridge, 0.)
|
|
|
|
for ax, ay in xy_angles]
|
|
|
|
center_positions = [
|
2019-12-05 15:03:37 +01:00
|
|
|
(cd, dp.calc_stable_position(ip), dp.calc_stable_position(op))
|
2018-09-05 16:38:19 +02:00
|
|
|
for cd, ip, op in zip(center_dists, inner_pos, outer_pos)]
|
|
|
|
# Calculate positions of outer measurements
|
|
|
|
outer_center = MeasureOuterRadius * scale
|
|
|
|
start_pos = [(ax * outer_center, ay * outer_center) for ax, ay in xy_angles]
|
|
|
|
shifted_angles = xy_angles[2:] + xy_angles[:2]
|
|
|
|
first_pos = [(ax * inner_ridge + spx, ay * inner_ridge + spy, 0.)
|
|
|
|
for (ax, ay), (spx, spy) in zip(shifted_angles, start_pos)]
|
|
|
|
second_pos = [(ax * outer_ridge + spx, ay * outer_ridge + spy, 0.)
|
|
|
|
for (ax, ay), (spx, spy) in zip(shifted_angles, start_pos)]
|
|
|
|
outer_positions = [
|
2019-12-05 15:03:37 +01:00
|
|
|
(od, dp.calc_stable_position(fp), dp.calc_stable_position(sp))
|
2018-09-05 16:38:19 +02:00
|
|
|
for od, fp, sp in zip(outer_dists, first_pos, second_pos)]
|
|
|
|
return center_positions + outer_positions
|
|
|
|
|
|
|
|
|
2018-09-03 19:34:27 +02:00
|
|
|
######################################################################
|
|
|
|
# Delta Calibrate class
|
|
|
|
######################################################################
|
|
|
|
|
2017-12-04 00:54:34 +01:00
|
|
|
class DeltaCalibrate:
|
|
|
|
def __init__(self, config):
|
|
|
|
self.printer = config.get_printer()
|
2019-12-05 16:44:08 +01:00
|
|
|
self.printer.register_event_handler("klippy:connect",
|
|
|
|
self.handle_connect)
|
2018-05-20 17:28:28 +02:00
|
|
|
# Calculate default probing points
|
|
|
|
radius = config.getfloat('radius', above=0.)
|
|
|
|
points = [(0., 0.)]
|
|
|
|
scatter = [.95, .90, .85, .70, .75, .80]
|
|
|
|
for i in range(6):
|
|
|
|
r = math.radians(90. + 60. * i)
|
|
|
|
dist = radius * scatter[i]
|
|
|
|
points.append((math.cos(r) * dist, math.sin(r) * dist))
|
|
|
|
self.probe_helper = probe.ProbePointsHelper(
|
2018-09-26 16:32:57 +02:00
|
|
|
config, self.probe_finalize, default_points=points)
|
2019-05-18 05:47:27 +02:00
|
|
|
self.probe_helper.minimum_points(3)
|
2018-09-04 22:48:36 +02:00
|
|
|
# Restore probe stable positions
|
|
|
|
self.last_probe_positions = []
|
|
|
|
for i in range(999):
|
|
|
|
height = config.getfloat("height%d" % (i,), None)
|
|
|
|
if height is None:
|
|
|
|
break
|
|
|
|
height_pos = load_config_stable(config, "height%d_pos" % (i,))
|
|
|
|
self.last_probe_positions.append((height, height_pos))
|
2019-12-05 23:54:10 +01:00
|
|
|
# Restore manually entered heights
|
|
|
|
self.manual_heights = []
|
|
|
|
for i in range(999):
|
|
|
|
height = config.getfloat("manual_height%d" % (i,), None)
|
|
|
|
if height is None:
|
|
|
|
break
|
|
|
|
height_pos = load_config_stable(config, "manual_height%d_pos"
|
|
|
|
% (i,))
|
|
|
|
self.manual_heights.append((height, height_pos))
|
2018-09-05 16:38:19 +02:00
|
|
|
# Restore distance measurements
|
|
|
|
self.delta_analyze_entry = {'SCALE': (1.,)}
|
|
|
|
self.last_distances = []
|
|
|
|
for i in range(999):
|
|
|
|
dist = config.getfloat("distance%d" % (i,), None)
|
|
|
|
if dist is None:
|
|
|
|
break
|
|
|
|
distance_pos1 = load_config_stable(config, "distance%d_pos1" % (i,))
|
|
|
|
distance_pos2 = load_config_stable(config, "distance%d_pos2" % (i,))
|
|
|
|
self.last_distances.append((dist, distance_pos1, distance_pos2))
|
|
|
|
# Register gcode commands
|
2017-12-04 00:54:34 +01:00
|
|
|
self.gcode = self.printer.lookup_object('gcode')
|
2018-09-05 16:38:19 +02:00
|
|
|
self.gcode.register_command('DELTA_CALIBRATE', self.cmd_DELTA_CALIBRATE,
|
|
|
|
desc=self.cmd_DELTA_CALIBRATE_help)
|
|
|
|
self.gcode.register_command('DELTA_ANALYZE', self.cmd_DELTA_ANALYZE,
|
|
|
|
desc=self.cmd_DELTA_ANALYZE_help)
|
2019-12-05 16:44:08 +01:00
|
|
|
def handle_connect(self):
|
|
|
|
kin = self.printer.lookup_object('toolhead').get_kinematics()
|
|
|
|
if not hasattr(kin, "get_calibration"):
|
|
|
|
raise self.printer.config_error(
|
|
|
|
"Delta calibrate is only for delta printers")
|
2019-12-05 15:03:37 +01:00
|
|
|
def save_state(self, probe_positions, distances, delta_params):
|
2018-09-04 22:48:36 +02:00
|
|
|
# Save main delta parameters
|
|
|
|
configfile = self.printer.lookup_object('configfile')
|
2019-12-05 15:03:37 +01:00
|
|
|
delta_params.save_state(configfile)
|
2018-09-04 22:48:36 +02:00
|
|
|
# Save probe stable positions
|
|
|
|
section = 'delta_calibrate'
|
|
|
|
configfile.remove_section(section)
|
|
|
|
for i, (z_offset, spos) in enumerate(probe_positions):
|
|
|
|
configfile.set(section, "height%d" % (i,), z_offset)
|
|
|
|
configfile.set(section, "height%d_pos" % (i,),
|
2018-09-26 18:15:39 +02:00
|
|
|
"%.3f,%.3f,%.3f" % tuple(spos))
|
2019-12-05 23:54:10 +01:00
|
|
|
# Save manually entered heights
|
|
|
|
for i, (z_offset, spos) in enumerate(self.manual_heights):
|
|
|
|
configfile.set(section, "manual_height%d" % (i,), z_offset)
|
|
|
|
configfile.set(section, "manual_height%d_pos" % (i,),
|
|
|
|
"%.3f,%.3f,%.3f" % tuple(spos))
|
2018-09-05 16:38:19 +02:00
|
|
|
# Save distance measurements
|
|
|
|
for i, (dist, spos1, spos2) in enumerate(distances):
|
|
|
|
configfile.set(section, "distance%d" % (i,), dist)
|
|
|
|
configfile.set(section, "distance%d_pos1" % (i,),
|
|
|
|
"%.3f,%.3f,%.3f" % tuple(spos1))
|
|
|
|
configfile.set(section, "distance%d_pos2" % (i,),
|
|
|
|
"%.3f,%.3f,%.3f" % tuple(spos2))
|
2018-09-26 16:32:57 +02:00
|
|
|
def probe_finalize(self, offsets, positions):
|
2018-09-04 22:48:36 +02:00
|
|
|
# Convert positions into (z_offset, stable_position) pairs
|
2018-08-18 18:25:57 +02:00
|
|
|
z_offset = offsets[2]
|
2017-12-04 00:54:34 +01:00
|
|
|
kin = self.printer.lookup_object('toolhead').get_kinematics()
|
2019-12-05 16:44:08 +01:00
|
|
|
delta_params = kin.get_calibration()
|
2019-12-05 15:03:37 +01:00
|
|
|
probe_positions = [(z_offset, delta_params.calc_stable_position(p))
|
2018-09-04 22:48:36 +02:00
|
|
|
for p in positions]
|
|
|
|
# Perform analysis
|
2018-09-05 16:38:19 +02:00
|
|
|
self.calculate_params(probe_positions, self.last_distances)
|
|
|
|
def calculate_params(self, probe_positions, distances):
|
2019-12-05 23:54:10 +01:00
|
|
|
height_positions = self.manual_heights + probe_positions
|
2018-09-04 22:48:36 +02:00
|
|
|
# Setup for coordinate descent analysis
|
|
|
|
kin = self.printer.lookup_object('toolhead').get_kinematics()
|
2019-12-05 16:44:08 +01:00
|
|
|
orig_delta_params = odp = kin.get_calibration()
|
2019-12-05 15:03:37 +01:00
|
|
|
adj_params, params = odp.coordinate_descent_params(distances)
|
2018-09-05 16:38:19 +02:00
|
|
|
logging.info("Calculating delta_calibrate with:\n%s\n%s\n"
|
2018-09-03 19:34:27 +02:00
|
|
|
"Initial delta_calibrate parameters: %s",
|
2019-12-05 23:54:10 +01:00
|
|
|
height_positions, distances, params)
|
2018-09-05 16:38:19 +02:00
|
|
|
z_weight = 1.
|
|
|
|
if distances:
|
|
|
|
z_weight = len(distances) / (MEASURE_WEIGHT * len(probe_positions))
|
2018-09-04 22:48:36 +02:00
|
|
|
# Perform coordinate descent
|
2017-12-04 00:54:34 +01:00
|
|
|
def delta_errorfunc(params):
|
2020-09-18 04:41:13 +02:00
|
|
|
try:
|
|
|
|
# Build new delta_params for params under test
|
|
|
|
delta_params = orig_delta_params.new_calibration(params)
|
|
|
|
getpos = delta_params.get_position_from_stable
|
|
|
|
# Calculate z height errors
|
|
|
|
total_error = 0.
|
|
|
|
for z_offset, stable_pos in height_positions:
|
|
|
|
x, y, z = getpos(stable_pos)
|
|
|
|
total_error += (z - z_offset)**2
|
|
|
|
total_error *= z_weight
|
|
|
|
# Calculate distance errors
|
|
|
|
for dist, stable_pos1, stable_pos2 in distances:
|
|
|
|
x1, y1, z1 = getpos(stable_pos1)
|
|
|
|
x2, y2, z2 = getpos(stable_pos2)
|
|
|
|
d = math.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
|
|
|
|
total_error += (d - dist)**2
|
|
|
|
return total_error
|
|
|
|
except ValueError:
|
|
|
|
return 9999999999999.9
|
2018-10-16 00:52:28 +02:00
|
|
|
new_params = mathutil.background_coordinate_descent(
|
|
|
|
self.printer, adj_params, params, delta_errorfunc)
|
2018-09-04 22:48:36 +02:00
|
|
|
# Log and report results
|
2018-02-16 19:30:49 +01:00
|
|
|
logging.info("Calculated delta_calibrate parameters: %s", new_params)
|
2019-12-05 15:03:37 +01:00
|
|
|
new_delta_params = orig_delta_params.new_calibration(new_params)
|
2019-12-05 23:54:10 +01:00
|
|
|
for z_offset, spos in height_positions:
|
2018-09-04 22:48:36 +02:00
|
|
|
logging.info("height orig: %.6f new: %.6f goal: %.6f",
|
2019-12-05 15:03:37 +01:00
|
|
|
orig_delta_params.get_position_from_stable(spos)[2],
|
|
|
|
new_delta_params.get_position_from_stable(spos)[2],
|
2018-09-04 22:48:36 +02:00
|
|
|
z_offset)
|
2018-09-05 16:38:19 +02:00
|
|
|
for dist, spos1, spos2 in distances:
|
2019-12-05 15:03:37 +01:00
|
|
|
x1, y1, z1 = orig_delta_params.get_position_from_stable(spos1)
|
|
|
|
x2, y2, z2 = orig_delta_params.get_position_from_stable(spos2)
|
2018-09-05 16:38:19 +02:00
|
|
|
orig_dist = math.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
|
2019-12-05 15:03:37 +01:00
|
|
|
x1, y1, z1 = new_delta_params.get_position_from_stable(spos1)
|
|
|
|
x2, y2, z2 = new_delta_params.get_position_from_stable(spos2)
|
2018-09-05 16:38:19 +02:00
|
|
|
new_dist = math.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
|
|
|
|
logging.info("distance orig: %.6f new: %.6f goal: %.6f",
|
|
|
|
orig_dist, new_dist, dist)
|
2019-12-05 15:03:37 +01:00
|
|
|
# Store results for SAVE_CONFIG
|
|
|
|
self.save_state(probe_positions, distances, new_delta_params)
|
2017-12-04 00:54:34 +01:00
|
|
|
self.gcode.respond_info(
|
2018-09-04 22:48:36 +02:00
|
|
|
"The SAVE_CONFIG command will update the printer config file\n"
|
2019-12-05 15:03:37 +01:00
|
|
|
"with these parameters and restart the printer.")
|
2018-09-05 16:38:19 +02:00
|
|
|
cmd_DELTA_CALIBRATE_help = "Delta calibration script"
|
2020-04-25 05:22:43 +02:00
|
|
|
def cmd_DELTA_CALIBRATE(self, gcmd):
|
|
|
|
self.probe_helper.start_probe(gcmd)
|
2019-12-05 23:54:10 +01:00
|
|
|
def add_manual_height(self, height):
|
|
|
|
# Determine current location of toolhead
|
|
|
|
toolhead = self.printer.lookup_object('toolhead')
|
|
|
|
toolhead.flush_step_generation()
|
|
|
|
kin = toolhead.get_kinematics()
|
2021-05-01 06:27:43 +02:00
|
|
|
kin_spos = {s.get_name(): s.get_commanded_position()
|
|
|
|
for s in kin.get_steppers()}
|
|
|
|
kin_pos = kin.calc_position(kin_spos)
|
2019-12-05 23:54:10 +01:00
|
|
|
# Convert location to a stable position
|
|
|
|
delta_params = kin.get_calibration()
|
|
|
|
stable_pos = tuple(delta_params.calc_stable_position(kin_pos))
|
|
|
|
# Add to list of manual heights
|
|
|
|
self.manual_heights.append((height, stable_pos))
|
|
|
|
self.gcode.respond_info(
|
|
|
|
"Adding manual height: %.3f,%.3f,%.3f is actually z=%.3f"
|
|
|
|
% (kin_pos[0], kin_pos[1], kin_pos[2], height))
|
2018-09-05 16:38:19 +02:00
|
|
|
def do_extended_calibration(self):
|
|
|
|
# Extract distance positions
|
|
|
|
if len(self.delta_analyze_entry) <= 1:
|
|
|
|
distances = self.last_distances
|
|
|
|
elif len(self.delta_analyze_entry) < 5:
|
|
|
|
raise self.gcode.error("Not all measurements provided")
|
|
|
|
else:
|
|
|
|
kin = self.printer.lookup_object('toolhead').get_kinematics()
|
2019-12-05 16:44:08 +01:00
|
|
|
delta_params = kin.get_calibration()
|
2018-09-05 16:38:19 +02:00
|
|
|
distances = measurements_to_distances(
|
|
|
|
self.delta_analyze_entry, delta_params)
|
|
|
|
if not self.last_probe_positions:
|
|
|
|
raise self.gcode.error(
|
|
|
|
"Must run basic calibration with DELTA_CALIBRATE first")
|
|
|
|
# Perform analysis
|
|
|
|
self.calculate_params(self.last_probe_positions, distances)
|
|
|
|
cmd_DELTA_ANALYZE_help = "Extended delta calibration tool"
|
2020-04-25 05:22:43 +02:00
|
|
|
def cmd_DELTA_ANALYZE(self, gcmd):
|
2019-12-05 23:54:10 +01:00
|
|
|
# Check for manual height entry
|
2020-04-25 05:22:43 +02:00
|
|
|
mheight = gcmd.get_float('MANUAL_HEIGHT', None)
|
2019-12-05 23:54:10 +01:00
|
|
|
if mheight is not None:
|
|
|
|
self.add_manual_height(mheight)
|
|
|
|
return
|
2018-09-05 16:38:19 +02:00
|
|
|
# Parse distance measurements
|
|
|
|
args = {'CENTER_DISTS': 6, 'CENTER_PILLAR_WIDTHS': 3,
|
|
|
|
'OUTER_DISTS': 6, 'OUTER_PILLAR_WIDTHS': 6, 'SCALE': 1}
|
|
|
|
for name, count in args.items():
|
2020-04-25 05:22:43 +02:00
|
|
|
data = gcmd.get(name, None)
|
|
|
|
if data is None:
|
2018-09-05 16:38:19 +02:00
|
|
|
continue
|
|
|
|
try:
|
2020-06-12 16:04:13 +02:00
|
|
|
parts = list(map(float, data.split(',')))
|
2018-09-05 16:38:19 +02:00
|
|
|
except:
|
2020-04-25 05:22:43 +02:00
|
|
|
raise gcmd.error("Unable to parse parameter '%s'" % (name,))
|
2018-09-05 16:38:19 +02:00
|
|
|
if len(parts) != count:
|
2020-04-25 05:22:43 +02:00
|
|
|
raise gcmd.error("Parameter '%s' must have %d values"
|
|
|
|
% (name, count))
|
2018-09-05 16:38:19 +02:00
|
|
|
self.delta_analyze_entry[name] = parts
|
|
|
|
logging.info("DELTA_ANALYZE %s = %s", name, parts)
|
|
|
|
# Perform analysis if requested
|
2020-04-25 05:22:43 +02:00
|
|
|
action = gcmd.get('CALIBRATE', None)
|
|
|
|
if action is not None:
|
|
|
|
if action != 'extended':
|
|
|
|
raise gcmd.error("Unknown calibrate action")
|
2018-09-05 16:38:19 +02:00
|
|
|
self.do_extended_calibration()
|
2017-12-04 00:54:34 +01:00
|
|
|
|
|
|
|
def load_config(config):
|
|
|
|
return DeltaCalibrate(config)
|