mirror of
				https://github.com/Klipper3d/klipper.git
				synced 2025-10-31 10:25:57 +01:00 
			
		
		
		
	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 <dmbutyugin@google.com>
This commit is contained in:
		
							
								
								
									
										138
									
								
								config/example-generic-caretesian.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								config/example-generic-caretesian.cfg
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										177
									
								
								config/sample-corexyuv.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								config/sample-corexyuv.cfg
									
									
									
									
									
										Normal file
									
								
							| @@ -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=<dual_carriage_x_shaper> SHAPER_FREQ_X=<dual_carriage_x_freq> SHAPER_TYPE_Y=<dual_carriage_y_shaper> SHAPER_FREQ_Y=<dual_carriage_y_freq> | ||||
| #    SET_DUAL_CARRIAGE CARRIAGE=x MODE=PRIMARY | ||||
| #    SET_DUAL_CARRIAGE CARRIAGE=y MODE=PRIMARY | ||||
| #    SET_INPUT_SHAPER SHAPER_TYPE_X=<primary_carriage_x_shaper> SHAPER_FREQ_X=<primary_carriage_x_freq> SHAPER_TYPE_Y=<primary_carriage_y_shaper> SHAPER_FREQ_Y=<primary_carriage_y_freq> | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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=<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`. `<carriage>` 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=<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=<stepper_name> CARRIAGES=<carriages> | ||||
| [DISABLE_CHECKS=[0|1]]`: Set or update the stepper carriages. | ||||
| `<stepper_name>` must reference an existing stepper defined in `printer.cfg`, | ||||
| and `<carriages>` 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 | ||||
|   | ||||
| @@ -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["<carriage>"]`: The mode of the carriage `<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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										52
									
								
								klippy/chelper/kin_generic.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								klippy/chelper/kin_generic.c
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| // Generic cartesian kinematics stepper position calculation | ||||
| // | ||||
| // Copyright (C) 2024  Dmitry Butyugin <dmbutyugin@google.com> | ||||
| // | ||||
| // This file may be distributed under the terms of the GNU GPLv3 license. | ||||
|  | ||||
| #include <stddef.h> // offsetof | ||||
| #include <stdlib.h> // malloc | ||||
| #include <string.h> // 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; | ||||
| } | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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]) | ||||
|   | ||||
							
								
								
									
										358
									
								
								klippy/kinematics/generic_cartesian.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										358
									
								
								klippy/kinematics/generic_cartesian.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,358 @@ | ||||
| # Code for generic handling the kinematics of cartesian-like printers | ||||
| # | ||||
| # Copyright (C) 2024-2025  Dmitry Butyugin <dmbutyugin@google.com> | ||||
| # | ||||
| # 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) | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| # Support for duplication and mirroring modes for IDEX printers | ||||
| # | ||||
| # Copyright (C) 2021  Fabrice Gallet <tircown@gmail.com> | ||||
| # Copyright (C) 2023  Dmitry Butyugin <dmbutyugin@google.com> | ||||
| # Copyright (C) 2023-2025  Dmitry Butyugin <dmbutyugin@google.com> | ||||
| # | ||||
| # 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(): | ||||
|         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) | ||||
|         target_dc = self.dc[index] | ||||
|         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 | ||||
|             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: | ||||
|                 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]), | ||||
|                     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): | ||||
|   | ||||
							
								
								
									
										92
									
								
								klippy/kinematics/kinematic_stepper.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								klippy/kinematics/kinematic_stepper.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| # Kinematic stepper class for generic cartesian kinematics | ||||
| # | ||||
| # Copyright (C) 2024  Dmitry Butyugin <dmbutyugin@google.com> | ||||
| # | ||||
| # 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) | ||||
| @@ -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') | ||||
|   | ||||
| @@ -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] | ||||
|   | ||||
| @@ -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)] | ||||
|   | ||||
| @@ -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 " | ||||
|                 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, | ||||
|     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 | ||||
|   | ||||
							
								
								
									
										172
									
								
								test/klippy/corexyuv.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								test/klippy/corexyuv.cfg
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										52
									
								
								test/klippy/corexyuv.test
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								test/klippy/corexyuv.test
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										165
									
								
								test/klippy/generic_cartesian.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								test/klippy/generic_cartesian.cfg
									
									
									
									
									
										Normal file
									
								
							| @@ -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] | ||||
							
								
								
									
										64
									
								
								test/klippy/generic_cartesian.test
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								test/klippy/generic_cartesian.test
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
		Reference in New Issue
	
	Block a user