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:
Kevin O'Connor committed 2026-05-01 23:39:15 -04:00
1 parent 460ee78340
commit ef7b13b1f2
5 files changed
+241 -83

No files matched your search

+77 -70
View File
@@ -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
+6
View File
@@ -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
+151 -12
View File
@@ -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)
+2 -1
View File
@@ -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
+5
View File
@@ -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