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_temperature2': '0',
30 'print_temperature3': '0',
31 'print_temperature4': '0',
32 'print_bed_temperature': '70',
34 'filament_diameter': '2.89',
35 'filament_diameter2': '0',
36 'filament_diameter3': '0',
37 'filament_diameter4': '0',
38 'filament_density': '1.00',
39 'retraction_min_travel': '5.0',
40 'retraction_enable': 'False',
41 'retraction_speed': '40.0',
42 'retraction_amount': '4.5',
43 'retraction_extra': '0.0',
44 'retract_on_jumps_only': 'True',
45 'travel_speed': '150',
47 'bottom_layer_speed': '20',
48 'cool_min_layer_time': '5',
49 'fan_enabled': 'True',
52 'fan_speed_max': '100',
53 'model_matrix': '1,0,0,0,1,0,0,0,1',
54 'extra_base_wall_thickness': '0.0',
55 'sequence': 'Loops > Perimeter > Infill',
56 'force_first_layer_sequence': 'True',
57 'infill_type': 'Line',
61 'support_distance': '0.5',
62 'support_dual_extrusion': 'False',
64 'enable_skin': 'False',
65 'enable_raft': 'False',
66 'cool_min_feedrate': '10',
67 'bridge_speed': '100',
69 'raft_base_material_amount': '100',
70 'raft_interface_material_amount': '100',
71 'bottom_thickness': '0.3',
72 'hop_on_move': 'False',
74 'object_center_x': '-1',
75 'object_center_y': '-1',
78 'gcode_extension': 'gcode',
79 'alternative_center': '',
87 #######################################################################################
88 'start.gcode': """;Sliced {filename} at: {day} {date} {time}
89 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
90 ;Print time: {print_time}
91 ;Filament used: {filament_amount}m {filament_weight}g
92 ;Filament cost: {filament_cost}
94 G90 ;absolute positioning
95 M107 ;start with the fan off
97 G28 X0 Y0 ;move X/Y to min endstops
98 G28 Z0 ;move Z to min endstops
100 G1 Z15.0 F{max_z_speed} ;move the platform down 15mm
102 G92 E0 ;zero the extruded length
103 G1 F200 E3 ;extrude 3mm of feed stock
104 G92 E0 ;zero the extruded length again
108 #######################################################################################
109 'end.gcode': """;End GCode
110 M104 S0 ;extruder heater off
111 M140 S0 ;heated bed heater off (if you have it)
113 G91 ;relative positioning
114 G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
115 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
116 G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way
119 G90 ;absolute positioning
121 #######################################################################################
122 'support_start.gcode': '',
123 'support_end.gcode': '',
124 'cool_start.gcode': '',
125 'cool_end.gcode': '',
127 #######################################################################################
128 '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.
131 G91 ;relative positioning
132 G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
133 G1 Z+0.5 E-5 F{travel_speed} ;move Z up a bit and retract filament even more
134 G90 ;absolute positioning
136 G1 Z{clear_z} F{max_z_speed}
138 G1 X{object_center_x} Y{object_center_y} F{travel_speed}
142 #######################################################################################
143 'switchExtruder.gcode': """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
148 G1 X{new_x} Y{new_y} Z{new_z} F{travel_speed}
153 preferencesDefaultSettings = {
154 'startMode': 'Simple',
155 'lastFile': os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'resources', 'example', 'UltimakerRobot_support.stl')),
156 'machine_width': '205',
157 'machine_depth': '205',
158 'machine_height': '200',
159 'machine_type': 'unknown',
160 'machine_center_is_zero': 'False',
161 'ultimaker_extruder_upgrade': 'False',
162 'has_heated_bed': 'False',
163 'reprap_name': 'RepRap',
164 'extruder_amount': '1',
165 'extruder_offset_x1': '-22.0',
166 'extruder_offset_y1': '0.0',
167 'extruder_offset_x2': '0.0',
168 'extruder_offset_y2': '0.0',
169 'extruder_offset_x3': '0.0',
170 'extruder_offset_y3': '0.0',
171 'filament_density': '1300',
173 'serial_port': 'AUTO',
174 'serial_port_auto': '',
175 'serial_baud': 'AUTO',
176 'serial_baud_auto': '',
177 'slicer': 'Cura (Skeinforge based)',
178 'save_profile': 'False',
179 'filament_cost_kg': '0',
180 'filament_cost_meter': '0',
182 'sdshortnames': 'False',
183 'check_for_updates': 'True',
184 'submit_slice_information': 'False',
186 'planner_always_autoplace': 'True',
187 'extruder_head_size_min_x': '75.0',
188 'extruder_head_size_min_y': '18.0',
189 'extruder_head_size_max_x': '18.0',
190 'extruder_head_size_max_y': '35.0',
191 'extruder_head_size_height': '60.0',
193 'model_colour': '#7AB645',
194 'model_colour2': '#CB3030',
195 'model_colour3': '#DDD93C',
196 'model_colour4': '#4550D3',
198 'window_maximized': 'False',
199 'window_pos_x': '-1',
200 'window_pos_y': '-1',
201 'window_width': '-1',
202 'window_height': '-1',
203 'window_normal_sash': '320',
206 #########################################################
207 ## Profile and preferences functions
208 #########################################################
212 if platform.system() == "Windows":
213 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
214 #If we have a frozen python install, we need to step out of the library.zip
215 if hasattr(sys, 'frozen'):
216 basePath = os.path.normpath(os.path.join(basePath, ".."))
218 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
219 if not os.path.isdir(basePath):
220 os.makedirs(basePath)
223 def getDefaultProfilePath():
224 return os.path.join(getBasePath(), 'current_profile.ini')
226 def loadGlobalProfile(filename):
227 #Read a configuration file as global config
228 global globalProfileParser
229 globalProfileParser = ConfigParser.ConfigParser()
231 globalProfileParser.read(filename)
232 except ConfigParser.ParsingError:
235 def resetGlobalProfile():
236 #Read a configuration file as global config
237 global globalProfileParser
238 globalProfileParser = ConfigParser.ConfigParser()
240 if getPreference('machine_type') == 'ultimaker':
241 putProfileSetting('nozzle_size', '0.4')
242 if getPreference('ultimaker_extruder_upgrade') == 'True':
243 putProfileSetting('retraction_enable', 'True')
245 putProfileSetting('nozzle_size', '0.5')
247 def saveGlobalProfile(filename):
248 #Save the current profile to an ini file
249 globalProfileParser.write(open(filename, 'w'))
251 def loadGlobalProfileFromString(options):
252 global globalProfileParser
253 globalProfileParser = ConfigParser.ConfigParser()
254 globalProfileParser.add_section('profile')
255 globalProfileParser.add_section('alterations')
256 options = base64.b64decode(options)
257 options = zlib.decompress(options)
258 (profileOpts, alt) = options.split('\f', 1)
259 for option in profileOpts.split('\b'):
261 (key, value) = option.split('=', 1)
262 globalProfileParser.set('profile', key, value)
263 for option in alt.split('\b'):
265 (key, value) = option.split('=', 1)
266 globalProfileParser.set('alterations', key, value)
268 def getGlobalProfileString():
269 global globalProfileParser
270 if not globals().has_key('globalProfileParser'):
271 loadGlobalProfile(getDefaultProfilePath())
276 if globalProfileParser.has_section('profile'):
277 for key in globalProfileParser.options('profile'):
278 if key in tempOverride:
279 p.append(key + "=" + tempOverride[key])
282 p.append(key + "=" + globalProfileParser.get('profile', key))
283 if globalProfileParser.has_section('alterations'):
284 for key in globalProfileParser.options('alterations'):
285 if key in tempOverride:
286 p.append(key + "=" + tempOverride[key])
289 alt.append(key + "=" + globalProfileParser.get('alterations', key))
290 for key in tempOverride:
291 if key not in tempDone:
292 p.append(key + "=" + tempOverride[key])
293 ret = '\b'.join(p) + '\f' + '\b'.join(alt)
294 ret = base64.b64encode(zlib.compress(ret, 9))
297 def getGlobalPreferencesString():
298 global globalPreferenceParser
299 if globalPreferenceParser is None:
300 globalPreferenceParser = ConfigParser.ConfigParser()
302 globalPreferenceParser.read(getPreferencePath())
303 except ConfigParser.ParsingError:
307 if globalPreferenceParser.has_section('preference'):
308 for key in globalPreferenceParser.options('preference'):
309 p.append(key + "=" + globalPreferenceParser.get('preference', key))
311 ret = base64.b64encode(zlib.compress(ret, 9))
315 def getProfileSetting(name):
316 if name in tempOverride:
317 return unicode(tempOverride[name], "utf-8")
318 #Check if we have a configuration file loaded, else load the default.
319 if not globals().has_key('globalProfileParser'):
320 loadGlobalProfile(getDefaultProfilePath())
321 if not globalProfileParser.has_option('profile', name):
322 if name in profileDefaultSettings:
323 default = profileDefaultSettings[name]
325 print("Missing default setting for: '" + name + "'")
326 profileDefaultSettings[name] = ''
328 if not globalProfileParser.has_section('profile'):
329 globalProfileParser.add_section('profile')
330 globalProfileParser.set('profile', name, str(default))
331 #print(name + " not found in profile, so using default: " + str(default))
333 return globalProfileParser.get('profile', name)
335 def getProfileSettingFloat(name):
337 setting = getProfileSetting(name).replace(',', '.')
338 return float(eval(setting, {}, {}))
339 except (ValueError, SyntaxError, TypeError):
342 def putProfileSetting(name, value):
343 #Check if we have a configuration file loaded, else load the default.
344 if not globals().has_key('globalProfileParser'):
345 loadGlobalProfile(getDefaultProfilePath())
346 if not globalProfileParser.has_section('profile'):
347 globalProfileParser.add_section('profile')
348 globalProfileParser.set('profile', name, str(value))
350 def isProfileSetting(name):
351 if name in profileDefaultSettings:
355 ## Preferences functions
356 global globalPreferenceParser
357 globalPreferenceParser = None
359 def getPreferencePath():
360 return os.path.join(getBasePath(), 'preferences.ini')
362 def getPreferenceFloat(name):
364 setting = getPreference(name).replace(',', '.')
365 return float(eval(setting, {}, {}))
366 except (ValueError, SyntaxError, TypeError):
369 def getPreferenceColour(name):
370 colorString = getPreference(name)
371 return [float(int(colorString[1:3], 16)) / 255, float(int(colorString[3:5], 16)) / 255, float(int(colorString[5:7], 16)) / 255, 1.0]
373 def getPreference(name):
374 if name in tempOverride:
375 return unicode(tempOverride[name])
376 global globalPreferenceParser
377 if globalPreferenceParser is None:
378 globalPreferenceParser = ConfigParser.ConfigParser()
380 globalPreferenceParser.read(getPreferencePath())
381 except ConfigParser.ParsingError:
383 if not globalPreferenceParser.has_option('preference', name):
384 if name in preferencesDefaultSettings:
385 default = preferencesDefaultSettings[name]
387 print("Missing default setting for: '" + name + "'")
388 preferencesDefaultSettings[name] = ''
390 if not globalPreferenceParser.has_section('preference'):
391 globalPreferenceParser.add_section('preference')
392 globalPreferenceParser.set('preference', name, str(default))
393 #print(name + " not found in preferences, so using default: " + str(default))
395 return unicode(globalPreferenceParser.get('preference', name), "utf-8")
397 def putPreference(name, value):
398 #Check if we have a configuration file loaded, else load the default.
399 global globalPreferenceParser
400 if globalPreferenceParser == None:
401 globalPreferenceParser = ConfigParser.ConfigParser()
403 globalPreferenceParser.read(getPreferencePath())
404 except ConfigParser.ParsingError:
406 if not globalPreferenceParser.has_section('preference'):
407 globalPreferenceParser.add_section('preference')
408 globalPreferenceParser.set('preference', name, unicode(value).encode("utf-8"))
409 globalPreferenceParser.write(open(getPreferencePath(), 'w'))
411 def isPreference(name):
412 if name in preferencesDefaultSettings:
416 ## Temp overrides for multi-extruder slicing and the project planner.
418 def setTempOverride(name, value):
419 tempOverride[name] = unicode(value).encode("utf-8")
420 def clearTempOverride(name):
421 del tempOverride[name]
422 def resetTempOverride():
425 #########################################################
426 ## Utility functions to calculate common profile values
427 #########################################################
428 def calculateEdgeWidth():
429 wallThickness = getProfileSettingFloat('wall_thickness')
430 nozzleSize = getProfileSettingFloat('nozzle_size')
432 if wallThickness < nozzleSize:
435 lineCount = int(wallThickness / nozzleSize + 0.0001)
436 lineWidth = wallThickness / lineCount
437 lineWidthAlt = wallThickness / (lineCount + 1)
438 if lineWidth > nozzleSize * 1.5:
442 def calculateLineCount():
443 wallThickness = getProfileSettingFloat('wall_thickness')
444 nozzleSize = getProfileSettingFloat('nozzle_size')
446 if wallThickness < nozzleSize:
449 lineCount = int(wallThickness / nozzleSize + 0.0001)
450 lineWidth = wallThickness / lineCount
451 lineWidthAlt = wallThickness / (lineCount + 1)
452 if lineWidth > nozzleSize * 1.5:
456 def calculateSolidLayerCount():
457 layerHeight = getProfileSettingFloat('layer_height')
458 solidThickness = getProfileSettingFloat('solid_layer_thickness')
459 return int(math.ceil(solidThickness / layerHeight - 0.0001))
461 def getMachineCenterCoords():
462 if getPreference('machine_center_is_zero') == 'True':
464 return [getPreferenceFloat('machine_width') / 2, getPreferenceFloat('machine_depth') / 2]
466 def getObjectMatrix():
468 return map(float, getProfileSetting('model_matrix').split(','))
470 return [1,0,0, 0,1,0, 0,0,1]
473 #########################################################
474 ## Alteration file functions
475 #########################################################
476 def replaceTagMatch(m):
480 return pre + time.strftime('%H:%M:%S').encode('utf-8', 'replace')
482 return pre + time.strftime('%d %b %Y').encode('utf-8', 'replace')
484 return pre + time.strftime('%a').encode('utf-8', 'replace')
485 if tag == 'print_time':
486 return pre + '#P_TIME#'
487 if tag == 'filament_amount':
488 return pre + '#F_AMNT#'
489 if tag == 'filament_weight':
490 return pre + '#F_WGHT#'
491 if tag == 'filament_cost':
492 return pre + '#F_COST#'
493 if pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
494 f = getProfileSettingFloat(tag) * 60
495 elif isProfileSetting(tag):
496 f = getProfileSettingFloat(tag)
497 elif isPreference(tag):
498 f = getProfileSettingFloat(tag)
500 return '%s?%s?' % (pre, tag)
502 return pre + str(int(f))
505 def replaceGCodeTags(filename, gcodeInt):
506 f = open(filename, 'r+')
508 data = data.replace('#P_TIME#', ('%5d:%02d' % (int(gcodeInt.totalMoveTimeMinute / 60), int(gcodeInt.totalMoveTimeMinute % 60)))[-8:])
509 data = data.replace('#F_AMNT#', ('%8.2f' % (gcodeInt.extrusionAmount / 1000))[-8:])
510 data = data.replace('#F_WGHT#', ('%8.2f' % (gcodeInt.calculateWeight() * 1000))[-8:])
511 cost = gcodeInt.calculateCost()
514 data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
519 ### Get aleration raw contents. (Used internally in Cura)
520 def getAlterationFile(filename):
521 #Check if we have a configuration file loaded, else load the default.
522 if not globals().has_key('globalProfileParser'):
523 loadGlobalProfile(getDefaultProfilePath())
525 if not globalProfileParser.has_option('alterations', filename):
526 if filename in alterationDefault:
527 default = alterationDefault[filename]
529 print("Missing default alteration for: '" + filename + "'")
530 alterationDefault[filename] = ''
532 if not globalProfileParser.has_section('alterations'):
533 globalProfileParser.add_section('alterations')
534 #print("Using default for: %s" % (filename))
535 globalProfileParser.set('alterations', filename, default)
536 return unicode(globalProfileParser.get('alterations', filename), "utf-8")
538 def setAlterationFile(filename, value):
539 #Check if we have a configuration file loaded, else load the default.
540 if not globals().has_key('globalProfileParser'):
541 loadGlobalProfile(getDefaultProfilePath())
542 if not globalProfileParser.has_section('alterations'):
543 globalProfileParser.add_section('alterations')
544 globalProfileParser.set('alterations', filename, value.encode("utf-8"))
545 saveGlobalProfile(getDefaultProfilePath())
547 ### Get the alteration file for output. (Used by Skeinforge)
548 def getAlterationFileContents(filename, extruderCount = 1):
551 alterationContents = getAlterationFile(filename)
552 if filename == 'start.gcode':
553 #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.
554 #We also set our steps per E here, if configured.
555 eSteps = getPreferenceFloat('steps_per_e')
557 prefix += 'M92 E%f\n' % (eSteps)
558 temp = getProfileSettingFloat('print_temperature')
560 if getPreference('has_heated_bed') == 'True':
561 bedTemp = getProfileSettingFloat('print_bed_temperature')
563 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
564 prefix += 'M140 S%f\n' % (bedTemp)
565 if temp > 0 and not '{print_temperature}' in alterationContents:
566 if extruderCount > 0:
567 for n in xrange(1, extruderCount):
569 if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
570 t = getProfileSettingFloat('print_temperature%d' % (n+1))
571 prefix += 'M104 T%d S%f\n' % (n, temp)
572 for n in xrange(0, extruderCount):
574 if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
575 t = getProfileSettingFloat('print_temperature%d' % (n+1))
576 prefix += 'M109 T%d S%f\n' % (n, temp)
579 prefix += 'M109 S%f\n' % (temp)
580 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
581 prefix += 'M190 S%f\n' % (bedTemp)
582 elif filename == 'end.gcode':
583 #Append the profile string to the end of the GCode, so we can load it from the GCode file later.
584 postfix = ';CURA_PROFILE_STRING:%s\n' % (getGlobalProfileString())
585 elif filename == 'replace.csv':
586 #Always remove the extruder on/off M codes. These are no longer needed in 5D printing.
587 prefix = 'M101\nM103\n'
588 elif filename == 'support_start.gcode' or filename == 'support_end.gcode':
589 #Add support start/end code
590 if getProfileSetting('support_dual_extrusion') == 'True' and int(getPreference('extruder_amount')) > 1:
591 if filename == 'support_start.gcode':
592 setTempOverride('extruder', '1')
594 setTempOverride('extruder', '0')
595 alterationContents = getAlterationFileContents('switchExtruder.gcode')
596 clearTempOverride('extruder')
598 alterationContents = ''
599 return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n'
603 def getPluginConfig():
605 return pickle.loads(getProfileSetting('plugin_config'))
609 def setPluginConfig(config):
610 putProfileSetting('plugin_config', pickle.dumps(config))
612 def getPluginBasePaths():
614 if platform.system() != "Windows":
615 ret.append(os.path.expanduser('~/.cura/plugins/'))
616 if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
617 ret.append(os.path.normpath(os.path.join(resources.resourceBasePath, "Cura/plugins")))
619 ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
624 for basePath in getPluginBasePaths():
625 for filename in glob.glob(os.path.join(basePath, '*.py')):
626 filename = os.path.basename(filename)
627 if filename.startswith('_'):
629 with open(os.path.join(basePath, filename), "r") as f:
630 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
633 if not line.startswith('#'):
635 line = line[1:].split(':', 1)
638 if line[0].upper() == 'NAME':
639 item['name'] = line[1].strip()
640 elif line[0].upper() == 'INFO':
641 item['info'] = line[1].strip()
642 elif line[0].upper() == 'TYPE':
643 item['type'] = line[1].strip()
644 elif line[0].upper() == 'DEPEND':
646 elif line[0].upper() == 'PARAM':
647 m = re.match('([a-zA-Z][a-zA-Z0-9_]*)\(([a-zA-Z_]*)(?::([^\)]*))?\) +(.*)', line[1].strip())
649 item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
651 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
652 if item['name'] != None and item['type'] == 'postprocess':
656 def runPostProcessingPlugins(gcodefilename):
657 pluginConfigList = getPluginConfig()
658 pluginList = getPluginList()
660 for pluginConfig in pluginConfigList:
662 for pluginTest in pluginList:
663 if pluginTest['filename'] == pluginConfig['filename']:
669 for basePath in getPluginBasePaths():
670 testFilename = os.path.join(basePath, pluginConfig['filename'])
671 if os.path.isfile(testFilename):
672 pythonFile = testFilename
673 if pythonFile is None:
676 locals = {'filename': gcodefilename}
677 for param in plugin['params']:
678 value = param['default']
679 if param['name'] in pluginConfig['params']:
680 value = pluginConfig['params'][param['name']]
682 if param['type'] == 'float':
686 value = float(param['default'])
688 locals[param['name']] = value
690 execfile(pythonFile, locals)
692 locationInfo = traceback.extract_tb(sys.exc_info()[2])[-1]
693 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])
696 def getSDcardDrives():
698 if platform.system() == "Windows":
699 from ctypes import windll
700 bitmask = windll.kernel32.GetLogicalDrives()
701 for letter in string.uppercase:
703 drives.append(letter + ':/')
705 if platform.system() == "Darwin":
707 for volume in glob.glob('/Volumes/*'):
708 if stat.S_ISLNK(os.lstat(volume).st_mode):
710 drives.append(volume)