2017-12-06 09:00:33 +03:00
|
|
|
# Z-Probe support
|
|
|
|
#
|
2019-02-10 02:20:28 +03:00
|
|
|
# Copyright (C) 2017-2019 Kevin O'Connor <kevin@koconnor.net>
|
2017-12-06 09:00:33 +03:00
|
|
|
#
|
|
|
|
# This file may be distributed under the terms of the GNU GPLv3 license.
|
2019-02-10 02:20:28 +03:00
|
|
|
import pins, homing, manual_probe
|
2017-12-06 09:00:33 +03:00
|
|
|
|
2018-03-13 05:51:44 +03:00
|
|
|
HINT_TIMEOUT = """
|
|
|
|
Make sure to home the printer before probing. If the probe
|
2018-03-14 22:30:58 +03:00
|
|
|
did not move far enough to trigger, then consider reducing
|
|
|
|
the Z axis minimum position so the probe can travel further
|
|
|
|
(the Z minimum position can be negative).
|
2018-03-13 05:51:44 +03:00
|
|
|
"""
|
|
|
|
|
2017-12-06 09:00:33 +03:00
|
|
|
class PrinterProbe:
|
2018-11-25 05:41:25 +03:00
|
|
|
def __init__(self, config, mcu_probe):
|
2017-12-06 09:00:33 +03:00
|
|
|
self.printer = config.get_printer()
|
2019-02-10 02:20:28 +03:00
|
|
|
self.name = config.get_name()
|
2018-11-25 05:41:25 +03:00
|
|
|
self.mcu_probe = mcu_probe
|
2017-12-06 09:00:33 +03:00
|
|
|
self.speed = config.getfloat('speed', 5.0)
|
2018-08-18 19:25:57 +03:00
|
|
|
self.x_offset = config.getfloat('x_offset', 0.)
|
|
|
|
self.y_offset = config.getfloat('y_offset', 0.)
|
2018-03-17 21:00:37 +03:00
|
|
|
self.z_offset = config.getfloat('z_offset')
|
2018-03-14 22:30:58 +03:00
|
|
|
# Infer Z position to move to during a probe
|
|
|
|
if config.has_section('stepper_z'):
|
|
|
|
zconfig = config.getsection('stepper_z')
|
|
|
|
self.z_position = zconfig.getfloat('position_min', 0.)
|
|
|
|
else:
|
|
|
|
pconfig = config.getsection('printer')
|
|
|
|
self.z_position = pconfig.getfloat('minimum_z_position', 0.)
|
2018-11-25 05:41:25 +03:00
|
|
|
# Register z_virtual_endstop pin
|
|
|
|
self.printer.lookup_object('pins').register_chip('probe', self)
|
2018-01-23 20:47:28 +03:00
|
|
|
# Register PROBE/QUERY_PROBE commands
|
2017-12-06 09:00:33 +03:00
|
|
|
self.gcode = self.printer.lookup_object('gcode')
|
2019-02-10 02:20:28 +03:00
|
|
|
self.gcode.register_command('PROBE', self.cmd_PROBE,
|
|
|
|
desc=self.cmd_PROBE_help)
|
|
|
|
self.gcode.register_command('QUERY_PROBE', self.cmd_QUERY_PROBE,
|
|
|
|
desc=self.cmd_QUERY_PROBE_help)
|
|
|
|
self.gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE,
|
|
|
|
desc=self.cmd_PROBE_CALIBRATE_help)
|
2019-02-18 08:23:32 +03:00
|
|
|
self.gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY,
|
|
|
|
desc=self.cmd_PROBE_ACCURACY_help)
|
2018-07-26 16:44:45 +03:00
|
|
|
def setup_pin(self, pin_type, pin_params):
|
|
|
|
if pin_type != 'endstop' or pin_params['pin'] != 'z_virtual_endstop':
|
2018-01-23 20:47:28 +03:00
|
|
|
raise pins.error("Probe virtual endstop only useful as endstop pin")
|
|
|
|
if pin_params['invert'] or pin_params['pullup']:
|
|
|
|
raise pins.error("Can not pullup/invert probe virtual endstop")
|
2018-09-27 16:47:37 +03:00
|
|
|
return self.mcu_probe
|
2018-08-18 19:25:57 +03:00
|
|
|
def get_offsets(self):
|
|
|
|
return self.x_offset, self.y_offset, self.z_offset
|
2017-12-06 09:00:33 +03:00
|
|
|
cmd_PROBE_help = "Probe Z-height at current XY position"
|
|
|
|
def cmd_PROBE(self, params):
|
2019-02-18 08:23:32 +03:00
|
|
|
self._probe(self.speed)
|
|
|
|
def _probe(self, speed):
|
2017-12-06 09:00:33 +03:00
|
|
|
toolhead = self.printer.lookup_object('toolhead')
|
2018-10-08 21:23:20 +03:00
|
|
|
homing_state = homing.Homing(self.printer)
|
2017-12-06 09:00:33 +03:00
|
|
|
pos = toolhead.get_position()
|
|
|
|
pos[2] = self.z_position
|
2018-10-09 04:49:56 +03:00
|
|
|
endstops = [(self.mcu_probe, "probe")]
|
2019-02-10 02:20:28 +03:00
|
|
|
verify = self.printer.get_start_args().get('debugoutput') is None
|
2017-12-06 09:00:33 +03:00
|
|
|
try:
|
2019-02-18 08:23:32 +03:00
|
|
|
homing_state.homing_move(pos, endstops, speed,
|
2019-02-10 02:20:28 +03:00
|
|
|
probe_pos=True, verify_movement=verify)
|
2017-12-06 09:00:33 +03:00
|
|
|
except homing.EndstopError as e:
|
2018-03-13 05:51:44 +03:00
|
|
|
reason = str(e)
|
|
|
|
if "Timeout during endstop homing" in reason:
|
|
|
|
reason += HINT_TIMEOUT
|
|
|
|
raise self.gcode.error(reason)
|
2018-06-22 19:24:20 +03:00
|
|
|
pos = toolhead.get_position()
|
|
|
|
self.gcode.respond_info("probe at %.3f,%.3f is z=%.6f" % (
|
|
|
|
pos[0], pos[1], pos[2]))
|
2017-12-06 09:00:33 +03:00
|
|
|
self.gcode.reset_last_position()
|
|
|
|
cmd_QUERY_PROBE_help = "Return the status of the z-probe"
|
|
|
|
def cmd_QUERY_PROBE(self, params):
|
|
|
|
toolhead = self.printer.lookup_object('toolhead')
|
|
|
|
print_time = toolhead.get_last_move_time()
|
|
|
|
self.mcu_probe.query_endstop(print_time)
|
|
|
|
res = self.mcu_probe.query_endstop_wait()
|
|
|
|
self.gcode.respond_info(
|
|
|
|
"probe: %s" % (["open", "TRIGGERED"][not not res],))
|
2019-02-18 08:23:32 +03:00
|
|
|
cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position"
|
|
|
|
def cmd_PROBE_ACCURACY(self, params):
|
|
|
|
toolhead = self.printer.lookup_object('toolhead')
|
|
|
|
probes = []
|
|
|
|
pos = toolhead.get_position()
|
|
|
|
number_of_reads = self.gcode.get_int('REPEAT', params, default=10,
|
|
|
|
minval=4, maxval=50)
|
|
|
|
speed = self.gcode.get_int('SPEED', params, default=self.speed,
|
|
|
|
minval=1, maxval=30)
|
|
|
|
z_start_position = self.gcode.get_float('Z', params, default=10.,
|
2019-02-25 20:52:54 +03:00
|
|
|
minval=self.z_offset, maxval=70.)
|
2019-02-18 08:23:32 +03:00
|
|
|
x_start_position = self.gcode.get_float('X', params, default=pos[0])
|
|
|
|
y_start_position = self.gcode.get_float('Y', params, default=pos[1])
|
|
|
|
self.gcode.respond_info("probe accuracy: at X:%.3f Y:%.3f Z:%.3f\n"
|
|
|
|
" "
|
|
|
|
"and read %d times with speed of %d mm/s" % (
|
|
|
|
x_start_position, y_start_position,
|
|
|
|
z_start_position, number_of_reads, speed))
|
|
|
|
# Probe bed "number_of_reads" times
|
|
|
|
sum_reads = 0
|
|
|
|
for i in range(number_of_reads):
|
|
|
|
# Move Z to start reading position
|
|
|
|
self._move_position(x_start_position, y_start_position,
|
|
|
|
z_start_position, speed)
|
|
|
|
# Probe
|
|
|
|
self._probe(speed)
|
|
|
|
# Get Z value, accumulate value to calculate average
|
|
|
|
# and save it to calculate standard deviation
|
|
|
|
pos = toolhead.get_position()
|
|
|
|
sum_reads += pos[2]
|
|
|
|
probes.append(pos[2])
|
|
|
|
# Move Z to start reading position
|
|
|
|
self._move_position(x_start_position, y_start_position,
|
|
|
|
z_start_position, speed)
|
|
|
|
# Calculate maximum, minimum and average values
|
|
|
|
max_value = max(probes)
|
|
|
|
min_value = min(probes)
|
|
|
|
avg_value = sum(probes) / number_of_reads
|
|
|
|
# calculate the standard deviation
|
|
|
|
deviation_sum = 0
|
|
|
|
for i in range(number_of_reads):
|
|
|
|
deviation_sum += pow(probes[i] - avg_value, 2)
|
|
|
|
sigma = (deviation_sum / number_of_reads) ** 0.5
|
|
|
|
# Median
|
|
|
|
sorted_probes = sorted(probes)
|
|
|
|
middle = number_of_reads//2
|
|
|
|
if (number_of_reads & 1) == 1:
|
|
|
|
# odd number of reads
|
|
|
|
median = sorted_probes[middle]
|
|
|
|
else:
|
|
|
|
# even number of reads
|
|
|
|
median = (sorted_probes[middle]+sorted_probes[middle-1])/2
|
|
|
|
# Show information
|
|
|
|
self.gcode.respond_info(
|
|
|
|
"probe accuracy results: maximum %.6f, minimum %.6f, "
|
|
|
|
"average %.6f, median %.6f, standard deviation %.6f" % (
|
|
|
|
max_value, min_value, avg_value, median, sigma))
|
|
|
|
def _move_position(self, x, y, z, speed):
|
|
|
|
toolhead = self.printer.lookup_object('toolhead')
|
|
|
|
pos = toolhead.get_position()
|
|
|
|
# set new position
|
|
|
|
pos[0] = x
|
|
|
|
pos[1] = y
|
|
|
|
pos[2] = z
|
|
|
|
# Move to position
|
|
|
|
try:
|
|
|
|
toolhead.move(pos, speed)
|
|
|
|
except homing.EndstopError as e:
|
|
|
|
raise self.gcode.error(str(e))
|
2019-02-10 02:20:28 +03:00
|
|
|
def probe_calibrate_finalize(self, kin_pos):
|
|
|
|
if kin_pos is None:
|
|
|
|
return
|
|
|
|
z_pos = self.z_offset - kin_pos[2]
|
|
|
|
self.gcode.respond_info(
|
|
|
|
"%s: z_offset: %.3f\n"
|
|
|
|
"The SAVE_CONFIG command will update the printer config file\n"
|
|
|
|
"with the above and restart the printer." % (self.name, z_pos))
|
|
|
|
configfile = self.printer.lookup_object('configfile')
|
|
|
|
configfile.set(self.name, 'z_offset', "%.3f" % (z_pos,))
|
|
|
|
cmd_PROBE_CALIBRATE_help = "Calibrate the probe's z_offset"
|
|
|
|
def cmd_PROBE_CALIBRATE(self, params):
|
|
|
|
# Perform initial probe
|
|
|
|
self.cmd_PROBE(params)
|
|
|
|
# Move away from the bed
|
|
|
|
toolhead = self.printer.lookup_object('toolhead')
|
|
|
|
curpos = toolhead.get_position()
|
|
|
|
curpos[2] += 5.
|
|
|
|
toolhead.move(curpos, self.speed)
|
|
|
|
# Move the nozzle over the probe point
|
|
|
|
curpos[0] += self.x_offset
|
|
|
|
curpos[1] += self.y_offset
|
|
|
|
toolhead.move(curpos, self.speed)
|
|
|
|
# Start manual probe
|
|
|
|
manual_probe.ManualProbeHelper(self.printer, params,
|
|
|
|
self.probe_calibrate_finalize)
|
2017-12-06 09:00:33 +03:00
|
|
|
|
2018-09-27 16:47:37 +03:00
|
|
|
# Endstop wrapper that enables probe specific features
|
2018-01-23 20:26:18 +03:00
|
|
|
class ProbeEndstopWrapper:
|
2018-09-27 16:47:37 +03:00
|
|
|
def __init__(self, config):
|
|
|
|
self.printer = config.get_printer()
|
|
|
|
self.position_endstop = config.getfloat('z_offset')
|
|
|
|
self.activate_gcode = config.get('activate_gcode', None)
|
|
|
|
self.deactivate_gcode = config.get('deactivate_gcode', None)
|
|
|
|
# Create an "endstop" object to handle the probe pin
|
|
|
|
ppins = self.printer.lookup_object('pins')
|
|
|
|
pin = config.get('pin')
|
|
|
|
pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True)
|
|
|
|
mcu = pin_params['chip']
|
|
|
|
mcu.register_config_callback(self._build_config)
|
|
|
|
self.mcu_endstop = mcu.setup_pin('endstop', pin_params)
|
2018-01-23 20:26:18 +03:00
|
|
|
# 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
|
|
|
|
self.query_endstop_wait = self.mcu_endstop.query_endstop_wait
|
|
|
|
self.TimeoutError = self.mcu_endstop.TimeoutError
|
2018-09-27 16:47:37 +03:00
|
|
|
def _build_config(self):
|
|
|
|
kin = self.printer.lookup_object('toolhead').get_kinematics()
|
|
|
|
for stepper in kin.get_steppers('Z'):
|
|
|
|
stepper.add_to_endstop(self)
|
2018-01-23 20:26:18 +03:00
|
|
|
def home_prepare(self):
|
2018-09-27 16:47:37 +03:00
|
|
|
if self.activate_gcode is not None:
|
|
|
|
gcode = self.printer.lookup_object('gcode')
|
|
|
|
gcode.run_script_from_command(self.activate_gcode)
|
2018-01-23 20:26:18 +03:00
|
|
|
self.mcu_endstop.home_prepare()
|
|
|
|
def home_finalize(self):
|
2018-09-27 16:47:37 +03:00
|
|
|
if self.deactivate_gcode is not None:
|
|
|
|
gcode = self.printer.lookup_object('gcode')
|
|
|
|
gcode.run_script_from_command(self.deactivate_gcode)
|
2018-01-23 20:26:18 +03:00
|
|
|
self.mcu_endstop.home_finalize()
|
2018-09-27 02:29:58 +03:00
|
|
|
def get_position_endstop(self):
|
2018-09-27 16:47:37 +03:00
|
|
|
return self.position_endstop
|
2018-01-23 20:26:18 +03:00
|
|
|
|
2017-12-04 02:54:34 +03:00
|
|
|
# Helper code that can probe a series of points and report the
|
|
|
|
# position at each point.
|
|
|
|
class ProbePointsHelper:
|
2018-09-26 17:32:57 +03:00
|
|
|
def __init__(self, config, finalize_callback, default_points=None):
|
2018-05-20 18:28:28 +03:00
|
|
|
self.printer = config.get_printer()
|
2018-09-26 17:32:57 +03:00
|
|
|
self.finalize_callback = finalize_callback
|
2018-05-20 18:28:28 +03:00
|
|
|
self.probe_points = default_points
|
|
|
|
# Read config settings
|
|
|
|
if default_points is None or config.get('points', None) is not None:
|
|
|
|
points = config.get('points').split('\n')
|
|
|
|
try:
|
|
|
|
points = [line.split(',', 1) for line in points if line.strip()]
|
|
|
|
self.probe_points = [(float(p[0].strip()), float(p[1].strip()))
|
|
|
|
for p in points]
|
|
|
|
except:
|
|
|
|
raise config.error("Unable to parse probe points in %s" % (
|
|
|
|
config.get_name()))
|
2018-07-14 22:33:08 +03:00
|
|
|
if len(self.probe_points) < 3:
|
|
|
|
raise config.error("Need at least 3 probe points for %s" % (
|
|
|
|
config.get_name()))
|
2018-05-20 18:28:28 +03:00
|
|
|
self.horizontal_move_z = config.getfloat('horizontal_move_z', 5.)
|
|
|
|
self.speed = self.lift_speed = config.getfloat('speed', 50., above=0.)
|
2018-08-18 19:25:57 +03:00
|
|
|
self.probe_offsets = (0., 0., 0.)
|
2018-08-25 19:00:08 +03:00
|
|
|
self.samples = config.getint('samples', 1, minval=1)
|
|
|
|
self.sample_retract_dist = config.getfloat(
|
|
|
|
'sample_retract_dist', 2., above=0.)
|
2018-05-20 18:28:28 +03:00
|
|
|
# Internal probing state
|
2017-12-04 02:54:34 +03:00
|
|
|
self.results = []
|
2019-02-10 06:54:24 +03:00
|
|
|
self.busy = self.manual_probe = False
|
2018-05-20 18:28:28 +03:00
|
|
|
self.gcode = self.toolhead = None
|
2018-05-21 21:48:01 +03:00
|
|
|
def get_lift_speed(self):
|
|
|
|
return self.lift_speed
|
2018-09-26 18:25:56 +03:00
|
|
|
def _lift_z(self, z_pos, add=False, speed=None):
|
2018-08-25 17:55:12 +03:00
|
|
|
# Lift toolhead
|
|
|
|
curpos = self.toolhead.get_position()
|
2018-08-25 19:00:08 +03:00
|
|
|
if add:
|
|
|
|
curpos[2] += z_pos
|
|
|
|
else:
|
|
|
|
curpos[2] = z_pos
|
2018-08-30 23:20:23 +03:00
|
|
|
if speed is None:
|
|
|
|
speed = self.lift_speed
|
2018-08-25 17:55:12 +03:00
|
|
|
try:
|
2018-08-30 23:20:23 +03:00
|
|
|
self.toolhead.move(curpos, speed)
|
2018-08-25 17:55:12 +03:00
|
|
|
except homing.EndstopError as e:
|
2018-09-26 18:25:56 +03:00
|
|
|
self._finalize(False)
|
2018-08-25 17:55:12 +03:00
|
|
|
raise self.gcode.error(str(e))
|
2018-09-26 18:25:56 +03:00
|
|
|
def _move_next(self):
|
|
|
|
# Lift toolhead
|
|
|
|
self._lift_z(self.horizontal_move_z)
|
|
|
|
# Check if done probing
|
2018-09-26 19:15:39 +03:00
|
|
|
if len(self.results) >= len(self.probe_points):
|
2018-09-26 18:25:56 +03:00
|
|
|
self.toolhead.get_last_move_time()
|
|
|
|
self._finalize(True)
|
|
|
|
return
|
|
|
|
# Move to next XY probe point
|
2018-09-26 19:15:39 +03:00
|
|
|
x, y = self.probe_points[len(self.results)]
|
2018-09-26 18:51:29 +03:00
|
|
|
curpos = self.toolhead.get_position()
|
|
|
|
curpos[0] = x
|
|
|
|
curpos[1] = y
|
|
|
|
curpos[2] = self.horizontal_move_z
|
|
|
|
try:
|
|
|
|
self.toolhead.move(curpos, self.speed)
|
|
|
|
except homing.EndstopError as e:
|
2018-09-26 18:25:56 +03:00
|
|
|
self._finalize(False)
|
2018-09-26 18:51:29 +03:00
|
|
|
raise self.gcode.error(str(e))
|
|
|
|
self.gcode.reset_last_position()
|
2019-02-10 06:54:24 +03:00
|
|
|
if self.manual_probe:
|
|
|
|
manual_probe.ManualProbeHelper(self.printer, {},
|
|
|
|
self._manual_probe_finalize)
|
2018-09-26 18:25:56 +03:00
|
|
|
def _automatic_probe_point(self):
|
2018-09-26 19:15:39 +03:00
|
|
|
positions = []
|
2018-08-25 19:00:08 +03:00
|
|
|
for i in range(self.samples):
|
2018-09-26 18:25:56 +03:00
|
|
|
try:
|
|
|
|
self.gcode.run_script_from_command("PROBE")
|
|
|
|
except self.gcode.error as e:
|
|
|
|
self._finalize(False)
|
|
|
|
raise
|
2018-09-26 19:15:39 +03:00
|
|
|
positions.append(self.toolhead.get_position())
|
2018-08-25 19:00:08 +03:00
|
|
|
if i < self.samples - 1:
|
|
|
|
# retract
|
2018-09-26 18:25:56 +03:00
|
|
|
self._lift_z(self.sample_retract_dist, add=True)
|
2018-09-26 19:15:39 +03:00
|
|
|
avg_pos = [sum([pos[i] for pos in positions]) / self.samples
|
|
|
|
for i in range(3)]
|
|
|
|
self.results.append(avg_pos)
|
2018-09-26 20:07:18 +03:00
|
|
|
def start_probe(self, params):
|
|
|
|
# Lookup objects
|
2018-05-20 18:28:28 +03:00
|
|
|
self.toolhead = self.printer.lookup_object('toolhead')
|
2017-12-04 02:54:34 +03:00
|
|
|
self.gcode = self.printer.lookup_object('gcode')
|
2018-09-26 20:07:18 +03:00
|
|
|
probe = self.printer.lookup_object('probe', None)
|
|
|
|
method = self.gcode.get_str('METHOD', params, 'automatic').lower()
|
|
|
|
if probe is not None and method == 'automatic':
|
2019-02-10 06:54:24 +03:00
|
|
|
self.manual_probe = False
|
2018-09-26 20:07:18 +03:00
|
|
|
self.lift_speed = min(self.speed, probe.speed)
|
|
|
|
self.probe_offsets = probe.get_offsets()
|
|
|
|
if self.horizontal_move_z < self.probe_offsets[2]:
|
|
|
|
raise self.gcode.error("horizontal_move_z can't be less than"
|
|
|
|
" probe's z_offset")
|
|
|
|
else:
|
2019-02-10 06:54:24 +03:00
|
|
|
self.manual_probe = True
|
2018-09-26 20:07:18 +03:00
|
|
|
self.lift_speed = self.speed
|
|
|
|
self.probe_offsets = (0., 0., 0.)
|
|
|
|
# Start probe
|
2018-05-20 18:28:28 +03:00
|
|
|
self.results = []
|
|
|
|
self.busy = True
|
2018-09-26 18:25:56 +03:00
|
|
|
self._lift_z(self.horizontal_move_z, speed=self.speed)
|
|
|
|
self._move_next()
|
2019-02-10 06:54:24 +03:00
|
|
|
if not self.manual_probe:
|
2018-09-26 18:25:56 +03:00
|
|
|
# Perform automatic probing
|
|
|
|
while self.busy:
|
|
|
|
self._automatic_probe_point()
|
|
|
|
self._move_next()
|
2019-02-10 06:54:24 +03:00
|
|
|
def _manual_probe_finalize(self, kin_pos):
|
|
|
|
if kin_pos is None:
|
|
|
|
self._finalize(False)
|
|
|
|
return
|
|
|
|
self.results.append(kin_pos)
|
2018-09-26 18:25:56 +03:00
|
|
|
self._move_next()
|
|
|
|
def _finalize(self, success):
|
2017-12-04 02:54:34 +03:00
|
|
|
self.busy = False
|
|
|
|
self.gcode.reset_last_position()
|
|
|
|
if success:
|
2018-09-26 17:32:57 +03:00
|
|
|
self.finalize_callback(self.probe_offsets, self.results)
|
2017-12-04 02:54:34 +03:00
|
|
|
|
2017-12-06 09:00:33 +03:00
|
|
|
def load_config(config):
|
2018-11-25 05:41:25 +03:00
|
|
|
return PrinterProbe(config, ProbeEndstopWrapper(config))
|