Hi @garethky , great work your doing here, and thanks for you contributions.
Edit:
Nevermind, I think what I’m describing is the “pullback” move you introduced 19 days ago, I was not up to date on this thread.
OUTDATED ORIGINAL POST
I was thinking about how to take advantage of the readily available HX711, and while 80 sps might be too slow, forcing you to make the probing process extremely slow to actually be accurate, in the common way of probing at least, what if we get the meassurement when we are raising the nozzle back up after touching the plate instead of when its going down?
Let me explain, we could still lower the nozzle with an acceptable speed, like 2.5 mm/s ? (at least that is what I’m using with a bltouch), and that would give you a margin of error of ~ 0.031 mm with the 80 sps, but don’t use that position just yet, make it stop there and raise the nozzle at a really slow speed, like say 0.5 mm/s, till the treshold is met again, and that would be the actual probing position to consider.
Since the margin of error was 0.031 going down, moving the nozzle back up (till the load cell treshold is met again but in the other direction) at 0.5 mm/s shoud take only around 0.06 seconds (60 ms) and that would take as from a margin of 0.031 mm (2.5 mm/s) to 0.006 mm (0.5 mm/s). The raising speed could even be lower since there is so little distance to travel.
Do you think this could work or it wouldn’t be viable for other reasons?
Yes, that’s the pullback move. I started working on that in August: Load Cell Probing Algorithm Testing - #49 by garethky That thread is even more nerdy and math heavy than this one! I can’t take credit for the idea, it’s the way Prusa implemented it for the MK4 and XL.
Here is my implementation of a working strain gauge endstop that I got working recently. It uses this strain gauge amplifier and this strain gauge. I probe once at 8mm/s followed by a slow 2.5mm/s probe. I made my own PCB with a quarter wheatstone bridge circuit and an ATTiny85 chip to process the signal and raise an output pin high when the signal is detected. I attached some images of my design. I super glued the strain gauge to a 3D printed adapter which has two blade flexures which are 1.5mm thick. I then coated the strain gauge with two layers of a brush on silicone conformal coating to help protect it since the strain gauge is very fragile (I broke several in my tests). The repeatability is decent at this slow probe speed around 0.04mm from lowest to highest reading. Maybe I’m doing something wrong but it seems like the Z offset (yes the z offset not the repeatability) seems to change with the probe speed but I don’t understand why. Let me know if you have any questions!
Lower the faster you go right? Makes sense to me. For an external trigger to work, it has to be as fast as a switch is. Klipper checks the endstop pin at the same rate as the probing move’s microstep rate. If you are polling the ADC slower than the micrstep rate, the amount of overshoot will increase with the probing speed, changing the z offset.
Strangely no, I have to go to around Z 0.4 to slide a paper under the nozzle when I probe at a speed of 1.5mm/s and I have to go to around Z 0.2 when I probe at 2.5mm/s. I could do more testing at more speeds in between to see if it is continuous or not. I have done at least 5 probes at each speed so I think that is enough to rule out noise.
Use PROBE_ACCURACY for testing and reporting the repeatability range. The code I’m working on here is repeatable to about 2.5 microns over 50 probes, sometimes its less than 1 micron. An “acceptable” probe is less than 25 microns (0.025mm) of range over 10 probes.
Different speeds come with different amounts of noise. The noise is be both electrical and physical. The amount of noise at the load cell isn’t directly correlated with speed, just like stepper noise isn’t correlated with speed (i.e. faster can be quieter or lounder).
There is another thread, Load Cell Design, where we are discussing physical design of load-cell/toolhead integration. Would love to find out more about your toolhead and how it was built over there. We lack toolheads and sensor boards to enable people to test this code. Its a chicken and egg problem.
I made a video talking more about my design in a little more detail and posted it on the load cell design thread.
Here are the PROBE_ACCURACY results with 10 probes:
maximum 0.038125
minimum 0.008750
range 0.029375
average 0.027688
median 0.030000
standard deviation 0.009070
Over Christmas I did an investigation of Prusa’s use of the Butterworth filter. I was able to replicate what they are doing and, very long story short, an improved version is implemented and working on the test rig:
The filter requires the SciPy library. Its compatible with Prusa’s filter, so you could configure an MK4 or XL with the same filter frequencies they used. But I think we can do even better. The implementation has a number of improvements over Prusa’s specifically for klipper:
Its in fixed point Q12 instead of floating point, aimed at the Cortex M0+ cores on most toolhead boards. So an FPU is not required.
Its fully configurable, no filter coefficients in the source code. Printer designers will be able to measure drift frequency using an FFT and to find optimal filter frequencies. A Python notebook is included in scripts to help with this.
The filter doesn’t have to be a bandstop filter. It can be just a highpass filter which reacts to taps faster than the bandstop filters that Prusa used.
Up to 2 Notch filters can be configured to remove noise from your power supply. They might also be able to remove a particularly stubborn frame resonance frequency.
The Host tears the load_cell_endstop to 0g at the start of homing moves. This is the one condition where the filter’s initialization state vector is all 0’s. This means there is no settling time for the filter. In the video above the 375ms settling time was set to 0 and it works just fine.
A “safety_limit” has been implemented that puts limits on the force to the toolhead based on the reference tear value that’s saved when the load cell is calibrated. If the filter is too aggressive you have this as a backup. Also, if you damage the load cell, this should cause it to stop working so you know something is wrong.
The filter is completely optional and off by default. If your not using a toolhead load cell you probably won’t need the filter.
bulk_sensor Refactor
This is underway right now, its going to be quite the change:
The single and ‘multi’ implementations of the HX71x will be unified so there is just 1 thing to maintain.
The config will see major changes. I have to go back to the sensor_type: hx711 style config where the sensor options are inside either the [load_cell] or [load_cell_probe] sections (following the pattern of [temperature_sensor] and [angle]). This keeps the initialization logic for the load_cell_endstop under the control of the containing module.
Aside from the load_cell_endstop support, hx71x.py will be a generic bulk_sensor that any other subsystem can adapt to be a data source. So you could use it as a PT100 sensor, filament width sensor, filament scale etc.
I want to get support for graceful resets in, so they don’t ruin a print. They will still result in a shutdown during probing. I think the Host has to be informed of the reset and request a restart of measurements.
Until I get done with all this the firmware wont build in the branch. It allocates too many message Ids. Post-refactor this isn’t a problem.
i tried to use your Implementation of a loadcell based endstop with my loadcell mount for a E3D hemera.
My test mule is a i3 fake printer.
somehow I am getting the following error in the klippy log file when using the probe accuracy command
Internal error on command:"PROBE_ACCURACY"
Internal Error on WebRequest: gcode/script
Traceback (most recent call last):
File "/home/pi/klipper/klippy/webhooks.py", line 256, in _process_request
func(web_request)
File "/home/pi/klipper/klippy/webhooks.py", line 431, in _handle_script
self.gcode.run_script(web_request.get_str('script'))
File "/home/pi/klipper/klippy/gcode.py", line 216, in run_script
self._process_commands(script.split('\n'), need_ack=False)
File "/home/pi/klipper/klippy/gcode.py", line 198, in _process_commands
handler(gcmd)
File "/home/pi/klipper/klippy/gcode.py", line 135, in <lambda>
func = lambda params: origfunc(self._get_extended_params(params))
File "/home/pi/klipper/klippy/extras/probe.py", line 227, in cmd_PROBE_ACCURACY
pos = self._probe(speed)
File "/home/pi/klipper/klippy/extras/load_cell_probe.py", line 698, in _probe
ppa.analyze()
File "/home/pi/klipper/klippy/extras/load_cell_probe.py", line 325, in analyze
if not self.validate_elbow_clearance():
File "/home/pi/klipper/klippy/extras/load_cell_probe.py", line 349, in validate_elbow_clearance
start_idx = index_near(self.time, self.tap_points[1]) + width
File "/home/pi/klipper/klippy/extras/load_cell_probe.py", line 85, in index_near
return int(np.argmax(np.asarray(time) >= instant) or len(time) -1)
TypeError: '>=' not supported between instances of 'float' and 'ForcePoint'
MCU 'mcu' shutdown: Command request
My config looks like this:
########################################
# Load Cell
########################################
[load_cell my_scale]
sensor_type: hx711
sclk_pin: PB3
# connected to the clock pin
dout_pin: PB4
# connected to the data output pin
gain: A-128
# Valid values for `gain` are `A-128`, `A-64`, `B-32`. The default is `A-128`.
# `A` denotes the input channel and the number denotes the gain. Only the 3
# listed combinations are supported by the chip. Note that changing the gain
# setting also selects the channel being read.
sample_rate: 80
# Valid values for `sample_rate` are `10` or `80`. The default value is `10`.
# This must match the wiring of the chip. The sample rate cannot be changed
# in software.
[load_cell_probe]
load_cell: load_cell my_scale
# The complete name of a load cell printer object. Required.
trigger_force_grams: 60.0
# The force that the probe will trigger at. 50g is the default.
trigger_count: 2
# The number of samples over the trigger_force_grams threshold that will cause
# the probe to trigger
pullback_dist: 0.5
# The distance of the pullback move in mm. This move needs to be long enough
# to bring the probe away from the bed after it makes contact.
pullback_speed: 0.4
# Speed of the pullback move. The default value is to move at a speed of 1
# sample every 1 micron based one the sensors sample rate is.
settling_time: 0.425
# Additional time to wait before taring the probe. This allows any vibrations
# to settle and bowden tubes time to flex etc. This improves repeatability.
pullback_extra_time: 0.4
# Time to collect additional samples after the pullback move ends in seconds.
# This improves accuracy by giving the algorithm more points after the probe
# breaks contact with the bed. Disabling this entirely may impact reliability.
tare_count: 10
# The number of samples to use when automatically taring the load_cell before
# each probe. The default value is: sample_per_second * (1 / 60) * 4. This
# collects samples from 4 cycles of 60Hz mains power to cancel power line
# noise.
#bad_tap_module:
# Name of a printer object that implements the BadTapModule interface. This
# checks taps to see if they meet minimum requirements and can
#nozzle_cleaner_module:
# Name of a printer object that implements the NozzleCleanerModule interface
# than can handle nozzle cleaning. If one is provided the nozzle_cleaner_gcode
# is disabled.
#nozzle_cleaner_gcode:
# A Gcode macro that is called when a bad tap is detected and the nozzle needs
# to be cleaned. The default Gcode prints a warning to the console.
z_offset: 0.0
speed: 5.0
samples: 1
sample_retract_dist: 2.0
#lift_speed:
samples_result: average
samples_tolerance: 0.100
#samples_tolerance_retries: 0
#activate_gcode:
#deactivate_gcode:
# See the "probe" section for a description of the above parameters.
Is anybody struggleing with the same issue?
Has anybody fixed this issue already?
This looks like a Heisenbug, I should be hitting this. I’m on 2.7, mainly to stop myself from including any Python 3 only feature. Maybe Python 3 is more strict?
Either way its an outright coding error. the .time is missing from the tap point in that method. It should look like this:
# check for space around elbows to calculate r_squared
def validate_elbow_clearance(self):
width = self.r_squared_widths[-1]
start_idx = index_near(self.time, self.tap_points[1].time) + width
end_idx = index_near(self.time, self.tap_points[4].time) - width
return start_idx > 0 and end_idx < len(self.time)
Unfortunately I’m not in a position to update the branch right now because I’m in the middle of the bulk_sensor work. I cant even test this fix.
But I am looking forward to working withe anyone and everyone that want to test this in maybe a week or so!
I am on python3 3.9.2
maybe I can uninstall that version and try the python version 2.7.18 which is displayed when checking for the “standard” python version
Sounds like a good way to stay compatible with older installations.
Thanks for the quick response!
No worries!
I might be able to edit the file I am using right now. Or would you say that this would be a bad idea?
I’d be glad to help you with this as far as my skills allow it.
I gotta say that I am qiute a newbie regarding coding or testing code
Edit:
Just edited the code, so far I managed to do two succesfull probe accuracy tests.
So far so good, great job!
I stumbled upon a new issue.
I tried using python 2.7 and python 3
Failure message with python 2.7
File "/home/pi/klipper/klippy/extras/load_cell.py", line 425, in load_config_prefix
return LoadCell(config)
File "/home/pi/klipper/klippy/extras/load_cell.py", line 318, in __init__
self.sensor = multiplex_adc.MultiplexAdcSensorWrapper(config, sensor)
File "/home/pi/klipper/klippy/extras/multiplex_adc.py", line 232, in __init__
self.api_dump = motion_report.APIDumpHelper(
AttributeError: 'module' object has no attribute 'APIDumpHelper'
webhooks client 1969539032: New connection
Using python 3
Unhandled exception during connect
Traceback (most recent call last):
File "/home/pi/klipper/klippy/klippy.py", line 175, in _connect
self._read_config()
File "/home/pi/klipper/klippy/klippy.py", line 141, in _read_config
self.load_object(config, section_config.get_name(), None)
File "/home/pi/klipper/klippy/klippy.py", line 130, in load_object
self.objects[section] = init_func(config.getsection(section))
File "/home/pi/klipper/klippy/extras/load_cell.py", line 425, in load_config_prefix
return LoadCell(config)
File "/home/pi/klipper/klippy/extras/load_cell.py", line 318, in __init__
self.sensor = multiplex_adc.MultiplexAdcSensorWrapper(config, sensor)
File "/home/pi/klipper/klippy/extras/multiplex_adc.py", line 232, in __init__
self.api_dump = motion_report.APIDumpHelper(
AttributeError: module 'extras.motion_report' has no attribute 'APIDumpHelper'
webhooks client 1971101928: New connection
Is there something changed?
With the old version of klipper it worked.
I see my bad, I’ll try it during the weekend.
My circuit was behaving quite weird after properly working.
The Endstop was constantly triggered although I increased the trigger force step by step.
An Oscilloscope might have helped me identify the issue.
But sadly I don’t have one right now.
Since you were writing that the new version should be more stable I am waiting for that one. I am in no rush to print.