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 version
13 #########################################################
14 ## Default settings when none are found.
15 #########################################################
17 #Single place to store the defaults, so we have a consistent set of default settings.
18 profileDefaultSettings = {
20 'layer_height': '0.2',
21 'wall_thickness': '0.8',
22 'solid_layer_thickness': '0.6',
24 'skirt_line_count': '1',
27 'print_temperature': '220',
28 'print_bed_temperature': '70',
30 'filament_diameter': '2.89',
31 'filament_density': '1.00',
32 'retraction_min_travel': '5.0',
33 'retraction_enable': 'False',
34 'retraction_speed': '40.0',
35 'retraction_amount': '4.5',
36 'retraction_extra': '0.0',
37 'retract_on_jumps_only': 'True',
38 'travel_speed': '150',
40 'bottom_layer_speed': '20',
41 'cool_min_layer_time': '5',
42 'fan_enabled': 'True',
45 'fan_speed_max': '100',
52 'model_rotate_base': '0',
53 'model_multiply_x': '1',
54 'model_multiply_y': '1',
55 'extra_base_wall_thickness': '0.0',
56 'sequence': 'Loops > Perimeter > Infill',
57 'force_first_layer_sequence': 'True',
58 'infill_type': 'Line',
62 'support_distance': '0.5',
63 'support_dual_extrusion': 'False',
65 'enable_skin': 'False',
66 'enable_raft': 'False',
67 'cool_min_feedrate': '10',
68 'bridge_speed': '100',
70 'raft_base_material_amount': '100',
71 'raft_interface_material_amount': '100',
72 'bottom_thickness': '0.3',
73 'hop_on_move': 'False',
76 'add_start_end_gcode': 'True',
77 'gcode_extension': 'gcode',
78 'alternative_center': '',
83 #######################################################################################
84 'start.gcode': """;Sliced {filename} at: {day} {date} {time}
85 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
86 ;Print time: {print_time}
87 ;Filament used: {filament_amount}m {filament_weight}g
88 ;Filament cost: {filament_cost}
90 G90 ;absolute positioning
91 M107 ;start with the fan off
93 G28 X0 Y0 ;move X/Y to min endstops
94 G28 Z0 ;move Z to min endstops
95 G92 X0 Y0 Z0 E0 ;reset software position to front/left/z=0.0
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
104 #######################################################################################
105 'end.gcode': """;End GCode
106 M104 S0 ;extruder heater off
107 M140 S0 ;heated bed heater off (if you have it)
109 G91 ;relative positioning
110 G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
111 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
112 G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way
115 G90 ;absolute positioning
117 #######################################################################################
118 'support_start.gcode': '',
119 'support_end.gcode': '',
120 'cool_start.gcode': '',
121 'cool_end.gcode': '',
123 #######################################################################################
124 '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.
127 G91 ;relative positioning
128 G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
129 G1 Z+0.5 E-5 F{travel_speed} ;move Z up a bit and retract filament even more
130 G90 ;absolute positioning
132 G1 Z{clear_z} F{max_z_speed}
134 G1 X{object_center_x} Y{object_center_x} F{travel_speed}
138 #######################################################################################
139 'switchExtruder.gcode': """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
148 preferencesDefaultSettings = {
149 'startMode': 'Simple',
150 'lastFile': os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'example', 'UltimakerRobot_support.stl')),
151 'machine_width': '205',
152 'machine_depth': '205',
153 'machine_height': '200',
154 'machine_type': 'unknown',
155 'ultimaker_extruder_upgrade': 'False',
156 'has_heated_bed': 'False',
157 'extruder_amount': '1',
158 'extruder_offset_x1': '-22.0',
159 'extruder_offset_y1': '0.0',
160 'extruder_offset_x2': '0.0',
161 'extruder_offset_y2': '0.0',
162 'extruder_offset_x3': '0.0',
163 'extruder_offset_y3': '0.0',
164 'filament_density': '1300',
166 'serial_port': 'AUTO',
167 'serial_port_auto': '',
168 'serial_baud': 'AUTO',
169 'serial_baud_auto': '',
170 'slicer': 'Cura (Skeinforge based)',
171 'save_profile': 'False',
172 'filament_cost_kg': '0',
173 'filament_cost_meter': '0',
175 'sdshortnames': 'True',
177 'extruder_head_size_min_x': '70.0',
178 'extruder_head_size_min_y': '18.0',
179 'extruder_head_size_max_x': '18.0',
180 'extruder_head_size_max_y': '35.0',
181 'extruder_head_size_height': '80.0',
183 'model_colour': '#72CB30',
184 'model_colour2': '#CB3030',
185 'model_colour3': '#DDD93C',
186 'model_colour4': '#4550D3',
189 #########################################################
190 ## Profile and preferences functions
191 #########################################################
194 def getDefaultProfilePath():
195 if platform.system() == "Windows":
196 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
197 #If we have a frozen python install, we need to step out of the library.zip
198 if hasattr(sys, 'frozen'):
199 basePath = os.path.normpath(os.path.join(basePath, ".."))
201 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
202 if not os.path.isdir(basePath):
203 os.makedirs(basePath)
204 return os.path.join(basePath, 'current_profile.ini')
206 def loadGlobalProfile(filename):
207 #Read a configuration file as global config
208 global globalProfileParser
209 globalProfileParser = ConfigParser.ConfigParser()
210 globalProfileParser.read(filename)
212 def resetGlobalProfile():
213 #Read a configuration file as global config
214 global globalProfileParser
215 globalProfileParser = ConfigParser.ConfigParser()
217 if getPreference('machine_type') == 'ultimaker':
218 putProfileSetting('nozzle_size', '0.4')
219 if getPreference('ultimaker_extruder_upgrade') == 'True':
220 putProfileSetting('retraction_enable', 'True')
222 putProfileSetting('nozzle_size', '0.5')
224 def saveGlobalProfile(filename):
225 #Save the current profile to an ini file
226 globalProfileParser.write(open(filename, 'w'))
228 def loadGlobalProfileFromString(options):
229 global globalProfileParser
230 globalProfileParser = ConfigParser.ConfigParser()
231 globalProfileParser.add_section('profile')
232 globalProfileParser.add_section('alterations')
233 options = base64.b64decode(options)
234 options = zlib.decompress(options)
235 (profileOpts, alt) = options.split('\f', 1)
236 for option in profileOpts.split('\b'):
238 (key, value) = option.split('=', 1)
239 globalProfileParser.set('profile', key, value)
240 for option in alt.split('\b'):
242 (key, value) = option.split('=', 1)
243 globalProfileParser.set('alterations', key, value)
245 def getGlobalProfileString():
246 global globalProfileParser
247 if not globals().has_key('globalProfileParser'):
248 loadGlobalProfile(getDefaultProfilePath())
253 if globalProfileParser.has_section('profile'):
254 for key in globalProfileParser.options('profile'):
255 if key in tempOverride:
256 p.append(key + "=" + tempOverride[key])
259 p.append(key + "=" + globalProfileParser.get('profile', key))
260 if globalProfileParser.has_section('alterations'):
261 for key in globalProfileParser.options('alterations'):
262 if key in tempOverride:
263 p.append(key + "=" + tempOverride[key])
266 alt.append(key + "=" + globalProfileParser.get('alterations', key))
267 for key in tempOverride:
268 if key not in tempDone:
269 p.append(key + "=" + tempOverride[key])
270 ret = '\b'.join(p) + '\f' + '\b'.join(alt)
271 ret = base64.b64encode(zlib.compress(ret, 9))
274 def getProfileSetting(name):
275 if name in tempOverride:
276 return unicode(tempOverride[name], "utf-8")
277 #Check if we have a configuration file loaded, else load the default.
278 if not globals().has_key('globalProfileParser'):
279 loadGlobalProfile(getDefaultProfilePath())
280 if not globalProfileParser.has_option('profile', name):
281 if name in profileDefaultSettings:
282 default = profileDefaultSettings[name]
284 print("Missing default setting for: '" + name + "'")
285 profileDefaultSettings[name] = ''
287 if not globalProfileParser.has_section('profile'):
288 globalProfileParser.add_section('profile')
289 globalProfileParser.set('profile', name, str(default))
290 #print(name + " not found in profile, so using default: " + str(default))
292 return globalProfileParser.get('profile', name)
294 def getProfileSettingFloat(name):
296 setting = getProfileSetting(name).replace(',', '.')
297 return float(eval(setting, {}, {}))
298 except (ValueError, SyntaxError, TypeError):
301 def putProfileSetting(name, value):
302 #Check if we have a configuration file loaded, else load the default.
303 if not globals().has_key('globalProfileParser'):
304 loadGlobalProfile(getDefaultProfilePath())
305 if not globalProfileParser.has_section('profile'):
306 globalProfileParser.add_section('profile')
307 globalProfileParser.set('profile', name, str(value))
309 def isProfileSetting(name):
310 if name in profileDefaultSettings:
314 ## Preferences functions
315 global globalPreferenceParser
316 globalPreferenceParser = None
318 def getPreferencePath():
319 if platform.system() == "Windows":
320 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
321 #If we have a frozen python install, we need to step out of the library.zip
322 if hasattr(sys, 'frozen'):
323 basePath = os.path.normpath(os.path.join(basePath, ".."))
325 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
326 if not os.path.isdir(basePath):
327 os.makedirs(basePath)
328 return os.path.join(basePath, 'preferences.ini')
330 def getPreferenceFloat(name):
332 setting = getPreference(name).replace(',', '.')
333 return float(eval(setting, {}, {}))
334 except (ValueError, SyntaxError, TypeError):
337 def getPreferenceColour(name):
338 colorString = getPreference(name)
339 return [float(int(colorString[1:3], 16)) / 255, float(int(colorString[3:5], 16)) / 255, float(int(colorString[5:7], 16)) / 255, 1.0]
341 def getPreference(name):
342 if name in tempOverride:
343 return unicode(tempOverride[name])
344 global globalPreferenceParser
345 if globalPreferenceParser == None:
346 globalPreferenceParser = ConfigParser.ConfigParser()
347 globalPreferenceParser.read(getPreferencePath())
348 if not globalPreferenceParser.has_option('preference', name):
349 if name in preferencesDefaultSettings:
350 default = preferencesDefaultSettings[name]
352 print("Missing default setting for: '" + name + "'")
353 preferencesDefaultSettings[name] = ''
355 if not globalPreferenceParser.has_section('preference'):
356 globalPreferenceParser.add_section('preference')
357 globalPreferenceParser.set('preference', name, str(default))
358 #print(name + " not found in preferences, so using default: " + str(default))
360 return unicode(globalPreferenceParser.get('preference', name), "utf-8")
362 def putPreference(name, value):
363 #Check if we have a configuration file loaded, else load the default.
364 global globalPreferenceParser
365 if globalPreferenceParser == None:
366 globalPreferenceParser = ConfigParser.ConfigParser()
367 globalPreferenceParser.read(getPreferencePath())
368 if not globalPreferenceParser.has_section('preference'):
369 globalPreferenceParser.add_section('preference')
370 globalPreferenceParser.set('preference', name, unicode(value).encode("utf-8"))
371 globalPreferenceParser.write(open(getPreferencePath(), 'w'))
373 def isPreference(name):
374 if name in preferencesDefaultSettings:
378 ## Temp overrides for multi-extruder slicing and the project planner.
380 def setTempOverride(name, value):
381 tempOverride[name] = unicode(value).encode("utf-8")
382 def clearTempOverride(name):
383 del tempOverride[name]
384 def resetTempOverride():
387 #########################################################
388 ## Utility functions to calculate common profile values
389 #########################################################
390 def calculateEdgeWidth():
391 wallThickness = getProfileSettingFloat('wall_thickness')
392 nozzleSize = getProfileSettingFloat('nozzle_size')
394 if wallThickness < nozzleSize:
397 lineCount = int(wallThickness / nozzleSize)
398 lineWidth = wallThickness / lineCount
399 lineWidthAlt = wallThickness / (lineCount + 1)
400 if lineWidth > nozzleSize * 1.5:
404 def calculateLineCount():
405 wallThickness = getProfileSettingFloat('wall_thickness')
406 nozzleSize = getProfileSettingFloat('nozzle_size')
408 if wallThickness < nozzleSize:
411 lineCount = int(wallThickness / nozzleSize + 0.0001)
412 lineWidth = wallThickness / lineCount
413 lineWidthAlt = wallThickness / (lineCount + 1)
414 if lineWidth > nozzleSize * 1.5:
418 def calculateSolidLayerCount():
419 layerHeight = getProfileSettingFloat('layer_height')
420 solidThickness = getProfileSettingFloat('solid_layer_thickness')
421 return int(math.ceil(solidThickness / layerHeight - 0.0001))
423 #########################################################
424 ## Alteration file functions
425 #########################################################
426 def replaceTagMatch(m):
430 return pre + time.strftime('%H:%M:%S')
432 return pre + time.strftime('%d %b %Y')
434 return pre + time.strftime('%a')
435 if tag == 'print_time':
436 return pre + '#P_TIME#'
437 if tag == 'filament_amount':
438 return pre + '#F_AMNT#'
439 if tag == 'filament_weight':
440 return pre + '#F_WGHT#'
441 if tag == 'filament_cost':
442 return pre + '#F_COST#'
443 if pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
444 f = getProfileSettingFloat(tag) * 60
445 elif isProfileSetting(tag):
446 f = getProfileSettingFloat(tag)
447 elif isPreference(tag):
448 f = getProfileSettingFloat(tag)
450 return '%s?%s?' % (pre, tag)
452 return pre + str(int(f))
455 def replaceGCodeTags(filename, gcodeInt):
456 f = open(filename, 'r+')
458 data = data.replace('#P_TIME#', ('%5d:%02d' % (int(gcodeInt.totalMoveTimeMinute / 60), int(gcodeInt.totalMoveTimeMinute % 60)))[-8:])
459 data = data.replace('#F_AMNT#', ('%8.2f' % (gcodeInt.extrusionAmount / 1000))[-8:])
460 data = data.replace('#F_WGHT#', ('%8.2f' % (gcodeInt.calculateWeight() * 1000))[-8:])
461 cost = gcodeInt.calculateCost()
464 data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
469 ### Get aleration raw contents. (Used internally in Cura)
470 def getAlterationFile(filename):
471 #Check if we have a configuration file loaded, else load the default.
472 if not globals().has_key('globalProfileParser'):
473 loadGlobalProfile(getDefaultProfilePath())
475 if not globalProfileParser.has_option('alterations', filename):
476 if filename in alterationDefault:
477 default = alterationDefault[filename]
479 print("Missing default alteration for: '" + filename + "'")
480 alterationDefault[filename] = ''
482 if not globalProfileParser.has_section('alterations'):
483 globalProfileParser.add_section('alterations')
484 #print("Using default for: %s" % (filename))
485 globalProfileParser.set('alterations', filename, default)
486 return unicode(globalProfileParser.get('alterations', filename), "utf-8")
488 def setAlterationFile(filename, value):
489 #Check if we have a configuration file loaded, else load the default.
490 if not globals().has_key('globalProfileParser'):
491 loadGlobalProfile(getDefaultProfilePath())
492 if not globalProfileParser.has_section('alterations'):
493 globalProfileParser.add_section('alterations')
494 globalProfileParser.set('alterations', filename, value.encode("utf-8"))
495 saveGlobalProfile(getDefaultProfilePath())
497 ### Get the alteration file for output. (Used by Skeinforge)
498 def getAlterationFileContents(filename):
501 alterationContents = getAlterationFile(filename)
502 if filename == 'start.gcode':
503 #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.
504 #We also set our steps per E here, if configured.
505 eSteps = getPreferenceFloat('steps_per_e')
507 prefix += 'M92 E%f\n' % (eSteps)
508 temp = getProfileSettingFloat('print_temperature')
510 if getPreference('has_heated_bed') == 'True':
511 bedTemp = getProfileSettingFloat('print_bed_temperature')
513 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
514 prefix += 'M140 S%f\n' % (bedTemp)
515 if temp > 0 and not '{print_temperature}' in alterationContents:
516 prefix += 'M109 S%f\n' % (temp)
517 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
518 prefix += 'M190 S%f\n' % (bedTemp)
519 elif filename == 'end.gcode':
520 #Append the profile string to the end of the GCode, so we can load it from the GCode file later.
521 postfix = ';CURA_PROFILE_STRING:%s\n' % (getGlobalProfileString())
522 elif filename == 'replace.csv':
523 #Always remove the extruder on/off M codes. These are no longer needed in 5D printing.
524 prefix = 'M101\nM103\n'
525 elif filename == 'support_start.gcode' or filename == 'support_end.gcode':
526 #Add support start/end code
527 if getProfileSetting('support_dual_extrusion') == 'True' and int(getPreference('extruder_amount')) > 1:
528 if filename == 'support_start.gcode':
529 setTempOverride('extruder', '1')
531 setTempOverride('extruder', '0')
532 alterationContents = getAlterationFileContents('switchExtruder.gcode')
533 clearTempOverride('extruder')
535 alterationContents = ''
536 return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8')
540 def getPluginConfig():
542 return pickle.loads(getProfileSetting('plugin_config'))
546 def setPluginConfig(config):
547 putProfileSetting('plugin_config', pickle.dumps(config))
549 def getPluginBasePaths():
551 if platform.system() != "Windows":
552 ret.append(os.path.expanduser('~/.cura/plugins/'))
553 ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
558 for basePath in getPluginBasePaths():
559 for filename in glob.glob(os.path.join(basePath, '*.py')):
560 filename = os.path.basename(filename)
561 if filename.startswith('_'):
563 with open(os.path.join(basePath, filename), "r") as f:
564 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
567 if not line.startswith('#'):
569 line = line[1:].split(':', 1)
572 if line[0].upper() == 'NAME':
573 item['name'] = line[1].strip()
574 elif line[0].upper() == 'INFO':
575 item['info'] = line[1].strip()
576 elif line[0].upper() == 'TYPE':
577 item['type'] = line[1].strip()
578 elif line[0].upper() == 'DEPEND':
580 elif line[0].upper() == 'PARAM':
581 m = re.match('([a-zA-Z]*)\(([a-zA-Z_]*)(?::([^\)]*))?\) +(.*)', line[1].strip())
583 item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
585 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
586 if item['name'] != None and item['type'] == 'postprocess':
590 def runPostProcessingPlugins(gcodefilename):
591 pluginConfigList = getPluginConfig()
592 pluginList = getPluginList()
594 for pluginConfig in pluginConfigList:
596 for pluginTest in pluginList:
597 if pluginTest['filename'] == pluginConfig['filename']:
603 for basePath in getPluginBasePaths():
604 testFilename = os.path.join(basePath, pluginConfig['filename'])
605 if os.path.isfile(testFilename):
606 pythonFile = testFilename
607 if pythonFile == None:
610 locals = {'filename': gcodefilename}
611 for param in plugin['params']:
612 value = param['default']
613 if param['name'] in pluginConfig['params']:
614 value = pluginConfig['params'][param['name']]
616 if param['type'] == 'float':
620 value = float(param['default'])
622 locals[param['name']] = value
624 execfile(pythonFile, locals)
626 locationInfo = traceback.extract_tb(sys.exc_info()[2])[-1]
627 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])
630 def getSDcardDrives():
632 if platform.system() == "Windows":
633 from ctypes import windll
634 bitmask = windll.kernel32.GetLogicalDrives()
635 for letter in string.uppercase:
637 drives.append(letter + ':/')
639 if platform.system() == "Darwin":
641 for volume in glob.glob('/Volumes/*'):
642 if stat.S_ISLNK(os.lstat(volume).st_mode):
644 drives.append(volume)