chiark / gitweb /
Fixed the imports.
[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 version
12
13 #########################################################
14 ## Default settings when none are found.
15 #########################################################
16
17 #Single place to store the defaults, so we have a consistent set of default settings.
18 profileDefaultSettings = {
19         'nozzle_size': '0.4',
20         'layer_height': '0.2',
21         'wall_thickness': '0.8',
22         'solid_layer_thickness': '0.6',
23         'fill_density': '20',
24         'skirt_line_count': '1',
25         'skirt_gap': '3.0',
26         'print_speed': '50',
27         'print_temperature': '220',
28         'print_bed_temperature': '70',
29         'support': 'None',
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',
39         'max_z_speed': '3.0',
40         'bottom_layer_speed': '20',
41         'cool_min_layer_time': '5',
42         'fan_enabled': 'True',
43         'fan_layer': '1',
44         'fan_speed': '100',
45         'fan_speed_max': '100',
46         'model_scale': '1.0',
47         'flip_x': 'False',
48         'flip_y': 'False',
49         'flip_z': 'False',
50         'swap_xz': 'False',
51         'swap_yz': 'False',
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',
59         'solid_top': 'True',
60         'fill_overlap': '15',
61         'support_rate': '50',
62         'support_distance': '0.5',
63         'support_dual_extrusion': 'False',
64         'joris': 'False',
65         'enable_skin': 'False',
66         'enable_raft': 'False',
67         'cool_min_feedrate': '10',
68         'bridge_speed': '100',
69         'raft_margin': '5',
70         'raft_base_material_amount': '100',
71         'raft_interface_material_amount': '100',
72         'bottom_thickness': '0.3',
73         'hop_on_move': 'False',
74         'plugin_config': '',
75         
76         'add_start_end_gcode': 'True',
77         'gcode_extension': 'gcode',
78         'alternative_center': '',
79         'clear_z': '0.0',
80         'extruder': '0',
81 }
82 alterationDefault = {
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}
89 G21        ;metric values
90 G90        ;absolute positioning
91 M107       ;start with the fan off
92
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
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 """,
104 #######################################################################################
105         'end.gcode': """;End GCode
106 M104 S0                     ;extruder heater off
107 M140 S0                     ;heated bed heater off (if you have it)
108
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
113
114 M84                         ;steppers off
115 G90                         ;absolute positioning
116 """,
117 #######################################################################################
118         'support_start.gcode': '',
119         'support_end.gcode': '',
120         'cool_start.gcode': '',
121         'cool_end.gcode': '',
122         'replace.csv': '',
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.
125 G92 E0
126
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
131
132 G1 Z{clear_z} F{max_z_speed}
133 G92 E0
134 G1 X{object_center_x} Y{object_center_x} F{travel_speed}
135 G1 F200 E6
136 G92 E0
137 """,
138 #######################################################################################
139         'switchExtruder.gcode': """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
140 G92 E0
141 G1 E-5 F5000
142 G92 E0
143 T{extruder}
144 G1 E5 F5000
145 G92 E0
146 """,
147 }
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',
165         'steps_per_e': '0',
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',
174         'sdpath': '',
175         'sdshortnames': 'True',
176         
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',
182         
183         'model_colour': '#72CB30',
184         'model_colour2': '#CB3030',
185         'model_colour3': '#DDD93C',
186         'model_colour4': '#4550D3',
187 }
188
189 #########################################################
190 ## Profile and preferences functions
191 #########################################################
192
193 ## Profile functions
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, ".."))
200         else:
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')
205
206 def loadGlobalProfile(filename):
207         #Read a configuration file as global config
208         global globalProfileParser
209         globalProfileParser = ConfigParser.ConfigParser()
210         globalProfileParser.read(filename)
211
212 def resetGlobalProfile():
213         #Read a configuration file as global config
214         global globalProfileParser
215         globalProfileParser = ConfigParser.ConfigParser()
216
217         if getPreference('machine_type') == 'ultimaker':
218                 putProfileSetting('nozzle_size', '0.4')
219                 if getPreference('ultimaker_extruder_upgrade') == 'True':
220                         putProfileSetting('retraction_enable', 'True')
221         else:
222                 putProfileSetting('nozzle_size', '0.5')
223
224 def saveGlobalProfile(filename):
225         #Save the current profile to an ini file
226         globalProfileParser.write(open(filename, 'w'))
227
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'):
237                 if len(option) > 0:
238                         (key, value) = option.split('=', 1)
239                         globalProfileParser.set('profile', key, value)
240         for option in alt.split('\b'):
241                 if len(option) > 0:
242                         (key, value) = option.split('=', 1)
243                         globalProfileParser.set('alterations', key, value)
244
245 def getGlobalProfileString():
246         global globalProfileParser
247         if not globals().has_key('globalProfileParser'):
248                 loadGlobalProfile(getDefaultProfilePath())
249         
250         p = []
251         alt = []
252         tempDone = []
253         if globalProfileParser.has_section('profile'):
254                 for key in globalProfileParser.options('profile'):
255                         if key in tempOverride:
256                                 p.append(key + "=" + tempOverride[key])
257                                 tempDone.append(key)
258                         else:
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])
264                                 tempDone.append(key)
265                         else:
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))
272         return ret
273
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]
283                 else:
284                         print("Missing default setting for: '" + name + "'")
285                         profileDefaultSettings[name] = ''
286                         default = ''
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))
291                 return default
292         return globalProfileParser.get('profile', name)
293
294 def getProfileSettingFloat(name):
295         try:
296                 setting = getProfileSetting(name).replace(',', '.')
297                 return float(eval(setting, {}, {}))
298         except (ValueError, SyntaxError, TypeError):
299                 return 0.0
300
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))
308
309 def isProfileSetting(name):
310         if name in profileDefaultSettings:
311                 return True
312         return False
313
314 ## Preferences functions
315 global globalPreferenceParser
316 globalPreferenceParser = None
317
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, ".."))
324         else:
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')
329
330 def getPreferenceFloat(name):
331         try:
332                 setting = getPreference(name).replace(',', '.')
333                 return float(eval(setting, {}, {}))
334         except (ValueError, SyntaxError, TypeError):
335                 return 0.0
336
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]
340
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]
351                 else:
352                         print("Missing default setting for: '" + name + "'")
353                         preferencesDefaultSettings[name] = ''
354                         default = ''
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))
359                 return default
360         return unicode(globalPreferenceParser.get('preference', name), "utf-8")
361
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'))
372
373 def isPreference(name):
374         if name in preferencesDefaultSettings:
375                 return True
376         return False
377
378 ## Temp overrides for multi-extruder slicing and the project planner.
379 tempOverride = {}
380 def setTempOverride(name, value):
381         tempOverride[name] = unicode(value).encode("utf-8")
382 def clearTempOverride(name):
383         del tempOverride[name]
384 def resetTempOverride():
385         tempOverride.clear()
386
387 #########################################################
388 ## Utility functions to calculate common profile values
389 #########################################################
390 def calculateEdgeWidth():
391         wallThickness = getProfileSettingFloat('wall_thickness')
392         nozzleSize = getProfileSettingFloat('nozzle_size')
393         
394         if wallThickness < nozzleSize:
395                 return wallThickness
396
397         lineCount = int(wallThickness / nozzleSize)
398         lineWidth = wallThickness / lineCount
399         lineWidthAlt = wallThickness / (lineCount + 1)
400         if lineWidth > nozzleSize * 1.5:
401                 return lineWidthAlt
402         return lineWidth
403
404 def calculateLineCount():
405         wallThickness = getProfileSettingFloat('wall_thickness')
406         nozzleSize = getProfileSettingFloat('nozzle_size')
407         
408         if wallThickness < nozzleSize:
409                 return 1
410
411         lineCount = int(wallThickness / nozzleSize + 0.0001)
412         lineWidth = wallThickness / lineCount
413         lineWidthAlt = wallThickness / (lineCount + 1)
414         if lineWidth > nozzleSize * 1.5:
415                 return lineCount + 1
416         return lineCount
417
418 def calculateSolidLayerCount():
419         layerHeight = getProfileSettingFloat('layer_height')
420         solidThickness = getProfileSettingFloat('solid_layer_thickness')
421         return int(math.ceil(solidThickness / layerHeight - 0.0001))
422
423 #########################################################
424 ## Alteration file functions
425 #########################################################
426 def replaceTagMatch(m):
427         pre = m.group(1)
428         tag = m.group(2)
429         if tag == 'time':
430                 return pre + time.strftime('%H:%M:%S')
431         if tag == 'date':
432                 return pre + time.strftime('%d %b %Y')
433         if tag == 'day':
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)
449         else:
450                 return '%s?%s?' % (pre, tag)
451         if (f % 1) == 0:
452                 return pre + str(int(f))
453         return pre + str(f)
454
455 def replaceGCodeTags(filename, gcodeInt):
456         f = open(filename, 'r+')
457         data = f.read(2048)
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()
462         if cost == False:
463                 cost = 'Unknown'
464         data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
465         f.seek(0)
466         f.write(data)
467         f.close()
468
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())
474         
475         if not globalProfileParser.has_option('alterations', filename):
476                 if filename in alterationDefault:
477                         default = alterationDefault[filename]
478                 else:
479                         print("Missing default alteration for: '" + filename + "'")
480                         alterationDefault[filename] = ''
481                         default = ''
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")
487
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())
496
497 ### Get the alteration file for output. (Used by Skeinforge)
498 def getAlterationFileContents(filename):
499         prefix = ''
500         postfix = ''
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')
506                 if eSteps > 0:
507                         prefix += 'M92 E%f\n' % (eSteps)
508                 temp = getProfileSettingFloat('print_temperature')
509                 bedTemp = 0
510                 if getPreference('has_heated_bed') == 'True':
511                         bedTemp = getProfileSettingFloat('print_bed_temperature')
512                 
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')
530                         else:
531                                 setTempOverride('extruder', '0')
532                         alterationContents = getAlterationFileContents('switchExtruder.gcode')
533                         clearTempOverride('extruder')
534                 else:
535                         alterationContents = ''
536         return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8')
537
538 ###### PLUGIN #####
539
540 def getPluginConfig():
541         try:
542                 return pickle.loads(getProfileSetting('plugin_config'))
543         except:
544                 return []
545
546 def setPluginConfig(config):
547         putProfileSetting('plugin_config', pickle.dumps(config))
548
549 def getPluginBasePaths():
550         ret = []
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')))
554         return ret
555
556 def getPluginList():
557         ret = []
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('_'):
562                                 continue
563                         with open(os.path.join(basePath, filename), "r") as f:
564                                 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
565                                 for line in f:
566                                         line = line.strip()
567                                         if not line.startswith('#'):
568                                                 break
569                                         line = line[1:].split(':', 1)
570                                         if len(line) != 2:
571                                                 continue
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':
579                                                 pass
580                                         elif line[0].upper() == 'PARAM':
581                                                 m = re.match('([a-zA-Z]*)\(([a-zA-Z_]*)(?:\:([^\)]*))?\) +(.*)', line[1].strip())
582                                                 if m != None:
583                                                         item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
584                                         else:
585                                                 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
586                                 if item['name'] != None and item['type'] == 'postprocess':
587                                         ret.append(item)
588         return ret
589
590 def runPostProcessingPlugins(gcodefilename):
591         pluginConfigList = getPluginConfig()
592         pluginList = getPluginList()
593         
594         for pluginConfig in pluginConfigList:
595                 plugin = None
596                 for pluginTest in pluginList:
597                         if pluginTest['filename'] == pluginConfig['filename']:
598                                 plugin = pluginTest
599                 if plugin == None:
600                         continue
601                 
602                 pythonFile = None
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:
608                         continue
609                 
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']]
615                         
616                         if param['type'] == 'float':
617                                 try:
618                                         value = float(value)
619                                 except:
620                                         value = float(param['default'])
621                         
622                         locals[param['name']] = value
623                 try:
624                         execfile(pythonFile, locals)
625                 except:
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])
628         return None
629
630 def getSDcardDrives():
631         drives = ['']
632         if platform.system() == "Windows":
633                 from ctypes import windll
634                 bitmask = windll.kernel32.GetLogicalDrives()
635                 for letter in string.uppercase:
636                         if bitmask & 1:
637                                 drives.append(letter + ':/')
638                         bitmask >>= 1
639         if platform.system() == "Darwin":
640                 drives = []
641                 for volume in glob.glob('/Volumes/*'):
642                         if stat.S_ISLNK(os.lstat(volume).st_mode):
643                                 continue
644                         drives.append(volume)
645         return drives