mirror of
https://github.com/Klipper3d/klipper.git
synced 2026-10-19 01:59:55 -05:00
probe_eddy_current: Add new PROBE_EDDY_CURRENT_TAP_CALIBRATE command
Add a command to help configure the tap_threshold parameter. Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
This commit is contained in:
1 parent
460ee78340
commit
ef7b13b1f2
5 files changed
+241
-83
No files matched your search
+77
-70
@@ -98,86 +98,93 @@ gcode:
|
||||
|
||||
## Tap calibration
|
||||
|
||||
The Eddy probe measures the resonance frequency of the coil.
|
||||
By the absolute value of the frequency and the calibration curve from
|
||||
`PROBE_EDDY_CURRENT_CALIBRATE`, it is therefore possible to detect
|
||||
where the bed is without physical contact.
|
||||
Eddy current probes support a special kind of probing referred to as
|
||||
"tap" probing. This mechanism directs the toolhead to descend until
|
||||
the nozzle makes contact with the bed. That bed contact may cause a
|
||||
change in sensor measurements which can be detected and used to halt
|
||||
further downward movement. The nozzle is then lifted away from the bed
|
||||
and the sensor measurements during the lifting movement are analyzed
|
||||
to determine the location where the nozzle breaks contact with the
|
||||
bed.
|
||||
|
||||
By use of the same knowledge, we know that frequency changes with
|
||||
the distance. It is possible to track that change in real time and
|
||||
detect the time/position where contact happens - a change of frequency
|
||||
starts to change in a different way.
|
||||
For example, stopped to change because of the collision.
|
||||
In order to utilize "tap" probing it is necessary to configure a
|
||||
`tap_threshold` parameter. This parameter determines when downward
|
||||
toolhead movement during a "tap" probe should be halted. A value too
|
||||
large could result in a nozzle/bed contact not detected, which could
|
||||
result in the nozzle crashing uncontrollably into the bed. A value too
|
||||
small could result in a "tap" probe attempt halting before making
|
||||
contact with the bed, which could result in wildly inaccurate probe
|
||||
results.
|
||||
|
||||
Because eddy output is not perfect: there is sensor noise,
|
||||
mechanical oscillation, thermal expansion and other discrepancies,
|
||||
it is required to calibrate the stop threshold for your machine.
|
||||
Practically, it ensures that the Eddy's output data absolute value
|
||||
change per second (velocity) is high enough - higher than the noise level,
|
||||
and that upon collision it always decreases by at least this value.
|
||||
The `PROBE_EDDY_CURRENT_TAP_CALIBRATE` command can be used to
|
||||
configure an appropriate `tap_threshold` value. This tool may be run
|
||||
after completing the main `PROBE_EDDY_CURRENT_CALIBRATE` calibration.
|
||||
Follow these steps to calibrate `tap_threshold`:
|
||||
|
||||
The suggested calibration routine works as follows:
|
||||
1. Home Z
|
||||
2. Place the toolhead at the center of the bed.
|
||||
3. Move the Z axis far away (30 mm, for example).
|
||||
4. Run `PROBE METHOD=tap`
|
||||
5. If it stops before colliding, increase the `tap_threshold`.
|
||||
1. Verify that both the nozzle and bed are clean. Enable the printer,
|
||||
home the printer, move the toolhead to a position near the center
|
||||
of the bed, and make sure the nozzle is between 3 - 10 millimeters
|
||||
from the bed.
|
||||
|
||||
Repeat until the nozzle softly touches the bed.
|
||||
This is easier to do with a clean nozzle and
|
||||
by visually inspecting the process.
|
||||
2. The next step involves commanding the nozzle to make contact with
|
||||
the bed. This process always has some risks, so be prepared to
|
||||
issue an emergency halt (`M112`) if the probing descent does not
|
||||
stop after contacting the bed. When ready issue the following
|
||||
command:
|
||||
`PROBE_EDDY_CURRENT_TAP_CALIBRATE TAP=guess`
|
||||
This command analyzes the data found during the main probe
|
||||
calibration to make an initial coarse guess for the tap_threshold
|
||||
value and it then performs the corresponding "tap" probe. Ideally
|
||||
the above command will cause the probe to descend until it hits the
|
||||
bed, lift away from the bed, and then report a valid probe result.
|
||||
If not, see the paragraphs at the end of this section to
|
||||
troubleshoot. If the attempt was successful then continue to the
|
||||
next step.
|
||||
|
||||
You can streamline the process by placing the toolhead in the center once.
|
||||
Then, upon config restart, trick the machine into thinking that Z is homed.
|
||||
```
|
||||
SET_KINEMATIC_POSITION X=<middle> Y=<middle> Z=0
|
||||
G0 Z5 # Optional retract
|
||||
PROBE METHOD=tap
|
||||
```
|
||||
3. The next step is to run another tap probe with a "refined"
|
||||
threshold setting. The tool utilizes information gathered during a
|
||||
previous successful tap probe to determine this improved threshold.
|
||||
Make sure that the nozzle is near the center of the bed, that it is
|
||||
between 3 - 10mm above the bed, be ready to issue an emergency
|
||||
halt, and then run the following command:
|
||||
`PROBE_EDDY_CURRENT_TAP_CALIBRATE TAP=refine`
|
||||
Ideally this command will also succeed; if not, see the paragraphs
|
||||
at the end of this section to troubleshoot. If the attempt was
|
||||
successful then continue to the next step.
|
||||
|
||||
Here is an example sequence of threshold values to test:
|
||||
```
|
||||
50 -> 250 -> 500 -> 1000 -> 2000 -> 4000 -> 8000
|
||||
8000 -> 6000 -> 5000
|
||||
```
|
||||
Your value will normally be between those.
|
||||
- Too high a value leaves a less safe margin for early collision -
|
||||
if something is between the nozzle and the bed, or if the nozzle
|
||||
is too close to the bed before the tap.
|
||||
- Too low - can make the toolhead stop in mid-air
|
||||
because of the noise.
|
||||
4. If probing with the refined threshold is successful then the next
|
||||
test is to verify that it is stable over multiple probe attempts.
|
||||
Make sure that the nozzle is near the center of the bed, that it is
|
||||
between 3 - 10mm above the bed, be ready to issue an emergency
|
||||
halt, and then run the following command:
|
||||
`PROBE_EDDY_CURRENT_TAP_CALIBRATE TAP=verify`
|
||||
This command will probe the bed five times in a row. Ideally the
|
||||
above command will also succeed; if not, see the paragraphs at the
|
||||
end of this section to troubleshoot. If the attempt was successful
|
||||
then continue to the next step.
|
||||
|
||||
You can estimate the initial threshold value by analyzing your own
|
||||
calibration routine output:
|
||||
```
|
||||
probe_eddy_current: noise 0.000642mm, MAD_Hz=11.314
|
||||
...
|
||||
z: 1.010 # noise 0.000400mm, MAD_Hz=14.000
|
||||
```
|
||||
The estimation will be:
|
||||
```
|
||||
MAD_Hz * 100
|
||||
11.314 * 100 = 113.14
|
||||
```
|
||||
5. If all of the above steps are successful then one can issue a
|
||||
`SAVE_CONFIG` command to save the "tap_threshold" parameter to the
|
||||
printer.cfg file. Calibration should now be complete.
|
||||
|
||||
To further fine tune threshold, one can use `PROBE_ACCURACY METHOD=tap`.
|
||||
The range is expected to be about 0.02 mm,
|
||||
with the default probe speed of 5 mm/s.
|
||||
Elevated coil temperature may increase noise and may require additional tuning.
|
||||
If any of the steps above did not succeed then it may be necessary to
|
||||
troubleshoot and manually determine an appropriate `tap_threshold`.
|
||||
This is done by running commands of the form:
|
||||
`PROBE METHOD=tap TAP_THRESHOLD=<value>`
|
||||
Where `<value>` is a threshold to test.
|
||||
|
||||
You can validate the tap precision by measuring the paper thickness
|
||||
from the initial calibration guide. It is expected to be ~0.1mm.
|
||||
In general, if a probe attempt halts before making contact with the
|
||||
bed, then this indicates that the provided `TAP_THRESHOLD` parameter
|
||||
is too low. Try increasing it by about 10% and retry. Similarly, if a
|
||||
probe attempt does not halt after making contact with the bed then it
|
||||
indicates that `TAP_THRESHOLD` is too high. Consider decreasing the
|
||||
attempted value in half.
|
||||
|
||||
Tap precision is limited by the sampling frequency and
|
||||
the speed of the descent.
|
||||
If you take 24 photos per second of the moving train, you can only estimate
|
||||
where the train was between photos.
|
||||
|
||||
It is possible to reduce the descending speed.
|
||||
|
||||
It is possible to tap over non-conductive surfaces as long as there is metal
|
||||
behind it within the sensor's sensitivity range.
|
||||
Max distance can be approximated to be about 1.5x of the coil's narrowest part.
|
||||
If the automated calibration tool failed during the initial "guess"
|
||||
stage, then one can use the tap_threshold value reported by the tool
|
||||
as a starting point for manual attempts. Once a successful probe
|
||||
attempt is completed then one can return to the main steps described
|
||||
above starting at the "refine" stage.
|
||||
|
||||
## Thermal Drift Calibration
|
||||
|
||||
|
||||
@@ -1269,6 +1269,12 @@ heights. The tool will take a couple of minutes to complete. After
|
||||
completion, use the SAVE_CONFIG command to store the results in the
|
||||
printer.cfg file.
|
||||
|
||||
#### PROBE_EDDY_CURRENT_TAP_CALIBRATE
|
||||
`PROBE_EDDY_CURRENT_TAP_CALIBRATE [TAP=guess|refine|verify]`: This
|
||||
starts a tool that can calibrate the probe's "tap_threshold"
|
||||
parameter. See the
|
||||
[eddy probe documentation](Eddy_Probe.md#tap-calibration) for details.
|
||||
|
||||
#### LDC_CALIBRATE_DRIVE_CURRENT
|
||||
`LDC_CALIBRATE_DRIVE_CURRENT CHIP=<config_name>` This tool will
|
||||
calibrate the ldc1612 DRIVE_CURRENT0 register. Prior to using this
|
||||
|
||||
@@ -7,6 +7,11 @@ import sys, logging, math, bisect
|
||||
import mcu, mathutil
|
||||
from . import ldc1612, trigger_analog, probe, manual_probe
|
||||
|
||||
|
||||
######################################################################
|
||||
# Calibration
|
||||
######################################################################
|
||||
|
||||
OUT_OF_RANGE = 99.9
|
||||
|
||||
# Tool for calibrating the sensor Z detection and applying that calibration
|
||||
@@ -63,6 +68,8 @@ class EddyCalibration:
|
||||
offset = prev_zpos - prev_freq * gain
|
||||
zpos = adj_freq * gain + offset
|
||||
samples[i] = (samp_time, freq, round(zpos, 6))
|
||||
def get_calibration(self):
|
||||
return list(self.cal_freqs), list(self.cal_zpos)
|
||||
def freq_to_height(self, freq):
|
||||
dummy_sample = [(0., freq, 0.)]
|
||||
self.apply_calibration(dummy_sample)
|
||||
@@ -296,6 +303,133 @@ class EddyCalibration:
|
||||
def register_drift_compensation(self, comp):
|
||||
self.drift_comp = comp
|
||||
|
||||
# Tool for calibrating tap_threshold
|
||||
class EddyTapCalibration:
|
||||
def __init__(self, config, calibration, eddy_tap):
|
||||
self._printer = config.get_printer()
|
||||
self._name = config.get_name()
|
||||
self._calibration = calibration
|
||||
self._eddy_tap = eddy_tap
|
||||
self._refine_tap_threshold = None
|
||||
gcode = self._printer.lookup_object("gcode")
|
||||
gcode.register_command("PROBE_EDDY_CURRENT_TAP_CALIBRATE",
|
||||
self.cmd_TAP_CALIBRATE,
|
||||
desc=self.cmd_TAP_CALIBRATE_help)
|
||||
def _analyze_main_calibration(self):
|
||||
freqs, zpos = self._calibration.get_calibration()
|
||||
if len(freqs) < 2:
|
||||
return None
|
||||
# Find best fit for: freq = c0 + c1*z + c2*z*z
|
||||
eqs = []
|
||||
ans = []
|
||||
for freq, z in zip(freqs, zpos):
|
||||
if z <= 0.750:
|
||||
ans.append([freq])
|
||||
eqs.append([1., z, z*z])
|
||||
eqst = mathutil.mat_transp(eqs)
|
||||
eqst_eqs = mathutil.mat_mat_mul(eqst, eqs)
|
||||
eqst_ans = mathutil.mat_mat_mul(eqst, ans)
|
||||
return mathutil.gaussian_solve(eqst_eqs, eqst_ans)
|
||||
def _describe_main_calibration(self, coeffs):
|
||||
if coeffs is None:
|
||||
return ["Main calibration data not available.", ""]
|
||||
msg = ("Calibration: f=%.3f s=%.3f q=%.3f"
|
||||
% (coeffs[0][0], coeffs[1][0], coeffs[2][0]))
|
||||
return [msg, ""]
|
||||
def _describe_last_tap(self, last_tap):
|
||||
if last_tap is None:
|
||||
return ["Run tap probe for last tap analysis."]
|
||||
status, depress_dist, coeffs = last_tap
|
||||
z_contact, freq_contact, depress_slope, slope, slope2 = coeffs
|
||||
contact_slope_delta = depress_slope - slope
|
||||
m1 = ("Last tap: z=%.6f f=%.3f s=%.3f q=%.3f"
|
||||
% (z_contact, freq_contact, slope, slope2))
|
||||
m2 = (" depress_dist=%.6f depress_slope=%.3f"
|
||||
% (depress_dist, depress_slope))
|
||||
m3 = (" contact_slope_delta=%.3f" % (contact_slope_delta,))
|
||||
msgs = [m1, m2, m3]
|
||||
if status != "success":
|
||||
msgs.extend(["", "Warning! Last tap did not succeed."])
|
||||
return msgs
|
||||
def _try_tap(self, gcmd, tap_threshold, samples=1):
|
||||
# Create dummy gcmd with SAMPLES=1
|
||||
fo_params = dict(gcmd.get_command_parameters())
|
||||
fo_params['METHOD'] = "tap"
|
||||
fo_params['TAP_THRESHOLD'] = "%.3f" % (tap_threshold,)
|
||||
fo_params['SAMPLES'] = str(samples)
|
||||
gcode = self._printer.lookup_object('gcode')
|
||||
fo_gcmd = gcode.create_gcode_command("", "", fo_params)
|
||||
gcmd.respond_info("Tap probing with TAP_THRESHOLD=%s SAMPLES=%s"
|
||||
% (fo_params['TAP_THRESHOLD'], fo_params['SAMPLES']))
|
||||
# Run "tap" probe
|
||||
probe = self._printer.lookup_object('probe')
|
||||
probe_session = probe.start_probe_session(fo_gcmd)
|
||||
probe_session.run_probe(fo_gcmd)
|
||||
positions = probe_session.pull_probed_results()
|
||||
probe_session.end_probe_session()
|
||||
gcmd.respond_info("Tap probing reports z=%.6f" % (positions[0][2],))
|
||||
def _save_tap_threshold(self, gcmd, tap_threshold):
|
||||
configfile = self._printer.lookup_object('configfile')
|
||||
gcmd.respond_info(
|
||||
"%s: tap_threshold: %.3f\n"
|
||||
"The SAVE_CONFIG command will update the printer config file\n"
|
||||
"with the above and restart the printer."
|
||||
% (self._name, tap_threshold))
|
||||
configfile.set(self._name, 'tap_threshold', "%.3f" % (tap_threshold,))
|
||||
cmd_TAP_CALIBRATE_help = "Calibrate tap_threshold for 'tap' probing"
|
||||
def cmd_TAP_CALIBRATE(self, gcmd):
|
||||
mc_coeffs = self._analyze_main_calibration()
|
||||
last_tap = self._eddy_tap.get_last_tap_info()
|
||||
tap_test = gcmd.get("TAP", None)
|
||||
if tap_test is None:
|
||||
# Provide technical information
|
||||
mc_msgs = self._describe_main_calibration(mc_coeffs)
|
||||
lt_msgs = self._describe_last_tap(last_tap)
|
||||
gcmd.respond_info('\n'.join(mc_msgs + lt_msgs))
|
||||
elif tap_test == 'guess':
|
||||
# Attempt tap based on main calibration
|
||||
self._refine_tap_threshold = None
|
||||
if mc_coeffs is None:
|
||||
raise gcmd.error(
|
||||
"Must complete PROBE_EDDY_CURRENT_CALIBRATE first")
|
||||
self._try_tap(gcmd, mc_coeffs[1][0] * -0.10)
|
||||
elif tap_test == 'refine':
|
||||
# Attempt tap based on change in slope observed during last tap
|
||||
self._refine_tap_threshold = None
|
||||
if last_tap is None or last_tap[0] != "success":
|
||||
raise gcmd.error("Must complete valid 'tap' probe first")
|
||||
status, depress_dist, coeffs = last_tap
|
||||
z_contact, freq_contact, depress_slope, slope, slope2 = coeffs
|
||||
contact_slope_delta = depress_slope - slope
|
||||
try_tap_threshold = contact_slope_delta * 0.20
|
||||
self._try_tap(gcmd, try_tap_threshold)
|
||||
self._refine_tap_threshold = try_tap_threshold
|
||||
elif tap_test == 'verify':
|
||||
# Retry tap several times to verify it is stable
|
||||
if self._refine_tap_threshold is None:
|
||||
raise gcmd.error("Must complete valid 'refine' step first")
|
||||
self._try_tap(gcmd, self._refine_tap_threshold, 5)
|
||||
self._save_tap_threshold(gcmd, self._refine_tap_threshold)
|
||||
else:
|
||||
raise gcmd.error("Please provide a valid TAP parameter")
|
||||
|
||||
class DummyDriftCompensation:
|
||||
def get_temperature(self):
|
||||
return 0.
|
||||
def note_z_calibration_start(self):
|
||||
pass
|
||||
def note_z_calibration_finish(self):
|
||||
pass
|
||||
def adjust_freq(self, freq, temp=None):
|
||||
return freq
|
||||
def unadjust_freq(self, freq, temp=None):
|
||||
return freq
|
||||
|
||||
|
||||
######################################################################
|
||||
# Measurement collection
|
||||
######################################################################
|
||||
|
||||
# Tool to gather samples and convert them to probe positions
|
||||
class EddyGatherSamples:
|
||||
def __init__(self, printer, sensor_helper):
|
||||
@@ -403,6 +537,11 @@ def probe_results_from_avg(measures, toolhead_pos, calibration, offsets):
|
||||
return manual_probe.create_probe_result(toolhead_pos,
|
||||
(offsets[0], offsets[1], sensor_z))
|
||||
|
||||
|
||||
######################################################################
|
||||
# Probe sessions
|
||||
######################################################################
|
||||
|
||||
MAX_VALID_RAW_VALUE=0x03ffffff
|
||||
|
||||
# Helper for implementing PROBE style commands (descend until trigger)
|
||||
@@ -509,6 +648,7 @@ class EddyTap:
|
||||
self._least_squares_cache = {}
|
||||
self._current_tap_threshold = 0.
|
||||
self._setup_tap()
|
||||
self._last_tap = None
|
||||
# Setup for "tap" probe request
|
||||
def _setup_tap(self):
|
||||
# Create sos filter "design"
|
||||
@@ -707,6 +847,7 @@ class EddyTap:
|
||||
def _error_detect(self, msg):
|
||||
raise self._printer.command_error("Unable to detect tap: %s" % (msg,))
|
||||
def _analyze_pullback(self, measures, start_time, end_time):
|
||||
self._last_tap = None
|
||||
reactor = self._printer.get_reactor()
|
||||
self._validate_samples_time(measures, start_time, end_time)
|
||||
# Correlate measurements to toolhead position at time of measurement
|
||||
@@ -721,6 +862,7 @@ class EddyTap:
|
||||
# Find best fit for extracted measurements
|
||||
coeffs = self._find_least_squares(data)
|
||||
z_contact, freq_contact, depress_slope, slope, slope2 = coeffs
|
||||
self._last_tap = ("fail", z_contact - min_z, coeffs)
|
||||
reactor.pause(0.)
|
||||
sps = self._sensor_helper.get_samples_per_second()
|
||||
contact_slope_delta = depress_slope - slope
|
||||
@@ -734,6 +876,7 @@ class EddyTap:
|
||||
if z_contact - min_z < 0.030 or z_contact - min_z > 0.250:
|
||||
self._error_detect("invalid depress distance (%.6f vs %.6f:%.6f)"
|
||||
% (z_contact - min_z, 0.030, 0.250))
|
||||
self._last_tap = ("success", z_contact - min_z, coeffs)
|
||||
# Report probe position
|
||||
trig_idx = len(data)-1
|
||||
while trig_idx > 0 and data[trig_idx-1][1][2] > z_contact:
|
||||
@@ -742,6 +885,8 @@ class EddyTap:
|
||||
adj_z_contact = z_contact - self._tap_z_offset
|
||||
return manual_probe.ProbeResult(trig_pos[0], trig_pos[1], adj_z_contact,
|
||||
trig_pos[0], trig_pos[1], trig_pos[2])
|
||||
def get_last_tap_info(self):
|
||||
return self._last_tap
|
||||
# Probe session interface
|
||||
def start_probe_session(self, gcmd):
|
||||
self._prep_trigger_analog_tap(gcmd)
|
||||
@@ -837,6 +982,11 @@ class EddyScanningProbe:
|
||||
self._gather.finish()
|
||||
self._gather = None
|
||||
|
||||
|
||||
######################################################################
|
||||
# Main probe interface
|
||||
######################################################################
|
||||
|
||||
# Eddy specific ProbeOffsets class (does not store z_offset)
|
||||
class EddyProbeOffsets:
|
||||
def __init__(self, config):
|
||||
@@ -895,6 +1045,7 @@ class PrinterEddyProbe:
|
||||
# Probing via "tap" interface
|
||||
self.eddy_tap = EddyTap(config, self.sensor_helper,
|
||||
self.param_helper, trig_analog)
|
||||
EddyTapCalibration(config, self.calibration, self.eddy_tap)
|
||||
# Probing via "scan" and "rapid_scan" requests
|
||||
self.eddy_scan = EddyScanningProbe(config, self.sensor_helper,
|
||||
self.calibration, self.probe_offsets)
|
||||
@@ -928,17 +1079,5 @@ class PrinterEddyProbe:
|
||||
def register_drift_compensation(self, comp):
|
||||
self.calibration.register_drift_compensation(comp)
|
||||
|
||||
class DummyDriftCompensation:
|
||||
def get_temperature(self):
|
||||
return 0.
|
||||
def note_z_calibration_start(self):
|
||||
pass
|
||||
def note_z_calibration_finish(self):
|
||||
pass
|
||||
def adjust_freq(self, freq, temp=None):
|
||||
return freq
|
||||
def unadjust_freq(self, freq, temp=None):
|
||||
return freq
|
||||
|
||||
def load_config_prefix(config):
|
||||
return PrinterEddyProbe(config)
|
||||
@@ -82,4 +82,5 @@ max_z_accel: 100
|
||||
# Dummy calibration data
|
||||
[probe_eddy_current eddy]
|
||||
calibrate =
|
||||
0.050000:3300000.000,1.000000:3200000.000,5.000000:3000000.000
|
||||
0.050000:3300000.000,0.100000:3200000.000,0.150000:3000000.000,
|
||||
0.200000:2900000.000,1.000000:2800000.000,5.000000:2700000.000
|
||||
@@ -23,9 +23,14 @@ BED_MESH_CALIBRATE METHOD=rapid_scan
|
||||
# Move again
|
||||
G1 Z5 X0 Y0
|
||||
|
||||
# Test PROBE_EDDY_CURRENT_TAP_CALIBRATE
|
||||
PROBE_EDDY_CURRENT_TAP_CALIBRATE
|
||||
|
||||
# Do "tap" probe
|
||||
PROBE METHOD=tap
|
||||
|
||||
PROBE_EDDY_CURRENT_TAP_CALIBRATE
|
||||
|
||||
# Do regular probe
|
||||
G1 Z5
|
||||
PROBE
|
||||
|
||||
Reference in new issue
Block a user