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