What's new
VORON Design

Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members!

User code modifications in Python without restart / loss of homing

endstop

Active member
Hi,
posting this here as I wrote it for my 2.4/350 Voron Stealthchanger.
I'll have to implement quite a bit of custom functionality for my envisioned "workflow" but found that code - both Klipper modules in Python and GCODE - can't be changed without Klipper reload and loss of homing.
Add Quad Gantry Leveling for toolchanges, possibly bed mesh, and this gets old real quick.

Enter "KlipperHotload" - my project - to (re)compile Python code on the fly. For what I need, it's a gamechanger, I will probably stop using GCODE files and code everything I need in Python.

In a nutshell (see README in above link) it provides one macro - I picked the letter "U" as it's not used in GCODE -
U PATH={CONFIG}/myCodeDir FILE=myCodeFile.py FUN=myFunction SOME_ARG_TO_FUN=123 SOME_OTHER_ARG=456 ...

to run a given function from a given file, passing arguments (where {CONFIG} interpolates to the directory of printer.cfg). There are a few tricks built in to assist with keeping GCODE concise, e.g. PATH and FILE may need to be set only once, the last value becomes default.
If only a single function is required, it collapses to
U SOME_ARG_TO_FUN=123 SOME_OTHER_ARG=456 ...

or if I don't need arguments just
U

which is about as short as it gets :)
Speaking of "short", it's just a single ~200 line file in klipper/klippy/extras plus tests directory.

I hope someone will find it useful.
 
Last edited:
we already have hot reloadable macros from another extension, but I honestly was tempted to make macros writable in Lua via tools like lupa at one point.
 
I am no coder. Just so I understand what this is, you can make a change to your .cfg and use the function without a restart of Klipper?
 
we already have hot reloadable macros from another extension, but I honestly was tempted to make macros writable in Lua via tools like lupa at one point.
Thanks. Would you remember the name of the project? I can put a comment on my git repo that there is an established solution.
I am no coder. Just so I understand what this is, you can make a change to your .cfg and use the function without a restart of Klipper?
It's not that easy, unfortunately, .cfg files are static. But what I can do is e.g. redefine GCODEs so they point to my own implementation (in Python). And changes to that apply immediately.

For example, this is the code that prints my first-layer calibration "chips", with three different layer thicknesses depending on docked nozzle size. It moves them across the printbed so I can print repeatedly, then clean up everything at once. By default, this would be invoked as
Code:
U FUN=firstLayerTest
or I put that command into a macro "FIRSTLAYERTEST" to have it appear in KlipperScreen etc.

1781017730472.png
Python:
def firstLayerTest(self, gcmd):
    ''' prints calibration chip for multiple first layer height levels.
    With correct Z offset, all should look consistently good.
    Too high Z offset results in separate strings instead of the desired continuous film for the lowest height. '''
    assertToolLoaded(self)

    toolhead = self.printer.lookup_object('toolhead')
    extruder = toolhead.get_extruder()
    nDiam = extruder.nozzle_diameter

    def cmd(c): # shorthand: closure on self
        self.log(c)
        self.gcode.run_script_from_command(c)
    
    def printAccel():
        cmd("SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0.1 VELOCITY=500 ACCEL=2000 SQUARE_CORNER_VELOCITY=20")
    
    # init
    if not 'firstLayerTest' in self:
        # this test structure has such low Z height that it can be printed repeatedly without removal.
        # Manage space on the print bed:
        self.firstLayerTest = {}
        self.firstLayerTest["x"] = 10
        
    x = self.firstLayerTest["x"]
    y1 = 10
    y2 = 50
    dFil = 1.75 # filament diameter
    rFil = dFil/2
    aFil = math.pi*rFil*rFil # circle area (filament cross-section)
    fPurge = 10*60
    fExtr = 50*60
    fMove = 500*60
    
    cmd("SAVE_GCODE_STATE NAME=FIRSTLAYERTEST")
    cmd("G90")   # absolute positioning
    cmd("M83")   # relative extrusion
        
    helloRestoreMachineLimits(self, gcmd)
    cmd(f"G1 X{x} Y{y1} Z10 F{fMove}")
    printAccel()

    if str(nDiam) == "0.6":
        hList = [0.5, 0.3, 0.15] # layer height. Note: Downwards steps may cause collision!
    elif str(nDiam) == "0.4":
        hList = [0.35, 0.2, 0.12] # layer height. Note: Downwards steps may cause collision!
    elif str(nDiam) == "0.2":
        hList = [0.2, 0.12, 0.08] # layer height. Note: Downwards steps may cause collision!
    elif str(nDiam) == "0.1":
        fExtr = 40*60
        hList = [0.08, 0.065, 0.05] # layer height. Note: Downwards steps may cause collision!
    else:
        raise RuntimeError("nozzle diameter "+str(nDiam)+" not implemented - please add in "+__FILE__)
    w = nDiam * 0.45/0.40 # line width
        
    start = True
    for h in hList:
        if start:
            start = False
            # purge a blob
            cmd(f"G1 X{x} Y{y1} Z{h} F{fMove}")
            cmd(f"G1 E10 F120") # F120 = 120 mm/min = 2 mm/sec

        for ix in range(10):
            dist = y2-y1       
            vol = w*dist*h
            lFil = vol / aFil
            
            cmd(f"G1 X{x} Y{y1} Z{h} F{fMove}")
            cmd(f"G1 Y{y2} E{lFil} F{fExtr}")
            x = x + w
            cmd(f"G1 X{x} F{fMove}")
            cmd(f"G1 Y{y1} E{lFil} F{fExtr}")
            x = x + w

    # fat line for removal
    h = 0.5
    dist = y2-y1       
    vol = w*dist*h
    lFil = vol / aFil

    cmd(f"G1 X{x} Y{y1} Z{h} F{fMove}")
    cmd(f"G1 Y{y2} E{lFil} F{fExtr}")
    x = x + w
    cmd(f"G1 X{x} F{fMove}")
    cmd(f"G1 Y{y1} E{lFil} F{fExtr}")
    x = x + w

    # lift head
    helloRestoreMachineLimits(self, gcmd)
    self.gcode.run_script_from_command(f"G1 Z30 Y300 F{fMove}")
    cmd("RESTORE_GCODE_STATE NAME=FIRSTLAYERTEST")
    
    # right end of plate? Restart from left end
    x = x + 10*w
    if x > 300:
        x = 10

    # store position for next run so prints don't need to be removed immediately
    self.firstLayerTest["x"] = x

I have my toolchanger workflow largely implemented in Python. Having to restart, rehome, quad gantry level etc. the whole box for any single configuration change would be totally impractical.
 
Last edited:
Top