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 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine'))
45 class EngineResult(object):
47 Result from running the CuraEngine.
48 Contains the engine log, polygons retrieved from the engine, the GCode and some meta-data.
52 self._gcodeData = StringIO.StringIO()
54 self._replaceInfo = {}
56 self._printTimeSeconds = None
57 self._filamentMM = [0.0] * 4
58 self._modelHash = None
59 self._profileString = profile.getProfileString()
60 self._preferencesString = profile.getPreferencesString()
61 self._gcodeInterpreter = gcodeInterpreter.gcode()
62 self._gcodeLoadThread = None
63 self._finished = False
65 def getFilamentWeight(self, e=0):
66 #Calculates the weight of the filament in kg
67 radius = float(profile.getProfileSetting('filament_diameter')) / 2
68 volumeM3 = (self._filamentMM[e] * (math.pi * radius * radius)) / (1000*1000*1000)
69 return volumeM3 * profile.getPreferenceFloat('filament_physical_density')
71 def getFilamentCost(self, e=0):
72 cost_kg = profile.getPreferenceFloat('filament_cost_kg')
73 cost_meter = profile.getPreferenceFloat('filament_cost_meter')
74 if cost_kg > 0.0 and cost_meter > 0.0:
75 return "%.2f / %.2f" % (self.getFilamentWeight(e) * cost_kg, self._filamentMM[e] / 1000.0 * cost_meter)
77 return "%.2f" % (self.getFilamentWeight(e) * cost_kg)
78 elif cost_meter > 0.0:
79 return "%.2f" % (self._filamentMM[e] / 1000.0 * cost_meter)
82 def getPrintTime(self):
83 if int(self._printTimeSeconds / 60 / 60) < 1:
84 return '%d minutes' % (int(self._printTimeSeconds / 60) % 60)
85 if int(self._printTimeSeconds / 60 / 60) == 1:
86 return '%d hour %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
87 return '%d hours %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
89 def getFilamentAmount(self, e=0):
90 if self._filamentMM[e] == 0.0:
92 return '%0.2f meter %0.0f gram' % (float(self._filamentMM[e]) / 1000.0, self.getFilamentWeight(e) * 1000.0)
95 return self._engineLog
98 data = self._gcodeData.getvalue()
99 if len(self._replaceInfo) > 0:
100 block0 = data[0:2048]
101 for k, v in self._replaceInfo.items():
102 v = (v + ' ' * len(k))[:len(k)]
103 block0 = block0.replace(k, v)
104 return block0 + data[2048:]
107 def setGCode(self, gcode):
108 self._gcodeData = StringIO.StringIO(gcode)
109 self._replaceInfo = {}
111 def addLog(self, line):
112 self._engineLog.append(line)
114 def setHash(self, hash):
115 self._modelHash = hash
117 def setFinished(self, result):
118 self._finished = result
120 def isFinished(self):
121 return self._finished
123 def getGCodeLayers(self, loadCallback):
124 if not self._finished:
126 if self._gcodeInterpreter.layerList is None and self._gcodeLoadThread is None:
127 self._gcodeInterpreter.progressCallback = self._gcodeInterpreterCallback
128 self._gcodeLoadThread = threading.Thread(target=lambda : self._gcodeInterpreter.load(self._gcodeData))
129 self._gcodeLoadCallback = loadCallback
130 self._gcodeLoadThread.daemon = True
131 self._gcodeLoadThread.start()
132 return self._gcodeInterpreter.layerList
134 def _gcodeInterpreterCallback(self, progress):
135 if len(self._gcodeInterpreter.layerList) % 5 == 0:
137 return self._gcodeLoadCallback(self, progress)
139 def submitInfoOnline(self):
140 if profile.getPreference('submit_slice_information') != 'True':
142 if version.isDevVersion():
145 'processor': platform.processor(),
146 'machine': platform.machine(),
147 'platform': platform.platform(),
148 'profile': self._profileString,
149 'preferences': self._preferencesString,
150 'modelhash': self._modelHash,
151 'version': version.getVersion(),
154 f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
160 class Engine(object):
162 Class used to communicate with the CuraEngine.
163 The CuraEngine is ran as a 2nd process and reports back information trough stderr.
164 GCode trough stdout and has a socket connection for polygon information and loading the 3D model into the engine.
166 GUI_CMD_REQUEST_MESH = 0x01
167 GUI_CMD_SEND_POLYGONS = 0x02
169 def __init__(self, progressCallback):
172 self._callback = progressCallback
173 self._progressSteps = ['inset', 'skin', 'export']
177 self._serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
178 self._serverPortNr = 0xC20A
181 self._serversocket.bind(('127.0.0.1', self._serverPortNr))
183 print "Failed to listen on port: %d" % (self._serverPortNr)
184 self._serverPortNr += 1
185 if self._serverPortNr > 0xFFFF:
186 print "Failed to listen on any port..."
190 print 'Listening for engine communications on %d' % (self._serverPortNr)
191 self._serversocket.listen(1)
192 thread = threading.Thread(target=self._socketListenThread)
196 def _socketListenThread(self):
198 sock, _ = self._serversocket.accept()
199 thread = threading.Thread(target=self._socketConnectionThread, args=(sock,))
203 def _socketConnectionThread(self, sock):
212 cmd = struct.unpack('@i', data)[0]
213 if cmd == self.GUI_CMD_REQUEST_MESH:
214 meshInfo = self._modelData[0]
215 self._modelData = self._modelData[1:]
216 sock.sendall(struct.pack('@i', meshInfo[0]))
217 sock.sendall(meshInfo[1].tostring())
218 elif cmd == self.GUI_CMD_SEND_POLYGONS:
219 cnt = struct.unpack('@i', sock.recv(4))[0]
220 layerNr = struct.unpack('@i', sock.recv(4))[0]
221 z = struct.unpack('@i', sock.recv(4))[0]
222 z = float(z) / 1000.0
223 typeNameLen = struct.unpack('@i', sock.recv(4))[0]
224 typeName = sock.recv(typeNameLen)
225 while len(self._result._polygons) < layerNr + 1:
226 self._result._polygons.append({})
227 polygons = self._result._polygons[layerNr]
228 if typeName not in polygons:
229 polygons[typeName] = []
230 for n in xrange(0, cnt):
231 length = struct.unpack('@i', sock.recv(4))[0]
233 while len(data) < length * 8 * 2:
234 recvData = sock.recv(length * 8 * 2 - len(data))
235 if len(recvData) < 1:
238 polygon2d = numpy.array(numpy.fromstring(data, numpy.int64), numpy.float32) / 1000.0
239 polygon2d = polygon2d.reshape((len(polygon2d) / 2, 2))
240 polygon = numpy.empty((len(polygon2d), 3), numpy.float32)
241 polygon[:,:-1] = polygon2d
243 polygons[typeName].append(polygon)
245 print "Unknown command on socket: %x" % (cmd)
249 self._serversocket.close()
251 def abortEngine(self):
252 if self._process is not None:
254 self._process.terminate()
257 if self._thread is not None:
262 if self._thread is not None:
268 def runEngine(self, scene):
269 if len(scene.objects()) < 1:
272 for obj in scene.objects():
273 if scene.checkPlatform(obj):
274 extruderCount = max(extruderCount, len(obj._meshList))
276 extruderCount = max(extruderCount, profile.minimalExtruderCount())
278 commandList = [getEngineFilename(), '-v', '-p']
279 for k, v in self._engineSettings(extruderCount).iteritems():
280 commandList += ['-s', '%s=%s' % (k, str(v))]
281 commandList += ['-g', '%d' % (self._serverPortNr)]
284 hash = hashlib.sha512()
285 order = scene.printOrder()
287 pos = numpy.array(profile.getMachineCenterCoords()) * 1000
290 for obj in scene.objects():
291 if scene.checkPlatform(obj):
292 oMin = obj.getMinimum()[0:2] + obj.getPosition()
293 oMax = obj.getMaximum()[0:2] + obj.getPosition()
298 objMin[0] = min(oMin[0], objMin[0])
299 objMin[1] = min(oMin[1], objMin[1])
300 objMax[0] = max(oMax[0], objMax[0])
301 objMax[1] = max(oMax[1], objMax[1])
304 pos += (objMin + objMax) / 2.0 * 1000
305 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
307 vertexTotal = [0] * 4
309 for obj in scene.objects():
310 if scene.checkPlatform(obj):
311 meshMax = max(meshMax, len(obj._meshList))
312 for n in xrange(0, len(obj._meshList)):
313 vertexTotal[n] += obj._meshList[n].vertexCount
315 for n in xrange(0, meshMax):
316 verts = numpy.zeros((0, 3), numpy.float32)
317 for obj in scene.objects():
318 if scene.checkPlatform(obj):
319 if n < len(obj._meshList):
320 vertexes = (numpy.matrix(obj._meshList[n].vertexes, copy = False) * numpy.matrix(obj._matrix, numpy.float32)).getA()
321 vertexes -= obj._drawOffset
322 vertexes += numpy.array([obj.getPosition()[0], obj.getPosition()[1], 0.0])
323 verts = numpy.concatenate((verts, vertexes))
324 hash.update(obj._meshList[n].vertexes.tostring())
325 engineModelData.append((vertexTotal[n], verts))
327 commandList += ['$' * meshMax]
331 obj = scene.objects()[n]
332 for mesh in obj._meshList:
333 engineModelData.append((mesh.vertexCount, mesh.vertexes))
334 hash.update(mesh.vertexes.tostring())
335 pos = obj.getPosition() * 1000
336 pos += numpy.array(profile.getMachineCenterCoords()) * 1000
337 commandList += ['-m', ','.join(map(str, obj._matrix.getA().flatten()))]
338 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
339 commandList += ['$' * len(obj._meshList)]
341 modelHash = hash.hexdigest()
342 if self._objCount > 0:
343 self._modelData = engineModelData
344 self._thread = threading.Thread(target=self._watchProcess, args=(commandList, self._thread, modelHash))
345 self._thread.daemon = True
348 def _watchProcess(self, commandList, oldThread, modelHash):
349 if oldThread is not None:
350 if self._process is not None:
351 self._process.terminate()
355 self._process = self._runEngineProcess(commandList)
357 traceback.print_exc()
359 if self._thread != threading.currentThread():
360 self._process.terminate()
362 self._result = EngineResult()
363 self._result.setHash(modelHash)
366 logThread = threading.Thread(target=self._watchStderr, args=(self._process.stderr,))
367 logThread.daemon = True
370 data = self._process.stdout.read(4096)
372 self._result._gcodeData.write(data)
373 data = self._process.stdout.read(4096)
375 returnCode = self._process.wait()
378 pluginError = pluginInfo.runPostProcessingPlugins(self._result)
379 if pluginError is not None:
381 self._result.addLog(pluginError)
382 self._result.setFinished(True)
385 for line in self._result.getLog():
390 def _watchStderr(self, stderr):
392 line = stderr.readline()
395 if line.startswith('Progress:'):
396 line = line.split(':')
397 if line[1] == 'process':
399 elif line[1] in self._progressSteps:
400 progressValue = float(line[2]) / float(line[3])
401 progressValue /= len(self._progressSteps)
402 progressValue += 1.0 / len(self._progressSteps) * self._progressSteps.index(line[1])
404 progressValue /= self._objCount
405 progressValue += 1.0 / self._objCount * objectNr
407 self._callback(progressValue)
410 elif line.startswith('Print time:'):
411 self._result._printTimeSeconds = int(line.split(':')[1].strip())
412 elif line.startswith('Filament:'):
413 self._result._filamentMM[0] = int(line.split(':')[1].strip())
414 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
415 radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
416 self._result._filamentMM[0] /= (math.pi * radius * radius)
417 elif line.startswith('Filament2:'):
418 self._result._filamentMM[1] = int(line.split(':')[1].strip())
419 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
420 radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
421 self._result._filamentMM[1] /= (math.pi * radius * radius)
422 elif line.startswith('Replace:'):
423 self._result._replaceInfo[line.split(':')[1].strip()] = line.split(':')[2].strip()
425 self._result.addLog(line)
426 line = stderr.readline()
428 def _engineSettings(self, extruderCount):
430 'layerThickness': int(profile.getProfileSettingFloat('layer_height') * 1000),
431 'initialLayerThickness': int(profile.getProfileSettingFloat('bottom_thickness') * 1000) if profile.getProfileSettingFloat('bottom_thickness') > 0.0 else int(profile.getProfileSettingFloat('layer_height') * 1000),
432 'filamentDiameter': int(profile.getProfileSettingFloat('filament_diameter') * 1000),
433 'filamentFlow': int(profile.getProfileSettingFloat('filament_flow')),
434 'extrusionWidth': int(profile.calculateEdgeWidth() * 1000),
435 'insetCount': int(profile.calculateLineCount()),
436 'downSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_bottom') == 'True' else 0,
437 'upSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_top') == 'True' else 0,
438 'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
439 'initialSpeedupLayers': int(4),
440 'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
441 'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
442 'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
443 'inset0Speed': int(profile.getProfileSettingFloat('inset0_speed')) if int(profile.getProfileSettingFloat('inset0_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
444 'insetXSpeed': int(profile.getProfileSettingFloat('insetx_speed')) if int(profile.getProfileSettingFloat('insetx_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
445 'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
446 'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
447 'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
448 'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(profile.getProfileSettingFloat('support_angle')),
449 'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
450 'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
451 'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
452 'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
453 '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),
454 'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
455 'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
456 'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
457 'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
458 'retractionZHop': int(profile.getProfileSettingFloat('retraction_hop') * 1000),
459 'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
460 'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
461 'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
462 'objectSink': max(0, int(profile.getProfileSettingFloat('object_sink') * 1000)),
463 'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
464 'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
465 'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
466 'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
467 'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
469 'extruderOffset[1].X': int(profile.getMachineSettingFloat('extruder_offset_x1') * 1000),
470 'extruderOffset[1].Y': int(profile.getMachineSettingFloat('extruder_offset_y1') * 1000),
471 'extruderOffset[2].X': int(profile.getMachineSettingFloat('extruder_offset_x2') * 1000),
472 'extruderOffset[2].Y': int(profile.getMachineSettingFloat('extruder_offset_y2') * 1000),
473 'extruderOffset[3].X': int(profile.getMachineSettingFloat('extruder_offset_x3') * 1000),
474 'extruderOffset[3].Y': int(profile.getMachineSettingFloat('extruder_offset_y3') * 1000),
477 fanFullHeight = int(profile.getProfileSettingFloat('fan_full_height') * 1000)
478 settings['fanFullOnLayerNr'] = (fanFullHeight - settings['initialLayerThickness'] - 1) / settings['layerThickness'] + 1
479 if settings['fanFullOnLayerNr'] < 0:
480 settings['fanFullOnLayerNr'] = 0
481 if profile.getProfileSetting('support_type') == 'Lines':
482 settings['supportType'] = 1
484 if profile.getProfileSettingFloat('fill_density') == 0:
485 settings['sparseInfillLineDistance'] = -1
486 elif profile.getProfileSettingFloat('fill_density') == 100:
487 settings['sparseInfillLineDistance'] = settings['extrusionWidth']
488 #Set the up/down skins height to 10000 if we want a 100% filled object.
489 # This gives better results then normal 100% infill as the sparse and up/down skin have some overlap.
490 settings['downSkinCount'] = 10000
491 settings['upSkinCount'] = 10000
493 settings['sparseInfillLineDistance'] = int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density'))
494 if profile.getProfileSetting('platform_adhesion') == 'Brim':
495 settings['skirtDistance'] = 0
496 settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
497 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
498 settings['skirtDistance'] = 0
499 settings['skirtLineCount'] = 0
500 settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
501 settings['raftLineSpacing'] = int(profile.getProfileSettingFloat('raft_line_spacing') * 1000)
502 settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
503 settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
504 settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
505 settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
507 settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
508 settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
509 settings['skirtMinLength'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
511 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
512 settings['fixHorrible'] |= 0x01
513 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
514 settings['fixHorrible'] |= 0x02
515 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
516 settings['fixHorrible'] |= 0x10
517 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
518 settings['fixHorrible'] |= 0x04
520 if settings['layerThickness'] <= 0:
521 settings['layerThickness'] = 1000
522 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
523 settings['gcodeFlavor'] = 1
524 if profile.getProfileSetting('spiralize') == 'True':
525 settings['spiralizeMode'] = 1
526 if profile.getProfileSetting('wipe_tower') == 'True' and extruderCount > 1:
527 settings['wipeTowerSize'] = int(math.sqrt(profile.getProfileSettingFloat('wipe_tower_volume') * 1000 * 1000 * 1000 / settings['layerThickness']))
528 if profile.getProfileSetting('ooze_shield') == 'True':
529 settings['enableOozeShield'] = 1
532 def _runEngineProcess(self, cmdList):
534 if subprocess.mswindows:
535 su = subprocess.STARTUPINFO()
536 su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
537 su.wShowWindow = subprocess.SW_HIDE
538 kwargs['startupinfo'] = su
539 kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
540 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)