Macro Creation Tutorial

Is it possible to test for 2 if statements in a macro, like you would use elif in python?
So for instance:

{% if  ( printer.toolhead.position.z + 10 ) > 280 %}
   G90
   G0 X150 Y150 Z280 F24000 #Wipe out
{% elif  printer.toolhead.position.z < 40 %}
   G91
   G0 Z1 F24000
   G90
   G0 X150 Y150 Z50 F24000 #Wipe out
{% else %}
   G91
   G0 Z1 F24000
   G90
   G0 X150 Y150 F24000 #Wipe out
   G91
   G0 Z9 F24000
{% endif %}

Lets test this idea:

[gcode_macro TEST]
variable_total : 0
gcode:
    {% set points = range(1,4) %}
    {% for point in points %}
        { action_respond_info("Calling probe on: %i" % (point)) }
        TEST_PROBE POINT={point}
    {% endfor %}
    { action_respond_info("Total is: %s" % (printer['gcode_macro TEST'].total)) }

[gcode_macro TEST_PROBE]
gcode:
    { action_respond_info("Probing: %i" % (params.POINT | int)) }
    SET_GCODE_VARIABLE MACRO=TEST VARIABLE=total VALUE={printer['gcode_macro TEST'].total + 1}

will output:

$ TEST
// Calling probe on: 1
// Calling probe on: 2
// Calling probe on: 3
// Total is: 0
// Probing: 1
// Probing: 2
// Probing: 3

Sad times. Normal intuition about how program flow works does not work in Jinja/klipper. Macros execute completely before the sub-routines called from inside the macro are invoked. Then those are invoked in-order and so on. But where there is a will there is a wayā€¦ a deep dark way :rabbit: :hole:

We can pause the print and use delayed_gcode to monitor the probing until its completed, then resume the print to make use of the result:

[gcode_macro TEST]
variable_total : 0
gcode:
    {% set points = range(1,4) %}
    {% for point in points %}
        { action_respond_info("Calling probe on: %i" % (point)) }
        TEST_PROBE POINT={point}
    {% endfor %}
    PAUSE
    UPDATE_DELAYED_GCODE ID=_TEST_MONITOR DURATION=1

[gcode_macro TEST_PROBE]
gcode:
    { action_respond_info("Probing: %i" % (params.POINT | int)) }
    SET_GCODE_VARIABLE MACRO=TEST VARIABLE=total VALUE={printer['gcode_macro TEST'].total + 1}

[delayed_gcode _TEST_MONITOR]
gcode:
    {% set test_macro = printer['gcode_macro TEST']%}
    {% if test_macro.total >= 3 %}
        UPDATE_DELAYED_GCODE ID=_TEST_MONITOR DURATION=0  # cancel monitor
        { action_respond_info("Total is: %s" % (test_macro.total)) }
        RESUME
    {% endif %}

which outputs:

$ TEST
// Calling probe on: 1
// Calling probe on: 2
// Calling probe on: 3
// Probing: 1
// Probing: 2
// Probing: 3
// action:paused
// Total is: 3
// action:resumed

This has pros and cons. One of the major cons is that PAUSE doesnā€™t stop macro execution, it only stops execution of the GCode file being printed. So you would have to call TEST directly from a line in the gcode file for this to work as expected during a print. The other major downside is it gets very complicated to reason about when coding.

Iā€™ve used this technique this in my heatsoaking code: klipper-voron2.4-config/heatsoak.cfg at mainline Ā· garethky/klipper-voron2.4-config Ā· GitHub
There is a lot more complexity around detecting if you hit the resume or print cancel buttons while paused. The good news is that you can respond to both of those events in the delayed_gcode which makes long running processes cancel-able without a printer restart. More in the readme: klipper-voron2.4-config/heatsoak.readme.md at mainline Ā· garethky/klipper-voron2.4-config Ā· GitHub

1 Like

Thanks so much for replying to my question about if the SET_GCODE_VARIABLE and SAVE_VARIABLE commands in your example above could be used as global scope variables. I deleted my question shortly after writing it (before you responded), because I found another possible way to do this from another of your macro examples using the save_variables command. I will try out that command to see how well it would work. As you say, it is a shame that a macro is not able to do a bed probe and pick up the result in the same macro. That would have made my life a lot simpler.

You just cant get around the execution order problem. Templates are evaluated completely before any sub-routines are run. If a macro is going to provide data you are basically out of luck. You might be better off doing it in Python as a Klippy extension.

