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._modelData = engineModelData
354 self._thread = threading.Thread(target=self._watchProcess, args=(commandList, self._thread, modelHash))
355 self._thread.daemon = True
358 def _watchProcess(self, commandList, oldThread, modelHash):
359 if oldThread is not None:
360 if self._process is not None:
361 self._process.terminate()
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.setHash(modelHash)
376 logThread = threading.Thread(target=self._watchStderr, args=(self._process.stderr,))
377 logThread.daemon = True
380 data = self._process.stdout.read(4096)
382 self._result._gcodeData.write(data)
383 data = self._process.stdout.read(4096)
385 returnCode = self._process.wait()
388 pluginError = pluginInfo.runPostProcessingPlugins(self._result)
389 if pluginError is not None:
391 self._result.addLog(pluginError)
392 self._result.setFinished(True)
395 for line in self._result.getLog():
400 def _watchStderr(self, stderr):
402 line = stderr.readline()
405 if line.startswith('Progress:'):
406 line = line.split(':')
407 if line[1] == 'process':
409 elif line[1] in self._progressSteps:
410 progressValue = float(line[2]) / float(line[3])
411 progressValue /= len(self._progressSteps)
412 progressValue += 1.0 / len(self._progressSteps) * self._progressSteps.index(line[1])
414 progressValue /= self._objCount
415 progressValue += 1.0 / self._objCount * objectNr
417 self._callback(progressValue)
420 elif line.startswith('Print time:'):
421 self._result._printTimeSeconds = int(line.split(':')[1].strip())
422 elif line.startswith('Filament:'):
423 self._result._filamentMM[0] = int(line.split(':')[1].strip())
424 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
425 radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
426 self._result._filamentMM[0] /= (math.pi * radius * radius)
427 elif line.startswith('Filament2:'):
428 self._result._filamentMM[1] = int(line.split(':')[1].strip())
429 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
430 radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
431 self._result._filamentMM[1] /= (math.pi * radius * radius)
432 elif line.startswith('Replace:'):
433 self._result._replaceInfo[line.split(':')[1].strip()] = line.split(':')[2].strip()
435 self._result.addLog(line)
436 line = stderr.readline()
438 def _engineSettings(self, extruderCount):
440 'layerThickness': int(profile.getProfileSettingFloat('layer_height') * 1000),
441 'initialLayerThickness': int(profile.getProfileSettingFloat('bottom_thickness') * 1000) if profile.getProfileSettingFloat('bottom_thickness') > 0.0 else int(profile.getProfileSettingFloat('layer_height') * 1000),
442 'filamentDiameter': int(profile.getProfileSettingFloat('filament_diameter') * 1000),
443 'filamentFlow': int(profile.getProfileSettingFloat('filament_flow')),
444 'extrusionWidth': int(profile.calculateEdgeWidth() * 1000),
445 'insetCount': int(profile.calculateLineCount()),
446 'downSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_bottom') == 'True' else 0,
447 'upSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_top') == 'True' else 0,
448 'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
449 'initialSpeedupLayers': int(4),
450 'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
451 'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
452 'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
453 'inset0Speed': int(profile.getProfileSettingFloat('inset0_speed')) if int(profile.getProfileSettingFloat('inset0_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
454 'insetXSpeed': int(profile.getProfileSettingFloat('insetx_speed')) if int(profile.getProfileSettingFloat('insetx_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
455 'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
456 'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
457 'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
458 'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(profile.getProfileSettingFloat('support_angle')),
459 'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
460 'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
461 'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
462 'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
463 '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),
464 'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
465 'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
466 'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
467 'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
468 'retractionZHop': int(profile.getProfileSettingFloat('retraction_hop') * 1000),
469 'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
470 'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
471 'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
472 'objectSink': max(0, int(profile.getProfileSettingFloat('object_sink') * 1000)),
473 'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
474 'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
475 'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
476 'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
477 'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
479 'extruderOffset[1].X': int(profile.getMachineSettingFloat('extruder_offset_x1') * 1000),
480 'extruderOffset[1].Y': int(profile.getMachineSettingFloat('extruder_offset_y1') * 1000),
481 'extruderOffset[2].X': int(profile.getMachineSettingFloat('extruder_offset_x2') * 1000),
482 'extruderOffset[2].Y': int(profile.getMachineSettingFloat('extruder_offset_y2') * 1000),
483 'extruderOffset[3].X': int(profile.getMachineSettingFloat('extruder_offset_x3') * 1000),
484 'extruderOffset[3].Y': int(profile.getMachineSettingFloat('extruder_offset_y3') * 1000),
487 fanFullHeight = int(profile.getProfileSettingFloat('fan_full_height') * 1000)
488 settings['fanFullOnLayerNr'] = (fanFullHeight - settings['initialLayerThickness'] - 1) / settings['layerThickness'] + 1
489 if settings['fanFullOnLayerNr'] < 0:
490 settings['fanFullOnLayerNr'] = 0
491 if profile.getProfileSetting('support_type') == 'Lines':
492 settings['supportType'] = 1
494 if profile.getProfileSettingFloat('fill_density') == 0:
495 settings['sparseInfillLineDistance'] = -1
496 elif profile.getProfileSettingFloat('fill_density') == 100:
497 settings['sparseInfillLineDistance'] = settings['extrusionWidth']
498 #Set the up/down skins height to 10000 if we want a 100% filled object.
499 # This gives better results then normal 100% infill as the sparse and up/down skin have some overlap.
500 settings['downSkinCount'] = 10000
501 settings['upSkinCount'] = 10000
503 settings['sparseInfillLineDistance'] = int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density'))
504 if profile.getProfileSetting('platform_adhesion') == 'Brim':
505 settings['skirtDistance'] = 0
506 settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
507 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
508 settings['skirtDistance'] = 0
509 settings['skirtLineCount'] = 0
510 settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
511 settings['raftLineSpacing'] = int(profile.getProfileSettingFloat('raft_line_spacing') * 1000)
512 settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
513 settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
514 settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
515 settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
517 settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
518 settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
519 settings['skirtMinLength'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
521 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
522 settings['fixHorrible'] |= 0x01
523 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
524 settings['fixHorrible'] |= 0x02
525 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
526 settings['fixHorrible'] |= 0x10
527 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
528 settings['fixHorrible'] |= 0x04
530 if settings['layerThickness'] <= 0:
531 settings['layerThickness'] = 1000
532 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
533 settings['gcodeFlavor'] = 1
534 elif profile.getMachineSetting('gcode_flavor') == 'MakerBot':
535 settings['gcodeFlavor'] = 2
536 if profile.getProfileSetting('spiralize') == 'True':
537 settings['spiralizeMode'] = 1
538 if profile.getProfileSetting('wipe_tower') == 'True' and extruderCount > 1:
539 settings['wipeTowerSize'] = int(math.sqrt(profile.getProfileSettingFloat('wipe_tower_volume') * 1000 * 1000 * 1000 / settings['layerThickness']))
540 if profile.getProfileSetting('ooze_shield') == 'True':
541 settings['enableOozeShield'] = 1
544 def _runEngineProcess(self, cmdList):
546 if subprocess.mswindows:
547 su = subprocess.STARTUPINFO()
548 su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
549 su.wShowWindow = subprocess.SW_HIDE
550 kwargs['startupinfo'] = su
551 kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
553 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)