Power-loss / crash recovery without saving anything during the print: measure where the nozzle stopped

Hi all,

I wanted to rescue long prints after a power cut or a frozen Pi without writing the position to the SD card every layer, which is how most power-loss macros work. I ended up with an add-on that measures where the nozzle stopped instead, then finds that exact spot in the G-code. It’s been working on my Ender 3 Pro, so I’m sharing it for feedback.

Repo: GitHub - mikeoverbay/klipper-power-loss-recovery · GitHub

The ideas

1. The machine is the record. With the power off, lead screws and belts don’t move, so the nozzle is still where it stopped. Klipper keeps each stepper’s MCU step count continuous through set_position() and homing (get_mcu_position()), so:

stop position = position after homing + (steps before homing − steps after) × step distance

That works for X, Y and Z: lift 5 mm, home X/Y, home Z at the X/Y corner, read the counts.

2. Heat before anything moves. After a power cut the nozzle has cooled into the top layer, so a cold lift tears the part off the bed. The command heats the nozzle in place (no motion) to 170 °C first. If it can’t get there, it turns the heater off and aborts without moving.

3. The layer comes from the log, not just from Z. klippy.log already records the print file position (sd_pos) about once a second. That gives the layer directly. It matters because with a large babystep, the measured Z alone can point at the wrong layer.

4. The exact move comes from X/Y. Inside that layer, it picks the extruding move whose path (the segment, not just its end points) passes closest to the measured X/Y. One surprise: the logged sd_pos was a couple of seconds behind the nozzle, not ahead of it. The last log lines never reach the SD card when the host dies, and the MCU keeps running its buffered moves. So the search runs forward from the logged position.

5. The babystep comes back too. measured Z − layer Z = the SET_GCODE_OFFSET that was active. The resume file re-applies it and clears it at the end.

6. One button, one popup. In Mainsail it’s a single POWER LOSS RESUME macro. It heats, measures and analyses, then a Mainsail action:prompt dialog shows:

  • the file
  • the layer
  • the resume point and how far it is from the measured spot
  • what it was printing (from Cura’s ;TYPE: labels)
  • the offset

Start print then builds the resume file into a hidden gcodes/.power_loss/ folder (so it can’t be deleted by mistake) and starts it. The idle timeout is held off between measuring and printing so homing isn’t lost.

Results on my machine

Ender 3 Pro, SKR Mini E3 V2, optical Z endstop, Klipper v0.13, Cura.

Z measurement. Set a known Z, FIRMWARE_RESTART to forget it, measure:

Z before reset Measured Error
3.000 3.000 0.000 mm
12.345 12.342 −0.003 mm
30.000 30.003 +0.003 mm
57.800 57.800 0.000 mm
100.000 100.005 +0.005 mm

That’s within ±2 microsteps of 0.0025 mm.

Exact resume. Two real mid-print stops, both in infill. The chosen resume move was 0.06 mm and 0.07 mm from the measured X/Y.

Limitations

  • Needs a Z-min endstop the gantry triggers (switch or optical), with the X/Y home corner clear of the part. A bed probe homing at the bed centre via safe_z_home would drive into the part.
  • Axes must hold position unpowered, and nobody should push the head before recovering.
  • X/Y motors can snap up to ±2 full steps when they’re enabled. Z is the precise axis; X/Y have been within a few hundredths of a millimetre so far.
  • Cura G-code only for now (;LAYER: markers, Z set before them). No G2/G3 arcs yet.
  • The part still has to stay stuck while cold, and the join usually shows.
  • It only reads stepper_z, so multi-Z setups assume the Z motors stay in sync.

Questions for the experienced folks

  • Is relying on MCU_stepper.get_mcu_position() staying continuous across set_position() and homing reasonable, or likely to break in future Klipper versions?
  • Is there a better way to get the last executed position than the measurement plus the (lagging) sd_pos in the log?
  • Edge cases I’ve missed?

Thanks for reading. Feedback welcome, and the README in the repo has the details and install steps.

Brilliant. I’ve recovered many prints manually by measuring the partial print height with calipers and hand editing the g-code. It never occurred to me that the Z height is in klippy.log.

The only thing I can point out is a print time bed mesh would be lost. If the “default” mesh is skewed there could be some artifacts. The current mesh points get logged. You could recover them. If [fade] is enabled you’d need to account for that.

Once again - Brilliant.

I would assume Z-max endstop would work too here.

this means in a badly tilted multi Z setup, stepper_z1 could crash into its endstop and alignment is lost?

how do you address a bed mesh that is used for the print? if it was loaded from config, the user needs to load it first I guess? And if it was generated during print start gcode, your tool would detect it as a local Z offset at the point the print stopped and apply that offset to the whole print(?)

And clearly z hop shouldn’t be used too, because it could lead to wrong layer detection when the power loss was during a z hop. And what happens if someone uses z hop, will your tool still detect the correct layer or will it maybe start a few layer too early?