I had changed filament the usual way, using the UNLOAD_FILAMENT and LOAD_FILAMENT macros, and then started a print. The printer homed obediently, ran Z-tilt and bed mesh – and then printed in mid-air.
The cause was a missing argument in my printer.cfg: G92 instead of G92 E0. More interesting than the cause, though, is why Klipper doesn't repair it on its own: I was firmly convinced that a G28 straightens out any shifted origin. It doesn't – at least not for Z when homing via the eddy probe.
How it showed up
It started with a harmless question: why does the bed move down about 60 mm after Z homing and straight back up again? Shortly afterwards, the printer was printing into thin air.
A look at the running instance showed two Z values that didn't match. At that moment the toolhead was at the park position X200/Y395:
toolhead.position Z = 102.601 # where the machine actually is
gcode_move.gcode_position Z = 15.320 # where the G-code thinks it is87 mm difference between what the machine does and what the G-code assumes. That is no longer a calibration error – that is a different coordinate system.
What I investigated
The formula from the source code
Klipper converts G-code coordinates in gcode_move.py. The relationship is simple:
gcode_position = last_position − base_positionPlugging in the two live values gives a base_z of 87.28. So something had shifted the Z origin of the G-code coordinate system upwards by a good 87 mm.
Sanity-checking the number
Before looking for the culprit, I wanted to know whether the 15.32 was even plausible. It is: 0.32 mm first layer plus the 15 mm by which END_PRINT lifts the head at the end (G1 Z15). So the number matches G-code that ran normally – just 87 mm above the bed.
What happened before the print
Klipper's gcode_store keeps the most recently executed commands. It contained the sequence that explained everything:
13:47:57 UNLOAD_FILAMENT
13:48:45 LOAD_FILAMENT
13:49:11 SDCARD_PRINT_FILEAnd the same pattern had already occurred in the morning. Filament change, then print start – twice on the same day.
Live experiments
Instead of reading further, I tried out on the machine what the individual commands do to the coordinate system:
- A bare
G92sets the G-code position to 0/0/0/0 – that is, X, Y, Z and E. G28 X Yresetsbase_xandbase_y.- A full
G28and even a rawG28.1 Zleavebase_zat 87.294.
That last point was the real shock.
Dead ends
"It's the G28 wrapper"
The printer runs the G28 wrapper from the BTT Eddy package. When all I saw was "60 mm down and back up", it was natural to suspect the wrapper or a configured travel height. But the numbers didn't fit that explanation – after that, the shifted origin turned up.
"The G92 is in the print file"
A G92 with a Z value in the print file would have had exactly this effect. A grep over the file disproved that: all 282 G92 lines in it read G92 E0. The slicer only resets the extruder, as it should.
"Homing cures the offset"
This was the most stubborn misconception, because it sounds so obvious. Homing means the machine finds its origin again, after all. Any offset should be gone afterwards.
It wasn't. After a full G28, base_z was still at 87.294. The machine was at Z 97.294 while the G-code assumed Z 10. Homing, Z-tilt and bed mesh all ran through without noticing that the G-code's Z axis starts 87 mm above the bed.
In my setup, G28 via the eddy probe does not reset the G-code Z origin. An offset in base_z that arises before the print starts survives the entire start sequence.
The cause
In printer.cfg, both filament macros ended with a bare G92. What I meant was G92 E0:
[gcode_macro LOAD_FILAMENT]
gcode:
G91
G1 E60 F1800
G1 E60 F180
G92 # meant: G92 E0
[gcode_macro UNLOAD_FILAMENT]
gcode:
G91
G1 E-120 F1800
G92 # meant: G92 E0Without an axis argument, G92 does something completely different than with one. In gcode_move.py (lines 189/190 in my version) it reads:
base_position[:4] = last_position[:4]So X, Y, Z and E are all zeroed at the current position. During the filament change the head was at Z 87.28 – and that is exactly where the new "zero" for the G-code lay from then on.
There was a second detail: both macros switched to relative coordinates with G91 and left it that way.
The fix
Filament macros without G92
I rebuilt both macros (pattern only, the exact lines may differ) so that they save the G-code state, move the extruder in relative mode and restore the state afterwards – entirely without G92. That also means no G91 is left hanging.
[gcode_macro LOAD_FILAMENT]
gcode:
SAVE_GCODE_STATE NAME=<name>
M83 # extruder only, relative
G1 E60 F1800
G1 E60 F180
RESTORE_GCODE_STATE NAME=<name>
[gcode_macro UNLOAD_FILAMENT]
gcode:
SAVE_GCODE_STATE NAME=<name>
M83
G1 E-120 F1800
RESTORE_GCODE_STATE NAME=<name>A safeguard line after homing
Fixing the macros removes this one trigger. But G28 still doesn't catch a shifted Z origin – no matter where it comes from. That's why START_PRINT now calls a new macro, _Z_FRAME_RESET, right after homing:
[gcode_macro START_PRINT]
gcode:
...
G28
_Z_FRAME_RESET
...The macro itself is short:
[gcode_macro _Z_FRAME_RESET]
description: Hard-resets gcode Z to the homing origin (see START_PRINT)
gcode:
G90
G92 Z{printer.toolhead.position.z - printer.gcode_move.homing_origin.z}It sets the G-code Z position to the real toolhead position minus any Z offset that may be set (homing_origin.z). That gets rid of a base_z shifted by a bare G92 without losing the Z offset.
There's a reason this is a separate macro and not a few lines directly in START_PRINT: Klipper renders a macro completely before the first line is executed. My first attempt failed precisely because of that. My conclusion: a separate macro is only rendered when it is called – that is, after homing.
Before the change, I backed up the old configuration as printer.cfg.vor-g92fix.
How I verified it
After a restart, I deliberately triggered the error again to see whether the safeguard line really catches it:
| Step | base_z | Result |
|---|---|---|
| after G28 | −0.0044 | equals homing_z – clean |
| Sabotage: G92 Z50 | −40.0044 | offset reproduced |
| after the new safeguard line | −0.0044 | back to the starting value |
| Final check | 0.098320 | equals homing_origin, difference 0 |
The sabotage test is the most important part: without it, I would only have shown that nothing goes wrong in the normal case. With it, it's proven that the safeguard line actually brings back an artificially shifted origin.
The first real print start on 31 Aug put the first layer at machine Z 0.346: G-code Z 0.240 plus offset 0.106. Exactly where it belongs.
What remains open
I only skimmed the source code (homing.py and safe_z_home.py) to find out why Z homing via the eddy probe doesn't reset base_z. For the printer it no longer matters, because _Z_FRAME_RESET catches the case. But I haven't understood the mechanism yet.
My recommendation from this case: if you have filament or other helper macros containing G92, check whether an axis really follows it. And after a G28, it's worth looking at gcode_move.base_position in the running instance: if Z there shows anything other than the homing offset, the printer is printing in a shifted coordinate system.
Printer: HevORT, Klipper v0.13, BTT Eddy with the G28 wrapper from the BTT Eddy package.
The numbers come from measurements on this machine. The behavior of G28 with respect to base_z applies to this setup; I haven't checked whether it's the same with other probes or endstops.
Ad · Affiliate link – if you buy through it, I may earn a commission. It doesn’t change the price for you.