Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for load cell based probe #3496

Closed
mhier opened this issue Nov 2, 2020 · 72 comments
Closed

Support for load cell based probe #3496

mhier opened this issue Nov 2, 2020 · 72 comments
Labels

Comments

@mhier
Copy link
Contributor

mhier commented Nov 2, 2020

I am in the process of writing the support for my 3D printer (Renkforce RF1000) which has a special type of bed level probes based on a load cell to measure the force when the nozzle touches the build plate. I am wondering what the best approach is to integrate this. I find the modular approach of the Klipper firmware very appealing and I would like to embrace it as much as possible. This is what I got so far:

I have written a module to support the I2C-connected ADC for measuring the load cell signal:
https://github.com/RF1000community/klipper/blob/master/klippy/extras/ads1100.py

This already works, I can add the ADC as pin for a a fake temperature sensor to get it into the system. Then I see the debug log messages printing, and the query adc command works as well.

Next I was thinking to write another implementation of the PrinterProbe class and register it as a "probe" object, so it is found by the ProbePointsHelper. That part (finding the object) also seems to work.

Now I wonder I best interface the new PrinterProbe implementation with the ADC module without breaking the abstraction. My idea was to register a callback function (via setup_adc_callback) to retrieve new samples when arriving and process them inside the run_probe() function. This doesn't seem to work though, because the callback function is no longer serviced while staying inside run_probe() - presumably because both would run in the same thread.

Is there another generic method to obtain the ADC samples (preferably with a timing based on the ADC sampling time)? If possible I would like to avoid introducing a private interface between the new PrinterProbe implementation class and the ADC module, because this would prevent to use another ADC type with the probe implementation.

PS: I am intending to send pull requests with the printer support once it is "complete". Is there any policy how do to this? Individual pull requests for each feature/module, or better one big pull request with everything? The printer support also involves a new stepper driver module which will even require modifications to the SPI code (it expects an inverted CS signal... so far I just hacked this...).

@klipper-gitissuebot
Copy link

Hi @mhier,

It did not look like there was a Klipper log file attached to this ticket. The log file has been engineered to answer common questions the Klipper developers have about the software and its environment (software version, hardware type, configuration, event timing, and hundreds of other questions).

Unfortunately, too many people have opened tickets without providing the log. That consumes developer time; time that would be better spent enhancing the software. If this ticket references an event that has occurred while running the software then the Klipper log must be attached to this ticket. Otherwise, this ticket will be automatically closed in a few days.

For information on obtaining the Klipper log file see: https://github.com/KevinOConnor/klipper/blob/master/docs/Contact.md

The log can still be attached to this ticket - just add a comment and attach the log to that comment.

Best regards,
~ Your friendly GitIssueBot

PS: I'm just an automated script, not a human being.

@KevinOConnor
Copy link
Collaborator

