diff --git a/.gitignore b/.gitignore index da4915d5d..1c963e1f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,175 @@ +# ------------------------------------------------------------------- # This gitignore is configured to not track user defined files at the root of the config # And thus allowing a full update of the repo while keeping user files untouched +# ------------------------------------------------------------------- + /*.cfg /*.conf /.VERSION + + +# ------------------------------------------------------------------- +# Python specifics added here as well to avoid pycache folder, etc... +# ------------------------------------------------------------------- + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/README.md b/README.md index bde7fae88..078af819e 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,21 @@ Finally, Klippain requires a few simple steps to configure and customize it for > General rule to keep the auto-update feature working: **never modify Klippain files directly**, but instead add overrides as per the documentation! To proceed, you can modify all the pre-installed templates in your config root folder (`printer.cfg`, `mcu.cfg`, `variables.cfg` and `overrides.cfg`) as they will be preserved on update. +## Removing Klippain + +In case Klippain doesn't suit your needs or if you installed it by mistake, you can easily remove Klippain and revert to your previous configuration by using the automated uninstall script. During the uninstallation process, the script will remove all specific Klippain files and configurations. Additionally, you will be given an option to restore your previously backed-up configuration, allowing your printer to return to its last working state (from before Klippain was installed). + +To run the uninstall script, execute the following command over SSH: + +```bash +wget -O - https://raw.githubusercontent.com/Frix-x/klippain/main/uninstall.sh | bash +``` + + > **Note** + > + > All backups are preserved during the uninstallation process. So, you can easily revert back at any time if you wish to :stuck_out_tongue_winking_eye: + + ## Support the Project I strive to accommodate user requests that align with this configuration's design. Feel free to open an issue or a PR for specific hardware device support or new features. diff --git a/config/hardware/accelerometers/adxl345_sht_v2.x.cfg b/config/hardware/accelerometers/adxl345_sht_v2.x.cfg new file mode 100644 index 000000000..b4de2fd8a --- /dev/null +++ b/config/hardware/accelerometers/adxl345_sht_v2.x.cfg @@ -0,0 +1,7 @@ +[include generics/adxl345_hardware_spi1.cfg] + +# As it's a toolhead ADXL, we add some default pins overrides from here +[adxl345] +cs_pin: toolhead:ADXL_CS +# Supplied config from Mellow is wrong - it states spi1, but at least for a 072 based unit is it on spi2 +spi_bus: spi2 diff --git a/config/hardware/accelerometers/adxl345_usb_rampon.cfg b/config/hardware/accelerometers/adxl345_usb_rampon.cfg new file mode 100644 index 000000000..a91c95b03 --- /dev/null +++ b/config/hardware/accelerometers/adxl345_usb_rampon.cfg @@ -0,0 +1,25 @@ +# This ADXL file is dedicated to be used with ADXL boards +# connected over USB to the pi as dedicated and standalone ADXL-MCU boards + +# This include KUSBA, ... + + +# You need to override the following to be able to set the proper serial in your overrides.cfg file +[mcu adxl] +serial: /dev/serial/by-id/xxx + +[adxl345] +cs_pin: adxl:CS +axes_map: x,y,z + +[resonance_tester] +accel_chip: adxl345 +probe_points: + -1,-1,-1 + + +# Include the IS calibration macros to unlock them when +# an accelerometer is installed on the machine +[include ../../../macros/helpers/resonance_override.cfg] +[include ../../../macros/calibration/IS_shaper_calibrate.cfg] +[include ../../../macros/calibration/IS_vibrations_measurement.cfg] diff --git a/config/hardware/accelerometers/adxl345_usb_rp2040_spi1.cfg b/config/hardware/accelerometers/adxl345_usb_rp2040_spi1.cfg new file mode 100644 index 000000000..67c924515 --- /dev/null +++ b/config/hardware/accelerometers/adxl345_usb_rp2040_spi1.cfg @@ -0,0 +1,29 @@ +# This ADXL file is dedicated to be used with USB RP2040 boards where the ADXL +# is connected to SPI1 + +# This include FYSTEC PortableInputShaper, ... + + +# You need to set the proper serial in your overrides.cfg file +[mcu adxl] +serial: /dev/serial/by-id/xxx + +[adxl345] +cs_pin: adxl:gpio13 +spi_software_sclk_pin: adxl:gpio10 +spi_software_mosi_pin: adxl:gpio11 +spi_software_miso_pin: adxl:gpio12 +axes_map: x,y,z +# FYSTEC POS: x,-z,y + +[resonance_tester] +accel_chip: adxl345 +probe_points: + -1,-1,-1 + + +# Include the IS calibration macros to unlock them when +# an accelerometer is installed on the machine +[include ../../../macros/helpers/resonance_override.cfg] +[include ../../../macros/calibration/IS_shaper_calibrate.cfg] +[include ../../../macros/calibration/IS_vibrations_measurement.cfg] diff --git a/config/hardware/axis/Z/V2.4_galileo2Z_1.8deg.cfg b/config/hardware/axis/Z/V2.4_galileo2Z_1.8deg.cfg new file mode 100644 index 000000000..d67e9f3ad --- /dev/null +++ b/config/hardware/axis/Z/V2.4_galileo2Z_1.8deg.cfg @@ -0,0 +1,27 @@ +[stepper_z] +rotation_distance: 40 +gear_ratio: 9:1 +microsteps: 32 +full_steps_per_rotation: 200 + +[stepper_z1] +rotation_distance: 40 +gear_ratio: 9:1 +microsteps: 32 +full_steps_per_rotation: 200 + +[stepper_z2] +rotation_distance: 40 +gear_ratio: 9:1 +microsteps: 32 +full_steps_per_rotation: 200 + +[stepper_z3] +rotation_distance: 40 +gear_ratio: 9:1 +microsteps: 32 +full_steps_per_rotation: 200 + +# We also include the default wiring and speeds from here to avoid duplicating +[include default_wiring_4M.cfg] +[include default_speed.cfg] diff --git a/config/hardware/bed_heaters/creality.cfg b/config/hardware/bed_heaters/creality.cfg new file mode 100644 index 000000000..a7d85a671 --- /dev/null +++ b/config/hardware/bed_heaters/creality.cfg @@ -0,0 +1,15 @@ +[gcode_macro _USER_VARIABLES] +variable_heaterbed_enabled: True +gcode: + + +[heater_bed] +heater_pin: BED_HEATER +sensor_type: EPCOS 100K B57560G104F +sensor_pin: BED_TEMPERATURE +max_power: 1 +min_temp: 0 +max_temp: 150 + +# We also include the "no wait too much for temperature" patch +[include ../../../macros/helpers/bed_heater_ctrl.cfg] diff --git a/config/hardware/bed_heaters/keenovo.cfg b/config/hardware/bed_heaters/keenovo.cfg new file mode 100644 index 000000000..69a09555a --- /dev/null +++ b/config/hardware/bed_heaters/keenovo.cfg @@ -0,0 +1,15 @@ +[gcode_macro _USER_VARIABLES] +variable_heaterbed_enabled: True +gcode: + + +[heater_bed] +heater_pin: BED_HEATER +sensor_type: NTC 100K MGB18-104F39050L32 +sensor_pin: BED_TEMPERATURE +max_power: 1 +min_temp: 0 +max_temp: 120 + +# We also include the "no wait too much for temperature" patch +[include ../../../macros/helpers/bed_heater_ctrl.cfg] diff --git a/config/hardware/extruder/galileo2.cfg b/config/hardware/extruder/galileo2.cfg new file mode 100644 index 000000000..4f54c3c9d --- /dev/null +++ b/config/hardware/extruder/galileo2.cfg @@ -0,0 +1,28 @@ +[gcode_macro _USER_VARIABLES] +variable_extruder_enabled: True +gcode: + + +[extruder] +# Galileo 2 Gear Ratio +# new_rd = previous_rd * mesured_distance / requested_distance +rotation_distance: 47.088 +gear_ratio: 9:1 +microsteps: 16 +full_steps_per_rotation: 200 + +nozzle_diameter: 0.400 +filament_diameter: 1.75 +max_extrude_only_distance: 110 +max_extrude_cross_section: 5 +sensor_type: ATC Semitec 104GT-2 +min_temp: 10 +max_temp: 270 +max_power: 1.0 +min_extrude_temp: 172 +pressure_advance: 0.0475 +pressure_advance_smooth_time: 0.040 + +# We also include the default wiring and low thermal hotend patch +[include default_wiring.cfg] +[include ../../../macros/helpers/hotend_heater_ctrl.cfg] diff --git a/config/hardware/filament_sensors/switch_sensor.cfg b/config/hardware/filament_sensors/switch_sensor.cfg index 1bc5f2401..56264c81b 100644 --- a/config/hardware/filament_sensors/switch_sensor.cfg +++ b/config/hardware/filament_sensors/switch_sensor.cfg @@ -4,7 +4,6 @@ gcode: [filament_switch_sensor runout_sensor] -extruder: extruder switch_pin: RUNOUT_SENSOR pause_on_runout: True #runout_gcode: diff --git a/config/hardware/heated_bed.cfg b/config/hardware/heated_bed.cfg index 4500b9177..e01a23680 100644 --- a/config/hardware/heated_bed.cfg +++ b/config/hardware/heated_bed.cfg @@ -1,15 +1,3 @@ -[gcode_macro _USER_VARIABLES] -variable_heaterbed_enabled: True -gcode: +## This file is deprecated in favor of the included file. - -[heater_bed] -heater_pin: BED_HEATER -sensor_type: NTC 100K MGB18-104F39050L32 -sensor_pin: BED_TEMPERATURE -max_power: 1 -min_temp: 0 -max_temp: 120 - -# We also include the "no wait too much for temperature" patch -[include ../../macros/helpers/bed_heater_ctrl.cfg] +[include bed_heaters/keenovo.cfg] diff --git a/config/mcu_definitions/main/BTT_Manta_M8P_v2.0.cfg b/config/mcu_definitions/main/BTT_Manta_M8P_v2.0.cfg new file mode 100644 index 000000000..2cb82bc33 --- /dev/null +++ b/config/mcu_definitions/main/BTT_Manta_M8P_v2.0.cfg @@ -0,0 +1,48 @@ +[board_pins mcu_manufacturer] +aliases: + MCU_M1_STEP=PE6 , MCU_M1_DIR=PE5 , MCU_M1_EN=PC14 , MCU_M1_CS=PC13 , + MCU_M2_STEP=PE2 , MCU_M2_DIR=PE1 , MCU_M2_EN=PE4 , MCU_M2_CS=PE3 , + MCU_M3_STEP=BP8 , MCU_M3_DIR=PB7 , MCU_M3_EN=PE0 , MCU_M3_CS=PB9 , + MCU_M4_STEP=PB4 , MCU_M4_DIR=PB3 , MCU_M4_EN=PB6 , MCU_M4_CS=PB5 , + MCU_M5_STEP=PG13 , MCU_M5_DIR=PG12 , MCU_M5_EN=PG15 , MCU_M5_CS=PG14 , + MCU_M6_STEP=PG9 , MCU_M6_DIR=PD7 , MCU_M6_EN=PG11 , MCU_M6_CS=PG10 , + MCU_M7_STEP=PD4 , MCU_M7_DIR=PD3 , MCU_M7_EN=PD6 , MCU_M7_CS=PD5 , + MCU_M8_STEP=PC7 , MCU_M8_DIR=PC8 , MCU_M8_EN=PD2 , MCU_M8_CS=PC6 , + + MCU_M1_STOP=PF4 , MCU_M2_STOP=PF3 , MCU_M3_STOP=PF2 , MCU_M4_STOP=PF1 , + MCU_M5_STOP=PF0 , MCU_M6_STOP=PC15 , + + MCU_HE0=PA0 , MCU_HE1=PA1 , MCU_HE2=PA3 , MCU_HE3=PA5 , + + MCU_BED_OUT=PF5 , + + MCU_THB=PB1 , MCU_TH0=PB0 , MCU_TH1=PC5 , MCU_TH2=PC4 , MCU_TH3=PA7 , + + MCU_FAN0=PF7 , MCU_FAN1=PF9 , MCU_FAN2=PF6 , MCU_FAN3=PF8 , MCU_FAN4=PA4 , + MCU_FAN5=PC2 , MCU_FAN5_TACH=PA6 , + MCU_FAN6=PC1 , MCU_FAN6_TACH=PA2 , + + MCU_RGB1=PD15 , + MCU_PS_ON=PD14 , + MCU_POWER_DET=PB10 , + + MCU_PROBE1=PD13 , MCU_PROBE2=PD12 , + MCU_IND_PROBE=PD8 , + + MCU_SPI2_MOSI=PC12 , MCU_SPI2_MISO=PC11 , MCU_SPI2_SCK=PC10 , MCU_SPI2_CS=PA15 , + + # TX RX + MCU_CAN_TRANSMIT=PD1 , MCU_CAN_RECIEVE=PD0 , + + MCU_FWS1=PC0 , MCU_FWS=PF10 , + + # EXP ribbon + EXP1_1=<5V> , EXP1_2= , + EXP1_3=PF11 , EXP1_4=PF12 , + EXP1_5=PF13 , EXP1_6=PF14 , + EXP1_7=PF15 , EXP1_8=PG0 , + EXP1_9=PG1 , EXP1_10=PE7 , + EXP1_11= , EXP1_12=PE8 , + EXP1_13=PE14 , EXP1_14=PE10 , + EXP1_15=PE11 , EXP1_16=PE15 , + EXP1_17=PE12 , EXP1_18=PE13 diff --git a/config/mcu_definitions/main/Fysetc_Spider_v3.x.cfg b/config/mcu_definitions/main/Fysetc_Spider_v3.x.cfg new file mode 100644 index 000000000..8107f8c03 --- /dev/null +++ b/config/mcu_definitions/main/Fysetc_Spider_v3.x.cfg @@ -0,0 +1,44 @@ +[board_pins mcu_manufacturer] +aliases: + MCU_X_MOT_STEP=PE11 , MCU_X_MOT_DIR=PE10 , MCU_X_MOT_ENABLE=PE9 , MCU_X_MOT_CS_PDN=PE7 , + MCU_Y_MOT_STEP=PD8 , MCU_Y_MOT_DIR=PB12 , MCU_Y_MOT_ENABLE=PD9 , MCU_Y_MOT_CS_PDN=PE15 , + MCU_Z_MOT_STEP=PD14 , MCU_Z_MOT_DIR=PD13 , MCU_Z_MOT_ENABLE=PD15 , MCU_Z_MOT_CS_PDN=PD10 , + MCU_E0_MOT_STEP=PD5 , MCU_E0_MOT_DIR=PD6 , MCU_E0_MOT_ENABLE=PD4 , MCU_E0_MOT_CS_PDN=PD7 , + MCU_E1_MOT_STEP=PE6 , MCU_E1_MOT_DIR=PC13 , MCU_E1_MOT_ENABLE=PE5 , MCU_E1_MOT_CS_PDN=PC14 , + MCU_E2_MOT_STEP=PE2 , MCU_E2_MOT_DIR=PE4 , MCU_E2_MOT_ENABLE=PE3 , MCU_E2_MOT_CS_PDN=PC15 , + MCU_E3_MOT_STEP=PD12 , MCU_E3_MOT_DIR=PC4 , MCU_E3_MOT_ENABLE=PE8 , MCU_E3_MOT_CS_PDN=PA15 , + MCU_E4_MOT_STEP=PE1 , MCU_E4_MOT_DIR=PE0 , MCU_E4_MOT_ENABLE=PC5 , MCU_E4_MOT_CS_PDN=PD11 , + MCU_SPI4_MOSI=PE14 , MCU_SPI4_MISO=PE13 , MCU_SPI4_SCK=PE12 , + + MCU_X_MIN=PB14 , MCU_Y_MIN=PB13 , MCU_Z_MIN=PA0 , + MCU_X_MAX=PA1 , MCU_Y_MAX=PA2 , MCU_Z_MAX=PA3 , + + MCU_FAN0=PA13 , MCU_FAN1=PA14 , MCU_FAN2=PB2 , MCU_FAN3=PB5 , MCU_FAN4=PB6 , MCU_FAN5=PB7 , + # MCU_RGB_R=PB6 , MCU_RGB_G=PB5 , MCU_RGB_B=PB7 , + MCU_G_DATA_5V=PD3 , + + MCU_HEAT0=PB15 , MCU_HEAT1=PC8 , MCU_HEAT2=PB3 , + + MCU_BED_OUT=PB4 , + + MCU_T0=PC0 , MCU_T1=PC1 , MCU_T2=PC2 , MCU_T3=PC3 , MCU_T4=PB1 , MCU_TB=PB0 , + + # AUX3 header + MCU_AUX3_RST= , MCU_AUX3_GND= , + MCU_AUX3_CS=PA4 , MCU_AUX3_SCK=PA5 , + MCU_AUX3_MOSI=PA7 , MCU_AUX3_MISO=PA6 , + MCU_AUX3_CD=PB10 , MCU_AUX3_5V=<5V> , + + # EXP1 header + EXP1_1=<5V> , EXP1_2= , + EXP1_3=PD1 , EXP1_4=PD0 , + EXP1_5=PC12 , EXP1_6=PC10 , # Slot in the socket on this side + EXP1_7=PD2 , EXP1_8=PC11 , + EXP1_9=PA8 , EXP1_10= PC9 , + + # EXP2 header + EXP2_1=<5V> , EXP2_2= , + EXP2_3= , EXP2_4=PB10 , + EXP2_5=PA7 , EXP2_6=PC7 , # Slot in the socket on this side + EXP2_7=PA4 , EXP2_8=PC6 , + EXP2_9=PA5 , EXP2_10=PA6 , diff --git a/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.0.cfg b/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.0.cfg index 5c3208630..fafa77537 100644 --- a/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.0.cfg +++ b/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.0.cfg @@ -1,18 +1,18 @@ [board_pins toolhead_manufacturer] mcu: toolhead aliases: - MCU_TMCDRIVER_STEP=PA9 , MCU_TMCDRIVER_DIR=PA8 , MCU_TMCDRIVER_ENABLE=PA10 , MCU_TMCDRIVER_UART=PA13 , + MCU_TMCDRIVER_STEP=PA9 , MCU_TMCDRIVER_DIR=PA8 , MCU_TMCDRIVER_ENABLE=PA10 , MCU_TMCDRIVER_UART=PA13 , - MCU_ENDSTOP1=PC13 , MCU_ENDSTOP2=PC14 , MCU_ENDSTOP3=PC15 , - MCU_PROBE=PA5 , - MCU_SERVOS=PA4 , + MCU_ENDSTOP1=PC13 , MCU_ENDSTOP2=PC14 , MCU_ENDSTOP3=PC15 , + MCU_PROBE=PA5 , + MCU_SERVOS=PA4 , - MCU_HOTEND0=PB1 , - MCU_TH0=PA0 , + MCU_HOTEND0=PB1 , + MCU_TH0=PA0 , - MCU_FAN0=PA1 , MCU_FAN1=PA2 , + MCU_FAN0=PA1 , MCU_FAN1=PA2 , - MCU_RGB=PA3 , + MCU_RGB=PA3 , - MCU_SPI1_CS=PA15 , MCU_SPI1_SCLK=PB3 , MCU_SPI1_MISO=PB4 , MCU_SPI1_MOSI=PB5 , - MCU_SPI2_CS=PB12 , MCU_SPI2_SCLK=PB13 , MCU_SPI2_MISO=PB14 , MCU_SPI2_MOSI=PB15 , + MCU_SPI1_CS=PA15 , MCU_SPI1_SCLK=PB3 , MCU_SPI1_MISO=PB4 , MCU_SPI1_MOSI=PB5 , + MCU_SPI2_CS=PB12 , MCU_SPI2_SCLK=PB13 , MCU_SPI2_MISO=PB14 , MCU_SPI2_MOSI=PB15 , diff --git a/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.1.cfg b/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.1.cfg index a51b158f0..f19d1b1e7 100644 --- a/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.1.cfg +++ b/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.1.cfg @@ -1,18 +1,18 @@ [board_pins toolhead_manufacturer] mcu: toolhead aliases: - MCU_TMCDRIVER_STEP=PD0 , MCU_TMCDRIVER_DIR=PD1 , MCU_TMCDRIVER_ENABLE=PD2 , MCU_TMCDRIVER_UART=PA15 , + MCU_TMCDRIVER_STEP=PD0 , MCU_TMCDRIVER_DIR=PD1 , MCU_TMCDRIVER_ENABLE=PD2 , MCU_TMCDRIVER_UART=PA15 , - MCU_ENDSTOP1=PB6 , MCU_ENDSTOP2=PB5 , MCU_ENDSTOP3=PB7 , - MCU_PROBE=PB8 , - MCU_SERVOS=PB9 , + MCU_ENDSTOP1=PB6 , MCU_ENDSTOP2=PB5 , MCU_ENDSTOP3=PB7 , + MCU_PROBE=PB8 , + MCU_SERVOS=PB9 , - MCU_HOTEND0=PA2 , - MCU_TH0=PA3 , + MCU_HOTEND0=PA2 , + MCU_TH0=PA3 , - MCU_FAN1=PA0 , MCU_FAN2=PA1 , + MCU_FAN1=PA0 , MCU_FAN2=PA1 , - MCU_RGB=PD3 , + MCU_RGB=PD3 , - MCU_SPI1_CS=PA4 , MCU_SPI1_SCLK=PA5 , MCU_SPI1_MISO=PA6 , MCU_SPI1_MOSI=PA7 , - MCU_SPI2_CS=PB12 , MCU_SPI2_SCLK=PB10 , MCU_SPI2_MISO=PB2 , MCU_SPI2_MOSI=PB11 , + MCU_SPI1_CS=PA4 , MCU_SPI1_SCLK=PA5 , MCU_SPI1_MISO=PA6 , MCU_SPI1_MOSI=PA7 , + MCU_SPI2_CS=PB12 , MCU_SPI2_SCLK=PB10 , MCU_SPI2_MISO=PB2 , MCU_SPI2_MOSI=PB11 , diff --git a/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.2.cfg b/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.2.cfg index 635d19edb..1bb60026d 100644 --- a/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.2.cfg +++ b/config/mcu_definitions/toolhead/BTT_EBB36-42_v1.2.cfg @@ -1,18 +1,18 @@ [board_pins toolhead_manufacturer] mcu: toolhead aliases: - MCU_TMCDRIVER_STEP=PD0 , MCU_TMCDRIVER_DIR=PD1 , MCU_TMCDRIVER_ENABLE=PD2 , MCU_TMCDRIVER_UART=PA15 , + MCU_TMCDRIVER_STEP=PD0 , MCU_TMCDRIVER_DIR=PD1 , MCU_TMCDRIVER_ENABLE=PD2 , MCU_TMCDRIVER_UART=PA15 , - MCU_ENDSTOP1=PB6 , MCU_ENDSTOP2=PB5 , MCU_ENDSTOP3=PB7 , - MCU_PROBE=PB8 , - MCU_SERVOS=PB9 , + MCU_ENDSTOP1=PB6 , MCU_ENDSTOP2=PB5 , MCU_ENDSTOP3=PB7 , + MCU_PROBE=PB8 , + MCU_SERVOS=PB9 , - MCU_HOTEND0=PB13 , - MCU_TH0=PA3 , + MCU_HOTEND0=PB13 , + MCU_TH0=PA3 , - MCU_FAN1=PA0 , MCU_FAN2=PA1 , + MCU_FAN1=PA0 , MCU_FAN2=PA1 , - MCU_RGB=PD3 , + MCU_RGB=PD3 , - MCU_SPI1_CS=PA4 , MCU_SPI1_SCLK=PA5 , MCU_SPI1_MISO=PA6 , MCU_SPI1_MOSI=PA7 , - MCU_SPI2_CS=PB12 , MCU_SPI2_SCLK=PB10 , MCU_SPI2_MISO=PB2 , MCU_SPI2_MOSI=PB11 , + MCU_SPI1_CS=PA4 , MCU_SPI1_SCLK=PA5 , MCU_SPI1_MISO=PA6 , MCU_SPI1_MOSI=PA7 , + MCU_SPI2_CS=PB12 , MCU_SPI2_SCLK=PB10 , MCU_SPI2_MISO=PB2 , MCU_SPI2_MOSI=PB11 , diff --git a/config/mcu_definitions/toolhead/BTT_SB2209_RP2040_v1.0.cfg b/config/mcu_definitions/toolhead/BTT_SB2209_RP2040_v1.0.cfg new file mode 100644 index 000000000..6250c316b --- /dev/null +++ b/config/mcu_definitions/toolhead/BTT_SB2209_RP2040_v1.0.cfg @@ -0,0 +1,20 @@ +[board_pins toolhead_manufacturer] +mcu: toolhead +aliases: + + MCU_E0_STEP=gpio18 , MCU_E0_DIR=gpio19 , MCU_E0_EN=gpio17 , MCU_E0_UART=gpio20 , + + MCU_ENDSTOP=gpio24 , + MCU_PROBE1=gpio21 , MCU_PROBE2=gpio22 , + + MCU_HE0=gpio7 , + MCU_TH0=gpio27 , + + MCU_IND_FAN=gpio6 , + MCU_FAN1_PWM=gpio14 , MCU_FAN2_PWM=gpio13 , + MCU_4WFAN_TACH=gpio12 , MCU_4WFAN_PWM=gpio15 , + + MCU_31865_MOSI=gpio8 , MCU_31865_CS=gpio9 , MCU_31865_CLK=gpio10 , MCU_31865_MISO=gpio11 , + MCU_ADXL345_MOSI=gpio0 , MCU_ADXL345_CS=gpio1 , MCU_ADXL345_CLK=gpio2 , MCU_ADXL345_MISO=gpio3 , + + MCU_RGB=gpio16 , diff --git a/config/mcu_definitions/toolhead/BTT_SB2209_v1.0.cfg b/config/mcu_definitions/toolhead/BTT_SB2209_v1.0.cfg index 6a77f8a5a..8a2654412 100644 --- a/config/mcu_definitions/toolhead/BTT_SB2209_v1.0.cfg +++ b/config/mcu_definitions/toolhead/BTT_SB2209_v1.0.cfg @@ -1,23 +1,23 @@ [board_pins toolhead_manufacturer] mcu: toolhead aliases: - MCU_MOTORDRIVE_STEP=PD0 , MCU_MOTORDRIVE_DIR=PD1 , MCU_MOTORDRIVE_ENABLE=PD2 , MCU_MOTORDRIVE_UART=PA15 , - MCU_MOTORDRIVE_DIAG=PB3 , + MCU_MOTORDRIVE_STEP=PD0 , MCU_MOTORDRIVE_DIR=PD1 , MCU_MOTORDRIVE_ENABLE=PD2 , MCU_MOTORDRIVE_UART=PA15 , + MCU_MOTORDRIVE_DIAG=PB3 , - MCU_STOP1=PB6 , MCU_STOP2=PB5 , MCU_STOP3=PB7 , - MCU_PROBE=PB8 , - MCU_SERVOS=PB9 , + MCU_STOP1=PB6 , MCU_STOP2=PB5 , MCU_STOP3=PB7 , + MCU_PROBE=PB8 , + MCU_SERVOS=PB9 , - MCU_HE0=PB13 , - MCU_TH0=PA3 , - MCU_NTC=PA2 , + MCU_HE0=PB13 , + MCU_TH0=PA3 , + MCU_NTC=PA2 , - MCU_FAN1_PWM=PA0 , MCU_FAN2_PWM=PA1 , - MCU_4WFAN_TACH=PB15 , MCU_4WFAN_PWM=PB14 , + MCU_FAN1_PWM=PA0 , MCU_FAN2_PWM=PA1 , + MCU_4WFAN_TACH=PB15 , MCU_4WFAN_PWM=PB14 , - MCU_RGB=PD3 , - MCU_PS=PC13 , + MCU_RGB=PD3 , + MCU_PS=PC13 , - MCU_SPI1_NSS=PA4 , MCU_SPI1_CLK=PA5 , MCU_SPI1_MISO=PA6 , MCU_SPI1_MOSI=PA7 , - MCU_SPI2_NSS=PB12 , MCU_SPI2_CLK=PB10 , MCU_SPI2_MISO=PB2 , MCU_SPI2_MOSI=PB11 , - MCU_SPI_OUT_NSS=PA10 , MCU_SPI_OUT_IO1 = PA9 , MCU_SPI_OUT_IO2 = PA8 , # shared pins between SPI2 and SPI_OUT + MCU_SPI1_NSS=PA4 , MCU_SPI1_CLK=PA5 , MCU_SPI1_MISO=PA6 , MCU_SPI1_MOSI=PA7 , + MCU_SPI2_NSS=PB12 , MCU_SPI2_CLK=PB10 , MCU_SPI2_MISO=PB2 , MCU_SPI2_MOSI=PB11 , + MCU_SPI_OUT_NSS=PA10 , MCU_SPI_OUT_IO1 = PA9 , MCU_SPI_OUT_IO2 = PA8 , # shared pins between SPI2 and SPI_OUT diff --git a/config/mcu_definitions/toolhead/BTT_SB2240_v1.0.cfg b/config/mcu_definitions/toolhead/BTT_SB2240_v1.0.cfg index e5ddba6f5..e0decf07e 100644 --- a/config/mcu_definitions/toolhead/BTT_SB2240_v1.0.cfg +++ b/config/mcu_definitions/toolhead/BTT_SB2240_v1.0.cfg @@ -1,23 +1,23 @@ [board_pins toolhead_manufacturer] mcu: toolhead aliases: - MCU_MOTORDRIVE_STEP=PD0 , MCU_MOTORDRIVE_DIR=PD1 , MCU_MOTORDRIVE_ENABLE=PD2 , MCU_MOTORDRIVE_DIAG=PB3 , - MCU_MOTOR_SPI_NSS=PA15 , # CLK/MOSI/MISO are shared with SPI2 configuration + MCU_MOTORDRIVE_STEP=PD0 , MCU_MOTORDRIVE_DIR=PD1 , MCU_MOTORDRIVE_ENABLE=PD2 , MCU_MOTORDRIVE_DIAG=PB3 , + MCU_MOTOR_SPI_NSS=PA15 , # CLK/MOSI/MISO are shared with SPI2 configuration - MCU_STOP1=PB6 , MCU_STOP2=PB5 , MCU_STOP3=PB7 , - MCU_PROBE=PB8 , - MCU_SERVOS=PB9 , + MCU_STOP1=PB6 , MCU_STOP2=PB5 , MCU_STOP3=PB7 , + MCU_PROBE=PB8 , + MCU_SERVOS=PB9 , - MCU_HE0=PB13 , - MCU_TH0=PA3 , - MCU_NTC=PA2 , + MCU_HE0=PB13 , + MCU_TH0=PA3 , + MCU_NTC=PA2 , - MCU_FAN1_PWM=PA0 , MCU_FAN2_PWM=PA1 , - MCU_4WFAN_TACH=PB15 , MCU_4WFAN_PWM=PB14 , + MCU_FAN1_PWM=PA0 , MCU_FAN2_PWM=PA1 , + MCU_4WFAN_TACH=PB15 , MCU_4WFAN_PWM=PB14 , - MCU_RGB=PD3 , - MCU_PS=PC13 , + MCU_RGB=PD3 , + MCU_PS=PC13 , - MCU_SPI1_NSS=PA4 , MCU_SPI1_CLK=PA5 , MCU_SPI1_MISO=PA6 , MCU_SPI1_MOSI=PA7 , - MCU_SPI2_NSS=PB12 , MCU_SPI2_CLK=PB10 , MCU_SPI2_MISO=PB2 , MCU_SPI2_MOSI=PB11 , - MCU_SPI_OUT_NSS=PA10 , MCU_SPI_OUT_IO1 = PA9 , MCU_SPI_OUT_IO2 = PA8 , # shared pins between SPI2 and SPI_OUT + MCU_SPI1_NSS=PA4 , MCU_SPI1_CLK=PA5 , MCU_SPI1_MISO=PA6 , MCU_SPI1_MOSI=PA7 , + MCU_SPI2_NSS=PB12 , MCU_SPI2_CLK=PB10 , MCU_SPI2_MISO=PB2 , MCU_SPI2_MOSI=PB11 , + MCU_SPI_OUT_NSS=PA10 , MCU_SPI_OUT_IO1 = PA9 , MCU_SPI_OUT_IO2 = PA8 , # shared pins between SPI2 and SPI_OUT diff --git a/docs/features/is_workflow.md b/docs/features/is_workflow.md index 08d4cc7d8..68774b8db 100644 --- a/docs/features/is_workflow.md +++ b/docs/features/is_workflow.md @@ -1,58 +1,7 @@ # Input Shaper workflow -## Description +This has now moved to its own repository: [Klippain Shake&Tune module](https://github.com/Frix-x/klippain-shaketune) -This set of two macros (and associated bash script) is here to help you configure the input shaper algorithms of Klipper. It's a fully automated workflow that: - 1. Run the tests either for the two axis or the two belts to measure the machine behavior using the ADXL345. This is under the hood a scripted call to the Klipper `TEST_RESONANCES` command with the different parameters needed for each axis. - 2. Then, it call an automatic bash script that automate a few things: - 1. it generate the graphs. - 2. it then move the graphs and associated csv to the [ADXL results folder](./../../adxl_results/). - 3. it manage the folder to delete the older files and keep only a set (default is three) of the most recent results. - -Results can be found in the [ADXL results folder](./../../adxl_results/) that is placed directly at the root of the config folder to allow the access directly from your browser using the FLuidd/Maisail file manager. No more SSH is needed to calibrate your input shaper! You can get some hints on the results in my documentation about how to [read and interpret the IS graphs](../input_shaper.md). - -| Belts resonances example | X resonances example | Y resonances example | -|:----------------------:|:----------------------:|:---------------------:| -| ![belts resonances example](./../images/resonances_belts_example.png) | ![X resonances example](./../images/resonances_x_example.png) | ![Y resonances example](./../images/resonances_y_example.png) | - - -## Installation - - 1. Copy the [IS_shaper_calibrate.cfg](./../../macros/calibration/IS_shaper_calibrate.cfg) macro file directly into your own config. - 2. Be sure to have the `gcode_shell_command.py` Klipper extension installed. Easiest way to install it is to use the advanced section of KIAUH. This is done automatically when running Klippain. - 3. Add my [scripts](./../../scripts/) folder at the root of your own config (ie. in your `~/printer_data/config/` directory). - - Note: if using Windows to do the copy/paste of the files, be careful with the line endings for the `plot_graphs.sh` file and the `graph_vibrations.py` file: **Linux line endings (LF or \n) are mandatory!** If the file are using Windows line endings, you will get errors like `\r : unknown command` when running the script. If you're not confident regarding your text editor behavior, the best way is to directly download the files on the pi by using for example wget over SSH: - - ```bash - wget -P ~/printer_data/config/scripts https://raw.githubusercontent.com/Frix-x/klippain/main/scripts/plot_graphs.sh - wget -P ~/printer_data/config/scripts https://raw.githubusercontent.com/Frix-x/klippain/main/scripts/graph_vibrations.py - ``` - - 4. Make the scripts executable using SSH. When in the folder (`cd ~/printer_data/config/scripts`), use: - - ```bash - chmod +x ./plot_graphs.sh - chmod +x ./graph_vibrations.py - ``` - - 5. Add this new section at the end of your `printer.cfg` file: - ``` - [gcode_shell_command plot_graph] - command: bash /home/pi/printer_data/config/scripts/plot_graphs.sh - timeout: 500.0 - verbose: True - ``` - - Note: if your user is not `pi`, please correct the path in the command accordingly. - - 6. (Optional) You can modify the first lines of the `plot_graphs.sh` script to configure where you want to store the results. Default: `~/printer_data/config/adxl_results` - - -## Usage - -Be sure your machine is homed and then call one of the following macros: - - `BELTS_SHAPER_CALIBRATION` to get the belt resonnance graphs. This is usefull to verify the belts tension, but also to check if the belt paths are OK. - - `AXES_SHAPER_CALIBRATION` to get the standard input shaper graphs and suppress the ringing/ghosting artefacts in your prints. - -Then, look for the results in the results folder. You can find them directly in your config folder by using the WebUI of Mainsail/Fluidd. You can get some hints on the results in my documentation about how to [read and interpret the IS graphs](../input_shaper.md). + > **Note** + > + > For Klippain users, nothing is needed to do as the latest version of K-Shake&Tune is always included in the full Klippain package! diff --git a/docs/features/vibr_measurements.md b/docs/features/vibr_measurements.md index d4071ccb5..bac733af6 100644 --- a/docs/features/vibr_measurements.md +++ b/docs/features/vibr_measurements.md @@ -1,73 +1,7 @@ # Vibrations measurements -This macro helps you to identify the speed settings that exacerbate the vibrations of the machine (ie. where the frame resonate badly). It also helps to find the clean speed ranges where the machine is silent. +This has now moved to its own repository: [Klippain Shake&Tune module](https://github.com/Frix-x/klippain-shaketune) - -## Description - -I had some strong vibrations at very specific speeds on my machine (52mm/s for example) and I wanted to find all these problematic speeds to avoid them in my slicer profile and finally get the silent machine I was dreaming! - -It's a fully automated workflow that work by moving the toolhead while using the accelerometer: - 1. It run a sequence of movements on the axis you want to measure, at different speed settings, while recording the global machine vibrations using the accelerometer. - 2. Then, it call an automatic bash script that automate a few things: - 1. it generate the vibrations graph for the specified axis using the custom made python script. - 2. it then move the graph and associated archive of CSVs to the [ADXL results folder](./../../adxl_results/). - 3. it manage the folder to delete the older files and keep only a set (default is three) of the most recent results. - -Results can be found in the [ADXL results folder](./../../adxl_results/) that is placed directly at the root of the config folder to allow the access directly from your browser using the FLuidd/Maisail file manager. No SSH is needed! You can get some hints on the results in my documentation about how to [read and interpret the IS graphs](../input_shaper.md). - -![vibrations measurement example](./../images/vibrations_example.png) - - -## Installation - - 1. Copy the [IS_vibrations_measurement.cfg](./../../macros/calibration/IS_vibrations_measurement.cfg) macro file directly into your own config. - 2. Be sure to have the `gcode_shell_command.py` Klipper extension installed. Easiest way to install it is to use the advanced section of KIAUH. This is done automatically when running Klippain. - 3. Add my [scripts](./../../scripts/) folder at the root of your own config (ie. in your `~/printer_data/config/` directory). - - Note: if using Windows to do the copy/paste of the files, be careful with the line endings for the `plot_graphs.sh` file and the `graph_vibrations.py` file: **Linux line endings (LF or \n) are mandatory!** If the file are using Windows line endings, you will get errors like `\r : unknown command` when running the script. If you're not confident regarding your text editor behavior, the best way is to directly download the files on the pi by using for example wget over SSH: - - ```bash - wget -P ~/printer_data/config/scripts https://raw.githubusercontent.com/Frix-x/klippain/main/scripts/plot_graphs.sh - wget -P ~/printer_data/config/scripts https://raw.githubusercontent.com/Frix-x/klippain/main/scripts/graph_vibrations.py - ``` - - 4. Make the scripts executable using SSH. When in the folder (`cd ~/printer_data/config/scripts`), use: - - ```bash - chmod +x ./plot_graphs.sh - chmod +x ./graph_vibrations.py - ``` - - 5. Add this new section at the end of your `printer.cfg` file: - ``` - [gcode_shell_command plot_graph] - command: bash /home/pi/printer_data/config/scripts/plot_graphs.sh - timeout: 500.0 - verbose: True - ``` - - Note: if your user is not `pi`, please correct the path in the command accordingly. - - 6. (Optional) You can modify the first lines of the `plot_graphs.sh` script to configure where you want to store the results. Default: `~/printer_data/config/adxl_results` - - -## Usage - -First you need to do the standard input shaper calibration! This macro should not be used before as it would be useless and the results invalid. - -Then, call the `VIBRATIONS_CALIBRATION` macro with the direction and speed range you want to measure. Here are the parameters available: - -| parameters | default value | description | -|-----------:|---------------|-------------| -|SIZE|60|size in mm of the area where the movements are done| -|DIRECTION|"XY"|direction vector where you want to do the measurements. Can be set to either "XY", "AB", "ABXY", "A", "B", "X", "Y", "Z"| -|Z_HEIGHT|20|z height to put the toolhead before starting the movements. Be careful, if your ADXL is under the nozzle, increase it to avoid a crash of the ADXL on the bed of the machine| -|VERBOSE|1|Wether to log the current speed in the console| -|MIN_SPEED|20|minimum speed of the toolhead in mm/s for the movements| -|MAX_SPEED|200|maximum speed of the toolhead in mm/s for the movements| -|SPEED_INCREMENT|2|speed increments of the toolhead in mm/s between every movements| -|TRAVEL_SPEED|200|speed in mm/s used for all the travels moves| -|ACCEL_CHIP|"adxl345"|accelerometer chip name in the config| - -Wait for the script to finish the computation and look for the results in the results folder. You can find them directly in your config folder by using the WebUI of Mainsail/Fluidd. You can get some hints on the results in my documentation about how to [read and interpret the IS graphs](../input_shaper.md). + > **Note** + > + > For Klippain users, nothing is needed to do as the latest version of K-Shake&Tune is always included in the full Klippain package! diff --git a/docs/images/IS_docs/belt_graphs/belts_problem.png b/docs/images/IS_docs/belt_graphs/belts_problem.png deleted file mode 100644 index 309da1a0c..000000000 Binary files a/docs/images/IS_docs/belt_graphs/belts_problem.png and /dev/null differ diff --git a/docs/images/IS_docs/belt_graphs/belts_problem2.png b/docs/images/IS_docs/belt_graphs/belts_problem2.png deleted file mode 100644 index e03fe88ef..000000000 Binary files a/docs/images/IS_docs/belt_graphs/belts_problem2.png and /dev/null differ diff --git a/docs/images/IS_docs/belt_graphs/different_tensions.png b/docs/images/IS_docs/belt_graphs/different_tensions.png deleted file mode 100644 index f851cea76..000000000 Binary files a/docs/images/IS_docs/belt_graphs/different_tensions.png and /dev/null differ diff --git a/docs/images/IS_docs/belt_graphs/different_tensions2.png b/docs/images/IS_docs/belt_graphs/different_tensions2.png deleted file mode 100644 index 2cb7c96b1..000000000 Binary files a/docs/images/IS_docs/belt_graphs/different_tensions2.png and /dev/null differ diff --git a/docs/images/IS_docs/belt_graphs/perfect graph.png b/docs/images/IS_docs/belt_graphs/perfect graph.png deleted file mode 100644 index 90c1583bc..000000000 Binary files a/docs/images/IS_docs/belt_graphs/perfect graph.png and /dev/null differ diff --git a/docs/images/IS_docs/ghosting.png b/docs/images/IS_docs/ghosting.png deleted file mode 100644 index 7db79d5cf..000000000 Binary files a/docs/images/IS_docs/ghosting.png and /dev/null differ diff --git a/docs/images/IS_docs/harmonic_oscillator.png b/docs/images/IS_docs/harmonic_oscillator.png deleted file mode 100644 index 3b0e33bd7..000000000 Binary files a/docs/images/IS_docs/harmonic_oscillator.png and /dev/null differ diff --git a/docs/images/IS_docs/how_IS_works.png b/docs/images/IS_docs/how_IS_works.png deleted file mode 100644 index d7b670cd1..000000000 Binary files a/docs/images/IS_docs/how_IS_works.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/TAP_125hz.png b/docs/images/IS_docs/shaper_graphs/TAP_125hz.png deleted file mode 100644 index ca4b65764..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/TAP_125hz.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/TAP_125hz_2.png b/docs/images/IS_docs/shaper_graphs/TAP_125hz_2.png deleted file mode 100644 index 894053a64..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/TAP_125hz_2.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/fan-off.png b/docs/images/IS_docs/shaper_graphs/fan-off.png deleted file mode 100644 index 238fc7beb..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/fan-off.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/fan-on.png b/docs/images/IS_docs/shaper_graphs/fan-on.png deleted file mode 100644 index 34c8fffe3..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/fan-on.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/insane_accels.png b/docs/images/IS_docs/shaper_graphs/insane_accels.png deleted file mode 100644 index 4592dd474..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/insane_accels.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/insane_accels2.png b/docs/images/IS_docs/shaper_graphs/insane_accels2.png deleted file mode 100644 index e1675c04a..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/insane_accels2.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/low_canbus.png b/docs/images/IS_docs/shaper_graphs/low_canbus.png deleted file mode 100644 index c2b4d493d..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/low_canbus.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/low_canbus_solved.png b/docs/images/IS_docs/shaper_graphs/low_canbus_solved.png deleted file mode 100644 index d3e2630a9..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/low_canbus_solved.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/low_freq_bad.png b/docs/images/IS_docs/shaper_graphs/low_freq_bad.png deleted file mode 100644 index 0e5c57f3f..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/low_freq_bad.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/low_freq_bad2.png b/docs/images/IS_docs/shaper_graphs/low_freq_bad2.png deleted file mode 100644 index 964490d03..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/low_freq_bad2.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/reso_good_x.png b/docs/images/IS_docs/shaper_graphs/reso_good_x.png deleted file mode 100644 index 7838cb1a7..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/reso_good_x.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/reso_good_y.png b/docs/images/IS_docs/shaper_graphs/reso_good_y.png deleted file mode 100644 index b3d07cb34..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/reso_good_y.png and /dev/null differ diff --git a/docs/images/IS_docs/shaper_graphs/shaper_reco.png b/docs/images/IS_docs/shaper_graphs/shaper_reco.png deleted file mode 100644 index 2d27fe680..000000000 Binary files a/docs/images/IS_docs/shaper_graphs/shaper_reco.png and /dev/null differ diff --git a/docs/input_shaper.md b/docs/input_shaper.md index cfb59c778..806e130bc 100644 --- a/docs/input_shaper.md +++ b/docs/input_shaper.md @@ -1,163 +1,3 @@ # Tuning Klipper's Input Shaper system -As more and more people use my macros, questions about interpreting the resonnance testing results arise. This document aims to provide some guidance on how to interpret them. Keep in mind that there is no universal method: different people may interpret the results differently or could have other opinions. It's important to experiment and find what works best for your own 3D printer. - - -## Understanding ringing -When a 3D printer moves, the motors apply some force to move the toolhead along a precise path. This force is transmitted from the motor shaft to the toolhead through the entire printer motion system. When the toolhead reaches a sharp corner and needs to change direction, its inertia makes it want to continue the movement in a straight line. The motors force the toolhead to turn, but the belts act like springs, allowing the toolhead to oscillate in the perpendicular direction. These oscillations produce visible artifacts on the printed parts, known as ringing or ghosting. - -![](./images/IS_docs/ghosting.png) - - -## Reading the graphs - -When tuning Input Shaper, keep the following in mind: - 1. **Focus on the shape of the graphs, not the exact numbers**. There could be differences between ADXL boards or even printers, so there is no specific "target" value. This means that you shouldn't expect to get the same graphs between different printers, even if they are similar in term of brand, parts, size and assembly. - 1. Small differences between consecutive test runs are normal, as ADXL quality and sensitivity is quite variable between boards. - 1. Perform the tests when the machine is heat-soaked and close to printing conditions, as belt tension can change with temperature. - 1. Avoid running the toolhead fans during the tests, as they introduce unnecessary noise to the graphs, making them harder to interpret. This means that even if you should heatsoak the printer, you should also refrain from activating the hotend heater during the test, as it will also trigger the hotend fan. However, as a bad fan can introduce some vibrations, feel free to use the test to diagnose an unbalanced fan as seen in the [Examples of Input Shaper graphs](#examples-of-input-shaper-graphs) section. - 1. Ensure the accuracy of your ADXL measurements by running a `MEASURE_AXES_NOISE` test and checking that the result is below 100 for all axes. If it's not, check your ADXL and wiring before continuing. - 1. The graphs can only show symptoms of possible problems and in different ways. Those symptoms can sometimes suggest causes, but they rarely pinpoint issues. - 1. Remember why you're running these tests (clean prints) and don't become too obsessive over perfect graphs. - - > **Note** - > - > Click on the section names below to expand them - -
-1. Belt graphs
- -**Before starting, ensure that the belts are properly tensioned**. For example, you can follow the [Voron belt tensioning documentation](https://docs.vorondesign.com/tuning/secondary_printer_tuning.html#belt-tension). This is crucial! - -Next, generate the belt graphs using the `BELTS_SHAPER_CALIBRATION` macro. Refer to the [IS workflow documentation](./features/is_workflow.md) for more information. - -#### Read the graphs - -On these graphs, you want both curves to look similar and overlap to form a single curve. Try to make them fit as closely as possible. It's acceptable to have "noise" around the main peak, but it should be present on both curves with a comparable amplitude. Keep in mind that when you tighten a belt, its main peak should move diagonally toward the upper right corner, changing significantly in amplitude and slightly in frequency. Additionally, the magnitude order of the main peaks *should typically* range from ~100k to ~1M on most machines. - -The resonant frequency/amplitude of the curves depends primarily on three parameters (and the actual tension): - - the *mass of the toolhead*, which is identical for both belts and has no effect here - - the *belt "elasticity"*, which changes over time as the belt wears. Ensure that you use the **same belt brand and type** for both A and B belts and that they were **installed at the same time** - - the *belt path length*, which is why they must have the **exact same number of teeth** so that one belt path is not longer than the other when tightened at the same tension - -**If these three parameters are met, there is no way that the curves could be different** or you can be sure that there is an underlying problem in at least one of the belt paths. Also, if the belt graphs have low amplitude curves (no distinct peaks) and a lot of noise, you will probably also have poor input shaper graphs. So before you continue, ensure that you have good belt graphs or fix your belt paths. Start by checking the belt tension, bearings, gantry screws, alignment of the belts on the idlers, and so on. - -#### Examples of belt graphs - -| Comment | Belt graphs examples 1 | Belt graphs examples 2 | -| --- | --- | --- | -| **Both of these two graphs are considered good**. As you can see, the main peak doesn't have to be perfect if you can get both curves to overlap | ![](./images/IS_docs/belt_graphs/perfect%20graph.png) | ![](./images/resonances_belts_example.png) | -| **These two graphs show incorrect belt tension**: in each case, one of the belts has insufficient tension (first is B belt, second is A belt). Begin by tightening it half a turn and measuring again | ![](./images/IS_docs/belt_graphs/different_tensions.png) | ![](./images/IS_docs/belt_graphs/different_tensions2.png) | -| **These two graphs indicate a belt path problem**: the belt tension could be adequate, but something else is happening in the belt paths. Start by checking the bearings and belt wear, or belt alignment | ![](./images/IS_docs/belt_graphs/belts_problem.png) | ![](./images/IS_docs/belt_graphs/belts_problem2.png) | - -
- - -
-2. Input Shaper graphs
- -**Before starting, ensure that the belts are properly tensioned** and that you already have good and clear belt graphs (see the previous section). - -Next, generate the Input Shaper graphs using the `AXES_SHAPER_CALIBRATION` macro. Refer to the [IS workflow documentation](./features/is_workflow.md) for more information. - -#### Read the graphs - -To effectively analyze input shaper graphs, there is no one-size-fits-all approach due to the variety of factors that can impact the 3D printer's performance or input shaper measurements. However, here are some hints on reading the graphs: - - A graph with a **single and thin peak** well detached from the background noise is ideal, as it can be easily filtered by input shaping. But depending on the machine and its mechanical configuration, it's not always possible to obtain this shape. The key to getting better graphs is a clean mechanical assembly with a special focus on the rigidity and stiffness of everything, from the table through the frame of the printer to the toolhead. - - As for the belt graphs, **focus on the shape of the graphs, not the exact frequency and energy value**. Indeed, the energy value doesn't provide much useful information. Use it only to compare two of your own graphs and to measure the impact of your mechanical changes between two consecutive tests, but never use it to compare against graphs from other people or other machines. - -When you are satisfied with your graphs, you will need to use the auto-computed values at the top to set the Input Shaping filters in your Klipper configuration. - -![](./images/IS_docs/shaper_graphs/shaper_reco.png) - -Here is some info to help you understand them: - - These data are automatically computed by a specific Klipper algorithm. This algorithm works pretty well if the graphs are clean enough. But **if your graphs are junk, it can't do magic and will give you pretty bad recommendations**: they will do nothing or even make the ringing worse, so do not use the values and fix your printer first! - - The recommended acceleration values (`accel<=...`) are not meant to be read alone. You need to also look at the `vibr` and `sm` values. They will give you the percentage of remaining vibrations and the smoothing after Input Shaping, if you use the recommended acceleration. - - Nothing will prevent you from using higher acceleration values; they are not a limit. However, if you do so, expect more vibrations and smoothing. Also, Input Shaping may find its limits and not be able to suppress all the ringing on your parts. - - The remaining vibrations `vibr` value is highly linked to ringing. So try to choose a filter with a very low value or even 0% if possible. - - High acceleration values are not useful at all if there is still a high level of remaining vibrations. You should address any mechanical issues before continuing. - - Each line represents the name of a different filtering algorithm. Each of them has its pros and cons: - * `ZV` is a pretty light filter and usually has some remaining vibrations. My recommendation would be to use it only if you want to do speed benchies and get the highest acceleration values while maintaining a low amount of smoothing on your parts. If you have "perfect" graphs and do not care that much about some remaining ringing, you can try it. - * `MZV` is most of the time the best filter on a well-tuned machine. It's a good compromise for low remaining vibrations while still allowing pretty good acceleration values. Keep in mind, `MZV` is only recommended by the algorithm on good graphs. - * `EI` works "ok" if you are not able to get better graphs. But first, try to fix your mechanical issues as best as you can before using it: almost every printer should be able to run `MZV` instead. - * `2HUMP_EI` and `3HUMP_EI` are not recommended and should be used only as a last resort. Usually, they lead to a high level of smoothing in order to suppress the ringing while also using relatively low acceleration values. If you get these algorithms recommended, you can almost be sure that you have mechanical problems under the hood (that lead to pretty bad or "wide" graphs). - -Then, just add to your configuration: -``` -[input_shaper] -shaper_freq_x: ... # center frequency for the X axis filter -shaper_type_x: ... # filter type for the X axis -shaper_freq_y: ... # center frequency for the Y axis filter -shaper_type_y: ... # filter type for the Y axis -``` - -#### Useful facts and myths debunking - -Sometimes people advise limiting the data to 100 Hz by manually editing the resulting .csv file because excitation does not go that high and these values should be ignored and considered wrong. This is a misconception and a bad idea because the excitation frequency is very different from the response frequency of the system, and they are not correlated at all. Indeed, it's plausible to get higher vibration frequencies, and editing the file manually will just "ignore" them and make them invisible even if they are still there on your printer. While higher frequency vibrations may not have a substantial effect on print quality, they can still indicate other issues within the system, likely noise and wear to the mechanical parts. Instead, focus on addressing the mechanical issues causing these problems. - -Another point is that I do not recommend using an extra-light X-beam (aluminum or carbon) on your machine, as it can negatively impact the printer's performance and Input Shaping results. Indeed, there is more than just mass at play (see the [theory behind it](#theory-behind-it)): lower mass also means more flexibility and more prone to wobble under high accelerations. This will impact negatively the Y axis graphs as the X-beam will flex under high accelerations. - -Finally, keep in mind that each axis has its own properties, such as mass and geometry, which will lead to different behaviors for each of them and will require different filters. Using the same input shaping settings for both axes is only valid if both axes are similar mechanically: this may be true for some machines, mainly Cross gantry configurations such as [CroXY](https://github.com/CroXY3D/CroXY) or [Annex-Engineering](https://github.com/Annex-Engineering) printers, but not for others. - -#### Examples of Input Shaper graphs - -In the following examples, the graphs are random graphs found online or sent to me for analysis. They are not necessarily to be read in pairs: the two graph columns are here to illustrate the comment with more than one example. - -| Comment | Example 1 | Example 2 | -| --- | --- | --- | -| **These two graphs are considered good**. As you can see, there is only one thin peak, well separated from the background noise | ![](./images/IS_docs/shaper_graphs/reso_good_x.png) | ![](./images/IS_docs/shaper_graphs/reso_good_y.png) | -| **These two graphs are really bad**: there is a lot of noise all over the spectrum. Something is really wrong and you should check all moving parts and screws. You should also check the belt tension and proper geometry of the gantry (racking) | ![](./images/IS_docs/shaper_graphs/insane_accels.png) | ![](./images/IS_docs/shaper_graphs/insane_accels2.png) | -| These two graphs have some **low frequency energy**. This usually means that there is some binding or grinding in the kinematics: something isn't moving freely. Check the belt alignment on the idlers, bearings, etc... | ![](./images/IS_docs/shaper_graphs/low_freq_bad.png) | ![](./images/IS_docs/shaper_graphs/low_freq_bad2.png) | -| These two graphs show **the TAP wobble problem**: check that the TAP MGN rail has the correct preload for stiffness and that the magnets are correct N52. Also pay attention to the assembly to make sure that everything is properly tightened | ![](./images/IS_docs/shaper_graphs/TAP_125hz.png) | ![](./images/IS_docs/shaper_graphs/TAP_125hz_2.png) | -| Here you can see **the effect of an unbalanced fan**: even if you should let the fan off during the final IS tuning, you can use this test to validate their correct behavior: an unbalanced fan usually add some very thin peak around 100-150Hz that disapear when the fan is off during the measurement | ![](./images/IS_docs/shaper_graphs/fan-on.png) | ![](./images/IS_docs/shaper_graphs/fan-off.png) | -| The graph on the left shows **a CANbus problem** (problem solved on the right): although the general shape looks good, the graph is not smooth but spiky. There is also usually some low frequency energy. This happens when the bus speed is too low: set it to 1M to solve the problem | ![](./images/IS_docs/shaper_graphs/low_canbus.png) | ![](./images/IS_docs/shaper_graphs/low_canbus_solved.png) | - -
- - -
-3. Klippain vibrations graphs
- -More details to be added later in this section... - -
- - -
-Special note on accelerometer (ADXL) mounting point
- -Input Shaping algorithms work by suppressing a single resonant frequency (or a range around a single resonant frequency). When setting the filter, **the primary goal is to target the resonant frequency of the toolhead and belts system** (see the [theory behind it](#theory-behind-it)), as this system has the most significant impact on print quality and is the root cause of ringing. - -When setting up Input Shaper, it is important to consider the accelerometer mounting point. There are mainly two possibilities, each with its pros and cons: - 1. **Directly at the nozzle tip**: This method provides a more accurate and comprehensive measurement of everything in your machine. It captures the main resonant frequency along with other vibrations and movements, such as toolhead wobbling and printer frame movements. This approach is excellent for diagnosing your machine's kinematics and troubleshooting problems. However, it also leads to noisier graphs, making it harder for the algorithm to select the correct filter for input shaping. Graphs may appear worse, but this is due to the different "point of view" of the printer's behavior. - 1. **At the toolhead's center of gravity**: I personally recommend mounting the accelerometer in this way, as it provides a clear view of the main resonant frequency you want to target, allowing for accurate input shaper filter settings. This approach results in cleaner graphs with less visible noise from other subsystem vibrations, making interpretation easier for both automatic algorithms and users. However, this method provides less detail in the graphs and may be slightly less effective for troubleshooting printer problems. - -A suggested workflow is to first use the nozzle mount to diagnose mechanical issues, such as loose screws or a bad X carriage. Once the mechanics are in good condition, switch to a mounting point closer to the toolhead's center of gravity for setting the input shaper filter settings by using cleaner graphs that highlights the most impactful frequency. - -
- - -## Theory behind it - -### Modeling the motion system -The motion system of a 3D printer can be described as a spring and mass system, best modeled as a [harmonic oscillator](https://en.wikipedia.org/wiki/Harmonic_oscillator). This type of system has two key parameters: - -| Schematics | Undamped resonnant frequency
(natural frequency) | Damping ratio ζ | -| --- | --- | --- | -| ![](./images/IS_docs/harmonic_oscillator.png) | $$\frac{1}{2\pi}\sqrt{\frac{k}{m}}$$ | $$\frac{c}{2}\sqrt{\frac{1}{km}}$$ | -| See [here for examples](https://beltoforion.de/en/harmonic_oscillator/) | `k` [N/m]: spring constant
`m` [g]: moving mass | `c` [N·s/m]: viscous damping coefficient
`k` [N/m]: spring constant
`m` [g]: moving mass | - -When an oscillating input force is applied at a resonant frequency (or a Fourier component of it) on a dynamic system, the system will oscillate at a higher amplitude than when the same force is applied at other, non-resonant frequencies. This is called a resonance and can be dangerous for some systems but on our printers this will mainly lead to vibrations and oscillations of the toolhead. - -On the other hand, the damping ratio (ζ) is a dimensionless measure describing how oscillations in a system decay after a perturbation. It can vary from underdamped (ζ < 1), through critically damped (ζ = 1) to overdamped (ζ > 1). - -In 3D printers, it's quite challenging to measure the spring constant `k` and even more challenging to measure the viscous damping coefficient `c`, as they are affected by various factors such as belts, plastic parts, frame rigidity, rails, friction, grease, and motor control. Furthermore, a 3D printer is made up of many subsystems, each with its own behavior. Some subsystems, such as the toolhead/belts system, have a bigger impact on ringing than others, such as the motor shaft resonance for example. - -### How Input Shaping helps -The rapid movement of machines is a challenging control problem because it often results in high levels of vibration. As a result, machines are typically moved relatively slowly. Input shaping is an open-loop control method that allows for higher speeds of motion by limiting vibration induced by the reference command. It can also improve the reliability of the stealthChop mode of Trinamic stepper drivers. - -It works by creating a command signal that cancels its own vibration, achieved by [convoluting](https://en.wikipedia.org/wiki/Convolution) specifically crafted impulse signals (A2) with the original system control signal (A1). The resulting shaped signal is then used to drive the system (Total Response). To craft these impulses, the system's undamped resonant frequency and damping ratio are used. - -![](./images/IS_docs/how_IS_works.png) - -Klipper measures these parameters by exciting the printer with a series of input commands and recording the response behavior using an accelerometer. Resonances can be identified on the resulting graphs by large spikes indicating their frequency and energy. Additionnaly, the damping ratio is usually unknown and hard to estimate without a special equipment, so Klipper uses 0.1 value by default, which is a good all-round value that works well for most 3D printers. +This documentation has now moved into the [Klippain Shake&Tune module](https://github.com/Frix-x/klippain-shaketune/tree/main/docs) documentation. diff --git a/docs/overrides.md b/docs/overrides.md index e79208eb8..e8273c4a7 100644 --- a/docs/overrides.md +++ b/docs/overrides.md @@ -15,7 +15,7 @@ Use overrides to tweak machine dimensions, invert motor directions, change axis Since my defaults aim to be as generic as possible, you won't need many overrides. However, there are still some common elements to check. -For example, **pay special attention to axis limits** in the `[stepper_...]` sections, run current, etc. Verify thermistor types in `[extruder]` and `[heated_bed]` sections. If using a multi-MCU configuration, you'll need to override any section where pins are connected to the secondary or toolhead boards to specify it. Finally, use overrides if you want to change motor direction or add a pull-up/down (using `!`, `^`, and `~`). +For example, **pay special attention to axis limits** in the `[stepper_...]` sections, run current, etc. Verify thermistor types in `[extruder]` and `[heater_bed]` sections. If using a multi-MCU configuration, you'll need to override any section where pins are connected to the secondary or toolhead boards to specify it. Finally, use overrides if you want to change motor direction or add a pull-up/down (using `!`, `^`, and `~`). Additionally, if you want to add a new macro to Klippain or even replace an existing one to adapt it to your use case, you can do it the same way! @@ -38,7 +38,7 @@ dir_pin: !mcu:Z2_DIR Changing a thermistor type (like for the bed), can be done this way: ``` -[heated_bed] +[heater_bed] sensor_type: ... ``` diff --git a/install.sh b/install.sh index dc9e22b29..7fe9053b6 100755 --- a/install.sh +++ b/install.sh @@ -128,8 +128,9 @@ function install_config { # CHMOD the scripts to be sure they are all executables (Git should keep the modes on files but it's to be sure) chmod +x ${FRIX_CONFIG_PATH}/install.sh - for file in graph_vibrations.py plot_graphs.sh; do - chmod +x ${FRIX_CONFIG_PATH}/scripts/$file + chmod +x ${FRIX_CONFIG_PATH}/uninstall.sh + for file in is_workflow.py graph_vibrations.py graph_shaper.py graph_belts.py; do + chmod +x ${FRIX_CONFIG_PATH}/scripts/is_workflow/$file done # Symlink the gcode_shell_command.py file in the correct Klipper folder (erased to always get the last version) diff --git a/macros/base/homing/homing_conditional.cfg b/macros/base/homing/homing_conditional.cfg index fc2138379..b3498a7a7 100644 --- a/macros/base/homing/homing_conditional.cfg +++ b/macros/base/homing/homing_conditional.cfg @@ -3,14 +3,14 @@ description: Homing only if necessary gcode: {% set status_leds_enabled = printer["gcode_macro _USER_VARIABLES"].status_leds_enabled %} - {% if status_leds_enabled %} - STATUS_LEDS COLOR="HOMING" - {% endif %} - {% if "xyz" not in printer.toolhead.homed_axes %} + {% if status_leds_enabled %} + STATUS_LEDS COLOR="HOMING" + {% endif %} G28 {% endif %} {% if status_leds_enabled %} STATUS_LEDS COLOR="READY" - {% endif %} + {% endif %} + \ No newline at end of file diff --git a/macros/base/homing/tilting.cfg b/macros/base/homing/tilting.cfg index 55ff4db01..50fe6457a 100644 --- a/macros/base/homing/tilting.cfg +++ b/macros/base/homing/tilting.cfg @@ -1,11 +1,12 @@ [gcode_macro _TILT_CALIBRATE] description: Do a QGL, Z_tilt, etc... depending of the machine configuration gcode: - {% set FORCE_OPERATION = params.FORCE|default(true) %} + {% set FORCE_OPERATION = params.FORCE|default('true') %} {% set conf_QGL = printer["gcode_macro _USER_VARIABLES"].qgl_enabled %} {% set conf_ztilt = printer["gcode_macro _USER_VARIABLES"].ztilt_enabled %} {% set status_leds_enabled = printer["gcode_macro _USER_VARIABLES"].status_leds_enabled %} {% set probe_type_enabled = printer["gcode_macro _USER_VARIABLES"].probe_type_enabled %} + {% set verbose = printer["gcode_macro _USER_VARIABLES"].verbose %} {% if probe_type_enabled == "dockable" %} SET_GCODE_VARIABLE MACRO=_PROBE_ON_ERROR_ACTION VARIABLE=probing VALUE=True @@ -16,14 +17,14 @@ gcode: {% endif %} {% if conf_QGL %} - {% if printer.quad_gantry_level.applied|lower == 'false' or FORCE_OPERATION %} + {% if printer.quad_gantry_level.applied|lower == 'false' or FORCE_OPERATION|lower == 'true' %} {% if verbose %} RESPOND MSG="QGL..." {% endif %} QUAD_GANTRY_LEVEL {% endif %} {% elif conf_ztilt %} - {% if printer.z_tilt.applied|lower == 'false' or FORCE_OPERATION %} + {% if printer.z_tilt.applied|lower == 'false' or FORCE_OPERATION|lower == 'true' %} {% if verbose %} RESPOND MSG="Z tilt adjust..." {% endif %} diff --git a/macros/base/start_print.cfg b/macros/base/start_print.cfg index e298f2e75..e49cc2cfd 100644 --- a/macros/base/start_print.cfg +++ b/macros/base/start_print.cfg @@ -9,10 +9,11 @@ variable_chamber_maxtime: 0 variable_initial_tool: 0 variable_material: "XXX" variable_fl_size: "0_0_0_0" +variable_bed_mesh_profile: "" gcode: # Get all the parameters passed from the slicer - {% set BED_TEMP = params.BED_TEMP|default(printer["gcode_macro _USER_VARIABLES"].print_default_bed_temp)|float %} # Extruder temperature - {% set EXTRUDER_TEMP = params.EXTRUDER_TEMP|default(printer["gcode_macro _USER_VARIABLES"].print_default_extruder_temp)|float %} # Bed temperature + {% set BED_TEMP = params.BED_TEMP|default(printer["gcode_macro _USER_VARIABLES"].print_default_bed_temp)|float %} # Bed temperature + {% set EXTRUDER_TEMP = params.EXTRUDER_TEMP|default(printer["gcode_macro _USER_VARIABLES"].print_default_extruder_temp)|float %} # Extruder temperature {% set Z_ADJUST = params.Z_ADJUST|default(0)|float %} # Optionnal Z adjustement from the slicer profile (ex. use it if you have textured vs smooth slicer profiles) {% set SOAK = params.SOAK|default(printer["gcode_macro _USER_VARIABLES"].print_default_soak)|int %} # Heatsoak time of the bed in minutes {% set CHAMBER_TEMP = params.CHAMBER|default(printer["gcode_macro _USER_VARIABLES"].print_default_chamber_temp)|int %} # Chamber temperature setpoint @@ -20,6 +21,7 @@ gcode: {% set INITIAL_TOOL = params.INITIAL_TOOL|default(0)|int %} # Initial tool (for the ERCF initialization) {% set MATERIAL = params.MATERIAL|default(printer["gcode_macro _USER_VARIABLES"].print_default_material)|string %} # Material type set in the slicer {% set FL_SIZE = params.SIZE|default("0_0_0_0")|string %} # Get bounding box of the first layer for the adaptive bed mesh + {% set BED_MESH_PROFILE = params.MESH|default("")|string %} # Bed mesh profile to load # Set the variables to be used in all the modules based on the slicer parameters SET_GCODE_VARIABLE MACRO=START_PRINT VARIABLE=bed_temp VALUE={BED_TEMP} @@ -30,6 +32,7 @@ gcode: SET_GCODE_VARIABLE MACRO=START_PRINT VARIABLE=initial_tool VALUE={INITIAL_TOOL} SET_GCODE_VARIABLE MACRO=START_PRINT VARIABLE=material VALUE='"{MATERIAL}"' SET_GCODE_VARIABLE MACRO=START_PRINT VARIABLE=fl_size VALUE='"{FL_SIZE}"' + SET_GCODE_VARIABLE MACRO=START_PRINT VARIABLE=bed_mesh_profile VALUE='"{BED_MESH_PROFILE}"' # Get all the config options and configurations for this macro {% set verbose = printer["gcode_macro _USER_VARIABLES"].verbose %} @@ -70,6 +73,14 @@ gcode: BED_MESH_CLEAR {% endif %} + # If a filter is enabled and already running due to a print that just finished, we stop + # it now and deactivate the pending delayed gcode that could be running. The filter + # could be restarted later in the START_PRINT sequence depending of the parameters + {% if filter_enabled %} + UPDATE_DELAYED_GCODE ID=_STOP_FILTER_DELAYED DURATION=0 + STOP_FILTER + {% endif %} + {% if klippain_ercf_enabled %} {% if printer.ercf.enabled %} {% if ercf_reset_stats_on_start_print %} @@ -122,11 +133,11 @@ gcode: {% elif action == "extruder_preheating" %} _MODULE_EXTRUDER_PREHEATING {% elif action == "custom1" %} - _MODULE_CUSTOM1 + _MODULE_CUSTOM1 {rawparams} {% elif action == "custom2" %} - _MODULE_CUSTOM2 + _MODULE_CUSTOM2 {rawparams} {% elif action == "custom3" %} - _MODULE_CUSTOM3 + _MODULE_CUSTOM3 {rawparams} {% else %} { action_raise_error("Unknown module called in START_PRINT! Please verify your startprint_actions variable override!") } {% endif %} @@ -186,7 +197,7 @@ gcode: STATUS_LEDS COLOR="HEATING" {% endif %} - {% if printer.heater_bed.target < (BED_TEMP - 8) %} + {% if printer.heater_bed.temperature < (BED_TEMP - 8) %} # If the machine is equiped by a chamber temperature sensor and a recirculating filter (check is automatic under the hood), # then we look if a specific chamber temperature is needed and we power on the recirculating filter to spread the heat {% if (CHAMBER_TEMP > 0) and filter_enabled %} @@ -357,20 +368,28 @@ gcode: gcode: # ----- BED MESH ------------------------------------------- {% set FL_SIZE = printer["gcode_macro START_PRINT"].fl_size %} + {% set BED_MESH_PROFILE = printer["gcode_macro START_PRINT"].bed_mesh_profile %} {% set verbose = printer["gcode_macro _USER_VARIABLES"].verbose %} {% set bed_mesh_enabled = printer["gcode_macro _USER_VARIABLES"].bed_mesh_enabled %} {% set status_leds_enabled = printer["gcode_macro _USER_VARIABLES"].status_leds_enabled %} {% if bed_mesh_enabled %} - {% if status_leds_enabled %} - STATUS_LEDS COLOR="MESHING" - {% endif %} - {% if verbose %} - RESPOND MSG="Bed mesh measurement..." - {% endif %} + {% if BED_MESH_PROFILE == "" %} + {% if status_leds_enabled %} + STATUS_LEDS COLOR="MESHING" + {% endif %} + {% if verbose %} + RESPOND MSG="Bed mesh measurement..." + {% endif %} - ADAPTIVE_BED_MESH SIZE={FL_SIZE} + ADAPTIVE_BED_MESH SIZE={FL_SIZE} + {% else %} + {% if verbose %} + RESPOND MSG="Load bed mesh profile : {BED_MESH_PROFILE}" + {% endif %} + BED_MESH_PROFILE LOAD={BED_MESH_PROFILE} + {% endif %} {% endif %} diff --git a/macros/calibration/IS_shaper_calibrate.cfg b/macros/calibration/IS_shaper_calibrate.cfg index 8c52b8f56..04aeaf432 100644 --- a/macros/calibration/IS_shaper_calibrate.cfg +++ b/macros/calibration/IS_shaper_calibrate.cfg @@ -2,18 +2,15 @@ ###### STANDARD INPUT_SHAPER CALIBRATIONS ###### ################################################ # Written by Frix_x#0161 # -# @version: 1.3 +# @version: 1.4 # CHANGELOG: +# v1.4: added possibility to only run one axis at a time for the axes shaper calibration # v1.3: added possibility to override the default parameters # v1.2: added EXCITATE_AXIS_AT_FREQ to hold a specific excitating frequency on an axis and diagnose mechanical problems # v1.1: added M400 to validate that the files are correctly saved to disk # v1.0: first version of the automatic input shaper workflow -# ------------------------------------------------------------------------------------------------------------------------- -# If you want to use it into your own config, please install it as a standalone macro as described in the -# installation section of this file: docs > features > is_workflow.md -# ------------------------------------------------------------------------------------------------------------------------- ### What is it ? ### # This macro helps you to configure the input shaper algorithm of Klipper by running the tests sequencially and calling an automatic script @@ -35,16 +32,39 @@ gcode: {% set min_freq = params.FREQ_START|default(5)|float %} {% set max_freq = params.FREQ_END|default(133.3)|float %} {% set hz_per_sec = params.HZ_PER_SEC|default(1)|float %} + {% set axis = params.AXIS|default("all")|string|lower %} + + {% set X, Y = False, False %} + + {% if axis == "all" %} + {% set X, Y = True, True %} + {% elif axis == "x" %} + {% set X = True %} + {% elif axis == "y" %} + {% set Y = True %} + {% else %} + { action_raise_error("AXIS selection invalid. Should be either all, x or y!") } + {% endif %} - TEST_RESONANCES AXIS=X FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec} - M400 - TEST_RESONANCES AXIS=Y FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec} - M400 + {% if X %} + TEST_RESONANCES AXIS=X OUTPUT=raw_data NAME=x FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec} + M400 - {% if verbose %} - RESPOND MSG="Graphs generation... Please wait a minute or two and look in the configured folder." + {% if verbose %} + RESPOND MSG="X axis shaper graphs generation..." + {% endif %} + RUN_SHELL_COMMAND CMD=plot_graph PARAMS=SHAPER + {% endif %} + + {% if Y %} + TEST_RESONANCES AXIS=Y OUTPUT=raw_data NAME=y FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec} + M400 + + {% if verbose %} + RESPOND MSG="Y axis shaper graphs generation..." + {% endif %} + RUN_SHELL_COMMAND CMD=plot_graph PARAMS=SHAPER {% endif %} - RUN_SHELL_COMMAND CMD=plot_graph PARAMS=SHAPER [gcode_macro BELTS_SHAPER_CALIBRATION] @@ -61,7 +81,7 @@ gcode: M400 {% if verbose %} - RESPOND MSG="Graphs generation... Please wait a minute or two and look in the configured folder." + RESPOND MSG="Belts graphs generation..." {% endif %} RUN_SHELL_COMMAND CMD=plot_graph PARAMS=BELTS diff --git a/macros/calibration/IS_vibrations_measurement.cfg b/macros/calibration/IS_vibrations_measurement.cfg index debd946e9..40af7707d 100644 --- a/macros/calibration/IS_vibrations_measurement.cfg +++ b/macros/calibration/IS_vibrations_measurement.cfg @@ -2,16 +2,13 @@ ###### VIBRATIONS AND SPEED OPTIMIZATIONS ###### ################################################ # Written by Frix_x#0161 # -# @version: 2.0 +# @version: 2.1 # CHANGELOG: +# v2.1: allow decimal entries for speed and increment and added the E axis as an option to be neasured # v2.0: added the possibility to measure mutliple axis # v1.0: first speed and vibrations optimization macro -# ------------------------------------------------------------------------------------------------------------------------- -# If you want to use it into your own config, please install it as a standalone macro as described in the -# installation section of this file: docs > features > vibr_measurements.md -# ------------------------------------------------------------------------------------------------------------------------- ### What is it ? ### # This macro helps you to identify the speed settings that exacerbate the vibrations of the machine (ie. where the frame resonate badly). @@ -44,9 +41,9 @@ gcode: {% set z_height = params.Z_HEIGHT|default(20)|int %} # z height to put the toolhead before starting the movements {% set verbose = params.VERBOSE|default(true) %} # Wether to log the current speed in the console - {% set min_speed = params.MIN_SPEED|default(20)|int * 60 %} # minimum feedrate for the movements - {% set max_speed = params.MAX_SPEED|default(200)|int * 60 %} # maximum feedrate for the movements - {% set speed_increment = params.SPEED_INCREMENT|default(2)|int * 60 %} # feedrate increment between each move + {% set min_speed = params.MIN_SPEED|default(20)|float * 60 %} # minimum feedrate for the movements + {% set max_speed = params.MAX_SPEED|default(200)|float * 60 %} # maximum feedrate for the movements + {% set speed_increment = params.SPEED_INCREMENT|default(2)|float * 60 %} # feedrate increment between each move {% set feedrate_travel = params.TRAVEL_SPEED|default(200)|int * 60 %} # travel feedrate between moves {% set accel_chip = params.ACCEL_CHIP|default("adxl345") %} # ADXL chip name in the config @@ -56,6 +53,7 @@ gcode: # {% set mid_x = printer.toolhead.axis_maximum.x|float / 2 %} {% set mid_y = printer.toolhead.axis_maximum.y|float / 2 %} + {% set nb_samples = ((max_speed - min_speed) / speed_increment + 1) | int %} {% set direction_factor = { 'XY' : { @@ -123,6 +121,10 @@ gcode: '0' : {'x': 0.0, 'y': 0.0, 'z': 1.0 }, '1' : {'x': 0.0, 'y': 0.0, 'z': 0.0 } } + }, + 'E' : { + 'start' : {'x': 0.0, 'y': 0.0 }, + 'move_factor' : 0.05 } } %} @@ -134,12 +136,16 @@ gcode: { action_raise_error("Must Home printer first!") } {% endif %} - {% if (size / (max_speed / 60)) < 0.25 %} + {% if params.SPEED_INCREMENT|default(2)|float * 100 != (params.SPEED_INCREMENT|default(2)|float * 100)|int %} + { action_raise_error("Only 2 decimal digits are allowed for SPEED_INCREMENT") } + {% endif %} + + {% if (size / (max_speed / 60)) < 0.25 and direction != 'E' %} { action_raise_error("SIZE is too small for this MAX_SPEED. Increase SIZE or decrease MAX_SPEED!") } {% endif %} {% if not (direction in direction_factor) %} - { action_raise_error("DIRECTION is not valid. Only XY, AB, ABXY, A, B, X, Y or Z is allowed!") } + { action_raise_error("DIRECTION is not valid. Only XY, AB, ABXY, A, B, X, Y, Z or E is allowed!") } {% endif %} {action_respond_info("")} @@ -149,6 +155,7 @@ gcode: SAVE_GCODE_STATE NAME=STATE_VIBRATIONS_CALIBRATION + M83 G90 # Going to the start position @@ -156,21 +163,24 @@ gcode: G1 X{mid_x + (size * direction_factor[direction].start.x) } Y{mid_y + (size * direction_factor[direction].start.y)} F{feedrate_travel} # vibration pattern for each frequency - {% for curr_speed in range(min_speed, max_speed + speed_increment) %} - {% if (curr_speed - min_speed) % speed_increment == 0 %} - {% if verbose %} - RESPOND MSG="Current speed: {(curr_speed / 60)|int} mm/s" - {% endif %} + {% for curr_sample in range(0, nb_samples) %} + {% set curr_speed = min_speed + curr_sample * speed_increment %} + {% if verbose %} + RESPOND MSG="{"Current speed: %.2f mm/s" % (curr_speed / 60)|float}" + {% endif %} - ACCELEROMETER_MEASURE CHIP={accel_chip} + ACCELEROMETER_MEASURE CHIP={accel_chip} + {% if direction == 'E' %} + G0 E{curr_speed*direction_factor[direction].move_factor} F{curr_speed} + {% else %} {% for key, factor in direction_factor[direction].move_factors|dictsort %} G1 X{mid_x + (size * factor.x) } Y{mid_y + (size * factor.y)} Z{z_height + (size * factor.z)} F{curr_speed} {% endfor %} - ACCELEROMETER_MEASURE CHIP={accel_chip} NAME=sp{(curr_speed / 60)|int}n1 - - G4 P300 - M400 {% endif %} + ACCELEROMETER_MEASURE CHIP={accel_chip} NAME=sp{("%.2f" % (curr_speed / 60)|float)|replace('.','_')}n1 + + G4 P300 + M400 {% endfor %} {% if verbose %} diff --git a/macros/hardware_functions/status_leds.cfg b/macros/hardware_functions/status_leds.cfg index 92aff5d91..5642f91c0 100644 --- a/macros/hardware_functions/status_leds.cfg +++ b/macros/hardware_functions/status_leds.cfg @@ -96,6 +96,9 @@ gcode: [gcode_macro STATUS_LEDS] gcode: {% set color = params.COLOR|default('off')|lower %} + {% set logo_leds_name = printer["gcode_macro _USER_VARIABLES"]["status_leds_logo_led_name"] %} + {% set nozzle_leds_name = printer["gcode_macro _USER_VARIABLES"]["status_leds_nozzle_led_name"] %} + {% set logo_transmit = 0 if (logo_leds_name == nozzle_leds_name) else 1 %} {% set status_color = { 'ready': { @@ -177,7 +180,7 @@ gcode: {% endif %} {% if printer["gcode_macro _USER_VARIABLES"].status_leds_enabled %} - _SET_LEDS_BY_NAME LEDS="logo" COLOR={status_color[color].logo} TRANSMIT=0 + _SET_LEDS_BY_NAME LEDS="logo" COLOR={status_color[color].logo} TRANSMIT={logo_transmit} _SET_LEDS_BY_NAME LEDS="nozzle" COLOR={status_color[color].nozzle} TRANSMIT=1 {% endif %} diff --git a/macros/helpers/bed_heater_ctrl.cfg b/macros/helpers/bed_heater_ctrl.cfg index 654477c53..1e8cad7ab 100644 --- a/macros/helpers/bed_heater_ctrl.cfg +++ b/macros/helpers/bed_heater_ctrl.cfg @@ -6,12 +6,19 @@ rename_existing: M190.1 gcode: {% set S = params.S|float %} + {% set actual_temp = printer.heater_bed.temperature|float %} {% set fix_heaters_temperature_settle = printer["gcode_macro _USER_VARIABLES"].fix_heaters_temperature_settle %} {% if fix_heaters_temperature_settle %} M140 {% for p in params %}{'%s%s' % (p, params[p])}{% endfor %} - TEMPERATURE_WAIT SENSOR=heater_bed MINIMUM={S} + {% if S != 0 %} + {% if actual_temp <= S %} + TEMPERATURE_WAIT SENSOR=heater_bed MINIMUM={S} + {% else %} + TEMPERATURE_WAIT SENSOR=heater_bed MAXIMUM={S} + {% endif %} + {% endif %} {% else %} M190.1 {% for p in params %}{'%s%s' % (p, params[p])}{% endfor %} {% endif %} diff --git a/macros/helpers/filament_swap.cfg b/macros/helpers/filament_swap.cfg index 10a981c0e..bccab66af 100644 --- a/macros/helpers/filament_swap.cfg +++ b/macros/helpers/filament_swap.cfg @@ -47,6 +47,9 @@ gcode: G4 P3000 G1 E{DISTANCE|float * -1} F3000 + # Flushing Klipper's buffer to ensure the unload is done before continuing + M400 + RESTORE_GCODE_STATE NAME=UNLOAD_FILAMENT_state {% if filament_sensor_enabled %} @@ -103,6 +106,9 @@ gcode: G1 E{DISTANCE|float} F200 G1 E50 F150 + # Flushing Klipper's buffer to ensure the load is done before continuing + M400 + G92 E0 RESTORE_GCODE_STATE NAME=LOAD_FILAMENT_state @@ -170,6 +176,10 @@ gcode: # set last pressure advance SET_PRESSURE_ADVANCE ADVANCE={old_pressure_advance} + + # Flushing Klipper's buffer to ensure the tip shaping sequence is done before continuing + M400 + RESTORE_GCODE_STATE NAME=TIP_SHAPING_state {% if filament_sensor_enabled %} diff --git a/macros/helpers/hotend_heater_ctrl.cfg b/macros/helpers/hotend_heater_ctrl.cfg index c406a3ea6..c704e3745 100644 --- a/macros/helpers/hotend_heater_ctrl.cfg +++ b/macros/helpers/hotend_heater_ctrl.cfg @@ -2,16 +2,24 @@ # on low thermal inertia devices such as the BambuLabs hotend. This allows a # shunt of the "waiting time" during temperature settle in case there is some problems + [gcode_macro M109] rename_existing: M109.1 gcode: {% set S = params.S|float %} + {% set actual_temp = printer.extruder.temperature|float %} {% set fix_heaters_temperature_settle = printer["gcode_macro _USER_VARIABLES"].fix_heaters_temperature_settle %} {% if fix_heaters_temperature_settle %} M104 {% for p in params %}{'%s%s' % (p, params[p])}{% endfor %} - TEMPERATURE_WAIT SENSOR=extruder MINIMUM={S} + {% if S != 0 %} + {% if actual_temp <= S %} + TEMPERATURE_WAIT SENSOR=extruder MINIMUM={S} + {% else %} + TEMPERATURE_WAIT SENSOR=extruder MAXIMUM={S} + {% endif %} + {% endif %} {% else %} M109.1 {% for p in params %}{'%s%s' % (p, params[p])}{% endfor %} - {% endif %} + {% endif %} \ No newline at end of file diff --git a/macros/helpers/nozzle_cleaning.cfg b/macros/helpers/nozzle_cleaning.cfg index 3f7fb2061..aa7c599b9 100644 --- a/macros/helpers/nozzle_cleaning.cfg +++ b/macros/helpers/nozzle_cleaning.cfg @@ -111,6 +111,7 @@ gcode: G92 E0 # Wait some time to let the nozzle ooze before cleaning + # No M400 needed here since G4 is also flushing Klipper's buffer G4 P{OOZE_TIME * 1000} {% if filament_sensor_enabled %} diff --git a/macros/helpers/prime_line.cfg b/macros/helpers/prime_line.cfg index 84e800d8c..fef104b69 100644 --- a/macros/helpers/prime_line.cfg +++ b/macros/helpers/prime_line.cfg @@ -99,6 +99,8 @@ gcode: G1 E-0.2 F2100 G92 E0 G1 Z3 F{Sz} + + # Flushing Klipper's buffer to ensure the primeline sequence is done before continuing M400 {% if klippain_ercf_enabled %} diff --git a/scripts/graph_vibrations.py b/scripts/graph_vibrations.py deleted file mode 100755 index 915d66179..000000000 --- a/scripts/graph_vibrations.py +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env python3 - -################################################## -###### SPEED AND VIBRATIONS PLOTTING SCRIPT ###### -################################################## -# Written by Frix_x#0161 # -# @version: 1.2 - -# CHANGELOG: -# v1.2: fixed a bug that could happen when username is not "pi" (thanks @spikeygg) -# v1.1: better graph formatting -# v1.0: first version of the script - - -# Be sure to make this script executable using SSH: type 'chmod +x ./graph_vibrations.py' when in the folder ! - -##################################################################### -################ !!! DO NOT EDIT BELOW THIS LINE !!! ################ -##################################################################### - -import optparse, matplotlib, re, sys, importlib, os, operator -from collections import OrderedDict -import numpy as np -import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager -import matplotlib.ticker - -matplotlib.use('Agg') - - -###################################################################### -# Computation -###################################################################### - -def calc_freq_response(data): - # Use Klipper standard input shaper objects to do the computation - helper = shaper_calibrate.ShaperCalibrate(printer=None) - return helper.process_accelerometer_data(data) - - -def calc_psd(datas, group, max_freq): - psd_list = [] - first_freqs = None - signal_axes = ['x', 'y', 'z', 'all'] - - for i in range(0, len(datas), group): - - # Round up to the nearest power of 2 for faster FFT - N = datas[i].shape[0] - T = datas[i][-1,0] - datas[i][0,0] - M = 1 << int((N/T) * 0.5 - 1).bit_length() - if N <= M: - # If there is not enough lines in the array to be able to round up to the - # nearest power of 2, we need to pad some zeros at the end of the array to - # avoid entering a blocking state from Klipper shaper_calibrate.py - datas[i] = np.pad(datas[i], [(0, (M-N)+1), (0, 0)], mode='constant', constant_values=0) - - freqrsp = calc_freq_response(datas[i]) - for n in range(group - 1): - data = datas[i + n + 1] - - # Round up to the nearest power of 2 for faster FFT - N = data.shape[0] - T = data[-1,0] - data[0,0] - M = 1 << int((N/T) * 0.5 - 1).bit_length() - if N <= M: - # If there is not enough lines in the array to be able to round up to the - # nearest power of 2, we need to pad some zeros at the end of the array to - # avoid entering a blocking state from Klipper shaper_calibrate.py - data = np.pad(data, [(0, (M-N)+1), (0, 0)], mode='constant', constant_values=0) - - freqrsp.add_data(calc_freq_response(data)) - - if not psd_list: - # First group, just put it in the result list - first_freqs = freqrsp.freq_bins - psd = freqrsp.psd_sum[first_freqs <= max_freq] - px = freqrsp.psd_x[first_freqs <= max_freq] - py = freqrsp.psd_y[first_freqs <= max_freq] - pz = freqrsp.psd_z[first_freqs <= max_freq] - psd_list.append([psd, px, py, pz]) - else: - # Not the first group, we need to interpolate every new signals - # to the first one to equalize the frequency_bins between them - signal_normalized = dict() - freqs = freqrsp.freq_bins - for axe in signal_axes: - signal = freqrsp.get_psd(axe) - signal_normalized[axe] = np.interp(first_freqs, freqs, signal) - - # Remove data above max_freq on all axes and add to the result list - psd = signal_normalized['all'][first_freqs <= max_freq] - px = signal_normalized['x'][first_freqs <= max_freq] - py = signal_normalized['y'][first_freqs <= max_freq] - pz = signal_normalized['z'][first_freqs <= max_freq] - psd_list.append([psd, px, py, pz]) - - return first_freqs[first_freqs <= max_freq], psd_list - - -def calc_powertot(psd_list, freqs): - pwrtot_sum = [] - pwrtot_x = [] - pwrtot_y = [] - pwrtot_z = [] - - for psd in psd_list: - pwrtot_sum.append(np.trapz(psd[0], freqs)) - pwrtot_x.append(np.trapz(psd[1], freqs)) - pwrtot_y.append(np.trapz(psd[2], freqs)) - pwrtot_z.append(np.trapz(psd[3], freqs)) - - return [pwrtot_sum, pwrtot_x, pwrtot_y, pwrtot_z] - - -###################################################################### -# Graphing -###################################################################### - -def plot_total_power(ax, speeds, power_total): - ax.set_title('Vibrations decomposition') - ax.set_xlabel('Speed (mm/s)') - ax.set_ylabel('Energy') - - ax.plot(speeds, power_total[0], label="X+Y+Z", alpha=0.6) - ax.plot(speeds, power_total[1], label="X", alpha=0.6) - ax.plot(speeds, power_total[2], label="Y", alpha=0.6) - ax.plot(speeds, power_total[3], label="Z", alpha=0.6) - - ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) - ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) - ax.grid(which='major', color='grey') - ax.grid(which='minor', color='lightgrey') - fontP = matplotlib.font_manager.FontProperties() - fontP.set_size('medium') - ax.legend(loc='best', prop=fontP) - - return - - -def plot_spectrogram(ax, speeds, freqs, power_spectral_densities, max_freq): - spectrum = np.empty([len(freqs), len(speeds)]) - - for i in range(len(speeds)): - for j in range(len(freqs)): - spectrum[j, i] = power_spectral_densities[i][0][j] - - ax.set_title("Summed vibrations spectrogram") - ax.pcolormesh(speeds, freqs, spectrum, norm=matplotlib.colors.LogNorm(), - cmap='inferno', shading='gouraud') - ax.set_ylim([0., max_freq]) - ax.set_ylabel('Frequency (hz)') - ax.set_xlabel('Speed (mm/s)') - - return - - -###################################################################### -# Startup and main routines -###################################################################### - -def parse_log(logname, opts): - with open(logname) as f: - for header in f: - if not header.startswith('#'): - break - if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'): - # Raw accelerometer data - return np.loadtxt(logname, comments='#', delimiter=',') - # Power spectral density data or shaper calibration data - opts.error("File %s does not contain raw accelerometer data and therefore " - "is not supported by graph_vibrations.py script. Please use " - "calibrate_shaper.py script to process it instead." % (logname,)) - - -def extract_speed(logname, opts): - try: - speed = re.search('sp(.+?)n', os.path.basename(logname)).group(1) - except AttributeError: - opts.error("File %s does not contain speed in its name and therefore " - "is not supported by graph_vibrations.py script." % (logname,)) - return int(speed) - - -def sort_and_slice(raw_speeds, raw_datas, remove): - # Sort to get the speeds and their datas aligned and in ascending order - raw_speeds, raw_datas = zip(*sorted(zip(raw_speeds, raw_datas), key=operator.itemgetter(0))) - - # Remove beginning and end of the datas for each file to get only - # constant speed data and remove the start/stop phase of the movements - datas = [] - for data in raw_datas: - sliced = round((len(data) * remove / 100) / 2) - datas.append(data[sliced:len(data)-sliced]) - - return raw_speeds, datas - - -def setup_klipper_import(kdir): - global shaper_calibrate - sys.path.append(os.path.join(kdir, 'klippy')) - shaper_calibrate = importlib.import_module('.shaper_calibrate', 'extras') - - -def main(): - # Parse command-line arguments - usage = "%prog [options] " - opts = optparse.OptionParser(usage) - opts.add_option("-o", "--output", type="string", dest="output", - default=None, help="filename of output graph") - opts.add_option("-a", "--axis", type="string", dest="axisname", - default=None, help="axis name to be shown on the side of the graph") - opts.add_option("-f", "--max_freq", type="float", default=1000., - help="maximum frequency to graph") - opts.add_option("-r", "--remove", type="int", default=0, - help="percentage of data removed at start/end of each files") - opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir", - default="/home/pi/klipper", help="main klipper directory") - options, args = opts.parse_args() - if len(args) < 1: - opts.error("No CSV file(s) to analyse") - if options.output is None: - opts.error("You must specify an output file.png to use the script (option -o)") - if options.remove > 50 or options.remove < 0: - opts.error("You must specify a correct percentage (option -r) in the 0-50 range") - - setup_klipper_import(options.klipperdir) - - # Parse the raw data and get them ready for analysis - raw_datas = [parse_log(filename, opts) for filename in args] - raw_speeds = [extract_speed(filename, opts) for filename in args] - speeds, datas = sort_and_slice(raw_speeds, raw_datas, options.remove) - - # As we assume that we have the same number of file for each speeds. We can group - # the PSD results by this number (to combine vibrations at given speed on all movements) - group_by = speeds.count(speeds[0]) - # Compute psd and total power of the signal - freqs, power_spectral_densities = calc_psd(datas, group_by, options.max_freq) - power_total = calc_powertot(power_spectral_densities, freqs) - - fig, axs = matplotlib.pyplot.subplots(2, 1, sharex=True) - fig.suptitle("Machine vibrations - " + options.axisname + " moves", fontsize=16) - - # Remove speeds duplicates and graph the processed datas - speeds = list(OrderedDict((x, True) for x in speeds).keys()) - plot_total_power(axs[0], speeds, power_total) - plot_spectrogram(axs[1], speeds, freqs, power_spectral_densities, options.max_freq) - - fig.set_size_inches(10, 10) - fig.tight_layout() - fig.subplots_adjust(top=0.92) - - fig.savefig(options.output) - -if __name__ == '__main__': - main() diff --git a/scripts/is_workflow/graph_belts.py b/scripts/is_workflow/graph_belts.py new file mode 100755 index 000000000..4c31f8fd2 --- /dev/null +++ b/scripts/is_workflow/graph_belts.py @@ -0,0 +1,638 @@ +#!/usr/bin/env python3 + +################################################# +######## CoreXY BELTS CALIBRATION SCRIPT ######## +################################################# +# Written by Frix_x#0161 # +# @version: 2.1 + +# CHANGELOG: +# v2.1: replaced the TwoSlopNorm by a custom made norm to allow the script to work on older versions of matplotlib +# v2.0: updated the script to align it to the new K-Shake&Tune module +# v1.0: first version of this tool for enhanced vizualisation of belt graphs + + +# Be sure to make this script executable using SSH: type 'chmod +x ./graph_belts.py' when in the folder! + +##################################################################### +################ !!! DO NOT EDIT BELOW THIS LINE !!! ################ +##################################################################### + +import optparse, matplotlib, sys, importlib, os +from textwrap import wrap +from collections import namedtuple +import numpy as np +import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager +import matplotlib.ticker, matplotlib.gridspec, matplotlib.colors +import matplotlib.patches +import locale +from datetime import datetime + +matplotlib.use('Agg') + + +ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # For paired peaks names + +PEAKS_DETECTION_THRESHOLD = 0.20 +CURVE_SIMILARITY_SIGMOID_K = 0.6 +DC_GRAIN_OF_SALT_FACTOR = 0.75 +DC_THRESHOLD_METRIC = 1.5e9 +DC_MAX_UNPAIRED_PEAKS_ALLOWED = 4 + +# Define the SignalData namedtuple +SignalData = namedtuple('CalibrationData', ['freqs', 'psd', 'peaks', 'paired_peaks', 'unpaired_peaks']) + +KLIPPAIN_COLORS = { + "purple": "#70088C", + "orange": "#FF8D32", + "dark_purple": "#150140", + "dark_orange": "#F24130", + "red_pink": "#F2055C" +} + + +# Set the best locale for time and date formating (generation of the titles) +try: + locale.setlocale(locale.LC_TIME, locale.getdefaultlocale()) +except locale.Error: + locale.setlocale(locale.LC_TIME, 'C') + +# Override the built-in print function to avoid problem in Klipper due to locale settings +original_print = print +def print_with_c_locale(*args, **kwargs): + original_locale = locale.setlocale(locale.LC_ALL, None) + locale.setlocale(locale.LC_ALL, 'C') + original_print(*args, **kwargs) + locale.setlocale(locale.LC_ALL, original_locale) +print = print_with_c_locale + + +###################################################################### +# Computation of the PSD graph +###################################################################### + +# Calculate estimated "power spectral density" using existing Klipper tools +def calc_freq_response(data): + helper = shaper_calibrate.ShaperCalibrate(printer=None) + return helper.process_accelerometer_data(data) + + +# Calculate or estimate a "similarity" factor between two PSD curves and scale it to a percentage. This is +# used here to quantify how close the two belts path behavior and responses are close together. +def compute_curve_similarity_factor(signal1, signal2): + freqs1 = signal1.freqs + psd1 = signal1.psd + freqs2 = signal2.freqs + psd2 = signal2.psd + + # Interpolate PSDs to match the same frequency bins and do a cross-correlation + psd2_interp = np.interp(freqs1, freqs2, psd2) + cross_corr = np.correlate(psd1, psd2_interp, mode='full') + + # Find the peak of the cross-correlation and compute a similarity normalized by the energy of the signals + peak_value = np.max(cross_corr) + similarity = peak_value / (np.sqrt(np.sum(psd1**2) * np.sum(psd2_interp**2))) + + # Apply sigmoid scaling to get better numbers and get a final percentage value + scaled_similarity = sigmoid_scale(-np.log(1 - similarity), CURVE_SIMILARITY_SIGMOID_K) + + return scaled_similarity + + +# This find all the peaks in a curve by looking at when the derivative term goes from positive to negative +# Then only the peaks found above a threshold are kept to avoid capturing peaks in the low amplitude noise of a signal +def detect_peaks(psd, freqs, window_size=5, vicinity=3): + # Smooth the curve using a moving average to avoid catching peaks everywhere in noisy signals + kernel = np.ones(window_size) / window_size + smoothed_psd = np.convolve(psd, kernel, mode='valid') + mean_pad = [np.mean(psd[:window_size])] * (window_size // 2) + smoothed_psd = np.concatenate((mean_pad, smoothed_psd)) + + # Find peaks on the smoothed curve + smoothed_peaks = np.where((smoothed_psd[:-2] < smoothed_psd[1:-1]) & (smoothed_psd[1:-1] > smoothed_psd[2:]))[0] + 1 + detection_threshold = PEAKS_DETECTION_THRESHOLD * psd.max() + smoothed_peaks = smoothed_peaks[smoothed_psd[smoothed_peaks] > detection_threshold] + + # Refine peak positions on the original curve + refined_peaks = [] + for peak in smoothed_peaks: + local_max = peak + np.argmax(psd[max(0, peak-vicinity):min(len(psd), peak+vicinity+1)]) - vicinity + refined_peaks.append(local_max) + + return np.array(refined_peaks), freqs[refined_peaks] + + +# This function create pairs of peaks that are close in frequency on two curves (that are known +# to be resonances points and must be similar on both belts on a CoreXY kinematic) +def pair_peaks(peaks1, freqs1, psd1, peaks2, freqs2, psd2): + # Compute a dynamic detection threshold to filter and pair peaks efficiently + # even if the signal is very noisy (this get clipped to a maximum of 10Hz diff) + distances = [] + for p1 in peaks1: + for p2 in peaks2: + distances.append(abs(freqs1[p1] - freqs2[p2])) + distances = np.array(distances) + + median_distance = np.median(distances) + iqr = np.percentile(distances, 75) - np.percentile(distances, 25) + + threshold = median_distance + 1.5 * iqr + threshold = min(threshold, 10) + + # Pair the peaks using the dynamic thresold + paired_peaks = [] + unpaired_peaks1 = list(peaks1) + unpaired_peaks2 = list(peaks2) + + while unpaired_peaks1 and unpaired_peaks2: + min_distance = threshold + 1 + pair = None + + for p1 in unpaired_peaks1: + for p2 in unpaired_peaks2: + distance = abs(freqs1[p1] - freqs2[p2]) + if distance < min_distance: + min_distance = distance + pair = (p1, p2) + + if pair is None: # No more pairs below the threshold + break + + p1, p2 = pair + paired_peaks.append(((p1, freqs1[p1], psd1[p1]), (p2, freqs2[p2], psd2[p2]))) + unpaired_peaks1.remove(p1) + unpaired_peaks2.remove(p2) + + return paired_peaks, unpaired_peaks1, unpaired_peaks2 + + +###################################################################### +# Computation of a basic signal spectrogram +###################################################################### + +def compute_spectrogram(data): + N = data.shape[0] + Fs = N / (data[-1,0] - data[0,0]) + # Round up to a power of 2 for faster FFT + M = 1 << int(.5 * Fs - 1).bit_length() + window = np.kaiser(M, 6.) + def _specgram(x): + return matplotlib.mlab.specgram( + x, Fs=Fs, NFFT=M, noverlap=M//2, window=window, + mode='psd', detrend='mean', scale_by_freq=False) + + d = {'x': data[:,1], 'y': data[:,2], 'z': data[:,3]} + pdata, bins, t = _specgram(d['x']) + for ax in 'yz': + pdata += _specgram(d[ax])[0] + return pdata, bins, t + + +###################################################################### +# Computation of the differential spectrogram +###################################################################### + +# Performs a standard bilinear interpolation for a given x, y point based on surrounding input grid values. This function +# is part of the logic to re-align both belts spectrogram in order to combine them in the differential spectrogram. +def bilinear_interpolate(x, y, points, values): + x1, x2 = points[0] + y1, y2 = points[1] + + f11, f12 = values[0] + f21, f22 = values[1] + + interpolated_value = ( + (f11 * (x2 - x) * (y2 - y) + + f21 * (x - x1) * (y2 - y) + + f12 * (x2 - x) * (y - y1) + + f22 * (x - x1) * (y - y1)) / ((x2 - x1) * (y2 - y1)) + ) + + return interpolated_value + + +# Interpolate source_data (2D) to match target_x and target_y in order to interpolate and +# get similar time and frequency dimensions for the differential spectrogram +def interpolate_2d(target_x, target_y, source_x, source_y, source_data): + interpolated_data = np.zeros((len(target_y), len(target_x))) + + for i, y in enumerate(target_y): + for j, x in enumerate(target_x): + # Find indices of surrounding points in source data + # and ensure we don't exceed array bounds + x_indices = np.searchsorted(source_x, x) - 1 + y_indices = np.searchsorted(source_y, y) - 1 + x_indices = max(0, min(len(source_x) - 1, x_indices)) + y_indices = max(0, min(len(source_y) - 1, y_indices)) + + if x_indices == len(source_x) - 2: + x_indices -= 1 + if y_indices == len(source_y) - 2: + y_indices -= 1 + + x1, x2 = source_x[x_indices], source_x[x_indices + 1] + y1, y2 = source_y[y_indices], source_y[y_indices + 1] + + f11 = source_data[y_indices, x_indices] + f12 = source_data[y_indices, x_indices + 1] + f21 = source_data[y_indices + 1, x_indices] + f22 = source_data[y_indices + 1, x_indices + 1] + + interpolated_data[i, j] = bilinear_interpolate(x, y, ((x1, x2), (y1, y2)), ((f11, f12), (f21, f22))) + + return interpolated_data + + +# This function identifies a "ridge" of high gradient magnitude in a spectrogram (pdata) - ie. a resonance diagonal line. Starting from +# the maximum value in the first column, it iteratively follows the direction of the highest gradient in the vicinity (window configured using +# the n_average parameter). The result is a sequence of indices that traces the resonance line across the original spectrogram. +def detect_ridge(pdata, n_average=3): + grad_y, grad_x = np.gradient(pdata) + magnitude = np.sqrt(grad_x**2 + grad_y**2) + + # Start at the maximum value in the first column + start_idx = np.argmax(pdata[:, 0]) + path = [start_idx] + + # Walk through the spectrogram following the path of the ridge + for j in range(1, pdata.shape[1]): + # Look in the vicinity of the previous point + vicinity = magnitude[max(0, path[-1]-n_average):min(pdata.shape[0], path[-1]+n_average+1), j] + # Take an average of top few points + sorted_indices = np.argsort(vicinity) + top_indices = sorted_indices[-n_average:] + next_idx = int(np.mean(top_indices) + max(0, path[-1]-n_average)) + path.append(next_idx) + + return np.array(path) + + +# This function calculates the time offset between two resonances lines (ridge1 and ridge2) using cross-correlation in +# the frequency domain (using FFT). The result provides the lag (or offset) at which the two sequences are most similar. +# This is used to re-align both belts spectrograms on their resonances lines in order to create the combined spectrogram. +def compute_cross_correlation_offset(ridge1, ridge2): + # Ensure that the two arrays have the same shape + if len(ridge1) < len(ridge2): + ridge1 = np.pad(ridge1, (0, len(ridge2) - len(ridge1))) + elif len(ridge1) > len(ridge2): + ridge2 = np.pad(ridge2, (0, len(ridge1) - len(ridge2))) + + cross_corr = np.fft.fftshift(np.fft.ifft(np.fft.fft(ridge1) * np.conj(np.fft.fft(ridge2)))) + return np.argmax(np.abs(cross_corr)) - len(ridge1) // 2 + + +# This function shifts data along its second dimension - ie. time here - by a specified shift_amount +def shift_data_in_time(data, shift_amount): + if shift_amount > 0: + return np.pad(data, ((0, 0), (shift_amount, 0)), mode='constant')[:, :-shift_amount] + elif shift_amount < 0: + return np.pad(data, ((0, 0), (0, -shift_amount)), mode='constant')[:, -shift_amount:] + else: + return data + + +# Main logic function to combine two similar spectrogram - ie. from both belts paths - by detecting similarities (ridges), computing +# the time lag and realigning them. Finally this function combine (by substracting signals) the aligned spectrograms in a new one. +# This result of a mostly zero-ed new spectrogram with some colored zones highlighting differences in the belts paths. +def combined_spectrogram(data1, data2): + pdata1, bins1, t1 = compute_spectrogram(data1) + pdata2, _, _ = compute_spectrogram(data2) + + # Detect ridges + ridge1 = detect_ridge(pdata1) + ridge2 = detect_ridge(pdata2) + + # Compute offset using cross-correlation and shit/align and interpolate the spectrograms + offset = compute_cross_correlation_offset(ridge1, ridge2) + pdata2_aligned = shift_data_in_time(pdata2, offset) + pdata2_interpolated = interpolate_2d(t1, bins1, t1, bins1, pdata2_aligned) + + # Combine the spectrograms + combined_data = np.abs(pdata1 - pdata2_interpolated) + return combined_data, bins1, t1 + + +# Compute a composite and highly subjective value indicating the "mechanical health of the printer (0 to 100%)" that represent the +# likelihood of mechanical issues on the printer. It is based on the differential spectrogram sum of gradient, salted with a bit +# of the estimated similarity cross-correlation from compute_curve_similarity_factor() and with a bit of the number of unpaired peaks. +# This result in a percentage value quantifying the machine behavior around the main resonances that give an hint if only touching belt tension +# will give good graphs or if there is a chance of mechanical issues in the background (above 50% should be considered as probably problematic) +def compute_mhi(combined_data, similarity_coefficient, num_unpaired_peaks): + filtered_data = combined_data[combined_data > 100] + + # First compute a "total variability metric" based on the sum of the gradient that sum the magnitude of will emphasize regions of the + # spectrogram where there are rapid changes in magnitude (like the edges of resonance peaks). + total_variability_metric = np.sum(np.abs(np.gradient(filtered_data))) + # Scale the metric to a percentage using the threshold (found empirically on a large number of user data shared to me) + base_percentage = (np.log1p(total_variability_metric) / np.log1p(DC_THRESHOLD_METRIC)) * 100 + + # Adjust the percentage based on the similarity_coefficient to add a grain of salt + adjusted_percentage = base_percentage * (1 - DC_GRAIN_OF_SALT_FACTOR * (similarity_coefficient / 100)) + + # Adjust the percentage again based on the number of unpaired peaks to add a second grain of salt + peak_confidence = num_unpaired_peaks / DC_MAX_UNPAIRED_PEAKS_ALLOWED + final_percentage = (1 - peak_confidence) * adjusted_percentage + peak_confidence * 100 + + # Ensure the result lies between 0 and 100 by clipping the computed value + final_percentage = np.clip(final_percentage, 0, 100) + + return final_percentage, mhi_lut(final_percentage) + + +# LUT to transform the MHI into a textual value easy to understand for the users of the script +def mhi_lut(mhi): + if 0 <= mhi <= 30: + return "Excellent mechanical health" + elif 30 < mhi <= 45: + return "Good mechanical health" + elif 45 < mhi <= 55: + return "Acceptable mechanical health" + elif 55 < mhi <= 70: + return "Potential signs of a mechanical issue" + elif 70 < mhi <= 85: + return "Likely a mechanical issue" + elif 85 < mhi <= 100: + return "Mechanical issue detected" + + +###################################################################### +# Graphing +###################################################################### + +def plot_compare_frequency(ax, lognames, signal1, signal2, max_freq): + # Get the belt name for the legend to avoid putting the full file name + signal1_belt = (lognames[0].split('/')[-1]).split('_')[-1][0] + signal2_belt = (lognames[1].split('/')[-1]).split('_')[-1][0] + + if signal1_belt == 'A' and signal2_belt == 'B': + signal1_belt += " (axis 1,-1)" + signal2_belt += " (axis 1, 1)" + elif signal1_belt == 'B' and signal2_belt == 'A': + signal1_belt += " (axis 1, 1)" + signal2_belt += " (axis 1,-1)" + else: + print("Warning: belts doesn't seem to have the correct name A and B (extracted from the filename.csv)") + + # Plot the two belts PSD signals + ax.plot(signal1.freqs, signal1.psd, label="Belt " + signal1_belt, color=KLIPPAIN_COLORS['purple']) + ax.plot(signal2.freqs, signal2.psd, label="Belt " + signal2_belt, color=KLIPPAIN_COLORS['orange']) + + # Trace the "relax region" (also used as a threshold to filter and detect the peaks) + psd_lowest_max = min(signal1.psd.max(), signal2.psd.max()) + peaks_warning_threshold = PEAKS_DETECTION_THRESHOLD * psd_lowest_max + ax.axhline(y=peaks_warning_threshold, color='black', linestyle='--', linewidth=0.5) + ax.fill_between(signal1.freqs, 0, peaks_warning_threshold, color='green', alpha=0.15, label='Relax Region') + + # Trace and annotate the peaks on the graph + paired_peak_count = 0 + unpaired_peak_count = 0 + offsets_table_data = [] + + for _, (peak1, peak2) in enumerate(signal1.paired_peaks): + label = ALPHABET[paired_peak_count] + amplitude_offset = abs(((signal2.psd[peak2[0]] - signal1.psd[peak1[0]]) / max(signal1.psd[peak1[0]], signal2.psd[peak2[0]])) * 100) + frequency_offset = abs(signal2.freqs[peak2[0]] - signal1.freqs[peak1[0]]) + offsets_table_data.append([f"Peaks {label}", f"{frequency_offset:.1f} Hz", f"{amplitude_offset:.1f} %"]) + + ax.plot(signal1.freqs[peak1[0]], signal1.psd[peak1[0]], "x", color='black') + ax.plot(signal2.freqs[peak2[0]], signal2.psd[peak2[0]], "x", color='black') + ax.plot([signal1.freqs[peak1[0]], signal2.freqs[peak2[0]]], [signal1.psd[peak1[0]], signal2.psd[peak2[0]]], ":", color='gray') + + ax.annotate(label + "1", (signal1.freqs[peak1[0]], signal1.psd[peak1[0]]), + textcoords="offset points", xytext=(8, 5), + ha='left', fontsize=13, color='black') + ax.annotate(label + "2", (signal2.freqs[peak2[0]], signal2.psd[peak2[0]]), + textcoords="offset points", xytext=(8, 5), + ha='left', fontsize=13, color='black') + paired_peak_count += 1 + + for peak in signal1.unpaired_peaks: + ax.plot(signal1.freqs[peak], signal1.psd[peak], "x", color='black') + ax.annotate(str(unpaired_peak_count + 1), (signal1.freqs[peak], signal1.psd[peak]), + textcoords="offset points", xytext=(8, 5), + ha='left', fontsize=13, color='red', weight='bold') + unpaired_peak_count += 1 + + for peak in signal2.unpaired_peaks: + ax.plot(signal2.freqs[peak], signal2.psd[peak], "x", color='black') + ax.annotate(str(unpaired_peak_count + 1), (signal2.freqs[peak], signal2.psd[peak]), + textcoords="offset points", xytext=(8, 5), + ha='left', fontsize=13, color='red', weight='bold') + unpaired_peak_count += 1 + + # Compute the similarity (using cross-correlation of the PSD signals) + ax2 = ax.twinx() # To split the legends in two box + ax2.yaxis.set_visible(False) + similarity_factor = compute_curve_similarity_factor(signal1, signal2) + ax2.plot([], [], ' ', label=f'Estimated similarity: {similarity_factor:.1f}%') + ax2.plot([], [], ' ', label=f'Number of unpaired peaks: {unpaired_peak_count}') + print(f"Belts estimated similarity: {similarity_factor:.1f}%") + + # Setting axis parameters, grid and graph title + ax.set_xlabel('Frequency (Hz)') + ax.set_xlim([0, max_freq]) + ax.set_ylabel('Power spectral density') + psd_highest_max = max(signal1.psd.max(), signal2.psd.max()) + ax.set_ylim([0, psd_highest_max + psd_highest_max * 0.05]) + + ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) + ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) + ax.ticklabel_format(axis='y', style='scientific', scilimits=(0,0)) + ax.grid(which='major', color='grey') + ax.grid(which='minor', color='lightgrey') + fontP = matplotlib.font_manager.FontProperties() + fontP.set_size('small') + ax.set_title('Belts Frequency Profiles (estimated similarity: {:.1f}%)'.format(similarity_factor), fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') + + # Print the table of offsets ontop of the graph below the original legend (upper right) + if len(offsets_table_data) > 0: + columns = ["", "Frequency delta", "Amplitude delta", ] + offset_table = ax.table(cellText=offsets_table_data, colLabels=columns, bbox=[0.66, 0.75, 0.33, 0.15], loc='upper right', cellLoc='center') + offset_table.auto_set_font_size(False) + offset_table.set_fontsize(8) + offset_table.auto_set_column_width([0, 1, 2]) + offset_table.set_zorder(100) + cells = [key for key in offset_table.get_celld().keys()] + for cell in cells: + offset_table[cell].set_facecolor('white') + offset_table[cell].set_alpha(0.6) + + ax.legend(loc='upper left', prop=fontP) + ax2.legend(loc='upper right', prop=fontP) + + return similarity_factor, unpaired_peak_count + + +def plot_difference_spectrogram(ax, data1, data2, signal1, signal2, similarity_factor, max_freq): + combined_data, bins, t = combined_spectrogram(data1, data2) + + # Compute the MHI value from the differential spectrogram sum of gradient, salted with + # the similarity factor and the number or unpaired peaks from the belts frequency profile + # Be careful, this value is highly opinionated and is pretty experimental! + mhi, textual_mhi = compute_mhi(combined_data, similarity_factor, len(signal1.unpaired_peaks) + len(signal2.unpaired_peaks)) + print(f"[experimental] Mechanical Health Indicator: {textual_mhi.lower()} ({mhi:.1f}%)") + ax.set_title(f"Differential Spectrogram", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') + ax.plot([], [], ' ', label=f'{textual_mhi} (experimental)') + + # Draw the differential spectrogram with a specific custom norm to get white or light orange zero values and red for max values + colors = ['white', 'bisque', 'red', 'black'] + n_bins = [0, 0.12, 0.9, 1] # These values where found experimentaly to get a good higlhlighting of the differences only + cm = matplotlib.colors.LinearSegmentedColormap.from_list('WhiteToRed', list(zip(n_bins, colors))) + norm = matplotlib.colors.Normalize(vmin=np.min(combined_data), vmax=np.max(combined_data)) + ax.pcolormesh(bins, t, combined_data.T, cmap=cm, norm=norm, shading='gouraud') + + ax.set_xlabel('Frequency (hz)') + ax.set_xlim([0., max_freq]) + ax.set_ylabel('Time (s)') + ax.set_ylim([0, t[-1]]) + + fontP = matplotlib.font_manager.FontProperties() + fontP.set_size('medium') + ax.legend(loc='best', prop=fontP) + + # Plot vertical lines for unpaired peaks + unpaired_peak_count = 0 + for _, peak in enumerate(signal1.unpaired_peaks): + ax.axvline(signal1.freqs[peak], color='red', linestyle='dotted', linewidth=1.5) + ax.annotate(f"Peak {unpaired_peak_count + 1}", (signal1.freqs[peak], t[-1]*0.05), + textcoords="data", color='red', rotation=90, fontsize=10, + verticalalignment='bottom', horizontalalignment='right') + unpaired_peak_count +=1 + + for _, peak in enumerate(signal2.unpaired_peaks): + ax.axvline(signal2.freqs[peak], color='red', linestyle='dotted', linewidth=1.5) + ax.annotate(f"Peak {unpaired_peak_count + 1}", (signal2.freqs[peak], t[-1]*0.05), + textcoords="data", color='red', rotation=90, fontsize=10, + verticalalignment='bottom', horizontalalignment='right') + unpaired_peak_count +=1 + + # Plot vertical lines and zones for paired peaks + for idx, (peak1, peak2) in enumerate(signal1.paired_peaks): + label = ALPHABET[idx] + x_min = min(peak1[1], peak2[1]) + x_max = max(peak1[1], peak2[1]) + ax.axvline(x_min, color=KLIPPAIN_COLORS['purple'], linestyle='dotted', linewidth=1.5) + ax.axvline(x_max, color=KLIPPAIN_COLORS['purple'], linestyle='dotted', linewidth=1.5) + ax.fill_between([x_min, x_max], 0, np.max(combined_data), color=KLIPPAIN_COLORS['purple'], alpha=0.3) + ax.annotate(f"Peaks {label}", (x_min, t[-1]*0.05), + textcoords="data", color=KLIPPAIN_COLORS['purple'], rotation=90, fontsize=10, + verticalalignment='bottom', horizontalalignment='right') + + return + + +###################################################################### +# Custom tools +###################################################################### + +# Simple helper to compute a sigmoid scalling (from 0 to 100%) +def sigmoid_scale(x, k=1): + return 1 / (1 + np.exp(-k * x)) * 100 + +# Original Klipper function to get the PSD data of a raw accelerometer signal +def compute_signal_data(data, max_freq): + calibration_data = calc_freq_response(data) + freqs = calibration_data.freq_bins[calibration_data.freq_bins <= max_freq] + psd = calibration_data.get_psd('all')[calibration_data.freq_bins <= max_freq] + peaks, _ = detect_peaks(psd, freqs) + return SignalData(freqs=freqs, psd=psd, peaks=peaks, paired_peaks=None, unpaired_peaks=None) + + +###################################################################### +# Startup and main routines +###################################################################### + +def parse_log(logname): + with open(logname) as f: + for header in f: + if not header.startswith('#'): + break + if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'): + # Raw accelerometer data + return np.loadtxt(logname, comments='#', delimiter=',') + # Power spectral density data or shaper calibration data + raise ValueError("File %s does not contain raw accelerometer data and therefore " + "is not supported by this script. Please use the official Klipper " + "graph_accelerometer.py script to process it instead." % (logname,)) + + +def setup_klipper_import(kdir): + global shaper_calibrate + kdir = os.path.expanduser(kdir) + sys.path.append(os.path.join(kdir, 'klippy')) + shaper_calibrate = importlib.import_module('.shaper_calibrate', 'extras') + + +def belts_calibration(lognames, klipperdir="~/klipper", max_freq=200.): + setup_klipper_import(klipperdir) + + # Parse data + datas = [parse_log(fn) for fn in lognames] + if len(datas) > 2: + raise ValueError("Incorrect number of .csv files used (this function needs two files to compare them)") + + # Compute calibration data for the two datasets with automatic peaks detection + signal1 = compute_signal_data(datas[0], max_freq) + signal2 = compute_signal_data(datas[1], max_freq) + + # Pair the peaks across the two datasets + paired_peaks, unpaired_peaks1, unpaired_peaks2 = pair_peaks(signal1.peaks, signal1.freqs, signal1.psd, + signal2.peaks, signal2.freqs, signal2.psd) + signal1 = signal1._replace(paired_peaks=paired_peaks, unpaired_peaks=unpaired_peaks1) + signal2 = signal2._replace(paired_peaks=paired_peaks, unpaired_peaks=unpaired_peaks2) + + fig = matplotlib.pyplot.figure() + gs = matplotlib.gridspec.GridSpec(2, 1, height_ratios=[4, 3]) + ax1 = fig.add_subplot(gs[0]) + ax2 = fig.add_subplot(gs[1]) + + # Add title + title_line1 = "RELATIVE BELT CALIBRATION TOOL" + fig.text(0.12, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold') + try: + filename = lognames[0].split('/')[-1] + dt = datetime.strptime(f"{filename.split('_')[1]} {filename.split('_')[2]}", "%Y%m%d %H%M%S") + title_line2 = dt.strftime('%x %X') + except: + print("Warning: CSV filenames look to be different than expected (%s , %s)" % (lognames[0], lognames[1])) + title_line2 = lognames[0].split('/')[-1] + " / " + lognames[1].split('/')[-1] + fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple']) + + # Plot the graphs + similarity_factor, _ = plot_compare_frequency(ax1, lognames, signal1, signal2, max_freq) + plot_difference_spectrogram(ax2, datas[0], datas[1], signal1, signal2, similarity_factor, max_freq) + + fig.set_size_inches(8.3, 11.6) + fig.tight_layout() + fig.subplots_adjust(top=0.89) + + # Adding a small Klippain logo to the top left corner of the figure + ax_logo = fig.add_axes([0.001, 0.899, 0.1, 0.1], anchor='NW', zorder=-1) + ax_logo.imshow(matplotlib.pyplot.imread(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'klippain.png'))) + ax_logo.axis('off') + + return fig + + +def main(): + # Parse command-line arguments + usage = "%prog [options] " + opts = optparse.OptionParser(usage) + opts.add_option("-o", "--output", type="string", dest="output", + default=None, help="filename of output graph") + opts.add_option("-f", "--max_freq", type="float", default=200., + help="maximum frequency to graph") + opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir", + default="~/klipper", help="main klipper directory") + options, args = opts.parse_args() + if len(args) < 1: + opts.error("Incorrect number of arguments") + if options.output is None: + opts.error("You must specify an output file.png to use the script (option -o)") + + fig = belts_calibration(args, options.klipperdir, options.max_freq) + fig.savefig(options.output) + + +if __name__ == '__main__': + main() diff --git a/scripts/is_workflow/graph_shaper.py b/scripts/is_workflow/graph_shaper.py new file mode 100755 index 000000000..fdada5fa3 --- /dev/null +++ b/scripts/is_workflow/graph_shaper.py @@ -0,0 +1,395 @@ +#!/usr/bin/env python3 + +################################################# +######## INPUT SHAPER CALIBRATION SCRIPT ######## +################################################# +# Derived from the calibrate_shaper.py official Klipper script +# Copyright (C) 2020 Dmitry Butyugin +# Copyright (C) 2020 Kevin O'Connor +# +# Written by Frix_x#0161 # +# @version: 2.0 + +# CHANGELOG: +# v2.0: updated the script to align it to the new K-Shake&Tune module +# v1.1: - improved the damping ratio computation with linear approximation for more precision +# - reworked the top graph to add more information to it with colored zones, +# automated peak detection, etc... +# - added a full spectrogram of the signal on the bottom to allow deeper analysis +# v1.0: first version of this script inspired from the official Klipper +# shaper calibration script to add an automatic damping ratio estimation to it + + +# Be sure to make this script executable using SSH: type 'chmod +x ./graph_shaper.py' when in the folder! + +##################################################################### +################ !!! DO NOT EDIT BELOW THIS LINE !!! ################ +##################################################################### + +import optparse, matplotlib, sys, importlib, os, math +from textwrap import wrap +import numpy as np +import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager +import matplotlib.ticker, matplotlib.gridspec +import locale +from datetime import datetime + +matplotlib.use('Agg') + + +PEAKS_DETECTION_THRESHOLD = 0.05 +PEAKS_EFFECT_THRESHOLD = 0.12 +SPECTROGRAM_LOW_PERCENTILE_FILTER = 5 +MAX_SMOOTHING = 0.1 + +KLIPPAIN_COLORS = { + "purple": "#70088C", + "dark_purple": "#150140", + "dark_orange": "#F24130" +} + + +# Set the best locale for time and date formating (generation of the titles) +try: + locale.setlocale(locale.LC_TIME, locale.getdefaultlocale()) +except locale.Error: + locale.setlocale(locale.LC_TIME, 'C') + +# Override the built-in print function to avoid problem in Klipper due to locale settings +original_print = print +def print_with_c_locale(*args, **kwargs): + original_locale = locale.setlocale(locale.LC_ALL, None) + locale.setlocale(locale.LC_ALL, 'C') + original_print(*args, **kwargs) + locale.setlocale(locale.LC_ALL, original_locale) +print = print_with_c_locale + + +###################################################################### +# Computation +###################################################################### + +# Find the best shaper parameters using Klipper's official algorithm selection +def calibrate_shaper_with_damping(datas, max_smoothing): + helper = shaper_calibrate.ShaperCalibrate(printer=None) + + calibration_data = helper.process_accelerometer_data(datas[0]) + for data in datas[1:]: + calibration_data.add_data(helper.process_accelerometer_data(data)) + + calibration_data.normalize_to_frequencies() + + shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, print) + + freqs = calibration_data.freq_bins + psd = calibration_data.psd_sum + fr, zeta = compute_damping_ratio(psd, freqs) + + print("Recommended shaper is %s @ %.1f Hz" % (shaper.name, shaper.freq)) + print("Axis has a main resonant frequency at %.1fHz with an estimated damping ratio of %.3f" % (fr, zeta)) + + return shaper.name, all_shapers, calibration_data, fr, zeta + + +# Compute damping ratio by using the half power bandwidth method with interpolated frequencies +def compute_damping_ratio(psd, freqs): + max_power_index = np.argmax(psd) + fr = freqs[max_power_index] + max_power = psd[max_power_index] + + half_power = max_power / math.sqrt(2) + idx_below = np.where(psd[:max_power_index] <= half_power)[0][-1] + idx_above = np.where(psd[max_power_index:] <= half_power)[0][0] + max_power_index + freq_below_half_power = freqs[idx_below] + (half_power - psd[idx_below]) * (freqs[idx_below + 1] - freqs[idx_below]) / (psd[idx_below + 1] - psd[idx_below]) + freq_above_half_power = freqs[idx_above - 1] + (half_power - psd[idx_above - 1]) * (freqs[idx_above] - freqs[idx_above - 1]) / (psd[idx_above] - psd[idx_above - 1]) + + bandwidth = freq_above_half_power - freq_below_half_power + zeta = bandwidth / (2 * fr) + + return fr, zeta + + +def compute_spectrogram(data): + N = data.shape[0] + Fs = N / (data[-1,0] - data[0,0]) + # Round up to a power of 2 for faster FFT + M = 1 << int(.5 * Fs - 1).bit_length() + window = np.kaiser(M, 6.) + def _specgram(x): + return matplotlib.mlab.specgram( + x, Fs=Fs, NFFT=M, noverlap=M//2, window=window, + mode='psd', detrend='mean', scale_by_freq=False) + + d = {'x': data[:,1], 'y': data[:,2], 'z': data[:,3]} + pdata, bins, t = _specgram(d['x']) + for ax in 'yz': + pdata += _specgram(d[ax])[0] + return pdata, bins, t + + +# This find all the peaks in a curve by looking at when the derivative term goes from positive to negative +# Then only the peaks found above a threshold are kept to avoid capturing peaks in the low amplitude noise of a signal +# An added "virtual" threshold allow me to quantify in an opiniated way the peaks that "could have" effect on the printer +# behavior and are likely known to produce or contribute to the ringing/ghosting in printed parts +def detect_peaks(psd, freqs, window_size=5, vicinity=3): + # Smooth the curve using a moving average to avoid catching peaks everywhere in noisy signals + kernel = np.ones(window_size) / window_size + smoothed_psd = np.convolve(psd, kernel, mode='valid') + mean_pad = [np.mean(psd[:window_size])] * (window_size // 2) + smoothed_psd = np.concatenate((mean_pad, smoothed_psd)) + + # Find peaks on the smoothed curve + smoothed_peaks = np.where((smoothed_psd[:-2] < smoothed_psd[1:-1]) & (smoothed_psd[1:-1] > smoothed_psd[2:]))[0] + 1 + detection_threshold = PEAKS_DETECTION_THRESHOLD * psd.max() + effect_threshold = PEAKS_EFFECT_THRESHOLD * psd.max() + smoothed_peaks = smoothed_peaks[smoothed_psd[smoothed_peaks] > detection_threshold] + + # Refine peak positions on the original curve + refined_peaks = [] + for peak in smoothed_peaks: + local_max = peak + np.argmax(psd[max(0, peak-vicinity):min(len(psd), peak+vicinity+1)]) - vicinity + refined_peaks.append(local_max) + + peak_freqs = ["{:.1f}".format(f) for f in freqs[refined_peaks]] + + num_peaks = len(refined_peaks) + num_peaks_above_effect_threshold = np.sum(psd[refined_peaks] > effect_threshold) + + print("Peaks detected on the graph: %d @ %s Hz (%d above effect threshold)" % (num_peaks, ", ".join(map(str, peak_freqs)), num_peaks_above_effect_threshold)) + + return np.array(refined_peaks), num_peaks, num_peaks_above_effect_threshold + + +###################################################################### +# Graphing +###################################################################### + +def plot_freq_response_with_damping(ax, calibration_data, shapers, performance_shaper, fr, zeta, max_freq): + freqs = calibration_data.freq_bins + psd = calibration_data.psd_sum[freqs <= max_freq] + px = calibration_data.psd_x[freqs <= max_freq] + py = calibration_data.psd_y[freqs <= max_freq] + pz = calibration_data.psd_z[freqs <= max_freq] + freqs = freqs[freqs <= max_freq] + + fontP = matplotlib.font_manager.FontProperties() + fontP.set_size('x-small') + + ax.set_xlabel('Frequency (Hz)') + ax.set_xlim([0, max_freq]) + ax.set_ylabel('Power spectral density') + ax.set_ylim([0, psd.max() + psd.max() * 0.05]) + + ax.plot(freqs, psd, label='X+Y+Z', color='purple') + ax.plot(freqs, px, label='X', color='red') + ax.plot(freqs, py, label='Y', color='green') + ax.plot(freqs, pz, label='Z', color='blue') + + ax.xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(5)) + ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) + ax.ticklabel_format(axis='y', style='scientific', scilimits=(0,0)) + ax.grid(which='major', color='grey') + ax.grid(which='minor', color='lightgrey') + + ax2 = ax.twinx() + ax2.yaxis.set_visible(False) + + lowvib_shaper_vibrs = float('inf') + lowvib_shaper = None + lowvib_shaper_freq = None + lowvib_shaper_accel = 0 + + # Draw the shappers curves and add their specific parameters in the legend + # This adds also a way to find the best shaper with a low level of vibrations (with a resonable level of smoothing) + for shaper in shapers: + shaper_max_accel = round(shaper.max_accel / 100.) * 100. + label = "%s (%.1f Hz, vibr=%.1f%%, sm~=%.2f, accel<=%.f)" % ( + shaper.name.upper(), shaper.freq, + shaper.vibrs * 100., shaper.smoothing, + shaper_max_accel) + ax2.plot(freqs, shaper.vals, label=label, linestyle='dotted') + + # Get the performance shaper + if shaper.name == performance_shaper: + performance_shaper_freq = shaper.freq + performance_shaper_vibr = shaper.vibrs * 100. + performance_shaper_vals = shaper.vals + + # Get the low vibration shaper + if (shaper.vibrs * 100 < lowvib_shaper_vibrs or (shaper.vibrs * 100 == lowvib_shaper_vibrs and shaper_max_accel > lowvib_shaper_accel)) and shaper.smoothing < MAX_SMOOTHING: + lowvib_shaper_accel = shaper_max_accel + lowvib_shaper = shaper.name + lowvib_shaper_freq = shaper.freq + lowvib_shaper_vibrs = shaper.vibrs * 100 + lowvib_shaper_vals = shaper.vals + + # User recommendations are added to the legend: one is Klipper's original suggestion that is usually good for performances + # and the other one is the custom "low vibration" recommendation that looks for a suitable shaper that doesn't have excessive + # smoothing and that have a lower vibration level. If both recommendation are the same shaper, or if no suitable "low + # vibration" shaper is found, then only a single line as the "best shaper" recommendation is added to the legend + if lowvib_shaper != None and lowvib_shaper != performance_shaper and lowvib_shaper_vibrs <= performance_shaper_vibr: + ax2.plot([], [], ' ', label="Recommended performance shaper: %s @ %.1f Hz" % (performance_shaper.upper(), performance_shaper_freq)) + ax.plot(freqs, psd * performance_shaper_vals, label='With %s applied' % (performance_shaper.upper()), color='cyan') + ax2.plot([], [], ' ', label="Recommended low vibrations shaper: %s @ %.1f Hz" % (lowvib_shaper.upper(), lowvib_shaper_freq)) + ax.plot(freqs, psd * lowvib_shaper_vals, label='With %s applied' % (lowvib_shaper.upper()), color='lime') + else: + ax2.plot([], [], ' ', label="Recommended best shaper: %s @ %.1f Hz" % (performance_shaper.upper(), performance_shaper_freq)) + ax.plot(freqs, psd * performance_shaper_vals, label='With %s applied' % (performance_shaper.upper()), color='cyan') + + # And the estimated damping ratio is finally added at the end of the legend + ax2.plot([], [], ' ', label="Estimated damping ratio (ζ): %.3f" % (zeta)) + + # Draw the detected peaks and name them + # This also draw the detection threshold and warning threshold (aka "effect zone") + peaks, _, _ = detect_peaks(psd, freqs) + peaks_warning_threshold = PEAKS_DETECTION_THRESHOLD * psd.max() + peaks_effect_threshold = PEAKS_EFFECT_THRESHOLD * psd.max() + + ax.plot(freqs[peaks], psd[peaks], "x", color='black', markersize=8) + for idx, peak in enumerate(peaks): + if psd[peak] > peaks_effect_threshold: + fontcolor = 'red' + fontweight = 'bold' + else: + fontcolor = 'black' + fontweight = 'normal' + ax.annotate(f"{idx+1}", (freqs[peak], psd[peak]), + textcoords="offset points", xytext=(8, 5), + ha='left', fontsize=13, color=fontcolor, weight=fontweight) + ax.axhline(y=peaks_warning_threshold, color='black', linestyle='--', linewidth=0.5) + ax.axhline(y=peaks_effect_threshold, color='black', linestyle='--', linewidth=0.5) + ax.fill_between(freqs, 0, peaks_warning_threshold, color='green', alpha=0.15, label='Relax Region') + ax.fill_between(freqs, peaks_warning_threshold, peaks_effect_threshold, color='orange', alpha=0.2, label='Warning Region') + + + # Add the main resonant frequency and damping ratio of the axis to the graph title + ax.set_title("Axis Frequency Profile (ω0=%.1fHz, ζ=%.3f)" % (fr, zeta), fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') + ax.legend(loc='upper left', prop=fontP) + ax2.legend(loc='upper right', prop=fontP) + + return freqs[peaks] + + +# Plot a time-frequency spectrogram to see how the system respond over time during the +# resonnance test. This can highlight hidden spots from the standard PSD graph from other harmonics +def plot_spectrogram(ax, data, peaks, max_freq): + pdata, bins, t = compute_spectrogram(data) + + # We need to normalize the data to get a proper signal on the spectrogram + # However, while using "LogNorm" provide too much background noise, using + # "Normalize" make only the resonnance appearing and hide interesting elements + # So we need to filter out the lower part of the data (ie. find the proper vmin for LogNorm) + vmin_value = np.percentile(pdata, SPECTROGRAM_LOW_PERCENTILE_FILTER) + + ax.set_title("Time-Frequency Spectrogram", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') + ax.pcolormesh(bins, t, pdata.T, norm=matplotlib.colors.LogNorm(vmin=vmin_value), + cmap='inferno', shading='gouraud') + + # Add peaks lines in the spectrogram to get hint from peaks found in the first graph + if peaks is not None: + for idx, peak in enumerate(peaks): + ax.axvline(peak, color='cyan', linestyle='dotted', linewidth=0.75) + ax.annotate(f"Peak {idx+1}", (peak, t[-1]*0.9), + textcoords="data", color='cyan', rotation=90, fontsize=10, + verticalalignment='top', horizontalalignment='right') + + ax.set_xlim([0., max_freq]) + ax.set_ylabel('Time (s)') + ax.set_xlabel('Frequency (Hz)') + + return + + +###################################################################### +# Startup and main routines +###################################################################### + +def parse_log(logname): + with open(logname) as f: + for header in f: + if not header.startswith('#'): + break + if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'): + # Raw accelerometer data + return np.loadtxt(logname, comments='#', delimiter=',') + # Power spectral density data or shaper calibration data + raise ValueError("File %s does not contain raw accelerometer data and therefore " + "is not supported by this script. Please use the official Klipper " + "calibrate_shaper.py script to process it instead." % (logname,)) + + +def setup_klipper_import(kdir): + global shaper_calibrate + kdir = os.path.expanduser(kdir) + sys.path.append(os.path.join(kdir, 'klippy')) + shaper_calibrate = importlib.import_module('.shaper_calibrate', 'extras') + + +def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, max_freq=200.): + setup_klipper_import(klipperdir) + + # Parse data + datas = [parse_log(fn) for fn in lognames] + + # Calibrate shaper and generate outputs + performance_shaper, shapers, calibration_data, fr, zeta = calibrate_shaper_with_damping(datas, max_smoothing) + + fig = matplotlib.pyplot.figure() + gs = matplotlib.gridspec.GridSpec(2, 1, height_ratios=[4, 3]) + ax1 = fig.add_subplot(gs[0]) + ax2 = fig.add_subplot(gs[1]) + + # Add title + title_line1 = "INPUT SHAPER CALIBRATION TOOL" + fig.text(0.12, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold') + try: + filename_parts = (lognames[0].split('/')[-1]).split('_') + dt = datetime.strptime(f"{filename_parts[1]} {filename_parts[2]}", "%Y%m%d %H%M%S") + title_line2 = dt.strftime('%x %X') + ' -- ' + filename_parts[3].upper().split('.')[0] + ' axis' + except: + print("Warning: CSV filename look to be different than expected (%s)" % (lognames[0])) + title_line2 = lognames[0].split('/')[-1] + fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple']) + + # Plot the graphs + peaks = plot_freq_response_with_damping(ax1, calibration_data, shapers, performance_shaper, fr, zeta, max_freq) + plot_spectrogram(ax2, datas[0], peaks, max_freq) + + fig.set_size_inches(8.3, 11.6) + fig.tight_layout() + fig.subplots_adjust(top=0.89) + + # Adding a small Klippain logo to the top left corner of the figure + ax_logo = fig.add_axes([0.001, 0.899, 0.1, 0.1], anchor='NW', zorder=-1) + ax_logo.imshow(matplotlib.pyplot.imread(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'klippain.png'))) + ax_logo.axis('off') + + return fig + + +def main(): + # Parse command-line arguments + usage = "%prog [options] " + opts = optparse.OptionParser(usage) + opts.add_option("-o", "--output", type="string", dest="output", + default=None, help="filename of output graph") + opts.add_option("-f", "--max_freq", type="float", default=200., + help="maximum frequency to graph") + opts.add_option("-s", "--max_smoothing", type="float", default=None, + help="maximum shaper smoothing to allow") + opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir", + default="~/klipper", help="main klipper directory") + options, args = opts.parse_args() + if len(args) < 1: + opts.error("Incorrect number of arguments") + if options.output is None: + opts.error("You must specify an output file.png to use the script (option -o)") + if options.max_smoothing is not None and options.max_smoothing < 0.05: + opts.error("Too small max_smoothing specified (must be at least 0.05)") + + fig = shaper_calibration(args, options.klipperdir, options.max_smoothing, options.max_freq) + fig.savefig(options.output) + + +if __name__ == '__main__': + main() diff --git a/scripts/is_workflow/graph_vibrations.py b/scripts/is_workflow/graph_vibrations.py new file mode 100755 index 000000000..0ba6a632c --- /dev/null +++ b/scripts/is_workflow/graph_vibrations.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 + +################################################## +###### SPEED AND VIBRATIONS PLOTTING SCRIPT ###### +################################################## +# Written by Frix_x#0161 # +# @version: 2.0 + +# CHANGELOG: +# v2.0: - updated the script to align it to the new K-Shake&Tune module +# - new features for peaks detection and advised speed zones +# v1.2: fixed a bug that could happen when username is not "pi" (thanks @spikeygg) +# v1.1: better graph formatting +# v1.0: first version of the script + + +# Be sure to make this script executable using SSH: type 'chmod +x ./graph_vibrations.py' when in the folder ! + +##################################################################### +################ !!! DO NOT EDIT BELOW THIS LINE !!! ################ +##################################################################### + +import optparse, matplotlib, re, sys, importlib, os, operator +from collections import OrderedDict +import numpy as np +import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager +import matplotlib.ticker, matplotlib.gridspec +import locale +from datetime import datetime + +matplotlib.use('Agg') + + +PEAKS_DETECTION_THRESHOLD = 0.05 +PEAKS_RELATIVE_HEIGHT_THRESHOLD = 0.04 +VALLEY_DETECTION_THRESHOLD = 0.1 # Lower is more sensitive + +KLIPPAIN_COLORS = { + "purple": "#70088C", + "dark_purple": "#150140", + "dark_orange": "#F24130" +} + + +# Set the best locale for time and date formating (generation of the titles) +try: + locale.setlocale(locale.LC_TIME, locale.getdefaultlocale()) +except locale.Error: + locale.setlocale(locale.LC_TIME, 'C') + +# Override the built-in print function to avoid problem in Klipper due to locale settings +original_print = print +def print_with_c_locale(*args, **kwargs): + original_locale = locale.setlocale(locale.LC_ALL, None) + locale.setlocale(locale.LC_ALL, 'C') + original_print(*args, **kwargs) + locale.setlocale(locale.LC_ALL, original_locale) +print = print_with_c_locale + + +###################################################################### +# Computation +###################################################################### + +def calc_freq_response(data): + # Use Klipper standard input shaper objects to do the computation + helper = shaper_calibrate.ShaperCalibrate(printer=None) + return helper.process_accelerometer_data(data) + + +def calc_psd(datas, group, max_freq): + psd_list = [] + first_freqs = None + signal_axes = ['x', 'y', 'z', 'all'] + + for i in range(0, len(datas), group): + + # Round up to the nearest power of 2 for faster FFT + N = datas[i].shape[0] + T = datas[i][-1,0] - datas[i][0,0] + M = 1 << int((N/T) * 0.5 - 1).bit_length() + if N <= M: + # If there is not enough lines in the array to be able to round up to the + # nearest power of 2, we need to pad some zeros at the end of the array to + # avoid entering a blocking state from Klipper shaper_calibrate.py + datas[i] = np.pad(datas[i], [(0, (M-N)+1), (0, 0)], mode='constant', constant_values=0) + + freqrsp = calc_freq_response(datas[i]) + for n in range(group - 1): + data = datas[i + n + 1] + + # Round up to the nearest power of 2 for faster FFT + N = data.shape[0] + T = data[-1,0] - data[0,0] + M = 1 << int((N/T) * 0.5 - 1).bit_length() + if N <= M: + # If there is not enough lines in the array to be able to round up to the + # nearest power of 2, we need to pad some zeros at the end of the array to + # avoid entering a blocking state from Klipper shaper_calibrate.py + data = np.pad(data, [(0, (M-N)+1), (0, 0)], mode='constant', constant_values=0) + + freqrsp.add_data(calc_freq_response(data)) + + if not psd_list: + # First group, just put it in the result list + first_freqs = freqrsp.freq_bins + psd = freqrsp.psd_sum[first_freqs <= max_freq] + px = freqrsp.psd_x[first_freqs <= max_freq] + py = freqrsp.psd_y[first_freqs <= max_freq] + pz = freqrsp.psd_z[first_freqs <= max_freq] + psd_list.append([psd, px, py, pz]) + else: + # Not the first group, we need to interpolate every new signals + # to the first one to equalize the frequency_bins between them + signal_normalized = dict() + freqs = freqrsp.freq_bins + for axe in signal_axes: + signal = freqrsp.get_psd(axe) + signal_normalized[axe] = np.interp(first_freqs, freqs, signal) + + # Remove data above max_freq on all axes and add to the result list + psd = signal_normalized['all'][first_freqs <= max_freq] + px = signal_normalized['x'][first_freqs <= max_freq] + py = signal_normalized['y'][first_freqs <= max_freq] + pz = signal_normalized['z'][first_freqs <= max_freq] + psd_list.append([psd, px, py, pz]) + + return first_freqs[first_freqs <= max_freq], psd_list + + +def calc_powertot(psd_list, freqs): + pwrtot_sum = [] + pwrtot_x = [] + pwrtot_y = [] + pwrtot_z = [] + + for psd in psd_list: + pwrtot_sum.append(np.trapz(psd[0], freqs)) + pwrtot_x.append(np.trapz(psd[1], freqs)) + pwrtot_y.append(np.trapz(psd[2], freqs)) + pwrtot_z.append(np.trapz(psd[3], freqs)) + + return [pwrtot_sum, pwrtot_x, pwrtot_y, pwrtot_z] + + +# This find all the peaks in a curve by looking at when the derivative term goes from positive to negative +# Then only the peaks found above a threshold are kept to avoid capturing peaks in the low amplitude noise of a signal +# Additionaly, we validate that a peak is a real peak based of its neighbors as we can have pretty flat zones in vibration +# graphs with a lot of false positive due to small "noise" in these flat zones +def detect_peaks(power_total, speeds, window_size=10, vicinity=10): + # Smooth the curve using a moving average to avoid catching peaks everywhere in noisy signals + kernel = np.ones(window_size) / window_size + smoothed_psd = np.convolve(power_total, kernel, mode='valid') + mean_pad = [np.mean(power_total[:window_size])] * (window_size // 2) + smoothed_psd = np.concatenate((mean_pad, smoothed_psd)) + + # Find peaks on the smoothed curve (and excluding the last value of the serie often detected when in a flat zone) + smoothed_peaks = np.where((smoothed_psd[:-3] < smoothed_psd[1:-2]) & (smoothed_psd[1:-2] > smoothed_psd[2:-1]))[0] + 1 + detection_threshold = PEAKS_DETECTION_THRESHOLD * power_total.max() + + valid_peaks = [] + for peak in smoothed_peaks: + peak_height = smoothed_psd[peak] - np.min(smoothed_psd[max(0, peak-vicinity):min(len(smoothed_psd), peak+vicinity+1)]) + if peak_height > PEAKS_RELATIVE_HEIGHT_THRESHOLD * smoothed_psd[peak] and smoothed_psd[peak] > detection_threshold: + valid_peaks.append(peak) + + # Refine peak positions on the original curve + refined_peaks = [] + for peak in valid_peaks: + local_max = peak + np.argmax(power_total[max(0, peak-vicinity):min(len(power_total), peak+vicinity+1)]) - vicinity + refined_peaks.append(local_max) + + peak_speeds = ["{:.1f}".format(speeds[i]) for i in refined_peaks] + num_peaks = len(refined_peaks) + print("Vibrations peaks detected: %d @ %s mm/s (avoid running these speeds in your slicer profile)" % (num_peaks, ", ".join(map(str, peak_speeds)))) + + return np.array(refined_peaks), num_peaks + + +# The goal is to find zone outside of peaks (flat low energy zones) to advise them as good speeds range to use in the slicer +def identify_low_energy_zones(power_total): + valleys = [] + + # Calculate the mean and standard deviation of the entire power_total + mean_energy = np.mean(power_total) + std_energy = np.std(power_total) + + # Define a threshold value as mean minus a certain number of standard deviations + threshold_value = mean_energy - VALLEY_DETECTION_THRESHOLD * std_energy + + # Find valleys in power_total based on the threshold + in_valley = False + start_idx = 0 + for i, value in enumerate(power_total): + if not in_valley and value < threshold_value: + in_valley = True + start_idx = i + elif in_valley and value >= threshold_value: + in_valley = False + valleys.append((start_idx, i)) + + # If the last point is still in a valley, close the valley + if in_valley: + valleys.append((start_idx, len(power_total) - 1)) + + max_signal = np.max(power_total) + + # Calculate mean energy for each valley as a percentage of the maximum of the signal + valley_means_percentage = [] + for start, end in valleys: + if not np.isnan(np.mean(power_total[start:end])): + valley_means_percentage.append((start, end, (np.mean(power_total[start:end]) / max_signal) * 100)) + + # Sort valleys based on mean percentage values + sorted_valleys = sorted(valley_means_percentage, key=lambda x: x[2]) + + return sorted_valleys + + +# Resample the signal to achieve denser data points in order to get more precise valley placing and +# avoid having to use the original sampling of the signal (that is equal to the speed increment used for the test) +def resample_signal(speeds, power_total, new_spacing=0.1): + new_speeds = np.arange(speeds[0], speeds[-1] + new_spacing, new_spacing) + new_power_total = np.interp(new_speeds, speeds, power_total) + return new_speeds, new_power_total + + +###################################################################### +# Graphing +###################################################################### + +def plot_total_power(ax, speeds, power_total): + resampled_speeds, resampled_power_total = resample_signal(speeds, power_total[0]) + + ax.set_title("Vibrations decomposition", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') + ax.set_xlabel('Speed (mm/s)') + ax.set_ylabel('Energy') + + ax2 = ax.twinx() + ax2.yaxis.set_visible(False) + + power_total_sum = np.array(resampled_power_total) + speed_array = np.array(resampled_speeds) + max_y = power_total_sum.max() + power_total_sum.max() * 0.05 + ax.set_xlim([speed_array.min(), speed_array.max()]) + ax.set_ylim([0, max_y]) + ax2.set_ylim([0, max_y]) + + ax.plot(resampled_speeds, resampled_power_total, label="X+Y+Z", color='purple') + ax.plot(speeds, power_total[1], label="X", color='red') + ax.plot(speeds, power_total[2], label="Y", color='green') + ax.plot(speeds, power_total[3], label="Z", color='blue') + + peaks, num_peaks = detect_peaks(resampled_power_total, resampled_speeds) + low_energy_zones = identify_low_energy_zones(resampled_power_total) + + if peaks.size: + ax.plot(speed_array[peaks], power_total_sum[peaks], "x", color='black', markersize=8) + for idx, peak in enumerate(peaks): + fontcolor = 'red' + fontweight = 'bold' + ax.annotate(f"{idx+1}", (speed_array[peak], power_total_sum[peak]), + textcoords="offset points", xytext=(8, 5), + ha='left', fontsize=13, color=fontcolor, weight=fontweight) + ax2.plot([], [], ' ', label=f'Number of peaks: {num_peaks}') + else: + ax2.plot([], [], ' ', label=f'No peaks detected') + + for idx, (start, end, energy) in enumerate(low_energy_zones): + ax.axvline(speed_array[start], color='red', linestyle='dotted', linewidth=1.5) + ax.axvline(speed_array[end], color='red', linestyle='dotted', linewidth=1.5) + ax2.fill_between(speed_array[start:end], 0, power_total_sum[start:end], color='green', alpha=0.2, label=f'Zone {idx+1}: {speed_array[start]:.1f} to {speed_array[end]:.1f} mm/s (mean energy: {energy:.2f}%)') + + ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) + ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) + ax.grid(which='major', color='grey') + ax.grid(which='minor', color='lightgrey') + fontP = matplotlib.font_manager.FontProperties() + fontP.set_size('small') + ax.legend(loc='upper left', prop=fontP) + ax2.legend(loc='upper right', prop=fontP) + + if peaks.size: + return speed_array[peaks] + else: + return None + + +def plot_spectrogram(ax, speeds, freqs, power_spectral_densities, peaks, max_freq): + spectrum = np.empty([len(freqs), len(speeds)]) + + for i in range(len(speeds)): + for j in range(len(freqs)): + spectrum[j, i] = power_spectral_densities[i][0][j] + + ax.set_title("Vibrations spectrogram", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') + ax.pcolormesh(speeds, freqs, spectrum, norm=matplotlib.colors.LogNorm(), + cmap='inferno', shading='gouraud') + + # Add peaks lines in the spectrogram to get hint from peaks found in the first graph + if peaks is not None: + for idx, peak in enumerate(peaks): + ax.axvline(peak, color='cyan', linestyle='dotted', linewidth=0.75) + ax.annotate(f"Peak {idx+1}", (peak, freqs[-1]*0.9), + textcoords="data", color='cyan', rotation=90, fontsize=10, + verticalalignment='top', horizontalalignment='right') + + ax.set_ylim([0., max_freq]) + ax.set_ylabel('Frequency (hz)') + ax.set_xlabel('Speed (mm/s)') + + return + + +###################################################################### +# Startup and main routines +###################################################################### + +def parse_log(logname): + with open(logname) as f: + for header in f: + if not header.startswith('#'): + break + if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'): + # Raw accelerometer data + return np.loadtxt(logname, comments='#', delimiter=',') + # Power spectral density data or shaper calibration data + raise ValueError("File %s does not contain raw accelerometer data and therefore " + "is not supported by graph_vibrations.py script. Please use " + "calibrate_shaper.py script to process it instead." % (logname,)) + + +def extract_speed(logname): + try: + speed = re.search('sp(.+?)n', os.path.basename(logname)).group(1).replace('_','.') + except AttributeError: + raise ValueError("File %s does not contain speed in its name and therefore " + "is not supported by graph_vibrations.py script." % (logname,)) + return float(speed) + + +def sort_and_slice(raw_speeds, raw_datas, remove): + # Sort to get the speeds and their datas aligned and in ascending order + raw_speeds, raw_datas = zip(*sorted(zip(raw_speeds, raw_datas), key=operator.itemgetter(0))) + + # Remove beginning and end of the datas for each file to get only + # constant speed data and remove the start/stop phase of the movements + datas = [] + for data in raw_datas: + sliced = round((len(data) * remove / 100) / 2) + datas.append(data[sliced:len(data)-sliced]) + + return raw_speeds, datas + + +def setup_klipper_import(kdir): + global shaper_calibrate + kdir = os.path.expanduser(kdir) + sys.path.append(os.path.join(kdir, 'klippy')) + shaper_calibrate = importlib.import_module('.shaper_calibrate', 'extras') + + +def vibrations_calibration(lognames, klipperdir="~/klipper", axisname=None, max_freq=1000., remove=0): + setup_klipper_import(klipperdir) + + # Parse the raw data and get them ready for analysis + raw_datas = [parse_log(filename) for filename in lognames] + raw_speeds = [extract_speed(filename) for filename in lognames] + speeds, datas = sort_and_slice(raw_speeds, raw_datas, remove) + + # As we assume that we have the same number of file for each speeds. We can group + # the PSD results by this number (to combine vibrations at given speed on all movements) + group_by = speeds.count(speeds[0]) + # Compute psd and total power of the signal + freqs, power_spectral_densities = calc_psd(datas, group_by, max_freq) + power_total = calc_powertot(power_spectral_densities, freqs) + + fig = matplotlib.pyplot.figure() + gs = matplotlib.gridspec.GridSpec(2, 1, height_ratios=[4, 3]) + ax1 = fig.add_subplot(gs[0]) + ax2 = fig.add_subplot(gs[1]) + + title_line1 = "VIBRATIONS MEASUREMENT TOOL" + fig.text(0.12, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold') + try: + filename_parts = (lognames[0].split('/')[-1]).split('_') + dt = datetime.strptime(f"{filename_parts[1]} {filename_parts[2].split('-')[0]}", "%Y%m%d %H%M%S") + title_line2 = dt.strftime('%x %X') + ' -- ' + axisname.upper() + ' axis' + except: + print("Warning: CSV filename look to be different than expected (%s)" % (lognames[0])) + title_line2 = lognames[0].split('/')[-1] + fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple']) + + # Remove speeds duplicates and graph the processed datas + speeds = list(OrderedDict((x, True) for x in speeds).keys()) + + peaks = plot_total_power(ax1, speeds, power_total) + plot_spectrogram(ax2, speeds, freqs, power_spectral_densities, peaks, max_freq) + + fig.set_size_inches(8.3, 11.6) + fig.tight_layout() + fig.subplots_adjust(top=0.89) + + # Adding a small Klippain logo to the top left corner of the figure + ax_logo = fig.add_axes([0.001, 0.899, 0.1, 0.1], anchor='NW', zorder=-1) + ax_logo.imshow(matplotlib.pyplot.imread(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'klippain.png'))) + ax_logo.axis('off') + + return fig + + +def main(): + # Parse command-line arguments + usage = "%prog [options] " + opts = optparse.OptionParser(usage) + opts.add_option("-o", "--output", type="string", dest="output", + default=None, help="filename of output graph") + opts.add_option("-a", "--axis", type="string", dest="axisname", + default=None, help="axis name to be shown on the side of the graph") + opts.add_option("-f", "--max_freq", type="float", default=1000., + help="maximum frequency to graph") + opts.add_option("-r", "--remove", type="int", default=0, + help="percentage of data removed at start/end of each files") + opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir", + default="~/klipper", help="main klipper directory") + options, args = opts.parse_args() + if len(args) < 1: + opts.error("No CSV file(s) to analyse") + if options.output is None: + opts.error("You must specify an output file.png to use the script (option -o)") + if options.remove > 50 or options.remove < 0: + opts.error("You must specify a correct percentage (option -r) in the 0-50 range") + + fig = vibrations_calibration(args, options.klipperdir, options.axisname, options.max_freq, options.remove) + fig.savefig(options.output) + + +if __name__ == '__main__': + main() diff --git a/scripts/is_workflow/is_workflow.py b/scripts/is_workflow/is_workflow.py new file mode 100755 index 000000000..be38addb2 --- /dev/null +++ b/scripts/is_workflow/is_workflow.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +############################################ +###### INPUT SHAPER KLIPPAIN WORKFLOW ###### +############################################ +# Written by Frix_x#0161 # +# @version: 2.0 + +# CHANGELOG: +# v2.0: new version of this as a Python script (to replace the old bash script) and implement the newer and improved shaper plotting scripts +# v1.7: updated the handling of shaper files to account for the new analysis scripts as we are now using raw data directly +# v1.6: - updated the handling of shaper graph files to be able to optionnaly account for added positions in the filenames and remove them +# - fixed a bug in the belt graph on slow SD card or Pi clones (Klipper was still writing in the file while we were already reading it) +# v1.5: fixed klipper unnexpected fail at the end of the execution, even if graphs were correctly generated (unicode decode error fixed) +# v1.4: added the ~/klipper dir parameter to the call of graph_vibrations.py for a better user handling (in case user is not "pi") +# v1.3: some documentation improvement regarding the line endings that needs to be LF for this file +# v1.2: added the movement name to be transfered to the Python script in vibration calibration (to print it on the result graphs) +# v1.1: multiple fixes and tweaks (mainly to avoid having empty files read by the python scripts after the mv command) +# v1.0: first version of the script based on a Zellneralex script + +# Usage: +# This script was designed to be used with gcode_shell_commands directly from Klipper +# Parameters availables: +# BELTS - To generate belts diagrams after calling the Klipper TEST_RESONANCES AXIS=1,(-)1 OUTPUT=raw_data +# SHAPER - To generate input shaper diagrams after calling the Klipper TEST_RESONANCES AXIS=X/Y OUTPUT=raw_data +# VIBRATIONS - To generate vibration diagram after calling the custom (Frix_x#0161) VIBRATIONS_CALIBRATION macro + + + +import os +import time +import glob +import sys +import shutil +import tarfile +from datetime import datetime + +################################################################################################################# +RESULTS_FOLDER = os.path.expanduser('~/printer_data/config/K-ShakeTune_results') +KLIPPER_FOLDER = os.path.expanduser('~/klipper') +STORE_RESULTS = 3 +################################################################################################################# + +from graph_belts import belts_calibration +from graph_shaper import shaper_calibration +from graph_vibrations import vibrations_calibration + +RESULTS_SUBFOLDERS = ['belts', 'inputshaper', 'vibrations'] + + +def is_file_open(filepath): + for proc in os.listdir('/proc'): + if proc.isdigit(): + for fd in glob.glob(f'/proc/{proc}/fd/*'): + try: + if os.path.samefile(fd, filepath): + return True + except FileNotFoundError: + pass + return False + + +def get_belts_graph(): + current_date = datetime.now().strftime('%Y%m%d_%H%M%S') + lognames = [] + + globbed_files = glob.glob('/tmp/raw_data_axis*.csv') + if not globbed_files: + print("No CSV files found in the /tmp folder to create the belt graphs!") + sys.exit(1) + if len(globbed_files) < 2: + print("Not enough CSV files found in the /tmp folder. Two files are required for the belt graphs!") + sys.exit(1) + sorted_files = sorted(globbed_files, key=os.path.getmtime, reverse=True) + + for filename in sorted_files[:2]: + # Wait for the file handler to be released by Klipper + while is_file_open(filename): + time.sleep(3) + + # Extract the tested belt from the filename and rename/move the CSV file to the result folder + belt = os.path.basename(filename).split('_')[3].split('.')[0].upper() + new_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[0], f'belt_{current_date}_{belt}.csv') + shutil.move(filename, new_file) + + # Save the file path for later + lognames.append(new_file) + + # Generate the belts graph and its name + fig = belts_calibration(lognames, KLIPPER_FOLDER) + png_filename = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[0], f'belts_{current_date}.png') + + return fig, png_filename + + +def get_shaper_graph(): + current_date = datetime.now().strftime('%Y%m%d_%H%M%S') + + # Get all the files and sort them based on last modified time to select the most recent one + globbed_files = glob.glob('/tmp/raw_data*.csv') + if not globbed_files: + print("No CSV files found in the /tmp folder to create the input shaper graphs!") + sys.exit(1) + sorted_files = sorted(globbed_files, key=os.path.getmtime, reverse=True) + filename = sorted_files[0] + + # Wait for the file handler to be released by Klipper + while is_file_open(filename): + time.sleep(3) + + # Extract the tested axis from the filename and rename/move the CSV file to the result folder + axis = os.path.basename(filename).split('_')[3].split('.')[0].upper() + new_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[1], f'resonances_{current_date}_{axis}.csv') + shutil.move(filename, new_file) + + # Generate the shaper graph and its name + fig = shaper_calibration([new_file], KLIPPER_FOLDER) + png_filename = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[1], f'resonances_{current_date}_{axis}.png') + + return fig, png_filename + + +def get_vibrations_graph(axis_name): + current_date = datetime.now().strftime('%Y%m%d_%H%M%S') + lognames = [] + + globbed_files = glob.glob('/tmp/adxl345-*.csv') + if not globbed_files: + print("No CSV files found in the /tmp folder to create the vibration graphs!") + sys.exit(1) + if len(globbed_files) < 3: + print("Not enough CSV files found in the /tmp folder. At least 3 files are required for the vibration graphs!") + sys.exit(1) + + for filename in globbed_files: + # Wait for the file handler to be released by Klipper + while is_file_open(filename): + time.sleep(3) + + # Cleanup of the filename and moving it in the result folder + cleanfilename = os.path.basename(filename).replace('adxl345', f'vibr_{current_date}') + new_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], cleanfilename) + shutil.move(filename, new_file) + + # Save the file path for later + lognames.append(new_file) + + # Sync filesystem to avoid problems as there is a lot of file copied + os.sync() + + # Generate the vibration graph and its name + fig = vibrations_calibration(lognames, KLIPPER_FOLDER, axis_name) + png_filename = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], f'vibrations_{current_date}_{axis_name}.png') + + # Archive all the csv files in a tarball and remove them to clean up the results folder + with tarfile.open(os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], f'vibrations_{current_date}_{axis_name}.tar.gz'), 'w:gz') as tar: + for csv_file in glob.glob(os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], f'vibr_{current_date}*.csv')): + tar.add(csv_file, recursive=False) + os.remove(csv_file) + + return fig, png_filename + + +# Utility function to get old files based on their modification time +def get_old_files(folder, extension, limit): + files = [os.path.join(folder, f) for f in os.listdir(folder) if f.endswith(extension)] + files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + return files[limit:] + +def clean_files(): + # Define limits based on STORE_RESULTS + keep1 = STORE_RESULTS + 1 + keep2 = 2 * STORE_RESULTS + 1 + + # Find old files in each directory + old_belts_files = get_old_files(os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[0]), '.png', keep1) + old_inputshaper_files = get_old_files(os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[1]), '.png', keep2) + old_vibrations_files = get_old_files(os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2]), '.png', keep1) + + # Remove the old belt files + for old_file in old_belts_files: + file_date = "_".join(os.path.splitext(os.path.basename(old_file))[0].split('_')[1:3]) + for suffix in ['A', 'B']: + csv_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[0], f'belt_{file_date}_{suffix}.csv') + if os.path.exists(csv_file): + os.remove(csv_file) + os.remove(old_file) + + # Remove the old shaper files + for old_file in old_inputshaper_files: + csv_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[1], os.path.splitext(os.path.basename(old_file))[0] + ".csv") + if os.path.exists(csv_file): + os.remove(csv_file) + os.remove(old_file) + + # Remove the old vibrations files + for old_file in old_vibrations_files: + os.remove(old_file) + tar_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], os.path.splitext(os.path.basename(old_file))[0] + ".tar.gz") + if os.path.exists(tar_file): + os.remove(tar_file) + + +def main(): + # Check if results folders are there or create them + for result_subfolder in RESULTS_SUBFOLDERS: + folder = os.path.join(RESULTS_FOLDER, result_subfolder) + if not os.path.exists(folder): + os.makedirs(folder) + + if len(sys.argv) < 2: + print("Usage: plot_graphs.py [SHAPER|BELTS|VIBRATIONS]") + sys.exit(1) + + if sys.argv[1].lower() == 'belts': + fig, png_filename = get_belts_graph() + elif sys.argv[1].lower() == 'shaper': + fig, png_filename = get_shaper_graph() + elif sys.argv[1].lower() == 'vibrations': + fig, png_filename = get_vibrations_graph(axis_name=sys.argv[2]) + else: + print("Usage: plot_graphs.py [SHAPER|BELTS|VIBRATIONS]") + sys.exit(1) + + fig.savefig(png_filename) + + clean_files() + print(f"Graphs created. You will find the results in {RESULTS_FOLDER}") + + +if __name__ == '__main__': + main() diff --git a/scripts/is_workflow/is_workflow_cmd.cfg b/scripts/is_workflow/is_workflow_cmd.cfg new file mode 100644 index 000000000..be19f8a8e --- /dev/null +++ b/scripts/is_workflow/is_workflow_cmd.cfg @@ -0,0 +1,4 @@ +[gcode_shell_command plot_graph] +command: ~/printer_data/config/scripts/is_workflow/is_workflow.py +timeout: 600.0 +verbose: True diff --git a/scripts/is_workflow/klippain.png b/scripts/is_workflow/klippain.png new file mode 100644 index 000000000..7b8ce84a9 Binary files /dev/null and b/scripts/is_workflow/klippain.png differ diff --git a/scripts/plot_graphs.sh b/scripts/plot_graphs.sh deleted file mode 100755 index ef8ec0f65..000000000 --- a/scripts/plot_graphs.sh +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env bash -################################### -###### GRAPH PLOTTING SCRIPT ###### -################################### -# Written by Frix_x#0161 # -# @version: 1.6 - -# CHANGELOG: -# v1.6: - updated the handling of shaper graph files to be able to optionnaly account for added positions in the filenames and remove them -# - fixed a bug in the belt graph on slow SD card or Pi clones (Klipper was still writing in the file while we were already reading it) -# v1.5: fixed klipper unnexpected fail at the end of the execution, even if graphs were correctly generated (unicode decode error fixed) -# v1.4: added the ~/klipper dir parameter to the call of graph_vibrations.py for a better user handling (in case user is not "pi") -# v1.3: some documentation improvement regarding the line endings that needs to be LF for this file -# v1.2: added the movement name to be transfered to the Python script in vibration calibration (to print it on the result graphs) -# v1.1: multiple fixes and tweaks (mainly to avoid having empty files read by the python scripts after the mv command) -# v1.0: first version of the script based on a Zellneralex script - -# Installation: -# 1. Copy this file somewhere in your config folder and edit the parameters below if needed -# Note: If using Windows to do the copy/paste, be careful with the line endings for this file: LF (or \n) is mandatory !!! No \r should be -# present in the file as it could lead to some errors like "\r : unknown command" when running the script. If you're not confident -# regarding your text editor behavior, the best way is to directly download the file on the pi by using for example wget: -# type 'wget -P ~/printer_data/config/scripts https://raw.githubusercontent.com/Frix-x/klippain/main/scripts/plot_graphs.sh' -# 2. Make it executable using SSH: type 'chmod +x ~/printer_data/config/scripts/plot_graphs.sh' (adjust the path if needed). -# 3. Be sure to have the gcode_shell_command.py Klipper extension installed (easiest way to install it is to use KIAUH in the Advanced section) -# 4. Create a gcode_shell_command to be able to start it from a macro (see my shell_commands.cfg file) - -# Usage: -# This script was designed to be used with gcode_shell_commands. Use it to call it. -# Parameters availables: -# SHAPER - To generate input shaper diagrams after calling the Klipper TEST_RESONANCES AXIS=X/Y -# BELTS - To generate belts diagrams after calling the Klipper TEST_RESONANCES AXIS=1,(-)1 OUTPUT=raw_data -# VIBRATIONS - To generate vibration diagram after calling the custom (Frix_x#0161) VIBRATIONS_CALIBRATION macro - - -################################################################################################################# -RESULTS_FOLDER=~/printer_data/config/adxl_results # Path to the folder where storing the results files -SCRIPTS_FOLDER=~/printer_data/config/scripts # Path to the folder where the graph_vibrations.py is located -KLIPPER_FOLDER=~/klipper # Path of the klipper main folder -STORE_RESULTS=3 # Number of results to keep (older files are automatically cleaned). 0 to keep them indefinitely -################################################################################################################# - - -##################################################################### -################ !!! DO NOT EDIT BELOW THIS LINE !!! ################ -##################################################################### - -export LC_ALL=C - -function is_fopen() { - filepath=$(realpath "$1") - for pid in $(ls /proc | grep -E '^[0-9]+$'); do - if [ -d "/proc/$pid/fd" ]; then - for fd in /proc/$pid/fd/*; do - if [ -L "$fd" ] && [ "$(readlink -f "$fd")" == "$filepath" ]; then - return 0 - fi - done - fi - done - return 1 -} - -function plot_shaper_graph { - local generator filename newfilename date axis - generator="${KLIPPER_FOLDER}/scripts/calibrate_shaper.py" - - # For each file - while read filename; do - # Wait for the file handler to be released by Klipper - while is_fopen "${filename}"; do - sleep 3 - done - - # We remove the /tmp in front of the filename - newfilename="$(echo ${filename} | sed -e "s/\\/tmp\///")" - - # We check if there is the position added by Klipper and remove it - if [[ ${newfilename} =~ ^resonances_[[:alpha:]]_([0-9]*\.)+[0-9]*_ ]]; then - newfilename="$(echo ${newfilename} | sed -E 's/(^resonances_[[:alpha:]])_(([0-9]*\.)+[0-9]*_)+/\1_/')" - fi - - # We extract the date and axis name from the filename - date="$(basename "${newfilename}" | cut -d '.' -f1 | awk -F'_' '{print $3"_"$4}')" - axis="$(basename "${newfilename}" | cut -d '_' -f2)" - - # Then we move the file to the result folder - mv "${filename}" "${isf}"/inputshaper/"${newfilename}" - sync && sleep 2 - - # Finally we compute the shaper graphs - "${generator}" "${isf}"/inputshaper/"${newfilename}" -o "${isf}"/inputshaper/resonances_"${axis}"_"${date}".png - done <<< "$(find /tmp -type f -name "resonances_*.csv" 2>&1 | grep -v "Permission")" -} - -function plot_belts_graph { - local date_ext generator filename belt - date_ext="$(date +%Y%m%d_%H%M%S)" - generator="${KLIPPER_FOLDER}/scripts/graph_accelerometer.py" - - # For each file - while read filename; do - # Wait for the file handler to be released by Klipper - while is_fopen "${filename}"; do - sleep 3 - done - - # We extract the belt tested from the filename - belt="$(basename "${filename}" | cut -d '_' -f4 | cut -d '.' -f1 | sed -e 's/\(.*\)/\U\1/')" - - # And we move it to the result folder while injecting the date and belt inside the filename - mv "${filename}" "${isf}"/belts/belt_"${date_ext}"_"${belt}".csv - done <<< "$(find /tmp -type f -name "raw_data_axis*.csv" 2>&1 | grep -v "Permission")" - sync && sleep 2 - - # Finally we compute the belts graph - "${generator}" -c "${isf}"/belts/belt_"${date_ext}"_*.csv -o "${isf}"/belts/belts_"${date_ext}".png -} - -function plot_vibr_graph { - local date_ext generator filename newfilename - date_ext="$(date +%Y%m%d_%H%M%S)" - generator="${SCRIPTS_FOLDER}/graph_vibrations.py" - - # For each file - while read filename; do - # Wait for the file handler to be released by Klipper - while is_fopen "${filename}"; do - sleep 3 - done - - # Cleanup of the filename and moving it in the result folder - newfilename="$(echo ${filename} | sed -e "s/\\/tmp\/adxl345/vibr_${date_ext}/")" - mv "${filename}" "${isf}"/vibrations/"${newfilename}" - done <<< "$(find /tmp -type f -name "adxl345-*.csv" 2>&1 | grep -v "Permission")" - sync && sleep 2 - - # We compute the vibration graphs using all the csv files - "${generator}" "${isf}"/vibrations/vibr_"${date_ext}"*.csv -o "${isf}"/vibrations/vibrations_"${date_ext}".png -a "$1" -k "${KLIPPER_FOLDER}" - - # Finally we cleanup the folder by moving the csv files in an archive - tar cfz "${isf}"/vibrations/vibrations_"${date_ext}".tar.gz "${isf}"/vibrations/vibr_"${date_ext}"*.csv - rm "${isf}"/vibrations/vibr_"${date_ext}"*.csv -} - -function clean_files { - local filename keep1 keep2 old csv date - keep1=$(( ${STORE_RESULTS} + 1 )) - keep2=$(( ${STORE_RESULTS} * 2 + 1)) - - while read filename; do - if [ ! -z "${filename}" ]; then - old+=("${filename}") - csv="$(basename "${filename}" | cut -d '.' -f1)" - old+=("${isf}"/inputshaper/"${csv}".csv) - fi - done <<< "$(find "${isf}"/inputshaper/ -type f -name '*.png' -printf '%T@ %p\n' | sort -k 1 -n -r | sed 's/^[^ ]* //' | tail -n +"${keep2}")" - - while read filename; do - if [ ! -z "${filename}" ]; then - old+=("${filename}") - date="$(basename "${filename}" | cut -d '.' -f1 | awk -F'_' '{print $2"_"$3}')" - old+=("${isf}"/belts/belt_"${date}"_A.csv) - old+=("${isf}"/belts/belt_"${date}"_B.csv) - fi - done <<< "$(find "${isf}"/belts/ -type f -name '*.png' -printf '%T@ %p\n' | sort -k 1 -n -r | sed 's/^[^ ]* //' | tail -n +"${keep1}")" - - while read filename; do - if [ ! -z "${filename}" ]; then - old+=("${filename}") - csv="$(basename "${filename}" | cut -d '.' -f1)" - old+=("${isf}"/vibrations/"${csv}".tar.gz) - fi - done <<< "$(find "${isf}"/vibrations/ -type f -name '*.png' -printf '%T@ %p\n' | sort -k 1 -n -r | sed 's/^[^ ]* //' | tail -n +"${keep1}")" - - if [ "${#old[@]}" -ne 0 -a "${STORE_RESULTS}" -ne 0 ]; then - for rmv in "${old[@]}"; do - rm "${rmv}" - done - fi -} - -############################# -### MAIN #################### -############################# - -if [ ! -d "${RESULTS_FOLDER}/inputshaper" ]; then - mkdir -p "${RESULTS_FOLDER}/inputshaper" -fi -if [ ! -d "${RESULTS_FOLDER}/belts" ]; then - mkdir -p "${RESULTS_FOLDER}/belts" -fi -if [ ! -d "${RESULTS_FOLDER}/vibrations" ]; then - mkdir -p "${RESULTS_FOLDER}/vibrations" -fi - -isf="${RESULTS_FOLDER//\~/${HOME}}" - -case ${1} in - SHAPER|shaper) - plot_shaper_graph - ;; - BELTS|belts) - plot_belts_graph - ;; - VIBRATIONS|vibrations) - plot_vibr_graph ${2} - ;; - *) - echo -e "\nUsage:" - echo -e "\t${0} SHAPER, BELTS or VIBRATIONS" - echo -e "\t\tSHAPER\tGenerate input shaper diagram" - echo -e "\t\tBELT\tGenerate belt tension diagram" - echo -e "\t\tVIBRATIONS axis-name\tGenerate vibration response diagram\n" - exit 1 -esac - -clean_files - -echo "Graphs created. You will find the results in ${isf}" diff --git a/scripts/shell_commands.cfg b/scripts/shell_commands.cfg index e3e263948..c6723f0a1 100644 --- a/scripts/shell_commands.cfg +++ b/scripts/shell_commands.cfg @@ -1,12 +1,10 @@ # This file include some gcode_shell_commands -# Be sure to have the gcode_shell_command.py Klipper extension installed (easiest way to install it is to use KIAUH in the Advanced section) - -[gcode_shell_command plot_graph] -command: bash /home/pi/printer_data/config/scripts/plot_graphs.sh -timeout: 500.0 -verbose: True +# Be sure to have the gcode_shell_command.py Klipper extension installed. Easiest way is +# to use KIAUH in the Advanced section but this is done automatically when installing Klippain [gcode_shell_command system_info] -command: python3 /home/pi/printer_data/config/scripts/system_info.py +command: ~/printer_data/config/scripts/system_info.py timeout: 5.0 verbose: True + +[include is_workflow/is_workflow_cmd.cfg] diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 000000000..71d543575 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +################################################# +######## AUTOMATED UNINSTALL SCRIPT ############# +################################################# +# Written by Frix_x +# @version: 1.0 + +# CHANGELOG: +# v1.0: first version of the script to allow an user to revert to his old config +# in case the install script was called by error... ;) + + +# Where the user Klipper config is located (ie. the one used by Klipper to work) +USER_CONFIG_PATH="${HOME}/printer_data/config" +# Where Frix-x repository config files are stored (Klippain read-only files that are untouched) +FRIX_CONFIG_PATH="${HOME}/klippain_config" +# Path used to store backups when updating (backups are automatically dated when saved inside) +BACKUP_PATH="${HOME}/klippain_config_backups" +# Where the Klipper folder is located (ie. the internal Klipper firmware machinery) +KLIPPER_PATH="${HOME}/klipper" + + +set -eu +export LC_ALL=C + +# Step 1: Verify that the script is not run as root and Klipper is installed. +# Then warn and ask the user if he is sure to proceed to revert to his old config +function preflight_checks { + if [ "$EUID" -eq 0 ]; then + echo "[PRE-CHECK] This script must not be run as root!" + exit -1 + fi + + if [ "$(sudo systemctl list-units --full -all -t service --no-legend | grep -F 'klipper.service')" ]; then + printf "[PRE-CHECK] Klipper service found! Continuing...\n\n" + else + echo "[ERROR] Klipper service not found, Klippain is unlikely to be installed! Exiting..." + exit -1 + fi + + local uninstall_klippain_answer + if [ ! -f "${USER_CONFIG_PATH}/.VERSION" ]; then + echo "[PRE-CHECK] This uninstall script will fully remove Klippain" + echo "[PRE-CHECK] If a backup from your old configuration (before using Klippain) is found, it will be restored" + echo "[PRE-CHECK] Be sure that the printer is idle before continuing!" + + read < /dev/tty -rp "[PRE-CHECK] Are you sure want to proceed and uninstall Klippain? (y/N) " uninstall_klippain_answer + if [[ -z "$uninstall_klippain_answer" ]]; then + uninstall_klippain_answer="n" + fi + uninstall_klippain_answer="${uninstall_klippain_answer,,}" + + if [[ "$uninstall_klippain_answer" =~ ^(yes|y)$ ]]; then + printf "[PRE-CHECK] Klippain will be uninstalled...\n\n" + else + echo "[PRE-CHECK] Klippain uninstall script was canceled!" + exit -1 + fi + fi +} + +# Step 2: Delete everything in ~/printer_data/config and the Klippain repository +function delete_current_klippain { + if [ -d "${USER_CONFIG_PATH}" ]; then + rm -rf ${USER_CONFIG_PATH} + mkdir ${USER_CONFIG_PATH} + printf "[UNINSTALL] Klippain user files deleted!\n\n" + else + echo "[WARNING] User config path not found! Nothing to delete here. Continuing..." + fi + + if [ -d "${FRIX_CONFIG_PATH}" ]; then + rm -rf ${FRIX_CONFIG_PATH} + printf "[UNINSTALL] Klippain read-only files deleted!\n\n" + else + echo "[WARNING] Klippain path not found! Nothing to delete here. Continuing..." + fi +} + +# Step 3: Find the latest backup without a .VERSION file and restore it if needed +function restore_latest_backup { + local restore_backup latest_backup + + read < /dev/tty -rp "[RESTORE] Would you like to restore your last config backup? This script will look for the last one before running Klippain (Y/n) " restore_backup + if [[ -z "$restore_backup" ]]; then + restore_backup="y" + fi + restore_backup="${restore_backup,,}" + + # Check and exit if the user do not wants to restore a backup + if [[ "$restore_backup" =~ ^(no|n)$ ]]; then + printf "[RESTORE] Skipping... No backup will be restored and you will need to manually populate your own printer.cfg file!\n\n" + return + fi + + latest_backup=$(find ${BACKUP_PATH} -type d -not -path "${BACKUP_PATH}" -exec sh -c 'if [ ! -f "$1/.VERSION" ]; then echo "$1"; fi' sh {} \; | sort -r | head -n 1) + if [ -n "${latest_backup}" ]; then + cp -fa ${latest_backup}/. ${USER_CONFIG_PATH} 2>/dev/null || : + printf "[RESTORE] Latest backup restored from: ${latest_backup}\n\n" + else + echo "[WARNING] No valid backup found in the Klippain backup folder... The restore process was skipped!" + fi +} + +# Step 5: Restart Klipper +function restart_klipper { + echo "[RESTART] Restarting Klipper..." + sudo systemctl restart klipper +} + +printf "\n=============================\n" +echo "- Klippain uninstall script -" +printf "=============================\n\n" + +# Run steps +preflight_checks +delete_current_klippain +restore_latest_backup +restart_klipper + +echo "[POST-UNINSTALL] Klippain was uninstalled!" +echo "[POST-UNINSTALL] Do not hesitate to give me your feedback, why you uninstalled Klippain and if there is something I can improve :)" +echo "[POST-UNINSTALL] Maybe see you again in the future..." +printf "\nPS: If a backup has been restored, check that everything is working and then you can safely delete the Klippain backup folder\n" diff --git a/user_templates/mcu_defaults/main/BTT_Manta_M8P_v2.0.cfg b/user_templates/mcu_defaults/main/BTT_Manta_M8P_v2.0.cfg new file mode 100644 index 000000000..a91c6a0eb --- /dev/null +++ b/user_templates/mcu_defaults/main/BTT_Manta_M8P_v2.0.cfg @@ -0,0 +1,53 @@ + +#---------------------------------------------# +#### BTT Manta M8P v2.0 MCU definition ######## +#---------------------------------------------# + +[mcu] +##-------------------------------------------------------------------- +# This board works by using a serial connection by default. If you +# want to use CAN, invert the commented lines and use canbus_uuid. + +serial: /dev/serial/by-id/change-me-to-the-correct-mcu-path +# canbus_uuid: change-me-to-the-correct-canbus-id +##-------------------------------------------------------------------- + +[include config/mcu_definitions/main/BTT_Manta_M8P_v2.0.cfg] # Do not remove this line +[board_pins mantam8p20_mcu] +mcu: mcu +aliases: + X_STEP=MCU_M1_STEP , X_DIR=MCU_M1_DIR , X_ENABLE=MCU_M1_EN , X_TMCUART=MCU_M1_CS , + Y_STEP=MCU_M2_STEP , Y_DIR=MCU_M2_DIR , Y_ENABLE=MCU_M2_EN , Y_TMCUART=MCU_M2_CS , + + Z_STEP=MCU_M3_STEP , Z_DIR=MCU_M3_DIR , Z_ENABLE=MCU_M3_EN , Z_TMCUART=MCU_M3_CS , + Z1_STEP=MCU_M4_STEP , Z1_DIR=MCU_M4_DIR , Z1_ENABLE=MCU_M4_EN , Z1_TMCUART=MCU_M4_CS , + Z2_STEP=MCU_M5_STEP , Z2_DIR=MCU_M5_DIR , Z2_ENABLE=MCU_M5_EN , Z2_TMCUART=MCU_M5_CS , + Z3_STEP=MCU_M6_STEP , Z3_DIR=MCU_M6_DIR , Z3_ENABLE=MCU_M6_EN , Z3_TMCUART=MCU_M6_CS , + + E_STEP=MCU_M7_STEP , E_DIR=MCU_M7_DIR , E_ENABLE=MCU_M7_EN , E_TMCUART=MCU_M7_CS , + + DRIVER_SPI_MOSI=MCU_SPI2_MOSI , # Used in case of SPI drivers such as TMC2240 or TMC5160 + DRIVER_SPI_MISO=MCU_SPI2_MISO , # Used in case of SPI drivers such as TMC2240 or TMC5160 + DRIVER_SPI_SCK=MCU_SPI2_SCK , # Used in case of SPI drivers such as TMC2240 or TMC5160 + + X_STOP=MCU_M1_STOP , Y_STOP=MCU_M2_STOP , Z_STOP=MCU_M3_STOP , + PROBE_INPUT=MCU_PROBE2 , + RUNOUT_SENSOR=MCU_FWS1 , + + E_HEATER=MCU_HE0 , E_TEMPERATURE=MCU_TH0 , + BED_HEATER=MCU_BED_OUT , BED_TEMPERATURE=MCU_THB , + + PART_FAN=MCU_FAN0 , E_FAN=MCU_FAN1 , + CONTROLLER_FAN=MCU_FAN2 , + EXHAUST_FAN=MCU_FAN3 , + FILTER_FAN=MCU_FAN4 , + HOST_CONTROLLER_FAN=MCU_FAN5 , + + CHAMBER_TEMPERATURE=MCU_TH1 , + ELECTRICAL_CABINET_TEMPERATURE=MCU_TH2 , + + LIGHT_OUTPUT=MCU_HE2 , + STATUS_NEOPIXEL=MCU_RGB1 , + LIGHT_NEOPIXEL=MCU_M5_STOP , + + SERVO_PIN=MCU_PROBE1 , diff --git a/user_templates/mcu_defaults/main/BTT_SKR_Mini_E3_v2.cfg b/user_templates/mcu_defaults/main/BTT_SKR_Mini_E3_v2.cfg index 024815f75..19c2844c1 100644 --- a/user_templates/mcu_defaults/main/BTT_SKR_Mini_E3_v2.cfg +++ b/user_templates/mcu_defaults/main/BTT_SKR_Mini_E3_v2.cfg @@ -22,6 +22,7 @@ aliases: X_STOP=MCU_XSTOP , Y_STOP=MCU_YSTOP , Z_STOP=MCU_ZSTOP , RUNOUT_SENSOR=MCU_E0STOP , PROBE_INPUT=MCU_PROBE , + SERVO_PIN=MCU_SERVOS , E_HEATER=MCU_HE0 , E_TEMPERATURE=MCU_TH0 , BED_HEATER=MCU_BED , BED_TEMPERATURE=MCU_TB , diff --git a/user_templates/mcu_defaults/main/BTT_SKR_Mini_E3_v3.cfg b/user_templates/mcu_defaults/main/BTT_SKR_Mini_E3_v3.cfg index 980c9a3d9..f03eed60f 100644 --- a/user_templates/mcu_defaults/main/BTT_SKR_Mini_E3_v3.cfg +++ b/user_templates/mcu_defaults/main/BTT_SKR_Mini_E3_v3.cfg @@ -22,6 +22,7 @@ aliases: X_STOP=MCU_XSTOP , Y_STOP=MCU_YSTOP , Z_STOP=MCU_ZSTOP , RUNOUT_SENSOR=MCU_E0STOP , PROBE_INPUT=MCU_PROBE , + SERVO_PIN=MCU_SERVOS , E_HEATER=MCU_HE0 , E_TEMPERATURE=MCU_TH0 , BED_HEATER=MCU_BED , BED_TEMPERATURE=MCU_THB , diff --git a/user_templates/mcu_defaults/main/BTT_SKR_Pico_v1.0.cfg b/user_templates/mcu_defaults/main/BTT_SKR_Pico_v1.0.cfg index abe07163e..ceeaba930 100644 --- a/user_templates/mcu_defaults/main/BTT_SKR_Pico_v1.0.cfg +++ b/user_templates/mcu_defaults/main/BTT_SKR_Pico_v1.0.cfg @@ -12,10 +12,10 @@ serial: /dev/serial/by-id/change-me-to-the-correct-mcu-path [board_pins xye_SKR_mcu] mcu: mcu aliases: - X_STEP=MCU_X_STEP , X_DIR=MCU_X_DIR , X_ENABLE=MCU_X_EN , - Y_STEP=MCU_Y_STEP , Y_DIR=MCU_Y_DIR , Y_ENABLE=MCU_Y_EN , - Z_STEP=MCU_Z_STEP , Z_DIR=MCU_Z_DIR , Z_ENABLE=MCU_Z_EN , - E_STEP=MCU_E_STEP , E_DIR=MCU_E_DIR , E_ENABLE=MCU_E_EN , + X_STEP=MCU_X_STEP , X_DIR=MCU_X_DIR , X_ENABLE=MCU_X_EN , + Y_STEP=MCU_Y_STEP , Y_DIR=MCU_Y_DIR , Y_ENABLE=MCU_Y_EN , + Z_STEP=MCU_Z_STEP , Z_DIR=MCU_Z_DIR , Z_ENABLE=MCU_Z_EN , + E_STEP=MCU_E0_STEP , E_DIR=MCU_E0_DIR , E_ENABLE=MCU_E0_EN , TMCUART=MCU_TMCUART , TMCTX=MCU_TMCTX , diff --git a/user_templates/mcu_defaults/main/Fysetc_Spider_v3.x.cfg b/user_templates/mcu_defaults/main/Fysetc_Spider_v3.x.cfg new file mode 100644 index 000000000..0069a89ff --- /dev/null +++ b/user_templates/mcu_defaults/main/Fysetc_Spider_v3.x.cfg @@ -0,0 +1,52 @@ + +#---------------------------------------------# +#### Fysetc Spider v3.x MCU definition ######## +#---------------------------------------------# + +[mcu] +##-------------------------------------------------------------------- +# This board work by using a serial connection by default. If you +# want to use CAN, invert the commented lines and use canbus_uuid. + +serial: /dev/serial/by-id/change-me-to-the-correct-mcu-path +# canbus_uuid: change-me-to-the-correct-canbus-id +##-------------------------------------------------------------------- + +[include config/mcu_definitions/main/Fysetc_Spider_v3.x.cfg] # Do not remove this line +[board_pins spider_mcu] +mcu: mcu +aliases: + X_STEP=MCU_X_MOT_STEP , X_DIR=MCU_X_MOT_DIR , X_ENABLE=MCU_X_MOT_ENABLE , X_TMCUART=MCU_X_MOT_CS_PDN , + Y_STEP=MCU_Y_MOT_STEP , Y_DIR=MCU_Y_MOT_DIR , Y_ENABLE=MCU_Y_MOT_ENABLE , Y_TMCUART=MCU_Y_MOT_CS_PDN , + + Z_STEP=MCU_Z_MOT_STEP , Z_DIR=MCU_Z_MOT_DIR , Z_ENABLE=MCU_Z_MOT_ENABLE , Z_TMCUART=MCU_Z_MOT_CS_PDN , + Z1_STEP=MCU_E1_MOT_STEP , Z1_DIR=MCU_E1_MOT_DIR , Z1_ENABLE=MCU_E1_MOT_ENABLE , Z1_TMCUART=MCU_E1_MOT_CS_PDN , + Z2_STEP=MCU_E2_MOT_STEP , Z2_DIR=MCU_E2_MOT_DIR , Z2_ENABLE=MCU_E2_MOT_ENABLE , Z2_TMCUART=MCU_E2_MOT_CS_PDN , + Z3_STEP=MCU_E3_MOT_STEP , Z3_DIR=MCU_E3_MOT_DIR , Z3_ENABLE=MCU_E3_MOT_ENABLE , Z3_TMCUART=MCU_E3_MOT_CS_PDN , + + E_STEP=MCU_E0_MOT_STEP , E_DIR=MCU_E0_MOT_DIR , E_ENABLE=MCU_E0_MOT_ENABLE , E_TMCUART=MCU_E0_MOT_CS_PDN , + + DRIVER_SPI_MOSI=MCU_SPI4_MOSI , # Used in case of SPI drivers such as TMC2240 or TMC5160 + DRIVER_SPI_MISO=MCU_SPI4_MISO , # Used in case of SPI drivers such as TMC2240 or TMC5160 + DRIVER_SPI_SCK=MCU_SPI4_SCK , # Used in case of SPI drivers such as TMC2240 or TMC5160 + + X_STOP=MCU_X_MAX , Y_STOP=MCU_Y_MAX , Z_STOP=MCU_Z_MIN , + PROBE_INPUT=MCU_Z_MAX , + RUNOUT_SENSOR=MCU_Y_MIN , # If using sensorless homing: you will need to move the runout sensor on another pin + SERVO_PIN=MCU_X_MIN , # If using sensorless homing: you will need to move the servo on another pin + + E_HEATER=MCU_HEAT0 , E_TEMPERATURE=MCU_T0 , + BED_HEATER=MCU_BED_OUT , BED_TEMPERATURE=MCU_TB , + + PART_FAN=MCU_FAN1 , E_FAN=MCU_FAN0 , + CONTROLLER_FAN=MCU_FAN2 , + EXHAUST_FAN=MCU_HEAT2 , + FILTER_FAN=MCU_RGB_R , + HOST_CONTROLLER_FAN=MCU_RGB_G , + + CHAMBER_TEMPERATURE=MCU_TE1 , ELECTRICAL_CABINET_TEMPERATURE=MCU_TE2 , + + LIGHT_OUTPUT=MCU_HEAT1 , + LIGHT_NEOPIXEL=MCU_RGB_B , + STATUS_NEOPIXEL=MCU_G_DATA_5V , + diff --git a/user_templates/mcu_defaults/main/MY-OWN-CUSTOM-TEMPLATE.cfg b/user_templates/mcu_defaults/main/MY-OWN-CUSTOM-TEMPLATE.cfg new file mode 100644 index 000000000..f75465e9a --- /dev/null +++ b/user_templates/mcu_defaults/main/MY-OWN-CUSTOM-TEMPLATE.cfg @@ -0,0 +1,53 @@ + +#------------------------------------------# +#### CUSTOM TEMPLATE MCU definition ######## +#------------------------------------------# + +# This template file is a pre-filled file with Klippain pins alias conventions +# that can be used if your MCU board is not yet officially supported. Just fill +# in your MCU pins and you will be good to go :) + +[mcu] +##-------------------------------------------------------------------- +serial: /dev/serial/by-id/change-me-to-the-correct-mcu-path +# canbus_uuid: change-me-to-the-correct-canbus-id +##-------------------------------------------------------------------- + +[board_pins custom_mcu] +mcu: mcu +aliases: + X_STEP= , X_DIR= , X_ENABLE= , X_TMCUART= , + Y_STEP= , Y_DIR= , Y_ENABLE= , Y_TMCUART= , + + Z_STEP= , Z_DIR= , Z_ENABLE= , Z_TMCUART= , + Z1_STEP= , Z1_DIR= , Z1_ENABLE= , Z1_TMCUART= , + Z2_STEP= , Z2_DIR= , Z2_ENABLE= , Z2_TMCUART= , + Z3_STEP= , Z3_DIR= , Z3_ENABLE= , Z3_TMCUART= , + + E_STEP= , E_DIR= , E_ENABLE= , E_TMCUART= , + + # DRIVER_SPI_MOSI= , # Used in case of SPI drivers such as TMC2240 or TMC5160 + # DRIVER_SPI_MISO= , # Used in case of SPI drivers such as TMC2240 or TMC5160 + # DRIVER_SPI_SCK= , # Used in case of SPI drivers such as TMC2240 or TMC5160 + + X_STOP= , Y_STOP= , Z_STOP= , + PROBE_INPUT= , + RUNOUT_SENSOR= , + + E_HEATER= , E_TEMPERATURE= , + BED_HEATER= , BED_TEMPERATURE= , + + PART_FAN= , E_FAN= , + CONTROLLER_FAN= , + EXHAUST_FAN= , + FILTER_FAN= , + HOST_CONTROLLER_FAN= , + + CHAMBER_TEMPERATURE= , + ELECTRICAL_CABINET_TEMPERATURE= , + + LIGHT_OUTPUT= , + LIGHT_NEOPIXEL= , + STATUS_NEOPIXEL= , + + SERVO_PIN= , diff --git a/user_templates/mcu_defaults/toolhead/BTT_SB2209_RP2040_v1.0.cfg b/user_templates/mcu_defaults/toolhead/BTT_SB2209_RP2040_v1.0.cfg new file mode 100644 index 000000000..c8108fdb0 --- /dev/null +++ b/user_templates/mcu_defaults/toolhead/BTT_SB2209_RP2040_v1.0.cfg @@ -0,0 +1,77 @@ + +#---------------------------------------------------------# +#### BTT EBB SB2209 CAN v1.0 RP2040 MCU definition ######## +#---------------------------------------------------------# + +[mcu toolhead] +##-------------------------------------------------------------------- +canbus_uuid: change-me-to-the-correct-canbus-id +##-------------------------------------------------------------------- + +# If you want to override the wiring of the BTT EBB SB2209 CAN v1.0 RP2040, keep in mind that this +# board is defined using the "toolhead" name. So you should use "pin: toolhead:PIN_NAME" +# in your own overrides.cfg files. + +[include config/mcu_definitions/toolhead/BTT_SB2209_RP2040_v1.0.cfg] # Do not remove this line +[board_pins sb2040can_mcu] +mcu: toolhead +aliases: + E_STEP=MCU_E0_STEP , E_DIR=MCU_E0_DIR , E_ENABLE=MCU_E0_EN , E_TMCUART=MCU_E0_UART , + + X_STOP=MCU_ENDSTOP , + PROBE_INPUT=MCU_IND_FAN , + TOOLHEAD_SENSOR=MCU_PROBE2 , + + E_HEATER=MCU_HE0 , E_TEMPERATURE=MCU_TH0 , CHAMBER_TEMPERATURE=MCU_ONBOARD_NTCK100K , + + PART_FAN=MCU_FAN2_PWM , E_FAN=MCU_FAN1_PWM , + + STATUS_NEOPIXEL=MCU_RGB , + + ADXL_CS=MCU_ADXL345_CS , ADXL_SCLK=MCU_ADXL345_CLK , ADXL_MOSI=MCU_ADXL345_MOSI , ADXL_MISO=MCU_ADXL345_MISO , + + +#------------------------------------------------------# +# BTT EBB SB2209 CAN v1.0 RP2040 pins remapping # +#------------------------------------------------------# + +# These pins overrides are automatically added when you select a CANbus +# toolhead MCU during the installation process. They should provide a +# good base to work with. Feel free to adapt to your board if needed! + +[extruder] +step_pin: toolhead:E_STEP +dir_pin: toolhead:E_DIR +enable_pin: !toolhead:E_ENABLE +heater_pin: toolhead:E_HEATER +sensor_pin: toolhead:E_TEMPERATURE +## For PT100/PT1000 +# sensor_type: MAX31865 +# sensor_pin: toolhead:MCU_31865_CS +# spi_software_sclk_pin: toolhead:MCU_31865_CLK +# spi_software_mosi_pin: toolhead:MCU_31865_MOSI +# spi_software_miso_pin: toolhead:MCU_31865_MISO +# rtd_nominal_r: 100 +# rtd_reference_r: 430 +# rtd_num_of_wires: 2 + +[probe] +pin: ^toolhead:PROBE_INPUT + +[fan] +pin: toolhead:PART_FAN + +[heater_fan hotend_fan] +pin: toolhead:E_FAN + +## Uncomment the following line if not using sensorless homing +## and having the X endstop plugged to the toolhead MCU +# [stepper_x] +# endstop_pin: ^toolhead:X_STOP + +[neopixel status_leds] +pin: toolhead:STATUS_NEOPIXEL + +[tmc2209 extruder] +uart_pin: toolhead:E_TMCUART + diff --git a/user_templates/printer.cfg b/user_templates/printer.cfg index 81941b6d6..e790e3cc0 100644 --- a/user_templates/printer.cfg +++ b/user_templates/printer.cfg @@ -31,7 +31,9 @@ # [include config/hardware/axis/Y/0.9deg.cfg] ### Z axis ------------------------------------------------------------------------------- # [include config/hardware/axis/Z/V2.4_stock_1.8deg.cfg] + # [include config/hardware/axis/Z/V2.4_galileoZ_1.8deg.cfg] +# [include config/hardware/axis/Z/V2.4_galileo2Z_1.8deg.cfg] # [include config/hardware/axis/Z/Trident_TR8x8_1.8deg.cfg] # [include config/hardware/axis/Z/Trident_TR8x4_1.8deg.cfg] @@ -66,6 +68,7 @@ # [include config/hardware/extruder/cw1.cfg] # [include config/hardware/extruder/cw2.cfg] # [include config/hardware/extruder/galileo.cfg] +# [include config/hardware/extruder/galileo2.cfg] # [include config/hardware/extruder/lgx_heavy.cfg] # [include config/hardware/extruder/lgx_lite.cfg] # [include config/hardware/extruder/orbiter2.0.cfg] @@ -75,7 +78,8 @@ # ------------------------------------------------------------------------ HEATED BED ---- ### -------------------------------------------------------------------------------------- -# [include config/hardware/heated_bed.cfg] +# [include config/hardware/bed_heaters/keenovo.cfg] # Used for most machines with NTC100K bed temperature sensors +# [include config/hardware/bed_heaters/creality.cfg] # For machines with EPCOS 100k sensors like creality, etc... # ---------------------------------------------------------------------------------------- @@ -165,6 +169,8 @@ ### -------------------------------------------------------------------------------------- # [include config/hardware/accelerometers/adxl345_rpi.cfg] # For ADXL plugged directly on the Pi (official and recommended way) # [include config/hardware/accelerometers/adxl345_usb.cfg] # For KUBSA, ... +# [include config/hardware/accelerometers/adxl345_usb_rampon.cfg] # For KUBSA with Rampon firmware, ... +# [include config/hardware/accelerometers/adxl345_usb_rp2040_spi1.cfg] # For Fysetc PortableInputShaper, ... # [include config/hardware/accelerometers/adxl345_skr.cfg] # For ADXL plugged in SKRv1.4 (not a conventional way) # [include config/hardware/accelerometers/adxl345_sb2040.cfg] # For ADXL plugged in Mellow Fly-SB2040 boards diff --git a/user_templates/variables.cfg b/user_templates/variables.cfg index 392a14fef..c6f0468ed 100644 --- a/user_templates/variables.cfg +++ b/user_templates/variables.cfg @@ -187,7 +187,7 @@ variable_ercf_reset_stats_on_start_print: False ################################################ ## This section is only considered if a filter is available (and enabled) -variable_filter_default_time_on_end_print = 600 # seconds +variable_filter_default_time_on_end_print: 600 # seconds ################################################ # Other hardware options used in the macros