chiark / gitweb /
Merge branch 'master' into SteamEngine
[cura.git] / Cura / util / profile.py
1 from __future__ import absolute_import
2 from __future__ import division
3
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:
7         import ConfigParser
8 else:
9         import configparser as ConfigParser
10
11 from Cura.util import resources
12 from Cura.util import version
13
14 #########################################################
15 ## Default settings when none are found.
16 #########################################################
17
18 #Single place to store the defaults, so we have a consistent set of default settings.
19 profileDefaultSettings = {
20         'nozzle_size': '0.4',
21         'layer_height': '0.2',
22         'wall_thickness': '0.8',
23         'solid_layer_thickness': '0.6',
24         'fill_density': '20',
25         'skirt_line_count': '1',
26         'skirt_gap': '3.0',
27         'print_speed': '50',
28         'print_temperature': '220',
29         'print_temperature2': '0',
30         'print_temperature3': '0',
31         'print_temperature4': '0',
32         'print_bed_temperature': '70',
33         'support': 'None',
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',
43         'max_z_speed': '3.0',
44         'bottom_layer_speed': '20',
45         'cool_min_layer_time': '5',
46         'fan_enabled': 'True',
47         'fan_layer': '1',
48         'fan_speed': '100',
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',
55         'solid_top': 'True',
56         'fill_overlap': '15',
57         'support_rate': '50',
58         'support_distance': '0.5',
59         'support_dual_extrusion': 'False',
60         'joris': 'False',
61         'enable_skin': 'False',
62         'enable_raft': 'False',
63         'cool_min_feedrate': '10',
64         'bridge_speed': '100',
65         'raft_margin': '5',
66         'raft_base_material_amount': '100',
67         'raft_interface_material_amount': '100',
68         'bottom_thickness': '0.3',
69         'hop_on_move': 'False',
70         'plugin_config': '',
71         'object_center_x': '-1',
72         'object_center_y': '-1',
73         'object_sink': '0.0',
74         
75         'gcode_extension': 'gcode',
76         'alternative_center': '',
77         'clear_z': '0.0',
78         'extruder': '0',
79         'new_x': '0',
80         'new_y': '0',
81         'new_z': '0',
82 }
83 alterationDefault = {
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}
90 G21        ;metric values
91 G90        ;absolute positioning
92 M107       ;start with the fan off
93
94 G28 X0 Y0  ;move X/Y to min endstops
95 G28 Z0     ;move Z to min endstops
96
97 G1 Z15.0 F{max_z_speed} ;move the platform down 15mm
98
99 G92 E0                  ;zero the extruded length
100 G1 F200 E3              ;extrude 3mm of feed stock
101 G92 E0                  ;zero the extruded length again
102 G1 F{travel_speed}
103 M117 Printing...
104 """,
105 #######################################################################################
106         'end.gcode': """;End GCode
107 M104 S0                     ;extruder heater off
108 M140 S0                     ;heated bed heater off (if you have it)
109
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
114
115 M84                         ;steppers off
116 G90                         ;absolute positioning
117 """,
118 #######################################################################################
119         'support_start.gcode': '',
120         'support_end.gcode': '',
121         'cool_start.gcode': '',
122         'cool_end.gcode': '',
123         'replace.csv': '',
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.
126 G92 E0
127
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
132
133 G1 Z{clear_z} F{max_z_speed}
134 G92 E0
135 G1 X{object_center_x} Y{object_center_y} F{travel_speed}
136 G1 F200 E6
137 G92 E0
138 """,
139 #######################################################################################
140         'switchExtruder.gcode': """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
141 G92 E0
142 G1 E-36 F5000
143 G92 E0
144 T{extruder}
145 ;G1 X{new_x} Y{new_y} Z{new_z} F{travel_speed}
146 G1 E36 F5000
147 G92 E0
148 """,
149 }
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',
169         'steps_per_e': '0',
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',
178         'sdpath': '',
179         'sdshortnames': 'False',
180         'check_for_updates': 'True',
181         'submit_slice_information': 'False',
182
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',
189         
190         'model_colour': '#7AB645',
191         'model_colour2': '#CB3030',
192         'model_colour3': '#DDD93C',
193         'model_colour4': '#4550D3',
194
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',
201 }
202
203 #########################################################
204 ## Profile and preferences functions
205 #########################################################
206
207 ## Profile functions
208 def getBasePath():
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, ".."))
214         else:
215                 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
216         if not os.path.isdir(basePath):
217                 os.makedirs(basePath)
218         return basePath
219
220 def getDefaultProfilePath():
221         return os.path.join(getBasePath(), 'current_profile.ini')
222
223 def loadGlobalProfile(filename):
224         #Read a configuration file as global config
225         global globalProfileParser
226         globalProfileParser = ConfigParser.ConfigParser()
227         try:
228                 globalProfileParser.read(filename)
229         except ConfigParser.ParsingError:
230                 pass
231
232 def resetGlobalProfile():
233         #Read a configuration file as global config
234         global globalProfileParser
235         globalProfileParser = ConfigParser.ConfigParser()
236
237         if getPreference('machine_type') == 'ultimaker':
238                 putProfileSetting('nozzle_size', '0.4')
239                 if getPreference('ultimaker_extruder_upgrade') == 'True':
240                         putProfileSetting('retraction_enable', 'True')
241         else:
242                 putProfileSetting('nozzle_size', '0.5')
243
244 def saveGlobalProfile(filename):
245         #Save the current profile to an ini file
246         globalProfileParser.write(open(filename, 'w'))
247
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'):
257                 if len(option) > 0:
258                         (key, value) = option.split('=', 1)
259                         globalProfileParser.set('profile', key, value)
260         for option in alt.split('\b'):
261                 if len(option) > 0:
262                         (key, value) = option.split('=', 1)
263                         globalProfileParser.set('alterations', key, value)
264
265 def getGlobalProfileString():
266         global globalProfileParser
267         if not globals().has_key('globalProfileParser'):
268                 loadGlobalProfile(getDefaultProfilePath())
269         
270         p = []
271         alt = []
272         tempDone = []
273         if globalProfileParser.has_section('profile'):
274                 for key in globalProfileParser.options('profile'):
275                         if key in tempOverride:
276                                 p.append(key + "=" + tempOverride[key])
277                                 tempDone.append(key)
278                         else:
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])
284                                 tempDone.append(key)
285                         else:
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))
292         return ret
293
294 def getGlobalPreferencesString():
295         global globalPreferenceParser
296         if globalPreferenceParser is None:
297                 globalPreferenceParser = ConfigParser.ConfigParser()
298                 try:
299                         globalPreferenceParser.read(getPreferencePath())
300                 except ConfigParser.ParsingError:
301                         pass
302
303         p = []
304         if globalPreferenceParser.has_section('preference'):
305                 for key in globalPreferenceParser.options('preference'):
306                         p.append(key + "=" + globalPreferenceParser.get('preference', key))
307         ret = '\b'.join(p)
308         ret = base64.b64encode(zlib.compress(ret, 9))
309         return ret
310
311
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]
321                 else:
322                         print("Missing default setting for: '" + name + "'")
323                         profileDefaultSettings[name] = ''
324                         default = ''
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))
329                 return default
330         return globalProfileParser.get('profile', name)
331
332 def getProfileSettingFloat(name):
333         try:
334                 setting = getProfileSetting(name).replace(',', '.')
335                 return float(eval(setting, {}, {}))
336         except (ValueError, SyntaxError, TypeError):
337                 return 0.0
338
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))
346
347 def isProfileSetting(name):
348         if name in profileDefaultSettings:
349                 return True
350         return False
351
352 ## Preferences functions
353 global globalPreferenceParser
354 globalPreferenceParser = None
355
356 def getPreferencePath():
357         return os.path.join(getBasePath(), 'preferences.ini')
358
359 def getPreferenceFloat(name):
360         try:
361                 setting = getPreference(name).replace(',', '.')
362                 return float(eval(setting, {}, {}))
363         except (ValueError, SyntaxError, TypeError):
364                 return 0.0
365
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]
369
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()
376                 try:
377                         globalPreferenceParser.read(getPreferencePath())
378                 except ConfigParser.ParsingError:
379                         pass
380         if not globalPreferenceParser.has_option('preference', name):
381                 if name in preferencesDefaultSettings:
382                         default = preferencesDefaultSettings[name]
383                 else:
384                         print("Missing default setting for: '" + name + "'")
385                         preferencesDefaultSettings[name] = ''
386                         default = ''
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))
391                 return default
392         return unicode(globalPreferenceParser.get('preference', name), "utf-8")
393
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()
399                 try:
400                         globalPreferenceParser.read(getPreferencePath())
401                 except ConfigParser.ParsingError:
402                         pass
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'))
407
408 def isPreference(name):
409         if name in preferencesDefaultSettings:
410                 return True
411         return False
412
413 ## Temp overrides for multi-extruder slicing and the project planner.
414 tempOverride = {}
415 def setTempOverride(name, value):
416         tempOverride[name] = unicode(value).encode("utf-8")
417 def clearTempOverride(name):
418         del tempOverride[name]
419 def resetTempOverride():
420         tempOverride.clear()
421
422 #########################################################
423 ## Utility functions to calculate common profile values
424 #########################################################
425 def calculateEdgeWidth():
426         wallThickness = getProfileSettingFloat('wall_thickness')
427         nozzleSize = getProfileSettingFloat('nozzle_size')
428         
429         if wallThickness < nozzleSize:
430                 return wallThickness
431
432         lineCount = int(wallThickness / nozzleSize + 0.0001)
433         lineWidth = wallThickness / lineCount
434         lineWidthAlt = wallThickness / (lineCount + 1)
435         if lineWidth > nozzleSize * 1.5:
436                 return lineWidthAlt
437         return lineWidth
438
439 def calculateLineCount():
440         wallThickness = getProfileSettingFloat('wall_thickness')
441         nozzleSize = getProfileSettingFloat('nozzle_size')
442         
443         if wallThickness < nozzleSize:
444                 return 1
445
446         lineCount = int(wallThickness / nozzleSize + 0.0001)
447         lineWidth = wallThickness / lineCount
448         lineWidthAlt = wallThickness / (lineCount + 1)
449         if lineWidth > nozzleSize * 1.5:
450                 return lineCount + 1
451         return lineCount
452
453 def calculateSolidLayerCount():
454         layerHeight = getProfileSettingFloat('layer_height')
455         solidThickness = getProfileSettingFloat('solid_layer_thickness')
456         return int(math.ceil(solidThickness / layerHeight - 0.0001))
457
458 def getMachineCenterCoords():
459         if getPreference('machine_center_is_zero') == 'True':
460                 return [0, 0]
461         return [getPreferenceFloat('machine_width') / 2, getPreferenceFloat('machine_depth') / 2]
462
463 def getObjectMatrix():
464         try:
465                 return map(float, getProfileSetting('model_matrix').split(','))
466         except ValueError:
467                 return [1,0,0, 0,1,0, 0,0,1]
468
469
470 #########################################################
471 ## Alteration file functions
472 #########################################################
473 def replaceTagMatch(m):
474         pre = m.group(1)
475         tag = m.group(2)
476         if tag == 'time':
477                 return pre + time.strftime('%H:%M:%S').encode('utf-8', 'replace')
478         if tag == 'date':
479                 return pre + time.strftime('%d %b %Y').encode('utf-8', 'replace')
480         if tag == 'day':
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)
496         else:
497                 return '%s?%s?' % (pre, tag)
498         if (f % 1) == 0:
499                 return pre + str(int(f))
500         return pre + str(f)
501
502 def replaceGCodeTags(filename, gcodeInt):
503         f = open(filename, 'r+')
504         data = f.read(2048)
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()
509         if cost is None:
510                 cost = 'Unknown'
511         data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
512         f.seek(0)
513         f.write(data)
514         f.close()
515
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())
521         
522         if not globalProfileParser.has_option('alterations', filename):
523                 if filename in alterationDefault:
524                         default = alterationDefault[filename]
525                 else:
526                         print("Missing default alteration for: '" + filename + "'")
527                         alterationDefault[filename] = ''
528                         default = ''
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")
534
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())
543
544 ### Get the alteration file for output. (Used by Skeinforge)
545 def getAlterationFileContents(filename, extruderCount = 1):
546         prefix = ''
547         postfix = ''
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')
553                 if eSteps > 0:
554                         prefix += 'M92 E%f\n' % (eSteps)
555                 temp = getProfileSettingFloat('print_temperature')
556                 bedTemp = 0
557                 if getPreference('has_heated_bed') == 'True':
558                         bedTemp = getProfileSettingFloat('print_bed_temperature')
559                 
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):
565                                         t = temp
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):
570                                         t = temp
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)
574                                 prefix += 'T0\n'
575                         else:
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')
590                         else:
591                                 setTempOverride('extruder', '0')
592                         alterationContents = getAlterationFileContents('switchExtruder.gcode')
593                         clearTempOverride('extruder')
594                 else:
595                         alterationContents = ''
596         return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n'
597
598 ###### PLUGIN #####
599
600 def getPluginConfig():
601         try:
602                 return pickle.loads(getProfileSetting('plugin_config'))
603         except:
604                 return []
605
606 def setPluginConfig(config):
607         putProfileSetting('plugin_config', pickle.dumps(config))
608
609 def getPluginBasePaths():
610         ret = []
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")))
615         else:
616                 ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
617         return ret
618
619 def getPluginList():
620         ret = []
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('_'):
625                                 continue
626                         with open(os.path.join(basePath, filename), "r") as f:
627                                 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
628                                 for line in f:
629                                         line = line.strip()
630                                         if not line.startswith('#'):
631                                                 break
632                                         line = line[1:].split(':', 1)
633                                         if len(line) != 2:
634                                                 continue
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':
642                                                 pass
643                                         elif line[0].upper() == 'PARAM':
644                                                 m = re.match('([a-zA-Z][a-zA-Z0-9_]*)\(([a-zA-Z_]*)(?::([^\)]*))?\) +(.*)', line[1].strip())
645                                                 if m is not None:
646                                                         item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
647                                         else:
648                                                 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
649                                 if item['name'] != None and item['type'] == 'postprocess':
650                                         ret.append(item)
651         return ret
652
653 def runPostProcessingPlugins(gcodefilename):
654         pluginConfigList = getPluginConfig()
655         pluginList = getPluginList()
656         
657         for pluginConfig in pluginConfigList:
658                 plugin = None
659                 for pluginTest in pluginList:
660                         if pluginTest['filename'] == pluginConfig['filename']:
661                                 plugin = pluginTest
662                 if plugin is None:
663                         continue
664                 
665                 pythonFile = None
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:
671                         continue
672                 
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']]
678                         
679                         if param['type'] == 'float':
680                                 try:
681                                         value = float(value)
682                                 except:
683                                         value = float(param['default'])
684                         
685                         locals[param['name']] = value
686                 try:
687                         execfile(pythonFile, locals)
688                 except:
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])
691         return None
692
693 def getSDcardDrives():
694         drives = ['']
695         if platform.system() == "Windows":
696                 from ctypes import windll
697                 bitmask = windll.kernel32.GetLogicalDrives()
698                 for letter in string.uppercase:
699                         if bitmask & 1:
700                                 drives.append(letter + ':/')
701                         bitmask >>= 1
702         if platform.system() == "Darwin":
703                 drives = []
704                 for volume in glob.glob('/Volumes/*'):
705                         if stat.S_ISLNK(os.lstat(volume).st_mode):
706                                 continue
707                         drives.append(volume)
708         return drives