Interesting. FYI, there was a discussion on load cells a few months ago (see #2977).

I can add the ADC as pin for a a fake temperature sensor to get it into the system.

FYI, it isn't necessary to route the data back through the system as an ADC/temperature input. You can just directly register the reactor timer, query the i2c values, and then handle the results. Doing so will simplify the code and introduce less issues with blocking.

Now I wonder I best interface the new PrinterProbe implementation

That is going to be some work. The current probe system is based on the idea that the micro-controller can directly stop the steppers on an endstop/probe signal. That's important for homing because we don't want any mcu<->host communication delays to result in the head crashing into the bed.

So, if you want to implement a similar system it would require implementing all the sensor reading and interpretation in the micro-controller C code. Getting reliable timing over an i2c interface will also be a challenge.

In contrast, if you're not interested in homing with the sensor, and instead are just interested in doing probing, then it may be possible to do that in just the host code. It would require moving, waiting for the move to complete, measuring the load, moving again, measuring, etc. It would be slow, but may work okay. In that case, you probably want to look at how klippy/extras/bltouch.py interacts with klippy/extras/probe.py - your code would also need to override how the low-level PrinterProbe._probe() code works.

-Kevin

@mhier
Copy link
Contributor Author

mhier commented Nov 3, 2020

Thanks for the fast reply!

Interesting. FYI, there was a discussion on load cells a few months ago (see #2977).

Indeed, thanks for the link. We have a different chip (actually two chips, one amplifier and one ADC), but the principles are the same. I take it there was no real outcome of the other thread though. I am in a bit different position: I do have a working system with a Repetier-based firmware available in source code, so I know what to do (and I have plenty ideas how to do better with the now-available computation power).

I can add the ADC as pin for a a fake temperature sensor to get it into the system.

FYI, it isn't necessary to route the data back through the system as an ADC/temperature input. You can just directly register the reactor timer, query the i2c values, and then handle the results. Doing so will simplify the code and introduce less issues with blocking.

I know, still I like to avoid tying the module to the particular chip. This would be unfortunate, because people e.g. with a HX711as in #2977 (which looks like a much better choice than the RF1000's 16 bit 8 SPS ADC...) could not use the code. Maybe an intermediate solution is to add a new function to the ADC module interface, which is then required for all ADCs which shall be used with the new probe module.

In contrast, if you're not interested in homing with the sensor, and instead are just interested in doing probing, then it may be possible to do that in just the host code.

Sorry, this was unclear in my original post. I intend to do exactly that. Homing based on the sensor is anyway not a good idea, our ADC has a low sampling rate of only 8 SPS. Also load cell readings are subject to drifts (especially at high temperatures - some people need to do mesh bed leveling with a hot bed), which makes it impossible to use a simple threshold on the read out value. Instead we need a sophisticated algorithm moving the toolhead down and up iteratively until we found the contact point while applying a low force. A higher force will spoil the measurement long before doing damage to the hardware, because the load cells (and all other components) will bend slightly. - As I said, I do not have to reinvent this, as we have a working firmware doing exactly this.

It would require moving, waiting for the move to complete, measuring the load, moving again, measuring, etc. It would be slow, but may work okay.

This is what the Repetier firmware is doing. I don't expect it to be (considerably) slower with Klipper in the end, simply because we are mostly limited by the 8 SPS readout speed.

In that case, you probably want to look at how klippy/extras/bltouch.py interacts with klippy/extras/probe.py - your code would also need to override how the low-level PrinterProbe._probe() code works.

Thanks, this is what I will do!

Btw: Other features mentioned in #2977 are also already implemented (at least partially) in our Repetier-based firmware, and I intend to implement them for Klipper as well. We have e.g. a Z distance correction for the first layer based on the extrusion pressure, which does a great job to compensate for the thermal expansion of the extruder (which partially happens only after reaching the temperature). Also, since the RF1000 can double as a small CNC mill, I like to add stock part edge detection to set the origin and rotation easily. This works, because the load cells are somewhat sensitive also in X and Y (but algorithms need to be even more sophisticated).

@mhier
Copy link
Contributor Author

mhier commented Nov 4, 2020

I have a first, somewhat working version. The scan for one point takes quite some time. This is expected. The problem now is, that Octoprint will lose connection during the scan. Do I have to call some function periodically to make sure other tasks are properly serviced?

@KevinOConnor
Copy link
Collaborator

As long as some output is periodically produced (gcmd.respond_info() or gcode.respond_info()) then OctoPrint will not complain.

-Kevin

@github-actions
Copy link

Hello,

It looks like there hasn't been any recent updates on this
Klipper github issue. If you created this issue and no
longer consider it open, then please login to github and
close the issue. Otherwise, if there is no further activity
on this thread then it will be automatically closed in a few
days.

Best regards,

~ Your friendly GitIssueBot

PS: I'm just an automated script, not a human being.

@github-actions github-actions bot added the Stale label Nov 27, 2020
@mhier
Copy link
Contributor Author

mhier commented Nov 27, 2020

I am still working on this. I personally don't need this ticket to stay open, but maybe it would be a good idea to keep this open so others know that there is something ongoing. A quick status update:

Bed mesh calibration works now quite well. The repeatability is in the order of 1/100 mm (peak-to-peak) resp. 2/1000 mm (RMS):

Recv: // probe accuracy results: maximum -2.268871, minimum -2.275891, range 0.007020, average -2.271472, median -2.270901, standard deviation 0.002020

The absolute values are slightly off though, and I have to investigate why. The 0 position is slightly above the bed (0.05 to 0.1mm). It is not a show stopper, as it can be easily corrected, but it is still unclear whether this depends on the copy of the printer model (one other tester had reported a bigger discrepancy). I guess I also need more testers...

Also I want to implement more advanced features as well. Even if they don't go into the first pull request, I want to make sure I have a working interface for these advanced features. Also some code cleaning is still necessary.

@github-actions
Copy link

Hello,

It looks like there hasn't been any recent updates on this
Klipper github issue. If you created this issue and no
longer consider it open, then please login to github and
close the issue. Otherwise, if there is no further activity
on this thread then it will be automatically closed in a few
days.

Best regards,

~ Your friendly GitIssueBot

PS: I'm just an automated script, not a human being.

@mhier
Copy link
Contributor Author

mhier commented Jan 4, 2021

Sorry, I missed the bot because I didn't check the mails in the holiday season. I am still working on this, and I actually have a question:

Closely connected to the load-cell probe and the bed mesh scan, I have implemented a feature (in a separate module called z_offset_scan) which allows to remeasure the overall Z offset at a single point. This is helpful if e.g. a mechanical change was done which affects the absolute distance between nozzle and bed (e.g. change position where Z endstop triggeres) without changing the relative levelling of the bed (e.g. bed screws are not changed etc.).

I am wondering, whether this feature already exists in Klipper - I couldn't find anything exactly like this in the documentation. It may be somewhat motivated by the slowness of the load cell based probe - a full bed mesh scan takes easily 10 or 20 minutes (depending on number of points and the starting distance from the bed). Still I could imagine it would be a nice feature to have for everyone.

Now my actual question: For this feature to work as expected under all conditions, I would need to modify the bed mesh scan module: The z_offset_scan module needs to be informed when the bed mesh scan is completing a new scan, since the Z offset needs to be set to 0 in that case (any Z offset correction previously determined by the z_offset_scan module is then properly corrected by the new mesh already). Also it would ideally require knowledge of the profile management, as the Z offset may need to be managed per profile (I still have to think of the implications in detail here...).

Would such change be ok? Of course the bed mesh module still need to work without the z_offset_scan module. Or maybe the entire feature can be integrated into the bed mesh module.

What do you think?

@KevinOConnor KevinOConnor removed the Stale label Jan 4, 2021
@KevinOConnor KevinOConnor reopened this Jan 4, 2021
@KevinOConnor
Copy link
Collaborator

I have implemented a feature (in a separate module called z_offset_scan) which allows to remeasure the overall Z offset at a single point.

How does that differ from the existing PROBE_CALIBRATE command?

The z_offset_scan module needs to be informed when the bed mesh scan is completing a new scan, since the Z offset needs to be set to 0 in that case

The bed mesh code works by altering the coordinates of the incoming stream of g-code commands. When it does a scan, it always uses the coordinate system defined in the config file, and is thus not impacted by a mesh or other form of coordinate translation. Also, the bed mesh code already has a mechanism to apply (or not apply) the probe's z offset - it's the relative_reference_index config option.

-Kevin

@mhier
Copy link
Contributor Author

mhier commented Jan 5, 2021

Thanks for reopening 👍

How does that differ from the existing PROBE_CALIBRATE command?

In my understanding (which might be incorrect), PROBE_CALIBRATE calibrates the absolute probe offset before performing a bed mesh scan, involving a MANUAL_PROBE like procedure. The Z offset scan still relies on a properly calibrated probe and is fully automated. It will effectively update an existing bed mesh using a single measurement point. This is mostly a speed up, a Z offset scan can be replaced by a full bed mesh scan.

In addition, the Z offset scan also allows to measure the Z offset after homing (without updating the config). This can be used e.g. if the Z endstop is not precise (statistical error), or for milling (tool length sensor).

So if my understanding of the PROBE_CALIBRATE command, this is something completely different.

The z_offset_scan module needs to be informed when the bed mesh scan is completing a new scan, since the Z offset needs to be set to 0 in that case

The bed mesh code works by altering the coordinates of the incoming stream of g-code commands. When it does a scan, it always uses the coordinate system defined in the config file, and is thus not impacted by a mesh or other form of coordinate translation. Also, the bed mesh code already has a mechanism to apply (or not apply) the probe's z offset - it's the relative_reference_index config option.

Yes, I know. The problem is the other way round: The z offset scan module also implements a coordinate transform (same mechanism), which alters the Z coordinate and then passes on the transformed coordinates to the bed mesh module. Also when performing the Z offset scan, it will subtract the bed mesh correction value for the used XY position to avoid that part of the correction being applied twice.

Maybe the best way to implement this would be to avoid adding the second coordinate transform and instead alter the bed mesh itself. This would nicely play along with switching mesh profiles and intrinsically provide the correct behaviour if a new mesh is scanned. It still probably will require to alter the bed mesh module, since it has to alter the mesh also on the fly (for the milling case resp. bad endstop case).

@KevinOConnor
Copy link
Collaborator

I'm not really sure what you're trying to do. Lets see if we can walk through it.

In my understanding (which might be incorrect), PROBE_CALIBRATE calibrates the absolute probe offset before performing a bed mesh scan,

The PROBE_CALIBRATE command is a tool for finding the probe's z_offset. The z_offset is the distance, in millimetres, between the bed and nozzle when the probe "triggers". This value is intended to be a physical constant which is not really related to bed mesh.

The Z offset scan still relies on a properly calibrated probe and is fully automated. It will effectively update an existing bed mesh using a single measurement point.

Okay, that sounds more like Z_ENDSTOP_CALIBRATE. That tool is intended to find the distance, in millimetres, between the nozzle and the bed when the z endstop "triggers". It too is intended to be a physical constant.

In addition, the Z offset scan also allows to measure the Z offset after homing (without updating the config). This can be used e.g. if the Z endstop is not precise (statistical error), or for milling (tool length sensor).

Are you saying that the probe is more accurate than the endstop switch, but you can't use the probe for Z homing? If so, I'd probably handle that with a homing_override config section (and/or a series of macros). That is, a procedure that does something like: 1) home using Z endstop, 2) probe using Z probe, 3) call SET_KINEMATIC_POSITION Z=... to update the printer with a more accurate Z position.

