chiark / gitweb /
Update the statistics upload with preferences and version info.
[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_bed_temperature': '70',
30         'support': 'None',
31         'filament_diameter': '2.89',
32         'filament_density': '1.00',
33         'retraction_min_travel': '5.0',
34         'retraction_enable': 'False',
35         'retraction_speed': '40.0',
36         'retraction_amount': '4.5',
37         'retraction_extra': '0.0',
38         'retract_on_jumps_only': 'True',
39         'travel_speed': '150',
40         'max_z_speed': '3.0',
41         'bottom_layer_speed': '20',
42         'cool_min_layer_time': '5',
43         'fan_enabled': 'True',
44         'fan_layer': '1',
45         'fan_speed': '100',
46         'fan_speed_max': '100',
47         'model_matrix': '1,0,0,0,1,0,0,0,1',
48         'extra_base_wall_thickness': '0.0',
49         'sequence': 'Loops > Perimeter > Infill',
50         'force_first_layer_sequence': 'True',
51         'infill_type': 'Line',
52         'solid_top': 'True',
53         'fill_overlap': '15',
54         'support_rate': '50',
55         'support_distance': '0.5',
56         'support_dual_extrusion': 'False',
57         'joris': 'False',
58         'enable_skin': 'False',
59         'enable_raft': 'False',
60         'cool_min_feedrate': '10',
61         'bridge_speed': '100',
62         'raft_margin': '5',
63         'raft_base_material_amount': '100',
64         'raft_interface_material_amount': '100',
65         'bottom_thickness': '0.3',
66         'hop_on_move': 'False',
67         'plugin_config': '',
68         'object_center_x': '-1',
69         'object_center_y': '-1',
70         'object_sink': '0.0',
71         
72         'gcode_extension': 'gcode',
73         'alternative_center': '',
74         'clear_z': '0.0',
75         'extruder': '0',
76 }
77 alterationDefault = {
78 #######################################################################################
79         'start.gcode': """;Sliced {filename} at: {day} {date} {time}
80 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
81 ;Print time: {print_time}
82 ;Filament used: {filament_amount}m {filament_weight}g
83 ;Filament cost: {filament_cost}
84 G21        ;metric values
85 G90        ;absolute positioning
86 M107       ;start with the fan off
87
88 G28 X0 Y0  ;move X/Y to min endstops
89 G28 Z0     ;move Z to min endstops
90
91 G1 Z15.0 F{max_z_speed} ;move the platform down 15mm
92
93 G92 E0                  ;zero the extruded length
94 G1 F200 E3              ;extrude 3mm of feed stock
95 G92 E0                  ;zero the extruded length again
96 G1 F{travel_speed}
97 M117 Printing...
98 """,
99 #######################################################################################
100         'end.gcode': """;End GCode
101 M104 S0                     ;extruder heater off
102 M140 S0                     ;heated bed heater off (if you have it)
103
104 G91                                    ;relative positioning
105 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
106 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
107 G28 X0 Y0                              ;move X/Y to min endstops, so the head is out of the way
108
109 M84                         ;steppers off
110 G90                         ;absolute positioning
111 """,
112 #######################################################################################
113         'support_start.gcode': '',
114         'support_end.gcode': '',
115         'cool_start.gcode': '',
116         'cool_end.gcode': '',
117         'replace.csv': '',
118 #######################################################################################
119         '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.
120 G92 E0
121
122 G91                                    ;relative positioning
123 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
124 G1 Z+0.5 E-5 F{travel_speed}           ;move Z up a bit and retract filament even more
125 G90                                    ;absolute positioning
126
127 G1 Z{clear_z} F{max_z_speed}
128 G92 E0
129 G1 X{object_center_x} Y{object_center_y} F{travel_speed}
130 G1 F200 E6
131 G92 E0
132 """,
133 #######################################################################################
134         'switchExtruder.gcode': """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
135 G92 E0
136 G1 E-15 F5000
137 G92 E0
138 T{extruder}
139 G1 E15 F5000
140 G92 E0
141 """,
142 }
143 preferencesDefaultSettings = {
144         'startMode': 'Simple',
145         'lastFile': os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'resources', 'example', 'UltimakerRobot_support.stl')),
146         'machine_width': '205',
147         'machine_depth': '205',
148         'machine_height': '200',
149         'machine_type': 'unknown',
150         'machine_center_is_zero': 'False',
151         'ultimaker_extruder_upgrade': 'False',
152         'has_heated_bed': 'False',
153         'reprap_name': 'RepRap',
154         'extruder_amount': '1',
155         'extruder_offset_x1': '-22.0',
156         'extruder_offset_y1': '0.0',
157         'extruder_offset_x2': '0.0',
158         'extruder_offset_y2': '0.0',
159         'extruder_offset_x3': '0.0',
160         'extruder_offset_y3': '0.0',
161         'filament_density': '1300',
162         'steps_per_e': '0',
163         'serial_port': 'AUTO',
164         'serial_port_auto': '',
165         'serial_baud': 'AUTO',
166         'serial_baud_auto': '',
167         'slicer': 'Cura (Skeinforge based)',
168         'save_profile': 'False',
169         'filament_cost_kg': '0',
170         'filament_cost_meter': '0',
171         'sdpath': '',
172         'sdshortnames': 'False',
173         'check_for_updates': 'True',
174         'submit_slice_information': 'False',
175
176         'planner_always_autoplace': 'True',
177         'extruder_head_size_min_x': '75.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': '60.0',
182         
183         'model_colour': '#7AB645',
184         'model_colour2': '#CB3030',
185         'model_colour3': '#DDD93C',
186         'model_colour4': '#4550D3',
187
188         'window_maximized': 'False',
189         'window_pos_x': '-1',
190         'window_pos_y': '-1',
191         'window_width': '-1',
192         'window_height': '-1',
193         'window_normal_sash': '320',
194 }
195
196 #########################################################
197 ## Profile and preferences functions
198 #########################################################
199
200 ## Profile functions
201 def getBasePath():
202         if platform.system() == "Windows":
203                 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
204                 #If we have a frozen python install, we need to step out of the library.zip
205                 if hasattr(sys, 'frozen'):
206                         basePath = os.path.normpath(os.path.join(basePath, ".."))
207         else:
208                 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
209         if not os.path.isdir(basePath):
210                 os.makedirs(basePath)
211         return basePath
212
213 def getDefaultProfilePath():
214         return os.path.join(getBasePath(), 'current_profile.ini')
215
216 def loadGlobalProfile(filename):
217         #Read a configuration file as global config
218         global globalProfileParser
219         globalProfileParser = ConfigParser.ConfigParser()
220         try:
221                 globalProfileParser.read(filename)
222         except ConfigParser.ParsingError:
223                 pass
224
225 def resetGlobalProfile():
226         #Read a configuration file as global config
227         global globalProfileParser
228         globalProfileParser = ConfigParser.ConfigParser()
229
230         if getPreference('machine_type') == 'ultimaker':
231                 putProfileSetting('nozzle_size', '0.4')
232                 if getPreference('ultimaker_extruder_upgrade') == 'True':
233                         putProfileSetting('retraction_enable', 'True')
234         else:
235                 putProfileSetting('nozzle_size', '0.5')
236
237 def saveGlobalProfile(filename):
238         #Save the current profile to an ini file
239         globalProfileParser.write(open(filename, 'w'))
240
241 def loadGlobalProfileFromString(options):
242         global globalProfileParser
243         globalProfileParser = ConfigParser.ConfigParser()
244         globalProfileParser.add_section('profile')
245         globalProfileParser.add_section('alterations')
246         options = base64.b64decode(options)
247         options = zlib.decompress(options)
248         (profileOpts, alt) = options.split('\f', 1)
249         for option in profileOpts.split('\b'):
250                 if len(option) > 0:
251                         (key, value) = option.split('=', 1)
252                         globalProfileParser.set('profile', key, value)
253         for option in alt.split('\b'):
254                 if len(option) > 0:
255                         (key, value) = option.split('=', 1)
256                         globalProfileParser.set('alterations', key, value)
257
258 def getGlobalProfileString():
259         global globalProfileParser
260         if not globals().has_key('globalProfileParser'):
261                 loadGlobalProfile(getDefaultProfilePath())
262         
263         p = []
264         alt = []
265         tempDone = []
266         if globalProfileParser.has_section('profile'):
267                 for key in globalProfileParser.options('profile'):
268                         if key in tempOverride:
269                                 p.append(key + "=" + tempOverride[key])
270                                 tempDone.append(key)
271                         else:
272                                 p.append(key + "=" + globalProfileParser.get('profile', key))
273         if globalProfileParser.has_section('alterations'):
274                 for key in globalProfileParser.options('alterations'):
275                         if key in tempOverride:
276                                 p.append(key + "=" + tempOverride[key])
277                                 tempDone.append(key)
278                         else:
279                                 alt.append(key + "=" + globalProfileParser.get('alterations', key))
280         for key in tempOverride:
281                 if key not in tempDone:
282                         p.append(key + "=" + tempOverride[key])
283         ret = '\b'.join(p) + '\f' + '\b'.join(alt)
284         ret = base64.b64encode(zlib.compress(ret, 9))
285         return ret
286
287 def getGlobalPreferencesString():
288         global globalPreferenceParser
289         if globalPreferenceParser is None:
290                 globalPreferenceParser = ConfigParser.ConfigParser()
291                 try:
292                         globalPreferenceParser.read(getPreferencePath())
293                 except ConfigParser.ParsingError:
294                         pass
295
296         p = []
297         if globalPreferenceParser.has_section('preference'):
298                 for key in globalPreferenceParser.options('preference'):
299                         p.append(key + "=" + globalPreferenceParser.get('preference', key))
300         ret = '\b'.join(p)
301         ret = base64.b64encode(zlib.compress(ret, 9))
302         return ret
303
304
305 def getProfileSetting(name):
306         if name in tempOverride:
307                 return unicode(tempOverride[name], "utf-8")
308         #Check if we have a configuration file loaded, else load the default.
309         if not globals().has_key('globalProfileParser'):
310                 loadGlobalProfile(getDefaultProfilePath())
311         if not globalProfileParser.has_option('profile', name):
312                 if name in profileDefaultSettings:
313                         default = profileDefaultSettings[name]
314                 else:
315                         print("Missing default setting for: '" + name + "'")
316                         profileDefaultSettings[name] = ''
317                         default = ''
318                 if not globalProfileParser.has_section('profile'):
319                         globalProfileParser.add_section('profile')
320                 globalProfileParser.set('profile', name, str(default))
321                 #print(name + " not found in profile, so using default: " + str(default))
322                 return default
323         return globalProfileParser.get('profile', name)
324
325 def getProfileSettingFloat(name):
326         try:
327                 setting = getProfileSetting(name).replace(',', '.')
328                 return float(eval(setting, {}, {}))
329         except (ValueError, SyntaxError, TypeError):
330                 return 0.0
331
332 def putProfileSetting(name, value):
333         #Check if we have a configuration file loaded, else load the default.
334         if not globals().has_key('globalProfileParser'):
335                 loadGlobalProfile(getDefaultProfilePath())
336         if not globalProfileParser.has_section('profile'):
337                 globalProfileParser.add_section('profile')
338         globalProfileParser.set('profile', name, str(value))
339
340 def isProfileSetting(name):
341         if name in profileDefaultSettings:
342                 return True
343         return False
344
345 ## Preferences functions
346 global globalPreferenceParser
347 globalPreferenceParser = None
348
349 def getPreferencePath():
350         return os.path.join(getBasePath(), 'preferences.ini')
351
352 def getPreferenceFloat(name):
353         try:
354                 setting = getPreference(name).replace(',', '.')
355                 return float(eval(setting, {}, {}))
356         except (ValueError, SyntaxError, TypeError):
357                 return 0.0
358
359 def getPreferenceColour(name):
360         colorString = getPreference(name)
361         return [float(int(colorString[1:3], 16)) / 255, float(int(colorString[3:5], 16)) / 255, float(int(colorString[5:7], 16)) / 255, 1.0]
362
363 def getPreference(name):
364         if name in tempOverride:
365                 return unicode(tempOverride[name])
366         global globalPreferenceParser
367         if globalPreferenceParser is None:
368                 globalPreferenceParser = ConfigParser.ConfigParser()
369                 try:
370                         globalPreferenceParser.read(getPreferencePath())
371                 except ConfigParser.ParsingError:
372                         pass
373         if not globalPreferenceParser.has_option('preference', name):
374                 if name in preferencesDefaultSettings:
375                         default = preferencesDefaultSettings[name]
376                 else:
377                         print("Missing default setting for: '" + name + "'")
378                         preferencesDefaultSettings[name] = ''
379                         default = ''
380                 if not globalPreferenceParser.has_section('preference'):
381                         globalPreferenceParser.add_section('preference')
382                 globalPreferenceParser.set('preference', name, str(default))
383                 #print(name + " not found in preferences, so using default: " + str(default))
384                 return default
385         return unicode(globalPreferenceParser.get('preference', name), "utf-8")
386
387 def putPreference(name, value):
388         #Check if we have a configuration file loaded, else load the default.
389         global globalPreferenceParser
390         if globalPreferenceParser == None:
391                 globalPreferenceParser = ConfigParser.ConfigParser()
392                 try:
393                         globalPreferenceParser.read(getPreferencePath())
394                 except ConfigParser.ParsingError:
395                         pass
396         if not globalPreferenceParser.has_section('preference'):
397                 globalPreferenceParser.add_section('preference')
398         globalPreferenceParser.set('preference', name, unicode(value).encode("utf-8"))
399         globalPreferenceParser.write(open(getPreferencePath(), 'w'))
400
401 def isPreference(name):
402         if name in preferencesDefaultSettings:
403                 return True
404         return False
405
406 ## Temp overrides for multi-extruder slicing and the project planner.
407 tempOverride = {}
408 def setTempOverride(name, value):
409         tempOverride[name] = unicode(value).encode("utf-8")
410 def clearTempOverride(name):
411         del tempOverride[name]
412 def resetTempOverride():
413         tempOverride.clear()
414
415 #########################################################
416 ## Utility functions to calculate common profile values
417 #########################################################
418 def calculateEdgeWidth():
419         wallThickness = getProfileSettingFloat('wall_thickness')
420         nozzleSize = getProfileSettingFloat('nozzle_size')
421         
422         if wallThickness < nozzleSize:
423                 return wallThickness
424
425         lineCount = int(wallThickness / nozzleSize + 0.0001)
426         lineWidth = wallThickness / lineCount
427         lineWidthAlt = wallThickness / (lineCount + 1)
428         if lineWidth > nozzleSize * 1.5:
429                 return lineWidthAlt
430         return lineWidth
431
432 def calculateLineCount():
433         wallThickness = getProfileSettingFloat('wall_thickness')
434         nozzleSize = getProfileSettingFloat('nozzle_size')
435         
436         if wallThickness < nozzleSize:
437                 return 1
438
439         lineCount = int(wallThickness / nozzleSize + 0.0001)
440         lineWidth = wallThickness / lineCount
441         lineWidthAlt = wallThickness / (lineCount + 1)
442         if lineWidth > nozzleSize * 1.5:
443                 return lineCount + 1
444         return lineCount
445
446 def calculateSolidLayerCount():
447         layerHeight = getProfileSettingFloat('layer_height')
448         solidThickness = getProfileSettingFloat('solid_layer_thickness')
449         return int(math.ceil(solidThickness / layerHeight - 0.0001))
450
451 def getMachineCenterCoords():
452         if getPreference('machine_center_is_zero') == 'True':
453                 return [0, 0]
454         return [getPreferenceFloat('machine_width') / 2, getPreferenceFloat('machine_depth') / 2]
455
456 def getObjectMatrix():
457         try:
458                 return map(float, getProfileSetting('model_matrix').split(','))
459         except ValueError:
460                 return [1,0,0, 0,1,0, 0,0,1]
461
462
463 #########################################################
464 ## Alteration file functions
465 #########################################################
466 def replaceTagMatch(m):
467         pre = m.group(1)
468         tag = m.group(2)
469         if tag == 'time':
470                 return pre + time.strftime('%H:%M:%S').encode('utf-8', 'replace')
471         if tag == 'date':
472                 return pre + time.strftime('%d %b %Y').encode('utf-8', 'replace')
473         if tag == 'day':
474                 return pre + time.strftime('%a').encode('utf-8', 'replace')
475         if tag == 'print_time':
476                 return pre + '#P_TIME#'
477         if tag == 'filament_amount':
478                 return pre + '#F_AMNT#'
479         if tag == 'filament_weight':
480                 return pre + '#F_WGHT#'
481         if tag == 'filament_cost':
482                 return pre + '#F_COST#'
483         if pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
484                 f = getProfileSettingFloat(tag) * 60
485         elif isProfileSetting(tag):
486                 f = getProfileSettingFloat(tag)
487         elif isPreference(tag):
488                 f = getProfileSettingFloat(tag)
489         else:
490                 return '%s?%s?' % (pre, tag)
491         if (f % 1) == 0:
492                 return pre + str(int(f))
493         return pre + str(f)
494
495 def replaceGCodeTags(filename, gcodeInt):
496         f = open(filename, 'r+')
497         data = f.read(2048)
498         data = data.replace('#P_TIME#', ('%5d:%02d' % (int(gcodeInt.totalMoveTimeMinute / 60), int(gcodeInt.totalMoveTimeMinute % 60)))[-8:])
499         data = data.replace('#F_AMNT#', ('%8.2f' % (gcodeInt.extrusionAmount / 1000))[-8:])
500         data = data.replace('#F_WGHT#', ('%8.2f' % (gcodeInt.calculateWeight() * 1000))[-8:])
501         cost = gcodeInt.calculateCost()
502         if cost is None:
503                 cost = 'Unknown'
504         data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
505         f.seek(0)
506         f.write(data)
507         f.close()
508
509 ### Get aleration raw contents. (Used internally in Cura)
510 def getAlterationFile(filename):
511         #Check if we have a configuration file loaded, else load the default.
512         if not globals().has_key('globalProfileParser'):
513                 loadGlobalProfile(getDefaultProfilePath())
514         
515         if not globalProfileParser.has_option('alterations', filename):
516                 if filename in alterationDefault:
517                         default = alterationDefault[filename]
518                 else:
519                         print("Missing default alteration for: '" + filename + "'")
520                         alterationDefault[filename] = ''
521                         default = ''
522                 if not globalProfileParser.has_section('alterations'):
523                         globalProfileParser.add_section('alterations')
524                 #print("Using default for: %s" % (filename))
525                 globalProfileParser.set('alterations', filename, default)
526         return unicode(globalProfileParser.get('alterations', filename), "utf-8")
527
528 def setAlterationFile(filename, value):
529         #Check if we have a configuration file loaded, else load the default.
530         if not globals().has_key('globalProfileParser'):
531                 loadGlobalProfile(getDefaultProfilePath())
532         if not globalProfileParser.has_section('alterations'):
533                 globalProfileParser.add_section('alterations')
534         globalProfileParser.set('alterations', filename, value.encode("utf-8"))
535         saveGlobalProfile(getDefaultProfilePath())
536
537 ### Get the alteration file for output. (Used by Skeinforge)
538 def getAlterationFileContents(filename):
539         prefix = ''
540         postfix = ''
541         alterationContents = getAlterationFile(filename)
542         if filename == 'start.gcode':
543                 #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.
544                 #We also set our steps per E here, if configured.
545                 eSteps = getPreferenceFloat('steps_per_e')
546                 if eSteps > 0:
547                         prefix += 'M92 E%f\n' % (eSteps)
548                 temp = getProfileSettingFloat('print_temperature')
549                 bedTemp = 0
550                 if getPreference('has_heated_bed') == 'True':
551                         bedTemp = getProfileSettingFloat('print_bed_temperature')
552                 
553                 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
554                         prefix += 'M140 S%f\n' % (bedTemp)
555                 if temp > 0 and not '{print_temperature}' in alterationContents:
556                         prefix += 'M109 S%f\n' % (temp)
557                 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
558                         prefix += 'M190 S%f\n' % (bedTemp)
559         elif filename == 'end.gcode':
560                 #Append the profile string to the end of the GCode, so we can load it from the GCode file later.
561                 postfix = ';CURA_PROFILE_STRING:%s\n' % (getGlobalProfileString())
562         elif filename == 'replace.csv':
563                 #Always remove the extruder on/off M codes. These are no longer needed in 5D printing.
564                 prefix = 'M101\nM103\n'
565         elif filename == 'support_start.gcode' or filename == 'support_end.gcode':
566                 #Add support start/end code 
567                 if getProfileSetting('support_dual_extrusion') == 'True' and int(getPreference('extruder_amount')) > 1:
568                         if filename == 'support_start.gcode':
569                                 setTempOverride('extruder', '1')
570                         else:
571                                 setTempOverride('extruder', '0')
572                         alterationContents = getAlterationFileContents('switchExtruder.gcode')
573                         clearTempOverride('extruder')
574                 else:
575                         alterationContents = ''
576         return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n'
577
578 ###### PLUGIN #####
579
580 def getPluginConfig():
581         try:
582                 return pickle.loads(getProfileSetting('plugin_config'))
583         except:
584                 return []
585
586 def setPluginConfig(config):
587         putProfileSetting('plugin_config', pickle.dumps(config))
588
589 def getPluginBasePaths():
590         ret = []
591         if platform.system() != "Windows":
592                 ret.append(os.path.expanduser('~/.cura/plugins/'))
593         if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
594                 ret.append(os.path.normpath(os.path.join(resources.resourceBasePath, "Cura/plugins")))
595         else:
596                 ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
597         return ret
598
599 def getPluginList():
600         ret = []
601         for basePath in getPluginBasePaths():
602                 for filename in glob.glob(os.path.join(basePath, '*.py')):
603                         filename = os.path.basename(filename)
604                         if filename.startswith('_'):
605                                 continue
606                         with open(os.path.join(basePath, filename), "r") as f:
607                                 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
608                                 for line in f:
609                                         line = line.strip()
610                                         if not line.startswith('#'):
611                                                 break
612                                         line = line[1:].split(':', 1)
613                                         if len(line) != 2:
614                                                 continue
615                                         if line[0].upper() == 'NAME':
616                                                 item['name'] = line[1].strip()
617                                         elif line[0].upper() == 'INFO':
618                                                 item['info'] = line[1].strip()
619                                         elif line[0].upper() == 'TYPE':
620                                                 item['type'] = line[1].strip()
621                                         elif line[0].upper() == 'DEPEND':
622                                                 pass
623                                         elif line[0].upper() == 'PARAM':
624                                                 m = re.match('([a-zA-Z][a-zA-Z0-9_]*)\(([a-zA-Z_]*)(?::([^\)]*))?\) +(.*)', line[1].strip())
625                                                 if m is not None:
626                                                         item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
627                                         else:
628                                                 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
629                                 if item['name'] != None and item['type'] == 'postprocess':
630                                         ret.append(item)
631         return ret
632
633 def runPostProcessingPlugins(gcodefilename):
634         pluginConfigList = getPluginConfig()
635         pluginList = getPluginList()
636         
637         for pluginConfig in pluginConfigList:
638                 plugin = None
639                 for pluginTest in pluginList:
640                         if pluginTest['filename'] == pluginConfig['filename']:
641                                 plugin = pluginTest
642                 if plugin is None:
643                         continue
644                 
645                 pythonFile = None
646                 for basePath in getPluginBasePaths():
647                         testFilename = os.path.join(basePath, pluginConfig['filename'])
648                         if os.path.isfile(testFilename):
649                                 pythonFile = testFilename
650                 if pythonFile is None:
651                         continue
652                 
653                 locals = {'filename': gcodefilename}
654                 for param in plugin['params']:
655                         value = param['default']
656                         if param['name'] in pluginConfig['params']:
657                                 value = pluginConfig['params'][param['name']]
658                         
659                         if param['type'] == 'float':
660                                 try:
661                                         value = float(value)
662                                 except:
663                                         value = float(param['default'])
664                         
665                         locals[param['name']] = value
666                 try:
667                         execfile(pythonFile, locals)
668                 except:
669                         locationInfo = traceback.extract_tb(sys.exc_info()[2])[-1]
670                         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])
671         return None
672
673 def getSDcardDrives():
674         drives = ['']
675         if platform.system() == "Windows":
676                 from ctypes import windll
677                 bitmask = windll.kernel32.GetLogicalDrives()
678                 for letter in string.uppercase:
679                         if bitmask & 1:
680                                 drives.append(letter + ':/')
681                         bitmask >>= 1
682         if platform.system() == "Darwin":
683                 drives = []
684                 for volume in glob.glob('/Volumes/*'):
685                         if stat.S_ISLNK(os.lstat(volume).st_mode):
686                                 continue
687                         drives.append(volume)
688         return drives