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