Clarification request: Eddy Duo Z-offset persistence, non-tap Z_OFFSET_APPLY_PROBE, and homing correction

Basic Information:

Printer Model: Neptune 4 Max, Openneptune
MCU / Printerboard: MKS
Host / SBC: ???

klippy.log
klippy (5).zip (873.6 KB)

I am trying to understand the intended modern Klipper/Eddy workflow for reliable first-layer Z-offset persistence with a BTT Eddy Duo.

Setup

Printer: Elegoo Neptune 4 Max
Firmware/software: OpenNept4une / Klipper
Probe: BTT Eddy Duo / RP2040 USB-style Eddy MCU
Probe config: [probe_eddy_current btt_eddy]
Mesh workflow: KAMP adaptive mesh using BED_MESH_CALIBRATE ADAPTIVE=1
Goal: reliable first-layer Z persistence across firmware restarts, power cycles, and different material/bed-temperature workflows such as PETG vs PLA.

What I am trying to achieve

My desired workflow is:

1. Start a first-layer calibration print.
2. Adjust live Z / babystep in Fluidd during the print.
3. Save the corrected first-layer offset.
4. Restart firmware or power-cycle.
5. Print the same first-layer test again and get the same squish without re-adjusting.
6. Avoid custom variable-file workarounds if there is a correct stock Klipper/Eddy method.
7. Avoid modifying the Eddy calibration curve just to store a first-layer preference, unless that is truly the intended design.

What worked for me previously

I previously used a custom workaround that worked practically.

It intercepted Z_OFFSET_APPLY_PROBE, saved the current live Z adjustment from:

printer.gcode_move.homing_origin.z

into variables.cfg as a saved offset, then reapplied that value during PRINT_START using:

SET_GCODE_OFFSET Z=0
SET_GCODE_OFFSET Z_ADJUST={saved_offset} MOVE=0

That gave me very consistent first layers across firmware restarts and power cycles, and it worked fine with KAMP adaptive mesh.

However, I am trying to move away from this because it feels like an unofficial parallel Z-offset persistence system. I would prefer to use the intended Klipper/Eddy behavior if possible.

What confused me about stock non-tap Eddy behavior

When I removed my custom workaround and returned to stock Eddy behavior, it appeared that Z_OFFSET_APPLY_PROBE did not save a separate user Z-offset value. Instead, for non-tap Eddy behavior, it appears to persist the live Z adjustment by translating/modifying the Eddy calibration table.

From a user perspective, this is counterintuitive to me.

My mental model is:

Eddy calibration curve = physical sensor height/frequency calibration
Thermal drift calibration = compensation for temperature-related sensor drift
User Z offset / live baby-step = first-layer preference or nozzle-bed relationship

So I expected the first-layer Z adjustment to be saved as a separate persistent Z-offset value, not by modifying the Eddy sensor calibration curve.

Homing correction

I also found the documented Eddy homing-correction macro flow, where after G28 / CG28, an actual PROBE is run and then SET_KINEMATIC_POSITION is used to correct the Z coordinate frame.

The flow appears to be:

1. Clear runtime Z offset.
2. Home with G28 / CG28.
3. Run an Eddy PROBE.
4. Use the probe result to correct Klipper’s Z coordinate frame.
5. Run adaptive mesh.
6. Print.

Example macro:

[gcode_macro _RELOAD_Z_OFFSET_FROM_PROBE]
gcode:
    {% set Z = printer.toolhead.position.z %}
    SET_KINEMATIC_POSITION Z={Z - printer.probe.last_probe_position.z}

[gcode_macro SET_Z_FROM_PROBE]
gcode:
    {% set METHOD = params.METHOD|default("automatic") %}
    PROBE METHOD={METHOD}
    _RELOAD_Z_OFFSET_FROM_PROBE
    G0 Z5

This looks much closer to the kind of clean/official behavior I want, but I do not understand how it is intended to relate to persistent first-layer Z-offset storage.

Questions

  1. Why does non-tap Eddy Z_OFFSET_APPLY_PROBE persist baby-step correction by modifying/translating the Eddy calibration table instead of saving a separate persistent Z-offset value?

  2. Is calibration-table translation still considered the recommended workflow for non-tap Eddy users?

  3. Is there any stock-supported way to persist my first-layer Z preference without modifying the Eddy calibration curve and without using SAVE_VARIABLE / custom macros?

  4. Why does the official Eddy homing-correction macro correct the Z coordinate frame but not save or update a persistent Z offset?

  5. Why do example configs not already include the Eddy homing-correction macro by default when Eddy is used as the Z virtual endstop?

  6. For BTT Eddy Duo users who want reliable first-layer Z persistence across restarts, power cycles, and temperature changes, is tap-based probing now the intended clean path instead of non-tap Z_OFFSET_APPLY_PROBE?

