-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtower2RPM.py
389 lines (329 loc) · 15.5 KB
/
tower2RPM.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
#!/usr/bin/env python
import re
import sys
import math
def linspace(start, stop, n):
if n == 1:
yield stop
return
h = (stop - start) / (n - 1)
for i in range(n):
yield start + h * i
MAX_X = 250
K_VAL = 30 # NOTE: we extract user's filament K-val from gcode, but if it fails, sensible default.
FAN_TIME = 12 #seconds From RPM docs, 12 second cooling burst.
BUCKET_X = 253 # A sensible default, set "; BUCKET_X ###.# in your start gcode to configure.
BUCKET_OFFSET=10 # Position to move to before triggering the RPM.
PRINTER_MAX_VOLUMETRIC = 15 #mm^3/s. Use PS default, we min with the actual setting and filament setting later.
WIPING_OBJECTS = 0
# pre-read to get print settings
settings = {}
object_purge = {}
current_tc = ""
current_in_purge = 0
setting_re = re.compile(r'^\s*;\s*(\S+?)\s*=\s*(.*)$')
extruder_re = re.compile(r'E(\d+\.\d+)')
fp = open(sys.argv[1], 'r')
for line in fp:
line = line.strip()
match = setting_re.search(line)
match_E = extruder_re.search(line)
if "; toolchange #" in line:
if current_in_purge: # Did not find a purge finished in the last run, not infill.
object_purge[current_tc] = 0
current_tc = line.strip()
object_purge[current_tc] = 0
if current_tc and line.startswith("M900 K") and line.endswith("; Filament gcode"):
current_in_purge = 1
if current_in_purge and match_E:
object_purge[current_tc] += float(match_E.group(1))
if "; PURGING FINISHED" in line:
current_in_purge = 0
WIPING_OBJECTS = 1
if not match:
continue
settings[match.group(1)] = match.group(2)
fp.close()
#print("{}".format(object_purge))
if "single_extruder_multi_material" not in settings or \
settings["single_extruder_multi_material"] != "1":
print >> sys.stderr, "ERROR: Only single extruder MMU slices are supported."
sys.exit(1)
#if "wipe_tower" not in settings or \
# settings["wipe_tower"] != "1":
# print >> sys.stderr, "ERROR: Only prints that include a wipe tower are supported."
# sys.exit(1)
if "wipe_into_objects" in settings and \
settings["wipe_into_objects"] == "1":
print >> sys.stderr, "WARNING: Wiping into objects is not supported and may not work as expected/desired."
WIPING_OBJECTS = 0
if "wipe_into_infill" in settings and \
settings["wipe_into_infill"] == "1":
print >> sys.stderr, "WARNING: Wiping into infill is not supported and may not work as expected/desired."
WIPING_OBJECTS = 0
if "max_volumetric_speed" in settings:
if settings["max_volumetric_speed"] == "0":
print >> sys.stderr, "WARNING: Your print max volumetric speed is unconfigured. Using 15mm3/sec"
else:
PRINTER_MAX_VOLUMETRIC = min(float(settings["max_volumetric_speed"]),PRINTER_MAX_VOLUMETRIC)
# Cheat to guess at the total number of tools available.
# This doesn't tell us what is used, just what exists. But it is also useful for user retract values.
retract_settings = settings["retract_length"].strip().split(",")
filament_diameter = settings["filament_diameter"].strip().split(",")
retract_tc_settings = settings["retract_length_toolchange"].strip().split(",")
retract_speed_settings = settings["retract_speed"].strip().split(",")
filament_volumetric_settings = settings["filament_max_volumetric_speed"].strip().split(",")
min_purge_settings = settings["filament_minimal_purge_on_wipe_tower"].strip().split(",")
filament_cooling_moves_settings = settings["filament_cooling_moves"].strip().split(",")
filament_cooling_initial_speed_settings = settings["filament_cooling_initial_speed"].strip().split(",")
filament_cooling_final_speed_settings = settings["filament_cooling_final_speed"].strip().split(",")
filament_unloading_speed_start_settings = settings["filament_unloading_speed_start"].strip().split(",")
filament_unloading_speed_settings = settings["filament_unloading_speed"].strip().split(",")
filament_loading_speed_start_settings = settings["filament_loading_speed_start"].strip().split(",")
filament_loading_speed_end_settings = settings["filament_loading_speed"].strip().split(",")
filament_ramming_settings = settings["filament_ramming_parameters"].strip().split(";")
bottom_solid_layers = int(settings["bottom_solid_layers"].strip())
layer_height = float(settings["layer_height"].strip())
# We need this because of issue 2855 in PrusaSlicer...
LAST_SOLID_Z = bottom_solid_layers*layer_height
total_tools = len(retract_settings)
printer = {
"cooling_tube_pos": float(settings["cooling_tube_retraction"]),
"cooling_tube_length": float(settings["cooling_tube_length"]),
"filament_park_position": float(settings["parking_pos_retraction"]),
"extra_loading_move": float(settings["extra_loading_move"])
}
tools = []
for tool in range(total_tools):
tools.append({
"filament_diameter": float(filament_diameter[tool]),
"purge": [],
"retract" : float(retract_settings[tool]),
"retract_tc" : float(retract_tc_settings[tool]),
"retract_speed" : int(retract_speed_settings[tool])*60, # NOTE: *60 because F needs mm/min, PS gives mm/s
"min_purge_vol" : float(min_purge_settings[tool]),
"cooling_moves" : int(filament_cooling_moves_settings[tool]),
"start_cool_speed" : float(filament_cooling_initial_speed_settings[tool])*60,
"end_cool_speed" : float(filament_cooling_final_speed_settings[tool]) *60,
"start_unload_speed" : float(filament_unloading_speed_start_settings[tool])*60,
"end_unload_speed" : float(filament_unloading_speed_settings[tool]) *60,
"start_load_speed" : float(filament_loading_speed_start_settings[tool])*60,
"end_load_speed" : float(filament_loading_speed_end_settings[tool])*60,
"max_vol_rate": min(float(filament_volumetric_settings[tool]),PRINTER_MAX_VOLUMETRIC), # So we can figure out fastest we can purge at.
"ramming_parameters": []
})
tmp_ram = filament_ramming_settings[tool].strip().split("|")
for val in tmp_ram[0].strip().split(" "):
tools[tool]["ramming_parameters"].append(float(val.strip("\"")))
del tools[tool]["ramming_parameters"][:2] # First two are mostly useless here.
tool = 0
t = 0
for value in settings["wiping_volumes_matrix"].split(","):
value = float(value)
if t >= total_tools:
t = 0
tool += 1
tools[tool]["purge"].append(value)
t += 1
gcode = []
if WIPING_OBJECTS:
gcode.append("Object/infill wiping detected!")
tower = {"type": None, "gcode": []}
skip = 0
last = {"X": 0.0, "Y": 0.0, "Z": 0.0, "K": "M900 K{}".format(K_VAL)}
done = False
fan_on = False
tc_id = 0
printObject = {"type":None, "gcode":[]}
# Generates a purge for the RPM mechanism from BB3D
def purge_generate_RPM(length, maxrate):
_rpm_gcode = []
_RPM_PURGE_SIZE = 40 # linear mm
_RPM_CYCLES = int(math.ceil(length/_RPM_PURGE_SIZE))
for i in range(0,_RPM_CYCLES): # Determine no. of purges we need to do
_rpm_gcode.append("; Purge cycle {} of {}".format(i+1,_RPM_CYCLES))
_rpm_gcode.append("G1 E{} F{:.1f}".format(_RPM_PURGE_SIZE,maxrate)) # These can't go much higher, else you start to skip/grind. Do the 40mm in one go.
_rpm_gcode.append("M106") # Turn the fan on to cool the purge
_rpm_gcode.append("G4 S{:.0f}".format(FAN_TIME))
if i !=_RPM_CYCLES-1 or not fan_on:
_rpm_gcode.append("M107")# Turn the fan off again to resume the print or for the next purge
_rpm_gcode.append("G1 X{0:.1f} F12000".format(BUCKET_X-BUCKET_OFFSET)) # Bump the bucket twice.
_rpm_gcode.append("G1 X{0:.1f} F3000".format(BUCKET_X))
_rpm_gcode.append("G1 X{0:.1f} F10000".format(BUCKET_X-BUCKET_OFFSET))
if i != _RPM_CYCLES-1:
_rpm_gcode.append("G1 X{0:.1f} F3000".format(BUCKET_X)) # Return to bucket for next purge cycle.
_rpm_gcode.append("G4 S0; sync")
return _rpm_gcode
# Generates a ramming sequence for the BB3D purge mechanism.
def ram_generate_RPM(ram_tool):
_rpm_gcode = [];
_ram_lens = [];
_ram_total = 0.0;
_RPM_PURGE_SIZE = 40 # linear mm
_tool_linmm_per_mmcubed = (math.pi*math.pow(0.5*ram_tool["filament_diameter"],2))
# Calc max linear feedrate from volumetric:
_maxrate_mms = ram_tool["max_vol_rate"]/_tool_linmm_per_mmcubed;
_maxrate = _maxrate_mms*60.0;
# NOTE: This is from the ramming parameters. They are volumetric rates in steps of 1/4 second.
for ramrate in ram_tool["ramming_parameters"]:
# Rate is mm^3/sec, for 1/4 second of it, calculate the linear distance.
ram_len = (0.25*ramrate)/_tool_linmm_per_mmcubed;
_ram_lens.append(ram_len);
_ram_total +=ram_len;
# Pre-purge so the total ram is 40mm
_rpm_gcode.append("; Ramming RPM: {:.2f} mm".format(_ram_total))
_rpm_gcode.append("G1 E{} F{:.1f}".format(_RPM_PURGE_SIZE-_ram_total,_maxrate)) # These can't go much higher, else you start to skip/grind. Do the 40mm in one go.
for ram_len in _ram_lens:
# ESpeed is (length/time)*60 for linear mm/min. This will be lower than PS since we don't move X/Y.
ram_speed_f = (ram_len/0.25)*60
_rpm_gcode.append("G1 E{:.4f} F{:.0f}".format(ram_len,ram_speed_f))
_rpm_gcode.append("M106") # Turn the fan on to cool the purge
_rpm_gcode.append("G4 S{:.0f}".format(FAN_TIME))
_rpm_gcode.append("M107") # Fan off in prep for next colour purge.
_rpm_gcode.append("G1 X{0:.1f} F12000".format(BUCKET_X-BUCKET_OFFSET)) # Bump the bucket twice.
_rpm_gcode.append("G1 X{0:.1f} F3000".format(BUCKET_X))
_rpm_gcode.append("G1 X{0:.1f} F12000".format(BUCKET_X-BUCKET_OFFSET)) # Bump the bucket twice.
_rpm_gcode.append("G1 X{0:.1f} F3000".format(BUCKET_X))
return _rpm_gcode;
fp = open(sys.argv[1], 'r')
for line in fp:
line = line.strip()
if done:
gcode.append(line)
continue
if skip > 0:
skip-=1
continue
if line.startswith("; BUCKET_X"):
BUCKET_X = float(line[10:])
MAX_X = BUCKET_X - BUCKET_OFFSET
if line.startswith("M106"):
fan_on = True
if line.startswith("M107"):
fan_on = False
if line.startswith("T") and "RPM FROM -1" in line:
gcode.append(line)
continue
# The K here is the filament custom gcode K value for linear advance. Typ. 30 but not guaranteed if the user has tuned it.
if line.startswith("T") and "; RPM FROM" in line:
last["T"] = int(line[-2:])
tool = int(line[1:2])
# Fixed settings at the start taken from example
# PrusaSlicer 2.0.0 output
gcode.append("; ------------------------")
gcode.append("; BUCKET TOOL CHANGE START")
if tc_id:
gcode.append("; toolchange #{}".format(tc_id))
tc_id+=1
gcode.append("M220 B")
gcode.append("M220 S100")
if last["Z"] < 40.0: # TODO: make bucket height configurable
zMove = "Z40.0" # TODO: Make Z height and speed configurable
else:
zMove = ""
if fan_on: # Kill fan while still over print, it takes time to wind down. Should be off by the time we bucket.
gcode.append("M107")
gcode.append("G1 X{:.3f} {} F10000".format(BUCKET_X-BUCKET_OFFSET,zMove)) # TODO: Make speed configurable
gcode.append("G1 X{:.3f} F1000".format(BUCKET_X))
prev_tool =tools[last["T"]]
# Cooling tube math:
cooling_steps = linspace(prev_tool["start_cool_speed"],prev_tool["end_cool_speed"],2*prev_tool["cooling_moves"])
cool_retract = -15 + printer["cooling_tube_pos"] + printer["cooling_tube_length"]/2
park_retract = printer["filament_park_position"] - printer["cooling_tube_length"]/2 - printer["cooling_tube_pos"]
reload_distance = printer["filament_park_position"] + printer["extra_loading_move"]
if fan_on:
# Turn the fan off while we purge to the bucket
gcode.append("M107")
gcode += ram_generate_RPM(prev_tool)
# This is the retract and cooling move stuff, the consts are from PS's wipetower code.
gcode.append("G1 E-15.000 F{}".format(prev_tool["start_unload_speed"]))
gcode.append("G1 E-{:.4f} F{}".format(cool_retract*0.7,prev_tool["end_unload_speed"]))
gcode.append("G1 E-{:.4f} F{}".format(cool_retract*0.2,prev_tool["end_unload_speed"]*0.5))
gcode.append("G1 E-{:.4f} F{}".format(cool_retract*0.1,prev_tool["end_unload_speed"]*0.3))
gcode.append("; Cooling: {} to {} mm/s in {} moves".format(prev_tool["start_cool_speed"]/60,prev_tool["end_cool_speed"]/60,prev_tool["cooling_moves"]))
# TODO: figure out the cooling speed calc, it doesn't agree with PS at the moment. Probably because Y/X movement.
for move in cooling_steps:
gcode.append("G1 E{:.3f} F{}".format(printer["cooling_tube_length"],move))
gcode.append("G1 E-{:.3f} F{}".format(printer["cooling_tube_length"],next(cooling_steps)))
gcode.append("G1 E-{:.4f} F2000".format(park_retract))
gcode.append("G4 S0")
purge = tools[last["T"]]["purge"][tool]
diameter = tools[tool]["filament_diameter"]
retract = tools[tool]["retract"]
retract_speed = tools[tool]["retract_speed"]
vol_rate = tools[tool]["max_vol_rate"]
min_purge = tools[tool]["min_purge_vol"]
# Calc max linear feedrate from volumetric:
maxrate_mms = vol_rate/(math.pi*math.pow(0.5*diameter,2))
maxrate = maxrate_mms*60.0
length = purge / (math.pi * math.pow(0.5*diameter, 2))
min_length = min_purge / (math.pi * math.pow(0.5*diameter, 2))
if WIPING_OBJECTS and last["tc_id"] in object_purge.keys():
if last["Z"]<= LAST_SOLID_Z and object_purge[last["tc_id"]]>0:
gcode.append("; BAD PURGE DETECTED - Plicer issue #2855. Ignoring.")
else:
gcode.append("; {} wipe: object holds {} mm".format(last["tc_id"], object_purge[last["tc_id"]]))
bucket_purge = length - object_purge[last["tc_id"]]
gcode.append("; {:.4f} mm in bucket (min {:.2f}), remainder in infill/object ".format(bucket_purge,min_length))
bucket_purge = max(bucket_purge,min_length)
length = bucket_purge
gcode.append(line) # Reinsert toolchange/T-code
# TODO: support non-similar temperatures/true multi-material?
gcode.append("G4 S0")
gcode.append("G1 E{:.4f} F{:.0f}".format(reload_distance*.2,tools[tool]["start_load_speed"])) # Prime to the nozzle. This can be reasonably fast.
# HACK: the speed constant here doesn't agree with the PS code's outuput, probably because it's also moving in X
gcode.append("G1 E{:.4f} F{:.0f}".format(reload_distance*.7,tools[tool]["end_load_speed"])) # Prime to the
gcode.append("G1 E{:.4f} F{:.0f}".format(reload_distance*.1,tools[tool]["end_load_speed"]*0.1)) # Prime to the
gcode.append("G4 S0; sync")
gcode.append("; Purge generate for {:.2f} mm ({:.2f}mm^3) at F {:.0f}".format(length,purge,maxrate))
# TODO - swap purge behaviour depending on mechanism.
gcode += purge_generate_RPM(length, maxrate)
# HACK - There's a PS bug here where sometimes it won't use TC_retract after a toolchange. We peek ahead
# to work aroud it.
_peekAhead = []
_nextLine = "";
while (not _nextLine.startswith("G1 E")):
_nextLine = next(fp)
_peekAhead.append(_nextLine)
_splitLine = _nextLine.split(" ")
if (float(_splitLine[1][1:]) <0): # Bug is a random normal ret/unret combo instead of TC retract. Do nothing.
gcode.append("; PS bug detected, not retracting...")
else:
gcode.append("G1 E-{:.4f} F{:.4f}; TC retract".format(tools[tool]["retract_tc"],tools[tool]["retract_speed"]))
gcode.append("M220 R")
gcode.append("G1 F6000") # context specific or always fixed?
gcode.append("G4 S0")
gcode.append("G92 E0")
gcode.append("; BUCKET TOOL CHANGE END")
gcode.append("; ----------------------")
gcode += _peekAhead
continue
if "; Unload filament" in line:
# We're all done, let everything else go through
gcode.append("M107")
gcode.append("G1 E-15.0000 F3000")
gcode.append(line)
done = True
continue
if line.startswith("G1 "):
for bit in line.split(";")[0].split(" "):
if len(bit)== 0 or bit == "":
continue
coord = bit[0]
value = float(bit[1:])
if coord not in ["X", "Y", "Z"]:
continue
last[coord] = value
if last["X"] > MAX_X:
print >> sys.stderr, "ERROR: Print extends into RPM activation area!"
print >> sys.stderr, line
sys.exit(1)
# Skip everything until we get back in bounds
gcode.append(line)
fp.close()
if len(sys.argv) > 2:
fp = open(sys.argv[2], 'w')
else:
fp = open(sys.argv[1], 'w')
fp.write("\n".join(gcode))
fp.close()