forked from TypQxQ/Klipper_ToolChanger
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtool.py
871 lines (719 loc) · 45.7 KB
/
tool.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
# KTCC - Klipper Tool Changer Code
# Tool module, for each tool.
#
# Copyright (C) 2023 Andrei Ignat <[email protected]>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
# To try to keep terms apart:
# Mount: Tool is selected and loaded for use, be it a physical or a virtual on physical.
# Unmopunt: Tool is unselected and unloaded, be it a physical or a virtual on physical.
# Pickup: Tool is physically picked up and attached to the toolchanger head.
# Droppoff: Tool is physically parked and dropped of the toolchanger head.
# ToolLock: Toollock is engaged.
# ToolUnLock: Toollock is disengaged.
# KTCC exception error class
# class KTCCError(Exception):
# pass
# Each tool is getting an instance of this.
import logging
class Tool:
TOOL_UNKNOWN = -2
TOOL_UNLOCKED = -1
HEATER_STATE_ACTIVE = 2
HEATER_STATE_STANDBY = 1
HEATER_STATE_OFF = 0
def __init__(self, config = None):
self.name = None
self.toolgroup = None # defaults to 0. Check if tooltype is defined.
self.is_virtual = None
self.physical_parent_id = None # Parent tool is used as a Physical parent for all tools of this group. Only used if the tool i virtual. None gets remaped to -1.
self.extruder = None # Name of extruder connected to this tool. Defaults to None.
self.fan = None # Name of general fan configuration connected to this tool as a part fan. Defaults to "none".
self.meltzonelength = None # Length of the meltzone for retracting and inserting filament on toolchange. 18mm for e3d Revo
self.lazy_home_when_parking = None # (default: 0 - disabled) - When set to 1, will home unhomed XY axes if needed and will not move any axis if already homed and parked. 2 Will also home Z if not homed.
# Wipe. -1 = none, 1= Only load filament, 2= Wipe in front of carriage, 3= Pebble wiper, 4= First Silicone, then pebble. Defaults to None.
self.zone = None # Position of the parking zone in the format X, Y
self.park = None # Position to move to when fully parking the tool in the dock in the format X, Y
self.offset = None # Offset of the nozzle in the format X, Y, Z
self.config_offset = None # Offset of the nozzle in the format X, Y, Z. From config file.
self.pickup_gcode = None # The plain gcode string for pickup of the tool.
self.dropoff_gcode = None # The plain gcode string for droppoff of the tool.
self.virtual_toolload_gcode = None # The plain gcode string is to load for virtual tool having this tool as parent. This is for loading the virtual tool.
self.virtual_toolunload_gcode = None# The plain gcode string is to unload for virtual tool having this tool as parent. This is for unloading the virtual tool.
# self.timer_idle_to_standby = None
# self.timer_idle_to_powerdown = None
self.requires_pickup_for_virtual_load = None # May be needed for a filament swap to prevent ooze but not for a pen.
self.requires_pickup_for_virtual_unload = None # May be needed for a filament swap to prevent ooze but not for a pen. Used when forcing unload.
self.unload_virtual_at_dropoff = None # If it takes long time to unload/load it may be faster to leave it loaded and force unload at end of print.
self.virtual_loaded = -1 # The abstract tool loaded in the physical tool.
self.heater_state = 0 # 0 = off, 1 = standby temperature, 2 = active temperature. Placeholder.
self.heater_active_temp = 0 # Temperature to set when in active mode. Placeholder. Requred on Physical and virtual tool if any has extruder.
self.heater_standby_temp = 0 # Temperature to set when in standby mode. Placeholder. Requred on Physical and virtual tool if any has extruder.
self.idle_to_standby_time = None # Time in seconds from being parked to setting temperature to standby the temperature above. Use 0.1 to change imediatley to standby temperature. Requred on Physical tool
self.idle_to_powerdown_time = None # Time in seconds from being parked to setting temperature to 0. Use something like 86400 to wait 24h if you want to disable. Requred on Physical tool.
# Tool specific input shaper parameters. Initiated as Klipper standard.
self.shaper_freq_x = 0
self.shaper_freq_y = 0
self.shaper_type_x = "mzv"
self.shaper_type_y = "mzv"
self.shaper_damping_ratio_x = 0.1
self.shaper_damping_ratio_y = 0.1
# Retraction
self.retract_length = 0.0
self.retract_speed = 20.0
self.unretract_extra_length = 0.0
self.unretract_speed = 20.0
self.zhop = 0.0
# Pressure Advance
self.pressure_advance = 0.0
self.pressure_advance_smooth_time = 0.04
self.config = config
# Under Consideration:
# HeatMultiplyerAtFullFanSpeed = 1 # Multiplier to be aplied to hotend temperature when fan is at maximum. Will be multiplied with fan speed. Ex. 1.1 at 205*C and fan speed of 40% will set temperature to 213*C
# If called without config then just return a dummy object.
if config is None:
return None
# Load used objects.
self.printer = config.get_printer()
self.gcode = self.printer.lookup_object('gcode')
self.gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.toollock = self.printer.lookup_object('toollock')
self.log = self.printer.lookup_object('ktcclog')
self.firmware_retract = self.printer.lookup_object('firmware_retraction')
##### Name #####
try:
_, name = config.get_name().split(" ", 1)
self.name = int(name)
except ValueError:
raise config.error(
"Name of section '%s' contains illegal characters. Use only integer tool number."
% (config.get_name()))
##### ToolGroup #####
self.toolgroup = 'toolgroup ' + str(config.getint('tool_group'))
if config.has_section(self.toolgroup):
self.toolgroup = self.printer.lookup_object(self.toolgroup)
else:
raise config.error(
"ToolGroup of T'%s' is not defined. It must be configured before the tool."
% (config.get_name()))
tg_status = self.toolgroup.get_status()
##### Is Virtual #####
self.is_virtual = config.getboolean('is_virtual',
tg_status["is_virtual"])
##### Physical Parent #####
self.physical_parent_id = config.getint('physical_parent',
tg_status["physical_parent_id"])
if self.physical_parent_id is None:
self.physical_parent_id = self.TOOL_UNLOCKED
if self.physical_parent_id >= 0 and not self.physical_parent_id == self.name:
self.pp = self.printer.lookup_object("tool " + str(self.physical_parent_id))
else:
self.pp = Tool() # Initialize physical parent as a dummy object.
pp_status = self.pp.get_status()
# Used as sanity check for tools that are virtual with same physical as themselves.
if self.is_virtual and self.physical_parent_id == self.TOOL_UNLOCKED:
raise config.error(
"Section Tool '%s' cannot be virtual without a valid physical_parent. If Virtual and Physical then use itself as parent."
% (config.get_name()))
##### Extruder #####
self.extruder = config.get('extruder', pp_status['extruder'])
##### Fan #####
self.fan = config.get('fan', pp_status['fan'])
##### Meltzone Length #####
self.meltzonelength = self._get_config_parameter_with_inheritence('meltzonelength', 0)
##### Lazy Home when parking #####
self.lazy_home_when_parking = self._get_bool_config_parameter_with_inheritence('lazy_home_when_parking', False)
##### Coordinates #####
try:
self.zone = config.get('zone', pp_status['zone'])
if not isinstance(self.zone, list):
self.zone = str(self.zone).split(',')
self.park = config.get('park', pp_status['park'])
if not isinstance(self.park, list):
self.park = str(self.park).split(',')
self.offset = config.get('offset', pp_status['offset'])
if not isinstance(self.offset, list):
self.offset = str(self.offset).split(',')
self.config_offset = [e for e in self.offset]
# Remove any accidental blank spaces.
self.zone = [s.strip() for s in self.zone]
self.park = [s.strip() for s in self.park]
self.offset = [float(s) for s in self.offset]
if len(self.zone) < 3:
raise config.error("zone Offset is malformed, must be a list of x,y,z If you want it blank, use 0,0,0")
if len(self.park) < 3:
raise config.error("park Offset is malformed, must be a list of x,y,z If you want it blank, use 0,0,0")
if len(self.offset) < 3:
raise config.error("offset Offset is malformed, must be a list of x,y,z. If you want it blank, use 0,0,0")
except Exception as e:
raise config.error(
"Coordinates of section '%s' is not well formated: %s"
% (config.get_name(), str(e)))
# Tool specific input shaper parameters. Initiated with Klipper standard values where not specified.
self.shaper_freq_x = config.get('shaper_freq_x', pp_status['shaper_freq_x'])
self.shaper_freq_y = config.get('shaper_freq_y', pp_status['shaper_freq_y'])
self.shaper_type_x = config.get('shaper_type_x', pp_status['shaper_type_x'])
self.shaper_type_y = config.get('shaper_type_y', pp_status['shaper_type_y'])
self.shaper_damping_ratio_x = config.get('shaper_damping_ratio_x', pp_status['shaper_damping_ratio_x'])
self.shaper_damping_ratio_y = config.get('shaper_damping_ratio_y', pp_status['shaper_damping_ratio_y'])
##### Standby settings (if the tool has an extruder) #####
if self.extruder is not None:
self.idle_to_standby_time = self.config.getfloat(
"idle_to_standby_time", self.pp.idle_to_standby_time)
if self.idle_to_standby_time is None:
self.idle_to_standby_time = self.toolgroup.idle_to_standby_time
self.idle_to_powerdown_time = self.config.getfloat(
"idle_to_powerdown_time", self.pp.idle_to_powerdown_time)
if self.idle_to_powerdown_time is None:
self.idle_to_powerdown_time = self.toolgroup.idle_to_powerdown_time
# For all virtual tools that are not also a physical parent, use physical parent's timer.
if self.physical_parent_id > self.TOOL_UNLOCKED and self.physical_parent_id != self.name:
self.timer_idle_to_standby = self.pp.get_timer_to_standby()
self.timer_idle_to_powerdown = self.pp.get_timer_to_powerdown()
else:
# Set up new timers if physical tool.
self.timer_idle_to_standby = ToolStandbyTempTimer(self.printer, self.name, ToolStandbyTempTimer.TIMER_TO_STANDBY)
self.timer_idle_to_powerdown = ToolStandbyTempTimer(self.printer, self.name, ToolStandbyTempTimer.TIMER_TO_SHUTDOWN)
##### G-Code ToolChange #####
self.pickup_gcode_template = self._get_gcode_template_with_inheritence('pickup_gcode')
self.dropoff_gcode_template = self._get_gcode_template_with_inheritence('dropoff_gcode')
##### G-Code VirtualToolChange #####
if self.is_virtual:
self.virtual_toolload_gcode_template = self._get_gcode_template_with_inheritence('virtual_toolload_gcode')
self.virtual_toolunload_gcode_template = self._get_gcode_template_with_inheritence('virtual_toolunload_gcode')
##### Parameters for VirtualToolChange #####
self.requires_pickup_for_virtual_load = self.config.getboolean(
"requires_pickup_for_virtual_load", self.pp.requires_pickup_for_virtual_load)
if self.requires_pickup_for_virtual_load is None:
self.requires_pickup_for_virtual_load = self.toolgroup.requires_pickup_for_virtual_load
self.requires_pickup_for_virtual_unload = self.config.getboolean(
"requires_pickup_for_virtual_unload", self.pp.requires_pickup_for_virtual_unload)
if self.requires_pickup_for_virtual_unload is None:
self.requires_pickup_for_virtual_unload = self.toolgroup.requires_pickup_for_virtual_unload
self.unload_virtual_at_dropoff = self.config.getboolean(
"unload_virtual_at_dropoff", self.pp.unload_virtual_at_dropoff)
if self.unload_virtual_at_dropoff is None:
self.unload_virtual_at_dropoff = self.toolgroup.unload_virtual_at_dropoff
logging.warn("T%s unload_virtual_at_dropoff: %s" % (str(self.name), str(self.requires_pickup_for_virtual_load)))
##### Register Tool select command #####
self.gcode.register_command("KTCC_T" + str(self.name), self.cmd_SelectTool, desc=self.cmd_SelectTool_help)
def _get_bool_config_parameter_with_inheritence(self, config_param, default = None):
tmp = self.config.getboolean(config_param, self.pp.get_config(config_param))
if tmp is None:
tmp = self.toolgroup.get_config(config_param, default)
return tmp
def _get_config_parameter_with_inheritence(self, config_param, default = None):
tmp = self.config.get(config_param, self.pp.get_config(config_param))
if tmp is None:
tmp = self.toolgroup.get_config(config_param, default)
return tmp
def _get_gcode_template_with_inheritence(self, config_param, optional = False):
temp_gcode = self.pp.get_config(config_param) # First try to get gcode parameter from eventual physical Parent.
if temp_gcode is None: # If didn't get any from physical parent,
temp_gcode = self.toolgroup.get_config(config_param) # try getting from toolgroup.
if optional and temp_gcode is None:
temp_gcode = ""
# gcode = self.get_config(config_param, temp_gcode) # Get from this config and fallback on previous.
template = self.gcode_macro.load_template(self.config, config_param, temp_gcode)
return template
def get_config(self, config_param, default = None):
if self.config is None: return None
return self.config.get(config_param, default)
cmd_SelectTool_help = "Select Tool"
def cmd_SelectTool(self, gcmd):
self.log.trace("KTCC T" + str(self.name) + " Selected.")
param = gcmd.get_int('R', None, minval=0, maxval=2)
# Check if the requested tool has been remaped to another one.
tool_is_remaped = self.toollock.tool_is_remaped(int(self.name))
if tool_is_remaped > -1:
self.log.always("Tool %d is remaped to Tool %d" % (self.name, tool_is_remaped))
remaped_tool = self.printer.lookup_object('tool ' + str(tool_is_remaped))
remaped_tool.select_tool_actual(param)
return
else:
self.select_tool_actual(param)
# To avoid recursive remaping.
def select_tool_actual(self, param = None):
current_tool_id = int(self.toollock.get_status()['tool_current']) # int(self.toollock.get_tool_current())
self.log.trace("Current Tool is T" + str(current_tool_id) + ".")
self.log.trace("This tool is_virtual is " + str(self.is_virtual) + ".")
if current_tool_id == self.name: # If trying to select the already selected tool:
return # Exit
if current_tool_id < self.TOOL_UNLOCKED:
msg = "TOOL_PICKUP: Unknown tool already mounted Can't park it before selecting new tool."
self.log.always(msg)
raise self.printer.command_error(msg)
self.log.increase_tool_statistics(self.name, 'toolmounts_started')
if self.extruder is not None: # If the new tool to be selected has an extruder prepare warmup before actual tool change so all unload commands will be done while heating up.
self.set_heater(heater_state = self.HEATER_STATE_ACTIVE)
# If optional RESTORE_POSITION_TYPE parameter is passed as 1 or 2 then save current position and restore_position_on_toolchange_type as passed. Otherwise do not change either the restore_position_on_toolchange_type or saved_position. This makes it possible to call SAVE_POSITION or SAVE_CURRENT_POSITION before the actual T command.
if param is not None:
if param in [ 1, 2 ]:
self.toollock.SaveCurrentPosition(param) # Sets restore_position_on_toolchange_type to 1 or 2 and saves current position
else:
self.toollock.SavePosition() # Sets restore_position_on_toolchange_type to 0
# Drop any tools already mounted if not virtual on same.
if current_tool_id > self.TOOL_UNLOCKED: # If there is a current tool already selected and it's a known tool.
self.log.track_selected_tool_end(current_tool_id) # Log that the current tool is to be unmounted.
current_tool = self.printer.lookup_object('tool ' + str(current_tool_id))
# If the next tool is not another virtual tool on the same physical tool.
if int(self.physical_parent_id) == self.TOOL_UNLOCKED or self.physical_parent_id != int(current_tool.get_status()["physical_parent_id"]):
self.log.debug("Will Dropoff():%s" % str(current_tool_id))
current_tool.Dropoff()
current_tool_id = self.TOOL_UNLOCKED
else: # If it's another virtual tool on the same parent physical tool.
self.log.debug("Dropoff: T" + str(current_tool_id) + "- Virtual - Running UnloadVirtual")
current_tool.UnloadVirtual()
# Now we asume tool has been dropped if needed be.
# Check if this is a virtual tool.
if not self.is_virtual:
self.log.trace("cmd_SelectTool: T%s - Not Virtual - Pickup" % str(self.name))
self.Pickup()
else:
if current_tool_id > self.TOOL_UNLOCKED: # If still has a selected tool: (This tool is a virtual tool with same physical tool as the last)
current_tool = self.printer.lookup_object('tool ' + str(current_tool_id))
self.log.trace("cmd_SelectTool: T" + str(self.name) + "- Virtual - Physical Tool is not Dropped - ")
if self.physical_parent_id > self.TOOL_UNLOCKED and self.physical_parent_id == current_tool.get_status()["physical_parent_id"]:
self.log.trace("cmd_SelectTool: T" + str(self.name) + "- Virtual - Same physical tool - Pickup")
self.LoadVirtual()
else:
msg = "cmd_SelectTool: T" + str(self.name) + "- Virtual - Not Same physical tool"
msg += "Shouldn't reach this because it is dropped in previous."
self.log.debug(msg)
raise Exception(msg)
else: # New Physical tool with a virtual tool.
pp = self.printer.lookup_object('tool ' + str(self.physical_parent_id))
pp_virtual_loaded = pp.get_status()["virtual_loaded"]
self.log.trace("cmd_SelectTool: T" + str(self.name) + "- Virtual - Picking upp physical tool")
self.Pickup()
# If the new physical tool already has another virtual tool loaded:
if pp_virtual_loaded > self.TOOL_UNLOCKED:
if pp_virtual_loaded != self.name:
self.log.info("cmd_SelectTool: T" + str(pp_virtual_loaded) + "- Virtual - Running UnloadVirtual")
uv= self.printer.lookup_object('tool ' + str(pp_virtual_loaded))
if uv.extruder is not None: # If the new tool to be selected has an extruder prepare warmup before actual tool change so all unload commands will be done while heating up.
curtime = self.printer.get_reactor().monotonic()
# heater = self.printer.lookup_object(self.extruder).get_heater()
uv.set_heater(heater_state = self.HEATER_STATE_ACTIVE)
# if int(self.heater_state) == self.HEATER_STATE_ACTIVE and int(self.heater_standby_temp) < int(heater.get_status(curtime)["temperature"]):
self.toollock._Temperature_wait_with_tolerance(curtime, self.extruder, 2)
uv.UnloadVirtual()
self.set_heater(heater_state = self.HEATER_STATE_ACTIVE)
self.log.trace("cmd_SelectTool: T" + str(self.name) + "- Virtual - Picked up physical tool and now Loading virtual tool.")
self.LoadVirtual()
self.toollock.SaveCurrentTool(self.name)
self.log.track_selected_tool_start(self.name)
def Pickup(self):
self.log.track_mount_start(self.name) # Log the time it takes for tool mount.
# Check if homed
if not self.toollock.PrinterIsHomedForToolchange():
raise self.printer.command_error("Tool.Pickup: Printer not homed and Lazy homing option for tool %s is: %s" % (str(self.name), str(self.lazy_home_when_parking)))
return None
# If has an extruder then activate that extruder.
if self.extruder is not None:
self.gcode.run_script_from_command(
"ACTIVATE_EXTRUDER extruder=%s" %
(self.extruder))
# apply new retraction options
self.apply_retract_options()
# Run the gcode for pickup.
try:
context = self.pickup_gcode_template.create_template_context()
context['myself'] = self.get_status()
context['toollock'] = self.toollock.get_status()
self.pickup_gcode_template.run_gcode_from_command(context)
except Exception as e:
raise Exception("Pickup gcode: Script running error: %s" % (str(e)))
# Restore fan if has a fan.
if self.fan is not None:
self.gcode.run_script_from_command(
"SET_FAN_SPEED FAN=" + self.fan + " SPEED=" + str(self.toollock.get_status()['saved_fan_speed']))
# Set Tool specific input shaper. -- Deprecated --
if self.shaper_freq_x != 0 or self.shaper_freq_y != 0:
# self.log.always("shaper_freq will be deprecated. Use SET_INPUT_SHAPER inside the pickup gcode instead.")
cmd = ("SET_INPUT_SHAPER" +
" SHAPER_FREQ_X=" + str(self.shaper_freq_x) +
" SHAPER_FREQ_Y=" + str(self.shaper_freq_y) +
" DAMPING_RATIO_X=" + str(self.shaper_damping_ratio_x) +
" DAMPING_RATIO_Y=" + str(self.shaper_damping_ratio_y) +
" SHAPER_TYPE_X=" + str(self.shaper_type_x) +
" SHAPER_TYPE_Y=" + str(self.shaper_type_y) )
self.log.trace("Pickup_inpshaper: " + cmd)
self.gcode.run_script_from_command(cmd)
# Save current picked up tool and print on screen.
self.toollock.SaveCurrentTool(self.name)
if self.is_virtual:
self.log.always("Physical Tool for T%d picked up." % (self.name))
else:
self.log.always("T%d picked up." % (self.name))
self.log.track_mount_end(self.name) # Log number of toolchanges and the time it takes for tool mounting.
def Dropoff(self, force_virtual_unload = False):
self.log.always("Dropoff: T%s - Running." % str(self.name))
self.log.track_selected_tool_end(self.name) # Log that the current tool is to be unmounted.
# Check if homed
if not self.toollock.PrinterIsHomedForToolchange():
self.log.always("Tool.Dropoff: Printer not homed and Lazy homing option is: " + str(self.lazy_home_when_parking))
return None
# Check if this is a virtual tool.
self.log.trace("Dropoff: T" + str(self.name) + "- is_virtual: " + str(self.is_virtual))
if self.is_virtual:
# Only dropoff if it is required.
if self.unload_virtual_at_dropoff or force_virtual_unload:
self.log.debug("T%s: unload_virtual_at_dropoff: %s, force_virtual_unload: %s" % (str(self.name), str(self.unload_virtual_at_dropoff), str(force_virtual_unload)))
self.log.info("Dropoff: T" + str(self.name) + "- Virtual - Running UnloadVirtual")
self.UnloadVirtual()
self.log.track_unmount_start(self.name) # Log the time it takes for tool change.
# Run the gcode for dropoff.
try:
context = self.dropoff_gcode_template.create_template_context()
context['myself'] = self.get_status()
context['toollock'] = self.toollock.get_status()
self.dropoff_gcode_template.run_gcode_from_command(context)
except Exception as e:
raise Exception("Dropoff gcode: Script running error: %s" % (str(e)))
# Turn off fan if has a fan.
if self.fan is not None:
# wait all move finish before change fan
toolhead = self.printer.lookup_object('toolhead')
toolhead.wait_moves()
self.gcode.run_script_from_command(
"SET_FAN_SPEED FAN=" + self.fan + " SPEED=0" )
self.toollock.SaveCurrentTool(self.TOOL_UNLOCKED) # Dropoff successfull
self.log.track_unmount_end(self.name) # Log the time it takes for tool change.
def LoadVirtual(self):
self.log.info("Loading virtual tool: T%d." % self.name)
self.log.track_mount_start(self.name) # Log the time it takes for tool mount.
# Run the gcode for Virtual Load.
try:
context = self.virtual_toolload_gcode_template.create_template_context()
context['myself'] = self.get_status()
context['toollock'] = self.toollock.get_status()
self.virtual_toolload_gcode_template.run_gcode_from_command(context)
except Exception as e:
raise Exception("virtual_toolload_gcode: Script running error: %s" % (str(e)))
pp = self.printer.lookup_object('tool ' + str(self.physical_parent_id))
pp.set_virtual_loaded(int(self.name))
# Save current picked up tool and print on screen.
self.toollock.SaveCurrentTool(self.name)
self.log.trace("Virtual T%d Loaded" % (int(self.name)))
self.log.track_mount_end(self.name) # Log number of toolchanges and the time it takes for tool mounting.
def set_virtual_loaded(self, value = -1):
self.virtual_loaded = value
self.log.trace("Saved VirtualToolLoaded for T%s as: %s" % (str(self.name), str(value)))
def UnloadVirtual(self):
self.log.info("Unloading virtual tool: T%d." % self.name)
self.log.track_unmount_start(self.name) # Log the time it takes for tool unload.
# Run the gcode for Virtual Unload.
try:
context = self.virtual_toolunload_gcode_template.create_template_context()
context['myself'] = self.get_status()
context['toollock'] = self.toollock.get_status()
self.virtual_toolunload_gcode_template.run_gcode_from_command(context)
except Exception as e:
raise Exception("virtual_toolunload_gcode: Script running error:\n%s" % str(e))
pp = self.printer.lookup_object('tool ' + str(self.physical_parent_id))
pp.set_virtual_loaded(-1)
# Save current picked up tool and print on screen.
self.toollock.SaveCurrentTool(self.name)
self.log.trace("Virtual T%d Unloaded" % (int(self.name)))
self.log.track_unmount_end(self.name) # Log the time it takes for tool unload.
def set_retract(self, retract_length=None, retract_speed=None, unretract_extra_length=None, unretract_speed=None, zhop=None):
if retract_length is not None:
self.retract_length = retract_length
if retract_speed is not None:
self.retract_speed = retract_speed
if unretract_extra_length is not None:
self.unretract_extra_length = unretract_extra_length
if unretract_speed is not None:
self.unretract_speed = unretract_speed
if zhop is not None:
self.zhop = zhop
def apply_retract_options(self):
# if firmware retract is not enabled
if self.firmware_retract is None:
return
self.firmware_retract.cmd_SET_RETRACTION(
self.gcode.create_gcode_command(
"SET_RETRACTION",
"SET_RETRACTION",
{
"RETRACT_LENGTH": self.retract_length,
"RETRACT_SPEED": self.retract_speed,
"UNRETRACT_EXTRA_LENGTH": self.unretract_extra_length,
"UNRETRACT_SPEED": self.unretract_speed,
},
)
)
def set_pressure_advance(self, pressure_advance=None, smooth_time=None):
if pressure_advance is not None:
self.pressure_advance = pressure_advance
if smooth_time is not None:
self.pressure_advance_smooth_time = smooth_time
if smooth_time is not None or pressure_advance is not None:
extruder = self.printer.lookup_object(self.extruder)
if extruder is None:
return
args = {}
if smooth_time is not None:
args['SMOOTH_TIME'] = smooth_time
if pressure_advance is not None:
args['ADVANCE'] = pressure_advance
if extruder.extruder_stepper is not None:
extruder.extruder_stepper.cmd_SET_PRESSURE_ADVANCE(
self.gcode.create_gcode_command(
'SET_PRESSURE_ADVANCE',
'SET_PRESSURE_ADVANCE',
args,
)
)
def set_offset(self, **kwargs):
for i in kwargs:
if i == "x_pos":
self.offset[0] = float(kwargs[i])
elif i == "x_adjust":
self.offset[0] = float(self.offset[0]) + float(kwargs[i])
elif i == "y_pos":
self.offset[1] = float(kwargs[i])
elif i == "y_adjust":
self.offset[1] = float(self.offset[1]) + float(kwargs[i])
elif i == "z_pos":
self.offset[2] = float(kwargs[i])
elif i == "z_adjust":
self.offset[2] = float(self.offset[2]) + float(kwargs[i])
self.log.always("T%d offset now set to: %f, %f, %f." % (int(self.name), float(self.offset[0]), float(self.offset[1]), float(self.offset[2])))
def get_offset(self):
if self.offset is None:
return [0, 0, 0]
return [float(s) for s in self.offset]
def _set_state(self, heater_state):
self.heater_state = heater_state
def set_heater(self, **kwargs):
if self.extruder is None:
self.log.debug("set_heater: T%d has no extruder! Nothing to do." % self.name )
return None
# self.log.info("T%d heater is at begingin %s." % (self.name, self.heater_state ))
heater = self.printer.lookup_object(self.extruder).get_heater()
curtime = self.printer.get_reactor().monotonic()
changing_timer = False
# self is always pointing to virtual tool but its timers and extruder are always pointing to the physical tool. When changing multiple virtual tools heaters the statistics can remain open when changing by timers of the parent if another one got in between.
# Therefore it's important for all heater statistics to only point to physical parent.
if self.is_virtual == True:
tool_for_tracking_heater = self.physical_parent_id
else:
tool_for_tracking_heater = self.name
# First set state if changed, so we set correct temps.
if "heater_state" in kwargs:
chng_state = kwargs["heater_state"]
for i in kwargs:
if i == "heater_active_temp":
self.heater_active_temp = kwargs[i]
if int(self.heater_state) == self.HEATER_STATE_ACTIVE:
heater.set_temp(self.heater_active_temp)
elif i == "heater_standby_temp":
self.heater_standby_temp = kwargs[i]
if int(self.heater_state) == self.HEATER_STATE_STANDBY:
heater.set_temp(self.heater_standby_temp)
elif i == "idle_to_standby_time":
self.idle_to_standby_time = kwargs[i]
changing_timer = True
elif i == "idle_to_powerdown_time":
self.idle_to_powerdown_time = kwargs[i]
changing_timer = True
# If already in standby and timers are counting down, i.e. have not triggered since set in standby, then reset the ones counting down.
if int(self.heater_state) == self.HEATER_STATE_STANDBY and changing_timer:
if self.timer_idle_to_powerdown.get_status()["counting_down"] == True:
self.timer_idle_to_powerdown.set_timer(self.idle_to_powerdown_time, self.name)
if self.idle_to_powerdown_time > 2:
self.log.info("T%d heater will shut down in %s seconds." % (self.name, self.log._seconds_to_human_string(self.idle_to_powerdown_time) ))
if self.timer_idle_to_standby.get_status()["counting_down"] == True:
self.timer_idle_to_standby.set_timer(self.idle_to_standby_time, self.name)
if self.idle_to_standby_time > 2:
self.log.info("T%d heater will go in standby in %s seconds." % (self.name, self.log._seconds_to_human_string(self.idle_to_standby_time) ))
# Change Active mode, Continuing with part two of temp changing.:
if "heater_state" in kwargs:
if self.heater_state == chng_state: # If we don't actually change the state don't do anything.
if chng_state == self.HEATER_STATE_ACTIVE:
self.log.trace("set_heater: T%d heater state not changed. Setting active temp." % self.name )
heater.set_temp(self.heater_active_temp)
elif chng_state == self.HEATER_STATE_STANDBY:
self.log.trace("set_heater: T%d heater state not changed. Setting standby temp." % self.name )
heater.set_temp(self.heater_standby_temp)
else:
self.log.trace("set_heater: T%d heater state not changed." % self.name )
return None
if chng_state == self.HEATER_STATE_OFF: # If Change to Shutdown
self.log.trace("set_heater: T%d heater state now OFF." % self.name )
self.timer_idle_to_standby.set_timer(0, self.name)
self.timer_idle_to_powerdown.set_timer(0.1, self.name)
# self.log.track_standby_heater_end(self.name) # Set the standby as finishes in statistics.
# self.log.track_active_heater_end(self.name) # Set the active as finishes in statistics.
elif chng_state == self.HEATER_STATE_ACTIVE: # Else If Active
self.log.trace("set_heater: T%d heater state now ACTIVE." % self.name )
self.timer_idle_to_standby.set_timer(0, self.name)
self.timer_idle_to_powerdown.set_timer(0, self.name)
heater.set_temp(self.heater_active_temp)
self.log.track_standby_heater_end(tool_for_tracking_heater) # Set the standby as finishes in statistics.
self.log.track_active_heater_start(tool_for_tracking_heater) # Set the active as started in statistics.
elif chng_state == self.HEATER_STATE_STANDBY: # Else If Standby
self.log.trace("set_heater: T%d heater state now STANDBY." % self.name )
if int(self.heater_state) == self.HEATER_STATE_ACTIVE and int(self.heater_standby_temp) < int(heater.get_status(curtime)["temperature"]):
self.timer_idle_to_standby.set_timer(self.idle_to_standby_time, self.name)
self.timer_idle_to_powerdown.set_timer(self.idle_to_powerdown_time, self.name)
if self.idle_to_standby_time > 2:
self.log.always("T%d heater will go in standby in %s seconds." % (self.name, self.log._seconds_to_human_string(self.idle_to_standby_time) ))
else: # Else (Standby temperature is lower than the current temperature)
self.log.trace("set_heater: T%d standbytemp:%d;heater_state:%d; current_temp:%d." % (self.name, int(self.heater_state), int(self.heater_standby_temp), int(heater.get_status(curtime)["temperature"])))
self.timer_idle_to_standby.set_timer(0.1, self.name)
self.timer_idle_to_powerdown.set_timer(self.idle_to_powerdown_time, self.name)
if self.idle_to_powerdown_time > 2:
self.log.always("T%d heater will shut down in %s seconds." % (self.name, self.log._seconds_to_human_string(self.idle_to_powerdown_time)))
self.heater_state = chng_state
def get_timer_to_standby(self):
return self.timer_idle_to_standby
def get_timer_to_powerdown(self):
return self.timer_idle_to_powerdown
def get_status(self, eventtime= None):
status = {
"name": self.name,
"is_virtual": self.is_virtual,
"physical_parent_id": self.physical_parent_id,
"extruder": self.extruder,
"fan": self.fan,
"lazy_home_when_parking": self.lazy_home_when_parking,
"meltzonelength": self.meltzonelength,
"zone": self.zone,
"park": self.park,
"offset": self.offset,
"heater_state": self.heater_state,
"heater_active_temp": self.heater_active_temp,
"heater_standby_temp": self.heater_standby_temp,
"idle_to_standby_time": self.idle_to_standby_time,
"idle_to_powerdown_next_wake": self.idle_to_powerdown_time,
"shaper_freq_x": self.shaper_freq_x,
"shaper_freq_y": self.shaper_freq_y,
"shaper_type_x": self.shaper_type_x,
"shaper_type_y": self.shaper_type_y,
"shaper_damping_ratio_x": self.shaper_damping_ratio_x,
"shaper_damping_ratio_y": self.shaper_damping_ratio_y,
"virtual_loaded": self.virtual_loaded,
"requires_pickup_for_virtual_load": self.requires_pickup_for_virtual_load,
"requires_pickup_for_virtual_unload": self.requires_pickup_for_virtual_unload,
"unload_virtual_at_dropoff": self.unload_virtual_at_dropoff,
"retract_length": self.retract_length,
"retract_speed": self.retract_speed,
"unretract_extra_length": self.unretract_extra_length,
"unretract_speed": self.unretract_speed,
"pressure_advance": self.pressure_advance,
"pressure_advance_smooth_time": self.pressure_advance_smooth_time,
}
return status
# Based on DelayedGcode.
class ToolStandbyTempTimer:
TIMER_TO_SHUTDOWN = 0
TIMER_TO_STANDBY = 1
def __init__(self, printer, tool_id, temp_type):
self.printer = printer
self.tool_id = tool_id
self.last_virtual_tool_using_physical_timer = None
self.duration = 0.
self.temp_type = temp_type # 0= Time to shutdown, 1= Time to standby.
self.reactor = self.printer.get_reactor()
self.gcode = self.printer.lookup_object('gcode')
self.timer_handler = None
self.inside_timer = self.repeat = False
self.printer.register_event_handler("klippy:ready", self._handle_ready)
self.toollock = self.printer.lookup_object('toollock')
self.log = self.printer.lookup_object('ktcclog')
self.counting_down = False
self.nextwake = self.reactor.NEVER
def _handle_ready(self):
self.timer_handler = self.reactor.register_timer(
self._standby_tool_temp_timer_event, self.reactor.NEVER)
def _standby_tool_temp_timer_event(self, eventtime):
self.inside_timer = True
self.counting_down = False
try:
if self.last_virtual_tool_using_physical_timer is None:
raise Exception("last_virtual_tool_using_physical_timer is < None")
tool = self.printer.lookup_object("tool " + str(self.last_virtual_tool_using_physical_timer))
if tool.is_virtual == True:
tool_for_tracking_heater = tool.physical_parent_id
else:
tool_for_tracking_heater = tool.name
self.log.trace(
"_standby_tool_temp_timer_event: Running for T%s. temp_type:%s. %s" %
(str(self.tool_id),
"Time to shutdown" if self.temp_type == 0 else "Time to standby",
("For virtual tool T%s" % str(self.last_virtual_tool_using_physical_timer) )
if self.last_virtual_tool_using_physical_timer != self.tool_id else ""))
temperature = 0
heater = self.printer.lookup_object(tool.extruder).get_heater()
if self.temp_type == self.TIMER_TO_STANDBY:
self.log.track_standby_heater_start(self.tool_id) # Set the standby as started in statistics.
temperature = tool.get_status()["heater_standby_temp"]
heater.set_temp(temperature)
# self.log.trace("_standby_tool_temp_timer_event: Running heater.set_temp(%s)" % str(temperature))
else:
self.log.track_standby_heater_end(self.tool_id) # Set the standby as finishes in statistics.
tool.get_timer_to_standby().set_timer(0, self.last_virtual_tool_using_physical_timer) # Stop Standby timer.
#tool.get_timer_to_powerdown().set_timer(0, self.last_virtual_tool_using_physical_timer) # Stop Poweroff timer. (Already off)
tool._set_state(Tool.HEATER_STATE_OFF) # Set off state.
heater.set_temp(0) # Set temperature to 0.
# tool.set_heater(Tool.HEATER_STATE_OFF)
self.log.track_active_heater_end(self.tool_id) # Set the active as finishes in statistics.
except Exception as e:
raise Exception("Failed to set Standby temp for tool T%s: %s. %s" % (str(self.tool_id),
("for virtual T%s" % str(self.last_virtual_tool_using_physical_timer)),
str(e))) # if actual_tool_calling != self.tool_id else ""
self.nextwake = self.reactor.NEVER
if self.repeat:
self.nextwake = eventtime + self.duration
self.counting_down = True
self.inside_timer = self.repeat = False
return self.nextwake
def set_timer(self, duration, actual_tool_calling):
actual_tool_calling = actual_tool_calling
self.log.trace(str(self.timer_handler) + ".set_timer: T%s %s, temp_type:%s, duration:%s." % (
str(self.tool_id),
("for virtual T%s" % str(actual_tool_calling)) if actual_tool_calling != self.tool_id else "",
("Standby" if self.temp_type == 1 else "OFF"),
str(duration)))
self.duration = float(duration)
self.last_virtual_tool_using_physical_timer = actual_tool_calling
if self.inside_timer:
self.repeat = (self.duration != 0.)
else:
waketime = self.reactor.NEVER
if self.duration:
waketime = self.reactor.monotonic() + self.duration
self.nextwake = waketime
self.reactor.update_timer(self.timer_handler, waketime)
self.counting_down = True
def get_status(self, eventtime= None):
status = {
# "tool": self.tool,
"temp_type": self.temp_type,
"duration": self.duration,
"counting_down": self.counting_down,
"next_wake": self._time_left()
}
return status
def _time_left(self):
if self.nextwake == self.reactor.NEVER:
return "never"
else:
return str( self.nextwake - self.reactor.monotonic() )
# Todo:
# Inspired by https://github.com/jschuh/klipper-macros/blob/main/layers.cfg
class MeanLayerTime:
def __init__(self, printer):
# Run before toolchange to set time like in StandbyToolTimer.
# Save time for last 5 (except for first) layers
# Provide a mean layer time.
# Have Tool have a min and max 2standby time.
# If mean time for 3 layers is higher than max, then set min time.
# Reset time if layer time is higher than max time. Pause or anything else that has happened.
# Function to reset layer times.
pass
def load_config_prefix(config):
return Tool(config)