What I am trying to clarify

I am not claiming this is definitely a bug. I am trying to understand the intended design.

The custom variable-based workaround worked for me, but I would prefer not to keep using it if there is a correct stock Klipper/Eddy workflow.

What I am looking for is the recommended current path for:

BTT Eddy Duo
+
Eddy used as Z virtual endstop
+
KAMP adaptive mesh
+
live Z adjustment during first layer
+
persistent first-layer offset after SAVE_CONFIG/restart/power cycle
+
stable behavior across PLA/PETG bed temperatures
+
no custom variables.cfg workaround
+
no unexpected calibration-curve modification

Should I continue trying to make stock non-tap Eddy persistence work, or is the intended clean direction now to use tap-based probing / Eddy tap behavior?

…

Eddy is the proximity sensor, that is it.
Calibration curve is the mapping between Z values and sensor’s coil frequency.
z_offset has been deprecated and renamed in the current code for Eddy sensors, btw.
z_offset describes where probe will trigger.

For normal probes it is a fixed point in space.
By so when you adjust it, you changes the mapping between trigger point to Z

For eddy it is a Frequency from the calibration curve.
So by change in the z_offset/descend_z, you change where it should trigger.
But, eddy is a proximity sensor, so there is no real hardware trigger.
So by change the “where to trigger” you do change “where to trigger”.
So, if it set to 1 mm, it will be triggered at 1 mm according to the curve and so on.

So, any adjustment to Z with babystepping should be saved inside the calibration curve, because what you meant is that previous Z position was wrong, and by so curve was wrong.

There are some (old) additional explanations: Make BTT Eddy great again

Otherwise it is a “normal probe”, so there is not a lot of difference.
There is a thermal drift, which is mentioned in the docs, which have to be accounted for.

Hope that explains something,
-Timofey

The stored “calibration curve” is intended to provide a mapping between sensor frequency and distance between nozzle and bed. If one needs to “adjust live z / babystep” then the most likely reason is that the curve is not correct (it has a static offset to it). Thus, Z_OFFSET_APPLY_PROBE is used to improve that calibration curve. Its conceptually simpler to fix the curve then to track a separate “fix for my curve” setting.

That is a workaround for a separate issue. Due to a software limitation in the Klipper code, a G28 Z0 request with a [probe_eddy_current] probe:z_virtual_offset is not accurate at all. It effectively isn’t using the calibration curve. The current workaround for this software deficiency is to use a macro to immediately PROBE after G28 Z0 to use the more accurate probe software to set the Z position.

It depends on the printer and goals. One of the main advantages to “tap” is that it can account for differences in nozzle height for those users that frequently change nozzles.

However, to accurately use “tap” one typically needs to deploy it as part of an elaborate homing system - in particular, the printer hardware needs to be capable of reliably “tapping” the bed and be capable of reliably cleaning the nozzle prior to that “tap”.

At a very high-level, non-tap homing might look something similar to:

  1. Home XYZ. (If using eddy to home Z, then be sure to apply the homing correction immediately after G28 Z0.)
  2. Perform automatic bed tilt adjustments (eg, QUAD_GANTRY_LEVEL, Z_TILT_ADJUST, or SCREWS_TILT_CALCULATE MAX_DEVIATION=...).
  3. Heat bed and nozzle to a consistent temperature. Wait at that temperature for several minutes (eg, 15 minutes) to “heat soak” the printer.
  4. Run BED_MESH_CALIBRATE METHOD=scan and make sure there is not a zero_reference_position specified in the config. After this runs, the Z level of the print is determined by the calibration curve produced from PROBE_EDDY_CURRENT_CALIBRATE. As long as that curve was also produced at the same consistent bed/nozzle temperature set in the previous step then you should get good results. If the curve needs alteration then Z_OFFSET_APPLY_PROBE can be used to correct that curve.
  5. Set the bed/nozzle temperature to printing temperature, wait for that temperature, and begin the print.