Your gcode macro test above is fantastic! It contains all the tricky stuff that I would not have been able to figure out on my own from this tutorial. Using it as a template (since the syntax is greek to me), I was able to create a set of macros that, combined with a gcode print file, will do what I need to create a bed mesh generator with backlash compensation averaging. I average 4 probes, approaching the probe point from 4 directions. Then I will assemble a 15 x 15 point output to the console to cut and past into the printer.cfg. There is still a bit of hand work to process it, but it is manageable. The proof of principle is now done. I will share the code (clunky as it is) once I have it running perfectly. I only have one error that I can not figure out and have yet to find an example. I am trying to concatenate a series of strings to a single string of 15 probe results to print to the console. You can see what I am doing just from the example gcode of one probe point:

G28 Home
G90 ;absolute
G1 Z2 F3000 ;get close to bed
;
GOTO_PROBE_POINT X_Pos=0.001 Y_Pos=134.959
;Probe sequence
PROBE_BED X_Dir=1 Y_Dir=1 
SAVE_Z
PROBE_BED X_Dir=-1 Y_Dir=-1 
SUM_Z
PROBE_BED X_Dir=1 Y_Dir=-1 
SUM_Z
PROBE_BED X_Dir=-1 Y_Dir=1 
AVG_Z Probe_Count=4
SAVE_TO_LINE Repeat=15
PRINT_LINE

I get this error: !! Error evaluating ā€˜gcode_macro SAVE_TO_LINE:gcodeā€™: TypeError: ā€˜strā€™ object cannot be interpreted as an integer

Here are all the relative macros that make it work. The error is just in the SAVE_TO_LINE macro. Everything else is working.

[gcode_macro SAVE_Z]
variable_zavg : 0
gcode:
    # Call after 1st PROBE 
    SET_GCODE_VARIABLE MACRO=SAVE_Z VARIABLE=zavg VALUE={ printer.probe.last_z_result|float }
[gcode_macro SUM_Z]
gcode:
    # Call after 2nd PROBE to n-1th PROBE
    SET_GCODE_VARIABLE MACRO=SAVE_Z VARIABLE=zavg VALUE={printer['gcode_macro SAVE_Z'].zavg + printer.probe.last_z_result|float }
[gcode_macro AVG_Z]
gcode:
    # Call after last PROBE with Probe_Count=2-8
    SUM_Z
    SET_GCODE_VARIABLE MACRO=SAVE_Z VARIABLE=zavg VALUE={printer['gcode_macro SAVE_Z'].zavg / params.PROBE_COUNT|float }


[gcode_macro CLR_LINE]
variable_row_line : ""
gcode:
    SET_GCODE_VARIABLE MACRO=CLR_LINE VARIABLE=row_line VALUE=""
[gcode_macro PRINT_LINE]
gcode:
    M118 {printer['gcode_macro CLR_LINE'].row_line}
    CLR_LINE

[gcode_macro SAVE_TO_LINE]
gcode:
    {% set count = range(1,params.REPEAT) %}
    {% for point in count %}
    SET_GCODE_VARIABLE MACRO=CLR_LINE VARIABLE=row_line VALUE={printer['gcode_macro CLR_LINE'].row_line|string + ", " + printer['gcode_macro SAVE_Z'].zavg|round(5)|string }
    {% endfor %}

Thanks again for the help you gave that is making this possible. I appreciate any insites on the correct syntax to get rid of this last error.

In your SAVE_TO_LINE macro, try changing the first line to:
{% set count = range(1,params.REPEAT|int) %}

1 Like

Thank you. That was a stupid error on my part. I was looking for the error on the wrong line. I took a more systematic debug process now. After dozens of attempts at still trying to append text to a string variable, I have made no progress in getting past this error message:

!! Malformed command 'SET_GCODE_VARIABLE MACRO=CLR_LINE VARIABLE=row_line VALUE=, 0.08487'

The same command can save numbers, but seems to choke on strings. Perhaps I do not understand some syntax difference for strings. It is the only thing now keeping me from printing.

Here is the stripped down and expanded debug code for the macros in question:

[gcode_macro CLR_LINE]
variable_row_line : ""
gcode:
    #SET_GCODE_VARIABLE MACRO=CLR_LINE VARIABLE=row_line VALUE="" # this blows up with internal error unexpected EOF

