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