The z offset scan module also implements a coordinate transform (same mechanism)

Okay, but I don't understand what that module is trying to accomplish.

-Kevin

@mhier
Copy link
Contributor Author

mhier commented Jan 7, 2021

Ok, maybe the term "z_offset" has a different meaning for me, or in other words: I should find a different name :-)

The PROBE_CALIBRATE command is a tool for finding the probe's z_offset. The z_offset is the distance, in millimetres, between the bed and nozzle when the probe "triggers". This value is intended to be a physical constant which is not really related to bed mesh.

With a load-cell based probe, this z_offset is always 0, since the nozzle is the tip of the probe. At least the deviation from 0 should be much less than it would be possible to measure with a paper test. If this is different in another hardware setup, this is a different story (I do not intend to cover this now).

The Z offset scan still relies on a properly calibrated probe and is fully automated. It will effectively update an existing bed mesh using a single measurement point.

Okay, that sounds more like Z_ENDSTOP_CALIBRATE. That tool is intended to find the distance, in millimetres, between the nozzle and the bed when the z endstop "triggers". It too is intended to be a physical constant.

Yes, this is closely related. In my understanding the Z_ENDSTOP_CALIBRATE is:

  1. manual
  2. changes the Z value the printer assumes right after homing, hence it does not allow to correct the Z value without performing another homing operation
  3. probably requires to perform a new bed mesh scan after the endstop calibration has been done.