[gcode_macro SAVE_TO_LINE]
gcode:
    {% set TEMPA = printer['gcode_macro CLR_LINE'].row_line %}
    {% set TEMPB = printer['gcode_macro SAVE_Z'].zavg|round(5)|string %}
    {% set TEMPC = TEMPA + ", " + TEMPB %}
    M118 { TEMPC } #debug -- good to here
    SET_GCODE_VARIABLE MACRO=CLR_LINE VARIABLE=row_line VALUE={ TEMPC } # this errors with malformed command

I really appreciate your help with learning how to do this.

The value of your VALUE parameter contains a comma and a space so you need to wrap it in quotes. Try this:

SET_GCODE_VARIABLE MACRO=CLR_LINE VARIABLE=row_line VALUE='"{ TEMPC }"'
1 Like

Thank you so much! That was the trick and everything is working now. Now to flush out the gcode and complete the bed leveling experiments.

1 Like

Okā€¦ questionā€¦

Say I have a macro definition and I want to pass a parameter to it. I imagine it something like this:

TEST X

X IS the parameter and value you could sayā€¦

I played around with this:

Function:
[gcode_macro TEST]
gcode:
{ action_respond_info(ā€œOUTPUT: {}ā€.format(params)) }

Call made: TEST XYZ=1

and the output is: ā€˜OUTPUT: {ā€˜XYZā€™: ā€˜1ā€™}ā€™ ā€¦ the arrayā€¦ againā€¦ not what im looking for.

I suppose one way you could look at it as I want to pass a parameter name only to TEST and not a value along with itā€¦ so in sudo code something like this:

[gcode_macro TEST]
gcode:
{ action_respond_info(ā€œOUTPUT: {}ā€.format(params[0].name)) }

and get the output: ā€˜OUTPUT: Xā€™

How can I accomplish this?

Thanks so much for taking the time to create this resourceā€¦looking forward to more.

Ps. I found Chat gpt to be somewhat knowledgeable about klipper macros, though often uses python rather than jinja. You might find it useful in developing new sections quickly.

Itā€™s funny you mention that. I did that too out of pure curiosity. My impression was that ChatGPT sounds like itā€™s knowledgeable about Klipper macros, but it is actually not. I could not get it to produce even a simple functional macro even after multiple attempts to coach it. Iā€™ve found that with tentpole programming languages like Python and Perl, ChatGPT is capable of producing working code, though will often have bugs and errors. As long as you know the language well enough to recognize them, or at least to formulate prompts that will get ChatGPT to recognize them, you can usually run its generated code through multiple iterations to end up with something functional that does what you intended. Not so with Klipper macros, at least not in my admittedly superficial experience. In addition to mixing python and jinja like you mentioned, it will also create ā€œpseudo-jinjaā€ by recasting python expressions in a jinja-like construction, even though jinja has no corresponding syntax or function. On top of that, Klipper uses a somewhat non-standard implementation of jinja and ChatGPT seems completely unaware of that.

So, all that to say, for all the lurkers out there, please do not try to generate Klipper macros with ChatGPT and then come in here wondering why they donā€™t work.

Last week I had questions from somebody who wanted to make a 15 foot (4.5m) high statue that consisted of interlocking 3D printed PLA segments that had a poured concrete core (which would mass 6 tonnes). He worked it all out with ChatGPT but he wanted peopleā€™s opinions about his assumptions. He got angry when told to talk to somebody who made concrete statues/columns for a living and argued that what came out of ChatGPT seemed right.

I havenā€™t used ChatGPT myself but it seems to produce very confidence inspiring answers.

Ha, yeah interesting to play with. I did the same, showed it some proper macros and it seamed to get its logic together at least partially. As you said, some parts might be right but you need to know what is. It helped me get a randomizing function going. Above all, as you said, it sounds very confident. Yes, good point, donā€™t go trying to pull fully working macros out of Chat-GPT!!! Maybe next iteration.

Hi everyone, maybe you can help me, i have an issue with checking probe status. Looks like this:

[homing_override]
axes: XYZ
gcode:
  QUERY_PROBE
  {% if not printer.probe.last_query %}
  RESPOND TYPE=echo MSG="Probe connected"
  {% else %}
  RESPOND TYPE=echo MSG="Probe docked"
  {% endif %}

it works, but only on second attempt:

22:48:43
echo: Probe docked
22:48:43
probe: TRIGGERED
22:48:43
G28
22:48:39
echo: Probe connected
22:48:39
probe: TRIGGERED
22:48:38
G28

QUERY_PROBE outputs correct status, but ā€œifā€ loop did not catch it in time.

