diff --git a/.github/workflows/ci-build_test.yaml b/.github/workflows/ci-build_test.yaml
index 2d9dfba2b..776c013da 100644
--- a/.github/workflows/ci-build_test.yaml
+++ b/.github/workflows/ci-build_test.yaml
@@ -1,6 +1,8 @@
# Perform continuous integration tests on updates and pull requests
name: Build test
+
on:
+ workflow_dispatch:
push:
jobs:
diff --git a/.github/workflows/ci-static_analysis.yaml b/.github/workflows/ci-static_analysis.yaml
index 9f42b0899..4ca3f8f6a 100644
--- a/.github/workflows/ci-static_analysis.yaml
+++ b/.github/workflows/ci-static_analysis.yaml
@@ -1,5 +1,6 @@
# Perform continuous integration tests on pull requests
name: black
+
on: pull_request
jobs:
diff --git a/.github/workflows/klipper3d-deploy.yaml b/.github/workflows/klipper3d-deploy.yaml
index 0e4498491..507304603 100644
--- a/.github/workflows/klipper3d-deploy.yaml
+++ b/.github/workflows/klipper3d-deploy.yaml
@@ -1,8 +1,6 @@
name: klipper3d deploy
on:
workflow_dispatch:
- schedule:
- - cron: "0 0 * * *"
push:
branches:
- master
@@ -20,7 +18,7 @@ jobs:
with:
python-version: '3.8'
- - uses: actions/cache@v2
+ - uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('docs/_klipper3d/mkdocs-requirements.txt') }}
@@ -34,7 +32,7 @@ jobs:
run: docs/_klipper3d/build-translations.sh
- name: Deploy
- uses: JamesIves/github-pages-deploy-action@v4.2.5
+ uses: JamesIves/github-pages-deploy-action@v4.4.3
with:
branch: gh-pages # The branch the action should deploy to.
folder: site # The folder the action should deploy.
diff --git a/.github/workflows/upstream-sync.yaml b/.github/workflows/upstream-sync.yaml
index 5e66695e5..d3869a01f 100644
--- a/.github/workflows/upstream-sync.yaml
+++ b/.github/workflows/upstream-sync.yaml
@@ -35,7 +35,7 @@ jobs:
steps:
# Fetch origin/prev_upstream
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
ref: prev_upstream
path: "repo"
@@ -72,7 +72,7 @@ jobs:
- name: Setup python
if: ${{ steps.fetch.outputs.new_commits }} == '1'
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: 3.10.3
cache-dependency-path: scripts/requirements_dev.txt
diff --git a/.gitignore b/.gitignore
index 45735517a..2bc23f69f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ klippy/.version
.DS_Store
ci_build/
ci_cache/
+_test_.*
diff --git a/README.md b/README.md
index 5aa5be666..d2036295c 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,7 @@
+
+
+[![Action Status](https://github.com/DangerKlippers/danger-klipper/actions/workflows/ci-build_test.yaml/badge.svg?branch=master)](https://github.com/DangerKlippers/danger-klipper/actions/workflows/ci-build_test.yaml)
+
Welcome to the Danger Klipper project!
This is a community-maintained fork of the [Klipper](https://github.com/Klipper3d/klipper) firmware.
@@ -24,12 +28,17 @@ Features merged into the master branch:
- [probe: Drop the first result](https://github.com/DangerKlippers/danger-klipper/pull/2) ([klipper#3397](https://github.com/Klipper3d/klipper/issues/3397))
+- [probe: z_calibration](https://github.com/DangerKlippers/danger-klipper/pull/31) ([klipper#4614](https://github.com/Klipper3d/klipper/pull/4614) / [protoloft/z_calibration](https://github.com/protoloft/klipper_z_calibration))
-"Dangerous Klipper for dangerous users"
+- [core: danger_options](https://github.com/DangerKlippers/danger-klipper/pull/67)
-[![Klipper](docs/img/klipper-logo-small.png)](https://DangerKlippers.github.io/danger-klipper/)
+- [stepper: home_current](https://github.com/DangerKlippers/danger-klipper/pull/65)
-https://DangerKlippers.github.io/danger-klipper/
+- [homing: post-home retract](https://github.com/DangerKlippers/danger-klipper/pull/65)
+
+- [homing: sensorless minimum home distance](https://github.com/DangerKlippers/danger-klipper/pull/65)
+
+"Dangerous Klipper for dangerous users"
Klipper is a 3d-Printer firmware. It combines the power of a general
purpose computer with one or more micro-controllers. See the
diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md
index 725a294c5..814ca3294 100644
--- a/docs/Config_Reference.md
+++ b/docs/Config_Reference.md
@@ -73,6 +73,31 @@ pins such as "extra_mcu:ar9" may then be used elsewhere in the config
[mcu my_extra_mcu]
# See the "mcu" section for configuration parameters.
```
+## ⚠️ Danger Options
+A collection of DangerKlipper-specific system options
+```
+[danger_options]
+
+# If an unused config option or section should cause an error
+# if False, will warn but allow klipper to still run
+error_on_unused_config_options: True
+
+# If statistics should be logged
+# (helpful for keeping the log clean during development)
+log_statistics: True
+
+# If the config file should be logged at startup
+log_config_file_at_startup: True
+
+# If the bed mesh should be logged on startup
+# (helpful for keeping the log clean during development)
+log_bed_mesh_at_startup: True
+
+# If we should log detailed crash info when an exception occurs
+# Most of it is overly-verbose and fluff and we still get a stack trace
+# for normal exceptions, so setting to False can help save time while developing
+log_shutdown_info: True
+```
## Common kinematic settings
@@ -179,7 +204,9 @@ position_max:
# is 5mm/s.
#homing_retract_dist: 5.0
# Distance to backoff (in mm) before homing a second time during
-# homing. Set this to zero to disable the second home. The default
+# homing. If `use_sensorless_homing` is false, this setting can be set
+# to zero to disable the second home. If `use_sensorless_homing` is
+# true, this setting can be > 0 to backoff after homing. The default
# is 5mm.
#homing_retract_speed:
# Speed to use on the retract move after homing in case this should
@@ -194,6 +221,9 @@ position_max:
# better to use the default than to specify this parameter. The
# default is true if position_endstop is near position_max and false
# if near position_min.
+#use_sensorless_homing:
+# If true, disables the second home action if homing_retract_dist > 0.
+# The default is true if endstop_pin is configured to use virtual_endstop
```
### Cartesian Kinematics
@@ -2104,6 +2134,89 @@ calibrate_y: 112.5
# be near the center of the bed
```
+### ⚠️ [z_calibration]
+
+Automatic Z offset calibration. One may define this section if the printer
+is able to calibrate the nozzle's offset automatically. See
+[Z-Calibration guide](Z_Calibration.md) and
+[command reference](G-Codes.md#automatic-z-offset-calibration) for further
+information.
+
+```
+[z_calibration]
+nozzle_xy_position:
+# A X, Y coordinate (e.g. 100,100) of the nozzle, clicking on the Z endstop.
+switch_xy_position:
+# A X, Y coordinate (e.g. 100,100) of the probe's switch body, clicking on
+# the Z endstop.
+bed_xy_position: default from relative_reference_index of bed_mesh
+# a X, Y coordinate (e.g. 100,100) where the print surface (e.g. the center
+# point) is probed. These coordinates will be adapted by the
+# probe's X and Y offsets. The default is the relative_reference_index
+# of the configured bed_mesh, if configured. It's possible to change the relative
+# reference index at runtime or use the GCode argument BED_POSITION of CALIBRATE_Z.
+switch_offset:
+# The trigger point offset of the used mag-probe switch.
+# Larger values will position the nozzle closer to the bed.
+# This needs to be find out manually. More on this later
+# in this section..
+max_deviation: 1.0
+# The maximum allowed deviation of the calculated offset.
+# If the offset exceeds this value, it will stop!
+# The default is 1.0 mm.
+samples: default from "probe:samples" section
+# The number of times to probe each point. The probed z-values
+# will be averaged. The default is from the probe's configuration.
+samples_tolerance: default from "probe:samples_tolerance" section
+# The maximum Z distance (in mm) that a sample may differ from other
+# samples. The default is from the probe's configuration.
+samples_tolerance_retries: default from "probe:samples_tolerance_retries" section
+# The number of times to retry if a sample is found that exceeds
+# samples_tolerance. The default is from the probe's configuration.
+samples_result: default from "probe:samples_result" section
+# The calculation method when sampling more than once - either
+# "median" or "average". The default is from the probe's configuration.
+clearance: 2 * z_offset from the "probe:z_offset" section
+# The distance in mm to move up before moving to the next
+# position. The default is two times the z_offset from the probe's
+# configuration.
+position_min: default from "stepper_z:position_min" section.
+# Minimum valid distance (in mm) used for probing move. The
+# default is from the Z rail configuration.
+speed: 50
+# The moving speed in X and Y. The default is 50 mm/s.
+lift_speed: default from "probe:lift_speed" section
+# Speed (in mm/s) of the Z axis when lifting the probe between
+# samples and clearance moves. The default is from the probe's
+# configuration.
+probing_speed: default from "stepper_z:homing_speed" section.
+# The fast probing speed (in mm/s) used, when probing_first_fast
+# is activated. The default is from the Z rail configuration.
+probing_second_speed: default from "stepper_z:second_homing_speed" section.
+# The slower speed (in mm/s) for probing the recorded samples.
+# The default is second_homing_speed of the Z rail configuration.
+probing_retract_dist: default from "stepper_z:homing_retract_dist" section.
+# Distance to retract (in mm) before probing the next sample.
+# The default is homing_retract_dist from the Z rail configuration.
+probing_first_fast: false
+# If true, the first probing is done faster by the probing speed.
+# This is to get faster down and the result is not recorded as a
+# probing sample. The default is false.
+start_gcode:
+# A list of G-Code commands to execute prior to each calibration command.
+# See docs/Command_Templates.md for G-Code format. This can be used to
+# attach the probe.
+before_switch_gcode:
+# A list of G-Code commands to execute prior to each probing on the
+# mag-probe. See docs/Command_Templates.md for G-Code format. This can be
+# used to attach the probe after probing on the nozzle and before probing
+# on the mag-probe.
+end_gcode:
+# A list of G-Code commands to execute after each calibration command.
+# See docs/Command_Templates.md for G-Code format. This can be used to
+# detach the probe afterwards.
+```
+
## Additional stepper motors and extruders
### [stepper_z1]
@@ -3291,6 +3404,9 @@ run_current:
# when the stepper is not moving. Setting a hold_current is not
# recommended (see TMC_Drivers.md for details). The default is to
# not reduce the current.
+#home_current:
+# The amount of current (in amps RMS) to configure the driver to use
+# during homing procedures. The default is to not reduce the current.
#sense_resistor: 0.110
# The resistance (in ohms) of the motor sense resistor. The default
# is 0.110 ohms.
@@ -3384,6 +3500,9 @@ run_current:
# when the stepper is not moving. Setting a hold_current is not
# recommended (see TMC_Drivers.md for details). The default is to
# not reduce the current.
+#home_current:
+# The amount of current (in amps RMS) to configure the driver to use
+# during homing procedures. The default is to not reduce the current.
#sense_resistor: 0.110
# The resistance (in ohms) of the motor sense resistor. The default
# is 0.110 ohms.
@@ -3427,6 +3546,7 @@ uart_pin:
#interpolate: True
run_current:
#hold_current:
+#home_current:
#sense_resistor: 0.110
#stealthchop_threshold: 0
# See the "tmc2208" section for the definition of these parameters.
@@ -3495,6 +3615,9 @@ cs_pin:
run_current:
# The amount of current (in amps RMS) used by the driver during
# stepper movement. This parameter must be provided.
+#home_current:
+# The amount of current (in amps RMS) to configure the driver to use
+# during homing procedures. The default is to not reduce the current.
#sense_resistor:
# The resistance (in ohms) of the motor sense resistor. This
# parameter must be provided.
@@ -3572,6 +3695,9 @@ run_current:
# when the stepper is not moving. Setting a hold_current is not
# recommended (see TMC_Drivers.md for details). The default is to
# not reduce the current.
+#home_current:
+# The amount of current (in amps RMS) to configure the driver to use
+# during homing procedures. The default is to not reduce the current.
#rref: 12000
# The resistance (in ohms) of the resistor between IREF and GND. The
# default is 12000.
@@ -3693,6 +3819,9 @@ run_current:
# when the stepper is not moving. Setting a hold_current is not
# recommended (see TMC_Drivers.md for details). The default is to
# not reduce the current.
+#home_current:
+# The amount of current (in amps RMS) to configure the driver to use
+# during homing procedures. The default is to not reduce the current.
#sense_resistor: 0.075
# The resistance (in ohms) of the motor sense resistor. The default
# is 0.075 ohms.
diff --git a/docs/G-Codes.md b/docs/G-Codes.md
index 8fb7108c7..c4820944e 100644
--- a/docs/G-Codes.md
+++ b/docs/G-Codes.md
@@ -916,12 +916,16 @@ in the config file.
#### PID_CALIBRATE
`PID_CALIBRATE HEATER= TARGET=
-[WRITE_FILE=1]`: Perform a PID calibration test. The specified heater
-will be enabled until the specified target temperature is reached, and
-then the heater will be turned off and on for several cycles. If the
-WRITE_FILE parameter is enabled, then the file /tmp/heattest.txt will
-be created with a log of all temperature samples taken during the
-test.
+[WRITE_FILE=1] [TOLERANCE=0.02]`: Perform a PID calibration test. The
+specified heater will be enabled until the specified target temperature
+is reached, and then the heater will be turned off and on for several
+cycles. If the WRITE_FILE parameter is enabled, then the file
+/tmp/heattest.csv will be created with a log of all temperature samples
+taken during the test. TOLERANCE defaults to 0.02 if not passed in. The
+tighter the tolerance the better the calibration result will be, but how
+tight you can achieve depends on how clean your sensor readings are. low
+noise readings might allow 0.01, to be used, while noisy reading might
+require a value of 0.03 or higher.
#### SET_HEATER_PID
`SET_HEATER_PID HEATER= KP= KI= KD=`: Will
@@ -1212,7 +1216,7 @@ profile matching the supplied name from persistent memory. Note that
after SAVE or REMOVE operations have been run the SAVE_CONFIG gcode
must be run to make the changes to persistent memory permanent.
-### [smart_effector]
+### ⚠️ [smart_effector]
Several commands are available when a
[smart_effector config section](Config_Reference.md#smart_effector) is enabled.
@@ -1397,6 +1401,22 @@ the config. `REF_TEMP` manually overrides the reference temperature typically
set during homing (for use in e.g. non-standard homing routines) - will be reset
automatically upon homing.
+### ⚠️ [z_calibration]
+
+The following commands are available when a
+[z_calibration config section](Config_Reference.md#z_calibration) is enabled
+(also see the [Z-Calibration guide](Z_Calibration.md)):
+- `CALIBRATE_Z`: This calibrates the current offset between the nozzle and
+ the print surface.
+- `PROBE_Z_ACCURACY [PROBE_SPEED=] [LIFT_SPEED=] [SAMPLES=]
+ [SAMPLE_RETRACT_DIST=]`: Calculate the maximum, minimum,
+ average, median, and standard deviation of multiple probe
+ samples. By default, 10 SAMPLES are taken. Otherwise the optional
+ parameters default to their equivalent setting in the z_calibration or probe
+ config section.
+*Note* that appropriate macros and/or configurations are needed to attach
+and detach a mag-probe for these commands!
+
### [z_tilt]
The following commands are available when the
diff --git a/docs/PID.md b/docs/PID.md
new file mode 100644
index 000000000..02be267ef
--- /dev/null
+++ b/docs/PID.md
@@ -0,0 +1,188 @@
+
+# PID
+
+PID control is a widely used control method in the 3D printing world.
+It’s ubiquitous when it comes to temperature control, be it with heaters to
+generate heat or fans to remove heat. This document aims to provide a
+high-level overview of what PID is and how to use it best in Klipper.
+
+## PID Calibration
+### Preparing the Calibration
+When a calibration test is performed external influences should be minimized as
+much as possible:
+* Turn off fans
+* Turn off chamber heaters
+* Turn off the extruder heater when calibrating the bed and vice versa
+* Avoid external disturbances like drafts etc
+
+### Choosing the right PID Algorithm
+Klipper offers two different PID algorithms: Positional and Velocity
+
+* Positional (`pid`)
+ * The standard algorithm
+ * Very robust against noisy temperature readings
+ * Can cause overshoots
+ * Insufficient target control in edge cases
+* Velocity (`pid_v`)
+ * No overshoot
+ * Better target control in certain scenarios
+ * More susceptible to noisy sensors
+ * Might require larger smoothing time constants
+
+Refer to the [control statement](Config_Reference.md#extruder) in the
+Configuration Reference.
+
+### Running the PID Calibration
+The PID calibration is invoked via the [PID_CALIBRATE](G-Codes.md#pid_calibrate) command.
+This command will heat up the respective heater and let it cool down around
+the target temperature in multiple cycles to determine the needed
+parameters.
+
+Such a calibration cycles looks like the following snippet:
+```
+3:12 PM PID_CALIBRATE HEATER=extruder TARGET=220 TOLERANCE=0.01 WRITE_FILE=1
+3:15 PM sample:1 pwm:1.0000 asymmetry:3.7519 tolerance:n/a
+3:15 PM sample:2 pwm:0.6229 asymmetry:0.3348 tolerance:n/a
+3:16 PM sample:3 pwm:0.5937 asymmetry:0.0840 tolerance:n/a
+3:17 PM sample:4 pwm:0.5866 asymmetry:0.0169 tolerance:0.4134
+3:18 PM sample:5 pwm:0.5852 asymmetry:0.0668 tolerance:0.0377
+3:18 PM sample:6 pwm:0.5794 asymmetry:0.0168 tolerance:0.0142
+3:19 PM sample:7 pwm:0.5780 asymmetry:-0.1169 tolerance:0.0086
+3:19 PM PID parameters: pid_Kp=16.538 pid_Ki=0.801 pid_Kd=85.375
+ The SAVE_CONFIG command will update the printer config file
+ with these parameters and restart the printer.
+```
+Note the `asymmetry` information. It provides an indication if the heater's
+power is sufficient to ensure a symmetrical "heat up" versus "cool down /
+heat loss" behavior. It should start positive and converge to zero.
+A negative starting value indicates that the heat loss is faster than the heat
+up, this means the system is asymmetrical. The calibration will still be
+successful but reserves to counter disturbances might be low.
+
+## Advanced / Manual Calibration
+
+Many methods exist for calculating control parameters, such as Ziegler-Nichols,
+Cohen-Coon, Kappa-Tau, Lambda, and many more. By default, classical
+Ziegler-Nichols parameters are generated. If a user wants to experiment with
+other flavors of Ziegler-Nichols, or Cohen-Coon parameters, they can extract the
+constants from the log as seen below and enter them into this
+[spreadsheet](resources/pid_params.xls).
+
+```text
+Ziegler-Nichols constants: Ku=0.103092 Tu=41.800000
+Cohen-Coon constants: Km=-17.734845 Theta=6.600000 Tau=-10.182680
+```
+
+Classic Ziegler-Nichols parameters work in all scenarios. Cohen-Coon parameters
+work better with systems that have a large amount of dead time/delay. For
+example, if a printer has a bed with a large thermal mass that’s slow to heat
+up and stabilize, the Cohen-Coon parameters will generally do a better job at
+controlling it.
+
+## Further Readings
+### History
+
+The first rudimentary PID controller was developed by Elmer Sperry in 1911 to
+automate the control of a ship's rudder. Engineer Nicolas Minorsky published the
+first mathematical analysis of a PID controller in 1922. In 1942, John Ziegler &
+Nathaniel Nichols published their seminal paper, "Optimum Settings for Automatic
+Controllers," which described a trial-and-error method for tuning a PID
+controller, now commonly referred to as the "Ziegler-Nichols method.
+
+In 1984, Karl Astrom and Tore Hagglund published their paper "Automatic Tuning
+of Simple Regulators with Specifications on Phase and Amplitude Margins". In the
+paper they introduced an automatic tuning method commonly referred to as the
+"Astrom-Hagglund method" or the "relay method".
+
+In 2019 Brandon Taysom & Carl Sorensen published their paper "Adaptive Relay
+Autotuning under Static and Non-static Disturbances with Application to
+Friction Stir Welding", which laid out a method to generate more accurate
+results from a relay test. This is the PID calibration method currently used by
+Klipper.
+
+### Details of the Relay Test
+As previously mentioned, Klipper uses a relay test for calibration purposes. A
+standard relay test is conceptually simple. You turn the heater’s power on and
+off to get it to oscillate about the target temperature, as seen in the
+following graph.
+
+![simple relay test](img/pid_01.png)
+
+The above graph shows a common issue with a standard relay test. If the system
+being calibrated has too much or too little power for the chosen target
+temperature, it will produce biased and asymmetric results. As can be seen
+above, the system spends more time in the off state than on and has a larger
+amplitude above the target temperature than below.
+
+In an ideal system, both the on and off times and the amplitude above and below
+the target temperature would be the same. 3D printers don’t actively cool the
+hot end or bed, so they can never reach the ideal state.
+
+The following graph is a relay test based on the methodology laid out by
+Taysom & Sorensen. After each iteration, the data is analyzed and a new maximum
+power setting is calculated. As can be seen, the system starts the test
+asymmetric but ends very symmetric.
+
+![advanced relay test](img/pid_02.png)
+
+Asymmetry can be monitored in real time during a calibration run. It can also
+provide insight into how suitable the heater is for the current calibration
+parameters. When asymmetry starts off positive and converges to zero, the
+heater has more than enough power to achieve symmetry for the calibration
+parameters.
+
+```
+3:12 PM PID_CALIBRATE HEATER=extruder TARGET=220 TOLERANCE=0.01 WRITE_FILE=1
+3:15 PM sample:1 pwm:1.0000 asymmetry:3.7519 tolerance:n/a
+3:15 PM sample:2 pwm:0.6229 asymmetry:0.3348 tolerance:n/a
+3:16 PM sample:3 pwm:0.5937 asymmetry:0.0840 tolerance:n/a
+3:17 PM sample:4 pwm:0.5866 asymmetry:0.0169 tolerance:0.4134
+3:18 PM sample:5 pwm:0.5852 asymmetry:0.0668 tolerance:0.0377
+3:18 PM sample:6 pwm:0.5794 asymmetry:0.0168 tolerance:0.0142
+3:19 PM sample:7 pwm:0.5780 asymmetry:-0.1169 tolerance:0.0086
+3:19 PM PID parameters: pid_Kp=16.538 pid_Ki=0.801 pid_Kd=85.375
+ The SAVE_CONFIG command will update the printer config file
+ with these parameters and restart the printer.
+```
+
+When asymmetry starts off negative, It will not converge to zero. If Klipper
+does not error out, the calibration run will complete and provide good PID
+parameters, However the heater is less likely to handle disturbances as well
+as a heater with power in reserve.
+
+```
+3:36 PM PID_CALIBRATE HEATER=extruder TARGET=220 TOLERANCE=0.01 WRITE_FILE=1
+3:38 PM sample:1 pwm:1.0000 asymmetry:-2.1149 tolerance:n/a
+3:39 PM sample:2 pwm:1.0000 asymmetry:-2.0140 tolerance:n/a
+3:39 PM sample:3 pwm:1.0000 asymmetry:-1.8811 tolerance:n/a
+3:40 PM sample:4 pwm:1.0000 asymmetry:-1.8978 tolerance:0.0000
+3:40 PM PID parameters: pid_Kp=21.231 pid_Ki=1.227 pid_Kd=91.826
+ The SAVE_CONFIG command will update the printer config file
+ with these parameters and restart the printer.
+```
+
+### Pid Control Algorithms
+
+Klipper currently supports two control algorithms: Positional and Velocity.
+The fundamental difference between the two algorithms is that the Positional
+algorithm calculates what the PWM value should be for the current time
+interval, and the Velocity algorithm calculates how much the previous PWM
+setting should be changed to get the PWM value for the current time interval.
+
+Positional is the default algorithm, as it will work in every scenario. The
+Velocity algorithm can provide superior results to the Positional algorithm but
+requires lower noise sensor readings, or a larger smoothing time setting.
+
+The most noticeable difference between the two algorithms is that for the same
+configuration parameters, velocity control will eliminate or drastically reduce
+overshoot, as seen in the graphs below, as it isn’t susceptible to integral
+wind-up.
+
+![algorithm comparison](img/pid_03.png)
+
+![zoomed algorithm comparison](img/pid_04.png)
+
+In some scenarios Velocity control will also be better at holding the heater at
+its target temperature, and rejecting disturbances. The primary reason for this
+is that velocity control is more like a standard second order differential
+equation. It takes into account position, velocity, and acceleration.
diff --git a/docs/Z_Calibration.md b/docs/Z_Calibration.md
new file mode 100644
index 000000000..38e8e78a7
--- /dev/null
+++ b/docs/Z_Calibration.md
@@ -0,0 +1,289 @@
+# Automatic Z-Offset Calibration
+
+This document provides information on calibrating the nozzle's Z offset
+automatically. With this enabled, manual Z offset or first layer
+calibrations are needless. It computes always the correct offset independantly
+of the current temperature, used nozzle or used print bed or flex plate.
+
+# Why This
+
+- The Z endstop used in Voron V1 or V2 printers is a clever one because the
+ nozzle clicks on a switch which is fixed to the print bed. This enables the
+ exchange of nozzles without changing the offset (between switch and bed):
+ ![endstop offset](img/z_calibrate-endstop.png)
+- Or, by using a surface probing probe like a mag-probe as the Z endstop.
+ This enables the exchange of flex plates without adapting the offset:
+ ![probe offset](img/z_calibrate-probe.png)
+ An inductive probe would not work, since it does not probe the surface of
+ the bed directly!
+- But, isn't it possible to get both of it?
+
+It is possible and this it is what this extension does!
+
+# Requirements
+
+But, there are some requirements to use it:
+
+- A Z endstop where the tip of the nozzle drives on a switch (like the stock
+ Voron V1/V2 enstop). It will not work with the virtual pin of the probe
+ configured as endstop!
+- A (magnetic) switch based probe at the print head
+- Both, the Z endstop and mag-probe are configured properly and homing and any
+ kind of bed leveling are working.
+- Attach and detach macros of the mag-probe are needed for this configuration.
+
+# What It Does
+
+1. A normal homing of all axes using the Z endstop for Z (this is not part of
+ this plugin). After that, there is a defined zero point in Z. From now on,
+ everything is in relation to this point. So, a new homing would change
+ everything, since the homing is not that precise.
+2. Determine the height of the nozzle by probing the tip of it on the Z endstop
+ (like the homing in step 1. But this one can result in a slightly different
+ value):
+ ![nozzle position](img/z_calibrate-nozzle.png)
+3. Determine the height of the mag-probe by probing the body of the switch on
+ the z-endstop:
+ ![switch position](img/z_calibrate-switch.png)
+4. Calculate the offset between the tip of the nozzle and the trigger point of
+ the mag-probe:
+
+ `nozzle switch offset = mag probe height - nozzle height + switch offset`
+
+ ![switch offset](img/z_calibrate-offset.png)
+
+ The trigger point of the mag-probe cannot be probed directly. This is why
+ the body of the switch is clicked on the endstop indstead of the trigger
+ nob. This is why a small switch offset is used here to reflect the offset
+ between the nob and the body of the switch while it is triggerd.
+ This offset is fixed.
+5. Determine the height of the print surface by probing one point with the
+ mag-probe on the bed (preferably the center or the
+ "bed_mesh:relative_reference_index" of a configured/used mesh).
+6. Now, the final offset is calculated like this:
+
+ `probe offset = probed height - calculated nozzle switch offset`
+
+7. Finally, the calculated offset is applied by using the `SET_GCODE_OFFSET`
+ command (a previous offset is resetted before!).
+
+## Interference
+
+Temperature or humindity changes are not a big deal since the switch is not
+affected much by them and all values are probed in a small time period and only
+the releations to each other are used. The nozzle height in step 2 can be
+determined some time later and even many celsius higher in the printer's
+chamber, compared to the homing in step 1. That is why the nozzle is probed
+again and can vary a little to the first homing position.
+
+## Example Output
+
+The output of the calibration with all determined positions looks like this
+(the offset is the one which is applied as GCode offset):
+
+```
+Z-CALIBRATION: ENDSTOP=-0.300 NOZZLE=-0.300 SWITCH=6.208 PROBE=7.013 --> OFFSET=-0.170
+```
+
+The endstop value is the homed Z position which is always zero or the configure
+"stepper_z:position_endstop" setting - and in this case, it's even the same as
+the probed nozzle hight.
+
+# Configuration
+
+To activate the extension, a `[z_calibration]` section is needed in the printer
+configuration. The configuration properties are described
+[here](Config_Reference.md#z_calibration) in details.
+
+## Switch Offset
+
+The "z_calibration:switch_offset" is the already mentioned offset from the
+switch body (which is the probed position) to the actual trigger point above
+it. A starting point for this value can be taken from the datasheet like from
+the Omron switch (D2F-5: 0.5mm and SSG-5H: 0.7mm). It's good to start with a
+little less depending on the squishiness you prefer for the first layer (it's
+about 0.45 for the D2F-5). So, with a smaller offset value, the nozzle is more
+away from the bed! The value cannot be negative.
+
+For example, the datasheet of the D2F-5:
+
+![endstop offset](img/z_calibrate-d2f.png)
+
+And the calculation of the offset base:
+
+```
+offset base = OP (Operation Position) - switch body height
+ 0.5 mm = 5.5 mm - 5 mm
+```
+
+## Attaching and Detaching the Probe
+
+The attaching and detaching of the mag-probe can be done by creating a macro
+for the `CALIBRATE_Z` command and surround it by the appropriate commands:
+
+```
+[gcode_macro CALIBRATE_Z]
+description: Automatically calibrates the nozzles offset to the print surface and dock/undock MagProbe
+rename_existing: CALIBRATE_Z_BASE
+gcode:
+ ATTACH_PROBE # replace with the name of your specific attach macro
+ CALIBRATE_Z_BASE
+ DETACH_PROBE # replace with the name of your specific detach macro
+```
+
+It is also possible to use the `start_gcode` and `end_gcode` properties to
+call the attach and detach commands instead:
+
+```
+[z_calibration]
+...
+start_gcode: ATTACH_PROBE # replace with the name of your specific attach macro
+end_gcode: DETACH_PROBE # replace with the name of your specific detach macro
+```
+
+If there are any space restrictions and it is not possible to probe the nozzle
+on the endstop with the probe attached, the `before_switch_gcode` property can
+be used to attach the probe instead of the `start_gcode`. Then, the probe is
+not attached until the probe is probed on the endstop:
+
+```
+[z_calibration]
+...
+before_switch_gcode: ATTACH_PROBE # replace with the name of your specific attach macro
+end_gcode: DETACH_PROBE # replace with the name of your specific detach macro
+```
+
+## Bed Mesh
+
+If a bed mesh is used, the coordinates for probing on the print bed must be
+exactly the relative reference index point of the mesh since this is the point
+zero of the mesh! But, it is possible to omit these properties completely and
+the relative reference index point of the mesh will be taken automatically (for
+this, the "bed_mesh:relative_reference_index" setting is required and there is
+no support for round bed/mesh so far)!
+
+# How To Test It
+
+Do not bother too much about absolute values of the calculated offsets. These
+can vary a lot. Only the real position from the nozzle to the bed counts. To
+test this, the result of the calibration can be queried by `GET_POSITION`
+first:
+
+```
+> CALIBRATE_Z
+> Z-CALIBRATION: ENDSTOP=-0.300 NOZZLE=-0.267 SWITCH=2.370 PROBE=3.093 --> OFFSET=-0.010000
+> GET_POSITION
+> mcu: stepper_x:17085 stepper_y:15625 stepper_z:-51454 stepper_z1:-51454 stepper_z2:-51454 stepper_z3:-51454
+> stepper: stepper_x:552.500000 stepper_y:-47.500000 stepper_z:10.022500 stepper_z1:10.022500 stepper_z2:10.022500 stepper_z3:10.022500
+> kinematic: X:252.500000 Y:300.000000 Z:10.022500
+> toolhead: X:252.500000 Y:300.000000 Z:10.021472 E:0.000000
+> gcode: X:252.500000 Y:300.000000 Z:9.990000 E:0.000000
+> gcode base: X:0.000000 Y:0.000000 Z:-0.010000 E:0.000000
+> gcode homing: X:0.000000 Y:0.000000 Z:-0.010000
+```
+
+Here, the Z position in "gcode base" reflects the calibrated Z offset.
+
+Then, the offset can be tested by moving the nozzle slowly down to zero by
+moving it in multiple steps. It's good to do this by using GCodes, since
+the offset is applied as GCode-Offset. For example like this:
+
+```
+> G90
+> G0 Z5
+> G0 Z3
+> G0 Z1
+> G0 Z0.5
+> G0 Z0.3
+> G0 Z0.1
+```
+
+Check the distance to the print surface after every step. If there is a small
+discrepancy (which should be smaller than the offset base from the switch's
+datasheet), then adapt the "z_calibration:switch_offset" by that value.
+Decreasing the "switch_offset" will move the nozzle more away from the bed.
+
+And finally, if you have double checked, that the calibrated offset is correct,
+you can go for fine tuning the "z_calibration:switch_offset" by actually
+printing first layer tests. This needs to be done only once!
+
+# How To Use It
+
+## Command CALIBRATE_Z
+
+The calibration is started by using the `CALIBRATE_Z` command. There are no
+more parameters. A clean nozzle is needed for running this command.
+
+It does not matter when this calibration is called (and at what temperatures).
+But, it is importaint to call it just before starting a print when the printer
+is hot. So, it is good to add the `CALIBRATE_Z` command to the `PRINT_START`
+macro (which is called from the slicers start gCode). The sequence of this
+macro can look like this:
+
+1. Home all axes
+2. Heat up the bed and nozzle (and chamber)
+3. Get probe, make any bed leveling if needed (like QGL, Z-Tilt), park probe
+4. Purge and clean the nozzle
+5. Get probe, CALIBRATE_Z, park probe
+6. (Adjust Z offset if needed)
+7. Print intro line if used
+8. Start printing...
+
+**:exclamation: And remove any old Z offset adjustments here
+(like `SET_GCODE_OFFSET`)**
+
+For textured print surfaces, it might be necessary to go closer to the bed.
+To adjust the offset from the slicers start GCode, the following command can be
+added to the `PRINT_START` macro **after** calling the Z calibration:
+
+```
+ # Adjust the G-Code Z offset if needed
+ SET_GCODE_OFFSET Z_ADJUST={params.Z_ADJUST|default(0.0)|float} MOVE=1
+```
+
+Then, a `Z_ADJUST=0.0` can be added to the `PRINT_START` command in the Slicer.
+This does **not** reset the offset to this value but adjusts it by the given
+amount!
+
+>**NOTE:** Do not home Z again after running the Z calibration or it needs to
+> be executed again!
+
+## Command PROBE_Z_ACCURACY
+
+There is also a `PROBE_Z_ACCURACY` command to test the accuracy of the Z
+endstop (like the `PROBE_ACCURACY` command of the probe):
+
+```
+PROBE_Z_ACCURACY [PROBE_SPEED=] [LIFT_SPEED=] [SAMPLES=] [SAMPLE_RETRACT_DIST=]
+```
+
+It calculates the maximum, minimum, average, median and standard deviation of
+multiple probe samles on the endstop by taking the configured nozzle position
+on the endstop. The optional parameters default to their equivalent setting in
+the z_calibration config section.
+
+## Ooze Mitigation
+
+Ooze with any nozzle probe endstop can cause inaccuracies, as the filament will continue to leak or
+deform over the space of multiple probes. It is highly recommended to take some measures to prevent
+ooze buildup before the nozzle probe portion of this plugin.
+
+A slow long retraction, of as much as 15mm at print end, can reduce the potential for ooze. If you do
+this, consider adding a comparable extrude as the last command in your print start sequence to bring
+the plastic back to the tip. (Retracts longer than 5mm have been linked to clogs in many hotends,
+especially the Rapido. This may be best considered a last resort, depending on exact hardware and
+filament.)
+
+Heating the nozzle about a minute before scrubbing - using a purge bucket - will allow all the
+remaining plastic time to drain from the nozzle and be cleaned away by a simple wipe. If using a
+purge and scrubbing bucket, do not purge filament at this stage.
+
+An endstop switch that requires a stronger activation force, such as sexbolt with a spring, or
+unklicky z, can help squash any remaining ooze and improve consistency.
+
+Probing can be done with a hotend temperature below the full temperature for the print. If you print
+at 250, you can preheat the nozzle to 180, and run this script before finishing the heat up to full
+temperature. This may have varying effects depending on temperatures used.
+
+Also consider picking up your probe prior to your nozzle wipe, to allow this script to probe the
+nozzle immediately after cleaning it.
\ No newline at end of file
diff --git a/docs/img/pid_01.png b/docs/img/pid_01.png
new file mode 100644
index 000000000..75d55f646
Binary files /dev/null and b/docs/img/pid_01.png differ
diff --git a/docs/img/pid_02.png b/docs/img/pid_02.png
new file mode 100644
index 000000000..a4651dbce
Binary files /dev/null and b/docs/img/pid_02.png differ
diff --git a/docs/img/pid_03.png b/docs/img/pid_03.png
new file mode 100644
index 000000000..3fd35ef0e
Binary files /dev/null and b/docs/img/pid_03.png differ
diff --git a/docs/img/pid_04.png b/docs/img/pid_04.png
new file mode 100644
index 000000000..8c1f97eff
Binary files /dev/null and b/docs/img/pid_04.png differ
diff --git a/docs/img/z_calibrate-d2f.png b/docs/img/z_calibrate-d2f.png
new file mode 100644
index 000000000..0a073a2e3
Binary files /dev/null and b/docs/img/z_calibrate-d2f.png differ
diff --git a/docs/img/z_calibrate-endstop.png b/docs/img/z_calibrate-endstop.png
new file mode 100644
index 000000000..2114e9dae
Binary files /dev/null and b/docs/img/z_calibrate-endstop.png differ
diff --git a/docs/img/z_calibrate-nozzle.png b/docs/img/z_calibrate-nozzle.png
new file mode 100644
index 000000000..67a292f4d
Binary files /dev/null and b/docs/img/z_calibrate-nozzle.png differ
diff --git a/docs/img/z_calibrate-offset.png b/docs/img/z_calibrate-offset.png
new file mode 100644
index 000000000..23e1f961f
Binary files /dev/null and b/docs/img/z_calibrate-offset.png differ
diff --git a/docs/img/z_calibrate-probe.png b/docs/img/z_calibrate-probe.png
new file mode 100644
index 000000000..fa20566a6
Binary files /dev/null and b/docs/img/z_calibrate-probe.png differ
diff --git a/docs/img/z_calibrate-switch.png b/docs/img/z_calibrate-switch.png
new file mode 100644
index 000000000..00b372a33
Binary files /dev/null and b/docs/img/z_calibrate-switch.png differ
diff --git a/docs/resources/pid_params.xls b/docs/resources/pid_params.xls
new file mode 100644
index 000000000..cb347110f
Binary files /dev/null and b/docs/resources/pid_params.xls differ
diff --git a/klippy/configfile.py b/klippy/configfile.py
index e3800c7a0..dc7215da7 100644
--- a/klippy/configfile.py
+++ b/klippy/configfile.py
@@ -266,6 +266,8 @@ def __init__(self, printer):
self.status_save_pending = {}
self.status_settings = {}
self.status_warnings = []
+ self.unused_sections = []
+ self.unused_options = []
self.save_config_pending = False
gcode = self.printer.lookup_object("gcode")
gcode.register_command(
@@ -426,7 +428,7 @@ def read_main_config(self):
cfg = self._build_config_wrapper(regular_data + autosave_data, filename)
return cfg
- def check_unused_options(self, config):
+ def check_unused_options(self, config, error_on_unused):
fileconfig = config.fileconfig
objects = dict(self.printer.lookup_objects())
# Determine all the fields that have been accessed
@@ -439,16 +441,23 @@ def check_unused_options(self, config):
for section_name in fileconfig.sections():
section = section_name.lower()
if section not in valid_sections and section not in objects:
- raise error(
- "Section '%s' is not a valid config section" % (section,)
- )
+ if error_on_unused:
+ raise error(
+ "Section '%s' is not a valid config section"
+ % (section,)
+ )
+ else:
+ self.unused_sections.append(section)
for option in fileconfig.options(section_name):
option = option.lower()
if (section, option) not in access_tracking:
- raise error(
- "Option '%s' is not valid in section '%s'"
- % (option, section)
- )
+ if error_on_unused:
+ raise error(
+ "Option '%s' is not valid in section '%s'"
+ % (option, section)
+ )
+ else:
+ self.unused_options.append((section, option))
# Setup get_status()
self._build_status(config)
@@ -483,6 +492,20 @@ def _build_status(self, config):
res["section"] = section
res["option"] = option
self.status_warnings.append(res)
+ for section, option in self.unused_options:
+ res = {"type": "unused_option"}
+ res["message"] = "Option '%s' in section '%s' is invalid" % (
+ option,
+ section,
+ )
+ res["section"] = section
+ res["option"] = option
+ self.status_warnings.append(res)
+ for section in self.unused_sections:
+ res = {"type": "unused_section"}
+ res["message"] = "Section '%s' is invalid" % (section,)
+ res["section"] = section
+ self.status_warnings.append(res)
def get_status(self, eventtime):
return {
diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py
index 23e4ea4aa..f67786fff 100644
--- a/klippy/extras/bed_mesh.py
+++ b/klippy/extras/bed_mesh.py
@@ -161,7 +161,9 @@ def __init__(self, config):
def handle_connect(self):
self.toolhead = self.printer.lookup_object("toolhead")
- self.bmc.print_generated_points(logging.info)
+ self.danger_options = self.printer.lookup_object("danger_options")
+ if self.danger_options.log_bed_mesh_at_startup:
+ self.bmc.print_generated_points(logging.info)
def set_mesh(self, mesh):
if mesh is not None and self.fade_end != self.FADE_DISABLE:
diff --git a/klippy/extras/danger_options.py b/klippy/extras/danger_options.py
new file mode 100644
index 000000000..c1724223c
--- /dev/null
+++ b/klippy/extras/danger_options.py
@@ -0,0 +1,17 @@
+class DangerOptions:
+ def __init__(self, config):
+ self.log_statistics = config.getboolean("log_statistics", True)
+ self.log_config_file_at_startup = config.getboolean(
+ "log_config_file_at_startup", True
+ )
+ self.log_bed_mesh_at_startup = config.getboolean(
+ "log_bed_mesh_at_startup", True
+ )
+ self.log_shutdown_info = config.getboolean("log_shutdown_info", True)
+ self.error_on_unused_config_options = config.getboolean(
+ "error_on_unused_config_options", True
+ )
+
+
+def load_config(config):
+ return DangerOptions(config)
diff --git a/klippy/extras/homing.py b/klippy/extras/homing.py
index a0add5f93..12decd06d 100644
--- a/klippy/extras/homing.py
+++ b/klippy/extras/homing.py
@@ -4,11 +4,13 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math
+import logging
HOMING_START_DELAY = 0.001
ENDSTOP_SAMPLE_TIME = 0.000015
ENDSTOP_SAMPLE_COUNT = 4
+
# Return a completion that completes when all completions in a list complete
def multi_complete(printer, completions):
if len(completions) == 1:
@@ -47,6 +49,7 @@ def __init__(self, printer, endstops, toolhead=None):
toolhead = printer.lookup_object("toolhead")
self.toolhead = toolhead
self.stepper_positions = []
+ self.distance_elapsed = []
def get_mcu_endstops(self):
return [es for es, name in self.endstops]
@@ -116,6 +119,7 @@ def homing_move(
)
endstop_triggers.append(wait)
all_endstop_trigger = multi_complete(self.printer, endstop_triggers)
+
self.toolhead.dwell(HOMING_START_DELAY)
# Issue move
error = None
@@ -157,6 +161,16 @@ def homing_move(
sp.stepper_name: sp.halt_pos - sp.trig_pos
for sp in self.stepper_positions
}
+ steps_moved = {
+ sp.stepper_name: (sp.halt_pos - sp.start_pos)
+ * sp.stepper.get_step_dist()
+ for sp in self.stepper_positions
+ }
+ filled_steps_moved = {
+ sname: steps_moved.get(sname, 0)
+ for sname in [s.get_name() for s in kin.get_steppers()]
+ }
+ self.distance_elapsed = kin.calc_position(filled_steps_moved)
if any(over_steps.values()):
self.toolhead.set_position(movepos)
halt_kin_spos = {
@@ -216,10 +230,38 @@ def _fill_coord(self, coord):
def set_homed_position(self, pos):
self.toolhead.set_position(self._fill_coord(pos))
+ def _set_current_homing(self, homing_axes):
+ print_time = self.toolhead.get_last_move_time()
+ affected_rails = set()
+ for axis in homing_axes:
+ axis_name = "xyz"[axis] # only works for cartesian
+ partial_rails = self.toolhead.get_active_rails_for_axis(axis_name)
+ affected_rails = affected_rails | set(partial_rails)
+
+ for rail in affected_rails:
+ ch = rail.get_tmc_current_helper()
+ if ch is not None:
+ ch.set_current_for_homing(print_time)
+ self.toolhead.dwell(0.5)
+
+ def _set_current_post_homing(self, homing_axes):
+ print_time = self.toolhead.get_last_move_time()
+ affected_rails = set()
+ for axis in homing_axes:
+ axis_name = "xyz"[axis] # only works for cartesian
+ partial_rails = self.toolhead.get_active_rails_for_axis(axis_name)
+ affected_rails = affected_rails | set(partial_rails)
+
+ for rail in affected_rails:
+ ch = rail.get_tmc_current_helper()
+ if ch is not None:
+ ch.set_current_for_normal(print_time)
+ self.toolhead.dwell(0.5)
+
def home_rails(self, rails, forcepos, movepos):
# Notify of upcoming homing operation
self.printer.send_event("homing:home_rails_begin", self, rails)
- # Alter kinematics class to think printer is at forcepos
+ # Alter kinematics class to think printer is at forcepo
homing_axes = [axis for axis in range(3) if forcepos[axis] is not None]
startpos = self._fill_coord(forcepos)
homepos = self._fill_coord(movepos)
@@ -228,9 +270,24 @@ def home_rails(self, rails, forcepos, movepos):
endstops = [es for rail in rails for es in rail.get_endstops()]
hi = rails[0].get_homing_info()
hmove = HomingMove(self.printer, endstops)
+
+ self._set_current_homing(homing_axes)
+
hmove.homing_move(homepos, hi.speed)
+
+ homing_axis_distances = [
+ dist
+ for i, dist in enumerate(hmove.distance_elapsed)
+ if i in homing_axes
+ ]
+
# Perform second home
if hi.retract_dist:
+ needs_rehome = False
+ if any([dist < hi.retract_dist for dist in homing_axis_distances]):
+ needs_rehome = True
+
+ logging.info("needs rehome: %s", needs_rehome)
# Retract
startpos = self._fill_coord(forcepos)
homepos = self._fill_coord(movepos)
@@ -241,18 +298,37 @@ def home_rails(self, rails, forcepos, movepos):
hp - ad * retract_r for hp, ad in zip(homepos, axes_d)
]
self.toolhead.move(retractpos, hi.retract_speed)
- # Home again
- startpos = [
- rp - ad * retract_r for rp, ad in zip(retractpos, axes_d)
- ]
- self.toolhead.set_position(startpos)
- hmove = HomingMove(self.printer, endstops)
- hmove.homing_move(homepos, hi.second_homing_speed)
- if hmove.check_no_movement() is not None:
- raise self.printer.command_error(
- "Endstop %s still triggered after retract"
- % (hmove.check_no_movement(),)
- )
+ if not hi.use_sensorless_homing or needs_rehome:
+ self.toolhead.dwell(0.5)
+ # Home again
+ startpos = [
+ rp - ad * retract_r for rp, ad in zip(retractpos, axes_d)
+ ]
+ self.toolhead.set_position(startpos)
+ print_time = self.toolhead.get_last_move_time()
+ for endstop in endstops:
+ # re-querying a tmc endstop seems to reset the state
+ # otherwise it triggers almost immediately upon second home
+ endstop[0].query_endstop(print_time)
+ hmove = HomingMove(self.printer, endstops)
+ hmove.homing_move(homepos, hi.second_homing_speed)
+ if hmove.check_no_movement() is not None:
+ raise self.printer.command_error(
+ "Endstop %s still triggered after retract"
+ % (hmove.check_no_movement(),)
+ )
+
+ # Retract (again)
+ startpos = self._fill_coord(forcepos)
+ homepos = self._fill_coord(movepos)
+ axes_d = [hp - sp for hp, sp in zip(homepos, startpos)]
+ move_d = math.sqrt(sum([d * d for d in axes_d[:3]]))
+ retract_r = min(1.0, hi.retract_dist / move_d)
+ retractpos = [
+ hp - ad * retract_r for hp, ad in zip(homepos, axes_d)
+ ]
+ self.toolhead.move(retractpos, hi.retract_speed)
+ self._set_current_post_homing(homing_axes)
# Signal home operation complete
self.toolhead.flush_step_generation()
self.trigger_mcu_pos = {
diff --git a/klippy/extras/pid_calibrate.py b/klippy/extras/pid_calibrate.py
index 994bb32da..138da4fcd 100644
--- a/klippy/extras/pid_calibrate.py
+++ b/klippy/extras/pid_calibrate.py
@@ -23,13 +23,14 @@ def cmd_PID_CALIBRATE(self, gcmd):
heater_name = gcmd.get("HEATER")
target = gcmd.get_float("TARGET")
write_file = gcmd.get_int("WRITE_FILE", 0)
+ tolerance = gcmd.get_float("TOLERANCE", TUNE_PID_TOL, above=0.0)
pheaters = self.printer.lookup_object("heaters")
try:
heater = pheaters.lookup_heater(heater_name)
except self.printer.config_error as e:
raise gcmd.error(str(e))
self.printer.lookup_object("toolhead").get_last_move_time()
- calibrate = ControlAutoTune(heater, target)
+ calibrate = ControlAutoTune(heater, target, tolerance)
old_control = heater.set_control(calibrate)
try:
pheaters.set_temperature(heater, target, True)
@@ -38,11 +39,11 @@ def cmd_PID_CALIBRATE(self, gcmd):
raise
heater.set_control(old_control)
if write_file:
- calibrate.write_file("/tmp/heattest.txt")
+ calibrate.write_file("/tmp/heattest.csv")
if calibrate.check_busy(0.0, 0.0, 0.0):
raise gcmd.error("pid_calibrate interrupted")
# Log and report results
- Kp, Ki, Kd = calibrate.calc_final_pid()
+ Kp, Ki, Kd = calibrate.calc_pid()
logging.info("Autotune: final: Kp=%f Ki=%f Kd=%f", Kp, Ki, Kd)
gcmd.respond_info(
"PID parameters: pid_Kp=%.3f pid_Ki=%.3f pid_Kd=%.3f\n"
@@ -59,116 +60,252 @@ def cmd_PID_CALIBRATE(self, gcmd):
TUNE_PID_DELTA = 5.0
+TUNE_PID_TOL = 0.02
+TUNE_PID_SAMPLES = 3
+TUNE_PID_MAX_PEAKS = 60
class ControlAutoTune:
- def __init__(self, heater, target):
+ def __init__(self, heater, target, tolerance):
self.heater = heater
self.heater_max_power = heater.get_max_power()
- self.calibrate_temp = target
- # Heating control
+ # store the reference so we can push messages if needed
+ self.gcode = heater.printer.lookup_object("gcode")
+ # holds the various max power settings used during the test.
+ self.powers = [self.heater_max_power]
+ # holds the times the power setting was changed.
+ self.times = []
+ # the target temperature to tune for
+ self.target = target
+ # the tolerance that determines if the system has converged to an
+ # acceptable level
+ self.tolerance = tolerance
+ # the the temp that determines when to turn the heater off
+ self.temp_high = target + TUNE_PID_DELTA / 2.0
+ # the the temp that determines when to turn the heater on
+ self.temp_low = target - TUNE_PID_DELTA / 2.0
+ # is the heater on
self.heating = False
- self.peak = 0.0
- self.peak_time = 0.0
- # Peak recording
+ # the current potential peak value
+ self.peak = self.target
+ # the time values associated with the current potential peak
+ self.peak_times = []
+ # known peaks and their associated time values
self.peaks = []
- # Sample recording
- self.last_pwm = 0.0
- self.pwm_samples = []
- self.temp_samples = []
-
- # Heater control
- def set_pwm(self, read_time, value):
- if value != self.last_pwm:
- self.pwm_samples.append(
- (read_time + self.heater.get_pwm_delay(), value)
- )
- self.last_pwm = value
- self.heater.set_pwm(read_time, value)
+ # has the target temp been crossed at-least once
+ self.target_crossed = False
+ # has the tuning processed finished
+ self.done = False
+ # has the tuning processed started
+ self.started = False
+ # did an error happen
+ self.errored = False
+ # data from the test that can be optionally written to a file
+ self.data = []
def temperature_update(self, read_time, temp, target_temp):
- self.temp_samples.append((read_time, temp))
- # Check if the temperature has crossed the target and
- # enable/disable the heater if so.
- if self.heating and temp >= target_temp:
+ # tuning is done, so don't do any more calculations
+ if self.done:
+ return
+ # store test data
+ self.data.append(
+ (read_time, temp, self.heater.last_pwm_value, self.target)
+ )
+ # ensure the starting temp is low enough to run the test.
+ if not self.started and temp >= self.temp_low:
+ self.errored = True
+ self.finish(read_time)
+ self.gcode.respond_info("temperature to high to start calibration")
+ return
+ else:
+ self.started = True
+ # ensure the test doesn't run to long
+ if float(len(self.peaks)) > TUNE_PID_MAX_PEAKS:
+ self.errored = True
+ self.finish(read_time)
+ self.gcode.respond_info("calibration did not finish in time")
+ return
+ # indicate that the target temp has been crossed at-least once
+ if temp > self.target and self.target_crossed is False:
+ self.target_crossed = True
+ # only do work if the target temp has been crossed at-least once
+ if self.target_crossed:
+ # check for a new peak value
+ if temp > self.temp_high or temp < self.temp_low:
+ self.check_peak(read_time, temp)
+ # it's time to calculate and store a high peak
+ if self.peak > self.temp_high and temp < self.target:
+ self.store_peak()
+ # it's time to calculate and store a low peak
+ if self.peak < self.temp_low and temp > self.target:
+ self.store_peak()
+ # check if the conditions are right to evaluate a new sample
+ peaks = float(len(self.peaks)) - 1.0
+ powers = float(len(self.powers))
+ if (peaks % 2.0) == 0.0 and (powers * 2.0) == peaks:
+ self.log_info()
+ # check for convergence
+ if self.converged():
+ self.finish(read_time)
+ return
+ self.set_power()
+ # turn the heater off
+ if self.heating and temp >= self.temp_high:
self.heating = False
- self.check_peaks()
- self.heater.alter_target(self.calibrate_temp - TUNE_PID_DELTA)
- elif not self.heating and temp <= target_temp:
+ self.times.append(read_time)
+ self.heater.alter_target(self.temp_low)
+ # turn the heater on
+ if not self.heating and temp <= self.temp_low:
self.heating = True
- self.check_peaks()
- self.heater.alter_target(self.calibrate_temp)
- # Check if this temperature is a peak and record it if so
+ self.times.append(read_time)
+ self.heater.alter_target(self.temp_high)
+ # set the pwm output based on the heater state
if self.heating:
- self.set_pwm(read_time, self.heater_max_power)
- if temp < self.peak:
- self.peak = temp
- self.peak_time = read_time
+ self.heater.set_pwm(read_time, self.powers[-1])
else:
- self.set_pwm(read_time, 0.0)
- if temp > self.peak:
- self.peak = temp
- self.peak_time = read_time
+ self.heater.set_pwm(read_time, 0)
- def check_busy(self, eventtime, smoothed_temp, target_temp):
- if self.heating or len(self.peaks) < 12:
+ def check_peak(self, time, temp):
+ # deal with duplicate temps
+ if temp == self.peak:
+ self.peak_times.append(time)
+ # deal with storing high peak values
+ if temp > self.target and temp > self.peak:
+ self.peak = temp
+ self.peak_times = [time]
+ # deal with storing low peak values
+ if temp < self.target and temp < self.peak:
+ self.peak = temp
+ self.peak_times = [time]
+
+ def store_peak(self):
+ time = sum(self.peak_times) / float(len(self.peak_times))
+ self.peaks.append((time, self.peak))
+ self.peak = self.target
+ self.peak_times = []
+
+ def log_info(self):
+ # provide some useful info to the user
+ sample = len(self.powers)
+ pwm = self.powers[-1]
+ asymmetry = (self.peaks[-2][1] + self.peaks[-1][1]) / 2.0 - self.target
+ tolerance = self.get_sample_tolerance()
+ if tolerance is False:
+ fmt = "sample:%d pwm:%.4f asymmetry:%.4f tolerance:n/a\n"
+ data = (sample, pwm, asymmetry)
+ self.gcode.respond_info(fmt % data)
+ else:
+ fmt = "sample:%d pwm:%.4f asymmetry:%.4f tolerance:%.4f\n"
+ data = (sample, pwm, asymmetry, tolerance)
+ self.gcode.respond_info(fmt % data)
+
+ def get_sample_tolerance(self):
+ powers = len(self.powers)
+ if powers < TUNE_PID_SAMPLES + 1:
+ return False
+ powers = self.powers[-1 * (TUNE_PID_SAMPLES + 1) :]
+ return max(powers) - min(powers)
+
+ def converged(self):
+ tolerance = self.get_sample_tolerance()
+ if tolerance is False:
+ return False
+ if tolerance <= self.tolerance:
return True
return False
- # Analysis
- def check_peaks(self):
- self.peaks.append((self.peak, self.peak_time))
- if self.heating:
- self.peak = 9999999.0
- else:
- self.peak = -9999999.0
- if len(self.peaks) < 4:
+ def set_power(self):
+ peak_low = self.peaks[-2][1]
+ peak_high = self.peaks[-1][1]
+ power = self.powers[-1]
+ mid = power * ((self.target - peak_low) / (peak_high - peak_low))
+ if mid * 2.0 > self.heater_max_power:
+ # the new power is to high so just return max power
+ self.powers.append(self.heater_max_power)
return
- self.calc_pid(len(self.peaks) - 1)
+ self.powers.append(mid * 2.0)
+
+ def finish(self, time):
+ self.heater.set_pwm(time, 0)
+ self.heater.alter_target(0)
+ self.done = True
+ self.heating = False
+
+ def check_busy(self, eventtime, smoothed_temp, target_temp):
+ if eventtime == 0.0 and smoothed_temp == 0.0 and target_temp == 0.0:
+ if self.errored:
+ return True
+ return False
+ if self.done:
+ return False
+ return True
- def calc_pid(self, pos):
- temp_diff = self.peaks[pos][0] - self.peaks[pos - 1][0]
- time_diff = self.peaks[pos][1] - self.peaks[pos - 2][1]
- # Use Astrom-Hagglund method to estimate Ku and Tu
+ def write_file(self, filename):
+ f = open(filename, "w")
+ f.write("time, temp, pwm, target\n")
+ data = [
+ "%.5f, %.5f, %.5f, %.5f" % (time, temp, pwm, target)
+ for time, temp, pwm, target in self.data
+ ]
+ f.write("\n".join(data))
+ peaks = self.peaks[:]
+ powers = self.powers[:]
+ # pop off the
+ peaks.pop(0)
+ samples = []
+ for i in range(len(powers)):
+ samples.append(
+ (
+ i,
+ peaks[i * 2][0],
+ peaks[i * 2][1],
+ peaks[i * 2 + 1][0],
+ peaks[i * 2 + 1][1],
+ powers[i],
+ )
+ )
+ f.write("\nsample, low time, low, high time, high, max power\n")
+ data = [
+ "%.5f, %.5f, %.5f, %.5f, %.5f, %.5f"
+ % (sample, low_time, low, high_time, high, max_power)
+ for sample, low_time, low, high_time, high, max_power in samples
+ ]
+ f.write("\n".join(data))
+ f.close()
+
+ def calc_pid(self):
+ temp_diff = 0.0
+ time_diff = 0.0
+ theta = 0.0
+ for i in range(1, TUNE_PID_SAMPLES * 2, 2):
+ temp_diff = temp_diff + self.peaks[-i][1] - self.peaks[-i - 1][1]
+ time_diff = time_diff + self.peaks[-i][0] - self.peaks[-i - 2][0]
+ theta = theta + self.peaks[-i][0] - self.times[-i]
+ temp_diff = temp_diff / float(TUNE_PID_SAMPLES)
+ time_diff = time_diff / float(TUNE_PID_SAMPLES)
+ theta = theta / float(TUNE_PID_SAMPLES)
amplitude = 0.5 * abs(temp_diff)
- Ku = 4.0 * self.heater_max_power / (math.pi * amplitude)
+ power = self.powers[-1 * (TUNE_PID_SAMPLES) :]
+ power = sum(power) / float(len(power))
+ # calculate the various parameters
+ Ku = 4.0 * power / (math.pi * amplitude)
Tu = time_diff
+ Wu = (2.0 * math.pi) / Tu
+ tau = math.tan(math.pi - theta * Wu) / Wu
+ Km = -math.sqrt(tau**2 * Wu**2 + 1.0) / Ku
+ # log the extra details
+ logging.info("Ziegler-Nichols constants: Ku=%f Tu=%f", Ku, Tu)
+ logging.info(
+ "Cohen-Coon constants: Km=%f Theta=%f Tau=%f", Km, theta, tau
+ )
# Use Ziegler-Nichols method to generate PID parameters
Ti = 0.5 * Tu
Td = 0.125 * Tu
Kp = 0.6 * Ku * heaters.PID_PARAM_BASE
Ki = Kp / Ti
Kd = Kp * Td
- logging.info(
- "Autotune: raw=%f/%f Ku=%f Tu=%f Kp=%f Ki=%f Kd=%f",
- temp_diff,
- self.heater_max_power,
- Ku,
- Tu,
- Kp,
- Ki,
- Kd,
- )
return Kp, Ki, Kd
- def calc_final_pid(self):
- cycle_times = [
- (self.peaks[pos][1] - self.peaks[pos - 2][1], pos)
- for pos in range(4, len(self.peaks))
- ]
- midpoint_pos = sorted(cycle_times)[len(cycle_times) // 2][1]
- return self.calc_pid(midpoint_pos)
-
- # Offline analysis helper
- def write_file(self, filename):
- pwm = [
- "pwm: %.3f %.3f" % (time, value) for time, value in self.pwm_samples
- ]
- out = ["%.3f %.3f" % (time, temp) for time, temp in self.temp_samples]
- f = open(filename, "w")
- f.write("\n".join(pwm + out))
- f.close()
-
def load_config(config):
return PIDCalibrate(config)
diff --git a/klippy/extras/statistics.py b/klippy/extras/statistics.py
index 2fd18dc19..e3ca9acab 100644
--- a/klippy/extras/statistics.py
+++ b/klippy/extras/statistics.py
@@ -61,6 +61,7 @@ def get_status(self, eventtime):
class PrinterStats:
def __init__(self, config):
self.printer = config.get_printer()
+ self.danger_options = self.printer.lookup_object("danger_options")
reactor = self.printer.get_reactor()
self.stats_timer = reactor.register_timer(self.generate_stats)
self.stats_cb = []
@@ -78,7 +79,7 @@ def handle_ready(self):
def generate_stats(self, eventtime):
stats = [cb(eventtime) for cb in self.stats_cb]
- if max([s[0] for s in stats]):
+ if max([s[0] for s in stats]) and self.danger_options.log_statistics:
logging.info(
"Stats %.1f: %s", eventtime, " ".join([s[1] for s in stats])
)
diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py
index 28b4ea130..cb8f4662c 100644
--- a/klippy/extras/tmc.py
+++ b/klippy/extras/tmc.py
@@ -11,6 +11,7 @@
# Field helpers
######################################################################
+
# Return the position of the first bit set in a mask
def ffs(mask):
return (mask & -mask).bit_length() - 1
@@ -345,29 +346,53 @@ def cmd_SET_TMC_FIELD(self, gcmd):
def cmd_SET_TMC_CURRENT(self, gcmd):
ch = self.current_helper
- prev_cur, prev_hold_cur, req_hold_cur, max_cur = ch.get_current()
+ (
+ prev_cur,
+ prev_hold_cur,
+ req_hold_cur,
+ max_cur,
+ prev_home_cur,
+ ) = ch.get_current()
run_current = gcmd.get_float(
"CURRENT", None, minval=0.0, maxval=max_cur
)
hold_current = gcmd.get_float(
"HOLDCURRENT", None, above=0.0, maxval=max_cur
)
- if run_current is not None or hold_current is not None:
+ home_current = gcmd.get_float(
+ "HOMECURRENT", None, above=0.0, maxval=max_cur
+ )
+ if (
+ run_current is not None
+ or hold_current is not None
+ or home_current is not None
+ ):
if run_current is None:
run_current = prev_cur
if hold_current is None:
hold_current = req_hold_cur
+ if home_current is not None:
+ ch.set_home_current(home_current)
toolhead = self.printer.lookup_object("toolhead")
print_time = toolhead.get_last_move_time()
ch.set_current(run_current, hold_current, print_time)
- prev_cur, prev_hold_cur, req_hold_cur, max_cur = ch.get_current()
+ (
+ prev_cur,
+ prev_hold_cur,
+ req_hold_cur,
+ max_cur,
+ prev_home_cur,
+ ) = ch.get_current()
# Report values
if prev_hold_cur is None:
- gcmd.respond_info("Run Current: %0.2fA" % (prev_cur,))
+ gcmd.respond_info(
+ "Run Current: %0.2fA Home Current: %0.2fA"
+ % (prev_cur, prev_home_cur)
+ )
else:
gcmd.respond_info(
- "Run Current: %0.2fA Hold Current: %0.2fA"
- % (prev_cur, prev_hold_cur)
+ "Run Current: %0.2fA Hold Current: %0.2fA Home Current: %0.2fA"
+ % (prev_cur, prev_hold_cur, prev_home_cur)
)
# Stepper phase tracking
@@ -451,6 +476,8 @@ def _handle_mcu_identify(self):
# Lookup stepper object
force_move = self.printer.lookup_object("force_move")
self.stepper = force_move.lookup_stepper(self.stepper_name)
+ self.stepper.set_tmc_current_helper(self.current_helper)
+
# Note pulse duration and step_both_edge optimizations available
self.stepper.setup_default_pulse_duration(0.000000100, True)
@@ -551,6 +578,7 @@ def cmd_DUMP_TMC(self, gcmd):
# TMC virtual pins
######################################################################
+
# Helper class for "sensorless homing"
class TMCVirtualPinHelper:
def __init__(self, config, mcu_tmc):
@@ -636,6 +664,7 @@ def handle_homing_move_end(self, hmove):
# Config reading helpers
######################################################################
+
# Helper to initialize the wave table from config or defaults
def TMCWaveTableHelper(config, mcu_tmc):
set_config_field = mcu_tmc.get_fields().set_config_field
diff --git a/klippy/extras/tmc2130.py b/klippy/extras/tmc2130.py
index c7325b8dc..374a53608 100644
--- a/klippy/extras/tmc2130.py
+++ b/klippy/extras/tmc2130.py
@@ -203,21 +203,30 @@ def __init__(self, config, mcu_tmc):
self.name = config.get_name().split()[-1]
self.mcu_tmc = mcu_tmc
self.fields = mcu_tmc.get_fields()
- run_current = config.getfloat(
+ self.run_current = config.getfloat(
"run_current", above=0.0, maxval=MAX_CURRENT
)
- hold_current = config.getfloat(
+ self.hold_current = config.getfloat(
"hold_current", MAX_CURRENT, above=0.0, maxval=MAX_CURRENT
)
- self.req_hold_current = hold_current
+ self._home_current = config.getfloat(
+ "home_current", self.run_current, above=0.0, maxval=MAX_CURRENT
+ )
+ self.prev_current = self.run_current
+ self.req_hold_current = self.hold_current
self.sense_resistor = config.getfloat(
"sense_resistor", 0.110, above=0.0
)
- vsense, irun, ihold = self._calc_current(run_current, hold_current)
+ vsense, irun, ihold = self._calc_current(
+ self.run_current, self.hold_current
+ )
self.fields.set_field("vsense", vsense)
self.fields.set_field("ihold", ihold)
self.fields.set_field("irun", irun)
+ def set_home_current(self, new_home_current):
+ self._home_current = min(MAX_CURRENT, new_home_current)
+
def _calc_current_bits(self, current, vsense):
sense_resistor = self.sense_resistor + 0.020
vref = 0.32
@@ -256,7 +265,13 @@ def get_current(self):
vsense = self.fields.get_field("vsense")
run_current = self._calc_current_from_bits(irun, vsense)
hold_current = self._calc_current_from_bits(ihold, vsense)
- return run_current, hold_current, self.req_hold_current, MAX_CURRENT
+ return (
+ run_current,
+ hold_current,
+ self.req_hold_current,
+ MAX_CURRENT,
+ self._home_current,
+ )
def set_current(self, run_current, hold_current, print_time):
self.req_hold_current = hold_current
@@ -268,6 +283,14 @@ def set_current(self, run_current, hold_current, print_time):
val = self.fields.set_field("irun", irun)
self.mcu_tmc.set_register("IHOLD_IRUN", val, print_time)
+ def set_current_for_homing(self, print_time):
+ prev_run_cur, _, _, _, _ = self.get_current()
+ self._prev_current = prev_run_cur
+ self.set_current(self._home_current, self.hold_current, print_time)
+
+ def set_current_for_normal(self, print_time):
+ self.set_current(self._prev_current, self.hold_current, print_time)
+
######################################################################
# TMC2130 SPI
diff --git a/klippy/extras/tmc2240.py b/klippy/extras/tmc2240.py
index f75edf369..14ba8fa67 100644
--- a/klippy/extras/tmc2240.py
+++ b/klippy/extras/tmc2240.py
@@ -271,19 +271,30 @@ def __init__(self, config, mcu_tmc):
self.Rref = config.getfloat(
"rref", 12000.0, minval=12000.0, maxval=60000.0
)
- max_cur = self._get_ifs_rms(3)
- run_current = config.getfloat("run_current", above=0.0, maxval=max_cur)
- hold_current = config.getfloat(
- "hold_current", max_cur, above=0.0, maxval=max_cur
+ self.max_cur = self._get_ifs_rms(3)
+ self.run_current = config.getfloat(
+ "run_current", above=0.0, maxval=self.max_cur
)
- self.req_hold_current = hold_current
- current_range = self._calc_current_range(run_current)
+ self.hold_current = config.getfloat(
+ "hold_current", self.max_cur, above=0.0, maxval=self.max_cur
+ )
+ self._home_current = config.getfloat(
+ "home_current", self.run_current, above=0.0, maxval=self.max_cur
+ )
+ self.prev_current = self.run_current
+ self.req_hold_current = self.hold_current
+ current_range = self._calc_current_range(self.run_current)
self.fields.set_field("current_range", current_range)
- gscaler, irun, ihold = self._calc_current(run_current, hold_current)
+ gscaler, irun, ihold = self._calc_current(
+ self.run_current, self.hold_current
+ )
self.fields.set_field("globalscaler", gscaler)
self.fields.set_field("ihold", ihold)
self.fields.set_field("irun", irun)
+ def set_home_current(self, new_home_current):
+ self._home_current = min(self.max_cur, new_home_current)
+
def _get_ifs_rms(self, current_range=None):
if current_range is None:
current_range = self.fields.get_field("current_range")
@@ -331,7 +342,13 @@ def get_current(self):
ifs_rms = self._get_ifs_rms()
run_current = self._calc_current_from_field("irun")
hold_current = self._calc_current_from_field("ihold")
- return (run_current, hold_current, self.req_hold_current, ifs_rms)
+ return (
+ run_current,
+ hold_current,
+ self.req_hold_current,
+ ifs_rms,
+ self._home_current,
+ )
def set_current(self, run_current, hold_current, print_time):
self.req_hold_current = hold_current
@@ -342,6 +359,14 @@ def set_current(self, run_current, hold_current, print_time):
val = self.fields.set_field("irun", irun)
self.mcu_tmc.set_register("IHOLD_IRUN", val, print_time)
+ def set_current_for_homing(self, print_time):
+ prev_run_cur, _, _, _, _ = self.get_current()
+ self._prev_current = prev_run_cur
+ self.set_current(self._home_current, self.hold_current, print_time)
+
+ def set_current_for_normal(self, print_time):
+ self.set_current(self._prev_current, self.hold_current, print_time)
+
######################################################################
# TMC2240 printer object
diff --git a/klippy/extras/tmc2660.py b/klippy/extras/tmc2660.py
index d6eb0fbec..85a891969 100644
--- a/klippy/extras/tmc2660.py
+++ b/klippy/extras/tmc2660.py
@@ -123,6 +123,10 @@ def __init__(self, config, mcu_tmc):
self.current = config.getfloat(
"run_current", minval=0.1, maxval=MAX_CURRENT
)
+ self._home_current = config.getfloat(
+ "home_current", self.current, above=0.0, maxval=MAX_CURRENT
+ )
+ self._prev_current = self.current
self.sense_resistor = config.getfloat("sense_resistor")
vsense, cs = self._calc_current(self.current)
self.fields.set_field("cs", cs)
@@ -140,6 +144,9 @@ def __init__(self, config, mcu_tmc):
"idle_timeout:ready", self._handle_ready
)
+ def set_home_current(self, new_home_current):
+ self._home_current = min(MAX_CURRENT, new_home_current)
+
def _calc_current_bits(self, current, vsense):
vref = 0.165 if vsense else 0.310
sr = self.sense_resistor
@@ -185,17 +192,32 @@ def _update_current(self, current, print_time):
self.mcu_tmc.set_register("DRVCONF", val, print_time)
def get_current(self):
- return self.current, None, None, MAX_CURRENT
+ return (
+ self.current,
+ None,
+ None,
+ MAX_CURRENT,
+ self._home_current,
+ )
def set_current(self, run_current, hold_current, print_time):
self.current = run_current
self._update_current(run_current, print_time)
+ def set_current_for_homing(self, print_time):
+ prev_run_cur, _, _, _, _ = self.get_current()
+ self._prev_current = prev_run_cur
+ self.set_current(self._home_current, None, print_time)
+
+ def set_current_for_normal(self, print_time):
+ self.set_current(self._prev_current, None, print_time)
+
######################################################################
# TMC2660 SPI
######################################################################
+
# Helper code for working with TMC2660 devices via SPI
class MCU_TMC2660_SPI:
def __init__(self, config, name_to_reg, fields):
diff --git a/klippy/extras/tmc5160.py b/klippy/extras/tmc5160.py
index 80e1b3f6c..ecf8d3ac8 100644
--- a/klippy/extras/tmc5160.py
+++ b/klippy/extras/tmc5160.py
@@ -256,21 +256,30 @@ def __init__(self, config, mcu_tmc):
self.name = config.get_name().split()[-1]
self.mcu_tmc = mcu_tmc
self.fields = mcu_tmc.get_fields()
- run_current = config.getfloat(
+ self.run_current = config.getfloat(
"run_current", above=0.0, maxval=MAX_CURRENT
)
- hold_current = config.getfloat(
+ self.hold_current = config.getfloat(
"hold_current", MAX_CURRENT, above=0.0, maxval=MAX_CURRENT
)
- self.req_hold_current = hold_current
+ self._home_current = config.getfloat(
+ "home_current", self.run_current, above=0.0, maxval=MAX_CURRENT
+ )
+ self._prev_current = self.run_current
+ self.req_hold_current = self.hold_current
self.sense_resistor = config.getfloat(
"sense_resistor", 0.075, above=0.0
)
- gscaler, irun, ihold = self._calc_current(run_current, hold_current)
+ gscaler, irun, ihold = self._calc_current(
+ self.run_current, self.hold_current
+ )
self.fields.set_field("globalscaler", gscaler)
self.fields.set_field("ihold", ihold)
self.fields.set_field("irun", irun)
+ def set_home_current(self, new_home_current):
+ self._home_current = min(MAX_CURRENT, new_home_current)
+
def _calc_globalscaler(self, current):
globalscaler = int(
(current * 256.0 * math.sqrt(2.0) * self.sense_resistor / VREF)
@@ -313,7 +322,13 @@ def _calc_current_from_field(self, field_name):
def get_current(self):
run_current = self._calc_current_from_field("irun")
hold_current = self._calc_current_from_field("ihold")
- return run_current, hold_current, self.req_hold_current, MAX_CURRENT
+ return (
+ run_current,
+ hold_current,
+ self.req_hold_current,
+ MAX_CURRENT,
+ self._home_current,
+ )
def set_current(self, run_current, hold_current, print_time):
self.req_hold_current = hold_current
@@ -324,6 +339,14 @@ def set_current(self, run_current, hold_current, print_time):
val = self.fields.set_field("irun", irun)
self.mcu_tmc.set_register("IHOLD_IRUN", val, print_time)
+ def set_current_for_homing(self, print_time):
+ prev_run_cur, _, _, _, _ = self.get_current()
+ self._prev_current = prev_run_cur
+ self.set_current(self._home_current, self.hold_current, print_time)
+
+ def set_current_for_normal(self, print_time):
+ self.set_current(self._prev_current, self.hold_current, print_time)
+
######################################################################
# TMC5160 printer object
diff --git a/klippy/extras/z_calibration.py b/klippy/extras/z_calibration.py
new file mode 100644
index 000000000..324c629bb
--- /dev/null
+++ b/klippy/extras/z_calibration.py
@@ -0,0 +1,589 @@
+# Klipper plugin for a self-calibrating Z offset.
+#
+# Copyright (C) 2021-2023 Titus Meyer
+#
+# This file may be distributed under the terms of the GNU GPLv3 license.
+import logging
+from mcu import MCU_endstop
+
+
+class ZCalibrationHelper:
+ def __init__(self, config):
+ self.state = None
+ self.z_endstop = None
+ self.z_homing = None
+ self.last_state = False
+ self.last_z_offset = 0.0
+ self.position_z_endstop = None
+ self.config = config
+ self.printer = config.get_printer()
+ self.switch_offset = config.getfloat("switch_offset", 0.0, above=0.0)
+ # max_deviation is deprecated
+ self.max_deviation = config.getfloat("max_deviation", None, above=0.0)
+ config.deprecate("max_deviation")
+ self.offset_margins = self._get_offset_margins(
+ "offset_margins", "-1.0,1.0"
+ )
+ self.speed = config.getfloat("speed", 50.0, above=0.0)
+ # clearance is deprecated
+ self.clearance = config.getfloat("clearance", None, above=0.0)
+ config.deprecate("clearance")
+ self.safe_z_height = config.getfloat("safe_z_height", None, above=0.0)
+ self.samples = config.getint("samples", None, minval=1)
+ self.tolerance = config.getfloat("samples_tolerance", None, above=0.0)
+ self.retries = config.getint(
+ "samples_tolerance_retries", None, minval=0
+ )
+ atypes = {"none": None, "median": "median", "average": "average"}
+ self.samples_result = config.getchoice("samples_result", atypes, "none")
+ self.lift_speed = config.getfloat("lift_speed", None, above=0.0)
+ self.probing_speed = config.getfloat("probing_speed", None, above=0.0)
+ self.second_speed = config.getfloat(
+ "probing_second_speed", None, above=0.0
+ )
+ self.retract_dist = config.getfloat(
+ "probing_retract_dist", None, above=0.0
+ )
+ self.position_min = config.getfloat("position_min", None)
+ self.first_fast = config.getboolean("probing_first_fast", False)
+ self.nozzle_site = self._get_xy("nozzle_xy_position", True)
+ self.switch_site = self._get_xy("switch_xy_position", True)
+ self.switch_xy_offsets = self._get_xy("switch_xy_offsets", True)
+ self.bed_site = self._get_xy("bed_xy_position", True)
+ self.wiggle_offsets = self._get_xy("wiggle_xy_offsets", True)
+ gcode_macro = self.printer.load_object(config, "gcode_macro")
+ self.start_gcode = gcode_macro.load_template(config, "start_gcode", "")
+ self.switch_gcode = gcode_macro.load_template(
+ config, "before_switch_gcode", ""
+ )
+ self.end_gcode = gcode_macro.load_template(config, "end_gcode", "")
+ self.query_endstops = self.printer.load_object(config, "query_endstops")
+ self.printer.register_event_handler(
+ "klippy:connect", self.handle_connect
+ )
+ self.printer.register_event_handler(
+ "homing:home_rails_end", self.handle_home_rails_end
+ )
+ self.gcode = self.printer.lookup_object("gcode")
+ self.gcode.register_command(
+ "CALIBRATE_Z", self.cmd_CALIBRATE_Z, desc=self.cmd_CALIBRATE_Z_help
+ )
+ self.gcode.register_command(
+ "PROBE_Z_ACCURACY",
+ self.cmd_PROBE_Z_ACCURACY,
+ desc=self.cmd_PROBE_Z_ACCURACY_help,
+ )
+ self.gcode.register_command(
+ "CALCULATE_SWITCH_OFFSET",
+ self.cmd_CALCULATE_SWITCH_OFFSET,
+ desc=self.cmd_CALCULATE_SWITCH_OFFSET_help,
+ )
+
+ def get_status(self, eventtime):
+ return {
+ "last_query": self.last_state,
+ "last_z_offset": self.last_z_offset,
+ }
+
+ def handle_connect(self):
+ # get z-endstop object
+ for endstop, name in self.query_endstops.endstops:
+ if name == "z":
+ # check for virtual endstops..
+ if not isinstance(endstop, MCU_endstop):
+ raise self.printer.config_error(
+ "A virtual endstop for z"
+ " is not supported for %s" % (self.config.get_name())
+ )
+ self.z_endstop = EndstopWrapper(self.config, endstop)
+ # get z-endstop position from safe_z_home
+ if self.nozzle_site is None:
+ safe_z_home = self.printer.lookup_object(
+ "safe_z_home", default=None
+ )
+ if safe_z_home is None:
+ raise self.printer.config_error(
+ "No nozzle position"
+ " configured for %s" % (self.config.get_name())
+ )
+ self.nozzle_site = [
+ safe_z_home.home_x_pos,
+ safe_z_home.home_y_pos,
+ None,
+ ]
+ # check/calculate switch position by offsets
+ if self.switch_site is None:
+ if self.switch_xy_offsets is None:
+ raise self.printer.config_error(
+ "No switch position"
+ " configured for %s" % (self.config.get_name())
+ )
+ self.switch_site = [
+ self.nozzle_site[0] + self.switch_xy_offsets[0],
+ self.nozzle_site[1] + self.switch_xy_offsets[1],
+ None,
+ ]
+ # get probing settings
+ probe = self.printer.lookup_object("probe", default=None)
+ if probe is None:
+ raise self.printer.config_error(
+ "A probe is needed for %s" % (self.config.get_name())
+ )
+ if self.samples is None:
+ self.samples = probe.sample_count
+ if self.tolerance is None:
+ self.tolerance = probe.samples_tolerance
+ if self.retries is None:
+ self.retries = probe.samples_retries
+ if self.lift_speed is None:
+ self.lift_speed = probe.lift_speed
+ # clearance is deprecated
+ if self.clearance is not None and self.clearance == 0:
+ self.clearance = 20 # defaults to 20mm
+ if self.safe_z_height is None:
+ self.safe_z_height = probe.z_offset * 2
+ if self.safe_z_height < 3:
+ self.safe_z_height = 20 # defaults to 20mm
+ if self.samples_result is None:
+ self.samples_result = probe.samples_result
+
+ def handle_home_rails_end(self, homing_state, rails):
+ # get z homing position
+ for rail in rails:
+ if rail.get_steppers()[0].is_active_axis("z"):
+ # get homing settings from z rail
+ self.z_homing = rail.position_endstop
+ if self.probing_speed is None:
+ self.probing_speed = rail.homing_speed
+ if self.second_speed is None:
+ self.second_speed = rail.second_homing_speed
+ if self.retract_dist is None:
+ self.retract_dist = rail.homing_retract_dist
+ if self.position_min is None:
+ self.position_min = rail.position_min
+ self.position_z_endstop = rail.position_endstop
+
+ def _build_config(self):
+ pass
+
+ cmd_CALIBRATE_Z_help = (
+ "Automatically calibrates the nozzle offset" " to the print surface"
+ )
+
+ def cmd_CALIBRATE_Z(self, gcmd):
+ if self.z_homing is None:
+ raise gcmd.error("Must home axes first")
+ site_attr = gcmd.get("BED_POSITION", None)
+ if site_attr is not None:
+ # set bed site from BED_POSITION parameter
+ self.bed_site = self._parse_xy("BED_POSITION", site_attr)
+ elif self._get_xy("bed_xy_position", True) is not None:
+ # set bed site from configuration
+ self.bed_site = self._get_xy("bed_xy_position", False)
+ else:
+ # else get the mesh's zero reference position
+ try:
+ mesh = self.printer.lookup_object("bed_mesh", default=None)
+ if (
+ hasattr(mesh.bmc, "zero_ref_pos")
+ and mesh.bmc.zero_ref_pos is not None
+ ):
+ self.bed_site = mesh.bmc.zero_ref_pos
+ else:
+ # trying to read the deprecated rri
+ rri = mesh.bmc.relative_reference_index
+ self.bed_site = mesh.bmc.points[rri]
+ logging.debug(
+ "Z-CALIBRATION probe bed_x=%.3f bed_y=%.3f"
+ % (self.bed_site[0], self.bed_site[1])
+ )
+ except:
+ raise gcmd.error(
+ "Either use the BED_POSITION parameter,"
+ " configure a bed_xy_position or define"
+ " a mesh with a zero_reference_position"
+ " for %s" % (self.config.get_name())
+ )
+ self._log_config()
+ state = CalibrationState(self, gcmd)
+ state.calibrate_z()
+
+ cmd_PROBE_Z_ACCURACY_help = (
+ "Probe Z-Endstop accuracy at" " Nozzle-Endstop position"
+ )
+
+ def cmd_PROBE_Z_ACCURACY(self, gcmd):
+ if self.z_homing is None:
+ raise gcmd.error("Must home axes first")
+ speed = gcmd.get_float("PROBE_SPEED", self.second_speed, above=0.0)
+ lift_speed = gcmd.get_float("LIFT_SPEED", self.lift_speed, above=0.0)
+ sample_count = gcmd.get_int("SAMPLES", self.samples, minval=1)
+ sample_retract_dist = gcmd.get_float(
+ "SAMPLE_RETRACT_DIST", self.retract_dist, above=0.0
+ )
+ toolhead = self.printer.lookup_object("toolhead")
+ pos = toolhead.get_position()
+ self._move_safe_z(pos, lift_speed)
+ # move to z-endstop position
+ self._move(list(self.nozzle_site), self.speed)
+ pos = toolhead.get_position()
+ gcmd.respond_info(
+ "PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f"
+ " (samples=%d retract=%.3f"
+ " speed=%.1f lift_speed=%.1f)\n"
+ % (
+ pos[0],
+ pos[1],
+ pos[2],
+ sample_count,
+ sample_retract_dist,
+ speed,
+ lift_speed,
+ )
+ )
+ # Probe bed sample_count times
+ positions = []
+ while len(positions) < sample_count:
+ # Probe position
+ pos = self._probe(self.z_endstop, self.position_min, speed)
+ positions.append(pos)
+ # Retract
+ liftpos = [None, None, pos[2] + sample_retract_dist]
+ self._move(liftpos, lift_speed)
+ # Calculate maximum, minimum and average values
+ max_value = max([p[2] for p in positions])
+ min_value = min([p[2] for p in positions])
+ range_value = max_value - min_value
+ avg_value = self._calc_mean(positions)[2]
+ median = self._calc_median(positions)[2]
+ # calculate the standard deviation
+ deviation_sum = 0
+ for i in range(len(positions)):
+ deviation_sum += pow(positions[i][2] - avg_value, 2.0)
+ sigma = (deviation_sum / len(positions)) ** 0.5
+ # Show information
+ gcmd.respond_info(
+ "probe accuracy results: maximum %.6f, minimum %.6f, range %.6f,"
+ " average %.6f, median %.6f, standard deviation %.6f"
+ % (max_value, min_value, range_value, avg_value, median, sigma)
+ )
+
+ cmd_CALCULATE_SWITCH_OFFSET_help = (
+ "Calculates a switch_offset based on" " the current z position"
+ )
+
+ def cmd_CALCULATE_SWITCH_OFFSET(self, gcmd):
+ if self.last_z_offset is None:
+ raise gcmd.error("Must run CALIBRATE_Z first")
+ toolhead = self.printer.lookup_object("toolhead")
+ pos = toolhead.get_position()
+ new_switch_offset = self.switch_offset - (pos[2] - self.last_z_offset)
+ if new_switch_offset > 0.0:
+ gcmd.respond_info(
+ "switch_offset=%.3f - (current_z=%.3f - z_offset=%.3f"
+ ") --> new switch_offset=%.3f"
+ % (
+ self.switch_offset,
+ pos[2],
+ self.last_z_offset,
+ new_switch_offset,
+ )
+ )
+ else:
+ gcmd.respond_info(
+ "The resulting switch offset is negative! Either"
+ " the nozzle is still too far away or something"
+ " else is wrong..."
+ )
+
+ def _get_xy(self, name, optional=False):
+ if optional and self.config.get(name, None) is None:
+ return None
+ else:
+ return self._parse_xy(name, self.config.get(name))
+
+ def _parse_xy(self, name, site):
+ try:
+ x_pos, y_pos = site.split(",")
+ return [float(x_pos), float(y_pos), None]
+ except:
+ raise self.config.error(
+ "Unable to parse %s in %s" % (name, self.config.get_name())
+ )
+
+ def _get_offset_margins(self, name, default):
+ try:
+ margins = self.config.get(name, default).split(",")
+ for i, val in enumerate(margins):
+ margins[i] = float(val)
+ if len(margins) == 1:
+ val = abs(margins[0])
+ margins[0] = -val
+ margins.append(val)
+ return margins
+ except:
+ raise self.config.error(
+ "Unable to parse %s in %s" % (name, self.config.get_name())
+ )
+
+ def _probe(self, mcu_endstop, z_position, speed, wiggle=False):
+ toolhead = self.printer.lookup_object("toolhead")
+ pos = toolhead.get_position()
+ pos[2] = z_position
+ # probe
+ phoming = self.printer.lookup_object("homing")
+ curpos = phoming.probing_move(mcu_endstop, pos, speed)
+ # retract
+ self._move([None, None, curpos[2] + self.retract_dist], self.lift_speed)
+ if wiggle and self.wiggle_offsets is not None:
+ self._move(
+ [
+ curpos[0] + self.wiggle_offsets[0],
+ curpos[1] + self.wiggle_offsets[1],
+ None,
+ ],
+ self.speed,
+ )
+ self._move([curpos[0], curpos[1], None], self.speed)
+ self.gcode.respond_info(
+ "probe at %.3f,%.3f is z=%.6f" % (curpos[0], curpos[1], curpos[2])
+ )
+ return curpos
+
+ def _move(self, coord, speed):
+ self.printer.lookup_object("toolhead").manual_move(coord, speed)
+
+ def _move_safe_z(self, pos, lift_speed):
+ # clearance is deprecated
+ if self.clearance is not None:
+ if pos[2] < self.clearance:
+ # no clearance, better to move up (relative)
+ self._move([None, None, pos[2] + self.clearance], lift_speed)
+ else:
+ if pos[2] < self.safe_z_height:
+ # no safe z position, better to move up (absolute)
+ self._move([None, None, self.safe_z_height], lift_speed)
+
+ def _calc_mean(self, positions):
+ count = float(len(positions))
+ return [sum([pos[i] for pos in positions]) / count for i in range(3)]
+
+ def _calc_median(self, positions):
+ z_sorted = sorted(positions, key=(lambda p: p[2]))
+ middle = len(positions) // 2
+ if (len(positions) & 1) == 1:
+ # odd number of samples
+ return z_sorted[middle]
+ # even number of samples
+ return self._calc_mean(z_sorted[middle - 1 : middle + 1])
+
+ def _log_config(self):
+ logging.debug(
+ "Z-CALIBRATION: switch_offset=%.3f,"
+ " offset_margins=%.3f,%.3f, speed=%.3f,"
+ " samples=%i, tolerance=%.3f, retries=%i,"
+ " samples_result=%s, lift_speed=%.3f,"
+ " safe_z_height=%.3f, probing_speed=%.3f,"
+ " second_speed=%.3f, retract_dist=%.3f,"
+ " position_min=%.3f, probe_nozzle_x=%.3f,"
+ " probe_nozzle_y=%.3f, probe_switch_x=%.3f,"
+ " probe_switch_y=%.3f, probe_bed_x=%.3f,"
+ " probe_bed_y=%.3f"
+ % (
+ self.switch_offset,
+ self.offset_margins[0],
+ self.offset_margins[1],
+ self.speed,
+ self.samples,
+ self.tolerance,
+ self.retries,
+ self.samples_result,
+ self.lift_speed,
+ self.safe_z_height,
+ self.probing_speed,
+ self.second_speed,
+ self.retract_dist,
+ self.position_min,
+ self.nozzle_site[0],
+ self.nozzle_site[1],
+ self.switch_site[0],
+ self.switch_site[1],
+ self.bed_site[0],
+ self.bed_site[1],
+ )
+ )
+
+
+class EndstopWrapper:
+ def __init__(self, config, endstop):
+ self.mcu_endstop = endstop
+ # Wrappers
+ self.get_mcu = self.mcu_endstop.get_mcu
+ self.add_stepper = self.mcu_endstop.add_stepper
+ self.get_steppers = self.mcu_endstop.get_steppers
+ self.home_start = self.mcu_endstop.home_start
+ self.home_wait = self.mcu_endstop.home_wait
+ self.query_endstop = self.mcu_endstop.query_endstop
+
+
+class CalibrationState:
+ def __init__(self, helper, gcmd):
+ self.helper = helper
+ self.gcmd = gcmd
+ self.gcode = helper.gcode
+ self.z_endstop = helper.z_endstop
+ self.probe = helper.printer.lookup_object("probe")
+ self.toolhead = helper.printer.lookup_object("toolhead")
+ self.gcode_move = helper.printer.lookup_object("gcode_move")
+ self.max_deviation = helper.max_deviation
+ self.offset_margins = helper.offset_margins
+
+ def _probe_on_site(
+ self, endstop, site, check_probe=False, split_xy=False, wiggle=False
+ ):
+ pos = self.toolhead.get_position()
+ self.helper._move_safe_z(pos, self.helper.lift_speed)
+ # move to position
+ if split_xy:
+ self.helper._move([site[0], pos[1], None], self.helper.speed)
+ self.helper._move([site[0], site[1], site[2]], self.helper.speed)
+ else:
+ self.helper._move(site, self.helper.speed)
+ if check_probe:
+ # check if probe is attached and switch is closed
+ time = self.toolhead.get_last_move_time()
+ if self.probe.mcu_probe.query_endstop(time):
+ raise self.helper.printer.command_error(
+ "Probe switch not" " closed - Probe not" " attached?"
+ )
+ if self.helper.first_fast:
+ # first probe just to get down faster
+ self.helper._probe(
+ endstop,
+ self.helper.position_min,
+ self.helper.probing_speed,
+ wiggle=wiggle,
+ )
+ retries = 0
+ positions = []
+ while len(positions) < self.helper.samples:
+ # probe with second probing speed
+ curpos = self.helper._probe(
+ endstop,
+ self.helper.position_min,
+ self.helper.second_speed,
+ wiggle=wiggle,
+ )
+ positions.append(curpos[:3])
+ # check tolerance
+ z_positions = [p[2] for p in positions]
+ if max(z_positions) - min(z_positions) > self.helper.tolerance:
+ if retries >= self.helper.retries:
+ self.helper.end_gcode.run_gcode_from_command()
+ raise self.gcmd.error("Probe samples exceed tolerance")
+ self.gcmd.respond_info(
+ "Probe samples exceed tolerance." " Retrying..."
+ )
+ retries += 1
+ positions = []
+ # calculate result
+ if self.helper.samples_result == "median":
+ return self.helper._calc_median(positions)[2]
+ return self.helper._calc_mean(positions)[2]
+
+ def _add_probe_offset(self, site):
+ # calculate bed position by using the probe's offsets
+ probe_offsets = self.probe.get_offsets()
+ probe_site = list(site)
+ probe_site[0] -= probe_offsets[0]
+ probe_site[1] -= probe_offsets[1]
+ return probe_site
+
+ def _set_new_gcode_offset(self, offset):
+ # reset gcode z offset to 0
+ gcmd_offset = self.gcode.create_gcode_command(
+ "SET_GCODE_OFFSET", "SET_GCODE_OFFSET", {"Z": 0.0}
+ )
+ self.gcode_move.cmd_SET_GCODE_OFFSET(gcmd_offset)
+ # set new gcode z offset
+ gcmd_offset = self.gcode.create_gcode_command(
+ "SET_GCODE_OFFSET", "SET_GCODE_OFFSET", {"Z_ADJUST": offset}
+ )
+ self.gcode_move.cmd_SET_GCODE_OFFSET(gcmd_offset)
+
+ def calibrate_z(self):
+ self.helper.start_gcode.run_gcode_from_command()
+ # probe the nozzle
+ nozzle_zero = self._probe_on_site(
+ self.z_endstop,
+ self.helper.nozzle_site,
+ check_probe=False,
+ split_xy=True,
+ wiggle=True,
+ )
+ # probe the probe-switch
+ self.helper.switch_gcode.run_gcode_from_command()
+ # probe the body of the switch
+ switch_zero = self._probe_on_site(
+ self.z_endstop, self.helper.switch_site, check_probe=True
+ )
+ # probe position on bed
+ probe_site = self._add_probe_offset(self.helper.bed_site)
+ probe_zero = self._probe_on_site(
+ self.probe.mcu_probe, probe_site, check_probe=True
+ )
+ # calculate the offset
+ offset = probe_zero - (
+ switch_zero - nozzle_zero + self.helper.switch_offset
+ )
+ # print result
+ self.gcmd.respond_info(
+ "Z-CALIBRATION: probe=%.3f - (switch=%.3f"
+ " - nozzle=%.3f + switch_offset=%.3f) -->"
+ " new offset=%.6f"
+ % (
+ probe_zero,
+ switch_zero,
+ nozzle_zero,
+ self.helper.switch_offset,
+ offset,
+ )
+ )
+ self.gcmd.respond_info(
+ "HINT: z position_endstop=%.3f - offset=%.6f"
+ " --> possible z position_endstop=%.3f"
+ % (
+ self.helper.position_z_endstop,
+ offset,
+ self.helper.position_z_endstop - offset,
+ )
+ )
+ # check offset margins
+ if (
+ self.max_deviation is not None # deprecated
+ and abs(offset) > self.max_deviation
+ ):
+ self.helper.end_gcode.run_gcode_from_command()
+ raise self.helper.printer.command_error(
+ "Offset is greater than"
+ " allowed: offset=%.3f"
+ " > max_deviation=%.3f" % (offset, self.max_deviation)
+ )
+ elif offset < self.offset_margins[0] or offset > self.offset_margins[1]:
+ self.helper.end_gcode.run_gcode_from_command()
+ raise self.helper.printer.command_error(
+ "Offset %.3f is outside"
+ " the configured range of"
+ " min=%.3f and max=%.3f"
+ % (offset, self.offset_margins[0], self.offset_margins[1])
+ )
+ # set new offset
+ self._set_new_gcode_offset(offset)
+ # set states
+ self.helper.last_state = True
+ self.helper.last_z_offset = offset
+ self.helper.end_gcode.run_gcode_from_command()
+
+
+def load_config(config):
+ return ZCalibrationHelper(config)
diff --git a/klippy/klippy.py b/klippy/klippy.py
index 01f3d5450..65c3ee527 100644
--- a/klippy/klippy.py
+++ b/klippy/klippy.py
@@ -154,7 +154,11 @@ def load_object(self, config, section, default=configfile.sentinel):
def _read_config(self):
self.objects["configfile"] = pconfig = configfile.PrinterConfig(self)
config = pconfig.read_main_config()
- if self.bglogger is not None:
+ danger_options = self.load_object(config, "danger_options")
+ if (
+ self.bglogger is not None
+ and danger_options.log_config_file_at_startup
+ ):
pconfig.log_config(config)
# Create printer components
for m in [pins, mcu]:
@@ -164,7 +168,8 @@ def _read_config(self):
for m in [toolhead]:
m.add_printer_objects(config)
# Validate that there are no undefined parameters in the config file
- pconfig.check_unused_options(config)
+ error_on_unused = danger_options.error_on_unused_config_options
+ pconfig.check_unused_options(config, error_on_unused)
def _build_protocol_error_message(self, e):
host_version = self.start_args["software_version"]
@@ -428,6 +433,7 @@ def main():
bglogger = queuelogger.setup_bg_logging(options.logfile, debuglevel)
else:
logging.getLogger().setLevel(debuglevel)
+ logging.info("=======================")
logging.info("Starting Klippy...")
git_info = util.get_git_version()
git_vers = git_info["version"]
diff --git a/klippy/mcu.py b/klippy/mcu.py
index afbbbbc33..a13a65d45 100644
--- a/klippy/mcu.py
+++ b/klippy/mcu.py
@@ -18,6 +18,7 @@ class error(Exception):
# Command transmit helper classes
######################################################################
+
# Class to retry sending of a query command until a given response is received
class RetryAsyncCommand:
TIMEOUT_TIME = 5.0
@@ -745,6 +746,7 @@ class MCU:
def __init__(self, config, clocksync):
self._config = config
self._printer = printer = config.get_printer()
+ self.danger_options = printer.lookup_object("danger_options")
self._clocksync = clocksync
self._reactor = printer.get_reactor()
self._name = config.get_name()
@@ -846,14 +848,15 @@ def _handle_shutdown(self, params):
if clock is not None:
self._shutdown_clock = self.clock32_to_clock64(clock)
self._shutdown_msg = msg = params["static_string_id"]
- logging.info(
- "MCU '%s' %s: %s\n%s\n%s",
- self._name,
- params["#name"],
- self._shutdown_msg,
- self._clocksync.dump_debug(),
- self._serial.dump_debug(),
- )
+ if self.danger_options.log_shutdown_info:
+ logging.info(
+ "MCU '%s' %s: %s\n%s\n%s",
+ self._name,
+ params["#name"],
+ self._shutdown_msg,
+ self._clocksync.dump_debug(),
+ self._serial.dump_debug(),
+ )
prefix = "MCU '%s' shutdown: " % (self._name,)
if params["#name"] == "is_shutdown":
prefix = "Previous MCU '%s' shutdown: " % (self._name,)
diff --git a/klippy/stepper.py b/klippy/stepper.py
index eafab950b..77f3b26c5 100644
--- a/klippy/stepper.py
+++ b/klippy/stepper.py
@@ -18,6 +18,7 @@ class error(Exception):
MIN_BOTH_EDGE_DURATION = 0.000000200
+
# Interface to low-level mcu and chelper code
class MCU_stepper:
def __init__(
@@ -64,6 +65,13 @@ def __init__(
self._mcu.get_printer().register_event_handler(
"klippy:connect", self._query_mcu_position
)
+ self._tmc_current_helper = None
+
+ def get_tmc_current_helper(self):
+ return self._tmc_current_helper
+
+ def set_tmc_current_helper(self, tmc_current_helper):
+ self._tmc_current_helper = tmc_current_helper
def get_mcu(self):
return self._mcu
@@ -374,6 +382,7 @@ def parse_step_distance(config, units_in_radians=None, note_valid=False):
# Stepper controlled rails
######################################################################
+
# A motor control "rail" with one (or more) steppers and one (or more)
# endstops.
class PrinterRail:
@@ -394,6 +403,7 @@ def __init__(
self.get_name = mcu_stepper.get_name
self.get_commanded_position = mcu_stepper.get_commanded_position
self.calc_position_from_coord = mcu_stepper.calc_position_from_coord
+ self.get_tmc_current_helper = mcu_stepper.get_tmc_current_helper
# Primary endstop position
mcu_endstop = self.endstops[0][0]
if hasattr(mcu_endstop, "get_position_endstop"):
@@ -404,6 +414,12 @@ def __init__(
self.position_endstop = config.getfloat(
"position_endstop", default_position_endstop
)
+ endstop_pin = config.get("endstop_pin")
+ # check for ":virtual_endstop" to make sure we don't detect ":z_virtual_endstop"
+ endstop_is_virtual = (
+ endstop_pin is not None and ":virtual_endstop" in endstop_pin
+ )
+
# Axis range
if need_position_minmax:
self.position_min = config.getfloat("position_min", 0.0)
@@ -435,6 +451,10 @@ def __init__(
self.homing_positive_dir = config.getboolean(
"homing_positive_dir", None
)
+ self.use_sensorless_homing = config.getboolean(
+ "use_sensorless_homing", endstop_is_virtual
+ )
+
if self.homing_positive_dir is None:
axis_len = self.position_max - self.position_min
if self.position_endstop <= self.position_min + axis_len / 4.0:
@@ -472,6 +492,7 @@ def get_homing_info(self):
"retract_dist",
"positive_dir",
"second_homing_speed",
+ "use_sensorless_homing",
],
)(
self.homing_speed,
@@ -480,6 +501,7 @@ def get_homing_info(self):
self.homing_retract_dist,
self.homing_positive_dir,
self.second_homing_speed,
+ self.use_sensorless_homing,
)
return homing_info
diff --git a/klippy/toolhead.py b/klippy/toolhead.py
index 58f4c2368..4b30f3c23 100644
--- a/klippy/toolhead.py
+++ b/klippy/toolhead.py
@@ -11,6 +11,7 @@
# mm/second), _v2 is velocity squared (mm^2/s^2), _t is time (in
# seconds), _r is ratio (scalar between 0.0 and 1.0)
+
# Class to track each move request
class Move:
def __init__(self, toolhead, start_pos, end_pos, speed):
@@ -128,6 +129,7 @@ def set_junction(self, start_v2, cruise_v2, end_v2):
LOOKAHEAD_FLUSH_TIME = 0.250
+
# Class to track a list of pending move requests and to facilitate
# "look-ahead" across moves to reduce acceleration between moves.
class MoveQueue:
@@ -332,6 +334,17 @@ def __init__(self, config):
for module_name in modules:
self.printer.load_object(config, module_name)
+ def get_active_rails_for_axis(self, axis):
+ # axis is 'x,y,z'
+ active_rails = []
+ rails = self.kin.rails
+ for rail in rails:
+ for stepper in rail.get_steppers():
+ if stepper.is_active_axis(axis):
+ active_rails.append(rail)
+ break
+ return active_rails
+
# Print time tracking
def _update_move_time(self, next_print_time):
batch_time = MOVE_BATCH_TIME
diff --git a/test/klippy/danger_options.cfg b/test/klippy/danger_options.cfg
new file mode 100644
index 000000000..17e4e97d7
--- /dev/null
+++ b/test/klippy/danger_options.cfg
@@ -0,0 +1,84 @@
+
+[poopybutt]
+[danger_options]
+log_statistics: False
+log_config_file_at_startup: False
+error_on_unused_config_options: False
+log_bed_mesh_at_startup: False
+log_shutdown_info: False
+
+[stepper_x]
+step_pin: PF0
+dir_pin: PF1
+enable_pin: !PD7
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PE5
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_y]
+step_pin: PF6
+dir_pin: !PF7
+enable_pin: !PF2
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PJ1
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_z]
+step_pin: PL3
+dir_pin: PL1
+enable_pin: !PK0
+microsteps: 16
+rotation_distance: 8
+endstop_pin: probe:z_virtual_endstop
+position_max: 200
+
+[extruder]
+step_pin: PA4
+dir_pin: PA6
+enable_pin: !PA2
+microsteps: 16
+rotation_distance: 33.5
+nozzle_diameter: 0.400
+filament_diameter: 1.750
+heater_pin: PB4
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK5
+control: pid
+pid_Kp: 22.2
+pid_Ki: 1.08
+pid_Kd: 114
+min_temp: 0
+max_temp: 250
+
+[heater_bed]
+heater_pin: PH5
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK6
+control: watermark
+min_temp: 0
+max_temp: 130
+
+[probe]
+pin: PH6
+z_offset: 1.15
+drop_first_result: true
+
+[bed_mesh]
+mesh_min: 10,10
+mesh_max: 180,180
+
+[mcu]
+serial: /dev/ttyACM0
+
+[printer]
+kinematics: cartesian
+max_velocity: 300
+max_accel: 3000
+max_z_velocity: 5
+max_z_accel: 100
diff --git a/test/klippy/danger_options.test b/test/klippy/danger_options.test
new file mode 100644
index 000000000..b226ad6f0
--- /dev/null
+++ b/test/klippy/danger_options.test
@@ -0,0 +1,12 @@
+CONFIG danger_options.cfg
+DICTIONARY atmega2560.dict
+
+# Start by homing the printer.
+G28
+
+G1 F6000
+
+# Z / X / Y moves
+G1 Z1
+G1 X1
+G1 Y1
diff --git a/test/klippy/dockable_probe.cfg b/test/klippy/dockable_probe.cfg
new file mode 100644
index 000000000..c1b34db0d
--- /dev/null
+++ b/test/klippy/dockable_probe.cfg
@@ -0,0 +1,80 @@
+# Test config for probe:z_virtual_endstop
+[stepper_x]
+step_pin: PF0
+dir_pin: PF1
+enable_pin: !PD7
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PE5
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_y]
+step_pin: PF6
+dir_pin: !PF7
+enable_pin: !PF2
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PJ1
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_z]
+step_pin: PL3
+dir_pin: PL1
+enable_pin: !PK0
+microsteps: 16
+endstop_pin: ^PJ2
+position_endstop: 0
+rotation_distance: 8
+position_max: 200
+
+[extruder]
+step_pin: PA4
+dir_pin: PA6
+enable_pin: !PA2
+microsteps: 16
+rotation_distance: 33.5
+nozzle_diameter: 0.400
+filament_diameter: 1.750
+heater_pin: PB4
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK5
+control: pid
+pid_Kp: 22.2
+pid_Ki: 1.08
+pid_Kd: 114
+min_temp: 0
+max_temp: 250
+
+[heater_bed]
+heater_pin: PH5
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK6
+control: watermark
+min_temp: 0
+max_temp: 130
+
+[bed_mesh]
+mesh_min: 10,10
+mesh_max: 180,180
+
+[mcu]
+serial: /dev/ttyACM0
+
+[printer]
+kinematics: cartesian
+max_velocity: 300
+max_accel: 3000
+max_z_velocity: 5
+max_z_accel: 100
+
+[dockable_probe]
+dock_position: 100, 100
+approach_position: 150, 150
+detach_position: 120, 120
+pin: PH6
+z_offset: 1.15
+check_open_attach: false
diff --git a/test/klippy/dockable_probe.test b/test/klippy/dockable_probe.test
new file mode 100644
index 000000000..7e64e50b2
--- /dev/null
+++ b/test/klippy/dockable_probe.test
@@ -0,0 +1,30 @@
+# Test case for dockable_probe support
+CONFIG dockable_probe.cfg
+DICTIONARY atmega2560.dict
+
+# Start by homing the printer.
+G28
+G1 Z5 X10 Y10 F6000
+M400
+GET_POSITION
+
+# Z / X / Y moves
+#G1 Z1
+#G1 X1
+#G1 Y1
+
+# Move again
+#G1 Z5 X0 Y0
+
+# Test multiple dockable_probe commands without attaching the probe
+MOVE_TO_APPROACH_PROBE
+MOVE_TO_DOCK_PROBE
+MOVE_TO_EXTRACT_PROBE
+MOVE_TO_INSERT_PROBE
+MOVE_TO_DETACH_PROBE
+
+# Query the probe status
+QUERY_DOCKABLE_PROBE
+
+# Move again
+G1 Z9
diff --git a/test/klippy/fan_pwm_scaling.cfg b/test/klippy/fan_pwm_scaling.cfg
new file mode 100644
index 000000000..8c9c5908a
--- /dev/null
+++ b/test/klippy/fan_pwm_scaling.cfg
@@ -0,0 +1,65 @@
+# Config for extruder testing
+[stepper_x]
+step_pin: PF0
+dir_pin: PF1
+enable_pin: !PD7
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PE5
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_y]
+step_pin: PF6
+dir_pin: !PF7
+enable_pin: !PF2
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PJ1
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_z]
+step_pin: PL3
+dir_pin: PL1
+enable_pin: !PK0
+microsteps: 16
+rotation_distance: 8
+endstop_pin: ^PD3
+position_endstop: 0.5
+position_max: 200
+
+[extruder]
+step_pin: PA4
+dir_pin: PA6
+enable_pin: !PA2
+microsteps: 16
+rotation_distance: 33.5
+nozzle_diameter: 0.500
+filament_diameter: 3.500
+heater_pin: PB4
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK5
+control: pid
+pid_Kp: 22.2
+pid_Ki: 1.08
+pid_Kd: 114
+min_temp: 0
+max_temp: 210
+
+[fan]
+pin: PB5
+min_power: 0.1
+max_power: 1
+
+[mcu]
+serial: /dev/ttyACM0
+
+[printer]
+kinematics: cartesian
+max_velocity: 300
+max_accel: 3000
+max_z_velocity: 5
+max_z_accel: 100
diff --git a/test/klippy/fan_pwm_scaling.test b/test/klippy/fan_pwm_scaling.test
new file mode 100644
index 000000000..f726054fc
--- /dev/null
+++ b/test/klippy/fan_pwm_scaling.test
@@ -0,0 +1,16 @@
+# Pid hot modify tests
+DICTIONARY atmega2560.dict
+CONFIG fan_pwm_scaling.cfg
+
+# Extrude only
+G1 E5
+G1 E-2
+G1 E7
+
+# Home and extrusion moves
+G28
+G1 X20 Y20 Z1
+G1 X25 Y25 E7.5
+
+# Turn on the fan
+M106 S255
diff --git a/test/klippy/fan_reverse.cfg b/test/klippy/fan_reverse.cfg
new file mode 100644
index 000000000..97b8179a4
--- /dev/null
+++ b/test/klippy/fan_reverse.cfg
@@ -0,0 +1,72 @@
+# Config for extruder testing
+[stepper_x]
+step_pin: PF0
+dir_pin: PF1
+enable_pin: !PD7
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PE5
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_y]
+step_pin: PF6
+dir_pin: !PF7
+enable_pin: !PF2
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PJ1
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_z]
+step_pin: PL3
+dir_pin: PL1
+enable_pin: !PK0
+microsteps: 16
+rotation_distance: 8
+endstop_pin: ^PD3
+position_endstop: 0.5
+position_max: 200
+
+[extruder]
+step_pin: PA4
+dir_pin: PA6
+enable_pin: !PA2
+microsteps: 16
+rotation_distance: 33.5
+nozzle_diameter: 0.500
+filament_diameter: 3.500
+heater_pin: PB4
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK5
+control: pid
+pid_Kp: 22.2
+pid_Ki: 1.08
+pid_Kd: 114
+min_temp: 0
+max_temp: 210
+
+[temperature_fan my_temp_fan]
+pin: PB5
+reverse: true
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK6
+control: pid
+pid_Kp: 22.2
+pid_Ki: 1.08
+pid_Kd: 114
+min_temp: 0
+max_temp: 210
+
+[mcu]
+serial: /dev/ttyACM0
+
+[printer]
+kinematics: cartesian
+max_velocity: 300
+max_accel: 3000
+max_z_velocity: 5
+max_z_accel: 100
diff --git a/test/klippy/fan_reverse.test b/test/klippy/fan_reverse.test
new file mode 100644
index 000000000..dffd6f13c
--- /dev/null
+++ b/test/klippy/fan_reverse.test
@@ -0,0 +1,13 @@
+# Pid hot modify tests
+DICTIONARY atmega2560.dict
+CONFIG fan_reverse.cfg
+
+# Extrude only
+G1 E5
+G1 E-2
+G1 E7
+
+# Home and extrusion moves
+G28
+G1 X20 Y20 Z1
+G1 X25 Y25 E7.5
diff --git a/test/klippy/gcode_jinja2_ext_do.cfg b/test/klippy/gcode_jinja2_ext_do.cfg
new file mode 100644
index 000000000..aa577e0d6
--- /dev/null
+++ b/test/klippy/gcode_jinja2_ext_do.cfg
@@ -0,0 +1,80 @@
+# Test config for probe:z_virtual_endstop
+[stepper_x]
+step_pin: PF0
+dir_pin: PF1
+enable_pin: !PD7
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PE5
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_y]
+step_pin: PF6
+dir_pin: !PF7
+enable_pin: !PF2
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PJ1
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_z]
+step_pin: PL3
+dir_pin: PL1
+enable_pin: !PK0
+microsteps: 16
+rotation_distance: 8
+endstop_pin: probe:z_virtual_endstop
+position_max: 200
+
+[extruder]
+step_pin: PA4
+dir_pin: PA6
+enable_pin: !PA2
+microsteps: 16
+rotation_distance: 33.5
+nozzle_diameter: 0.400
+filament_diameter: 1.750
+heater_pin: PB4
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK5
+control: pid
+pid_Kp: 22.2
+pid_Ki: 1.08
+pid_Kd: 114
+min_temp: 0
+max_temp: 250
+
+[heater_bed]
+heater_pin: PH5
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK6
+control: watermark
+min_temp: 0
+max_temp: 130
+
+[probe]
+pin: PH6
+z_offset: 1.15
+drop_first_result: true
+
+[bed_mesh]
+mesh_min: 10,10
+mesh_max: 180,180
+
+[mcu]
+serial: /dev/ttyACM0
+
+[printer]
+kinematics: cartesian
+max_velocity: 300
+max_accel: 3000
+max_z_velocity: 5
+max_z_accel: 100
+
+[gcode_macro NONE_TEST]
+gcode:
+ {% do [1,2].append(3) %}
diff --git a/test/klippy/gcode_jinja2_ext_do.test b/test/klippy/gcode_jinja2_ext_do.test
new file mode 100644
index 000000000..67ac18618
--- /dev/null
+++ b/test/klippy/gcode_jinja2_ext_do.test
@@ -0,0 +1,24 @@
+# Test case for probe:drop_first_result support
+CONFIG gcode_jinja2_ext_do.cfg
+DICTIONARY atmega2560.dict
+
+# Start by homing the printer.
+G28
+G1 F6000
+
+# Z / X / Y moves
+G1 Z1
+G1 X1
+G1 Y1
+
+# Run bed_mesh_calibrate
+BED_MESH_CALIBRATE
+
+# Move again
+G1 Z5 X0 Y0
+
+# Test gcode with jinja2.ext.do required
+NONE_TEST
+
+# Move again
+G1 Z9
diff --git a/test/klippy/pid_hot_modify.cfg b/test/klippy/pid_hot_modify.cfg
new file mode 100644
index 000000000..d7123d08e
--- /dev/null
+++ b/test/klippy/pid_hot_modify.cfg
@@ -0,0 +1,68 @@
+# Config for extruder testing
+[stepper_x]
+step_pin: PF0
+dir_pin: PF1
+enable_pin: !PD7
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PE5
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_y]
+step_pin: PF6
+dir_pin: !PF7
+enable_pin: !PF2
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PJ1
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_z]
+step_pin: PL3
+dir_pin: PL1
+enable_pin: !PK0
+microsteps: 16
+rotation_distance: 8
+endstop_pin: ^PD3
+position_endstop: 0.5
+position_max: 200
+
+[extruder]
+step_pin: PA4
+dir_pin: PA6
+enable_pin: !PA2
+microsteps: 16
+rotation_distance: 33.5
+nozzle_diameter: 0.500
+filament_diameter: 3.500
+heater_pin: PB4
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK5
+control: pid
+pid_Kp: 22.2
+pid_Ki: 1.08
+pid_Kd: 114
+min_temp: 0
+max_temp: 210
+
+[extruder_stepper my_extra_stepper]
+extruder: extruder
+step_pin: PH5
+dir_pin: PH6
+enable_pin: !PB5
+microsteps: 16
+rotation_distance: 28.2
+
+[mcu]
+serial: /dev/ttyACM0
+
+[printer]
+kinematics: cartesian
+max_velocity: 300
+max_accel: 3000
+max_z_velocity: 5
+max_z_accel: 100
diff --git a/test/klippy/pid_hot_modify.test b/test/klippy/pid_hot_modify.test
new file mode 100644
index 000000000..107182f9a
--- /dev/null
+++ b/test/klippy/pid_hot_modify.test
@@ -0,0 +1,15 @@
+# Pid hot modify tests
+DICTIONARY atmega2560.dict
+CONFIG pid_hot_modify.cfg
+
+# Extrude only
+G1 E5
+G1 E-2
+G1 E7
+
+# Home and extrusion moves
+G28
+G1 X20 Y20 Z1
+G1 X25 Y25 E7.5
+
+SET_HEATER_PID HEATER=extruder KP=25 KI=2 KD=120
diff --git a/test/klippy/probe_drop_first_result.cfg b/test/klippy/probe_drop_first_result.cfg
new file mode 100644
index 000000000..e00f7a883
--- /dev/null
+++ b/test/klippy/probe_drop_first_result.cfg
@@ -0,0 +1,76 @@
+# Test config for probe:z_virtual_endstop
+[stepper_x]
+step_pin: PF0
+dir_pin: PF1
+enable_pin: !PD7
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PE5
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_y]
+step_pin: PF6
+dir_pin: !PF7
+enable_pin: !PF2
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PJ1
+position_endstop: 0
+position_max: 200
+homing_speed: 50
+
+[stepper_z]
+step_pin: PL3
+dir_pin: PL1
+enable_pin: !PK0
+microsteps: 16
+rotation_distance: 8
+endstop_pin: probe:z_virtual_endstop
+position_max: 200
+
+[extruder]
+step_pin: PA4
+dir_pin: PA6
+enable_pin: !PA2
+microsteps: 16
+rotation_distance: 33.5
+nozzle_diameter: 0.400
+filament_diameter: 1.750
+heater_pin: PB4
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK5
+control: pid
+pid_Kp: 22.2
+pid_Ki: 1.08
+pid_Kd: 114
+min_temp: 0
+max_temp: 250
+
+[heater_bed]
+heater_pin: PH5
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK6
+control: watermark
+min_temp: 0
+max_temp: 130
+
+[probe]
+pin: PH6
+z_offset: 1.15
+drop_first_result: true
+
+[bed_mesh]
+mesh_min: 10,10
+mesh_max: 180,180
+
+[mcu]
+serial: /dev/ttyACM0
+
+[printer]
+kinematics: cartesian
+max_velocity: 300
+max_accel: 3000
+max_z_velocity: 5
+max_z_accel: 100
diff --git a/test/klippy/probe_drop_first_result.test b/test/klippy/probe_drop_first_result.test
new file mode 100644
index 000000000..20cddc10a
--- /dev/null
+++ b/test/klippy/probe_drop_first_result.test
@@ -0,0 +1,25 @@
+# Test case for probe:drop_first_result support
+CONFIG probe_drop_first_result.cfg
+DICTIONARY atmega2560.dict
+
+# Start by homing the printer.
+G28
+G1 F6000
+
+# Z / X / Y moves
+G1 Z1
+G1 X1
+G1 Y1
+
+# Run bed_mesh_calibrate
+BED_MESH_CALIBRATE
+
+# Move again
+G1 Z5 X0 Y0
+
+# Do regular probe
+PROBE
+QUERY_PROBE
+
+# Move again
+G1 Z9
diff --git a/test/klippy/tmc.cfg b/test/klippy/tmc.cfg
index 4ae85f440..560691d73 100644
--- a/test/klippy/tmc.cfg
+++ b/test/klippy/tmc.cfg
@@ -37,10 +37,12 @@ rotation_distance: 40
endstop_pin: tmc5160_stepper_y:virtual_endstop
position_endstop: 0
position_max: 210
+use_sensorless_homing: true
[tmc5160 stepper_y]
cs_pin: PG2
-run_current: .5
+run_current: .7
+home_current: .5
sense_resistor: 0.220
diag1_pin: !PK7
diff --git a/test/klippy/z_calibration.cfg b/test/klippy/z_calibration.cfg
new file mode 100644
index 000000000..eb7728877
--- /dev/null
+++ b/test/klippy/z_calibration.cfg
@@ -0,0 +1,100 @@
+# Test config for z_tilt and quad_gantry_level
+[stepper_x]
+step_pin: PF0
+dir_pin: PF1
+enable_pin: !PD7
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PE5
+position_endstop: 0
+position_max: 250
+homing_speed: 50
+
+[stepper_y]
+step_pin: PF6
+dir_pin: !PF7
+enable_pin: !PF2
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PJ1
+position_endstop: 0
+position_max: 250
+homing_speed: 50
+
+[stepper_z]
+step_pin: PL3
+dir_pin: PL1
+enable_pin: !PK0
+microsteps: 16
+rotation_distance: 8
+endstop_pin: ^PD3
+position_endstop: 0.5
+position_max: 250
+
+[stepper_z1]
+step_pin: PC1
+dir_pin: PC3
+enable_pin: !PC7
+microsteps: 16
+rotation_distance: 8
+endstop_pin: ^PD2
+
+[stepper_z2]
+step_pin: PH1
+dir_pin: PH0
+enable_pin: !PA1
+microsteps: 16
+rotation_distance: 8
+
+[stepper_z3]
+step_pin: PE3
+dir_pin: PG5
+microsteps: 16
+rotation_distance: 8
+
+[extruder]
+step_pin: PA4
+dir_pin: PA6
+enable_pin: !PA2
+microsteps: 16
+rotation_distance: 33.5
+nozzle_diameter: 0.400
+filament_diameter: 1.750
+heater_pin: PB4
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK5
+control: pid
+pid_Kp: 22.2
+pid_Ki: 1.08
+pid_Kd: 114
+min_temp: 0
+max_temp: 250
+
+[heater_bed]
+heater_pin: PH5
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PK6
+control: watermark
+min_temp: 0
+max_temp: 130
+
+[probe]
+pin: PH6
+z_offset: 0
+
+[mcu]
+serial: /dev/ttyACM0
+
+[printer]
+kinematics: cartesian
+max_velocity: 300
+max_accel: 3000
+max_z_velocity: 5
+max_z_accel: 100
+
+[z_calibration]
+nozzle_xy_position: 100,100
+switch_xy_position: 101, 101
+bed_xy_position: 150, 150
+switch_offset: 0.5
+max_deviation: 3.0
diff --git a/test/klippy/z_calibration.test b/test/klippy/z_calibration.test
new file mode 100644
index 000000000..5404a15f4
--- /dev/null
+++ b/test/klippy/z_calibration.test
@@ -0,0 +1,12 @@
+# Test case for z_calibration
+CONFIG z_calibration.cfg
+DICTIONARY atmega2560.dict
+
+# Start by homing the printer.
+G28
+G1 Z5 X10 Y10 F6000
+M400
+GET_POSITION
+
+# Test CALIBRATE_Z
+CALIBRATE_Z