These 3 points a the main difference.

In addition, the Z offset scan also allows to measure the Z offset after homing (without updating the config). This can be used e.g. if the Z endstop is not precise (statistical error), or for milling (tool length sensor).

Are you saying that the probe is more accurate than the endstop switch, but you can't use the probe for Z homing? If so, I'd probably handle that with a homing_override config section (and/or a series of macros). That is, a procedure that does something like: 1) home using Z endstop, 2) probe using Z probe, 3) call SET_KINEMATIC_POSITION Z=... to update the printer with a more accurate Z position.

I still need to have some command which determines the new Z=0 position (contact point of bed and nozzle) to be able to write this kind of macro. Maybe this points me to a better solution for the implementation of the Z correction.

So I could change my new command (after finding a better name ;-)) such that it merely searches for the new Z=0 position and stops there. The user then has the option to call SET_KINEMATIC_POSITION Z=0 (or whatever Z coordinate he likes to have there). This would allow the "online" correction.

Is there an option to perform a Z_ENDSTOP_CALIBRATE using an automatic probe? If not, maybe the preferred way would be to implement that?

I think, then with these two solutions all use cases should be covered, and the implementation is much more "klippery" ;-)

@KevinOConnor
Copy link
Collaborator

FYI, there was a recent discussion on this at #3741 (comment) . @dmbutyugin - you may be interested in the comments at #3496 (comment) .

With a load-cell based probe, this z_offset is always 0, since the nozzle is the tip of the probe. At least the deviation from 0 should be much less than it would be possible to measure with a paper test.

FYI, every contact probe I've seen required a negative z_offset - that is, it always depresses the bed a small amount prior to the signal being received and processed.

Last I looked, I was able to calibrate heights to around the 10micron range using the "paper test". That should be accurate enough to detect and measure a negative z offset.

-Kevin

@dmbutyugin
Copy link
Collaborator

Thanks, Kevin.

With a load-cell based probe, this z_offset is always 0, since the nozzle is the tip of the probe. At least the deviation from 0 should be much less than it would be possible to measure with a paper test.

FYI, every contact probe I've seen required a negative z_offset - that is, it always depresses the bed a small amount prior to the signal being received and processed.

In principle, I can second that. However, when I used a paper test with PROBE_CALIBRATE to calibrate the 'offset' of ADXL345 probe vs the nozzle, I was getting results around -0.11..-0.09 mm, which are awfully close to the thickness of the paper sheet I was using for calibration (I measured it to be equal to 0.1mm +- 0.01mm with a micrometer). It's always necessary to leave some room for thermal expansion (and z_offset of the probe is likely the most straightforward way to do that). But it would seem that in this case the 'probe' has very little offset of its own vs the tip of the nozzle.

Is there an option to perform a Z_ENDSTOP_CALIBRATE using an automatic probe? If not, maybe the preferred way would be to implement that?

It also makes sense to have something like that for ADXL345-as-a-probe. The motivation is similar to the reasoning given in this ticket: due to the need to prepare the nozzle for calibration (clean the tip from the filament residue), it is less practical to use ADXL345 as a Z endstop, but the sensor might be precise enough to re-calibrate the bed level should something change in the system.

So, my proposal would be to really implement something like that in Klipper. I made an implementation of my own for ADXL345 as BED_OFFSET_CALIBRATE command, but that implementation is very crude - it simply recalculates the new Z endstop offset and allows the user to save it using SAVE_CONFIG. Ideally the implementation should allow online adjustment of the bed offset too. In principle, it would probably also be fine if that could be done using some macro (assuming the config values can be altered from within the macro too), e.g. if a PROBE command can return a value that can be used in a macro.

@mhier
Copy link
Contributor Author

mhier commented Jan 25, 2021

FYI, every contact probe I've seen required a negative z_offset - that is, it always depresses the bed a small amount prior to the signal being received and processed.

