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_density': '1.00',
36 'retraction_min_travel': '5.0',
37 'retraction_enable': 'False',
38 'retraction_speed': '40.0',
39 'retraction_amount': '4.5',
40 'retraction_extra': '0.0',
41 'retract_on_jumps_only': 'True',
42 'travel_speed': '150',
44 'bottom_layer_speed': '20',
45 'cool_min_layer_time': '5',
46 'fan_enabled': 'True',
49 'fan_speed_max': '100',
50 'model_matrix': '1,0,0,0,1,0,0,0,1',
51 'extra_base_wall_thickness': '0.0',
52 'sequence': 'Loops > Perimeter > Infill',
53 'force_first_layer_sequence': 'True',
54 'infill_type': 'Line',
58 'support_distance': '0.5',
59 'support_dual_extrusion': 'False',
61 'enable_skin': 'False',
62 'enable_raft': 'False',
63 'cool_min_feedrate': '10',
64 'bridge_speed': '100',
66 'raft_base_material_amount': '100',
67 'raft_interface_material_amount': '100',
68 'bottom_thickness': '0.3',
69 'hop_on_move': 'False',
71 'object_center_x': '-1',
72 'object_center_y': '-1',
75 'gcode_extension': 'gcode',
76 'alternative_center': '',
84 #######################################################################################
85 'start.gcode': """;Sliced {filename} at: {day} {date} {time}
86 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
87 ;Print time: {print_time}
88 ;Filament used: {filament_amount}m {filament_weight}g
89 ;Filament cost: {filament_cost}
91 G90 ;absolute positioning
92 M107 ;start with the fan off
94 G28 X0 Y0 ;move X/Y to min endstops
95 G28 Z0 ;move Z to min endstops
97 G1 Z15.0 F{max_z_speed} ;move the platform down 15mm
99 G92 E0 ;zero the extruded length
100 G1 F200 E3 ;extrude 3mm of feed stock
101 G92 E0 ;zero the extruded length again
105 #######################################################################################
106 'end.gcode': """;End GCode
107 M104 S0 ;extruder heater off
108 M140 S0 ;heated bed heater off (if you have it)
110 G91 ;relative positioning
111 G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
112 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
113 G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way
116 G90 ;absolute positioning
118 #######################################################################################
119 'support_start.gcode': '',
120 'support_end.gcode': '',
121 'cool_start.gcode': '',
122 'cool_end.gcode': '',
124 #######################################################################################
125 '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.
128 G91 ;relative positioning
129 G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
130 G1 Z+0.5 E-5 F{travel_speed} ;move Z up a bit and retract filament even more
131 G90 ;absolute positioning
133 G1 Z{clear_z} F{max_z_speed}
135 G1 X{object_center_x} Y{object_center_y} F{travel_speed}
139 #######################################################################################
140 'switchExtruder.gcode': """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
145 ;G1 X{new_x} Y{new_y} Z{new_z} F{travel_speed}
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 'machine_center_is_zero': 'False',
158 'ultimaker_extruder_upgrade': 'False',
159 'has_heated_bed': 'False',
160 'reprap_name': 'RepRap',
161 'extruder_amount': '1',
162 'extruder_offset_x1': '-22.0',
163 'extruder_offset_y1': '0.0',
164 'extruder_offset_x2': '0.0',
165 'extruder_offset_y2': '0.0',
166 'extruder_offset_x3': '0.0',
167 'extruder_offset_y3': '0.0',
168 'filament_density': '1300',
170 'serial_port': 'AUTO',
171 'serial_port_auto': '',
172 'serial_baud': 'AUTO',
173 'serial_baud_auto': '',
174 'slicer': 'Cura (Skeinforge based)',
175 'save_profile': 'False',
176 'filament_cost_kg': '0',
177 'filament_cost_meter': '0',
179 'sdshortnames': 'False',
180 'check_for_updates': 'True',
181 'submit_slice_information': 'False',
183 'planner_always_autoplace': 'True',
184 'extruder_head_size_min_x': '75.0',
185 'extruder_head_size_min_y': '18.0',
186 'extruder_head_size_max_x': '18.0',
187 'extruder_head_size_max_y': '35.0',
188 'extruder_head_size_height': '60.0',
190 'model_colour': '#7AB645',
191 'model_colour2': '#CB3030',
192 'model_colour3': '#DDD93C',
193 'model_colour4': '#4550D3',
195 'window_maximized': 'False',
196 'window_pos_x': '-1',
197 'window_pos_y': '-1',
198 'window_width': '-1',
199 'window_height': '-1',
200 'window_normal_sash': '320',
203 #########################################################
204 ## Profile and preferences functions
205 #########################################################
209 if platform.system() == "Windows":
210 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
211 #If we have a frozen python install, we need to step out of the library.zip
212 if hasattr(sys, 'frozen'):
213 basePath = os.path.normpath(os.path.join(basePath, ".."))
215 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
216 if not os.path.isdir(basePath):
217 os.makedirs(basePath)
220 def getDefaultProfilePath():
221 return os.path.join(getBasePath(), 'current_profile.ini')
223 def loadGlobalProfile(filename):
224 #Read a configuration file as global config
225 global globalProfileParser
226 globalProfileParser = ConfigParser.ConfigParser()
228 globalProfileParser.read(filename)
229 except ConfigParser.ParsingError:
232 def resetGlobalProfile():
233 #Read a configuration file as global config
234 global globalProfileParser
235 globalProfileParser = ConfigParser.ConfigParser()
237 if getPreference('machine_type') == 'ultimaker':
238 putProfileSetting('nozzle_size', '0.4')
239 if getPreference('ultimaker_extruder_upgrade') == 'True':
240 putProfileSetting('retraction_enable', 'True')
242 putProfileSetting('nozzle_size', '0.5')
244 def saveGlobalProfile(filename):
245 #Save the current profile to an ini file
246 globalProfileParser.write(open(filename, 'w'))
248 def loadGlobalProfileFromString(options):
249 global globalProfileParser
250 globalProfileParser = ConfigParser.ConfigParser()
251 globalProfileParser.add_section('profile')
252 globalProfileParser.add_section('alterations')
253 options = base64.b64decode(options)
254 options = zlib.decompress(options)
255 (profileOpts, alt) = options.split('\f', 1)
256 for option in profileOpts.split('\b'):
258 (key, value) = option.split('=', 1)
259 globalProfileParser.set('profile', key, value)
260 for option in alt.split('\b'):
262 (key, value) = option.split('=', 1)
263 globalProfileParser.set('alterations', key, value)
265 def getGlobalProfileString():
266 global globalProfileParser
267 if not globals().has_key('globalProfileParser'):
268 loadGlobalProfile(getDefaultProfilePath())
273 if globalProfileParser.has_section('profile'):
274 for key in globalProfileParser.options('profile'):
275 if key in tempOverride:
276 p.append(key + "=" + tempOverride[key])
279 p.append(key + "=" + globalProfileParser.get('profile', key))
280 if globalProfileParser.has_section('alterations'):
281 for key in globalProfileParser.options('alterations'):
282 if key in tempOverride:
283 p.append(key + "=" + tempOverride[key])
286 alt.append(key + "=" + globalProfileParser.get('alterations', key))
287 for key in tempOverride:
288 if key not in tempDone:
289 p.append(key + "=" + tempOverride[key])
290 ret = '\b'.join(p) + '\f' + '\b'.join(alt)
291 ret = base64.b64encode(zlib.compress(ret, 9))
294 def getGlobalPreferencesString():
295 global globalPreferenceParser
296 if globalPreferenceParser is None:
297 globalPreferenceParser = ConfigParser.ConfigParser()
299 globalPreferenceParser.read(getPreferencePath())
300 except ConfigParser.ParsingError:
304 if globalPreferenceParser.has_section('preference'):
305 for key in globalPreferenceParser.options('preference'):
306 p.append(key + "=" + globalPreferenceParser.get('preference', key))
308 ret = base64.b64encode(zlib.compress(ret, 9))
312 def getProfileSetting(name):
313 if name in tempOverride:
314 return unicode(tempOverride[name], "utf-8")
315 #Check if we have a configuration file loaded, else load the default.
316 if not globals().has_key('globalProfileParser'):
317 loadGlobalProfile(getDefaultProfilePath())
318 if not globalProfileParser.has_option('profile', name):
319 if name in profileDefaultSettings:
320 default = profileDefaultSettings[name]
322 print("Missing default setting for: '" + name + "'")
323 profileDefaultSettings[name] = ''
325 if not globalProfileParser.has_section('profile'):
326 globalProfileParser.add_section('profile')
327 globalProfileParser.set('profile', name, str(default))
328 #print(name + " not found in profile, so using default: " + str(default))
330 return globalProfileParser.get('profile', name)
332 def getProfileSettingFloat(name):
334 setting = getProfileSetting(name).replace(',', '.')
335 return float(eval(setting, {}, {}))
336 except (ValueError, SyntaxError, TypeError):
339 def putProfileSetting(name, value):
340 #Check if we have a configuration file loaded, else load the default.
341 if not globals().has_key('globalProfileParser'):
342 loadGlobalProfile(getDefaultProfilePath())
343 if not globalProfileParser.has_section('profile'):
344 globalProfileParser.add_section('profile')
345 globalProfileParser.set('profile', name, str(value))
347 def isProfileSetting(name):
348 if name in profileDefaultSettings:
352 ## Preferences functions
353 global globalPreferenceParser
354 globalPreferenceParser = None
356 def getPreferencePath():
357 return os.path.join(getBasePath(), 'preferences.ini')
359 def getPreferenceFloat(name):
361 setting = getPreference(name).replace(',', '.')
362 return float(eval(setting, {}, {}))
363 except (ValueError, SyntaxError, TypeError):
366 def getPreferenceColour(name):
367 colorString = getPreference(name)
368 return [float(int(colorString[1:3], 16)) / 255, float(int(colorString[3:5], 16)) / 255, float(int(colorString[5:7], 16)) / 255, 1.0]
370 def getPreference(name):
371 if name in tempOverride:
372 return unicode(tempOverride[name])
373 global globalPreferenceParser
374 if globalPreferenceParser is None:
375 globalPreferenceParser = ConfigParser.ConfigParser()
377 globalPreferenceParser.read(getPreferencePath())
378 except ConfigParser.ParsingError:
380 if not globalPreferenceParser.has_option('preference', name):
381 if name in preferencesDefaultSettings:
382 default = preferencesDefaultSettings[name]
384 print("Missing default setting for: '" + name + "'")
385 preferencesDefaultSettings[name] = ''
387 if not globalPreferenceParser.has_section('preference'):
388 globalPreferenceParser.add_section('preference')
389 globalPreferenceParser.set('preference', name, str(default))
390 #print(name + " not found in preferences, so using default: " + str(default))
392 return unicode(globalPreferenceParser.get('preference', name), "utf-8")
394 def putPreference(name, value):
395 #Check if we have a configuration file loaded, else load the default.
396 global globalPreferenceParser
397 if globalPreferenceParser == None:
398 globalPreferenceParser = ConfigParser.ConfigParser()
400 globalPreferenceParser.read(getPreferencePath())
401 except ConfigParser.ParsingError:
403 if not globalPreferenceParser.has_section('preference'):
404 globalPreferenceParser.add_section('preference')
405 globalPreferenceParser.set('preference', name, unicode(value).encode("utf-8"))
406 globalPreferenceParser.write(open(getPreferencePath(), 'w'))
408 def isPreference(name):
409 if name in preferencesDefaultSettings:
413 ## Temp overrides for multi-extruder slicing and the project planner.
415 def setTempOverride(name, value):
416 tempOverride[name] = unicode(value).encode("utf-8")
417 def clearTempOverride(name):
418 del tempOverride[name]
419 def resetTempOverride():
422 #########################################################
423 ## Utility functions to calculate common profile values
424 #########################################################
425 def calculateEdgeWidth():
426 wallThickness = getProfileSettingFloat('wall_thickness')
427 nozzleSize = getProfileSettingFloat('nozzle_size')
429 if wallThickness < nozzleSize:
432 lineCount = int(wallThickness / nozzleSize + 0.0001)
433 lineWidth = wallThickness / lineCount
434 lineWidthAlt = wallThickness / (lineCount + 1)
435 if lineWidth > nozzleSize * 1.5:
439 def calculateLineCount():
440 wallThickness = getProfileSettingFloat('wall_thickness')
441 nozzleSize = getProfileSettingFloat('nozzle_size')
443 if wallThickness < nozzleSize:
446 lineCount = int(wallThickness / nozzleSize + 0.0001)
447 lineWidth = wallThickness / lineCount
448 lineWidthAlt = wallThickness / (lineCount + 1)
449 if lineWidth > nozzleSize * 1.5:
453 def calculateSolidLayerCount():
454 layerHeight = getProfileSettingFloat('layer_height')
455 solidThickness = getProfileSettingFloat('solid_layer_thickness')
456 return int(math.ceil(solidThickness / layerHeight - 0.0001))
458 def getMachineCenterCoords():
459 if getPreference('machine_center_is_zero') == 'True':
461 return [getPreferenceFloat('machine_width') / 2, getPreferenceFloat('machine_depth') / 2]
463 def getObjectMatrix():
465 return map(float, getProfileSetting('model_matrix').split(','))
467 return [1,0,0, 0,1,0, 0,0,1]
470 #########################################################
471 ## Alteration file functions
472 #########################################################
473 def replaceTagMatch(m):
477 return pre + time.strftime('%H:%M:%S').encode('utf-8', 'replace')
479 return pre + time.strftime('%d %b %Y').encode('utf-8', 'replace')
481 return pre + time.strftime('%a').encode('utf-8', 'replace')
482 if tag == 'print_time':
483 return pre + '#P_TIME#'
484 if tag == 'filament_amount':
485 return pre + '#F_AMNT#'
486 if tag == 'filament_weight':
487 return pre + '#F_WGHT#'
488 if tag == 'filament_cost':
489 return pre + '#F_COST#'
490 if pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
491 f = getProfileSettingFloat(tag) * 60
492 elif isProfileSetting(tag):
493 f = getProfileSettingFloat(tag)
494 elif isPreference(tag):
495 f = getProfileSettingFloat(tag)
497 return '%s?%s?' % (pre, tag)
499 return pre + str(int(f))
502 def replaceGCodeTags(filename, gcodeInt):
503 f = open(filename, 'r+')
505 data = data.replace('#P_TIME#', ('%5d:%02d' % (int(gcodeInt.totalMoveTimeMinute / 60), int(gcodeInt.totalMoveTimeMinute % 60)))[-8:])
506 data = data.replace('#F_AMNT#', ('%8.2f' % (gcodeInt.extrusionAmount / 1000))[-8:])
507 data = data.replace('#F_WGHT#', ('%8.2f' % (gcodeInt.calculateWeight() * 1000))[-8:])
508 cost = gcodeInt.calculateCost()
511 data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
516 ### Get aleration raw contents. (Used internally in Cura)
517 def getAlterationFile(filename):
518 #Check if we have a configuration file loaded, else load the default.
519 if not globals().has_key('globalProfileParser'):
520 loadGlobalProfile(getDefaultProfilePath())
522 if not globalProfileParser.has_option('alterations', filename):
523 if filename in alterationDefault:
524 default = alterationDefault[filename]
526 print("Missing default alteration for: '" + filename + "'")
527 alterationDefault[filename] = ''
529 if not globalProfileParser.has_section('alterations'):
530 globalProfileParser.add_section('alterations')
531 #print("Using default for: %s" % (filename))
532 globalProfileParser.set('alterations', filename, default)
533 return unicode(globalProfileParser.get('alterations', filename), "utf-8")
535 def setAlterationFile(filename, value):
536 #Check if we have a configuration file loaded, else load the default.
537 if not globals().has_key('globalProfileParser'):
538 loadGlobalProfile(getDefaultProfilePath())
539 if not globalProfileParser.has_section('alterations'):
540 globalProfileParser.add_section('alterations')
541 globalProfileParser.set('alterations', filename, value.encode("utf-8"))
542 saveGlobalProfile(getDefaultProfilePath())
544 ### Get the alteration file for output. (Used by Skeinforge)
545 def getAlterationFileContents(filename, extruderCount = 1):
548 alterationContents = getAlterationFile(filename)
549 if filename == 'start.gcode':
550 #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.
551 #We also set our steps per E here, if configured.
552 eSteps = getPreferenceFloat('steps_per_e')
554 prefix += 'M92 E%f\n' % (eSteps)
555 temp = getProfileSettingFloat('print_temperature')
557 if getPreference('has_heated_bed') == 'True':
558 bedTemp = getProfileSettingFloat('print_bed_temperature')
560 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
561 prefix += 'M140 S%f\n' % (bedTemp)
562 if temp > 0 and not '{print_temperature}' in alterationContents:
563 if extruderCount > 0:
564 for n in xrange(1, extruderCount):
566 if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
567 t = getProfileSettingFloat('print_temperature%d' % (n+1))
568 prefix += 'M104 T%d S%f\n' % (n, temp)
569 for n in xrange(0, extruderCount):
571 if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
572 t = getProfileSettingFloat('print_temperature%d' % (n+1))
573 prefix += 'M109 T%d S%f\n' % (n, temp)
576 prefix += 'M109 S%f\n' % (temp)
577 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
578 prefix += 'M190 S%f\n' % (bedTemp)
579 elif filename == 'end.gcode':
580 #Append the profile string to the end of the GCode, so we can load it from the GCode file later.
581 postfix = ';CURA_PROFILE_STRING:%s\n' % (getGlobalProfileString())
582 elif filename == 'replace.csv':
583 #Always remove the extruder on/off M codes. These are no longer needed in 5D printing.
584 prefix = 'M101\nM103\n'
585 elif filename == 'support_start.gcode' or filename == 'support_end.gcode':
586 #Add support start/end code
587 if getProfileSetting('support_dual_extrusion') == 'True' and int(getPreference('extruder_amount')) > 1:
588 if filename == 'support_start.gcode':
589 setTempOverride('extruder', '1')
591 setTempOverride('extruder', '0')
592 alterationContents = getAlterationFileContents('switchExtruder.gcode')
593 clearTempOverride('extruder')
595 alterationContents = ''
596 return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n'
600 def getPluginConfig():
602 return pickle.loads(getProfileSetting('plugin_config'))
606 def setPluginConfig(config):
607 putProfileSetting('plugin_config', pickle.dumps(config))
609 def getPluginBasePaths():
611 if platform.system() != "Windows":
612 ret.append(os.path.expanduser('~/.cura/plugins/'))
613 if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
614 ret.append(os.path.normpath(os.path.join(resources.resourceBasePath, "Cura/plugins")))
616 ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
621 for basePath in getPluginBasePaths():
622 for filename in glob.glob(os.path.join(basePath, '*.py')):
623 filename = os.path.basename(filename)
624 if filename.startswith('_'):
626 with open(os.path.join(basePath, filename), "r") as f:
627 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
630 if not line.startswith('#'):
632 line = line[1:].split(':', 1)
635 if line[0].upper() == 'NAME':
636 item['name'] = line[1].strip()
637 elif line[0].upper() == 'INFO':
638 item['info'] = line[1].strip()
639 elif line[0].upper() == 'TYPE':
640 item['type'] = line[1].strip()
641 elif line[0].upper() == 'DEPEND':
643 elif line[0].upper() == 'PARAM':
644 m = re.match('([a-zA-Z][a-zA-Z0-9_]*)\(([a-zA-Z_]*)(?::([^\)]*))?\) +(.*)', line[1].strip())
646 item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
648 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
649 if item['name'] != None and item['type'] == 'postprocess':
653 def runPostProcessingPlugins(gcodefilename):
654 pluginConfigList = getPluginConfig()
655 pluginList = getPluginList()
657 for pluginConfig in pluginConfigList:
659 for pluginTest in pluginList:
660 if pluginTest['filename'] == pluginConfig['filename']:
666 for basePath in getPluginBasePaths():
667 testFilename = os.path.join(basePath, pluginConfig['filename'])
668 if os.path.isfile(testFilename):
669 pythonFile = testFilename
670 if pythonFile is None:
673 locals = {'filename': gcodefilename}
674 for param in plugin['params']:
675 value = param['default']
676 if param['name'] in pluginConfig['params']:
677 value = pluginConfig['params'][param['name']]
679 if param['type'] == 'float':
683 value = float(param['default'])
685 locals[param['name']] = value
687 execfile(pythonFile, locals)
689 locationInfo = traceback.extract_tb(sys.exc_info()[2])[-1]
690 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])
693 def getSDcardDrives():
695 if platform.system() == "Windows":
696 from ctypes import windll
697 bitmask = windll.kernel32.GetLogicalDrives()
698 for letter in string.uppercase:
700 drives.append(letter + ':/')
702 if platform.system() == "Darwin":
704 for volume in glob.glob('/Volumes/*'):
705 if stat.S_ISLNK(os.lstat(volume).st_mode):
707 drives.append(volume)