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 @@ +

Danger-Klipper Logo

+ +[![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