1 from __future__ import absolute_import
2 from __future__ import division
4 import os, traceback, math, re, zlib, base64, time, sys, platform, glob, string, stat
5 import cPickle as pickle
6 if sys.version_info[0] < 3:
9 import configparser as ConfigParser
11 from Cura.util import resources
12 from Cura.util import version
14 #########################################################
15 ## Default settings when none are found.
16 #########################################################
18 #Single place to store the defaults, so we have a consistent set of default settings.
19 profileDefaultSettings = {
21 'layer_height': '0.2',
22 'wall_thickness': '0.8',
23 'solid_layer_thickness': '0.6',
25 'skirt_line_count': '1',
28 'print_temperature': '220',
29 'print_bed_temperature': '70',
31 'filament_diameter': '2.89',
32 'filament_density': '1.00',
33 'retraction_min_travel': '5.0',
34 'retraction_enable': 'False',
35 'retraction_speed': '40.0',
36 'retraction_amount': '4.5',
37 'retraction_extra': '0.0',
38 'retract_on_jumps_only': 'True',
39 'travel_speed': '150',
41 'bottom_layer_speed': '20',
42 'cool_min_layer_time': '5',
43 'fan_enabled': 'True',
46 'fan_speed_max': '100',
53 'model_rotate_base': '0',
54 'model_multiply_x': '1',
55 'model_multiply_y': '1',
56 'extra_base_wall_thickness': '0.0',
57 'sequence': 'Loops > Perimeter > Infill',
58 'force_first_layer_sequence': 'True',
59 'infill_type': 'Line',
63 'support_distance': '0.5',
64 'support_dual_extrusion': 'False',
66 'enable_skin': 'False',
67 'enable_raft': 'False',
68 'cool_min_feedrate': '10',
69 'bridge_speed': '100',
71 'raft_base_material_amount': '100',
72 'raft_interface_material_amount': '100',
73 'bottom_thickness': '0.3',
74 'hop_on_move': 'False',
76 'object_center_x': '-1',
77 'object_center_y': '-1',
79 'gcode_extension': 'gcode',
80 'alternative_center': '',
85 #######################################################################################
86 'start.gcode': """;Sliced {filename} at: {day} {date} {time}
87 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
88 ;Print time: {print_time}
89 ;Filament used: {filament_amount}m {filament_weight}g
90 ;Filament cost: {filament_cost}
92 G90 ;absolute positioning
93 M107 ;start with the fan off
95 G28 X0 Y0 ;move X/Y to min endstops
96 G28 Z0 ;move Z to min endstops
97 G92 X0 Y0 Z0 E0 ;reset software position to front/left/z=0.0
99 G1 Z15.0 F{max_z_speed} ;move the platform down 15mm
101 G92 E0 ;zero the extruded length
102 G1 F200 E3 ;extrude 3mm of feed stock
103 G92 E0 ;zero the extruded length again
106 #######################################################################################
107 'end.gcode': """;End GCode
108 M104 S0 ;extruder heater off
109 M140 S0 ;heated bed heater off (if you have it)
111 G91 ;relative positioning
112 G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
113 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
114 G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way
117 G90 ;absolute positioning
119 #######################################################################################
120 'support_start.gcode': '',
121 'support_end.gcode': '',
122 'cool_start.gcode': '',
123 'cool_end.gcode': '',
125 #######################################################################################
126 'nextobject.gcode': """;Move to next object on the platform. clear_z is the minimal z height we need to make sure we do not hit any objects.
129 G91 ;relative positioning
130 G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
131 G1 Z+0.5 E-5 F{travel_speed} ;move Z up a bit and retract filament even more
132 G90 ;absolute positioning
134 G1 Z{clear_z} F{max_z_speed}
136 G1 X{object_center_x} Y{object_center_x} F{travel_speed}
140 #######################################################################################
141 'switchExtruder.gcode': """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
150 preferencesDefaultSettings = {
151 'startMode': 'Simple',
152 'lastFile': os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'resources', 'example', 'UltimakerRobot_support.stl')),
153 'machine_width': '205',
154 'machine_depth': '205',
155 'machine_height': '200',
156 'machine_type': 'unknown',
157 'ultimaker_extruder_upgrade': 'False',
158 'has_heated_bed': 'False',
159 'extruder_amount': '1',
160 'extruder_offset_x1': '-22.0',
161 'extruder_offset_y1': '0.0',
162 'extruder_offset_x2': '0.0',
163 'extruder_offset_y2': '0.0',
164 'extruder_offset_x3': '0.0',
165 'extruder_offset_y3': '0.0',
166 'filament_density': '1300',
168 'serial_port': 'AUTO',
169 'serial_port_auto': '',
170 'serial_baud': 'AUTO',
171 'serial_baud_auto': '',
172 'slicer': 'Cura (Skeinforge based)',
173 'save_profile': 'False',
174 'filament_cost_kg': '0',
175 'filament_cost_meter': '0',
177 'sdshortnames': 'False',
178 'check_for_updates': 'True',
180 'planner_always_autoplace': 'True',
181 'extruder_head_size_min_x': '75.0',
182 'extruder_head_size_min_y': '18.0',
183 'extruder_head_size_max_x': '18.0',
184 'extruder_head_size_max_y': '35.0',
185 'extruder_head_size_height': '60.0',
187 'model_colour': '#72CB30',
188 'model_colour2': '#CB3030',
189 'model_colour3': '#DDD93C',
190 'model_colour4': '#4550D3',
193 #########################################################
194 ## Profile and preferences functions
195 #########################################################
198 def getDefaultProfilePath():
199 if platform.system() == "Windows":
200 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
201 #If we have a frozen python install, we need to step out of the library.zip
202 if hasattr(sys, 'frozen'):
203 basePath = os.path.normpath(os.path.join(basePath, ".."))
205 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
206 if not os.path.isdir(basePath):
207 os.makedirs(basePath)
208 return os.path.join(basePath, 'current_profile.ini')
210 def loadGlobalProfile(filename):
211 #Read a configuration file as global config
212 global globalProfileParser
213 globalProfileParser = ConfigParser.ConfigParser()
214 globalProfileParser.read(filename)
216 def resetGlobalProfile():
217 #Read a configuration file as global config
218 global globalProfileParser
219 globalProfileParser = ConfigParser.ConfigParser()
221 if getPreference('machine_type') == 'ultimaker':
222 putProfileSetting('nozzle_size', '0.4')
223 if getPreference('ultimaker_extruder_upgrade') == 'True':
224 putProfileSetting('retraction_enable', 'True')
226 putProfileSetting('nozzle_size', '0.5')
228 def saveGlobalProfile(filename):
229 #Save the current profile to an ini file
230 globalProfileParser.write(open(filename, 'w'))
232 def loadGlobalProfileFromString(options):
233 global globalProfileParser
234 globalProfileParser = ConfigParser.ConfigParser()
235 globalProfileParser.add_section('profile')
236 globalProfileParser.add_section('alterations')
237 options = base64.b64decode(options)
238 options = zlib.decompress(options)
239 (profileOpts, alt) = options.split('\f', 1)
240 for option in profileOpts.split('\b'):
242 (key, value) = option.split('=', 1)
243 globalProfileParser.set('profile', key, value)
244 for option in alt.split('\b'):
246 (key, value) = option.split('=', 1)
247 globalProfileParser.set('alterations', key, value)
249 def getGlobalProfileString():
250 global globalProfileParser
251 if not globals().has_key('globalProfileParser'):
252 loadGlobalProfile(getDefaultProfilePath())
257 if globalProfileParser.has_section('profile'):
258 for key in globalProfileParser.options('profile'):
259 if key in tempOverride:
260 p.append(key + "=" + tempOverride[key])
263 p.append(key + "=" + globalProfileParser.get('profile', key))
264 if globalProfileParser.has_section('alterations'):
265 for key in globalProfileParser.options('alterations'):
266 if key in tempOverride:
267 p.append(key + "=" + tempOverride[key])
270 alt.append(key + "=" + globalProfileParser.get('alterations', key))
271 for key in tempOverride:
272 if key not in tempDone:
273 p.append(key + "=" + tempOverride[key])
274 ret = '\b'.join(p) + '\f' + '\b'.join(alt)
275 ret = base64.b64encode(zlib.compress(ret, 9))
278 def getProfileSetting(name):
279 if name in tempOverride:
280 return unicode(tempOverride[name], "utf-8")
281 #Check if we have a configuration file loaded, else load the default.
282 if not globals().has_key('globalProfileParser'):
283 loadGlobalProfile(getDefaultProfilePath())
284 if not globalProfileParser.has_option('profile', name):
285 if name in profileDefaultSettings:
286 default = profileDefaultSettings[name]
288 print("Missing default setting for: '" + name + "'")
289 profileDefaultSettings[name] = ''
291 if not globalProfileParser.has_section('profile'):
292 globalProfileParser.add_section('profile')
293 globalProfileParser.set('profile', name, str(default))
294 #print(name + " not found in profile, so using default: " + str(default))
296 return globalProfileParser.get('profile', name)
298 def getProfileSettingFloat(name):
300 setting = getProfileSetting(name).replace(',', '.')
301 return float(eval(setting, {}, {}))
302 except (ValueError, SyntaxError, TypeError):
305 def putProfileSetting(name, value):
306 #Check if we have a configuration file loaded, else load the default.
307 if not globals().has_key('globalProfileParser'):
308 loadGlobalProfile(getDefaultProfilePath())
309 if not globalProfileParser.has_section('profile'):
310 globalProfileParser.add_section('profile')
311 globalProfileParser.set('profile', name, str(value))
313 def isProfileSetting(name):
314 if name in profileDefaultSettings:
318 ## Preferences functions
319 global globalPreferenceParser
320 globalPreferenceParser = None
322 def getPreferencePath():
323 if platform.system() == "Windows":
324 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
325 #If we have a frozen python install, we need to step out of the library.zip
326 if hasattr(sys, 'frozen'):
327 basePath = os.path.normpath(os.path.join(basePath, ".."))
329 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
330 if not os.path.isdir(basePath):
331 os.makedirs(basePath)
332 return os.path.join(basePath, 'preferences.ini')
334 def getPreferenceFloat(name):
336 setting = getPreference(name).replace(',', '.')
337 return float(eval(setting, {}, {}))
338 except (ValueError, SyntaxError, TypeError):
341 def getPreferenceColour(name):
342 colorString = getPreference(name)
343 return [float(int(colorString[1:3], 16)) / 255, float(int(colorString[3:5], 16)) / 255, float(int(colorString[5:7], 16)) / 255, 1.0]
345 def getPreference(name):
346 if name in tempOverride:
347 return unicode(tempOverride[name])
348 global globalPreferenceParser
349 if globalPreferenceParser is None:
350 globalPreferenceParser = ConfigParser.ConfigParser()
351 globalPreferenceParser.read(getPreferencePath())
352 if not globalPreferenceParser.has_option('preference', name):
353 if name in preferencesDefaultSettings:
354 default = preferencesDefaultSettings[name]
356 print("Missing default setting for: '" + name + "'")
357 preferencesDefaultSettings[name] = ''
359 if not globalPreferenceParser.has_section('preference'):
360 globalPreferenceParser.add_section('preference')
361 globalPreferenceParser.set('preference', name, str(default))
362 #print(name + " not found in preferences, so using default: " + str(default))
364 return unicode(globalPreferenceParser.get('preference', name), "utf-8")
366 def putPreference(name, value):
367 #Check if we have a configuration file loaded, else load the default.
368 global globalPreferenceParser
369 if globalPreferenceParser == None:
370 globalPreferenceParser = ConfigParser.ConfigParser()
371 globalPreferenceParser.read(getPreferencePath())
372 if not globalPreferenceParser.has_section('preference'):
373 globalPreferenceParser.add_section('preference')
374 globalPreferenceParser.set('preference', name, unicode(value).encode("utf-8"))
375 globalPreferenceParser.write(open(getPreferencePath(), 'w'))
377 def isPreference(name):
378 if name in preferencesDefaultSettings:
382 ## Temp overrides for multi-extruder slicing and the project planner.
384 def setTempOverride(name, value):
385 tempOverride[name] = unicode(value).encode("utf-8")
386 def clearTempOverride(name):
387 del tempOverride[name]
388 def resetTempOverride():
391 #########################################################
392 ## Utility functions to calculate common profile values
393 #########################################################
394 def calculateEdgeWidth():
395 wallThickness = getProfileSettingFloat('wall_thickness')
396 nozzleSize = getProfileSettingFloat('nozzle_size')
398 if wallThickness < nozzleSize:
401 lineCount = int(wallThickness / nozzleSize + 0.0001)
402 lineWidth = wallThickness / lineCount
403 lineWidthAlt = wallThickness / (lineCount + 1)
404 if lineWidth > nozzleSize * 1.5:
408 def calculateLineCount():
409 wallThickness = getProfileSettingFloat('wall_thickness')
410 nozzleSize = getProfileSettingFloat('nozzle_size')
412 if wallThickness < nozzleSize:
415 lineCount = int(wallThickness / nozzleSize + 0.0001)
416 lineWidth = wallThickness / lineCount
417 lineWidthAlt = wallThickness / (lineCount + 1)
418 if lineWidth > nozzleSize * 1.5:
422 def calculateSolidLayerCount():
423 layerHeight = getProfileSettingFloat('layer_height')
424 solidThickness = getProfileSettingFloat('solid_layer_thickness')
425 return int(math.ceil(solidThickness / layerHeight - 0.0001))
427 def getMachineCenterCoords():
428 return [getPreferenceFloat('machine_width') / 2, getPreferenceFloat('machine_depth') / 2]
430 def getObjectMatrix():
431 rotate = getProfileSettingFloat('model_rotate_base')
432 rotate = rotate / 180.0 * math.pi
433 scaleX = getProfileSettingFloat('model_scale')
434 scaleY = getProfileSettingFloat('model_scale')
435 scaleZ = getProfileSettingFloat('model_scale')
436 if getProfileSetting('flipX') == 'True':
438 if getProfileSetting('flipY') == 'True':
440 if getProfileSetting('flipZ') == 'True':
442 mat00 = math.cos(rotate) * scaleX
443 mat01 =-math.sin(rotate) * scaleY
444 mat10 = math.sin(rotate) * scaleX
445 mat11 = math.cos(rotate) * scaleY
447 mat = [mat00,mat10,0, mat01,mat11,0, 0,0,scaleZ]
448 if getProfileSetting('swap_xz') == 'True':
449 mat = mat[6:9] + mat[3:6] + mat[0:3]
450 if getProfileSetting('swap_yz') == 'True':
451 mat = mat[0:3] + mat[6:9] + mat[3:6]
454 #########################################################
455 ## Alteration file functions
456 #########################################################
457 def replaceTagMatch(m):
461 return pre + time.strftime('%H:%M:%S').encode('utf-8', 'replace')
463 return pre + time.strftime('%d %b %Y').encode('utf-8', 'replace')
465 return pre + time.strftime('%a').encode('utf-8', 'replace')
466 if tag == 'print_time':
467 return pre + '#P_TIME#'
468 if tag == 'filament_amount':
469 return pre + '#F_AMNT#'
470 if tag == 'filament_weight':
471 return pre + '#F_WGHT#'
472 if tag == 'filament_cost':
473 return pre + '#F_COST#'
474 if pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
475 f = getProfileSettingFloat(tag) * 60
476 elif isProfileSetting(tag):
477 f = getProfileSettingFloat(tag)
478 elif isPreference(tag):
479 f = getProfileSettingFloat(tag)
481 return '%s?%s?' % (pre, tag)
483 return pre + str(int(f))
486 def replaceGCodeTags(filename, gcodeInt):
487 f = open(filename, 'r+')
489 data = data.replace('#P_TIME#', ('%5d:%02d' % (int(gcodeInt.totalMoveTimeMinute / 60), int(gcodeInt.totalMoveTimeMinute % 60)))[-8:])
490 data = data.replace('#F_AMNT#', ('%8.2f' % (gcodeInt.extrusionAmount / 1000))[-8:])
491 data = data.replace('#F_WGHT#', ('%8.2f' % (gcodeInt.calculateWeight() * 1000))[-8:])
492 cost = gcodeInt.calculateCost()
495 data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
500 ### Get aleration raw contents. (Used internally in Cura)
501 def getAlterationFile(filename):
502 #Check if we have a configuration file loaded, else load the default.
503 if not globals().has_key('globalProfileParser'):
504 loadGlobalProfile(getDefaultProfilePath())
506 if not globalProfileParser.has_option('alterations', filename):
507 if filename in alterationDefault:
508 default = alterationDefault[filename]
510 print("Missing default alteration for: '" + filename + "'")
511 alterationDefault[filename] = ''
513 if not globalProfileParser.has_section('alterations'):
514 globalProfileParser.add_section('alterations')
515 #print("Using default for: %s" % (filename))
516 globalProfileParser.set('alterations', filename, default)
517 return unicode(globalProfileParser.get('alterations', filename), "utf-8")
519 def setAlterationFile(filename, value):
520 #Check if we have a configuration file loaded, else load the default.
521 if not globals().has_key('globalProfileParser'):
522 loadGlobalProfile(getDefaultProfilePath())
523 if not globalProfileParser.has_section('alterations'):
524 globalProfileParser.add_section('alterations')
525 globalProfileParser.set('alterations', filename, value.encode("utf-8"))
526 saveGlobalProfile(getDefaultProfilePath())
528 ### Get the alteration file for output. (Used by Skeinforge)
529 def getAlterationFileContents(filename):
532 alterationContents = getAlterationFile(filename)
533 if filename == 'start.gcode':
534 #For the start code, hack the temperature and the steps per E value into it. So the temperature is reached before the start code extrusion.
535 #We also set our steps per E here, if configured.
536 eSteps = getPreferenceFloat('steps_per_e')
538 prefix += 'M92 E%f\n' % (eSteps)
539 temp = getProfileSettingFloat('print_temperature')
541 if getPreference('has_heated_bed') == 'True':
542 bedTemp = getProfileSettingFloat('print_bed_temperature')
544 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
545 prefix += 'M140 S%f\n' % (bedTemp)
546 if temp > 0 and not '{print_temperature}' in alterationContents:
547 prefix += 'M109 S%f\n' % (temp)
548 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
549 prefix += 'M190 S%f\n' % (bedTemp)
550 elif filename == 'end.gcode':
551 #Append the profile string to the end of the GCode, so we can load it from the GCode file later.
552 postfix = ';CURA_PROFILE_STRING:%s\n' % (getGlobalProfileString())
553 elif filename == 'replace.csv':
554 #Always remove the extruder on/off M codes. These are no longer needed in 5D printing.
555 prefix = 'M101\nM103\n'
556 elif filename == 'support_start.gcode' or filename == 'support_end.gcode':
557 #Add support start/end code
558 if getProfileSetting('support_dual_extrusion') == 'True' and int(getPreference('extruder_amount')) > 1:
559 if filename == 'support_start.gcode':
560 setTempOverride('extruder', '1')
562 setTempOverride('extruder', '0')
563 alterationContents = getAlterationFileContents('switchExtruder.gcode')
564 clearTempOverride('extruder')
566 alterationContents = ''
567 return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n'
571 def getPluginConfig():
573 return pickle.loads(getProfileSetting('plugin_config'))
577 def setPluginConfig(config):
578 putProfileSetting('plugin_config', pickle.dumps(config))
580 def getPluginBasePaths():
582 if platform.system() != "Windows":
583 ret.append(os.path.expanduser('~/.cura/plugins/'))
584 if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
585 ret.append(os.path.normpath(os.path.join(resources.resourceBasePath, "Cura/plugins")))
587 ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
592 for basePath in getPluginBasePaths():
593 for filename in glob.glob(os.path.join(basePath, '*.py')):
594 filename = os.path.basename(filename)
595 if filename.startswith('_'):
597 with open(os.path.join(basePath, filename), "r") as f:
598 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
601 if not line.startswith('#'):
603 line = line[1:].split(':', 1)
606 if line[0].upper() == 'NAME':
607 item['name'] = line[1].strip()
608 elif line[0].upper() == 'INFO':
609 item['info'] = line[1].strip()
610 elif line[0].upper() == 'TYPE':
611 item['type'] = line[1].strip()
612 elif line[0].upper() == 'DEPEND':
614 elif line[0].upper() == 'PARAM':
615 m = re.match('([a-zA-Z]*)\(([a-zA-Z_]*)(?::([^\)]*))?\) +(.*)', line[1].strip())
617 item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
619 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
620 if item['name'] != None and item['type'] == 'postprocess':
624 def runPostProcessingPlugins(gcodefilename):
625 pluginConfigList = getPluginConfig()
626 pluginList = getPluginList()
628 for pluginConfig in pluginConfigList:
630 for pluginTest in pluginList:
631 if pluginTest['filename'] == pluginConfig['filename']:
637 for basePath in getPluginBasePaths():
638 testFilename = os.path.join(basePath, pluginConfig['filename'])
639 if os.path.isfile(testFilename):
640 pythonFile = testFilename
641 if pythonFile is None:
644 locals = {'filename': gcodefilename}
645 for param in plugin['params']:
646 value = param['default']
647 if param['name'] in pluginConfig['params']:
648 value = pluginConfig['params'][param['name']]
650 if param['type'] == 'float':
654 value = float(param['default'])
656 locals[param['name']] = value
658 execfile(pythonFile, locals)
660 locationInfo = traceback.extract_tb(sys.exc_info()[2])[-1]
661 return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])
664 def getSDcardDrives():
666 if platform.system() == "Windows":
667 from ctypes import windll
668 bitmask = windll.kernel32.GetLogicalDrives()
669 for letter in string.uppercase:
671 drives.append(letter + ':/')
673 if platform.system() == "Darwin":
675 for volume in glob.glob('/Volumes/*'):
676 if stat.S_ISLNK(os.lstat(volume).st_mode):
678 drives.append(volume)