As I understand it, a “tap” based setup would look something like:

  1. Same as above. Home XYZ. (If using eddy to home Z, then be sure to apply the homing correction immediately after G28 Z0.)
  2. Same as above. Perform automatic bed tilt adjustments (eg, QUAD_GANTRY_LEVEL, Z_TILT_ADJUST, or SCREWS_TILT_CALCULATE MAX_DEVIATION=...).
  3. Similar to above. Heat bed to printing temperature and heat the nozzle to a consistent temperature high enough that the filament is soft, but not too high that it could damage the bed (eg, 170 C). Wait at that temperature for several minutes (eg, 15 minutes) to “heat soak” the printer.
  4. Perform some kind of automatic nozzle cleaning process. This can involve moving the nozzle back and forth across a silicon brush. It may also be possible to clean the nozzle by “tap” probing repeatedly somewhere near the corner of the bed (the repeated taps may dislodge any filament on the nozzle).
  5. Move to somewhere near the center of the bed (this is the XY “zero reference position”) and perform a PROBE METHOD=tap. Adjust the Z offset with the results of this probe using SET_KINEMATIC_POSITION.
  6. Perform a BED_MESH_CALIBRATE METHOD=scan and make sure that the zero_reference_position is set to the XY position used in the previous step. After this runs, the Z level of the print is determined by the “tap” action. This is particularly helpful in setups where one frequently changes nozzles as the Z offset will be relative to the current nozzle. If one needs to fine tune the Z level then one can use Z_OFFSET_APPLY_PROBE METHOD=tap to update the tap_z_offset in the config.
  7. Similar to above. Set the nozzle temperature to printing temperature, wait for that temperature, and begin the print.

Different printers have different individual steps, and I’m sure the above procedure could be further refined. The above steps are only intended to convey some high-level ideas.

Cheers,
-Kevin

why then does the calibration curve adjustment value differ in magnitude to the actual z offset adjustment value? Such as if i babystep 0.4mm up to achieve good squish during live first layer square test, upon z offset apply probe, the curve/table supposedly adjust by only 0.004. And even with that adjustment the first layer does not persist, but instead in testing appears to be at the same old incorrect z offset as the previous print and all my adjustments appear to be gone after save config and restart.

Also, subsequent baby steps and save z offset further adjust the curve by miniscule amounts and accumulate, however, still no persistence is detected in testing. Why is magnitude not equal?

I did indeed do the temperature drift calibration prior to all this…

Are you guys running standard eddy behavior with curve adjustment or are you running tap? can i have your cfgs for non-tap eddy.cfg and print start, with the new descend z?

Can you take a look at my cfgs and see if you spot anything that can be the cause?

Printer cfg and eddy cfg 20260504-181027.zip (12.9 KB)

My current config:

#*# [probe_eddy_current ldc1612]
#*# calibrate =
#*# -0.045000:3295890.605,-0.005000:3288970.492,0.035000:3282204.384,
#*# 0.075000:3275691.851,0.115000:3269313.573,0.155000:3263159.494,
#*# 0.195000:3257088.311,0.235000:3251285.008,0.275000:3245575.544,
#*# 0.315000:3240071.354,0.355000:3234648.124,0.395000:3229424.672,
SET_GCODE_OFFSET Z_ADJUST=+0.01 MOVE=0
Z_OFFSET_APPLY_PROBE

Updated values:

[probe_eddy_current ldc1612]
calibrate: 
-0.055000:3295890.605,-0.015000:3288970.492,0.025000:3282204.384,
0.065000:3275691.851,0.105000:3269313.573,0.145000:3263159.494,
0.185000:3257088.311,0.225000:3251285.008,0.265000:3245575.544,
0.305000:3240071.354,0.345000:3234648.124,0.385000:3229424.672,

So, from my PoV, it is updated by the exact amount of 0.01

I do not use tap, but I have configured temperature compensation, which helps a little.

Otherwise, I basically do this in the START_PRINT macro:

...
    # Start bed heating
    M140 S{BED_TEMP}
    M104 S{EXTRUDER_TEMP * 0.50}
    TEMPERATURE_WAIT SENSOR=extruder MINIMUM={EXTRUDER_TEMP * 0.50}
    {% if printer.toolhead.homed_axes != 'XYZ' %}
        G28 ; home all
    {% endif %}
    TEMPERATURE_WAIT SENSOR=heater_bed MINIMUM={BED_TEMP}
    G28 Z
    Z_TILT_ADJUST METHOD=scan
    G28 Z
    M104 S175
    TEMPERATURE_WAIT SENSOR=extruder MINIMUM=160
    BRUSH_NOZZLE
    SET_Z_FROM_PROBE # METHOD=tap
    # SET_SCAN_FROM_TAP

    BED_MESH_CALIBRATE ADAPTIVE=1 METHOD=scan HORIZONTAL_MOVE_Z={printer.configfile.settings["probe_eddy_current ldc1612"].descend_z}
    M104 S{EXTRUDER_TEMP * 0.85}
    PARK_HEAD

Otherwise, generally, you can simply share the log, because there is a complete config.

-Timofey