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():
29 if platform.system() == 'Windows':
30 if version.isDevVersion() and os.path.exists('C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'):
31 return 'C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'
32 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine.exe'))
33 if hasattr(sys, 'frozen'):
34 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../..', 'CuraEngine'))
35 if os.path.isfile('/usr/bin/CuraEngine'):
36 return '/usr/bin/CuraEngine'
37 if os.path.isfile('/usr/local/bin/CuraEngine'):
38 return '/usr/local/bin/CuraEngine'
39 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine'))
41 def getTempFilename():
42 warnings.simplefilter('ignore')
43 ret = os.tempnam(None, "Cura_Tmp")
44 warnings.simplefilter('default')
47 class EngineResult(object):
50 self._gcodeData = StringIO.StringIO()
52 self._replaceInfo = {}
54 self._printTimeSeconds = None
55 self._filamentMM = [0.0] * 4
56 self._modelHash = None
57 self._profileString = profile.getProfileString()
58 self._preferencesString = profile.getPreferencesString()
59 self._gcodeInterpreter = gcodeInterpreter.gcode()
60 self._gcodeLoadThread = None
61 self._finished = False
63 def getFilamentWeight(self, e=0):
64 #Calculates the weight of the filament in kg
65 radius = float(profile.getProfileSetting('filament_diameter')) / 2
66 volumeM3 = (self._filamentMM[e] * (math.pi * radius * radius)) / (1000*1000*1000)
67 return volumeM3 * profile.getPreferenceFloat('filament_physical_density')
69 def getFilamentCost(self, e=0):
70 cost_kg = profile.getPreferenceFloat('filament_cost_kg')
71 cost_meter = profile.getPreferenceFloat('filament_cost_meter')
72 if cost_kg > 0.0 and cost_meter > 0.0:
73 return "%.2f / %.2f" % (self.getFilamentWeight(e) * cost_kg, self._filamentMM[e] / 1000.0 * cost_meter)
75 return "%.2f" % (self.getFilamentWeight(e) * cost_kg)
76 elif cost_meter > 0.0:
77 return "%.2f" % (self._filamentMM[e] / 1000.0 * cost_meter)
80 def getPrintTime(self):
81 if int(self._printTimeSeconds / 60 / 60) < 1:
82 return '%d minutes' % (int(self._printTimeSeconds / 60) % 60)
83 if int(self._printTimeSeconds / 60 / 60) == 1:
84 return '%d hour %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
85 return '%d hours %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
87 def getFilamentAmount(self, e=0):
88 if self._filamentMM[e] == 0.0:
90 return '%0.2f meter %0.0f gram' % (float(self._filamentMM[e]) / 1000.0, self.getFilamentWeight(e) * 1000.0)
93 return self._engineLog
96 data = self._gcodeData.getvalue()
97 if len(self._replaceInfo) > 0:
99 for k, v in self._replaceInfo.items():
100 v = (v + ' ' * len(k))[:len(k)]
101 block0 = block0.replace(k, v)
102 return block0 + data[2048:]
105 def setGCode(self, gcode):
106 self._gcodeData = StringIO.StringIO(gcode)
107 self._replaceInfo = {}
109 def addLog(self, line):
110 self._engineLog.append(line)
112 def setHash(self, hash):
113 self._modelHash = hash
115 def setFinished(self, result):
116 self._finished = result
118 def isFinished(self):
119 return self._finished
121 def getGCodeLayers(self, loadCallback):
122 if not self._finished:
124 if self._gcodeInterpreter.layerList is None and self._gcodeLoadThread is None:
125 self._gcodeInterpreter.progressCallback = self._gcodeInterpreterCallback
126 self._gcodeLoadThread = threading.Thread(target=lambda : self._gcodeInterpreter.load(self._gcodeData))
127 self._gcodeLoadCallback = loadCallback
128 self._gcodeLoadThread.daemon = True
129 self._gcodeLoadThread.start()
130 return self._gcodeInterpreter.layerList
132 def _gcodeInterpreterCallback(self, progress):
133 if len(self._gcodeInterpreter.layerList) % 5 == 0:
135 return self._gcodeLoadCallback(self, progress)
137 def submitInfoOnline(self):
138 if profile.getPreference('submit_slice_information') != 'True':
140 if version.isDevVersion():
143 'processor': platform.processor(),
144 'machine': platform.machine(),
145 'platform': platform.platform(),
146 'profile': self._profileString,
147 'preferences': self._preferencesString,
148 'modelhash': self._modelHash,
149 'version': version.getVersion(),
152 f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
158 class Engine(object):
159 GUI_CMD_REQUEST_MESH = 0x01
160 GUI_CMD_SEND_POLYGONS = 0x02
162 def __init__(self, progressCallback):
165 self._callback = progressCallback
166 self._progressSteps = ['inset', 'skin', 'export']
170 self._serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
171 self._serverPortNr = 0xC20A
174 self._serversocket.bind(('127.0.0.1', self._serverPortNr))
176 print "Failed to listen on port: %d" % (self._serverPortNr)
177 self._serverPortNr += 1
178 if self._serverPortNr > 0xFFFF:
179 print "Failed to listen on any port..."
183 print 'Listening for engine communications on %d' % (self._serverPortNr)
184 self._serversocket.listen(1)
185 thread = threading.Thread(target=self._socketListenThread)
189 def _socketListenThread(self):
191 sock, _ = self._serversocket.accept()
192 thread = threading.Thread(target=self._socketConnectionThread, args=(sock,))
196 def _socketConnectionThread(self, sock):
205 cmd = struct.unpack('@i', data)[0]
206 if cmd == self.GUI_CMD_REQUEST_MESH:
207 meshInfo = self._modelData[0]
208 self._modelData = self._modelData[1:]
209 sock.sendall(struct.pack('@i', meshInfo[0]))
210 sock.sendall(meshInfo[1].tostring())
211 elif cmd == self.GUI_CMD_SEND_POLYGONS:
212 cnt = struct.unpack('@i', sock.recv(4))[0]
213 layerNr = struct.unpack('@i', sock.recv(4))[0]
214 z = struct.unpack('@i', sock.recv(4))[0]
215 z = float(z) / 1000.0
216 typeNameLen = struct.unpack('@i', sock.recv(4))[0]
217 typeName = sock.recv(typeNameLen)
218 while len(self._result._polygons) < layerNr + 1:
219 self._result._polygons.append({})
220 polygons = self._result._polygons[layerNr]
221 if typeName not in polygons:
222 polygons[typeName] = []
223 for n in xrange(0, cnt):
224 length = struct.unpack('@i', sock.recv(4))[0]
226 while len(data) < length * 8 * 2:
227 recvData = sock.recv(length * 8 * 2 - len(data))
228 if len(recvData) < 1:
231 polygon2d = numpy.array(numpy.fromstring(data, numpy.int64), numpy.float32) / 1000.0
232 polygon2d = polygon2d.reshape((len(polygon2d) / 2, 2))
233 polygon = numpy.empty((len(polygon2d), 3), numpy.float32)
234 polygon[:,:-1] = polygon2d
236 polygons[typeName].append(polygon)
238 print "Unknown command on socket: %x" % (cmd)
242 self._serversocket.close()
244 def abortEngine(self):
245 if self._process is not None:
247 self._process.terminate()
250 if self._thread is not None:
255 if self._thread is not None:
261 def runEngine(self, scene):
262 if len(scene.objects()) < 1:
265 for obj in scene.objects():
266 if scene.checkPlatform(obj):
267 extruderCount = max(extruderCount, len(obj._meshList))
269 extruderCount = max(extruderCount, profile.minimalExtruderCount())
271 commandList = [getEngineFilename(), '-v', '-p']
272 for k, v in self._engineSettings(extruderCount).iteritems():
273 commandList += ['-s', '%s=%s' % (k, str(v))]
274 commandList += ['-g', '%d' % (self._serverPortNr)]
277 hash = hashlib.sha512()
278 order = scene.printOrder()
280 pos = numpy.array(profile.getMachineCenterCoords()) * 1000
283 for obj in scene.objects():
284 if scene.checkPlatform(obj):
285 oMin = obj.getMinimum()[0:2] + obj.getPosition()
286 oMax = obj.getMaximum()[0:2] + obj.getPosition()
291 objMin[0] = min(oMin[0], objMin[0])
292 objMin[1] = min(oMin[1], objMin[1])
293 objMax[0] = max(oMax[0], objMax[0])
294 objMax[1] = max(oMax[1], objMax[1])
297 pos += (objMin + objMax) / 2.0 * 1000
298 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
300 vertexTotal = [0] * 4
302 for obj in scene.objects():
303 if scene.checkPlatform(obj):
304 meshMax = max(meshMax, len(obj._meshList))
305 for n in xrange(0, len(obj._meshList)):
306 vertexTotal[n] += obj._meshList[n].vertexCount
308 for n in xrange(0, meshMax):
309 verts = numpy.zeros((0, 3), numpy.float32)
310 for obj in scene.objects():
311 if scene.checkPlatform(obj):
312 if n < len(obj._meshList):
313 vertexes = (numpy.matrix(obj._meshList[n].vertexes, copy = False) * numpy.matrix(obj._matrix, numpy.float32)).getA()
314 vertexes -= obj._drawOffset
315 vertexes += numpy.array([obj.getPosition()[0], obj.getPosition()[1], 0.0])
316 verts = numpy.concatenate((verts, vertexes))
317 hash.update(obj._meshList[n].vertexes.tostring())
318 engineModelData.append((vertexTotal[n], verts))
320 commandList += ['$' * meshMax]
324 obj = scene.objects()[n]
325 for mesh in obj._meshList:
326 engineModelData.append((mesh.vertexCount, mesh.vertexes))
327 hash.update(mesh.vertexes.tostring())
328 pos = obj.getPosition() * 1000
329 pos += numpy.array(profile.getMachineCenterCoords()) * 1000
330 commandList += ['-m', ','.join(map(str, obj._matrix.getA().flatten()))]
331 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
332 commandList += ['$' * len(obj._meshList)]
334 modelHash = hash.hexdigest()
335 if self._objCount > 0:
336 self._modelData = engineModelData
337 self._thread = threading.Thread(target=self._watchProcess, args=(commandList, self._thread, modelHash))
338 self._thread.daemon = True
341 def _watchProcess(self, commandList, oldThread, modelHash):
342 if oldThread is not None:
343 if self._process is not None:
344 self._process.terminate()
348 self._process = self._runEngineProcess(commandList)
350 traceback.print_exc()
352 if self._thread != threading.currentThread():
353 self._process.terminate()
355 self._result = EngineResult()
356 self._result.setHash(modelHash)
359 logThread = threading.Thread(target=self._watchStderr, args=(self._process.stderr,))
360 logThread.daemon = True
363 data = self._process.stdout.read(4096)
365 self._result._gcodeData.write(data)
366 data = self._process.stdout.read(4096)
368 returnCode = self._process.wait()
371 pluginError = pluginInfo.runPostProcessingPlugins(self._result)
372 if pluginError is not None:
374 self._result.addLog(pluginError)
375 self._result.setFinished(True)
378 for line in self._result.getLog():
383 def _watchStderr(self, stderr):
385 line = stderr.readline()
388 if line.startswith('Progress:'):
389 line = line.split(':')
390 if line[1] == 'process':
392 elif line[1] in self._progressSteps:
393 progressValue = float(line[2]) / float(line[3])
394 progressValue /= len(self._progressSteps)
395 progressValue += 1.0 / len(self._progressSteps) * self._progressSteps.index(line[1])
397 progressValue /= self._objCount
398 progressValue += 1.0 / self._objCount * objectNr
400 self._callback(progressValue)
403 elif line.startswith('Print time:'):
404 self._result._printTimeSeconds = int(line.split(':')[1].strip())
405 elif line.startswith('Filament:'):
406 self._result._filamentMM[0] = int(line.split(':')[1].strip())
407 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
408 radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
409 self._result._filamentMM[0] /= (math.pi * radius * radius)
410 elif line.startswith('Filament2:'):
411 self._result._filamentMM[1] = int(line.split(':')[1].strip())
412 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
413 radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
414 self._result._filamentMM[1] /= (math.pi * radius * radius)
415 elif line.startswith('Replace:'):
416 self._result._replaceInfo[line.split(':')[1].strip()] = line.split(':')[2].strip()
418 self._result.addLog(line)
419 line = stderr.readline()
421 def _engineSettings(self, extruderCount):
423 'layerThickness': int(profile.getProfileSettingFloat('layer_height') * 1000),
424 'initialLayerThickness': int(profile.getProfileSettingFloat('bottom_thickness') * 1000) if profile.getProfileSettingFloat('bottom_thickness') > 0.0 else int(profile.getProfileSettingFloat('layer_height') * 1000),
425 'filamentDiameter': int(profile.getProfileSettingFloat('filament_diameter') * 1000),
426 'filamentFlow': int(profile.getProfileSettingFloat('filament_flow')),
427 'extrusionWidth': int(profile.calculateEdgeWidth() * 1000),
428 'insetCount': int(profile.calculateLineCount()),
429 'downSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_bottom') == 'True' else 0,
430 'upSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_top') == 'True' else 0,
431 'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
432 'initialSpeedupLayers': int(4),
433 'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
434 'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
435 'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
436 'inset0Speed': int(profile.getProfileSettingFloat('inset0_speed')) if int(profile.getProfileSettingFloat('inset0_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
437 'insetXSpeed': int(profile.getProfileSettingFloat('insetx_speed')) if int(profile.getProfileSettingFloat('insetx_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
438 'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
439 'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
440 'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
441 'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(profile.getProfileSettingFloat('support_angle')),
442 'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
443 'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
444 'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
445 'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
446 '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),
447 'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
448 'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
449 'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
450 'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
451 'retractionZHop': int(profile.getProfileSettingFloat('retraction_hop') * 1000),
452 'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
453 'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
454 'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
455 'objectSink': max(0, int(profile.getProfileSettingFloat('object_sink') * 1000)),
456 'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
457 'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
458 'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
459 'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
460 'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
462 'extruderOffset[1].X': int(profile.getMachineSettingFloat('extruder_offset_x1') * 1000),
463 'extruderOffset[1].Y': int(profile.getMachineSettingFloat('extruder_offset_y1') * 1000),
464 'extruderOffset[2].X': int(profile.getMachineSettingFloat('extruder_offset_x2') * 1000),
465 'extruderOffset[2].Y': int(profile.getMachineSettingFloat('extruder_offset_y2') * 1000),
466 'extruderOffset[3].X': int(profile.getMachineSettingFloat('extruder_offset_x3') * 1000),
467 'extruderOffset[3].Y': int(profile.getMachineSettingFloat('extruder_offset_y3') * 1000),
470 fanFullHeight = int(profile.getProfileSettingFloat('fan_full_height') * 1000)
471 settings['fanFullOnLayerNr'] = (fanFullHeight - settings['initialLayerThickness'] - 1) / settings['layerThickness'] + 1
472 if settings['fanFullOnLayerNr'] < 0:
473 settings['fanFullOnLayerNr'] = 0
475 if profile.getProfileSettingFloat('fill_density') == 0:
476 settings['sparseInfillLineDistance'] = -1
477 elif profile.getProfileSettingFloat('fill_density') == 100:
478 settings['sparseInfillLineDistance'] = settings['extrusionWidth']
479 #Set the up/down skins height to 10000 if we want a 100% filled object.
480 # This gives better results then normal 100% infill as the sparse and up/down skin have some overlap.
481 settings['downSkinCount'] = 10000
482 settings['upSkinCount'] = 10000
484 settings['sparseInfillLineDistance'] = int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density'))
485 if profile.getProfileSetting('platform_adhesion') == 'Brim':
486 settings['skirtDistance'] = 0
487 settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
488 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
489 settings['skirtDistance'] = 0
490 settings['skirtLineCount'] = 0
491 settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
492 settings['raftLineSpacing'] = int(profile.getProfileSettingFloat('raft_line_spacing') * 1000)
493 settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
494 settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
495 settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
496 settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
498 settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
499 settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
500 settings['skirtMinLength'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
502 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
503 settings['fixHorrible'] |= 0x01
504 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
505 settings['fixHorrible'] |= 0x02
506 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
507 settings['fixHorrible'] |= 0x10
508 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
509 settings['fixHorrible'] |= 0x04
511 if settings['layerThickness'] <= 0:
512 settings['layerThickness'] = 1000
513 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
514 settings['gcodeFlavor'] = 1
515 if profile.getProfileSetting('spiralize') == 'True':
516 settings['spiralizeMode'] = 1
517 if profile.getProfileSetting('wipe_tower') == 'True' and extruderCount > 1:
518 settings['wipeTowerSize'] = int(math.sqrt(profile.getProfileSettingFloat('wipe_tower_volume') * 1000 * 1000 * 1000 / settings['layerThickness']))
519 if profile.getProfileSetting('ooze_shield') == 'True':
520 settings['enableOozeShield'] = 1
523 def _runEngineProcess(self, cmdList):
525 if subprocess.mswindows:
526 su = subprocess.STARTUPINFO()
527 su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
528 su.wShowWindow = subprocess.SW_HIDE
529 kwargs['startupinfo'] = su
530 kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
531 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)