| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  | # Support for duplication and mirroring modes for IDEX printers | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Copyright (C) 2021  Fabrice Gallet <tircown@gmail.com> | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  | # Copyright (C) 2023-2025  Dmitry Butyugin <dmbutyugin@google.com> | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  | # | 
					
						
							|  |  |  | # This file may be distributed under the terms of the GNU GPLv3 license. | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  | import collections, logging, math | 
					
						
							| 
									
										
										
										
											2023-02-20 01:18:57 +01:00
										 |  |  | import chelper | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  | INACTIVE = 'INACTIVE' | 
					
						
							|  |  |  | PRIMARY = 'PRIMARY' | 
					
						
							|  |  |  | COPY = 'COPY' | 
					
						
							|  |  |  | MIRROR = 'MIRROR' | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  | class DualCarriages: | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |     VALID_MODES = [PRIMARY, COPY, MIRROR] | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |     def __init__(self, printer, primary_rails, dual_rails, axes, | 
					
						
							|  |  |  |                  safe_dist={}): | 
					
						
							|  |  |  |         self.printer = printer | 
					
						
							|  |  |  |         self.axes = axes | 
					
						
							| 
									
										
										
										
											2025-05-23 01:44:47 +02:00
										 |  |  |         self._init_steppers(primary_rails + dual_rails) | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         self.primary_rails = [ | 
					
						
							| 
									
										
										
										
											2025-05-23 01:44:47 +02:00
										 |  |  |                 DualCarriagesRail(printer, c, dual_rails[i], | 
					
						
							|  |  |  |                                   axes[i], active=True) | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |                 for i, c in enumerate(primary_rails)] | 
					
						
							|  |  |  |         self.dual_rails = [ | 
					
						
							| 
									
										
										
										
											2025-05-23 01:44:47 +02:00
										 |  |  |                 DualCarriagesRail(printer, c, primary_rails[i], | 
					
						
							|  |  |  |                                   axes[i], active=False) | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |                 for i, c in enumerate(dual_rails)] | 
					
						
							|  |  |  |         self.dc_rails = collections.OrderedDict( | 
					
						
							| 
									
										
										
										
											2025-05-11 02:10:54 +02:00
										 |  |  |                 [(c.rail.get_name(short=True), c) | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |                  for c in self.primary_rails + self.dual_rails]) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         self.saved_states = {} | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         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)) | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  |         self.printer.add_object('dual_carriage', self) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         self.printer.register_event_handler("klippy:ready", self._handle_ready) | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  |         gcode = self.printer.lookup_object('gcode') | 
					
						
							|  |  |  |         gcode.register_command( | 
					
						
							|  |  |  |                    'SET_DUAL_CARRIAGE', self.cmd_SET_DUAL_CARRIAGE, | 
					
						
							|  |  |  |                    desc=self.cmd_SET_DUAL_CARRIAGE_help) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         gcode.register_command( | 
					
						
							|  |  |  |                    'SAVE_DUAL_CARRIAGE_STATE', | 
					
						
							|  |  |  |                    self.cmd_SAVE_DUAL_CARRIAGE_STATE, | 
					
						
							|  |  |  |                    desc=self.cmd_SAVE_DUAL_CARRIAGE_STATE_help) | 
					
						
							|  |  |  |         gcode.register_command( | 
					
						
							|  |  |  |                    'RESTORE_DUAL_CARRIAGE_STATE', | 
					
						
							|  |  |  |                    self.cmd_RESTORE_DUAL_CARRIAGE_STATE, | 
					
						
							|  |  |  |                    desc=self.cmd_RESTORE_DUAL_CARRIAGE_STATE_help) | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |     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 get_dc_rail_wrapper(self, rail): | 
					
						
							|  |  |  |         for dc_rail in self.dc_rails.values(): | 
					
						
							|  |  |  |             if dc_rail.rail == rail: | 
					
						
							|  |  |  |                 return dc_rail | 
					
						
							| 
									
										
										
										
											2024-02-07 20:25:36 +01:00
										 |  |  |         return None | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |     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): | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  |         toolhead = self.printer.lookup_object('toolhead') | 
					
						
							|  |  |  |         toolhead.flush_step_generation() | 
					
						
							|  |  |  |         pos = toolhead.get_position() | 
					
						
							|  |  |  |         kin = toolhead.get_kinematics() | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         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) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         if target_dc.mode != PRIMARY: | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             newpos = pos[:axis] + [target_dc.get_axis_position(pos)] \ | 
					
						
							|  |  |  |                         + pos[axis+1:] | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |             target_dc.activate(PRIMARY, newpos, old_position=pos) | 
					
						
							|  |  |  |             toolhead.set_position(newpos) | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         kin.update_limits(axis, target_dc.rail.get_range()) | 
					
						
							|  |  |  |     def home(self, homing_state, axis): | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         kin = self.printer.lookup_object('toolhead').get_kinematics() | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         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: | 
					
						
							| 
									
										
										
										
											2023-09-30 02:46:42 +09:00
										 |  |  |             # The second carriage must home first, because the carriages home in | 
					
						
							|  |  |  |             # the same direction and the first carriage homes on the second one | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             dcs.reverse() | 
					
						
							|  |  |  |         for dc in dcs: | 
					
						
							|  |  |  |             self.toggle_active_dc_rail(dc) | 
					
						
							|  |  |  |             kin.home_axis(homing_state, axis, dc.rail) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         # Restore the original rails ordering | 
					
						
							| 
									
										
										
										
											2025-08-01 15:46:24 +02:00
										 |  |  |         self.activate_dc_mode(dcs[0], PRIMARY) | 
					
						
							| 
									
										
										
										
											2021-08-01 21:10:45 +02:00
										 |  |  |     def get_status(self, eventtime=None): | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         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): | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         pos = toolhead.get_position() | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         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(): | 
					
						
							| 
									
										
										
										
											2023-09-30 02:46:42 +09:00
										 |  |  |             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 | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         safe_dist = self.safe_dist[axis] | 
					
						
							| 
									
										
										
										
											2023-09-30 02:46:42 +09:00
										 |  |  |         if not safe_dist: | 
					
						
							|  |  |  |             return (range_min, range_max) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if mode == COPY: | 
					
						
							|  |  |  |             range_min = max(range_min, | 
					
						
							|  |  |  |                             axes_pos[0] - axes_pos[1] + dc1_rail.position_min) | 
					
						
							|  |  |  |             range_max = min(range_max, | 
					
						
							|  |  |  |                             axes_pos[0] - axes_pos[1] + dc1_rail.position_max) | 
					
						
							|  |  |  |         elif mode == MIRROR: | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             if self.get_dc_order(dcs[0], dcs[1]) > 0: | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |                 range_min = max(range_min, | 
					
						
							|  |  |  |                                 0.5 * (sum(axes_pos) + safe_dist)) | 
					
						
							|  |  |  |                 range_max = min(range_max, | 
					
						
							|  |  |  |                                 sum(axes_pos) - dc1_rail.position_min) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 range_max = min(range_max, | 
					
						
							|  |  |  |                                 0.5 * (sum(axes_pos) - safe_dist)) | 
					
						
							|  |  |  |                 range_min = max(range_min, | 
					
						
							|  |  |  |                                 sum(axes_pos) - dc1_rail.position_max) | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |             # mode == PRIMARY | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             active_idx = 1 if dcs[1].is_active() else 0 | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |             inactive_idx = 1 - active_idx | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             if self.get_dc_order(dcs[active_idx], dcs[inactive_idx]) > 0: | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |                 range_min = max(range_min, axes_pos[inactive_idx] + safe_dist) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 range_max = min(range_max, axes_pos[inactive_idx] - safe_dist) | 
					
						
							| 
									
										
										
										
											2023-09-30 02:46:42 +09:00
										 |  |  |         if range_min > range_max: | 
					
						
							|  |  |  |             # During multi-MCU homing it is possible that the carriage | 
					
						
							|  |  |  |             # position will end up below position_min or above position_max | 
					
						
							|  |  |  |             # if position_endstop is too close to the rail motion ends due | 
					
						
							|  |  |  |             # to inherent latencies of the data transmission between MCUs. | 
					
						
							|  |  |  |             # This can result in an invalid range_min > range_max range | 
					
						
							|  |  |  |             # in certain modes, which may confuse the kinematics code. | 
					
						
							|  |  |  |             # So, return an empty range instead, which will correctly | 
					
						
							|  |  |  |             # block the carriage motion until a different mode is selected | 
					
						
							|  |  |  |             # which actually permits carriage motion. | 
					
						
							|  |  |  |             return (range_min, range_min) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         return (range_min, range_max) | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |     def get_dc_order(self, first_dc, second_dc): | 
					
						
							|  |  |  |         if first_dc == second_dc: | 
					
						
							| 
									
										
										
										
											2023-09-30 02:46:42 +09:00
										 |  |  |             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 | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         first_rail = first_dc.rail | 
					
						
							|  |  |  |         second_rail = second_dc.rail | 
					
						
							| 
									
										
										
										
											2023-09-30 02:46:42 +09:00
										 |  |  |         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: | 
					
						
							|  |  |  |             # Carriages home away from each other | 
					
						
							|  |  |  |             return 1 if first_homing_info.positive_dir else -1 | 
					
						
							|  |  |  |         # Carriages home in the same direction | 
					
						
							|  |  |  |         if first_rail.position_endstop > second_rail.position_endstop: | 
					
						
							|  |  |  |             return 1 | 
					
						
							|  |  |  |         return -1 | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |     def activate_dc_mode(self, dc, mode): | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         toolhead = self.printer.lookup_object('toolhead') | 
					
						
							|  |  |  |         toolhead.flush_step_generation() | 
					
						
							|  |  |  |         kin = toolhead.get_kinematics() | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         axis = dc.axis | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         if mode == INACTIVE: | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             dc.inactivate(toolhead.get_position()) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         elif mode == PRIMARY: | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             self.toggle_active_dc_rail(dc) | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             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)) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |     def _handle_ready(self): | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         for dc_rail in self.dc_rails.values(): | 
					
						
							|  |  |  |             dc_rail.apply_transform() | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |     cmd_SET_DUAL_CARRIAGE_help = "Configure the dual carriages mode" | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  |     def cmd_SET_DUAL_CARRIAGE(self, gcmd): | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         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) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         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]: | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             if dc_rail in self.primary_rails: | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |                 raise gcmd.error( | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |                         "Mode=%s is not supported for carriage=%s" % ( | 
					
						
							|  |  |  |                             mode, dc_rail.get_name())) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |             curtime = self.printer.get_reactor().monotonic() | 
					
						
							|  |  |  |             kin = self.printer.lookup_object('toolhead').get_kinematics() | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             axis = 'xyz'[dc_rail.axis] | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |             if axis not in kin.get_status(curtime)['homed_axes']: | 
					
						
							|  |  |  |                 raise gcmd.error( | 
					
						
							|  |  |  |                         "Axis %s must be homed prior to enabling mode=%s" % | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |                         (axis.upper(), mode)) | 
					
						
							|  |  |  |         self.activate_dc_mode(dc_rail, mode) | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |     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') | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         self.saved_states[state_name] = self.save_dual_carriage_state() | 
					
						
							|  |  |  |     def save_dual_carriage_state(self): | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         pos = self.printer.lookup_object('toolhead').get_position() | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         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()}} | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |     cmd_RESTORE_DUAL_CARRIAGE_STATE_help = \ | 
					
						
							|  |  |  |             "Restore dual carriages modes and positions" | 
					
						
							|  |  |  |     def cmd_RESTORE_DUAL_CARRIAGE_STATE(self, gcmd): | 
					
						
							|  |  |  |         state_name = gcmd.get('NAME', 'default') | 
					
						
							|  |  |  |         saved_state = self.saved_states.get(state_name) | 
					
						
							|  |  |  |         if saved_state is None: | 
					
						
							|  |  |  |             raise gcmd.error("Unknown DUAL_CARRIAGE state: %s" % (state_name,)) | 
					
						
							| 
									
										
										
										
											2023-08-01 22:49:15 +03:00
										 |  |  |         move_speed = gcmd.get_float('MOVE_SPEED', 0., above=0.) | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         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.): | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         toolhead = self.printer.lookup_object('toolhead') | 
					
						
							|  |  |  |         toolhead.flush_step_generation() | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         if move: | 
					
						
							| 
									
										
										
										
											2024-07-14 20:12:32 +02:00
										 |  |  |             homing_speed = 99999999. | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             move_pos = list(toolhead.get_position()) | 
					
						
							| 
									
										
										
										
											2024-07-14 20:12:32 +02:00
										 |  |  |             cur_pos = [] | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             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) | 
					
						
							| 
									
										
										
										
											2024-07-14 20:12:32 +02:00
										 |  |  |                 cur_pos.append(toolhead.get_position()) | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             dl = [carriage_positions[dc.get_name()] - cur_pos[i][dc.axis] | 
					
						
							|  |  |  |                   for i, dc in enumerate(dcs)] | 
					
						
							|  |  |  |             for axis in self.axes: | 
					
						
							|  |  |  |                 dc_ind = [i for i, dc in enumerate(dcs) if dc.axis == axis] | 
					
						
							|  |  |  |                 if abs(dl[dc_ind[0]]) >= abs(dl[dc_ind[1]]): | 
					
						
							|  |  |  |                     primary_ind, secondary_ind = dc_ind[0], dc_ind[1] | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     primary_ind, secondary_ind = dc_ind[1], dc_ind[0] | 
					
						
							|  |  |  |                 primary_dc = dcs[primary_ind] | 
					
						
							|  |  |  |                 self.toggle_active_dc_rail(primary_dc) | 
					
						
							|  |  |  |                 move_pos[axis] = carriage_positions[primary_dc.get_name()] | 
					
						
							|  |  |  |                 dc_mode = INACTIVE if min(abs(dl[primary_ind]), | 
					
						
							|  |  |  |                                           abs(dl[secondary_ind])) < .000000001 \ | 
					
						
							|  |  |  |                         else COPY if dl[primary_ind] * dl[secondary_ind] > 0 \ | 
					
						
							|  |  |  |                         else MIRROR | 
					
						
							|  |  |  |                 if dc_mode != INACTIVE: | 
					
						
							|  |  |  |                     dcs[secondary_ind].activate(dc_mode, cur_pos[primary_ind]) | 
					
						
							|  |  |  |                     dcs[secondary_ind].override_axis_scaling( | 
					
						
							|  |  |  |                             abs(dl[secondary_ind] / dl[primary_ind]), | 
					
						
							|  |  |  |                             cur_pos[primary_ind]) | 
					
						
							| 
									
										
										
										
											2024-07-14 20:12:32 +02:00
										 |  |  |             toolhead.manual_move(move_pos, move_speed or homing_speed) | 
					
						
							|  |  |  |             toolhead.flush_step_generation() | 
					
						
							|  |  |  |             # Make sure the scaling coefficients are restored with the mode | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |             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) | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | class DualCarriagesRail: | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |     ENC_AXES = [b'x', b'y'] | 
					
						
							| 
									
										
										
										
											2025-05-23 01:44:47 +02:00
										 |  |  |     def __init__(self, printer, rail, dual_rail, axis, active): | 
					
						
							|  |  |  |         self.printer = printer | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  |         self.rail = rail | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         self.dual_rail = dual_rail | 
					
						
							|  |  |  |         self.sks = [s.get_stepper_kinematics() for s in rail.get_steppers()] | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  |         self.axis = axis | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         self.mode = (INACTIVE, PRIMARY)[active] | 
					
						
							|  |  |  |         self.offset = 0. | 
					
						
							|  |  |  |         self.scale = 1. if active else 0. | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |     def get_name(self): | 
					
						
							|  |  |  |         return self.rail.get_name() | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  |     def is_active(self): | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         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() | 
					
						
							| 
									
										
										
										
											2025-05-07 00:06:36 +02:00
										 |  |  |         for sk in self.sks: | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |             ffi_lib.dual_carriage_set_transform( | 
					
						
							|  |  |  |                     sk, self.ENC_AXES[self.axis], self.scale, self.offset) | 
					
						
							| 
									
										
										
										
											2025-05-23 01:44:47 +02:00
										 |  |  |         self.printer.send_event('dual_carriage:update_kinematics') | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |     def activate(self, mode, position, old_position=None): | 
					
						
							|  |  |  |         old_axis_position = self.get_axis_position(old_position or position) | 
					
						
							|  |  |  |         self.scale = -1. if mode == MIRROR else 1. | 
					
						
							|  |  |  |         self.offset = old_axis_position - position[self.axis] * self.scale | 
					
						
							|  |  |  |         self.apply_transform() | 
					
						
							|  |  |  |         self.mode = mode | 
					
						
							| 
									
										
										
										
											2021-06-28 00:37:05 +02:00
										 |  |  |     def inactivate(self, position): | 
					
						
							| 
									
										
										
										
											2023-08-01 18:23:52 +02:00
										 |  |  |         self.offset = self.get_axis_position(position) | 
					
						
							|  |  |  |         self.scale = 0. | 
					
						
							|  |  |  |         self.apply_transform() | 
					
						
							|  |  |  |         self.mode = INACTIVE | 
					
						
							| 
									
										
										
										
											2024-07-14 20:12:32 +02:00
										 |  |  |     def override_axis_scaling(self, new_scale, position): | 
					
						
							|  |  |  |         old_axis_position = self.get_axis_position(position) | 
					
						
							|  |  |  |         self.scale = math.copysign(new_scale, self.scale) | 
					
						
							|  |  |  |         self.offset = old_axis_position - position[self.axis] * self.scale | 
					
						
							|  |  |  |         self.apply_transform() |