I am measuring the force in dependence of Z, starting from a rough estimate of the contact position into negative Z direction (for a small distance only, of course). Then I am making a linear fit, which allows me to extrapolate the position with zero force. Hence I do not expect a systematic offset for this probe, as long as the force is linear with the Z depth (it usually should be, unless e.g. one uses a custom build plate on top of the original heat bed and it is not perfectly attached - I have tried to minimise the impact of this as well, but it wouldn't be 0).

On the other hand, I have to say that one of my testers is seeing a small, negative Z offset (ca. 0.1mm, potentially less). He is using a relatively soft surface (Pertinax, phenolic paper), maybe this makes a difference. We are still investigating where this is coming from. He is anyway seeing some discrepancy between his different methods to measure this offset, so I am not really sure what is going on.

I have checked the Z offset (on my printer) with 3 methods: "paper test", measure thickness of a printed brim/skirt, and visual inspection of the air gap between nozzle and bed (lower nozzle until the light of a LED right behind the nozzle is completely blocked). For me all three methods perfectly match within their precision (measure brim/skirt is worst, visual air gap is best), and I do not see any significant discrepancy from what the load cell probe is measuring.

In any case, we could introduce a static offset to correct our load cell or accelerometer based probes, if this seems necessary. One would have to determine this offset with a "paper test" or similar approach (and keep in mind it will likely depend on the bed surface material). This has to be done only once. A BED_OFFSET_CALIBRATE will be used much more frequently, e.g. whenever one exchanges a nozzle or even the entire hotend one would quickly use this command to correct for the changed distance.

@dmbutyugin I assume we can use the same code for the BED_OFFSET_CALIBRATE command for our two probe modules, am I correct? Can you point me to your code, so I can test it with my load cell probe?

@dmbutyugin
Copy link
Collaborator

Yes, the code is not very elaborate and can be found here.

@mhier
Copy link
Contributor Author

mhier commented Jan 26, 2021

@dmbutyugin Looks good. I think the approach to change the position_endstop is actually the best (see the discussion with Kevin above). I would suggest we create a separate module out of it. I also thought about unifying it with the manual probe implementation, but I think it's not really worth it - it's only sharing the z_endstop_finalize() function, which is only a few lines of code. What do you think? Will you do it and create a separate pull request for it? It's a bit pointless without our probe implementations, but getting this right is mostly what is missing for the load cell based probe, so I would hopefully be able to make the pull request soon after. (I can also take it and move it to a separate module myself, if you prefer...)

@mhier
Copy link
Contributor Author

mhier commented Feb 8, 2022

Waiting for this feature in klipepr too

Feel free to test it from my branch (see the PR). Let me know if you need help to set it up etc. What hardware do you have?

@yzheka
Copy link

yzheka commented Feb 8, 2022

@mhier I have anycubic kossel linear plus and 3 load cells 1kg rated. I will try your branch once i am ready with cells mounted.

@mhier
Copy link
Contributor Author

mhier commented Feb 8, 2022

@yzheka Question is also how you read out the load cells. Currently, my branch only supports the specific hardware setup of my printer. Luckily, the schematics of my printer's electronics is openly available:

https://asset.conrad.com/media10/add/160267/c1/-/en/001197630CD01/circuit-diagramm-1197630-main-board-i851ctc-v21-completely-suitable-for-3d-printer-renkforce-rf1000.pdf

It is in German language, sorry about that :-) The important part is on the second page top left ("DMS Verstärker und A/D").

I am also working on a support for the widely known HX711, but it seems to be not really trivial...

@yzheka
Copy link

yzheka commented Feb 8, 2022

@mhier I thought using your branch I can wire amplifier board digital pin and use it as probe.

@mhier
Copy link
Contributor Author

mhier commented Feb 8, 2022

Just digital on-off information is not sufficient, the result would be too imprecise. We need the analog signal. Also the onboard ADCs in the MCUs are typically not good enough, hence you likely need to use an external ADC.

@yzheka
Copy link

yzheka commented Feb 8, 2022

@mhier hmm, i guess use low cost Arduino to convert analog weight value to digital signal (like probe) will be easier.

@mhier
Copy link
Contributor Author

mhier commented Feb 8, 2022

@mhier hmm, i guess use low cost Arduino to convert analog weight value to digital signal (like probe) will be easier.

As I said, the on-chip ADCs in most microcontrollers are not good enough. You will need at least 16 bit resolution and differential input. Speed on the other hand does not matter. Not sure if you find this on a low-cost arduino... (if yes, let me know!)

@garethky
Copy link
Contributor

If I understand the PR correctly what you are doing is moving the toolhead to a new position and then taking a measurement from the load cell. This isn't really ideal but I understand why you had to do it that way. You cant use the endstop system on the MCU because that's digital, the 12 bit ADCs on the MCU aren't workable, and the code on the Pi takes too long to run to be interactive with the MCU. So move in small increments and take measurements after each movement is completed.

How long does one of these probe cycles take? What about doing a 5x5 bed mesh with 3 probes per point?

It seems like we need a board that can do the 16 bit ADC, run the detection algorithm and send a digital output signal to the MCU to trigger the endstop pin. The detector settings can be tuned from Klipper but the detection code doesn't run in Python inside Klippy, it happens on the board in C. The existing probing code based on endstops would just work as-is. (My guess is this is how touch probes integrate with a CNC's PLC).

