Skip to content

Commit

Permalink
probe_eddy_current: wip - tap detection
Browse files Browse the repository at this point in the history
Signed-off-by: Kevin O'Connor <[email protected]>
  • Loading branch information
KevinOConnor committed May 3, 2024
1 parent a0e98c5 commit db1d10f
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 1 deletion.
8 changes: 8 additions & 0 deletions docs/Config_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
46 changes: 46 additions & 0 deletions docs/Eddy_Probe.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
20 changes: 20 additions & 0 deletions klippy/extras/ldc1612.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
67 changes: 66 additions & 1 deletion klippy/extras/probe_eddy_current.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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]
Expand All @@ -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))
Expand Down Expand Up @@ -191,18 +236,34 @@ 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 = []
self._is_sampling = self._start_from_home = self._need_stop = False
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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions klippy/toolhead.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit db1d10f

Please sign in to comment.