Klipper: Evaluate calculations in gcode_macro

Created on 3 Nov 2018  路  16Comments  路  Source: KevinOConnor/klipper

I would like to save some time while my printer is warming up so I tried to write a gcode macro to preaheat my nozzle while the hotbed is warming up. However, I experience some problems.

My macro looks like this:

[gcode_macro PREHEAT]
gcode:
    M140 S{{BED_TEMP} - {BED_OFFSET}} ; bed
    M104 S{{HOTEND_TEMP} - {HOTEND_OFFSET}} ; hotend
default_parameter_BED_OFFSET: 20
default_parameter_HOTEND_OFFSET: 50

The idea is to call the macro with PREHEAT BED_TEMP=60 HOTEND_TEMP=190 and have the bed heat up to 40掳 and the hotend to 140掳 without waiting for the procedure to finish.

But it seems, I cannot convince klipper to evaluate the calculations {{BED_TEMP} - {BED_OFFSET}} and {{HOTEND_TEMP} - {HOTEND_OFFSET}}.

Is there a way to do that?
If not, can this be a feature to implement?

Most helpful comment

I have code that allows expressions like that in a gcode_macro (I modified the gcode_macro extras module).

However, there are some considerations:

  • {X+Y} with X=1 and Y=2 would result in "12" (because X and Y are strings, where "add" means "concatenate"). Instead {{X}+{Y}} does the job. Curiously you exactly used that syntax. {int(X)+int(Y)} would also work. The code could check, if the string looks like a number and convert it.
  • I replaced the format() method by re.sub(...) which means there are no formatting parameters (something like {X:0.1f}, which would probably only be useful for messages)
  • I think, gcode_macro is not the correct place to define expression interpolation. At least there should be a central function somewhere, because other parts like config files should use the same algorithm to be consistent. To put this in a central place there should probably be more decisions, for example about how global user variables could be handled in the configuration.
  • I would really like to evaluate expressions at run time rather than at configuration load time. This would allow to change these user variables on the fly and the gcode and other places would use the new values when executed after the change.
  • with expressions in gcode_macro, it would be possible to control most printer parameters (it's python). Unfortunately, python statements aren't expressions, so not everything is possible.
  • using more than simple expressions would easily become unreadable. I would prefer a python script, which could be placed in settings called prepare: and action:. These could provide functions to make the interpolation more readable or handle the real action with gcode:omitted.

[EDIT] I added python scripts. While this generally works well, I got some problems to access objects like printer, etc.
I need to explore some weird things about python exec, eval, scopes etc.

All 16 comments

Why not use start gcode in slicer? Slic3r in this case..

M104 S[first_layer_temperature] ; set extruder temp
M140 S[first_layer_bed_temperature] ; set bed temp
M190 S[first_layer_bed_temperature] ; wait for bed temp
M109 S[first_layer_temperature] ; wait for extruder temp

Thank you for your comment, @Hywelmartin!

Actually I try to set my hotend temperatures a bit lower than the target temperatures to prevent oozing while the bed is still warming up. I managed to achieve that in Slic3r PE but I failed in Slic3r (original).

However, I think doing the whole thing in Klipper would be even better, because I wouldn't need to tweak around when I change my Slic3r once in a while. I think this belongs in the firmware and the docs even encourage me to do so (see here: https://github.com/KevinOConnor/klipper/blob/master/docs/Slicers.md#klipper-gcode_macro)

It would indeed be nice to be able do some calculations... how did you do that in Slic3r PE?

Well you have different profiles per material
M104 S150 ; set extruder temp preheat
M140 S[first_layer_bed_temperature] ; set bed temp
M190 S90 ; wait for bed temp assumed 100 as goal
M104 S[first_layer_temperature] ; set extruder temp
M190 S[first_layer_bed_temperature] ; wait for bed temp
M109 S[first_layer_temperature] ; wait for extruder temp

In Slic3r PE I did this:

; Set standby temps without waiting
M140 S{first_layer_bed_temperature[current_extruder] - 20} ; bed
M104 S{first_layer_temperature[current_extruder] - 50} ; hotend

I have code that allows expressions like that in a gcode_macro (I modified the gcode_macro extras module).

However, there are some considerations:

  • {X+Y} with X=1 and Y=2 would result in "12" (because X and Y are strings, where "add" means "concatenate"). Instead {{X}+{Y}} does the job. Curiously you exactly used that syntax. {int(X)+int(Y)} would also work. The code could check, if the string looks like a number and convert it.
  • I replaced the format() method by re.sub(...) which means there are no formatting parameters (something like {X:0.1f}, which would probably only be useful for messages)
  • I think, gcode_macro is not the correct place to define expression interpolation. At least there should be a central function somewhere, because other parts like config files should use the same algorithm to be consistent. To put this in a central place there should probably be more decisions, for example about how global user variables could be handled in the configuration.
  • I would really like to evaluate expressions at run time rather than at configuration load time. This would allow to change these user variables on the fly and the gcode and other places would use the new values when executed after the change.
  • with expressions in gcode_macro, it would be possible to control most printer parameters (it's python). Unfortunately, python statements aren't expressions, so not everything is possible.
  • using more than simple expressions would easily become unreadable. I would prefer a python script, which could be placed in settings called prepare: and action:. These could provide functions to make the interpolation more readable or handle the real action with gcode:omitted.

[EDIT] I added python scripts. While this generally works well, I got some problems to access objects like printer, etc.
I need to explore some weird things about python exec, eval, scopes etc.

@hg42 I really appreciate your considerations and I would find it most useful if some of them (if not all) flow into upstream.

You are also right about gcode_macros are probably not the best place for those expressions. However, those expressions would need to be callable from gcode (macros).

I also think it's not really necessary to invent a new scripting language - python should be easy enough for the aspiring user...

In my opinion adding something like this is urgent, because it would give the advanced user much more control over their printer.

gcode_macros are probably not the best place for those expressions. However, those expressions would need to be callable from gcode (macros)

when I said it's not the correct place, I was talking about the part that evaluates and interpolates these expressions (template system) and the variables that are available inside these expressions.

The implementation of the template system is currently buried in gcode_macros because it's the first module that uses it and I am using this very short module as a test bed.
So, I don't see it as the final version of the gcode_macro module.
The template system should be moved to a central place and imported to other places.
It should be a separate module and could either be placed into another extras module (allowing different implementations which will help development). Or it could be moved to the code base, when it's becoming mature enough.

Meanwhile, I have gained more insights into python scope rules (coming from perl, ruby, javascript they are a bit unexpected).
Currently, the python and gcode scripts work on the same scope containing the gcode macro parameters and some general variables like config, printer, gcode (may be toolhead, etc).
The template system should probably define some standard variables, that can be used everywhere and allow the client module to define local variables like the macro parameters.

I'm currently exploring the available variables, their scopes and their livetime. I want to get this right, before creating a PR.
Then I would split the module into "template" and "gcode_macro".
I also need to explore, if the Klipper module system already allows to import modules to a different name (or define an alias, e.g. using a gcode_macro_with_expressions.py as implementation for a gcode_macro section, while not breaking autoload). If not, I could eventually create another extras module "alias" to provide this.

unfortunately the python code settings do not work as expected, because indentation isn't preserved in the multiline values.

the config parser strips leading spaces, I am using a workaround now, like this:

init:
  | def f(x):
  |     return x*x

@speendo - evaluation of expressions is not currently supported.

@hg42 - Just my 2 cents, but I would caution against "programming in the config file". If python code needs to be written I would recommend one create an "extras" module.

-Kevin

@KevinOConnor

I would caution against "programming in the config file"

I know you said that before...

However, I see people somewhere between pure users and software developers, that can understand and use simple expressions or small scripts based on clean and obvious interfaces.
They don't want to use or learn higher concepts like classes etc.
They usually work on the configuration or values in commands.
You are already seeing a lot of requests about these topics. It's all about configuration, gcode etc.

Even a non-technical person will notice the use of the same value at several places (=> user config variables).
Many might see relations between values that could be expressed by a simple formula (=> expressions).

The purpose of the python parts is interfacing to internals or simple functions that can be used in the gcode.

What do you mean with "caution", would you reject a PR like that?

As a bit of a hacker I quite like the idea of being able to do simple macros & variables too. But perhaps it isn't right to keep it in the config file - it's getting pretty large as it is ;-)

How about having a "printer.macros" (or similar) as a separate file which will be processed if present?

I guess kind of include would be nice anyways.
An easier way would be to read all files in a directory, say printer.cfg.d as it would be done on Debian.

I would probably put each section into it's own file.
This way I could easily compare stepper_x.cfg with stepper_y.cfg

include could be super nice for making it easy to version control your config files without symlinks etc... then to switch configs it could be as simple as a git checkout w/e and you would do an include from the ~/printer.cfg into ~/i3-mk3/printer.cfg and from there break it into fine grain files?

It looks like the original question was answered (there is no support for expressions in gcode_macro). If someone wishes to add support, feel free to open a pull request with the updated code. I'm going to close this for now as it looks like the topic has gone idle.

-Kevin

Was this page helpful?
0 / 5 - 0 ratings

Related issues

CHILLYSMOKES picture CHILLYSMOKES  路  5Comments

KevinOConnor picture KevinOConnor  路  6Comments

ChiliApple picture ChiliApple  路  4Comments

talfari picture talfari  路  5Comments

SergeyKrutyko picture SergeyKrutyko  路  6Comments