@mhier
Copy link
Contributor Author

mhier commented Feb 17, 2022

If I understand the PR correctly what you are doing is moving the toolhead to a new position and then taking a measurement from the load cell.

Correct.

the 12 bit ADCs on the MCU aren't workable

Even if they were, the converted value still would have to go through some more or less complicated algorithm, at which end a decision is made what to do next. Applying a simple threshold on the measured ADC value is far too imprecise and unreliable.

How long does one of these probe cycles take?

I don't have an exact time right now (I can measure it this evening if it helps). It is in the order of half a minute or so, depending how far one choses the initial distance from the bed. The total scan of a 5x5 mesh hence takes maybe 15 minutes or so. This is long compared to other probe systems, but I do not consider this a big problem, as one does not have to do this scan very often.

It seems like we need a board that can do the 16 bit ADC

I think it does not matter whether the ADC is integrated into the MCU or it is attached via I2C or SPI or so. If we do not want to put complex algorithms into the MCU code, there are some speed limitations to live with. Still, the original firmware of my printer was Repetier-based (so everything was in the MCU) and had already support for the load-cell-based probe (this is a feature my printer was shipped with by the manufacturer). The scan with that firmware was not faster. The time per point was a bit shorter, but it had to do more points, since the mesh interpolation was merely bilinear (due to limitations of what you can put into the small MCU - we have an ATMega2560 only).

I see one potential for improvement though: If we could schedule the I2C/SPI communication with the external ADC (or internal ADC measurements if the MCU has a good enough one), the algorithm could avoid waiting for the result. It could schedule alternating movements and measurements and do the computations a bit delayed. This would of course mean that especially during the first "fast" approach the head moves further "into" the bed until the Python part can react, so the speed needs to be slow enough such that this is acceptable.

@garethky
Copy link
Contributor

I want to get the probing time down to where it is with other methods so we can bed mesh at print start. We have found this to be more reliable than storing the mesh. I see load cell based probing as the way forward in FDM. First layer reliability is the biggest hurdle for new users and load cell probes on the tool head can virtually eliminate the machine side of the problem (surface prep and temperature are still an issue).

I guess we will see later this year when Prusa releases the ECAD files and code for the XL exactly how they are doing it. They claimed it would work with Klipper. In the meantime I feel your pain on trying to find a ready made dev board that would do the 16+ bit ADC, provide some compute and I/O pins. Seems like the simplest solutions is a HX711 breakout board + Arduino. Maybe later someone can make that into a RPI hat.

@mhier
Copy link
Contributor Author

mhier commented Feb 18, 2022

In my experience, it is sufficient to make a single point scan rather frequently (e.g. before every print), and shift the entire mesh based on that scan. My printer is build rather solid, so maybe this is different for other printers, but I think it would be worth trying. On my printer, I have no second guesses about the first layer at all any more, it just works even without the slightest manual adjustment. The single point scan is possible with my PR already. I have described this here:

https://github.com/RF1000community/klipper/blob/master/docs/LoadCellProbe.md#best-practises

My approach would be to get this PR through first and then see how to improve the speed. The integration of an HX711 is also not so trivial, see my forum post here: https://klipper.discourse.group/t/hx711-support-load-cell-sensor/2057

I am confident that all this can be done. My PR is waiting for quite some time already, because it is already quite complex. I don't want to increase complexity by even adding more right now...

@garethky
Copy link
Contributor

Ok, some hard stuff is left to do. The right end state seems to be:

  • ADC connected to an MCU over SPI (seems like the hard part right now)
  • A module is added to the MCU code to interact with the ADC, providing the following functionality:
    • Apply ADC settings from klippy (gain, sample rate etc.)
    • Send raw ADC data back to klippy from the MCU, possibly at some reduced sample rate
    • Configure a Trigger to occur when the next value from the ADC is sampled above the trigger setpoint.
    • Disable/Reset the Trigger

Then in Klippy we can:

  • Consume the raw ADC data and make it available to klippy modules for all the use cases that don't require instant stepper respnse. (weight of the print, extruder pressure/blockage detection, nozzle collision detection, live print tuning etc.)
  • For probing, set up a specific trigger setpoint for an individual probing move. This can take into account all factors klippy is aware of (speed and direction of the move, current correction for temperature drift etc.). Then we can use stepper_stop_on_trigger to perform the homing move and trigger from the ADC on the MCU to stop the move.

Job 1 is getting some ADC to work directly with the MCU over SPI. The HX711 has been brought up numerous times but you have found its SPI communications to be an issue. I trust your investigation. What about using one of the TI chips like ADS1220 or ADS ADS1262? (e.g. https://protocentral.com/product/protocentral-ads1262-32-bit-precision-adc-breakout-board/). If we pick a chip that makes out lives easier to implement the SPI communication that's a win. Programmer time and code complexity cost more than hardware.

@mhier
Copy link
Contributor Author

mhier commented Feb 21, 2022

There is already such module in my branch:

https://github.com/RF1000community/klipper/blob/master/klippy/extras/ads1100.py

I though I will send this change as a PR first as it is the more generic part, while the ADS1100 support does not make really much sense without this module. I have also even created a temporary hx711 module with the same interface, but more work is required to get a reliable communication.

* ADC connected to an MCU over SPI (seems like the hard part right now)

So this part is done for my hardware, but still needs to be done for more generic hardware (like the HX711).

Btw: some posts above I posted a link to the schematics of my printer's mainboard including the load cell amplifier and digitiser circuit using an ADS1100. If you reproduce that exactly, you should be able to use my code right away.

* For probing, set up a specific trigger setpoint for an individual probing move. This can take into account all factors klippy is aware of (speed and direction of the move, current correction for temperature drift etc.). Then we can use `stepper_stop_on_trigger` to perform the homing move and trigger from the ADC on the MCU to stop the move.

As I said, this is generally insufficient for probing. The algorithm needs to be a lot more complex than a simple threshold. Otherwise you would at least lose the big benefit of an absolute measurement (without offset to calibrate manually), if not degrade the precision beyond usability. I have tried that out already.

I have another idea how to improve the speed:

  • Write an MCU driver for whatever ADC is used (ADS1100, HX711 whatever) which ships a stream of ADC data with timestamps to the Python side.
  • Python code receives stream. Since data is timestamped, it can understand at which Z position the printer was and if it was moving or standing (readouts are MUCH more reliable when everything is at rest)
  • Algorithm needs to be changed such that it can plan measurements a few moves and ADC reads "ahead".

This will probably blow up the complexity of implementation by a lot. That's why I would prefer doing this in a second step only. I consider speed not so problematic, as this is mostly a matter of organising the workflow. If I require a new bed leveling (which is not very often), I will start it before I slice the model. When I am done with slicing, the leveling is done as well usually. The speed is really not that much of an issue here.

@garethky
Copy link
Contributor

Ok, I will order a selection of these ADC boards that I can easily get (ADS1115, ADS1232, ADS1263 & HX711) and a couple of load cells. Once I get my test rig I hope I can help with this effort.

Because of the 25ms inter-MCU delay I'm really focused on getting this to work as an endstop inside the MCU driving the steppers. Its a great idea to do post-processing of the collision event in Klippy for more accuracy using your linear_regression method. That's very similar to the spirit of what what multi-mcu homing does. I believe we are thinking along similar lines but its hard to explain with just words. But bottom line, that I think we both can see, is the MCU needs to stop the move on its own to speed things up. If we can get this solved I'm sure we will have a lot of contributors that will write the Python side for probing, auto-calibration, jam detection etc.

I think I'll start a discussion on to the forum, I'll tag you when I get around to posting (might take me 2-3 weeks to get there).

@Heltzen
Copy link

Heltzen commented Feb 21, 2022

I have three factory build 3Dgence printers running a load cell to do mesh bed leveling.
3DGence One, 3DGence P255 and 3DGence Industry F340.

I am new to klipper, but not 3D printning (doing it professional for 5 years)

If you need to do some testing i am willing to help.
The printers need a firmware update, but am hesitating to change the firmware since I do not know if I can continue to use the auto Calibration feature. The printers ar so rigid build, and well assembled that there is no manual bed leveling.

@mhier
Copy link
Contributor Author

mhier commented Feb 22, 2022

Its a great idea to do post-processing of the collision event in Klippy for more accuracy using your linear_regression method.

You may miss one detail here: The linear regression is not done on direct measurements, because this is way to inaccurate. Instead every single measurement point going into the regression has to be zero-compensated with another measurement without bed contact, and this other measurement has to be right before or right after the measurement itself. To do the linear regression, the algorithm is hence "hopping" in z direction multiple times up and down. This cannot be done with a simple homing move. On top of this, the motors should stand still while taking the measurement (both the actual measurement and the zero-compensation measurement). Hence movement needs to be controlled in synchronisation with the load cell measurements.

Sill I see potential to use your idea: The algorithm currently consists of 4 phases:

  1. "fast" rough approach
  2. verify bed contact (if not, go back to 1)
  3. backup head a bit to a position as close as possible above the bed
  4. measurements for linear regression

Step 1 could be replaced with a homing move, since this is essentially a plain threshold. We still need to subtract a zero measurement from the measurements, but for this rough approach it is sufficient to take a single zero measurement before starting the move. For the second step we have to take another zero measurement to confirm that our measurements are not just drifting away (e.g. due to mechanical tension or thermal effects), but this is a quick single measurement (-> can be done fully from Python).

Maybe also Step 3 could be replaced with an inverted homing move with slower speed. Here it is only important that we end up reliably at a position without bed contact but as close as possible to the bed to safe time in the last step.

For Step 4 a homing move cannot be used, but scheduled ADC measurements could help. So if we can make our MCU ADC driver(s) capable of providing a virtual endstop based on a threshold and also to perform ADC measurements synchronised to the movements, we probably can speed this algorithm up a by a lot.

One more thing comes to my mind now: A simple threshold on the ADC measurement for the virtual endstop should be fine, because the direction in which the ADC measurement should change is known (if the load cells are correctly mounted it will go into the negative direction, as the force is moving against the weight - we could make the expected direction configurable). Hence the threshold can be computed to take into account the zero measurement. So we actually need no zero compensation in the MCU.

Btw: Some things in my algorithm may be motivated by my particular hardware setup. The ADS1100 can only do 8 Hz conversion rate (at full 16 bit resolution). This is a strong limit. Even with the original Repetier-based firmware the probing was not really faster. If the ADC gets swapped with an 80 Hz capable HX711, things will be very different. But even for the ADS1100 I see potential, since resolution can be traded for speed. So I think it is definitively worth improving this!

If we can get this solved I'm sure we will have a lot of contributors that will write the Python side for probing, auto-calibration, jam detection etc.

Indeed, there are many nice things one can do with a load cell sensor, if it is properly mounted. In my printer's case it can measure the extrusion force, so we can do interesting things like compensating for the thermal elongation of the hotend during the first layer. I have implemented this already:

https://github.com/RF1000community/klipper/blob/master/klippy/extras/z_sense_offset.py

In the old Repetier-based firmware for my printer, our community as developed many other ideas which could be re-implemented on Klipper as well. For most of these ideas an asynchronous readout (w.r.t. the movements) is sufficient, but for many a synchronisation would help a lot.

I think I'll start a discussion on to the forum, I'll tag you when I get around to posting (might take me 2-3 weeks to get there).

Perfect! I am happy if this project finally gets some movement :-) I will try to make a proper HX711 driver in the MCU first. I have already done some investigation in this direction, and the HX711 anyway needs special MCU code (see https://klipper.discourse.group/t/hx711-support-load-cell-sensor/2057/4). Hence I think this is a good way to dive into writing an MCU driver for me - with the aim to support the above mentioned features in the end.

Still I would recommend you to try out getting the physical setup to work with the current implementation at first. There are a couple of people using this module in their production printers already (including me), but all are using the same printer model series. So it would be good to verify first the implementation on another physical setup, before we try to improve it.

@mhier
Copy link
Contributor Author

mhier commented Feb 22, 2022

I have three factory build 3Dgence printers running a load cell to do mesh bed leveling. 3DGence One, 3DGence P255 and 3DGence Industry F340.

Do you know what ADC chip the printers are using to read out the load cell? The first thing to do is to write a driver for it. Once that is done you can try this algorithm - you will at least have to tune a number of parameters to get it working reliably with your printer setup.

@D4SK
Copy link
Contributor

D4SK commented Mar 5, 2022

@mhier I recently wrote a module using mcu timers to communicate to the hx711 for this purpose. It just needs some integration to work with the probing code. Maybe we can collaborate on this. My discord is Konstantin#3476.

See D4SK@e376340
This reliably obtains adc values at 80hz, and provides calibration options for gain etc.

@mhier
Copy link
Contributor Author

mhier commented Mar 7, 2022

@D4SK That looks great, thanks! I will try to get this working with my probing code as soon as possible (after my vacation ;-)). I will contact you (probably not via discord, since I am not really active there - are you in the discourse forum? My nick is mhier there, too) once I have done the first steps.

