diff options
Diffstat (limited to 'klippy/extras/probe_eddy_current.py')
-rw-r--r-- | klippy/extras/probe_eddy_current.py | 240 |
1 files changed, 160 insertions, 80 deletions
diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 96c76670..529b097c 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -9,6 +9,7 @@ from . import ldc1612, probe, manual_probe OUT_OF_RANGE = 99.9 + # Tool for calibrating the sensor Z detection and applying that calibration class EddyCalibration: def __init__(self, config): @@ -18,25 +19,31 @@ class EddyCalibration: # Current calibration data self.cal_freqs = [] self.cal_zpos = [] - cal = config.get('calibrate', None) + cal = config.get("calibrate", None) if cal is not None: - cal = [list(map(float, d.strip().split(':', 1))) - for d in cal.split(',')] + cal = [list(map(float, d.strip().split(":", 1))) for d in cal.split(",")] self.load_calibration(cal) # Probe calibrate state - self.probe_speed = 0. + self.probe_speed = 0.0 # Register commands cname = self.name.split()[-1] - gcode = self.printer.lookup_object('gcode') - gcode.register_mux_command("PROBE_EDDY_CURRENT_CALIBRATE", "CHIP", - cname, self.cmd_EDDY_CALIBRATE, - desc=self.cmd_EDDY_CALIBRATE_help) + gcode = self.printer.lookup_object("gcode") + gcode.register_mux_command( + "PROBE_EDDY_CURRENT_CALIBRATE", + "CHIP", + cname, + self.cmd_EDDY_CALIBRATE, + desc=self.cmd_EDDY_CALIBRATE_help, + ) + def is_calibrated(self): return len(self.cal_freqs) > 2 + def load_calibration(self, cal): cal = sorted([(c[1], c[0]) for c in cal]) self.cal_freqs = [c[0] for c in cal] self.cal_zpos = [c[1] for c in cal] + def apply_calibration(self, samples): cur_temp = self.drift_comp.get_temperature() for i, (samp_time, freq, dummy_z) in enumerate(samples): @@ -56,18 +63,19 @@ class EddyCalibration: offset = prev_zpos - prev_freq * gain zpos = adj_freq * gain + offset samples[i] = (samp_time, freq, round(zpos, 6)) + def freq_to_height(self, freq): - dummy_sample = [(0., freq, 0.)] + dummy_sample = [(0.0, freq, 0.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)) rev_freqs = list(reversed(self.cal_freqs)) pos = bisect.bisect(rev_zpos, height) if pos == 0 or pos >= len(rev_zpos): - raise self.printer.command_error( - "Invalid probe_eddy_current height") + raise self.printer.command_error("Invalid probe_eddy_current height") this_freq = rev_freqs[pos] prev_freq = rev_freqs[pos - 1] this_zpos = rev_zpos[pos] @@ -76,25 +84,28 @@ class EddyCalibration: offset = prev_freq - prev_zpos * gain freq = height * gain + offset return self.drift_comp.unadjust_freq(freq) + def do_calibration_moves(self, move_speed): - toolhead = self.printer.lookup_object('toolhead') + toolhead = self.printer.lookup_object("toolhead") kin = toolhead.get_kinematics() move = toolhead.manual_move # Start data collection msgs = [] is_finished = False + def handle_batch(msg): if is_finished: return False msgs.append(msg) return True + self.printer.lookup_object(self.name).add_client(handle_batch) - toolhead.dwell(1.) + toolhead.dwell(1.0) self.drift_comp.note_z_calibration_start() # Move to each 40um position max_z = 4.0 samp_dist = 0.040 - req_zpos = [i*samp_dist for i in range(int(max_z / samp_dist) + 1)] + req_zpos = [i * samp_dist for i in range(int(max_z / samp_dist) + 1)] start_pos = toolhead.get_position() times = [] for zpos in req_zpos: @@ -111,8 +122,9 @@ class EddyCalibration: toolhead.dwell(0.200) # Find Z position based on actual commanded stepper position toolhead.flush_step_generation() - kin_spos = {s.get_name(): s.get_commanded_position() - for s in kin.get_steppers()} + kin_spos = { + s.get_name(): s.get_commanded_position() for s in kin.get_steppers() + } kin_pos = kin.calc_position(kin_spos) times.append((start_query_time, end_query_time, kin_pos[2])) toolhead.dwell(1.0) @@ -124,7 +136,7 @@ class EddyCalibration: cal = {} step = 0 for msg in msgs: - for query_time, freq, old_z in msg['data']: + for query_time, freq, old_z in msg["data"]: # Add to step tracking while step < len(times) and query_time > times[step][1]: step += 1 @@ -132,8 +144,10 @@ class EddyCalibration: cal.setdefault(times[step][2], []).append(freq) if len(cal) != len(times): raise self.printer.command_error( - "Failed calibration - incomplete sensor data") + "Failed calibration - incomplete sensor data" + ) return cal + def calc_freqs(self, meas): total_count = total_variance = 0 positions = {} @@ -142,17 +156,18 @@ class EddyCalibration: freq_avg = float(sum(freqs)) / count positions[pos] = freq_avg total_count += count - total_variance += sum([(f - freq_avg)**2 for f in freqs]) + total_variance += sum([(f - freq_avg) ** 2 for f in freqs]) return positions, math.sqrt(total_variance / total_count), total_count + def post_manual_probe(self, kin_pos): if kin_pos is None: # Manual Probe was aborted return curpos = list(kin_pos) - move = self.printer.lookup_object('toolhead').manual_move + move = self.printer.lookup_object("toolhead").manual_move # Move away from the bed probe_calibrate_z = curpos[2] - curpos[2] += 5. + curpos[2] += 5.0 move(curpos, self.probe_speed) # Move sensor over nozzle position pprobe = self.printer.lookup_object("probe") @@ -161,42 +176,47 @@ class EddyCalibration: curpos[1] -= y_offset move(curpos, self.probe_speed) # Descend back to bed - curpos[2] -= 5. - 0.050 + curpos[2] -= 5.0 - 0.050 move(curpos, self.probe_speed) # Perform calibration movement and capture cal = self.do_calibration_moves(self.probe_speed) # Calculate each sample position average and variance positions, std, total = self.calc_freqs(cal) - last_freq = 0. + last_freq = 0.0 for pos, freq in reversed(sorted(positions.items())): if freq <= last_freq: raise self.printer.command_error( - "Failed calibration - frequency not increasing each step") + "Failed calibration - frequency not increasing each step" + ) last_freq = freq gcode = self.printer.lookup_object("gcode") gcode.respond_info( "probe_eddy_current: stddev=%.3f in %d queries\n" "The SAVE_CONFIG command will update the printer config file\n" - "and restart the printer." % (std, total)) + "and restart the printer." % (std, total) + ) # Save results cal_contents = [] for i, (pos, freq) in enumerate(sorted(positions.items())): if not i % 3: - cal_contents.append('\n') + cal_contents.append("\n") cal_contents.append("%.6f:%.3f" % (pos - probe_calibrate_z, freq)) - cal_contents.append(',') + cal_contents.append(",") cal_contents.pop() - configfile = self.printer.lookup_object('configfile') - configfile.set(self.name, 'calibrate', ''.join(cal_contents)) + configfile = self.printer.lookup_object("configfile") + configfile.set(self.name, "calibrate", "".join(cal_contents)) + cmd_EDDY_CALIBRATE_help = "Calibrate eddy current probe" + def cmd_EDDY_CALIBRATE(self, gcmd): - self.probe_speed = gcmd.get_float("PROBE_SPEED", 5., above=0.) + self.probe_speed = gcmd.get_float("PROBE_SPEED", 5.0, above=0.0) # Start manual probe - manual_probe.ManualProbeHelper(self.printer, gcmd, - self.post_manual_probe) + manual_probe.ManualProbeHelper(self.printer, gcmd, self.post_manual_probe) + def register_drift_compensation(self, comp): self.drift_comp = comp + # Tool to gather samples and convert them to probe positions class EddyGatherSamples: def __init__(self, printer, sensor_helper, calibration, z_offset): @@ -211,9 +231,9 @@ class EddyGatherSamples: self._need_stop = False # Start samples if not self._calibration.is_calibrated(): - raise self._printer.command_error( - "Must calibrate probe_eddy_current first") + raise self._printer.command_error("Must calibrate probe_eddy_current first") sensor_helper.add_client(self._add_measurement) + def _add_measurement(self, msg): if self._need_stop: del self._samples[:] @@ -221,8 +241,10 @@ class EddyGatherSamples: self._samples.append(msg) self._check_samples() return True + def finish(self): self._need_stop = True + def _await_samples(self): # Make sure enough samples have been collected reactor = self._printer.get_reactor() @@ -232,18 +254,18 @@ class EddyGatherSamples: systime = reactor.monotonic() est_print_time = mcu.estimated_print_time(systime) if est_print_time > end_time + 1.0: - raise self._printer.command_error( - "probe_eddy_current sensor outage") + raise self._printer.command_error("probe_eddy_current sensor outage") reactor.pause(systime + 0.010) + def _pull_freq(self, start_time, end_time): # Find average sensor frequency between time range msg_num = discard_msgs = 0 - samp_sum = 0. + samp_sum = 0.0 samp_count = 0 while msg_num < len(self._samples): msg = self._samples[msg_num] msg_num += 1 - data = msg['data'] + data = msg["data"] if data[0][0] > end_time: break if data[-1][0] < start_time: @@ -256,19 +278,22 @@ class EddyGatherSamples: del self._samples[:discard_msgs] if not samp_count: # No sensor readings - raise error in pull_probed() - return 0. + return 0.0 return samp_sum / samp_count + def _lookup_toolhead_pos(self, pos_time): - toolhead = self._printer.lookup_object('toolhead') + toolhead = self._printer.lookup_object("toolhead") kin = toolhead.get_kinematics() - kin_spos = {s.get_name(): s.mcu_to_commanded_position( - s.get_past_mcu_position(pos_time)) - for s in kin.get_steppers()} + kin_spos = { + s.get_name(): s.mcu_to_commanded_position(s.get_past_mcu_position(pos_time)) + for s in kin.get_steppers() + } return kin.calc_position(kin_spos) + def _check_samples(self): while self._samples and self._probe_times: start_time, end_time, pos_time, toolhead_pos = self._probe_times[0] - if self._samples[-1]['data'][-1][0] < end_time: + if self._samples[-1]["data"][-1][0] < end_time: break freq = self._pull_freq(start_time, end_time) if pos_time is not None: @@ -278,32 +303,39 @@ class EddyGatherSamples: sensor_z = self._calibration.freq_to_height(freq) self._probe_results.append((sensor_z, toolhead_pos)) self._probe_times.pop(0) + def pull_probed(self): self._await_samples() results = [] for sensor_z, toolhead_pos in self._probe_results: if sensor_z is None: raise self._printer.command_error( - "Unable to obtain probe_eddy_current sensor readings") + "Unable to obtain probe_eddy_current sensor readings" + ) if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: raise self._printer.command_error( - "probe_eddy_current sensor not in valid range") + "probe_eddy_current sensor not in valid range" + ) # Callers expect position relative to z_offset, so recalculate bed_deviation = toolhead_pos[2] - sensor_z toolhead_pos[2] = self._z_offset + bed_deviation results.append(toolhead_pos) del self._probe_results[:] return results + def note_probe(self, start_time, end_time, toolhead_pos): self._probe_times.append((start_time, end_time, None, toolhead_pos)) self._check_samples() + def note_probe_and_position(self, start_time, end_time, pos_time): self._probe_times.append((start_time, end_time, pos_time, None)) self._check_samples() + # Helper for implementing PROBE style commands (descend until trigger) class EddyDescend: REASON_SENSOR_ERROR = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 + def __init__(self, config, sensor_helper, calibration, param_helper): self._printer = config.get_printer() self._sensor_helper = sensor_helper @@ -311,50 +343,60 @@ class EddyDescend: self._calibration = calibration self._param_helper = param_helper self._z_min_position = probe.lookup_minimum_z(config) - self._z_offset = config.getfloat('z_offset', minval=0.) + self._z_offset = config.getfloat("z_offset", minval=0.0) self._dispatch = mcu.TriggerDispatch(self._mcu) - self._trigger_time = 0. + self._trigger_time = 0.0 self._gather = None probe.LookupZSteppers(config, self._dispatch.add_stepper) + # Interface for phoming.probing_move() def get_steppers(self): return self._dispatch.get_steppers() - def home_start(self, print_time, sample_time, sample_count, rest_time, - triggered=True): - self._trigger_time = 0. + + def home_start( + self, print_time, sample_time, sample_count, rest_time, triggered=True + ): + self._trigger_time = 0.0 trigger_freq = self._calibration.height_to_freq(self._z_offset) trigger_completion = self._dispatch.start(print_time) self._sensor_helper.setup_home( - print_time, trigger_freq, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.REASON_SENSOR_ERROR) + print_time, + trigger_freq, + self._dispatch.get_oid(), + mcu.MCU_trsync.REASON_ENDSTOP_HIT, + self.REASON_SENSOR_ERROR, + ) return trigger_completion + def home_wait(self, home_end_time): self._dispatch.wait_end(home_end_time) trigger_time = self._sensor_helper.clear_home() res = self._dispatch.stop() if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - raise self._printer.command_error( - "Communication timeout during homing") + raise self._printer.command_error("Communication timeout during homing") raise self._printer.command_error("Eddy current sensor error") if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: - return 0. + return 0.0 if self._mcu.is_fileoutput(): return home_end_time self._trigger_time = trigger_time return trigger_time + # Probe session interface def start_probe_session(self, gcmd): - self._gather = EddyGatherSamples(self._printer, self._sensor_helper, - self._calibration, self._z_offset) + self._gather = EddyGatherSamples( + self._printer, self._sensor_helper, self._calibration, self._z_offset + ) return self + def run_probe(self, gcmd): - toolhead = self._printer.lookup_object('toolhead') + toolhead = self._printer.lookup_object("toolhead") pos = toolhead.get_position() pos[2] = self._z_min_position - speed = self._param_helper.get_probe_params(gcmd)['probe_speed'] + speed = self._param_helper.get_probe_params(gcmd)["probe_speed"] # Perform probing move - phoming = self._printer.lookup_object('homing') + phoming = self._printer.lookup_object("homing") trig_pos = phoming.probing_move(self, pos, speed) if not self._trigger_time: return trig_pos @@ -363,12 +405,15 @@ class EddyDescend: end_time = start_time + 0.100 toolhead_pos = toolhead.get_position() self._gather.note_probe(start_time, end_time, toolhead_pos) + def pull_probed_results(self): return self._gather.pull_probed() + def end_probe_session(self): self._gather.finish() self._gather = None + # Wrapper to emulate mcu_endstop for probe:z_virtual_endstop # Note that this does not provide accurate results class EddyEndstopWrapper: @@ -376,34 +421,48 @@ class EddyEndstopWrapper: self._sensor_helper = sensor_helper self._eddy_descend = eddy_descend self._hw_probe_session = None + # Interface for MCU_endstop def get_mcu(self): return self._sensor_helper.get_mcu() + def add_stepper(self, stepper): pass + def get_steppers(self): return self._eddy_descend.get_steppers() - def home_start(self, print_time, sample_time, sample_count, rest_time, - triggered=True): + + def home_start( + self, print_time, sample_time, sample_count, rest_time, triggered=True + ): return self._eddy_descend.home_start( - print_time, sample_time, sample_count, rest_time, triggered) + print_time, sample_time, sample_count, rest_time, triggered + ) + def home_wait(self, home_end_time): return self._eddy_descend.home_wait(home_end_time) + def query_endstop(self, print_time): - return False # XXX + return False # XXX + # Interface for HomingViaProbeHelper def multi_probe_begin(self): self._hw_probe_session = self._eddy_descend.start_probe_session(None) + def multi_probe_end(self): self._hw_probe_session.end_probe_session() self._hw_probe_session = None + def probe_prepare(self, hmove): pass + def probe_finish(self, hmove): pass + def get_position_endstop(self): return self._eddy_descend._z_offset + # Implementing probing with "METHOD=scan" class EddyScanningProbe: def __init__(self, printer, sensor_helper, calibration, z_offset, gcmd): @@ -411,15 +470,17 @@ class EddyScanningProbe: self._sensor_helper = sensor_helper self._calibration = calibration self._z_offset = z_offset - self._gather = EddyGatherSamples(printer, sensor_helper, - calibration, z_offset) + self._gather = EddyGatherSamples(printer, sensor_helper, calibration, z_offset) self._sample_time_delay = 0.050 self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) - self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' + self._is_rapid = gcmd.get("METHOD", "scan") == "rapid_scan" + def _rapid_lookahead_cb(self, printtime): start_time = printtime - self._sample_time / 2 self._gather.note_probe_and_position( - start_time, start_time + self._sample_time, printtime) + start_time, start_time + self._sample_time, printtime + ) + def run_probe(self, gcmd): toolhead = self._printer.lookup_object("toolhead") if self._is_rapid: @@ -429,7 +490,9 @@ class EddyScanningProbe: toolhead.dwell(self._sample_time_delay + self._sample_time) start_time = printtime + self._sample_time_delay self._gather.note_probe_and_position( - start_time, start_time + self._sample_time, start_time) + start_time, start_time + self._sample_time, start_time + ) + def pull_probed_results(self): if self._is_rapid: # Flush lookahead (so all lookahead callbacks are invoked) @@ -440,59 +503,76 @@ class EddyScanningProbe: for epos in results: self._printer.send_event("probe:update_results", epos) return results + def end_probe_session(self): self._gather.finish() self._gather = None + # Main "printer object" class PrinterEddyProbe: def __init__(self, config): self.printer = config.get_printer() self.calibration = EddyCalibration(config) # Sensor type - sensors = { "ldc1612": ldc1612.LDC1612 } - sensor_type = config.getchoice('sensor_type', {s: s for s in sensors}) + sensors = {"ldc1612": ldc1612.LDC1612} + sensor_type = config.getchoice("sensor_type", {s: s for s in sensors}) self.sensor_helper = sensors[sensor_type](config, self.calibration) # Probe interface self.param_helper = probe.ProbeParameterHelper(config) self.eddy_descend = EddyDescend( - config, self.sensor_helper, self.calibration, self.param_helper) + config, self.sensor_helper, self.calibration, self.param_helper + ) self.cmd_helper = probe.ProbeCommandHelper(config, self) self.probe_offsets = probe.ProbeOffsetsHelper(config) self.probe_session = probe.ProbeSessionHelper( - config, self.param_helper, self.eddy_descend.start_probe_session) + config, self.param_helper, self.eddy_descend.start_probe_session + ) mcu_probe = EddyEndstopWrapper(self.sensor_helper, self.eddy_descend) probe.HomingViaProbeHelper(config, mcu_probe, self.param_helper) - self.printer.add_object('probe', self) + self.printer.add_object("probe", self) + def add_client(self, cb): self.sensor_helper.add_client(cb) + def get_probe_params(self, gcmd=None): return self.param_helper.get_probe_params(gcmd) + def get_offsets(self): return self.probe_offsets.get_offsets() + def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) + def start_probe_session(self, gcmd): - method = gcmd.get('METHOD', 'automatic').lower() - if method in ('scan', 'rapid_scan'): + method = gcmd.get("METHOD", "automatic").lower() + if method in ("scan", "rapid_scan"): z_offset = self.get_offsets()[2] - return EddyScanningProbe(self.printer, self.sensor_helper, - self.calibration, z_offset, gcmd) + return EddyScanningProbe( + self.printer, self.sensor_helper, self.calibration, z_offset, gcmd + ) return self.probe_session.start_probe_session(gcmd) + def register_drift_compensation(self, comp): self.calibration.register_drift_compensation(comp) + class DummyDriftCompensation: def get_temperature(self): - return 0. + return 0.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) |