Each change is applied only from the second time.
Tried double QUERY_PROBE, didn`t help.

Macros in Klipper are based on a template generation language. So they are fed a frozen state of the printer and then evaluated against that state. The evaluation of the entire macro ends before the first GCode instruction executes.

So in your case, last_query is empty on the first run. On the second run it contains the results from the first run. Or put another way: the if block runs before QUERY_PROBE.

You could likely resolve this by putting the if blocks in a separate macro and calling that. That new macro call would execute after the QUERY_PROBE call and get the state of the printer after the probe completes.

Are you sure ?
Just tested, and got : !! Macro XXXXXXX called recursively

KevinOConnor wrote : ā€œA macro may not invoke itself (either directly or indirectly).ā€

[gcode_macro START_PRINT]
description: Gcode ran at the start of each print. This is called by the slicer to make per machine config easier and cross_slicer compatible.
gcode:
  {% set BED_TEMP = params.BED_TEMP|default(0)|float %}
  {% set EXTRUDER_TEMP = params.EXTRUDER_TEMP|default(0)|float %}
  {% if EXTRUDER_TEMP == 0 %}
    CANCEL_PRINT
    {action_raise_error("Your extruder temperature was not received from your slicer or was incorrectly set. Check that your code and parameters are accurate and restart the print. It may be helpful to look at the start gcode section of the slicer generated gcode file.")}
  {% else %}
    {% if BED_TEMP == 0 %}
      {action_respond_info("Your bed temperature was not received or is set to 0. If this is not intended, cancel the print and check that your code and parameters are accurate and restart the print.")}
    {% endif %}
    CLEAR_PAUSE
    M117 Preparing to print
    {% if 'bed_mesh' not in printer %}
      {action_respond_info("[bed_mesh] is not enabled. Skipping load profile")}
    {% else %}
      BED_MESH_PROFILE LOAD=default
      {% if bed_mesh.profile_name is none %}
        {action_respond_info("Default mesh profile does not seem to be calibrated and will not effect this print. Please calibrate the mesh to apply it for next time")}
      {% else %}
        {action_respond_info("Loaded default mesh")}
      {% endif %}
    {% endif %}
    M140 S{BED_TEMP} ; set bed temp and dont wait
    G90 ; use absolute coordinates
    M83 ; extruder relative mode
    M220 S100 ; reset speed
    M221 S95 ; THIS GCODE LOWERS EXTRUSION RATE TO .95 ON PRINT START. IF THIS CAUSES ISSUE3S CHANGE IT.
    G28 ; home all with default mesh bed level
    M117 Heating - Careful
    M190 S{BED_TEMP} ; wait for bed final temp
    M104 S{EXTRUDER_TEMP} ; set extruder final temp and dont wait
    M109 S{EXTRUDER_TEMP} ; wait for extruder final temp
    M117 Priming nozzle
    G92 E0.0; reset extrusion distance
    G1 X-80 Y-100 Z0.3 F4000; go to prime arc start
    G3 X80 Y-100 I80 J100 E15 F1500
    G92 E0.0
    G2 X-80 Y-100 I-80 J100 E10 F1000
    G92 E0.0 ; reset extrusion distance
    G10;
    G92 E0.0 ; reset extrusion distance
    G1 Z10 F1000;
    M117 Print started
  {% endif %}

Can anyone help explain why this macro only returns:

Error evaluating 'gcode_macro START_PRINT:gcode': jinja2.exceptions.UndefinedError: 'bed_mesh' is undefined

Iā€™m not really too sure what the issue is. Spent quite a while reading documentation, and still confused what is going wrong.

Thanks for any help.

edit: to clarify, Itā€™s hanging on the if statement that tests if bed_mesh is not in the printer object. The purpose of this is to detect whether or not bed_mesh has been configured and skip the mesh load if it hasnā€™t. However, It just keeps saying that bed_mesh is undefined. Which is weird because Iā€™m not testing the value of bed_mesh but rather whether or not the string ā€˜bed_meshā€™ appears in the printer object array.

No, itā€™s not. It gets past that, and then dies because of

{% if bed_mesh.profile_name is none %}

bed_mesh is not defined, so you canā€™t access it.

EDIT: Also, it would be better to start a new thread for problems like this, not hijack an existing unrelated one.

Sorry I will start a new thread next time. And yes I do see that now. I was tired while trying to fix it so it didnā€™t occur to me. Why would bed_mesh be undefined in this scenario? If I was reading moonraker docs correctly, the profile_name should return the name of the profile loaded in this case ā€˜defaultā€™ or null if there is no saved profile. Am I missing something else?