diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index eecb48ea5..026d47e58 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2013,6 +2013,14 @@ sensor_type: ldc1612 #z_offset: # The nominal distance (in mm) between the nozzle and bed that a # probing attempt should stop at. This parameter must be provided. +#tap_height: +# Specify this parameter to enable probing until a nozzle contact +# with bed is detected. The parameter here specifies the minimum +# distance between bed and nozzle (in mm, as detected by the probe) +# for the probe to enable "tap" mode. If this parameter is specified +# and the toolhead starts a probe request while above this height +# (as detected by the probe) then an error will be reported. The +# default is to not perform "tap" based probing. #i2c_address: #i2c_mcu: #i2c_bus: diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 221c855b6..6b259c556 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -54,3 +54,49 @@ result in changes in reported Z height. Changes in either the bed surface temperature or sensor hardware temperature can skew the results. It is important that calibration and probing is only done when the printer is at a stable temperature. + +# Using "tap" based probing + +This is an experimental feature only useful for development and +testing. It does not produce accurate results. It is likely to cause +uncontrolled contact between nozzle and bed that results in damage. It +regularly reports false readings. + +In "tap" mode the probe detects contact between nozzle and bed. To use +this feature: + +* Ensure that the Z carriage is capable of moving the nozzle into + contact with the bed without causing damage to the bed, the nozzle, + the printer frame, Z endstops, etc. +* Ensure that the probe is always over the metal bed while probing. + The nozzle/bed contact can only be detected when the probe can + accurately sense the distance to the bed. +* Ensure that there is a strong mechanical connection between nozzle + and probe, such that when the nozzle stops descending (due to + contact with the bed) the probe will also stop descending. +* Do not probe with a slow Z probing speed nor with a slow Z + acceleration. (Consider 10mm/s or faster Z speeds; consider + 100mm/s^2 or faster Z acceleration.) The probe descent speed must be + sufficiently high to reliably detect when the probe is no longer + descending. +* Ensure that the `[stepper_z]` `position_min` is set to a + sufficiently large negative value. (Consider using `position_min: + -5`.) This is to ensure that the nozzle always contacts the bed + prior to when the toolhead starts to decelerate. +* Ensure that the probe is always several millimeters away from the + bed prior to starting a probe attempt. A contact is not detected at + the start of probing; detection only starts after the toolhead + finishes acceleration and reaches a constant Z descent velocity. +* The "tap" detection can only detect direct contact with the metal + bed, or contact with an object that is a few millimeters above the + metal bed. The probe can not reliably detect contact with objects + that are more than a few millimeters above the metal bed. +* Fully calibrate the probe using the directions at the top of this + document. +* Add `tap_height: 2.0` to the `[probe_eddy_current]` config section + and remove any `x_offset`/`y_offset` definitions. This enables "tap" + probing. It also activates a safety check to halt descent if the + probe senses that it is less than 2mm from the bed prior to the + toolhead reaching a constant Z descent velocity. +* Ensure the nozzle is clean prior to probing. Any debris or plastic + remnants on the nozzle may skew the results. diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index dc7dd82a4..45d1bf9c4 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -155,6 +155,26 @@ def setup_home(self, print_time, trigger_freq, trsync_oid): mcu.MCU_trsync.REASON_ENDSTOP_HIT, mcu.MCU_trsync.REASON_SENSOR_ERROR, 0, 0, 0, 0]) + def setup_tap(self, print_time, tap_time, pretap_freq, trsync_oid, + tap_threshold, tap_factor): + clock = self.mcu.print_time_to_clock(print_time) + tap_clock = self.mcu.print_time_to_clock(tap_time) + if tap_clock - clock >= 1<<31: + raise self.printer.command_error( + "ldc1612 tap time too far in future") + ptfreq = int(pretap_freq * (1<<28) / float(LDC1612_FREQ) + 0.5) + adj_factor = tap_factor / self.data_rate + tapfreq = -int(tap_threshold * adj_factor * (1<<28) + / float(LDC1612_FREQ) + 0.5) + tfactor = int(adj_factor * (1<<32) + 0.5) + logging.info("ptf=%f/%d tt=%f/%d tf=%f/%d", pretap_freq, ptfreq, + tap_threshold, tapfreq, tap_factor, tfactor) + self.ldc1612_setup_home_cmd.send( + [self.oid, clock, ptfreq, trsync_oid, + mcu.MCU_trsync.REASON_ENDSTOP_HIT, + mcu.MCU_trsync.REASON_SENSOR_ERROR, + tapfreq, tfactor, tap_clock, + mcu.MCU_trsync.REASON_SENSOR_ERROR+1]) def clear_home(self): self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0, 0, 0, 0, 0, 0]) if self.mcu.is_fileoutput(): diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 147cf47b4..108ea16f6 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -7,6 +7,18 @@ import mcu from . import ldc1612, probe, manual_probe +TAP_NOISE_COMPENSATION = 0.8 + +# Linear regression (a*x + b) for list of (x,y) pairs +def simple_linear_regression(data): + inv_count = 1. / len(data) + x_avg = sum([d[0] for d in data]) * inv_count + y_avg = sum([d[1] for d in data]) * inv_count + x_var = sum([(d[0] - x_avg)**2 for d in data]) + xy_covar = sum([(d[0] - x_avg)*(d[1] - y_avg) for d in data]) + slope = xy_covar / x_var + return slope, y_avg - slope*x_avg + # Tool for calibrating the sensor Z detection and applying that calibration class EddyCalibration: def __init__(self, config): @@ -20,6 +32,9 @@ def __init__(self, config): cal = [list(map(float, d.strip().split(':', 1))) for d in cal.split(',')] self.load_calibration(cal) + # Estimate toolhead velocity to change in frequency + self.tap_threshold = self.tap_factor = 0. + self.calc_frequency_rate() # Probe calibrate state self.probe_speed = 0. # Register commands @@ -30,6 +45,32 @@ def __init__(self, config): desc=self.cmd_EDDY_CALIBRATE_help) def is_calibrated(self): return len(self.cal_freqs) > 2 + def calc_frequency_rate(self): + count = len(self.cal_freqs) + if count < 2: + return + data = [] + for i in range(count-1): + f = self.cal_freqs[i] + z = self.cal_zpos[i] + f_next = self.cal_freqs[i+1] + z_next = self.cal_zpos[i+1] + chg_freq_per_dist = -((f_next - f) / (z_next - z)) + data.append((f, chg_freq_per_dist)) + raw_freq_rate = simple_linear_regression(data) + freq_max_tap = -raw_freq_rate[1] / raw_freq_rate[0] + z_max_tap = self.freq_to_height(freq_max_tap) + # Pad rates to reduce impact of noise + adj_slope = raw_freq_rate[0] * TAP_NOISE_COMPENSATION + z_adj_tap = z_max_tap * TAP_NOISE_COMPENSATION + freq_adj_tap = self.height_to_freq(z_adj_tap) + self.tap_factor = adj_slope + self.tap_threshold = freq_adj_tap + # XXX + logging.info("eddy tap threshold thr=%.3f,%.3f tf=%s", + self.tap_threshold, + self.freq_to_height(self.tap_threshold), + self.tap_factor) def load_calibration(self, cal): cal = sorted([(c[1], c[0]) for c in cal]) self.cal_freqs = [c[0] for c in cal] @@ -51,6 +92,10 @@ def apply_calibration(self, samples): offset = prev_zpos - prev_freq * gain zpos = freq * gain + offset samples[i] = (samp_time, freq, round(zpos, 6)) + def freq_to_height(self, freq): + dummy_sample = [(0., freq, 0.)] + self.apply_calibration(dummy_sample) + return dummy_sample[0][2] def height_to_freq(self, height): # XXX - could optimize lookup rev_zpos = list(reversed(self.cal_zpos)) @@ -191,6 +236,8 @@ def __init__(self, config, sensor_helper, calibration): self._sensor_helper = sensor_helper self._mcu = sensor_helper.get_mcu() self._calibration = calibration + self._tap_height = config.getfloat('tap_height', None, minval=0.) + self._use_tap = self._tap_height and calibration.is_calibrated() self._z_offset = config.getfloat('z_offset', minval=0.) self._dispatch = mcu.TriggerDispatch(self._mcu) self._samples = [] @@ -198,11 +245,25 @@ def __init__(self, config, sensor_helper, calibration): self._trigger_time = 0. self._printer.register_event_handler('klippy:mcu_identify', self._handle_mcu_identify) + # XXX + self._printer.register_event_handler('toolhead:drip', self._handle_drip) + self._delayed_setup = 0. def _handle_mcu_identify(self): kin = self._printer.lookup_object('toolhead').get_kinematics() for stepper in kin.get_steppers(): if stepper.is_active_axis('z'): self.add_stepper(stepper) + def _handle_drip(self, print_time, move): + # XXX - mega hack - delayed sensor start to obtain toolhead accel_t + if not self._use_tap or not self._delayed_setup: + return + pretap_freq = self._calibration.height_to_freq(self._tap_height) + self._sensor_helper.setup_tap( + self._delayed_setup, print_time + move.accel_t, + pretap_freq, self._dispatch.get_oid(), + self._calibration.tap_threshold, + self._calibration.tap_factor * move.cruise_v) + self._delayed_setup = 0. # Measurement gathering def _start_measurements(self, is_home=False): self._need_stop = False @@ -235,6 +296,10 @@ def home_start(self, print_time, sample_time, sample_count, rest_time, self._start_measurements(is_home=True) trigger_freq = self._calibration.height_to_freq(self._z_offset) trigger_completion = self._dispatch.start(print_time) + if self._use_tap: + # XXX + self._delayed_setup = print_time + return trigger_completion self._sensor_helper.setup_home( print_time, trigger_freq, self._dispatch.get_oid()) return trigger_completion @@ -258,7 +323,7 @@ def probing_move(self, pos, speed): # Perform probing move phoming = self._printer.lookup_object('homing') trig_pos = phoming.probing_move(self, pos, speed) - if not self._trigger_time: + if not self._trigger_time or self._use_tap: return trig_pos # Wait for 200ms to elapse since trigger time reactor = self._printer.get_reactor() diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 4149d53b1..d3fd1a914 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -338,6 +338,10 @@ def _process_moves(self, moves): self.special_queuing_state = "" self.need_check_pause = -1. self._calc_print_time() + if self.special_queuing_state == "Drip" and moves: + # XXX - mega hack + self.printer.send_event("toolhead:drip", self.print_time, + moves[0]) # Queue moves into trapezoid motion queue (trapq) next_move_time = self.print_time for move in moves: