Gcode macros have their warts and limitations. So I played around with just using Python for the macros.
With resulted in something like this:
[gcode_macro clean_nozzle]
variable_total_wipes: 0
python:
wipe_count = int(params.get('TIMES', 8))
gcode.SAVE_GCODE_STATE(NAME='clean_nozzle_state')
gcode.G90()
gcode.G0(Z=15, F=300)
for wipe in range(wipe_count):
for coordinate in [(275, 4),(235, 4)]:
gcode.G0( X=coordinate[0], Y = coordinate[1] + 0.25 * wipe, Z=9.7, F=12000)
variables.total_wipes += 1
gcode.RESTORE_GCODE_STATE(NAME='clean_nozzle_state')
Why this is useful
- python compilation at klipper start, meaningful errors and line numbers
- immediate gcode evaluation with side effects immediately usable
- almost all string processing is eliminated, resulting in order of magnitude faster evaluation is a nice plus.
How this works
It is similar to normal jinja environment, but:
gcode
- a pseudo object to run Gcodes. Gcodes are executed immediately and printer state updated.- named arguments are passed direclty to the gcode handler
- positional arguments go through the normal gcode parsing, you can still do G0(“X10 Y20 Z30 F40”)
variables
- the gcode macro variables dict, allowing to directly read/change themprinter
- is still there, but now with live updates- dicts are wrapped in pseudo objects to allow both dot and braces syntax, like Jinja
- is isolated from the core Klipper Python code with same access as Jinja
What’s next
This is an early experiment to play with the idea. Proof of concept code here.
Some things that I have been considering:
- Maybe a flatter namespace would be easier to use:
G0() instead of gocde.G0()
total_wipes instead of variables.total_wipes
TIMES instead of params.TIMES
- Allow the code to define a function, which is then called with appropriate parameters. This will require some manual parsing, as python type annotations are not available in the runtime.
[gcode_macro clean_nozzle]
python:
total_wipes = 0 # A normal python variable, persisted across calls.
def run(times: int = 8):
for i in range(times):
...
- The above, plus add lifecycle functions, making this closer to Klipper extension code:
[gcode_macro clean_nozzle]
python:
total_wipes = 0
def handle_ready():
# Do something on kipper start.
def handle_homing_move_end(hmove):
# Do something after homing move
def run(times: int = 8):
# The macro
-
The python ConfigParser strips all whitespace and kills Python code, so made a quick fork of that. Not sure if there is a less intrusive way to fix this.
-
Conceptually python code can import other modules and be arbitarily large.
Performance wise it is fine, as it is all compiled at startup. There are some basic imports that are useful, like math, random. But this might open a potential security issue, we don’t want macros talking over the net? Or maybe?