Does this module already report the correct time stamp of the read out to the Python side?

@D4SK
Copy link
Contributor

D4SK commented Mar 7, 2022

@mhier It does report a time stamp, maybe not the correct one though. It just uses the #sent_time parameter from the mcu message, and converts it with estimated_print_time()

I think a continuous single probing approach is also very interesting. If the timestamps exist, the only problem left should
be finding the past stepper position for the timestamps.

@github-actions
Copy link

Hello,

It looks like there hasn't been any recent updates on this
Klipper github issue. If you created this issue and no
longer consider it open, then please login to github and
close the issue. Otherwise, if there is no further activity
on this thread then it will be automatically closed in a few
days.

Best regards,

~ Your friendly GitIssueBot

PS: I'm just an automated script, not a human being.

@github-actions github-actions bot added the Stale label Apr 12, 2022
@mhier
Copy link
Contributor Author

mhier commented Apr 12, 2022

There was some discussion in the PR #4738. I will have to split up the PR in smaller, more handleable pieces.

@github-actions github-actions bot removed the Stale label Apr 12, 2022
@D4SK
Copy link
Contributor

D4SK commented Apr 15, 2022

@mhier I recently found out the #sent_time parameter is probably not usable. Seems it is indicating the time the last host request was sent. So its probably best to attach a proper timestamp in the mcu code instead.

@github-actions
Copy link

Hello,

It looks like there hasn't been any recent updates on this
Klipper github issue. If you created this issue and no
longer consider it open, then please login to github and
close the issue. Otherwise, if there is no further activity
on this thread then it will be automatically closed in a few
days.

Best regards,

~ Your friendly GitIssueBot

PS: I'm just an automated script, not a human being.

@github-actions github-actions bot added the Stale label May 20, 2022
@github-actions github-actions bot locked and limited conversation to collaborators Nov 24, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

10 participants