This is a dangerous assumption as your userbase expands. Sketchy loadcells, recycled loadcells, Loadcells overloaded to slight plastic deformation, loadcells mounted with mechanical stress on the sense elements, could all succeed as “touch detectors” if their zero offset is accounted for.
OK, thanks, I’ll take a stab at it. I already started porting some features (like CRC data frame validation), I’ll port others (m04 sensor support, global chop mode) and make a PR.
FWIW, I briefly skimmed through @53Aries code when deciding whether to attempt to fix it or start from scratch and write my own. Ultimately, I decided to write my own code from scratch, so I did not use their code, and used ads1220 sensor source code as a general reference as to what needs to be implemented and typical patterns, and ads131m02 datasheet. Edit: also, notably, their commits do not have a signed-off-by line and were not made into a PR (indicating their agreement to the developer certificate of origin), making it infeasible to use their code in Klipper (without their explicit consent and agreement to the aforementioned certificate of origin).
Yes, this was supported by my implementation from the start (via clock_freq parameter), and I intend to keep it there.
OK, while I would still prefer to make it optional (see my comments below), I think it would be best to put this into a separate PR, and merge the support of ads131m0x sensors independently of that.
In general, I agree with that. However, the reality is that at 1kSPS, ads131m02 creates ~10% load of a core on RPi4, which is fairly performant, and there are less powerfull Pi hosts out there. And for stm32f072 MCU, which is used in FLY ALPS board, it creates ~7% core load, also non-negligible. Meaning, basically, that on lower-end hardware this may create unnecessary performance issues.
Separately, I would note that I want to use a loadcell not for probing, but for measuring the back force in the extruder and potentially calibrating the PA. The scale of the process is ~PA seconds, so for extruders and filaments with very low PA values (like, 0.01-0.02 seconds), I believe we need those 500-1000 samples per second or maybe even more to measure the transition processes in the hotend, and if necessary, apply software post-processing and filtering to denoise the signal. Though I do not have much to share just yet, but when or if I do, I’ll be sure to do that.
I hate to be a wet blanket but…
Have you really considered the physics of this? The column of pressurized plastic in the hole through the nozzle is going to negate some of the force. The residual pressure in the liquid puddle is also going to apply upward force on the end of the nozzle. How much contact force is there behind the puddle as the final “ironing” happens.
An initial parameter lets the puddle spread both sides. The next line can only spread one way. The entire width of the prior line is under the flat part of the nozzle end. Does it touch? How hard? All the time or only some times? Inside radius VS outside radius? What about approaching a inside square corner?
Factor in microscopic fluctuations in actual Z height from motion errors and voids or high spots in the prior layer.
If you sort that all out and actually calculate the realtime fluid pressure you have to rebuild your toolhead board with a FOC closed loop driver or there is no way to utilize the data.
Couldn’t the PA calibration routine be run at the start of a print, then issue a SET_PRESSURE_ADVANCE command that persists for the remainder of the print?
Yes… But what would that routine look like to achieve useful results?
The simple model is to treat the nozzle as a fixed orifice. Extruding filament with the nozzle elevated COULD easily determine a “flow factor” for this nozzle, this filament. at this temperature.
Careful attention to the time domain would be required to realign the filament force measurements to the extruder moves.
A flow correction factor would then need to be determined with test prints to account for back pressure that actual printing causes.
Then (hopefully) when the nozzle/filament/temperature changes a quick “flow factor” measurement and the flow correction factor could be used to adjust PA.
I’m afraid you are overthinking it. I’m not proposing real-time closed-loop flow compensation. I also do not think the current Klipper code is very suited for that either. However, we have a reasonably good open-loop control system for extruders - Pressure Advance. We know it is not perfect, but it works - in a sense that if enabled and adequately tuned, it allows to produce far better prints than without it - eliminating or substantially reducing defects and artifacts such as protruding corners, gaps and underextrusions after start of printing of a new segment (after a non-extruding move) and after a slow segment, horizontal lines on models after a bridge, etc. Calibration of PA requires manual efforts though and is time-consuming and prone to methodological and human errors - printing some test model, and measuring and interpreting the results. It would be great to have some automatic way to calibrate it and find the parameter(s) to feed it to it, similar to how input shaping can be calibrated. This is what I’d like to try to achieve. FWIW, some printer manufacturers already have implemented some versions of a calibration process, though the results vary.
I’ve put about a weeks worth of time into PA. I have code for the measurement and time/extruder move reconstruction. Everything dumps to a JSONL file for processing.
I tried a few different approaches to see if the optimal PA value can be detected from the data. So far no method produces results that match the manual test print method.
But the good news is I can see some subtle effects in the graphs for what PA that’s too high looks like. But PA that’s too low is harder.
I think there is something to this idea. I don’t know yet if some sort of correction factor is required. Testing is time consuming because the only way to validate a test is to print a visual test that matches those conditions.
And yes, I looked at the “Panda Touch”. They did not open source the firmware code which estimates good PA. Also I have not seen proof that its PA results match visual tests.
I actually had a few ideas in mind myself that I could see why they could work from the physics and mathematics standpoint. “Panda Touch” (I believe you meant BD Pressure) sensor, based on what you can gather from their open-sourced part of the implementation, seem to be using one of these ideas under the hood, though some important details have not been open sourced. I do have some preliminary results from another idea of mine that give me some cautious optimism, but I’m afraid it is too early to draw some positive conclusions from them though. So, if there’s going to be anything worth sharing, I’ll do that for sure. Separately, Bambulab have automatic PA calibration in their newer printers that seem to produce at least acceptable results (the exact method they use is, naturally, unknown; they do not use loadcells though, that is certain).
@garethky I opened a PR for ADS131M0x support.
Separately, out of curiosity I measured the impact of global-chop mode on the noise. Here’s how it looked on my sensor and board (fCLKIN = 8MHz):
oversampling_ratio: 4096, enable_global_chop: False:
Sample range / sensor capacity: 0.05852%
Sample range: [9.29% to 9.40%]
Measured samples per second: 976.7, configured: 977.0
oversampling_ratio: 1024, enable_global_chop: True
Sample range / sensor capacity: 0.07028%
Sample range: [7.63% to 7.77%]
Measured samples per second: 1295.5, configured: 1295.0
oversampling_ratio: 2048, enable_global_chop: True
Sample range / sensor capacity: 0.04477%
Sample range: [7.65% to 7.74%]
Measured samples per second: 649.5, configured: 649.0
Since there is no 1-1 correspondence in sampling rate between the modes with global-chop enabled and disabled, I picked the two oversampling_ratio(s) with enable_global_chop: True closest to enable_global_chop: False case by samples/sec. So, I would say that global-chop definitely shifts the output range a bit (<2%) - so indeed there is a small internal offset for the ADS131M02 sensor, but it does not substantially affect the noise level in any meaningful way - that is, a simple oversampling_ratio increase produces similar results.
For what it is worth, I think it would be preferable if the sensor is started upon the first consumer (probing, weight monitoring, filament runout, etc.) and then automatically stopped shortly after the last consumer is finished. This is how all the other sensors work and I don’t see a downside to it. If there are consumers that always run (eg, weight monitoring) then the sensor will always report values, but for those printers that are not using the data then there is no reason to continue to process it.
The bulk sensor code supports this by calling sensor.add_client(my_callback) when an internal user needs measurements, and then the user can return False from that callback when it is finished.
Cheers,
-Kevin
Hello garethky,
I follow your work here regarding load cell sensor. I own a Creality K1 Max and this year, I decided to fix it as in Dec 2025, Creality published the source code of prtouch_v2.py, prtouch_v2_wrapper.py in their official K1 Max repository.
I already work on this small project about 3 weeks, and it looks like I fixed everything what I can fix from python perspective.
This work is more about physical measurements and data processing rather than programming itself and this is my profession…
Many of my changes are directly linked with what you have discussed here and in your discussion from here: Load Cell Design - Developers - Klipper.
On my side, the system starts working, but the only thing I would like to patch further - the MCU timing as you mentioned in your post. Do you have any source code to reference to?
Thank yo in advance, Vlad.
Are you maybe looking for BulkSensor, which uses linear regression to re-construct the timing of events?
Creality made a crappy probing subsystem and there is probably is no saving it. I may have said this elsewhere but just to recap:
- They used the HX711 sensor. This sensor is too slow to use for probing. Your max probing speed is capped at 2mm/s.
- They used 4 of them to sample 4 separate load cells. But crucially they didn’t supply them with an external clock source that would synchronize them. So they don’t produce readings simultaneously or on any kind of regular cadence.
BulkSensor assumes that the event source is a regular interval that can be estimated linearly. You are stuck with two choices: pick one of the sensors as the clock source or produce an event every time one of the sensors reports a new sample. Both of these approaches have drawbacks. One smears the event out in time, the other in a non-regular event that Bulk Sensor cant accurately estimate.
The right answer is something like the ADS131M04 which can produce 4 simultaneous samples. We have support for that in Kalico right now.
Thank you very much for your reply - yes, I was looking for a fix of leveling MCU and I have fixed it, not completely, but at 90%, I suppose.
So, based on what I read here, in your another discussion back in 2022 about load cell sensors and by using your code in github, analysing Prusa github too, hereby what I achived:
- In src/prtouch_v2.c, the main change in my tree was moving PRTouch timing away from the old software-accumulated “system time” helper onto direct hardware timer reads.
- Trigger times and sample times are now derived from timer_read_time() and converted to units directly, instead of depending on a rescheduled software timer.
- This is reducing timestamp drift, race effects, and inconsistencies between pressure sampling and step timing. Now measurements are very stable in comparison what I used to have.
- The hardware timer is implemented based on native GD32E23x timer, not generic STN32: in src/gd32/gd32e23x_timer.c, the timer init path was hardened for the GD32E23x target.
- For the HX711 path, reads were changed to wait until all configured channels are ready before capturing a sample, instead of mixing readiness timeout behavior with partial reads.
- A per-sample timestamp was added, helper routines were introduced to reset pending state and read one sensor cleanly.
- The step/pressure FIFO timestamps now use the direct timer-based time source.
- In addition, the MCU → Python protocol was changed by adding step_cnt, but it doesn’t work so far as planed due to Creality hardware issue - need to find PC1 - PB10 hardware link between MCU0 and leveling MCU.
- On Python side, the cluster logic along with Creality code bug fixing and many code improvements/evolvements were implemented.
- Separation of measuring, movements, repeat decision and completion were implemented too.
- Geometry filter and channel selection by geometry were removed completely. All available channels are in use right now → this is more about physical measurement rather then programming (this is my speciality in addition to programming
). - A lot of MCU controlling toolhead movement and management improvements are implemented in the code.
So, the main issue which stays unsolved yet, but the system works quite well even with fallback logic, is the leveling MCU → main MCU hardware communication loop. It doesn’t work as it is and as it was planned by Creality. Base on tests, there might be not physical wires between those 2 pins - so, I will need to solve the mistery or at least understand the status.
So, as the source code of Creality is deeply and widely changed, now I am planning to implement multi-probe support inside of the code and thus enable PRTouch + Eddy option. Both sensor would complement each other and if it will work the bed leveling issues for K1 Max will be solved, finally.
Even now, the stability of measurements is very good in comparison with original Creality code. But the result is not satisfied me well. I print mostly at 100C with ABS, and at such temperature conditions I have huge hump near the middle of the bed, which ruins my prints.
So, I added on top of the stock naked plate a CR10S Pro metal plate, which stabilize the surface much better than it was before, so, for now I have ± 0.5mm of differences, but still, I believe those irregularities are not the real surface but artifacts of current leveling systems.
So, that is my status and again, thank you for your guidance!
Vlad.
Hi everyone,
I am back after a very long break being busy with other stuff, but maybe some of you may still remember me from the beginning of this thread
Finally I want to get rid of my old load cell probe implementation (which never ended up in the main branch, because it was way too complicated) and use @garethky s much more streamlined implementation (which is also way faster).
The only problem seems to be that it requires a very fast ADC, which I don’t have. With an 80 Hz HX711, the precision really does not seem to be ideal. This made me try out an idea from my old implementation: if we apply a linear fit to the last ADC samples right before the probe triggers but after the nozzle has already made contact with the bed, we can extrapolate the position where the force is 0, which is perfectly the point of contact. As long as we have at least 3 usable samples for the fit, this depends much less on the spacing of the samples and the trigger force. This allows us to both reduce the statistical error (mainly caused by time discrete sampling which adds at least one sampling period jitter) as well as the systematic error (caused by a finite trigger force which makes the probe always trigger late).
I have already made a modification of the implementation to try this out: klipper/klippy/extras/load_cell_probe.py at lcp-linear-fit · RF1000community/klipper · GitHub
The new feature is optional and has to be enabled with the new linear_fit config option (disabled by default). Also you will want to set the trigger force as high as possible, so you get as many samples as possible between the contact point and the trigger point.
With this, I see a significant improvement when using a HX711, even tough this chip is very much not ideal for my load cells (two 5kg TAL 220B wired in parallel), since I use only much less than 1% of the ADC range (had to calibrate even manually, calibration routine aborts when the difference is that small).
Here are the results from running the probe_accuracy command each 2 times for the state conditions:
speed 2, with linear_fit
// probe accuracy results: maximum -0.007239, minimum -0.015997, range 0.008758, average -0.013288, median -0.014382, standard deviation 0.002741
// probe accuracy results: maximum -0.010115, minimum -0.023834, range 0.013719, average -0.018420, median -0.019216, standard deviation 0.004019
speed 2, without linear_fit
// probe accuracy results: maximum -0.093391, minimum -0.118000, range 0.024609, average -0.101438, median -0.098273, standard deviation 0.008424
// probe accuracy results: maximum -0.071906, minimum -0.094172, range 0.022266, average -0.079719, median -0.076789, standard deviation 0.006929
speed 5, without linear_fit
// probe accuracy results: maximum -0.143000, minimum -0.193391, range 0.050391, average -0.174445, median -0.179328, standard deviation 0.013917
// probe accuracy results: maximum -0.138703, minimum -0.189875, range 0.051172, average -0.165148, median -0.164289, standard deviation 0.018683
speed 5, with linear_fit
// probe accuracy results: maximum -0.049050, minimum -0.068959, range 0.019909, average -0.059374, median -0.060353, standard deviation 0.006007
// probe accuracy results: maximum -0.029639, minimum -0.050400, range 0.020760, average -0.037761, median -0.037333, standard deviation 0.006135
So as you can see even at 5mm/s the range (worst case error) and standard deviation is better than without linear fit at 2mm/s. (I would still go with 2mm/s for the extra precision.) The improvement is around a factor of 2 when keeping the speed the same. Maybe, with a less noisy setup and some parameter tuning this can even be improved.
For the measurements with the linear_fit enabled, I have used the maximum of 250g for the trigger_force, while without linear_fit I used the default 75g (lower doesn’t really work for me, as the signal is too noisy).
I can create a pull request for this, if this is considered interesting, but I will have to update the documentation at least. Also I really want to hear first what @garethky has to say about this ![]()
@mhier, JFYI
My 2 cents, it seems similar to how the probe_eddy untap was implemented.
So, you can basically look at the history of changes and port your changes over.
Basically, your implementation is the first “approximation” of how it is implemented, where one can either guess the position, or one can guess the time and look up the history, but it is fragile, because after a “homing” move, there is a discrepancy in the step history
Where the second approximation is to record the untap (retract) move, and basically, otherwise, the logic is pretty similar
- History for klippy/extras/probe_eddy_current.py - Klipper3d/klipper · GitHub
- Untap analysis: probe_eddy_current: Calculate "tap" position from lift movement · Klipper3d/klipper@4db1bf8 · GitHub
- Tap analysis: probe_eddy_current: analyze tap data · Klipper3d/klipper@0795fb0 · GitHub
Hope that can be useful.
Ah, also, there is: PR: Load Cell Tap Analysis by garethky · Pull Request #6958 · Klipper3d/klipper · GitHub
Regards,
-Timofey
after a “homing” move, there is a discrepancy in the step history
Thanks for your valuable input. Just that I understand you correctly: The homing move is different from an ordinary move, because it is abruptly interrupted by the MCU instead of planning the move until its stop. As a consequence the step history right before the probe triggers may be inaccurate (may be in particular in multi-MCU setups?). Hence establishing a relationship between the z position and the measured load-cell force is inaccurate as well. Is this what you are saying? (I don’t really understand how this can influence data from before the probe trigger though…)
The eddy current implementation is a lot more complicated than ours have to be (because the Physics is different). Anyway, I could “steal” the way they are triggering the backup movement to record samples.
Maybe this explains why I am still seeing a small systematic error (I though it would be caused by the noisy setup).
PS: Looking at the eddy current implementation only helps a little, because it is hard to understand why things are done as they are done.
In addition to the low-level details shared by Timofey above, there is a high-level description of the eddy probe “tap” support at: New probe_eddy_current "tap" logic by KevinOConnor · Pull Request #7220 · Klipper3d/klipper · GitHub
As a consequence the step history right before the probe triggers may be inaccurate (may be in particular in multi-MCU setups?).
The step history before the halt is available and fully usable. However, things get complicated when extracting the step history after the mcu signals a halt. So, that makes it difficult to analyze the post-contact data. There also appears to be more physical oscillations in the data (as discussed in the link above), so in general analyzing the subsequent lift movement seems to have many advantages.
For what it is worth, I suspect there is likely a lot of overlap between eddy “tap” and loadcell “taps”. I’d guess the loadcell taps wouldn’t need the quadratic least squares, as I’d guess one could fit two linear lines to the loadcell data. That would likely be a simplification to the eddy code.
Cheers,
-Kevin
In addition to the low-level details shared by Timofey above, there is a high-level description of the eddy probe “tap” support at: New probe_eddy_current "tap" logic by KevinOConnor · Pull Request #7220 · Klipper3d/klipper · GitHub
Thanks, I will look into this later today in more detail. I need more time to digest all of that properly
Especially I should understand first, how the eddy current probe works at all (so far I assumed it would be contactless, but now I read something about the probe pressing the bed down, so my understanding is apparently wrong)…
The step history before the halt is available and fully usable. However, things get complicated when extracting the step history after the mcu signals a halt. So, that makes it difficult to analyze the post-contact data.
Forgive me, but I am a bit uncertain what you mean by this: I am analyzing only data which has been recorded before the MCU signals a halt. The analysis happens after the halt of course. Also the data is partially “post-contact” from a physical point of view, because the nozzle has in fact already hit the surface, but this cannot really make any difference for the data processing. Hence I assume you mean “post-halt data” rather than “post-contact data”, am I correct? So the analysis I have added should not be affected by such problems?
There also appears to be more physical oscillations in the data (as discussed in the link above), so in general analyzing the subsequent lift movement seems to have many advantages.
Ok, you probably refer to oscillations caused by mechanically hitting the surface of the bed with the nozzle, right? Yes, maybe from that point of view it can make sense to do the measurement when backing off. I have not yet seen such oscillations, but that doesn’t mean they are not there for everyone.
I’d guess the loadcell taps wouldn’t need the quadratic least squares, as I’d guess one could fit two linear lines to the loadcell data. That would likely be a simplification to the eddy code.
The loadcell behavior should be rather perfectly linear from a Physics point of view. This is behaving like a spring, so the force F = D*s (with s the distance and D the spring constant). Non-linearities will almost always point to some mechanical problems of the printer (e.g. something is loose), which we cannot fix in software.
On the other hand, the noise shall be completely uncorrelated to the z position. The “tare” baseline may wonder around a bit over time, but again this will be uncorrelated and impossible to predict or extrapolate. It is just a low-frequency noise component. Hence the best we can do is a single linear fit after subtracting the tare, while keeping the time between tare and measurement as short as possible. (This may be a possible improvement of my code: I can estimate the tare from data right before the nozzle physically hits the surface. Or when measuring during lift, after the nozzle has lost contact.)
The eddy current probe implementation looks a lot more complicated than an improved loadcell probe has to be. We don’t need a complicated calibration routine. Temperature effects might be present, but they will be slow compared to the measurement time even with a readout speed of only 80 Hz. I am using a loadcell probe for 10 years now in a production environment (with several years of actually developing on that subject), and I am pretty sure I have understood most effects (at least those which are visible at slow readout speeds).
how the eddy current probe works at all (so far I assumed it would be contactless, but now I read something about the probe pressing the bed down, so my understanding is apparently wrong)…
The eddy current probe is normally contactless. The “tap” mode is a bit of a “trick mode”. In normal operation the probe reports a coil resonant frequency - the higher the frequency the closer the probe is to the bed. During “tap”, the nozzle descends towards the bed, the sensor frequency increases, eventually the nozzle makes contact with the bed, and the sensor stops increasing in frequency (as once the nozzle makes contact with the bed the sensor also stops approaching the bed).
As I understand it, in many ways this is similar to load cell. In both cases, the sensors exhibit a dramatically different pattern at the point of contact, and the goal is to analyze the pre and post contact data to determine that contact point.
Forgive me, but I am a bit uncertain what you mean by this: I am analyzing only data which has been recorded before the MCU signals a halt.
There are several things going on during a “probe” attempt - specifically the following discrete events occur in the given order: the nozzle is commanded to descend; the nozzle makes contact with the bed; the mcu signals that the nozzle has made contact; the motors halt.
The host code does not yet know when the actual contact point time is. To figure that out, it wants to analyze data before the “mcu signal time” and also after the “mcu signal time”. However, it’s currently difficult for the host to analyze Z motor positions after the “mcu signal time”, because the amount of time between “mcu signal” and “motor halt” can vary. This is all pretty small stuff (think milliseconds of time variance), but the amount of probing precision desired is also very tiny (think microns).
Anyway, to keep things simple, the current host Klipper probing code will return a trigger_time (for example, self.trigger_analog.get_last_trigger_time()). During probe analysis it is fine to lookup the Z position before that reported trigger time, but looking up Z positions immediately after that trigger time should be avoided.
Ok, you probably refer to oscillations caused by mechanically hitting the surface of the bed with the nozzle, right?
Yes.
Cheers,
-Kevin
All right, thanks, I think I see your point now. The data for the load cell is kind of “inverse” compared to the eddy current, as it will be flat without contact and change linear with contact. I will look into “copying” the ascend logic. I still think the math can be a lot simpler, because it is linear and no complicated calibration is required. Maybe I will try out some smarter way to divide the samples into two regions, one above the surface for an updated tare, and one with contact to the surface for the linear fit (and a small gap in between, just to be safe). Will probably take a couple of days, since I have no time this week any more unfortunately…