2 Slice engine communication.
3 This module handles all communication with the slicing engine.
5 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
21 import cStringIO as StringIO
23 from Cura.util import profile
24 from Cura.util import pluginInfo
25 from Cura.util import version
26 from Cura.util import gcodeInterpreter
28 def getEngineFilename():
30 Finds and returns the path to the current engine executable. This is OS depended.
31 :return: The full path to the engine executable.
33 if platform.system() == 'Windows':
34 if version.isDevVersion() and os.path.exists('C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'):
35 return 'C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'
36 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine.exe'))
37 if hasattr(sys, 'frozen'):
38 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../..', 'CuraEngine'))
39 if os.path.isfile('/usr/bin/CuraEngine'):
40 return '/usr/bin/CuraEngine'
41 if os.path.isfile('/usr/local/bin/CuraEngine'):
42 return '/usr/local/bin/CuraEngine'
43 tempPath = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine'))
44 if os.path.isdir(tempPath):
45 tempPath = os.path.join(tempPath,'CuraEngine')
48 class EngineResult(object):
50 Result from running the CuraEngine.
51 Contains the engine log, polygons retrieved from the engine, the GCode and some meta-data.
55 self._gcodeData = StringIO.StringIO()
57 self._replaceInfo = {}
59 self._printTimeSeconds = None
60 self._filamentMM = [0.0] * 4
61 self._modelHash = None
62 self._profileString = profile.getProfileString()
63 self._preferencesString = profile.getPreferencesString()
64 self._gcodeInterpreter = gcodeInterpreter.gcode()
65 self._gcodeLoadThread = None
66 self._finished = False
68 def getFilamentWeight(self, e=0):
69 #Calculates the weight of the filament in kg
70 radius = float(profile.getProfileSetting('filament_diameter')) / 2
71 volumeM3 = (self._filamentMM[e] * (math.pi * radius * radius)) / (1000*1000*1000)
72 return volumeM3 * profile.getPreferenceFloat('filament_physical_density')
74 def getFilamentCost(self, e=0):
75 cost_kg = profile.getPreferenceFloat('filament_cost_kg')
76 cost_meter = profile.getPreferenceFloat('filament_cost_meter')
77 if cost_kg > 0.0 and cost_meter > 0.0:
78 return "%.2f / %.2f" % (self.getFilamentWeight(e) * cost_kg, self._filamentMM[e] / 1000.0 * cost_meter)
80 return "%.2f" % (self.getFilamentWeight(e) * cost_kg)
81 elif cost_meter > 0.0:
82 return "%.2f" % (self._filamentMM[e] / 1000.0 * cost_meter)
85 def getPrintTime(self):
86 if self._printTimeSeconds is None:
88 if int(self._printTimeSeconds / 60 / 60) < 1:
89 return '%d minutes' % (int(self._printTimeSeconds / 60) % 60)
90 if int(self._printTimeSeconds / 60 / 60) == 1:
91 return '%d hour %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
92 return '%d hours %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
94 def getFilamentAmount(self, e=0):
95 if self._filamentMM[e] == 0.0:
97 return '%0.2f meter %0.0f gram' % (float(self._filamentMM[e]) / 1000.0, self.getFilamentWeight(e) * 1000.0)
100 return self._engineLog
103 data = self._gcodeData.getvalue()
104 if len(self._replaceInfo) > 0:
105 block0 = data[0:2048]
106 for k, v in self._replaceInfo.items():
107 v = (v + ' ' * len(k))[:len(k)]
108 block0 = block0.replace(k, v)
109 return block0 + data[2048:]
112 def setGCode(self, gcode):
113 self._gcodeData = StringIO.StringIO(gcode)
114 self._replaceInfo = {}
116 def addLog(self, line):
117 self._engineLog.append(line)
119 def setHash(self, hash):
120 self._modelHash = hash
122 def setFinished(self, result):
123 self._finished = result
125 def isFinished(self):
126 return self._finished
128 def getGCodeLayers(self, loadCallback):
129 if not self._finished:
131 if self._gcodeInterpreter.layerList is None and self._gcodeLoadThread is None:
132 self._gcodeInterpreter.progressCallback = self._gcodeInterpreterCallback
133 self._gcodeLoadThread = threading.Thread(target=lambda : self._gcodeInterpreter.load(self._gcodeData))
134 self._gcodeLoadCallback = loadCallback
135 self._gcodeLoadThread.daemon = True
136 self._gcodeLoadThread.start()
137 return self._gcodeInterpreter.layerList
139 def _gcodeInterpreterCallback(self, progress):
140 if len(self._gcodeInterpreter.layerList) % 5 == 0:
142 return self._gcodeLoadCallback(self, progress)
144 def submitInfoOnline(self):
145 if profile.getPreference('submit_slice_information') != 'True':
147 if version.isDevVersion():
150 'processor': platform.processor(),
151 'machine': platform.machine(),
152 'platform': platform.platform(),
153 'profile': self._profileString,
154 'preferences': self._preferencesString,
155 'modelhash': self._modelHash,
156 'version': version.getVersion(),
159 f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
165 class Engine(object):
167 Class used to communicate with the CuraEngine.
168 The CuraEngine is ran as a 2nd process and reports back information trough stderr.
169 GCode trough stdout and has a socket connection for polygon information and loading the 3D model into the engine.
171 GUI_CMD_REQUEST_MESH = 0x01
172 GUI_CMD_SEND_POLYGONS = 0x02
173 GUI_CMD_FINISH_OBJECT = 0x03
175 def __init__(self, progressCallback):
178 self._callback = progressCallback
179 self._progressSteps = ['inset', 'skin', 'export']
183 self._serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
184 self._serverPortNr = 0xC20A
187 self._serversocket.bind(('127.0.0.1', self._serverPortNr))
189 print "Failed to listen on port: %d" % (self._serverPortNr)
190 self._serverPortNr += 1
191 if self._serverPortNr > 0xFFFF:
192 print "Failed to listen on any port..."
196 print 'Listening for engine communications on %d' % (self._serverPortNr)
197 self._serversocket.listen(1)
198 thread = threading.Thread(target=self._socketListenThread)
202 def _socketListenThread(self):
204 sock, _ = self._serversocket.accept()
205 thread = threading.Thread(target=self._socketConnectionThread, args=(sock,))
209 def _socketConnectionThread(self, sock):
219 cmd = struct.unpack('@i', data)[0]
220 if cmd == self.GUI_CMD_REQUEST_MESH:
221 meshInfo = self._modelData[0]
222 self._modelData = self._modelData[1:]
223 sock.sendall(struct.pack('@i', meshInfo[0]))
224 sock.sendall(meshInfo[1].tostring())
225 elif cmd == self.GUI_CMD_SEND_POLYGONS:
226 cnt = struct.unpack('@i', sock.recv(4))[0]
227 layerNr = struct.unpack('@i', sock.recv(4))[0]
228 layerNr += layerNrOffset
229 z = struct.unpack('@i', sock.recv(4))[0]
230 z = float(z) / 1000.0
231 typeNameLen = struct.unpack('@i', sock.recv(4))[0]
232 typeName = sock.recv(typeNameLen)
233 while len(self._result._polygons) < layerNr + 1:
234 self._result._polygons.append({})
235 polygons = self._result._polygons[layerNr]
236 if typeName not in polygons:
237 polygons[typeName] = []
238 for n in xrange(0, cnt):
239 length = struct.unpack('@i', sock.recv(4))[0]
241 while len(data) < length * 8 * 2:
242 recvData = sock.recv(length * 8 * 2 - len(data))
243 if len(recvData) < 1:
246 polygon2d = numpy.array(numpy.fromstring(data, numpy.int64), numpy.float32) / 1000.0
247 polygon2d = polygon2d.reshape((len(polygon2d) / 2, 2))
248 polygon = numpy.empty((len(polygon2d), 3), numpy.float32)
249 polygon[:,:-1] = polygon2d
251 polygons[typeName].append(polygon)
252 elif cmd == self.GUI_CMD_FINISH_OBJECT:
253 layerNrOffset = len(self._result._polygons)
255 print "Unknown command on socket: %x" % (cmd)
259 self._serversocket.close()
261 def abortEngine(self):
262 if self._process is not None:
264 self._process.terminate()
267 if self._thread is not None:
272 if self._thread is not None:
278 def runEngine(self, scene):
279 if len(scene.objects()) < 1:
282 for obj in scene.objects():
283 if scene.checkPlatform(obj):
284 extruderCount = max(extruderCount, len(obj._meshList))
286 extruderCount = max(extruderCount, profile.minimalExtruderCount())
288 commandList = [getEngineFilename(), '-v', '-p']
289 for k, v in self._engineSettings(extruderCount).iteritems():
290 commandList += ['-s', '%s=%s' % (k, str(v))]
291 commandList += ['-g', '%d' % (self._serverPortNr)]
294 hash = hashlib.sha512()
295 order = scene.printOrder()
297 pos = numpy.array(profile.getMachineCenterCoords()) * 1000
300 for obj in scene.objects():
301 if scene.checkPlatform(obj):
302 oMin = obj.getMinimum()[0:2] + obj.getPosition()
303 oMax = obj.getMaximum()[0:2] + obj.getPosition()
308 objMin[0] = min(oMin[0], objMin[0])
309 objMin[1] = min(oMin[1], objMin[1])
310 objMax[0] = max(oMax[0], objMax[0])
311 objMax[1] = max(oMax[1], objMax[1])
314 pos += (objMin + objMax) / 2.0 * 1000
315 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
317 vertexTotal = [0] * 4
319 for obj in scene.objects():
320 if scene.checkPlatform(obj):
321 meshMax = max(meshMax, len(obj._meshList))
322 for n in xrange(0, len(obj._meshList)):
323 vertexTotal[n] += obj._meshList[n].vertexCount
325 for n in xrange(0, meshMax):
326 verts = numpy.zeros((0, 3), numpy.float32)
327 for obj in scene.objects():
328 if scene.checkPlatform(obj):
329 if n < len(obj._meshList):
330 vertexes = (numpy.matrix(obj._meshList[n].vertexes, copy = False) * numpy.matrix(obj._matrix, numpy.float32)).getA()
331 vertexes -= obj._drawOffset
332 vertexes += numpy.array([obj.getPosition()[0], obj.getPosition()[1], 0.0])
333 verts = numpy.concatenate((verts, vertexes))
334 hash.update(obj._meshList[n].vertexes.tostring())
335 engineModelData.append((vertexTotal[n], verts))
337 commandList += ['$' * meshMax]
341 obj = scene.objects()[n]
342 for mesh in obj._meshList:
343 engineModelData.append((mesh.vertexCount, mesh.vertexes))
344 hash.update(mesh.vertexes.tostring())
345 pos = obj.getPosition() * 1000
346 pos += numpy.array(profile.getMachineCenterCoords()) * 1000
347 commandList += ['-m', ','.join(map(str, obj._matrix.getA().flatten()))]
348 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
349 commandList += ['$' * len(obj._meshList)]
351 modelHash = hash.hexdigest()
352 if self._objCount > 0:
353 self._thread = threading.Thread(target=self._watchProcess, args=(commandList, self._thread, engineModelData, modelHash))
354 self._thread.daemon = True
357 def _watchProcess(self, commandList, oldThread, engineModelData, modelHash):
358 if oldThread is not None:
359 if self._process is not None:
360 self._process.terminate()
363 self._modelData = engineModelData
365 self._process = self._runEngineProcess(commandList)
367 traceback.print_exc()
369 if self._thread != threading.currentThread():
370 self._process.terminate()
372 self._result = EngineResult()
373 self._result.addLog('Running: %s' % (' '.join(commandList)))
374 self._result.setHash(modelHash)
377 logThread = threading.Thread(target=self._watchStderr, args=(self._process.stderr,))
378 logThread.daemon = True
381 data = self._process.stdout.read(4096)
383 self._result._gcodeData.write(data)
384 data = self._process.stdout.read(4096)
386 returnCode = self._process.wait()
389 pluginError = pluginInfo.runPostProcessingPlugins(self._result)
390 if pluginError is not None:
392 self._result.addLog(pluginError)
393 self._result.setFinished(True)
396 for line in self._result.getLog():
401 def _watchStderr(self, stderr):
403 line = stderr.readline()
406 if line.startswith('Progress:'):
407 line = line.split(':')
408 if line[1] == 'process':
410 elif line[1] in self._progressSteps:
411 progressValue = float(line[2]) / float(line[3])
412 progressValue /= len(self._progressSteps)
413 progressValue += 1.0 / len(self._progressSteps) * self._progressSteps.index(line[1])
415 progressValue /= self._objCount
416 progressValue += 1.0 / self._objCount * objectNr
418 self._callback(progressValue)
421 elif line.startswith('Print time:'):
422 self._result._printTimeSeconds = int(line.split(':')[1].strip())
423 elif line.startswith('Filament:'):
424 self._result._filamentMM[0] = int(line.split(':')[1].strip())
425 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
426 radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
427 self._result._filamentMM[0] /= (math.pi * radius * radius)
428 elif line.startswith('Filament2:'):
429 self._result._filamentMM[1] = int(line.split(':')[1].strip())
430 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
431 radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
432 self._result._filamentMM[1] /= (math.pi * radius * radius)
433 elif line.startswith('Replace:'):
434 self._result._replaceInfo[line.split(':')[1].strip()] = line.split(':')[2].strip()
436 self._result.addLog(line)
437 line = stderr.readline()
439 def _engineSettings(self, extruderCount):
441 'layerThickness': int(profile.getProfileSettingFloat('layer_height') * 1000),
442 'initialLayerThickness': int(profile.getProfileSettingFloat('bottom_thickness') * 1000) if profile.getProfileSettingFloat('bottom_thickness') > 0.0 else int(profile.getProfileSettingFloat('layer_height') * 1000),
443 'filamentDiameter': int(profile.getProfileSettingFloat('filament_diameter') * 1000),
444 'filamentFlow': int(profile.getProfileSettingFloat('filament_flow')),
445 'extrusionWidth': int(profile.calculateEdgeWidth() * 1000),
446 'layer0extrusionWidth': int(profile.calculateEdgeWidth() * 1000),
447 'insetCount': int(profile.calculateLineCount()),
448 'downSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_bottom') == 'True' else 0,
449 'upSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_top') == 'True' else 0,
450 'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
451 'initialSpeedupLayers': int(4),
452 'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
453 'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
454 'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
455 'inset0Speed': int(profile.getProfileSettingFloat('inset0_speed')) if int(profile.getProfileSettingFloat('inset0_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
456 'insetXSpeed': int(profile.getProfileSettingFloat('insetx_speed')) if int(profile.getProfileSettingFloat('insetx_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
457 'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
458 'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
459 'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
460 'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(profile.getProfileSettingFloat('support_angle')),
461 'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
462 'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
463 'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
464 'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
465 'supportExtruder': 0 if profile.getProfileSetting('support_dual_extrusion') == 'First extruder' else (1 if profile.getProfileSetting('support_dual_extrusion') == 'Second extruder' and profile.minimalExtruderCount() > 1 else -1),
466 'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
467 'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
468 'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
469 'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
470 'retractionZHop': int(profile.getProfileSettingFloat('retraction_hop') * 1000),
471 'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
472 'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
473 'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
474 'objectSink': max(0, int(profile.getProfileSettingFloat('object_sink') * 1000)),
475 'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
476 'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
477 'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
478 'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
479 'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
481 'extruderOffset[1].X': int(profile.getMachineSettingFloat('extruder_offset_x1') * 1000),
482 'extruderOffset[1].Y': int(profile.getMachineSettingFloat('extruder_offset_y1') * 1000),
483 'extruderOffset[2].X': int(profile.getMachineSettingFloat('extruder_offset_x2') * 1000),
484 'extruderOffset[2].Y': int(profile.getMachineSettingFloat('extruder_offset_y2') * 1000),
485 'extruderOffset[3].X': int(profile.getMachineSettingFloat('extruder_offset_x3') * 1000),
486 'extruderOffset[3].Y': int(profile.getMachineSettingFloat('extruder_offset_y3') * 1000),
489 fanFullHeight = int(profile.getProfileSettingFloat('fan_full_height') * 1000)
490 settings['fanFullOnLayerNr'] = (fanFullHeight - settings['initialLayerThickness'] - 1) / settings['layerThickness'] + 1
491 if settings['fanFullOnLayerNr'] < 0:
492 settings['fanFullOnLayerNr'] = 0
493 if profile.getProfileSetting('support_type') == 'Lines':
494 settings['supportType'] = 1
496 if profile.getProfileSettingFloat('fill_density') == 0:
497 settings['sparseInfillLineDistance'] = -1
498 elif profile.getProfileSettingFloat('fill_density') == 100:
499 settings['sparseInfillLineDistance'] = settings['extrusionWidth']
500 #Set the up/down skins height to 10000 if we want a 100% filled object.
501 # This gives better results then normal 100% infill as the sparse and up/down skin have some overlap.
502 settings['downSkinCount'] = 10000
503 settings['upSkinCount'] = 10000
505 settings['sparseInfillLineDistance'] = int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density'))
506 if profile.getProfileSetting('platform_adhesion') == 'Brim':
507 settings['skirtDistance'] = 0
508 settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
509 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
510 settings['skirtDistance'] = 0
511 settings['skirtLineCount'] = 0
512 settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
513 settings['raftLineSpacing'] = int(profile.getProfileSettingFloat('raft_line_spacing') * 1000)
514 settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
515 settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
516 settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
517 settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
519 settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
520 settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
521 settings['skirtMinLength'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
523 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
524 settings['fixHorrible'] |= 0x01
525 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
526 settings['fixHorrible'] |= 0x02
527 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
528 settings['fixHorrible'] |= 0x10
529 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
530 settings['fixHorrible'] |= 0x04
532 if settings['layerThickness'] <= 0:
533 settings['layerThickness'] = 1000
534 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
535 settings['gcodeFlavor'] = 1
536 elif profile.getMachineSetting('gcode_flavor') == 'MakerBot':
537 settings['gcodeFlavor'] = 2
538 elif profile.getMachineSetting('gcode_flavor') == 'BFB':
539 settings['gcodeFlavor'] = 3
540 elif profile.getMachineSetting('gcode_flavor') == 'Mach3':
541 settings['gcodeFlavor'] = 4
542 if profile.getProfileSetting('spiralize') == 'True':
543 settings['spiralizeMode'] = 1
544 if profile.getProfileSetting('wipe_tower') == 'True' and extruderCount > 1:
545 settings['wipeTowerSize'] = int(math.sqrt(profile.getProfileSettingFloat('wipe_tower_volume') * 1000 * 1000 * 1000 / settings['layerThickness']))
546 if profile.getProfileSetting('ooze_shield') == 'True':
547 settings['enableOozeShield'] = 1
550 def _runEngineProcess(self, cmdList):
552 if subprocess.mswindows:
553 su = subprocess.STARTUPINFO()
554 su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
555 su.wShowWindow = subprocess.SW_HIDE
556 kwargs['startupinfo'] = su
557 kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
558 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)