From cc6736c3e35cb6f6e660d973be67ab4cef78ffb9 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Wed, 7 May 2025 00:06:36 +0200 Subject: [PATCH] kinematics: Generic Cartesian kinematics implementation (#6815) * tests: Added a regression test for generic_cartesian kinematics * kinematics: An intial implementation of generic_cartesian kinematics * generic_cartesian: Refactored kinematics configuration API * generic_cartesian: Use stepper instead of kinematic_stepper in configs * generic_cartesian: Added SET_STEPPER_KINEMATICS command * generic_cartesian: Fixed parsing of section names * docs: Generic Caretsian kinematics documentation and config samples * generic_cartesian: Implemented multi-mcu homing validation * generic_cartesian: Fixed typos in docs, minor fixes * generic_cartesian: Renamed `kinematics` option to `carriages` * generic_cartesian: Moved kinematic_stepper.py file * idex_modes: Internal refactoring of handling dual carriages * stepper: Refactored the code to not store a reference to config object * config: Updated example-generic-cartesian config * generic_cartesian: Restricted SET_STEPPER_CARRIAGES and exported status * idex_modes: Fixed handling stepper kinematics with input shaper enabled * config: Updated configs and tests for SET_DUAL_CARRIAGE new params * generic_cartesian: Avoid inheritance in the added classes Signed-off-by: Dmitry Butyugin --- config/example-generic-caretesian.cfg | 138 ++++++++++ config/sample-corexyuv.cfg | 177 ++++++++++++ docs/Config_Reference.md | 235 +++++++++++++++- docs/G-Codes.md | 59 +++- docs/Status_Reference.md | 6 + klippy/chelper/__init__.py | 9 +- klippy/chelper/kin_generic.c | 52 ++++ klippy/chelper/kin_idex.c | 1 + klippy/extras/endstop_phase.py | 4 +- klippy/extras/homing.py | 10 +- klippy/extras/manual_stepper.py | 2 +- klippy/extras/probe.py | 3 + klippy/kinematics/cartesian.py | 23 +- klippy/kinematics/deltesian.py | 4 +- klippy/kinematics/generic_cartesian.py | 358 +++++++++++++++++++++++++ klippy/kinematics/hybrid_corexy.py | 17 +- klippy/kinematics/hybrid_corexz.py | 17 +- klippy/kinematics/idex_modes.py | 294 ++++++++++++-------- klippy/kinematics/kinematic_stepper.py | 92 +++++++ klippy/kinematics/polar.py | 2 +- klippy/kinematics/rotary_delta.py | 6 +- klippy/mathutil.py | 15 ++ klippy/stepper.py | 77 +++--- test/klippy/corexyuv.cfg | 172 ++++++++++++ test/klippy/corexyuv.test | 52 ++++ test/klippy/generic_cartesian.cfg | 165 ++++++++++++ test/klippy/generic_cartesian.test | 64 +++++ 27 files changed, 1855 insertions(+), 199 deletions(-) create mode 100644 config/example-generic-caretesian.cfg create mode 100644 config/sample-corexyuv.cfg create mode 100644 klippy/chelper/kin_generic.c create mode 100644 klippy/kinematics/generic_cartesian.py create mode 100644 klippy/kinematics/kinematic_stepper.py create mode 100644 test/klippy/corexyuv.cfg create mode 100644 test/klippy/corexyuv.test create mode 100644 test/klippy/generic_cartesian.cfg create mode 100644 test/klippy/generic_cartesian.test diff --git a/config/example-generic-caretesian.cfg b/config/example-generic-caretesian.cfg new file mode 100644 index 00000000..2d6b3994 --- /dev/null +++ b/config/example-generic-caretesian.cfg @@ -0,0 +1,138 @@ +# This file is an example config file for cartesian style printers. +# One may copy and edit this file to configure a new printer with +# a generic cartesian kinematics. + +# DO NOT COPY THIS FILE WITHOUT CAREFULLY READING AND UPDATING IT +# FIRST. Incorrectly configured parameters may cause damage. + +# See docs/Config_Reference.md for a description of parameters. + +[carriage x] +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: ^PE5 + +[carriage y] +position_endstop: 0 +position_max: 200 +homing_speed: 50 +endstop_pin: ^PJ1 + +[extra_carriage y1] +primary_carriage: y +endstop_pin: ^PB6 + +[carriage z] +position_endstop: 0.5 +position_max: 100 +endstop_pin: ^PD3 + +[dual_carriage u] +primary_carriage: x +position_endstop: 300 +position_max: 300 +homing_speed: 50 +endstop_pin: ^PE4 + +[stepper my_stepper_x] +carriages: x+y +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 + +[stepper my_stepper_u] +carriages: u-y1 +step_pin: PH1 +dir_pin: PH0 +enable_pin: !PA1 +microsteps: 16 +rotation_distance: 40 + +[stepper my_stepper_y0] +carriages: y +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 + +[stepper my_stepper_y1] +carriages: y1 +step_pin: PE3 +dir_pin: !PH6 +enable_pin: !PG5 +microsteps: 16 +rotation_distance: 40 + +[stepper my_stepper_z0] +carriages: z +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 + +[stepper my_stepper_z1] +carriages: z +step_pin: PG1 +dir_pin: PG0 +enable_pin: !PH3 +microsteps: 16 +rotation_distance: 8 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[extruder1] +step_pin: PC1 +dir_pin: PC3 +enable_pin: !PC7 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK7 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[heater_bed] +heater_pin: PH5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK6 +control: watermark +min_temp: 0 +max_temp: 110 + +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: generic_cartesian +max_velocity: 500 +max_accel: 3000 +max_z_velocity: 20 +max_z_accel: 100 diff --git a/config/sample-corexyuv.cfg b/config/sample-corexyuv.cfg new file mode 100644 index 00000000..368b6464 --- /dev/null +++ b/config/sample-corexyuv.cfg @@ -0,0 +1,177 @@ +# This file contains a configuration snippet for a CoreXYUV +# printer with an independent dual extruder moving over X and Y axes. + +# See docs/Config_Reference.md for a description of parameters. + +[carriage x] +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: ^PE5 + +[carriage y] +position_endstop: 0 +position_max: 200 +homing_speed: 50 +endstop_pin: ^PJ1 + +[dual_carriage u] +primary_carriage: x +safe_distance: 70 +position_endstop: 300 +position_max: 300 +homing_speed: 50 +endstop_pin: ^PE4 + +[dual_carriage v] +primary_carriage: y +safe_distance: 50 +position_endstop: 200 +position_max: 200 +homing_speed: 50 +endstop_pin: ^PD4 + +[stepper a] +carriages: x+y +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 + +[stepper b] +carriages: u-v +step_pin: PH1 +dir_pin: PH0 +enable_pin: !PA1 +microsteps: 16 +rotation_distance: 40 + +[stepper c] +carriages: x-y +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 + +[stepper d] +carriages: u+v +step_pin: PE3 +dir_pin: !PH6 +enable_pin: !PG5 +microsteps: 16 +rotation_distance: 40 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[gcode_macro PARK_extruder] +gcode: + SET_DUAL_CARRIAGE CARRIAGE=x + SET_DUAL_CARRIAGE CARRIAGE=y + G90 + G1 X0 Y0 + +[gcode_macro T0] +gcode: + PARK_{printer.toolhead.extruder} + ACTIVATE_EXTRUDER EXTRUDER=extruder + SET_DUAL_CARRIAGE CARRIAGE=x + SET_DUAL_CARRIAGE CARRIAGE=y + +[extruder1] +step_pin: PC1 +dir_pin: PC3 +enable_pin: !PC7 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK7 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[gcode_macro PARK_extruder1] +gcode: + SET_DUAL_CARRIAGE CARRIAGE=u + SET_DUAL_CARRIAGE CARRIAGE=v + G90 + G1 X300 Y200 + +[gcode_macro T1] +gcode: + PARK_{printer.toolhead.extruder} + ACTIVATE_EXTRUDER EXTRUDER=extruder1 + SET_DUAL_CARRIAGE CARRIAGE=u + SET_DUAL_CARRIAGE CARRIAGE=v + +# A helper script to activate copy mode +[gcode_macro ACTIVATE_COPY_MODE] +gcode: + SET_DUAL_CARRIAGE CARRIAGE=x MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=y MODE=PRIMARY + G1 X0 Y0 + ACTIVATE_EXTRUDER EXTRUDER=extruder + SET_DUAL_CARRIAGE CARRIAGE=u MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=v MODE=PRIMARY + G1 X150 Y100 + SET_DUAL_CARRIAGE CARRIAGE=u MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=v MODE=COPY + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + +# A helper script to activate mirror mode +[gcode_macro ACTIVATE_MIRROR_MODE] +gcode: + SET_DUAL_CARRIAGE CARRIAGE=x MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=y MODE=PRIMARY + G1 X0 Y0 + ACTIVATE_EXTRUDER EXTRUDER=extruder + SET_DUAL_CARRIAGE CARRIAGE=u MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=v MODE=PRIMARY + G1 X300 Y100 + SET_DUAL_CARRIAGE CARRIAGE=u MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=v MODE=COPY + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + +[printer] +kinematics: generic_cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 + +## An optional input shaper support +#[input_shaper] +## The section is intentionally empty +# +#[delayed_gcode init_shaper] +#initial_duration: 0.1 +#gcode: +# SET_DUAL_CARRIAGE CARRIAGE=u +# SET_DUAL_CARRIAGE CARRIAGE=v +# SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= +# SET_DUAL_CARRIAGE CARRIAGE=x MODE=PRIMARY +# SET_DUAL_CARRIAGE CARRIAGE=y MODE=PRIMARY +# SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index e65b79bf..f9ef676b 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -84,8 +84,9 @@ The printer section controls high level printer settings. [printer] kinematics: # The type of printer in use. This option may be one of: cartesian, -# corexy, corexz, hybrid_corexy, hybrid_corexz, rotary_delta, delta, -# deltesian, polar, winch, or none. This parameter must be specified. +# corexy, corexz, hybrid_corexy, hybrid_corexz, generic_cartesian, +# rotary_delta, delta, deltesian, polar, winch, or none. +# This parameter must be specified. max_velocity: # Maximum velocity (in mm/s) of the toolhead (relative to the # print). This value may be changed at runtime using the @@ -712,6 +713,172 @@ anchor_z: # These parameters must be provided. ``` +### Generic Cartesian Kinematics + +See [example-generic-cartesian.cfg](../config/example-generic-caretesian.cfg) +for an example generic Cartesian kinematics config file. + +This printer kinematic class allows a user to define in a pretty flexible +manner an arbitrary Cartesian-style kinematics. In principle, the regular +cartesian, corexy, hybrid_corexy can be defined this way too. However, +more importantly, various otherwise unsupported kinematics such as +inverted hybrid_corexy or corexyuv can be defined using this kinematic. + +Notably, the definition of a generic Cartesian kinematic deviates +significantly from the other kinematic types. It follows the following +convention: a user defines a set of carriages with certain range of motion +that can move independently from each other (they should move over the +Cartesian axes X, Y, and Z, hence the name of the kinematic) and +corresponding endstops that allow the firmware to determine the position +of carriages during homing, as well as a set of steppers that move those +carriages. The `[printer]` section must specify the kinematic and +other printer-level settings same as the regular Cartesian kinematic: +``` +[printer] +kinematics: generic_cartesian +max_velocity: +max_accel: +#minimum_cruise_ratio: +#square_corner_velocity: +#max_accel_to_decel: +#max_z_velocity: +#max_z_accel: + +``` + +Then a user must define the following three carriages: `[carriage x]`, +`[carriage y]`, and `[carriage z]`, e.g. +``` +[carriage x] +endstop_pin: +# Endstop switch detection pin. If this endstop pin is on a +# different mcu than the stepper motor(s) moving this carriage, +# then it enables "multi-mcu homing". This parameter must be provided. +#position_min: 0 +# Minimum valid distance (in mm) the user may command the carriage to +# move to. The default is 0mm. +position_endstop: +# Location of the endstop (in mm). This parameter must be provided. +position_max: +# Maximum valid distance (in mm) the user may command the stepper to +# move to. This parameter must be provided. +#homing_speed: 5.0 +# Maximum velocity (in mm/s) of the carriage when homing. The default +# is 5mm/s. +#homing_retract_dist: 5.0 +# Distance to backoff (in mm) before homing a second time during +# homing. Set this to zero to disable the second home. The default +# is 5mm. +#homing_retract_speed: +# Speed to use on the retract move after homing in case this should +# be different from the homing speed, which is the default for this +# parameter +#second_homing_speed: +# Velocity (in mm/s) of the carriage when performing the second home. +# The default is homing_speed/2. +#homing_positive_dir: +# If true, homing will cause the carriage to move in a positive +# direction (away from zero); if false, home towards zero. It is +# better to use the default than to specify this parameter. The +# default is true if position_endstop is near position_max and false +# if near position_min. +``` + +Afterwards, a user specifies the stepper motors that move these carriages, +for instance +``` +[stepper my_stepper] +carriages: +# A string describing the carriages the stepper moves. All defined +# carriages can be specified here, as well as their linear combinations, +# e.g. x, x+y, y-0.5*z, x-z, etc. This parameter must be provided. +step_pin: +dir_pin: +enable_pin: +rotation_distance: +microsteps: +#full_steps_per_rotation: 200 +#gear_ratio: +#step_pulse_duration: +``` +See [stepper](#stepper) section for more information on the regular +stepper parameters. The `carriages` parameter defines how the stepper +affects the motion of the carriages. For example, `x+y` indicates that +the motion of the stepper in the positive direction by the distance `d` +moves the carriages `x` and `y` by the same distance `d` in the positive +direction, while `x-0.5*y` means the motion of the stepper in the positive +direction by the distance `d` moves the carriage `x` by the distance `d` +in the positive direction, but the carriage `y` will travel distance `d/2` +in the negative direction. + +More than a single stepper motor can be defined to drive the same axis +or belt. For example, on a CoreXY AWD setups two motors driving the same +belt can be defined as +``` +[carriage x] +endstop_pin: ... +... + +[carriage y] +endstop_pin: ... +... + +[stepper a0] +carriages: x-y +step_pin: ... +dir_pin: ... +enable_pin: ... +rotation_distance: ... +... + +[stepper a1] +carriages: x-y +step_pin: ... +dir_pin: ... +enable_pin: ... +rotation_distance: ... +... +``` +with `a0` and `a1` steppers having their own control pins, but +sharing the same `carriages` and corresponding endstops. + +There are situations when a user wants to have more than one endstop +per axis. Examples of such configurations include Y axis driven by +two independent stepper motors with belts attached to both ends of the +X beam, with effectively two carriages on Y axis each having an +independent endstop, and multi-stepper Z axis with each stepper having +its own endstop (not to be confused with the configurations with +multiple Z motors but only a single endstop). These configurations +can be declared by specifying additional carriage(s) with their endstops: + +``` +[extra_carriage my_carriage] +primary_carriage: +# The name of the primary carriage this carriage corresponds to. +# It also effectively defines the axis the carriage moves over. +# This parameter must be provided. +endstop_pin: +# Endstop switch detection pin. This parameter must be provided. +``` + +and the corresponding stepper motors, for example: +``` +[extra_carriage y1] +primary_carriage: y +endstop_pin: ... + +[stepper sy1] +carriages: y1 +... +``` +Notably, an `[extra_carriage]` does not define parameters such as +`position_min`, `position_max`, and `position_endstop`, but instead +inherits them from the specified `primary_carriage`, thus sharing +the same range of motion with the primary carriage. + +For the references on how to configure IDEX setups, see the +[dual carriage](#dual-carriage) section. + ### None Kinematics It is possible to define a special "none" kinematics to disable @@ -2207,8 +2374,8 @@ for an example configuration. ### [dual_carriage] -Support for cartesian and hybrid_corexy/z printers with dual carriages -on a single axis. The carriage mode can be set via the +Support for cartesian, generic_cartesian and hybrid_corexy/z printers with +dual carriages on a single axis. The carriage mode can be set via the SET_DUAL_CARRIAGE extended g-code command. For example, "SET_DUAL_CARRIAGE CARRIAGE=1" command will activate the carriage defined in this section (CARRIAGE=0 will return activation to the primary carriage). @@ -2235,7 +2402,7 @@ typically be achieved with or a similar command. See [sample-idex.cfg](../config/sample-idex.cfg) for an example -configuration. +configuration with a regular Cartesian kinematic. ``` [dual_carriage] @@ -2249,7 +2416,7 @@ axis: # error. If safe_distance is not provided, it will be inferred from # position_min and position_max for the dual and primary carriages. If set # to 0 (or safe_distance is unset and position_min and position_max are -# identical for the primary and dual carraiges), the carriages proximity +# identical for the primary and dual carriages), the carriages proximity # checks will be disabled. #step_pin: #dir_pin: @@ -2263,6 +2430,62 @@ axis: # See the "stepper" section for the definition of the above parameters. ``` +For an example of dual carriage configuration with `generic_cartesian` +kinematic, see the following configuration +[sample](../config/example-generic-caretesian.cfg). +Please note that in this case the `[dual_carriage]` configuration deviates +from the configuration described above: +``` +[dual_carriage my_dc_carriage] +primary_carriage: +# Defines the matching primary carriage of this dual carriage and +# the corresponding IDEX axis. Valid choices are x, y, z. +# This parameter must be provided. +#safe_distance: +# The minimum distance (in mm) to enforce between the dual and the primary +# carriages. If a G-Code command is executed that will bring the carriages +# closer than the specified limit, such a command will be rejected with an +# error. If safe_distance is not provided, it will be inferred from +# position_min and position_max for the dual and primary carriages. If set +# to 0 (or safe_distance is unset and position_min and position_max are +# identical for the primary and dual carriages), the carriages proximity +# checks will be disabled. +endstop_pin: +#position_min: +position_endstop: +position_max: +#homing_speed: +#homing_retract_dist: +#homing_retract_speed: +#second_homing_speed: +#homing_positive_dir: +... +``` +Refer to [generic cartesian](#generic-cartesian) section for more information +on the regular `carriage` parameters. + +Then a user must define one or more stepper motors moving the dual carriage +(and other carriages as appropriate), for instance +``` +[carriage x] +... + +[carriage y] +... + +[dual_carriage u] +primary_carriage: x +... + +[stepper dc_stepper] +carriages: u-y +... +``` + +It is worth noting that `generic_cartesian` kinematic can support two +dual carriages for X and Y axes. For reference, see for instance a +[sample](../config/sample-corexyuv.cfg) of CoreXYUV configuration. + ### [extruder_stepper] Support for additional steppers synchronized to the movement of an diff --git a/docs/G-Codes.md b/docs/G-Codes.md index ae3ebc2b..b8a0ce69 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -341,15 +341,18 @@ The following command is available when the enabled. #### SET_DUAL_CARRIAGE -`SET_DUAL_CARRIAGE CARRIAGE=[0|1] [MODE=[PRIMARY|COPY|MIRROR]]`: +`SET_DUAL_CARRIAGE CARRIAGE= [MODE=[PRIMARY|COPY|MIRROR]]`: This command will change the mode of the specified carriage. -If no `MODE` is provided it defaults to `PRIMARY`. Setting the mode -to `PRIMARY` deactivates the other carriage and makes the specified -carriage execute subsequent G-Code commands as-is. `COPY` and `MIRROR` -modes are supported only for `CARRIAGE=1`. When set to either of these -modes, carriage 1 will then track the subsequent moves of the carriage 0 -and either copy relative movements of it (in `COPY` mode) or execute them -in the opposite (mirror) direction (in `MIRROR` mode). +If no `MODE` is provided it defaults to `PRIMARY`. `` must +reference a defined primary or dual carriage for `generic_cartesian` +kinematics or be 0 (for primary carriage) or 1 (for dual carriage) +for all other kinematics supporting IDEX. Setting the mode to `PRIMARY` +deactivates the other carriage and makes the specified carriage execute +subsequent G-Code commands as-is. `COPY` and `MIRROR` modes are supported +only for dual carriages. When set to either of these modes, dual carriage +will then track the subsequent moves of its primary carriage and either +copy relative movements of it (in `COPY` mode) or execute them in the +opposite (mirror) direction (in `MIRROR` mode). #### SAVE_DUAL_CARRIAGE_STATE `SAVE_DUAL_CARRIAGE_STATE [NAME=]`: Save the current positions @@ -715,6 +718,46 @@ is specified then the toolhead move will be performed with the given speed (in mm/s); otherwise the toolhead move will use the restored g-code speed. +### [generic_cartesian] +The commands in this section become automatically available when +`kinematics: generic_cartesian` is specified as the printer kinematics. + +#### SET_STEPPER_CARRIAGES +`SET_STEPPER_CARRIAGES STEPPER= CARRIAGES= +[DISABLE_CHECKS=[0|1]]`: Set or update the stepper carriages. +`` must reference an existing stepper defined in `printer.cfg`, +and `` describes the carriages the stepper moves. See +[Generic Cartesian Kinematics](Config_Reference.md#generic-cartesian-kinematics) +for a more detailed overview of the `carriages` parameter in the +stepper configuration section. Note that it is only possible +to change the coefficients or signs of the carriages with this +command, but a user cannot add or remove the carriages that the stepper +controls. + +`SET_STEPPER_CARRIAGES` is an advanced tool, and the user is advised +to exercise an extreme caution using it, since specifying incorrect +configuration may physically damage the printer. + +Note that `SET_STEPPER_CARRIAGES` performs certain internal validations +of the new printer kinematics after the change. Keep in mind that if it +detects an issue, it may leave printer kinematics in an invalid state. +This means that if `SET_STEPPER_CARRIAGES` reports an error, it is unsafe +to issue other GCode commands, and the user must inspect the error message +and either fix the problem, or manually restore the previous stepper(s) +configuration. + +Since `SET_STEPPER_CARRIAGES` can update a configuration of a single +stepper at a time, some sequences of changes can lead to invalid +intermediate kinematic configurations, even if the final configuration +is valid. In such cases a user can pass `DISABLE_CHECKS=1` parameters to +all but the last command to disable intermediate checks. For example, +if `stepper a` and `stepper b` initially have `x-y` and `x+y` carriages +correspondingly, then the following sequence of commands will let a user +effectively swap the carriage controls: +`SET_STEPPER_CARRIAGES STEPPER=a CARRIAGES=x+y DISABLE_CHECKS=1` +and `SET_STEPPER_CARRIAGES STEPPER=b CARRIAGES=x-y`, while +still validating the final kinematics state. + ### [hall_filament_width_sensor] The following commands are available when the diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index e8f7f2c0..0276c413 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -570,6 +570,12 @@ on a cartesian, hybrid_corexy or hybrid_corexz robot - `carriage_1`: The mode of the carriage 1. Possible values are: "INACTIVE", "PRIMARY", "COPY", and "MIRROR". +On a `generic_cartesian` kinematic, the following information is +available in `dual_carriage`: +- `carriages[""]`: The mode of the carriage ``. Possible + values are "INACTIVE" and "PRIMARY" for the primary carriage and "INACTIVE", + "PRIMARY", "COPY", and "MIRROR" for the dual carriage. + ## virtual_sdcard The following information is available in the diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index fa1261be..9fe64dd6 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -21,7 +21,7 @@ SOURCE_FILES = [ 'pollreactor.c', 'msgblock.c', 'trdispatch.c', 'kin_cartesian.c', 'kin_corexy.c', 'kin_corexz.c', 'kin_delta.c', 'kin_deltesian.c', 'kin_polar.c', 'kin_rotary_delta.c', 'kin_winch.c', - 'kin_extruder.c', 'kin_shaper.c', 'kin_idex.c', + 'kin_extruder.c', 'kin_shaper.c', 'kin_idex.c', 'kin_generic.c' ] DEST_LIB = "c_helper.so" OTHER_FILES = [ @@ -106,6 +106,12 @@ defs_trapq = """ defs_kin_cartesian = """ struct stepper_kinematics *cartesian_stepper_alloc(char axis); """ +defs_kin_generic_cartesian = """ + struct stepper_kinematics *generic_cartesian_stepper_alloc(double a_x + , double a_y, double a_z); + void generic_cartesian_stepper_set_coeffs(struct stepper_kinematics *sk + , double a_x, double a_y, double a_z); +""" defs_kin_corexy = """ struct stepper_kinematics *corexy_stepper_alloc(char type); @@ -224,6 +230,7 @@ defs_all = [ defs_kin_cartesian, defs_kin_corexy, defs_kin_corexz, defs_kin_delta, defs_kin_deltesian, defs_kin_polar, defs_kin_rotary_delta, defs_kin_winch, defs_kin_extruder, defs_kin_shaper, defs_kin_idex, + defs_kin_generic_cartesian, ] # Update filenames to an absolute path diff --git a/klippy/chelper/kin_generic.c b/klippy/chelper/kin_generic.c new file mode 100644 index 00000000..e4077cdd --- /dev/null +++ b/klippy/chelper/kin_generic.c @@ -0,0 +1,52 @@ +// Generic cartesian kinematics stepper position calculation +// +// Copyright (C) 2024 Dmitry Butyugin +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include // offsetof +#include // malloc +#include // memset +#include "compiler.h" // __visible +#include "itersolve.h" // struct stepper_kinematics +#include "trapq.h" // move_get_coord + +struct generic_cartesian_stepper { + struct stepper_kinematics sk; + struct coord a; +}; + +static double +generic_cartesian_stepper_calc_position(struct stepper_kinematics *sk + , struct move *m, double move_time) +{ + struct generic_cartesian_stepper *cs = container_of( + sk, struct generic_cartesian_stepper, sk); + struct coord c = move_get_coord(m, move_time); + return cs->a.x * c.x + cs->a.y * c.y + cs->a.z * c.z; +} + +void __visible +generic_cartesian_stepper_set_coeffs(struct stepper_kinematics *sk + , double a_x, double a_y, double a_z) +{ + struct generic_cartesian_stepper *cs = container_of( + sk, struct generic_cartesian_stepper, sk); + cs->a.x = a_x; + cs->a.y = a_y; + cs->a.z = a_z; + cs->sk.active_flags = 0; + if (a_x) cs->sk.active_flags |= AF_X; + if (a_y) cs->sk.active_flags |= AF_Y; + if (a_z) cs->sk.active_flags |= AF_Z; +} + +struct stepper_kinematics * __visible +generic_cartesian_stepper_alloc(double a_x, double a_y, double a_z) +{ + struct generic_cartesian_stepper *cs = malloc(sizeof(*cs)); + memset(cs, 0, sizeof(*cs)); + cs->sk.calc_position_cb = generic_cartesian_stepper_calc_position; + generic_cartesian_stepper_set_coeffs(&cs->sk, a_x, a_y, a_z); + return &cs->sk; +} diff --git a/klippy/chelper/kin_idex.c b/klippy/chelper/kin_idex.c index 7657a73d..fd0fc948 100644 --- a/klippy/chelper/kin_idex.c +++ b/klippy/chelper/kin_idex.c @@ -77,5 +77,6 @@ dual_carriage_alloc(void) struct dual_carriage_stepper *dc = malloc(sizeof(*dc)); memset(dc, 0, sizeof(*dc)); dc->m.move_t = 2. * DUMMY_T; + dc->x_scale = dc->y_scale = 1.0; return &dc->sk; } diff --git a/klippy/extras/endstop_phase.py b/klippy/extras/endstop_phase.py index 2c7468bc..689ec4fa 100644 --- a/klippy/extras/endstop_phase.py +++ b/klippy/extras/endstop_phase.py @@ -52,7 +52,7 @@ class PhaseCalc: class EndstopPhase: def __init__(self, config): self.printer = config.get_printer() - self.name = config.get_name().split()[1] + self.name = " ".join(config.get_name().split()[1:]) # Obtain step_distance and microsteps from stepper config section sconfig = config.getsection(self.name) rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig) @@ -118,7 +118,7 @@ class EndstopPhase: return delta * self.step_dist def handle_home_rails_end(self, homing_state, rails): for rail in rails: - stepper = rail.get_steppers()[0] + stepper = rail.get_endstops()[0][0].get_steppers()[0] if stepper.get_name() == self.name: trig_mcu_pos = homing_state.get_trigger_position(self.name) align = self.align_endstop(rail) diff --git a/klippy/extras/homing.py b/klippy/extras/homing.py index add209ec..723648c1 100644 --- a/klippy/extras/homing.py +++ b/klippy/extras/homing.py @@ -45,7 +45,7 @@ class StepperPosition: class HomingMove: def __init__(self, printer, endstops, toolhead=None): self.printer = printer - self.endstops = endstops + self.endstops = [es for es in endstops if es[0].get_steppers()] if toolhead is None: toolhead = printer.lookup_object('toolhead') self.toolhead = toolhead @@ -71,7 +71,9 @@ class HomingMove: sname = stepper.get_name() kin_spos[sname] += offsets.get(sname, 0) * stepper.get_step_dist() thpos = self.toolhead.get_position() - return list(kin.calc_position(kin_spos))[:3] + thpos[3:] + cpos = kin.calc_position(kin_spos) + return [cp if cp is not None else tp + for cp, tp in zip(cpos, thpos[:3])] + thpos[3:] def homing_move(self, movepos, speed, probe_pos=False, triggered=True, check_triggered=True): # Notify start of homing/probing move @@ -233,6 +235,10 @@ class Homing: for s in kin.get_steppers()} newpos = kin.calc_position(kin_spos) for axis in force_axes: + if newpos[axis] is None: + raise self.printer.command_error( + "Cannot determine position of toolhead on " + "axis %s after homing" % "xyz"[axis]) homepos[axis] = newpos[axis] self.toolhead.set_position(homepos) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 1465590d..c15ce235 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -11,7 +11,7 @@ class ManualStepper: self.printer = config.get_printer() if config.get('endstop_pin', None) is not None: self.can_home = True - self.rail = stepper.PrinterRail( + self.rail = stepper.LookupRail( config, need_position_minmax=False, default_position_endstop=0.) self.steppers = self.rail.get_steppers() else: diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index c919b983..64d5f855 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -177,6 +177,9 @@ def lookup_minimum_z(config): if config.has_section('stepper_z'): zconfig = config.getsection('stepper_z') return zconfig.getfloat('position_min', 0., note_valid=False) + elif config.has_section('carriage z'): + zconfig = config.getsection('carriage z') + return zconfig.getfloat('position_min', 0., note_valid=False) pconfig = config.getsection('printer') return pconfig.getfloat('minimum_z_position', 0., note_valid=False) diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py index 2a50a05d..0524469a 100644 --- a/klippy/kinematics/cartesian.py +++ b/klippy/kinematics/cartesian.py @@ -29,14 +29,10 @@ class CartKinematics: self.rails.append(stepper.LookupMultiRail(dc_config)) self.rails[3].setup_itersolve('cartesian_stepper_alloc', dc_axis.encode()) - dc_rail_0 = idex_modes.DualCarriagesRail( - self.rails[self.dual_carriage_axis], - axis=self.dual_carriage_axis, active=True) - dc_rail_1 = idex_modes.DualCarriagesRail( - self.rails[3], axis=self.dual_carriage_axis, active=False) self.dc_module = idex_modes.DualCarriages( - dc_config, dc_rail_0, dc_rail_1, - axis=self.dual_carriage_axis) + self.printer, [self.rails[self.dual_carriage_axis]], + [self.rails[3]], axes=[self.dual_carriage_axis], + safe_dist=config.getfloat('safe_distance', None, minval=0.)) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) toolhead.register_step_generator(s.generate_steps) @@ -52,9 +48,10 @@ class CartKinematics: def calc_position(self, stepper_positions): rails = self.rails if self.dc_module: - primary_rail = self.dc_module.get_primary_rail().get_rail() - rails = (rails[:self.dc_module.axis] + - [primary_rail] + rails[self.dc_module.axis+1:]) + primary_rail = self.dc_module.get_primary_rail( + self.dual_carriage_axis) + rails = (rails[:self.dual_carriage_axis] + + [primary_rail] + rails[self.dual_carriage_axis+1:]) return [stepper_positions[rail.get_name()] for rail in rails] def update_limits(self, i, range): l, h = self.limits[i] @@ -67,8 +64,8 @@ class CartKinematics: rail.set_position(newpos) for axis_name in homing_axes: axis = "xyz".index(axis_name) - if self.dc_module and axis == self.dc_module.axis: - rail = self.dc_module.get_primary_rail().get_rail() + if self.dc_module and axis == self.dual_carriage_axis: + rail = self.dc_module.get_primary_rail(self.dual_carriage_axis) else: rail = self.rails[axis] self.limits[axis] = rail.get_range() @@ -93,7 +90,7 @@ class CartKinematics: # Each axis is homed independently and in order for axis in homing_state.get_axes(): if self.dc_module is not None and axis == self.dual_carriage_axis: - self.dc_module.home(homing_state) + self.dc_module.home(homing_state, self.dual_carriage_axis) else: self.home_axis(homing_state, axis, self.rails[axis]) def _check_endstops(self, move): diff --git a/klippy/kinematics/deltesian.py b/klippy/kinematics/deltesian.py index 1f7ddaa0..54b013a5 100644 --- a/klippy/kinematics/deltesian.py +++ b/klippy/kinematics/deltesian.py @@ -17,10 +17,10 @@ class DeltesianKinematics: self.rails = [None] * 3 stepper_configs = [config.getsection('stepper_' + s) for s in ['left', 'right', 'y']] - self.rails[0] = stepper.PrinterRail( + self.rails[0] = stepper.LookupRail( stepper_configs[0], need_position_minmax = False) def_pos_es = self.rails[0].get_homing_info().position_endstop - self.rails[1] = stepper.PrinterRail( + self.rails[1] = stepper.LookupRail( stepper_configs[1], need_position_minmax = False, default_position_endstop = def_pos_es) self.rails[2] = stepper.LookupMultiRail(stepper_configs[2]) diff --git a/klippy/kinematics/generic_cartesian.py b/klippy/kinematics/generic_cartesian.py new file mode 100644 index 00000000..c27c68cf --- /dev/null +++ b/klippy/kinematics/generic_cartesian.py @@ -0,0 +1,358 @@ +# Code for generic handling the kinematics of cartesian-like printers +# +# Copyright (C) 2024-2025 Dmitry Butyugin +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +import copy, itertools, logging, math +import gcode, mathutil, stepper +from . import idex_modes +from . import kinematic_stepper as ks + +def mat_mul(a, b): + if len(a[0]) != len(b): + return None + res = [] + for i in range(len(a)): + res.append([]) + for j in range(len(b[0])): + res[i].append(sum(a[i][k] * b[k][j] for k in range(len(b)))) + return res + +def mat_transp(a): + res = [] + for i in range(len(a[0])): + res.append([a[j][i] for j in range(len(a))]) + return res + +def mat_pseudo_inverse(m): + mt = mat_transp(m) + mtm = mat_mul(mt, m) + pinv = mat_mul(mathutil.matrix_inv(mtm), mt) + return pinv + +class MainCarriage: + def __init__(self, config, axis): + self.rail = stepper.GenericPrinterRail(config) + self.axis = ord(axis) - ord('x') + self.axis_name = axis + self.dual_carriage = None + def get_name(self): + return self.rail.get_name() + def get_axis(self): + return self.axis + def get_rail(self): + return self.rail + def add_stepper(self, kin_stepper): + self.rail.add_stepper(kin_stepper.get_stepper()) + def is_active(self): + if self.dual_carriage is None: + return True + return self.dual_carriage.get_dc_module().is_active(self.rail) + def set_dual_carriage(self, carriage): + old_dc = self.dual_carriage + self.dual_carriage = carriage + return old_dc + def get_dual_carriage(self): + return self.dual_carriage + +class ExtraCarriage: + def __init__(self, config, carriages): + self.name = config.get_name().split()[-1] + self.primary_carriage = config.getchoice('primary_carriage', carriages) + self.endstop_pin = config.get('endstop_pin') + def get_name(self): + return self.name + def get_axis(self): + return self.primary_carriage.get_axis() + def get_rail(self): + return self.primary_carriage.get_rail() + def add_stepper(self, kin_stepper): + self.get_rail().add_stepper(kin_stepper.get_stepper(), + self.endstop_pin, self.name) + +class DualCarriage: + def __init__(self, config, carriages): + self.printer = config.get_printer() + self.rail = stepper.GenericPrinterRail(config) + self.primary_carriage = config.getchoice('primary_carriage', carriages) + if self.primary_carriage.set_dual_carriage(self) is not None: + raise config.error( + "Redefinition of dual_carriage for carriage '%s'" % + self.primary_carriage.get_name()) + self.axis = self.primary_carriage.get_axis() + if self.axis > 1: + raise config.error("Invalid axis '%s' for dual_carriage" % + self.primary_carriage.get_axis_name()) + self.safe_dist = config.getfloat('safe_distance', None, minval=0.) + def get_name(self): + return self.rail.get_name() + def get_axis(self): + return self.primary_carriage.get_axis() + def get_rail(self): + return self.rail + def get_safe_dist(self): + return self.safe_dist + def get_dc_module(self): + return self.printer.lookup_object('dual_carriage') + def is_active(self): + return self.get_dc_module().is_active(self.rail) + def get_dual_carriage(self): + return self.primary_carriage + def add_stepper(self, kin_stepper): + self.rail.add_stepper(kin_stepper.get_stepper()) + +class GenericCartesianKinematics: + def __init__(self, toolhead, config): + self.printer = config.get_printer() + self._load_kinematics(config) + for s in self.get_steppers(): + s.set_trapq(toolhead.get_trapq()) + toolhead.register_step_generator(s.generate_steps) + self.dc_module = None + if self.dc_carriages: + pcs = [dc.get_dual_carriage() for dc in self.dc_carriages] + primary_rails = [pc.get_rail() for pc in pcs] + dual_rails = [dc.get_rail() for dc in self.dc_carriages] + axes = [dc.get_axis() for dc in self.dc_carriages] + safe_dist = {dc.get_axis() : dc.get_safe_dist() + for dc in self.dc_carriages} + self.dc_module = dc_module = idex_modes.DualCarriages( + self.printer, primary_rails, dual_rails, axes, safe_dist) + zero_pos = (0., 0.) + for acs in itertools.product(*zip(pcs, self.dc_carriages)): + for c in acs: + dc_module.get_dc_rail_wrapper(c.get_rail()).activate( + idex_modes.PRIMARY, zero_pos) + dc_rail = c.get_dual_carriage().get_rail() + dc_module.get_dc_rail_wrapper(dc_rail).inactivate(zero_pos) + self._check_kinematics(config.error) + for c in pcs: + dc_module.get_dc_rail_wrapper(c.get_rail()).activate( + idex_modes.PRIMARY, zero_pos) + dc_rail = c.get_dual_carriage().get_rail() + dc_module.get_dc_rail_wrapper(dc_rail).inactivate(zero_pos) + else: + self._check_kinematics(config.error) + # Setup boundary checks + max_velocity, max_accel = toolhead.get_max_velocity() + self.max_z_velocity = config.getfloat('max_z_velocity', max_velocity, + above=0., maxval=max_velocity) + self.max_z_accel = config.getfloat('max_z_accel', max_accel, + above=0., maxval=max_accel) + self.limits = [(1.0, -1.0)] * 3 + # Register gcode commands + gcode = self.printer.lookup_object('gcode') + gcode.register_command("SET_STEPPER_CARRIAGES", + self.cmd_SET_STEPPER_CARRIAGES, + desc=self.cmd_SET_STEPPER_CARRIAGES_help) + def _load_kinematics(self, config): + carriages = {a : MainCarriage(config.getsection('carriage ' + a), a) + for a in 'xyz'} + dc_carriages = [] + for c in config.get_prefix_sections('dual_carriage '): + dc_carriages.append(DualCarriage(c, carriages)) + for dc in dc_carriages: + name = dc.get_name() + if name in carriages: + raise config.error("Redefinition of carriage %s" % name) + carriages[name] = dc + self.carriages = dict(carriages) + self.dc_carriages = dc_carriages + ec_carriages = [] + for c in config.get_prefix_sections('extra_carriage '): + ec_carriages.append(ExtraCarriage(c, carriages)) + for ec in ec_carriages: + name = ec.get_name() + if name in carriages: + raise config.error("Redefinition of carriage %s" % name) + carriages[name] = ec + self.kin_steppers = self._load_steppers(config, carriages) + self.all_carriages = carriages + self._check_carriages_references(config.error) + self._check_multi_mcu_homing(config.error) + def _check_carriages_references(self, report_error): + carriages = dict(self.all_carriages) + for s in self.kin_steppers: + for c in s.get_carriages(): + carriages.pop(c.get_name(), None) + if carriages: + raise report_error( + "Carriage(s) %s must be referenced by some " + "stepper(s)" % (", ".join(carriages),)) + def _check_multi_mcu_homing(self, report_error): + for carriage in self.carriages.values(): + for es in carriage.get_rail().get_endstops(): + stepper_mcus = set([s.get_mcu() for s in es[0].get_steppers() + if s in carriage.get_rail().get_steppers()]) + if len(stepper_mcus) > 1: + raise report_error("Multi-mcu homing not supported on" + " multi-mcu shared carriage %s" % es[1]) + def _load_steppers(self, config, carriages): + return [ks.KinematicStepper(c, carriages) + for c in config.get_prefix_sections('stepper ')] + def get_steppers(self): + return [s.get_stepper() for s in self.kin_steppers] + def get_primary_carriages(self): + carriages = [self.carriages[a] for a in "xyz"] + if self.dc_module: + for a in self.dc_module.get_axes(): + primary_rail = self.dc_module.get_primary_rail(a) + for c in self.carriages.values(): + if c.get_rail() == primary_rail: + carriages[a] = c + return carriages + def _get_kinematics_coeffs(self): + matr = {s.get_name() : list(s.get_kin_coeffs()) + for s in self.kin_steppers} + offs = {s.get_name() : [0.] * 3 for s in self.kin_steppers} + if self.dc_module is None: + return ([matr[s.get_name()] for s in self.kin_steppers], + [0. for s in self.kin_steppers]) + axes = [dc.get_axis() for dc in self.dc_carriages] + orig_matr = copy.deepcopy(matr) + for dc in self.dc_carriages: + axis = dc.get_axis() + for c in [dc.get_dual_carriage(), dc]: + m, o = self.dc_module.get_transform(c.get_rail()) + for s in c.get_rail().get_steppers(): + matr[s.get_name()][axis] *= m + offs[s.get_name()][axis] += o + return ([matr[s.get_name()] for s in self.kin_steppers], + [mathutil.matrix_dot(orig_matr[s.get_name()], + offs[s.get_name()]) + for s in self.kin_steppers]) + def _check_kinematics(self, report_error): + matr, _ = self._get_kinematics_coeffs() + det = mathutil.matrix_det(mat_mul(mat_transp(matr), matr)) + if abs(det) < 0.00001: + raise report_error( + "Verify configured stepper(s) and their 'carriages' " + "specifications, the current configuration does not " + "allow independent movements of all printer axes.") + def calc_position(self, stepper_positions): + matr, offs = self._get_kinematics_coeffs() + spos = [stepper_positions[s.get_name()] for s in self.kin_steppers] + pinv = mat_pseudo_inverse(matr) + pos = mat_mul([[sp-o for sp, o in zip(spos, offs)]], mat_transp(pinv)) + for i in range(3): + if not any(pinv[i]): + pos[0][i] = None + return pos[0] + def update_limits(self, i, range): + l, h = self.limits[i] + # Only update limits if this axis was already homed, + # otherwise leave in un-homed state. + if l <= h: + self.limits[i] = range + def set_position(self, newpos, homing_axes): + for s in self.kin_steppers: + s.set_position(newpos) + for axis_name in homing_axes: + axis = "xyz".index(axis_name) + for c in self.carriages.values(): + if c.get_axis() == axis and c.is_active(): + self.limits[axis] = c.get_rail().get_range() + break + def clear_homing_state(self, clear_axes): + for axis, axis_name in enumerate("xyz"): + if axis_name in clear_axes: + self.limits[axis] = (1.0, -1.0) + def home_axis(self, homing_state, axis, rail): + # Determine movement + position_min, position_max = rail.get_range() + hi = rail.get_homing_info() + homepos = [None, None, None, None] + homepos[axis] = hi.position_endstop + forcepos = list(homepos) + if hi.positive_dir: + forcepos[axis] -= 1.5 * (hi.position_endstop - position_min) + else: + forcepos[axis] += 1.5 * (position_max - hi.position_endstop) + # Perform homing + homing_state.home_rails([rail], forcepos, homepos) + def home(self, homing_state): + self._check_kinematics(self.printer.command_error) + # Each axis is homed independently and in order + for axis in homing_state.get_axes(): + carriage = self.carriages["xyz"[axis]] + if carriage.get_dual_carriage() != None: + self.dc_module.home(homing_state, axis) + else: + self.home_axis(homing_state, axis, carriage.get_rail()) + def _check_endstops(self, move): + end_pos = move.end_pos + for i in (0, 1, 2): + if (move.axes_d[i] + and (end_pos[i] < self.limits[i][0] + or end_pos[i] > self.limits[i][1])): + if self.limits[i][0] > self.limits[i][1]: + raise move.move_error("Must home axis first") + raise move.move_error() + def check_move(self, move): + limits = self.limits + xpos, ypos = move.end_pos[:2] + if (xpos < limits[0][0] or xpos > limits[0][1] + or ypos < limits[1][0] or ypos > limits[1][1]): + self._check_endstops(move) + if not move.axes_d[2]: + # Normal XY move - use defaults + return + # Move with Z - update velocity and accel for slower Z axis + self._check_endstops(move) + z_ratio = move.move_d / abs(move.axes_d[2]) + move.limit_speed( + self.max_z_velocity * z_ratio, self.max_z_accel * z_ratio) + def get_status(self, eventtime): + axes = [a for a, (l, h) in zip("xyz", self.limits) if l <= h] + ranges = [c.get_rail().get_range() + for c in self.get_primary_carriages()] + axes_min = gcode.Coord(*[r[0] for r in ranges], e=0.) + axes_max = gcode.Coord(*[r[1] for r in ranges], e=0.) + return { + 'homed_axes': "".join(axes), + 'axis_minimum': axes_min, + 'axis_maximum': axes_max, + } + cmd_SET_STEPPER_CARRIAGES_help = "Set stepper carriages" + def cmd_SET_STEPPER_CARRIAGES(self, gcmd): + toolhead = self.printer.lookup_object('toolhead') + toolhead.flush_step_generation() + stepper_name = gcmd.get("STEPPER") + steppers = [stepper for stepper in self.kin_steppers + if stepper.get_name() == stepper_name + or stepper.get_name(short=True) == stepper_name] + if len(steppers) != 1: + raise gcmd.error("Invalid STEPPER '%s' specified" % stepper_name) + stepper = steppers[0] + carriages_str = gcmd.get("CARRIAGES").lower() + validate = not gcmd.get_int("DISABLE_CHECKS", 0) + old_carriages = stepper.get_carriages() + old_kin_coeffs = stepper.get_kin_coeffs() + stepper.update_carriages(carriages_str, self.all_carriages, gcmd.error) + new_carriages = stepper.get_carriages() + if new_carriages != old_carriages: + stepper.update_kin_coeffs(old_kin_coeffs) + raise gcmd.error("SET_STEPPER_CARRIAGES cannot add or remove " + "carriages that the stepper controls") + pos = toolhead.get_position() + stepper.set_position(pos) + if not validate: + return + if self.dc_module: + dc_state = self.dc_module.save_dual_carriage_state() + pcs = [dc.get_dual_carriage() for dc in self.dc_carriages] + axes = [dc.get_axis() for dc in self.dc_carriages] + for acs in itertools.product(*zip(pcs, self.dc_carriages)): + for c in acs: + self.dc_module.get_dc_rail_wrapper(c.get_rail()).activate( + idex_modes.PRIMARY, pos) + self.dc_module.get_dc_rail_wrapper( + c.get_dual_carriage().get_rail()).inactivate(pos) + self._check_kinematics(gcmd.error) + self.dc_module.restore_dual_carriage_state(dc_state, move=0) + else: + self._check_kinematics(gcmd.error) + +def load_kinematics(toolhead, config): + return GenericCartesianKinematics(toolhead, config) diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py index fbaf49e4..7bffdc0f 100644 --- a/klippy/kinematics/hybrid_corexy.py +++ b/klippy/kinematics/hybrid_corexy.py @@ -12,7 +12,7 @@ class HybridCoreXYKinematics: def __init__(self, toolhead, config): self.printer = config.get_printer() # itersolve parameters - self.rails = [ stepper.PrinterRail(config.getsection('stepper_x')), + self.rails = [ stepper.LookupRail(config.getsection('stepper_x')), stepper.LookupMultiRail(config.getsection('stepper_y')), stepper.LookupMultiRail(config.getsection('stepper_z'))] self.rails[1].get_endstops()[0][0].add_stepper( @@ -29,16 +29,13 @@ class HybridCoreXYKinematics: # dummy for cartesian config users dc_config.getchoice('axis', ['x'], default='x') # setup second dual carriage rail - self.rails.append(stepper.PrinterRail(dc_config)) + self.rails.append(stepper.LookupRail(dc_config)) self.rails[1].get_endstops()[0][0].add_stepper( self.rails[3].get_steppers()[0]) self.rails[3].setup_itersolve('corexy_stepper_alloc', b'+') - dc_rail_0 = idex_modes.DualCarriagesRail( - self.rails[0], axis=0, active=True) - dc_rail_1 = idex_modes.DualCarriagesRail( - self.rails[3], axis=0, active=False) self.dc_module = idex_modes.DualCarriages( - dc_config, dc_rail_0, dc_rail_1, axis=0) + self.printer, [self.rails[0]], [self.rails[3]], axes=[0], + safe_dist=config.getfloat('safe_distance', None, minval=0.)) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) toolhead.register_step_generator(s.generate_steps) @@ -69,8 +66,8 @@ class HybridCoreXYKinematics: rail.set_position(newpos) for axis_name in homing_axes: axis = "xyz".index(axis_name) - if self.dc_module and axis == self.dc_module.axis: - rail = self.dc_module.get_primary_rail().get_rail() + if self.dc_module and axis == 0: + rail = self.dc_module.get_primary_rail(axis) else: rail = self.rails[axis] self.limits[axis] = rail.get_range() @@ -93,7 +90,7 @@ class HybridCoreXYKinematics: def home(self, homing_state): for axis in homing_state.get_axes(): if self.dc_module is not None and axis == 0: - self.dc_module.home(homing_state) + self.dc_module.home(homing_state, axis) else: self.home_axis(homing_state, axis, self.rails[axis]) def _check_endstops(self, move): diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py index 6fa120f7..6399f236 100644 --- a/klippy/kinematics/hybrid_corexz.py +++ b/klippy/kinematics/hybrid_corexz.py @@ -12,7 +12,7 @@ class HybridCoreXZKinematics: def __init__(self, toolhead, config): self.printer = config.get_printer() # itersolve parameters - self.rails = [ stepper.PrinterRail(config.getsection('stepper_x')), + self.rails = [ stepper.LookupRail(config.getsection('stepper_x')), stepper.LookupMultiRail(config.getsection('stepper_y')), stepper.LookupMultiRail(config.getsection('stepper_z'))] self.rails[2].get_endstops()[0][0].add_stepper( @@ -29,16 +29,13 @@ class HybridCoreXZKinematics: # dummy for cartesian config users dc_config.getchoice('axis', ['x'], default='x') # setup second dual carriage rail - self.rails.append(stepper.PrinterRail(dc_config)) + self.rails.append(stepper.LookupRail(dc_config)) self.rails[2].get_endstops()[0][0].add_stepper( self.rails[3].get_steppers()[0]) self.rails[3].setup_itersolve('corexz_stepper_alloc', b'+') - dc_rail_0 = idex_modes.DualCarriagesRail( - self.rails[0], axis=0, active=True) - dc_rail_1 = idex_modes.DualCarriagesRail( - self.rails[3], axis=0, active=False) self.dc_module = idex_modes.DualCarriages( - dc_config, dc_rail_0, dc_rail_1, axis=0) + self.printer, [self.rails[0]], [self.rails[3]], axes=[0], + safe_dist=config.getfloat('safe_distance', None, minval=0.)) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) toolhead.register_step_generator(s.generate_steps) @@ -69,8 +66,8 @@ class HybridCoreXZKinematics: rail.set_position(newpos) for axis_name in homing_axes: axis = "xyz".index(axis_name) - if self.dc_module and axis == self.dc_module.axis: - rail = self.dc_module.get_primary_rail().get_rail() + if self.dc_module and axis == 0: + rail = self.dc_module.get_primary_rail(axis) else: rail = self.rails[axis] self.limits[axis] = rail.get_range() @@ -93,7 +90,7 @@ class HybridCoreXZKinematics: def home(self, homing_state): for axis in homing_state.get_axes(): if self.dc_module is not None and axis == 0: - self.dc_module.home(homing_state) + self.dc_module.home(homing_state, axis) else: self.home_axis(homing_state, axis, self.rails[axis]) def _check_endstops(self, move): diff --git a/klippy/kinematics/idex_modes.py b/klippy/kinematics/idex_modes.py index 2f2da416..fb07160a 100644 --- a/klippy/kinematics/idex_modes.py +++ b/klippy/kinematics/idex_modes.py @@ -1,10 +1,10 @@ # Support for duplication and mirroring modes for IDEX printers # # Copyright (C) 2021 Fabrice Gallet -# Copyright (C) 2023 Dmitry Butyugin +# Copyright (C) 2023-2025 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. -import logging, math +import collections, logging, math import chelper INACTIVE = 'INACTIVE' @@ -14,18 +14,34 @@ MIRROR = 'MIRROR' class DualCarriages: VALID_MODES = [PRIMARY, COPY, MIRROR] - def __init__(self, dc_config, rail_0, rail_1, axis): - self.printer = dc_config.get_printer() - self.axis = axis - self.dc = (rail_0, rail_1) + def __init__(self, printer, primary_rails, dual_rails, axes, + safe_dist={}): + self.printer = printer + self.axes = axes + self._init_steppers(primary_rails + dual_rails) + self.primary_rails = [ + DualCarriagesRail(c, dual_rails[i], axes[i], active=True) + for i, c in enumerate(primary_rails)] + self.dual_rails = [ + DualCarriagesRail(c, primary_rails[i], axes[i], active=False) + for i, c in enumerate(dual_rails)] + self.dc_rails = collections.OrderedDict( + [(c.rail.get_name(), c) + for c in self.primary_rails + self.dual_rails]) self.saved_states = {} - safe_dist = dc_config.getfloat('safe_distance', None, minval=0.) - if safe_dist is None: - dc0_rail = rail_0.get_rail() - dc1_rail = rail_1.get_rail() - safe_dist = min(abs(dc0_rail.position_min - dc1_rail.position_min), - abs(dc0_rail.position_max - dc1_rail.position_max)) - self.safe_dist = safe_dist + self.safe_dist = {} + for i, dc in enumerate(dual_rails): + axis = axes[i] + if isinstance(safe_dist, dict): + if axis in safe_dist: + self.safe_dist[axis] = safe_dist[axis] + continue + elif safe_dist is not None: + self.safe_dist[axis] = safe_dist + continue + pc = primary_rails[i] + self.safe_dist[axis] = min(abs(pc.position_min - dc.position_min), + abs(pc.position_max - dc.position_max)) self.printer.add_object('dual_carriage', self) self.printer.register_event_handler("klippy:ready", self._handle_ready) gcode = self.printer.lookup_object('gcode') @@ -40,58 +56,93 @@ class DualCarriages: 'RESTORE_DUAL_CARRIAGE_STATE', self.cmd_RESTORE_DUAL_CARRIAGE_STATE, desc=self.cmd_RESTORE_DUAL_CARRIAGE_STATE_help) - def get_rails(self): - return self.dc - def get_primary_rail(self): - for rail in self.dc: - if rail.mode == PRIMARY: - return rail + def _init_steppers(self, rails): + ffi_main, ffi_lib = chelper.get_ffi() + self.dc_stepper_kinematics = [] + self.orig_stepper_kinematics = [] + steppers = set() + for rail in rails: + c_steppers = rail.get_steppers() + if not c_steppers: + raise self.printer.config_error( + "At least one stepper must be " + "associated with carriage: %s" % rail.get_name()) + steppers.update(c_steppers) + for s in steppers: + sk = ffi_main.gc(ffi_lib.dual_carriage_alloc(), ffi_lib.free) + orig_sk = s.get_stepper_kinematics() + ffi_lib.dual_carriage_set_sk(sk, orig_sk) + self.dc_stepper_kinematics.append(sk) + self.orig_stepper_kinematics.append(orig_sk) + s.set_stepper_kinematics(sk) + def get_axes(self): + return self.axes + def get_primary_rail(self, axis): + for dc_rail in self.dc_rails.values(): + if dc_rail.mode == PRIMARY and dc_rail.axis == axis: + return dc_rail.rail return None - def toggle_active_dc_rail(self, index): + def get_dc_rail_wrapper(self, rail): + for dc_rail in self.dc_rails.values(): + if dc_rail.rail == rail: + return dc_rail + return None + def get_transform(self, rail): + dc_rail = self.get_dc_rail_wrapper(rail) + if dc_rail is not None: + return (dc_rail.scale, dc_rail.offset) + return (0., 0.) + def is_active(self, rail): + dc_rail = self.get_dc_rail_wrapper(rail) + return dc_rail.is_active() if dc_rail is not None else False + def toggle_active_dc_rail(self, target_dc): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() pos = toolhead.get_position() kin = toolhead.get_kinematics() - for i, dc in enumerate(self.dc): - dc_rail = dc.get_rail() - if i != index: - if dc.is_active(): - dc.inactivate(pos) - target_dc = self.dc[index] + axis = target_dc.axis + for dc in self.dc_rails.values(): + if dc != target_dc and dc.axis == axis and dc.is_active(): + dc.inactivate(pos) if target_dc.mode != PRIMARY: - newpos = pos[:self.axis] + [target_dc.get_axis_position(pos)] \ - + pos[self.axis+1:] + newpos = pos[:axis] + [target_dc.get_axis_position(pos)] \ + + pos[axis+1:] target_dc.activate(PRIMARY, newpos, old_position=pos) toolhead.set_position(newpos) - kin.update_limits(self.axis, target_dc.get_rail().get_range()) - def home(self, homing_state): + kin.update_limits(axis, target_dc.rail.get_range()) + def home(self, homing_state, axis): kin = self.printer.lookup_object('toolhead').get_kinematics() - enumerated_dcs = list(enumerate(self.dc)) - if (self.get_dc_order(0, 1) > 0) != \ - self.dc[0].get_rail().get_homing_info().positive_dir: + dcs = [dc for dc in self.dc_rails.values() if dc.axis == axis] + if (self.get_dc_order(dcs[0], dcs[1]) > 0) != \ + dcs[0].rail.get_homing_info().positive_dir: # The second carriage must home first, because the carriages home in # the same direction and the first carriage homes on the second one - enumerated_dcs.reverse() - for i, dc_rail in enumerated_dcs: - self.toggle_active_dc_rail(i) - kin.home_axis(homing_state, self.axis, dc_rail.get_rail()) + dcs.reverse() + for dc in dcs: + self.toggle_active_dc_rail(dc) + kin.home_axis(homing_state, axis, dc.rail) # Restore the original rails ordering - self.toggle_active_dc_rail(0) + self.toggle_active_dc_rail(dcs[0]) def get_status(self, eventtime=None): - return {('carriage_%d' % (i,)) : dc.mode - for (i, dc) in enumerate(self.dc)} - def get_kin_range(self, toolhead, mode): + status = {'carriages' : {dc.get_name() : dc.mode + for dc in self.dc_rails.values()}} + if len(self.dc_rails) == 2: + status.update({('carriage_%d' % (i,)) : dc.mode + for i, dc in enumerate(self.dc_rails.values())}) + return status + def get_kin_range(self, toolhead, mode, axis): pos = toolhead.get_position() - axes_pos = [dc.get_axis_position(pos) for dc in self.dc] - dc0_rail = self.dc[0].get_rail() - dc1_rail = self.dc[1].get_rail() - if mode != PRIMARY or self.dc[0].is_active(): + dcs = [dc for dc in self.dc_rails.values() if dc.axis == axis] + axes_pos = [dc.get_axis_position(pos) for dc in dcs] + dc0_rail = dcs[0].rail + dc1_rail = dcs[1].rail + if mode != PRIMARY or dcs[0].is_active(): range_min = dc0_rail.position_min range_max = dc0_rail.position_max else: range_min = dc1_rail.position_min range_max = dc1_rail.position_max - safe_dist = self.safe_dist + safe_dist = self.safe_dist[axis] if not safe_dist: return (range_min, range_max) @@ -101,7 +152,7 @@ class DualCarriages: range_max = min(range_max, axes_pos[0] - axes_pos[1] + dc1_rail.position_max) elif mode == MIRROR: - if self.get_dc_order(0, 1) > 0: + if self.get_dc_order(dcs[0], dcs[1]) > 0: range_min = max(range_min, 0.5 * (sum(axes_pos) + safe_dist)) range_max = min(range_max, @@ -113,9 +164,9 @@ class DualCarriages: sum(axes_pos) - dc1_rail.position_max) else: # mode == PRIMARY - active_idx = 1 if self.dc[1].is_active() else 0 + active_idx = 1 if dcs[1].is_active() else 0 inactive_idx = 1 - active_idx - if self.get_dc_order(active_idx, inactive_idx) > 0: + if self.get_dc_order(dcs[active_idx], dcs[inactive_idx]) > 0: range_min = max(range_min, axes_pos[inactive_idx] + safe_dist) else: range_max = min(range_max, axes_pos[inactive_idx] - safe_dist) @@ -131,14 +182,14 @@ class DualCarriages: # which actually permits carriage motion. return (range_min, range_min) return (range_min, range_max) - def get_dc_order(self, first, second): - if first == second: + def get_dc_order(self, first_dc, second_dc): + if first_dc == second_dc: return 0 # Check the relative order of the first and second carriages and # return -1 if the first carriage position is always smaller # than the second one and 1 otherwise - first_rail = self.dc[first].get_rail() - second_rail = self.dc[second].get_rail() + first_rail = first_dc.rail + second_rail = second_dc.rail first_homing_info = first_rail.get_homing_info() second_homing_info = second_rail.get_homing_info() if first_homing_info.positive_dir != second_homing_info.positive_dir: @@ -148,50 +199,71 @@ class DualCarriages: if first_rail.position_endstop > second_rail.position_endstop: return 1 return -1 - def activate_dc_mode(self, index, mode): + def activate_dc_mode(self, dc, mode): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() kin = toolhead.get_kinematics() + axis = dc.axis if mode == INACTIVE: - self.dc[index].inactivate(toolhead.get_position()) + dc.inactivate(toolhead.get_position()) elif mode == PRIMARY: - self.toggle_active_dc_rail(index) + self.toggle_active_dc_rail(dc) else: - self.toggle_active_dc_rail(0) - self.dc[index].activate(mode, toolhead.get_position()) - kin.update_limits(self.axis, self.get_kin_range(toolhead, mode)) + self.toggle_active_dc_rail(self.get_dc_rail_wrapper(dc.dual_rail)) + dc.activate(mode, toolhead.get_position()) + kin.update_limits(axis, self.get_kin_range(toolhead, mode, axis)) def _handle_ready(self): # Apply the transform later during Klipper initialization to make sure # that input shaping can pick up the correct stepper kinematic flags. - for dc in self.dc: - dc.apply_transform() + for dc_rail in self.dc_rails.values(): + dc_rail.apply_transform() cmd_SET_DUAL_CARRIAGE_help = "Configure the dual carriages mode" def cmd_SET_DUAL_CARRIAGE(self, gcmd): - index = gcmd.get_int('CARRIAGE', minval=0, maxval=1) + carriage_str = gcmd.get('CARRIAGE', None) + if carriage_str is None: + raise gcmd.error('CARRIAGE must be specified') + if carriage_str in self.dc_rails: + dc_rail = self.dc_rails[carriage_str] + else: + dc_rail = None + if len(self.dc_rails) == 2: + try: + index = int(carriage_str.strip()) + if index < 0 or index > 1: + raise gcmd.error('Invalid CARRIAGE=%d index' % index) + dc_rail = (self.dual_rails if index + else self.primary_rails)[0] + except ValueError: + pass + if dc_rail is None: + raise gcmd.error('Invalid CARRIAGE=%s specified' % carriage_str) mode = gcmd.get('MODE', PRIMARY).upper() if mode not in self.VALID_MODES: raise gcmd.error("Invalid mode=%s specified" % (mode,)) if mode in [COPY, MIRROR]: - if index == 0: + if dc_rail in self.primary_rails: raise gcmd.error( - "Mode=%s is not supported for carriage=0" % (mode,)) + "Mode=%s is not supported for carriage=%s" % ( + mode, dc_rail.get_name())) curtime = self.printer.get_reactor().monotonic() kin = self.printer.lookup_object('toolhead').get_kinematics() - axis = 'xyz'[self.axis] + axis = 'xyz'[dc_rail.axis] if axis not in kin.get_status(curtime)['homed_axes']: raise gcmd.error( "Axis %s must be homed prior to enabling mode=%s" % - (axis, mode)) - self.activate_dc_mode(index, mode) + (axis.upper(), mode)) + self.activate_dc_mode(dc_rail, mode) cmd_SAVE_DUAL_CARRIAGE_STATE_help = \ "Save dual carriages modes and positions" def cmd_SAVE_DUAL_CARRIAGE_STATE(self, gcmd): state_name = gcmd.get('NAME', 'default') + self.saved_states[state_name] = self.save_dual_carriage_state() + def save_dual_carriage_state(self): pos = self.printer.lookup_object('toolhead').get_position() - self.saved_states[state_name] = { - 'carriage_modes': [dc.mode for dc in self.dc], - 'axes_positions': [dc.get_axis_position(pos) for dc in self.dc], - } + return {'carriage_modes': {dc.get_name() : dc.mode + for dc in self.dc_rails.values()}, + 'carriage_positions': {dc.get_name() : dc.get_axis_position(pos) + for dc in self.dc_rails.values()}} cmd_RESTORE_DUAL_CARRIAGE_STATE_help = \ "Restore dual carriages modes and positions" def cmd_RESTORE_DUAL_CARRIAGE_STATE(self, gcmd): @@ -200,67 +272,69 @@ class DualCarriages: if saved_state is None: raise gcmd.error("Unknown DUAL_CARRIAGE state: %s" % (state_name,)) move_speed = gcmd.get_float('MOVE_SPEED', 0., above=0.) + move = gcmd.get_int('MOVE', 1) + self.restore_dual_carriage_state(saved_state, move, move_speed) + def restore_dual_carriage_state(self, saved_state, move, move_speed=0.): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() - if gcmd.get_int('MOVE', 1): + if move: homing_speed = 99999999. + move_pos = list(toolhead.get_position()) cur_pos = [] - for i, dc in enumerate(self.dc): - self.toggle_active_dc_rail(i) - homing_speed = min(homing_speed, dc.get_rail().homing_speed) + carriage_positions = saved_state['carriage_positions'] + dcs = list(self.dc_rails.values()) + for dc in dcs: + self.toggle_active_dc_rail(dc) + homing_speed = min(homing_speed, dc.rail.homing_speed) cur_pos.append(toolhead.get_position()) - move_pos = list(cur_pos[0]) - dl = [saved_state['axes_positions'][i] - cur_pos[i][self.axis] - for i in range(2)] - primary_ind = 0 if abs(dl[0]) >= abs(dl[1]) else 1 - self.toggle_active_dc_rail(primary_ind) - move_pos[self.axis] = saved_state['axes_positions'][primary_ind] - dc_mode = INACTIVE if min(abs(dl[0]), abs(dl[1])) < 0.000000001 \ - else COPY if dl[0] * dl[1] > 0 else MIRROR - if dc_mode != INACTIVE: - self.dc[1-primary_ind].activate(dc_mode, cur_pos[primary_ind]) - self.dc[1-primary_ind].override_axis_scaling( - abs(dl[1-primary_ind] / dl[primary_ind]), - cur_pos[primary_ind]) + dl = [carriage_positions[dc.get_name()] - cur_pos[i][dc.axis] + for i, dc in enumerate(dcs)] + for axis in self.axes: + dc_ind = [i for i, dc in enumerate(dcs) if dc.axis == axis] + if abs(dl[dc_ind[0]]) >= abs(dl[dc_ind[1]]): + primary_ind, secondary_ind = dc_ind[0], dc_ind[1] + else: + primary_ind, secondary_ind = dc_ind[1], dc_ind[0] + primary_dc = dcs[primary_ind] + self.toggle_active_dc_rail(primary_dc) + move_pos[axis] = carriage_positions[primary_dc.get_name()] + dc_mode = INACTIVE if min(abs(dl[primary_ind]), + abs(dl[secondary_ind])) < .000000001 \ + else COPY if dl[primary_ind] * dl[secondary_ind] > 0 \ + else MIRROR + if dc_mode != INACTIVE: + dcs[secondary_ind].activate(dc_mode, cur_pos[primary_ind]) + dcs[secondary_ind].override_axis_scaling( + abs(dl[secondary_ind] / dl[primary_ind]), + cur_pos[primary_ind]) toolhead.manual_move(move_pos, move_speed or homing_speed) toolhead.flush_step_generation() # Make sure the scaling coefficients are restored with the mode - self.dc[0].inactivate(move_pos) - self.dc[1].inactivate(move_pos) - for i, dc in enumerate(self.dc): - saved_mode = saved_state['carriage_modes'][i] - self.activate_dc_mode(i, saved_mode) + for dc in dcs: + dc.inactivate(move_pos) + for dc in self.dc_rails.values(): + saved_mode = saved_state['carriage_modes'][dc.get_name()] + self.activate_dc_mode(dc, saved_mode) class DualCarriagesRail: ENC_AXES = [b'x', b'y'] - def __init__(self, rail, axis, active): + def __init__(self, rail, dual_rail, axis, active): self.rail = rail + self.dual_rail = dual_rail + self.sks = [s.get_stepper_kinematics() for s in rail.get_steppers()] self.axis = axis self.mode = (INACTIVE, PRIMARY)[active] self.offset = 0. self.scale = 1. if active else 0. - ffi_main, ffi_lib = chelper.get_ffi() - self.dc_stepper_kinematics = [] - self.orig_stepper_kinematics = [] - for s in rail.get_steppers(): - sk = ffi_main.gc(ffi_lib.dual_carriage_alloc(), ffi_lib.free) - orig_sk = s.get_stepper_kinematics() - ffi_lib.dual_carriage_set_sk(sk, orig_sk) - # Set the default transform for the other axis - ffi_lib.dual_carriage_set_transform( - sk, self.ENC_AXES[1 - axis], 1., 0.) - self.dc_stepper_kinematics.append(sk) - self.orig_stepper_kinematics.append(orig_sk) - s.set_stepper_kinematics(sk) - def get_rail(self): - return self.rail + def get_name(self): + return self.rail.get_name() def is_active(self): return self.mode != INACTIVE def get_axis_position(self, position): return position[self.axis] * self.scale + self.offset def apply_transform(self): ffi_main, ffi_lib = chelper.get_ffi() - for sk in self.dc_stepper_kinematics: + for sk in self.sks: ffi_lib.dual_carriage_set_transform( sk, self.ENC_AXES[self.axis], self.scale, self.offset) def activate(self, mode, position, old_position=None): diff --git a/klippy/kinematics/kinematic_stepper.py b/klippy/kinematics/kinematic_stepper.py new file mode 100644 index 00000000..c82f0855 --- /dev/null +++ b/klippy/kinematics/kinematic_stepper.py @@ -0,0 +1,92 @@ +# Kinematic stepper class for generic cartesian kinematics +# +# Copyright (C) 2024 Dmitry Butyugin +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +import logging, re +import stepper, chelper + +def parse_carriages_string(carriages_str, printer_carriages, parse_error): + nxt = 0 + pat = re.compile('[+-]') + coeffs = [0.] * 3 + ref_carriages = [] + while nxt < len(carriages_str): + match = pat.search(carriages_str, nxt+1) + end = len(carriages_str) if match is None else match.start() + term = carriages_str[nxt:end].strip() + term_lst = term.split('*') + if len(term_lst) not in [1, 2]: + raise parse_error( + "Invalid term '%s' in '%s'" % (term, carriages_str)) + if len(term_lst) == 2: + try: + coeff = float(term_lst[0]) + except ValueError: + raise error("Invalid float '%s'" % term_lst[0]) + else: + coeff = -1. if term_lst[0].startswith('-') else 1. + if term_lst[0].startswith('-') or term_lst[0].startswith('+'): + term_lst[0] = term_lst[0][1:] + c = term_lst[-1] + if c not in printer_carriages: + raise parse_error("Invalid '%s' carriage referenced in '%s'" % + (c, carriages_str)) + carriage = printer_carriages[c] + j = carriage.get_axis() + if coeffs[j]: + raise error("Carriage '%s' was referenced multiple times in '%s'" % + (c, carriages_str)) + coeffs[j] = coeff + ref_carriages.append(carriage) + nxt = end + return coeffs, ref_carriages + +class KinematicStepper: + def __init__(self, config, printer_carriages): + self.printer = config.get_printer() + self.stepper = stepper.PrinterStepper(config) + self.kin_coeffs, self.carriages = parse_carriages_string( + config.get('carriages'), printer_carriages, config.error) + if not any(self.kin_coeffs): + raise config.error( + "'%s' must provide a valid 'carriages' configuration" % + self.stepper.get_name()) + self.stepper.setup_itersolve( + 'generic_cartesian_stepper_alloc', + self.kin_coeffs[0], self.kin_coeffs[1], self.kin_coeffs[2]) + self.stepper_sk = self.stepper.get_stepper_kinematics() + # Add stepper to the carriages it references + for sc in self.carriages: + sc.add_stepper(self) + def get_name(self, short=False): + name = self.stepper.get_name(short) + if short and name.startswith('stepper '): + return name[8:] + return name + def get_stepper(self): + return self.stepper + def get_kin_coeffs(self): + return tuple(self.kin_coeffs) + def get_active_axes(self): + return [i for i, c in enumerate(self.kin_coeffs) if c] + def get_carriages(self): + return self.carriages + def update_kin_coeffs(self, kin_coeffs): + self.kin_coeffs = kin_coeffs + ffi_main, ffi_lib = chelper.get_ffi() + ffi_lib.generic_cartesian_stepper_set_coeffs( + self.stepper_sk, kin_coeffs[0], kin_coeffs[1], kin_coeffs[2]) + def update_carriages(self, carriages_str, printer_carriages, report_error): + kin_coeffs, carriages = parse_carriages_string( + carriages_str, printer_carriages, + report_error or self.printer.command_error) + if report_error is not None and not any(kin_coeffs): + raise report_error( + "A valid string that references at least one carriage" + " must be provided for '%s'" % self.get_name()) + self.carriages = carriages + self.update_kin_coeffs(kin_coeffs) + def set_position(self, coord): + self.stepper.set_position(coord) diff --git a/klippy/kinematics/polar.py b/klippy/kinematics/polar.py index 73967c29..ffa15d83 100644 --- a/klippy/kinematics/polar.py +++ b/klippy/kinematics/polar.py @@ -11,7 +11,7 @@ class PolarKinematics: # Setup axis steppers stepper_bed = stepper.PrinterStepper(config.getsection('stepper_bed'), units_in_radians=True) - rail_arm = stepper.PrinterRail(config.getsection('stepper_arm')) + rail_arm = stepper.LookupRail(config.getsection('stepper_arm')) rail_z = stepper.LookupMultiRail(config.getsection('stepper_z')) stepper_bed.setup_itersolve('polar_stepper_alloc', b'a') rail_arm.setup_itersolve('polar_stepper_alloc', b'r') diff --git a/klippy/kinematics/rotary_delta.py b/klippy/kinematics/rotary_delta.py index 35f7ca98..732a89a8 100644 --- a/klippy/kinematics/rotary_delta.py +++ b/klippy/kinematics/rotary_delta.py @@ -10,14 +10,14 @@ class RotaryDeltaKinematics: def __init__(self, toolhead, config): # Setup tower rails stepper_configs = [config.getsection('stepper_' + a) for a in 'abc'] - rail_a = stepper.PrinterRail( + rail_a = stepper.LookupRail( stepper_configs[0], need_position_minmax=False, units_in_radians=True) a_endstop = rail_a.get_homing_info().position_endstop - rail_b = stepper.PrinterRail( + rail_b = stepper.LookupRail( stepper_configs[1], need_position_minmax=False, default_position_endstop=a_endstop, units_in_radians=True) - rail_c = stepper.PrinterRail( + rail_c = stepper.LookupRail( stepper_configs[2], need_position_minmax=False, default_position_endstop=a_endstop, units_in_radians=True) self.rails = [rail_a, rail_b, rail_c] diff --git a/klippy/mathutil.py b/klippy/mathutil.py index a6ab50d2..c741d915 100644 --- a/klippy/mathutil.py +++ b/klippy/mathutil.py @@ -135,3 +135,18 @@ def matrix_sub(m1, m2): def matrix_mul(m1, s): return [m1[0]*s, m1[1]*s, m1[2]*s] + +###################################################################### +# Matrix helper functions for 3x3 matrices +###################################################################### + +def matrix_det(a): + x0, x1, x2 = a + return matrix_dot(x0, matrix_cross(x1, x2)) + +def matrix_inv(a): + x0, x1, x2 = a + inv_det = 1. / matrix_det(a) + return [matrix_mul(matrix_cross(x1, x2), inv_det), + matrix_mul(matrix_cross(x2, x0), inv_det), + matrix_mul(matrix_cross(x0, x1), inv_det)] diff --git a/klippy/stepper.py b/klippy/stepper.py index a2f8c0a6..5d635a35 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -56,7 +56,8 @@ class MCU_stepper: def get_mcu(self): return self._mcu def get_name(self, short=False): - if short and self._name.startswith('stepper_'): + if short and self._name.startswith('stepper'): + # Skip an extra symbol after 'stepper' return self._name[8:] return self._name def units_in_radians(self): @@ -315,23 +316,21 @@ def parse_step_distance(config, units_in_radians=None, note_valid=False): # Stepper controlled rails ###################################################################### -# A motor control "rail" with one (or more) steppers and one (or more) +# A motor control carriage with one (or more) steppers and one (or more) # endstops. -class PrinterRail: +class GenericPrinterRail: def __init__(self, config, need_position_minmax=True, default_position_endstop=None, units_in_radians=False): - # Primary stepper and endstop self.stepper_units_in_radians = units_in_radians + self.printer = config.get_printer() + self.name = config.get_name().split()[-1] self.steppers = [] self.endstops = [] self.endstop_map = {} - self.add_extra_stepper(config) - mcu_stepper = self.steppers[0] - self.get_name = mcu_stepper.get_name - self.get_commanded_position = mcu_stepper.get_commanded_position - self.calc_position_from_coord = mcu_stepper.calc_position_from_coord + self.endstop_pin = config.get('endstop_pin') # Primary endstop position - mcu_endstop = self.endstops[0][0] + self.query_endstops = self.printer.load_object(config, 'query_endstops') + mcu_endstop = self.lookup_endstop(self.endstop_pin, self.name) if hasattr(mcu_endstop, "get_position_endstop"): self.position_endstop = mcu_endstop.get_position_endstop() elif default_position_endstop is None: @@ -380,6 +379,11 @@ class PrinterRail: raise config.error( "Invalid homing_positive_dir / position_endstop in '%s'" % (config.get_name(),)) + def get_name(self, short=False): + if short and self.name.startswith('stepper'): + # Skip an extra symbol after 'stepper' + return self.name[8:] + return self.name def get_range(self): return self.position_min, self.position_max def get_homing_info(self): @@ -394,16 +398,8 @@ class PrinterRail: return list(self.steppers) def get_endstops(self): return list(self.endstops) - def add_extra_stepper(self, config): - stepper = PrinterStepper(config, self.stepper_units_in_radians) - self.steppers.append(stepper) - if self.endstops and config.get('endstop_pin', None) is None: - # No endstop defined - use primary endstop - self.endstops[0][0].add_stepper(stepper) - return - endstop_pin = config.get('endstop_pin') - printer = config.get_printer() - ppins = printer.lookup_object('pins') + def lookup_endstop(self, endstop_pin, name): + ppins = self.printer.lookup_object('pins') pin_params = ppins.parse_pin(endstop_pin, True, True) # Normalize pin name pin_name = "%s:%s" % (pin_params['chip_name'], pin_params['pin']) @@ -415,19 +411,32 @@ class PrinterRail: self.endstop_map[pin_name] = {'endstop': mcu_endstop, 'invert': pin_params['invert'], 'pullup': pin_params['pullup']} - name = stepper.get_name(short=True) self.endstops.append((mcu_endstop, name)) - query_endstops = printer.load_object(config, 'query_endstops') - query_endstops.register_endstop(mcu_endstop, name) + self.query_endstops.register_endstop(mcu_endstop, name) else: mcu_endstop = endstop['endstop'] changed_invert = pin_params['invert'] != endstop['invert'] changed_pullup = pin_params['pullup'] != endstop['pullup'] if changed_invert or changed_pullup: - raise error("Printer rail %s shared endstop pin %s " - "must specify the same pullup/invert settings" % ( - self.get_name(), pin_name)) + raise self.printer.config_error( + "Printer rail %s shared endstop pin %s " + "must specify the same pullup/invert settings" % ( + self.get_name(), pin_name)) + return mcu_endstop + def add_stepper(self, stepper, endstop_pin=None, endstop_name=None): + if not self.steppers: + self.get_commanded_position = stepper.get_commanded_position + self.calc_position_from_coord = stepper.calc_position_from_coord + self.steppers.append(stepper) + if endstop_pin is not None: + mcu_endstop = self.lookup_endstop( + endstop_pin, endstop_name or stepper.get_name(short=True)) + else: + mcu_endstop = self.lookup_endstop(self.endstop_pin, self.name) mcu_endstop.add_stepper(stepper) + def add_stepper_from_config(self, config): + stepper = PrinterStepper(config, self.stepper_units_in_radians) + self.add_stepper(stepper, config.get('endstop_pin', None)) def setup_itersolve(self, alloc_func, *params): for stepper in self.steppers: stepper.setup_itersolve(alloc_func, *params) @@ -441,13 +450,21 @@ class PrinterRail: for stepper in self.steppers: stepper.set_position(coord) +def LookupRail(config, need_position_minmax=True, + default_position_endstop=None, units_in_radians=False): + rail = GenericPrinterRail(config, need_position_minmax, + default_position_endstop, units_in_radians) + rail.add_stepper_from_config(config) + return rail + # Wrapper for dual stepper motor support def LookupMultiRail(config, need_position_minmax=True, - default_position_endstop=None, units_in_radians=False): - rail = PrinterRail(config, need_position_minmax, - default_position_endstop, units_in_radians) + default_position_endstop=None, units_in_radians=False): + rail = LookupRail(config, need_position_minmax, + default_position_endstop, units_in_radians) for i in range(1, 99): if not config.has_section(config.get_name() + str(i)): break - rail.add_extra_stepper(config.getsection(config.get_name() + str(i))) + rail.add_stepper_from_config( + config.getsection(config.get_name() + str(i))) return rail diff --git a/test/klippy/corexyuv.cfg b/test/klippy/corexyuv.cfg new file mode 100644 index 00000000..4c0ab748 --- /dev/null +++ b/test/klippy/corexyuv.cfg @@ -0,0 +1,172 @@ +# Test config for generic cartesian kinematics with dual carriage +[carriage x] +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: ^PE5 + +[carriage y] +position_endstop: 0 +position_max: 200 +homing_speed: 50 +endstop_pin: ^PJ1 + +[carriage z] +position_endstop: 0.5 +position_max: 100 +endstop_pin: ^PD3 + +[extra_carriage z1] +primary_carriage: z +endstop_pin: ^PD2 + +[dual_carriage u] +primary_carriage: x +safe_distance: 70 +position_endstop: 300 +position_max: 300 +homing_speed: 50 +endstop_pin: ^PE4 + +[dual_carriage v] +primary_carriage: y +safe_distance: 50 +position_endstop: 200 +position_max: 200 +homing_speed: 50 +endstop_pin: ^PD4 + +[stepper a] +carriages: x+y +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 + +[stepper b] +carriages: u-v +step_pin: PH1 +dir_pin: PH0 +enable_pin: !PA1 +microsteps: 16 +rotation_distance: 40 + +[stepper c] +carriages: x-y +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 + +[stepper d] +carriages: u+v +step_pin: PE3 +dir_pin: !PH6 +enable_pin: !PG5 +microsteps: 16 +rotation_distance: 40 + +[stepper z] +carriages: z +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 + +[stepper z1] +carriages: z1 +step_pin: PG1 +dir_pin: PG0 +enable_pin: !PH3 +microsteps: 16 +rotation_distance: 8 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[gcode_macro PARK_extruder] +gcode: + SET_DUAL_CARRIAGE CARRIAGE=x + SET_DUAL_CARRIAGE CARRIAGE=y + G90 + G1 X0 Y0 + +[gcode_macro T0] +gcode: + PARK_{printer.toolhead.extruder} + SET_SERVO SERVO=my_servo angle=100 + ACTIVATE_EXTRUDER EXTRUDER=extruder + SET_DUAL_CARRIAGE CARRIAGE=x + SET_DUAL_CARRIAGE CARRIAGE=y + +[extruder1] +step_pin: PC1 +dir_pin: PC3 +enable_pin: !PC7 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK7 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[gcode_macro PARK_extruder1] +gcode: + SET_DUAL_CARRIAGE CARRIAGE=u + SET_DUAL_CARRIAGE CARRIAGE=v + G90 + G1 X300 Y200 + +[gcode_macro T1] +gcode: + PARK_{printer.toolhead.extruder} + SET_SERVO SERVO=my_servo angle=50 + ACTIVATE_EXTRUDER EXTRUDER=extruder1 + SET_DUAL_CARRIAGE CARRIAGE=u + SET_DUAL_CARRIAGE CARRIAGE=v + +[servo my_servo] +pin: PH4 + +[heater_bed] +heater_pin: PH5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK6 +control: watermark +min_temp: 0 +max_temp: 130 + +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: generic_cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 diff --git a/test/klippy/corexyuv.test b/test/klippy/corexyuv.test new file mode 100644 index 00000000..837f9b80 --- /dev/null +++ b/test/klippy/corexyuv.test @@ -0,0 +1,52 @@ +# Test cases on printers with dual carriage and multiple extruders +CONFIG corexyuv.cfg +DICTIONARY atmega2560.dict + +# First home the printer +G90 +G28 + +# Perform a dummy move +G1 X10 Y20 F6000 + +# Activate alternate carriage +SET_DUAL_CARRIAGE CARRIAGE=u +SET_DUAL_CARRIAGE CARRIAGE=v +G1 X170 Y190 F6000 + +# Go back to main carriage on X axis +SET_DUAL_CARRIAGE CARRIAGE=x +G1 X20 F6000 + +# Save dual carriage state +SAVE_DUAL_CARRIAGE_STATE + +G1 Y150 F6000 + +# Go back to main carriage on Y axis +SET_DUAL_CARRIAGE CARRIAGE=y +G1 X10 Y50 F6000 + +# Restore dual carriage state +RESTORE_DUAL_CARRIAGE_STATE + +# Test changing extruders +G1 X5 +T1 +G91 +G1 X-10 E.2 +T0 +G91 +G1 X20 E.2 +G90 + +QUERY_ENDSTOPS + +# Servo tests +SET_SERVO servo=my_servo angle=160 +SET_SERVO servo=my_servo angle=130 + +# Verify STEPPER_BUZZ +STEPPER_BUZZ STEPPER='stepper d' +STEPPER_BUZZ STEPPER=extruder +STEPPER_BUZZ STEPPER=extruder1 diff --git a/test/klippy/generic_cartesian.cfg b/test/klippy/generic_cartesian.cfg new file mode 100644 index 00000000..361a3f75 --- /dev/null +++ b/test/klippy/generic_cartesian.cfg @@ -0,0 +1,165 @@ +# Test config for generic cartesian kinematics with dual carriage +[carriage x] +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: ^PE5 + +[carriage y] +position_endstop: 0 +position_max: 200 +homing_speed: 50 +endstop_pin: ^PJ1 + +[extra_carriage y1] +primary_carriage: y +endstop_pin: ^PB6 + +[carriage z] +position_endstop: 0.5 +position_max: 100 +endstop_pin: ^PD3 + +[extra_carriage z1] +primary_carriage: z +endstop_pin: ^PD2 + +[dual_carriage u] +primary_carriage: x +position_endstop: 300 +position_max: 300 +homing_speed: 50 +endstop_pin: ^PE4 + +[stepper stepper_x] +carriages: x+y +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 + +[stepper dual_carriage] +carriages: u-y1 +step_pin: PH1 +dir_pin: PH0 +enable_pin: !PA1 +microsteps: 16 +rotation_distance: 40 + +[stepper stepper_y] +carriages: 1*y+0.5*z +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 + +[stepper stepper_y1] +carriages: 1*y1-0.5*z1 +step_pin: PE3 +dir_pin: !PH6 +enable_pin: !PG5 +microsteps: 16 +rotation_distance: 40 + +[stepper stepper_z] +carriages: z +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 + +[stepper stepper_z1] +carriages: z1 +step_pin: PG1 +dir_pin: PG0 +enable_pin: !PH3 +microsteps: 16 +rotation_distance: 8 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[gcode_macro PARK_extruder] +gcode: + SET_DUAL_CARRIAGE CARRIAGE=x + G90 + G1 X0 + +[gcode_macro T0] +gcode: + PARK_{printer.toolhead.extruder} + SET_SERVO SERVO=my_servo angle=100 + ACTIVATE_EXTRUDER EXTRUDER=extruder + SET_DUAL_CARRIAGE CARRIAGE=x + +[extruder1] +step_pin: PC1 +dir_pin: PC3 +enable_pin: !PC7 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK7 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[gcode_macro PARK_extruder1] +gcode: + SET_DUAL_CARRIAGE CARRIAGE=u + G90 + G1 X300 + +[gcode_macro T1] +gcode: + PARK_{printer.toolhead.extruder} + SET_SERVO SERVO=my_servo angle=50 + ACTIVATE_EXTRUDER EXTRUDER=extruder1 + SET_DUAL_CARRIAGE CARRIAGE=u + +[servo my_servo] +pin: PH4 + +[heater_bed] +heater_pin: PH5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK6 +control: watermark +min_temp: 0 +max_temp: 130 + +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: generic_cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 + +[input_shaper] diff --git a/test/klippy/generic_cartesian.test b/test/klippy/generic_cartesian.test new file mode 100644 index 00000000..869636a8 --- /dev/null +++ b/test/klippy/generic_cartesian.test @@ -0,0 +1,64 @@ +# Test cases on printers with dual carriage and multiple extruders +CONFIG generic_cartesian.cfg +DICTIONARY atmega2560.dict + +# Configure the input shaper +SET_DUAL_CARRIAGE CARRIAGE=u +SET_INPUT_SHAPER SHAPER_TYPE_X=ei SHAPER_FREQ_X=50 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=80 +SET_DUAL_CARRIAGE CARRIAGE=x +SET_INPUT_SHAPER SHAPER_TYPE_X=ei SHAPER_FREQ_X=50 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=80 + +# Then home the printer +G90 +G28 + +# Perform a dummy move +G1 X10 F6000 + +# Activate alternate carriage +SET_DUAL_CARRIAGE CARRIAGE=u +G1 X190 F6000 + +# Go back to main carriage +SET_DUAL_CARRIAGE CARRIAGE=x +G1 X100 F6000 + +# Save dual carriage state +SAVE_DUAL_CARRIAGE_STATE + +G1 X50 F6000 + +# Go back to alternate carriage +SET_DUAL_CARRIAGE CARRIAGE=u +G1 X130 F6000 + +# Restore dual carriage state +RESTORE_DUAL_CARRIAGE_STATE MOVE=1 + +# Test changing extruders +G1 X5 +T1 +G91 +G1 X-10 E.2 +T0 +G91 +G1 X20 E.2 +G90 + +# Test changing the stepper kinematics +SET_STEPPER_CARRIAGES STEPPER=dual_carriage CARRIAGES=u+y1 +SET_STEPPER_CARRIAGES STEPPER=stepper_x CARRIAGES=x-y + +G1 X30 E.2 +G1 Z3 + +QUERY_ENDSTOPS + +# Servo tests +SET_SERVO servo=my_servo angle=160 +SET_SERVO servo=my_servo angle=130 + +# Verify STEPPER_BUZZ +STEPPER_BUZZ STEPPER='stepper dual_carriage' +STEPPER_BUZZ STEPPER=extruder +STEPPER_BUZZ STEPPER=extruder1