chiark / gitweb /
Add option to disable SD auto detect. Log output if SD card remove fails.
[cura.git] / Cura / util / profile.py
1 from __future__ import absolute_import
2 from __future__ import division
3 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
4
5 import os, traceback, math, re, zlib, base64, time, sys, platform, glob, string, stat, types
6 import cPickle as pickle
7 if sys.version_info[0] < 3:
8         import ConfigParser
9 else:
10         import configparser as ConfigParser
11
12 from Cura.util import resources
13 from Cura.util import version
14 from Cura.util import validators
15
16 settingsDictionary = {}
17 settingsList = []
18 class setting(object):
19         def __init__(self, name, default, type, category, subcategory):
20                 self._name = name
21                 self._label = name
22                 self._tooltip = ''
23                 self._default = unicode(default)
24                 self._value = self._default
25                 self._type = type
26                 self._category = category
27                 self._subcategory = subcategory
28                 self._validators = []
29                 self._conditions = []
30
31                 if type is types.FloatType:
32                         validators.validFloat(self)
33                 elif type is types.IntType:
34                         validators.validInt(self)
35
36                 global settingsDictionary
37                 settingsDictionary[name] = self
38                 global settingsList
39                 settingsList.append(self)
40
41         def setLabel(self, label, tooltip = ''):
42                 self._label = label
43                 self._tooltip = tooltip
44                 return self
45
46         def setRange(self, minValue = None, maxValue = None):
47                 if len(self._validators) < 1:
48                         return
49                 self._validators[0].minValue = minValue
50                 self._validators[0].maxValue = maxValue
51                 return self
52
53         def getLabel(self):
54                 return self._label
55
56         def getTooltip(self):
57                 return self._tooltip
58
59         def getCategory(self):
60                 return self._category
61
62         def getSubCategory(self):
63                 return self._subcategory
64
65         def isPreference(self):
66                 return self._category == 'preference'
67
68         def isAlteration(self):
69                 return self._category == 'alteration'
70
71         def isProfile(self):
72                 return not self.isAlteration() and not self.isPreference()
73
74         def getName(self):
75                 return self._name
76
77         def getType(self):
78                 return self._type
79
80         def getValue(self):
81                 return self._value
82
83         def getDefault(self):
84                 return self._default
85
86         def setValue(self, value):
87                 self._value = unicode(value)
88
89         def validate(self):
90                 result = validators.SUCCESS
91                 msgs = []
92                 for validator in self._validators:
93                         res, err = validator.validate()
94                         if res == validators.ERROR:
95                                 result = res
96                         elif res == validators.WARNING and result != validators.ERROR:
97                                 result = res
98                         if res != validators.SUCCESS:
99                                 msgs.append(err)
100                 return result, '\n'.join(msgs)
101
102         def addCondition(self, conditionFunction):
103                 self._conditions.append(conditionFunction)
104
105         def checkConditions(self):
106                 for condition in self._conditions:
107                         if not condition():
108                                 return False
109                 return True
110
111 #########################################################
112 ## Settings
113 #########################################################
114 setting('layer_height',              0.1, float, 'basic',    'Quality').setRange(0.0001).setLabel('Layer height (mm)', 'Layer height in millimeters.\nThis is the most important setting to determen the quality of your print. Normal quality prints are 0.1mm, high quality is 0.06mm. You can go up to 0.25mm with an Ultimaker for very fast prints at low quality.')
115 setting('wall_thickness',            0.8, float, 'basic',    'Quality').setRange(0.0001).setLabel('Shell thickness (mm)', 'Thickness of the outside shell in the horizontal direction.\nThis is used in combination with the nozzle size to define the number\nof perimeter lines and the thickness of those perimeter lines.')
116 setting('retraction_enable',       False, bool,  'basic',    'Quality').setLabel('Enable retraction', 'Retract the filament when the nozzle is moving over a none-printed area. Details about the retraction can be configured in the advanced tab.')
117 setting('solid_layer_thickness',     0.6, float, 'basic',    'Fill').setRange(0).setLabel('Bottom/Top thickness (mm)', 'This controls the thickness of the bottom and top layers, the amount of solid layers put down is calculated by the layer thickness and this value.\nHaving this value a multiply of the layer thickness makes sense. And keep it near your wall thickness to make an evenly strong part.')
118 setting('fill_density',               20, float, 'basic',    'Fill').setRange(0, 100).setLabel('Fill Density (%)', 'This controls how densely filled the insides of your print will be. For a solid part use 100%, for an empty part use 0%. A value around 20% is usually enough.\nThis won\'t effect the outside of the print and only adjusts how strong the part becomes.')
119 setting('nozzle_size',               0.4, float, 'advanced', 'Machine').setRange(0.1,10).setLabel('Nozzle size (mm)', 'The nozzle size is very important, this is used to calculate the line width of the infill, and used to calculate the amount of outside wall lines and thickness for the wall thickness you entered in the print settings.')
120 setting('print_speed',                50, float, 'basic',    'Speed & Temperature').setRange(1).setLabel('Print speed (mm/s)', 'Speed at which printing happens. A well adjusted Ultimaker can reach 150mm/s, but for good quality prints you want to print slower. Printing speed depends on a lot of factors. So you will be experimenting with optimal settings for this.')
121 setting('print_temperature',         220, int,   'basic',    'Speed & Temperature').setRange(0,340).setLabel('Printing temperature (C)', 'Temperature used for printing. Set at 0 to pre-heat yourself.\nFor PLA a value of 210C is usually used.\nFor ABS a value of 230C or higher is required.')
122 setting('print_temperature2',          0, int,   'basic',    'Speed & Temperature').setRange(0,340).setLabel('2nd nozzle temperature (C)', 'Temperature used for printing. Set at 0 to pre-heat yourself.\nFor PLA a value of 210C is usually used.\nFor ABS a value of 230C or higher is required.')
123 setting('print_temperature3',          0, int,   'basic',    'Speed & Temperature').setRange(0,340).setLabel('3th nozzle temperature (C)', 'Temperature used for printing. Set at 0 to pre-heat yourself.\nFor PLA a value of 210C is usually used.\nFor ABS a value of 230C or higher is required.')
124 setting('print_temperature4',          0, int,   'basic',    'Speed & Temperature').setRange(0,340).setLabel('4th nozzle temperature (C)', 'Temperature used for printing. Set at 0 to pre-heat yourself.\nFor PLA a value of 210C is usually used.\nFor ABS a value of 230C or higher is required.')
125 setting('print_bed_temperature',      70, int,   'basic',    'Speed & Temperature').setRange(0,340).setLabel('Bed temperature (C)', 'Temperature used for the heated printer bed. Set at 0 to pre-heat yourself.')
126 setting('support',                'None', ['None', 'Touching buildplate', 'Everywhere'], 'basic', 'Support').setLabel('Support type', 'Type of support structure build.\n"Touching buildplate" is the most commonly used support setting.\n\nNone does not do any support.\nTouching buildplate only creates support where the support structure will touch the build platform.\nEverywhere creates support even on top of parts of the model.')
127 setting('platform_adhesion',      'None', ['None', 'Brim', 'Raft'], 'basic', 'Support').setLabel('Platform adhesion type', 'Different options that help in preventing corners from lifting due to warping.\nBrim adds a single layer thick flat area around your object which is easy to cut off afterwards, and the recommended option.\nRaft adds a thick raster at below the object and a thin interface between this and your object.\n(Note that enabling the brim or raft disables the skirt)')
128 setting('support_dual_extrusion',  False, bool, 'basic', 'Support').setLabel('Support dual extrusion', 'Print the support material with the 2nd extruder in a dual extrusion setup. The primary extruder will be used for normal material, while the second extruder is used to print support material.')
129 setting('filament_diameter',        2.89, float, 'basic',    'Filament').setRange(1).setLabel('Diameter (mm)', 'Diameter of your filament, as accurately as possible.\nIf you cannot measure this value you will have to calibrate it, a higher number means less extrusion, a smaller number generates more extrusion.')
130 setting('filament_diameter2',          0, float, 'basic',    'Filament').setRange(0).setLabel('Diameter2 (mm)', 'Diameter of your filament for the 2nd nozzle. Use 0 to use the same diameter as for nozzle 1.')
131 setting('filament_diameter3',          0, float, 'basic',    'Filament').setRange(0).setLabel('Diameter3 (mm)', 'Diameter of your filament for the 3th nozzle. Use 0 to use the same diameter as for nozzle 1.')
132 setting('filament_diameter4',          0, float, 'basic',    'Filament').setRange(0).setLabel('Diameter4 (mm)', 'Diameter of your filament for the 4th nozzle. Use 0 to use the same diameter as for nozzle 1.')
133 setting('filament_flow',            100., float, 'basic',    'Filament').setRange(1,300).setLabel('Flow (%)', 'Flow compensation, the amount of material extruded is multiplied by this value')
134 #setting('retraction_min_travel',     5.0, float, 'advanced', 'Retraction').setRange(0).setLabel('Minimum travel (mm)', 'Minimum amount of travel needed for a retraction to happen at all. To make sure you do not get a lot of retractions in a small area')
135 setting('retraction_speed',         40.0, float, 'advanced', 'Retraction').setRange(0.1).setLabel('Speed (mm/s)', 'Speed at which the filament is retracted, a higher retraction speed works better. But a very high retraction speed can lead to filament grinding.')
136 setting('retraction_amount',         4.5, float, 'advanced', 'Retraction').setRange(0).setLabel('Distance (mm)', 'Amount of retraction, set at 0 for no retraction at all. A value of 2.0mm seems to generate good results.')
137 #setting('retraction_extra',          0.0, float, 'advanced', 'Retraction').setRange(0).setLabel('Extra length on start (mm)', 'Extra extrusion amount when restarting after a retraction, to better "Prime" your extruder after retraction.')
138 setting('bottom_thickness',          0.3, float, 'advanced', 'Quality').setRange(0).setLabel('Initial layer thickness (mm)', 'Layer thickness of the bottom layer. A thicker bottom layer makes sticking to the bed easier. Set to 0.0 to have the bottom layer thickness the same as the other layers.')
139 setting('object_sink',               0.0, float, 'advanced', 'Quality').setLabel('Cut off object bottom (mm)', 'Sinks the object into the platform, this can be used for objects that do not have a flat bottom and thus create a too small first layer.')
140 #setting('enable_skin',             False, bool,  'advanced', 'Quality').setLabel('Duplicate outlines', 'Skin prints the outer lines of the prints twice, each time with half the thickness. This gives the illusion of a higher print quality.')
141 setting('travel_speed',            150.0, float, 'advanced', 'Speed').setRange(0.1).setLabel('Travel speed (mm/s)', 'Speed at which travel moves are done, a high quality build Ultimaker can reach speeds of 250mm/s. But some machines might miss steps then.')
142 setting('bottom_layer_speed',         20, float, 'advanced', 'Speed').setRange(0.1).setLabel('Bottom layer speed (mm/s)', 'Print speed for the bottom layer, you want to print the first layer slower so it sticks better to the printer bed.')
143 setting('infill_speed',              0.0, float, 'advanced', 'Speed').setRange(0.0).setLabel('Infill speed (mm/s)', 'Speed at which infill parts are printed. If set to 0 then the print speed is used for the infill. Printing the infill faster can greatly reduce printing, but this can negatively effect print quality..')
144 setting('cool_min_layer_time',         5, float, 'advanced', 'Cool').setRange(0).setLabel('Minimal layer time (sec)', 'Minimum time spend in a layer, gives the layer time to cool down before the next layer is put on top. If the layer will be placed down too fast the printer will slow down to make sure it has spend at least this amount of seconds printing this layer.')
145 setting('fan_enabled',              True, bool,  'advanced', 'Cool').setLabel('Enable cooling fan', 'Enable the cooling fan during the print. The extra cooling from the cooling fan is essential during faster prints.')
146
147 setting('skirt_line_count',            1, int,   'expert', 'Skirt').setRange(0).setLabel('Line count', 'The skirt is a line drawn around the object at the first layer. This helps to prime your extruder, and to see if the object fits on your platform.\nSetting this to 0 will disable the skirt. Multiple skirt lines can help priming your extruder better for small objects.')
148 setting('skirt_gap',                 3.0, float, 'expert', 'Skirt').setRange(0).setLabel('Start distance (mm)', 'The distance between the skirt and the first layer.\nThis is the minimal distance, multiple skirt lines will be put outwards from this distance.')
149 #setting('max_z_speed',               3.0, float, 'expert',   'Speed').setRange(0.1).setLabel('Max Z speed (mm/s)', 'Speed at which Z moves are done. When you Z axis is properly lubricated you can increase this for less Z blob.')
150 #setting('retract_on_jumps_only',    True, bool,  'expert',   'Retraction').setLabel('Retract on jumps only', 'Only retract when we are making a move that is over a hole in the model, else retract on every move. This effects print quality in different ways.')
151 setting('fan_layer',                   1, int,   'expert',   'Cool').setRange(0).setLabel('Fan on layer number', 'The layer at which the fan is turned on. The first layer is layer 0. The first layer can stick better if you turn on the fan on, on the 2nd layer.')
152 setting('fan_speed',                 100, int,   'expert',   'Cool').setRange(0,100).setLabel('Fan speed min (%)', 'When the fan is turned on, it is enabled at this speed setting. If cool slows down the layer, the fan is adjusted between the min and max speed. Minimal fan speed is used if the layer is not slowed down due to cooling.')
153 setting('fan_speed_max',             100, int,   'expert',   'Cool').setRange(0,100).setLabel('Fan speed max (%)', 'When the fan is turned on, it is enabled at this speed setting. If cool slows down the layer, the fan is adjusted between the min and max speed. Maximal fan speed is used if the layer is slowed down due to cooling by more than 200%.')
154 setting('cool_min_feedrate',          10, float, 'expert',   'Cool').setRange(0).setLabel('Minimum speed (mm/s)', 'The minimal layer time can cause the print to slow down so much it starts to ooze. The minimal feedrate protects against this. Even if a print gets slown down it will never be slower than this minimal speed.')
155 setting('cool_head_lift',          False, bool,  'expert',   'Cool').setLabel('Cool head lift', 'Lift the head if the minimal speed is hit because of cool slowdown, and wait the extra time so the minimal layer time is always hit.')
156 #setting('extra_base_wall_thickness', 0.0, float, 'expert',   'Accuracy').setRange(0).setLabel('Extra Wall thickness for bottom/top (mm)', 'Additional wall thickness of the bottom and top layers.')
157 #setting('sequence', 'Loops > Perimeter > Infill', ['Loops > Perimeter > Infill', 'Loops > Infill > Perimeter', 'Infill > Loops > Perimeter', 'Infill > Perimeter > Loops', 'Perimeter > Infill > Loops', 'Perimeter > Loops > Infill'], 'expert', 'Sequence')
158 #setting('force_first_layer_sequence', True, bool, 'expert', 'Sequence').setLabel('Force first layer sequence', 'This setting forces the order of the first layer to be \'Perimeter > Loops > Infill\'')
159 #setting('infill_type', 'Line', ['Line', 'Grid Circular', 'Grid Hexagonal', 'Grid Rectangular'], 'expert', 'Infill').setLabel('Infill pattern', 'Pattern of the none-solid infill. Line is default, but grids can provide a strong print.')
160 setting('solid_top', True, bool, 'expert', 'Infill').setLabel('Solid infill top', 'Create a solid top surface, if set to false the top is filled with the fill percentage. Useful for cups/vases.')
161 setting('solid_bottom', True, bool, 'expert', 'Infill').setLabel('Solid infill bottom', 'Create a solid bottom surface, if set to false the bottom is filled with the fill percentage. Useful for buildings.')
162 setting('fill_overlap', 15, int, 'expert', 'Infill').setRange(0,100).setLabel('Infill overlap (%)', 'Amount of overlap between the infill and the walls. There is a slight overlap with the walls and the infill so the walls connect firmly to the infill.')
163 setting('support_rate', 75, int, 'expert', 'Support').setRange(0,100).setLabel('Material amount (%)', 'Amount of material used for support, less material gives a weaker support structure which is easier to remove.')
164 #setting('support_distance',  0.5, float, 'expert', 'Support').setRange(0).setLabel('Distance from object (mm)', 'Distance between the support structure and the object. Empty gap in which no support structure is printed.')
165 #setting('joris', False, bool, 'expert', 'Joris').setLabel('Spiralize the outer contour', '[Joris] is a code name for smoothing out the Z move of the outer edge. This will create a steady Z increase over the whole print. It is intended to be used with a single walled wall thickness to make cups/vases.')
166 #setting('bridge_speed', 100, int, 'expert', 'Bridge').setRange(0,100).setLabel('Bridge speed (%)', 'Speed at which layers with bridges are printed, compared to normal printing speed.')
167 setting('brim_line_count', 20, int, 'expert', 'Brim').setRange(1,100).setLabel('Brim line amount', 'The amount of lines used for a brim, more lines means a larger brim which sticks better, but this also makes your effective print area smaller.')
168 setting('raft_margin', 5, float, 'expert', 'Raft').setRange(0).setLabel('Extra margin (mm)', 'If the raft is enabled, this is the extra raft area around the object which is also rafted. Increasing this margin will create a stronger raft while using more material and leaving less are for your print.')
169 setting('raft_line_spacing', 1.0, float, 'expert', 'Raft').setRange(0).setLabel('Line spacing (mm)', 'When you are using the raft this is the distance between the centerlines of the raft line.')
170 setting('raft_base_thickness', 0.3, float, 'expert', 'Raft').setRange(0).setLabel('Base thickness (mm)', 'When you are using the raft this is the thickness of the base layer which is put down.')
171 setting('raft_base_linewidth', 0.7, float, 'expert', 'Raft').setRange(0).setLabel('Base line width (mm)', 'When you are using the raft this is the width of the base layer lines which are put down.')
172 setting('raft_interface_thickness', 0.2, float, 'expert', 'Raft').setRange(0).setLabel('Interface thickness (mm)', 'When you are using the raft this is the thickness of the interface layer which is put down.')
173 setting('raft_interface_linewidth', 0.2, float, 'expert', 'Raft').setRange(0).setLabel('Interface line width (mm)', 'When you are using the raft this is the width of the interface layer lines which are put down.')
174 #setting('hop_on_move', False, bool, 'expert', 'Hop').setLabel('Enable hop on move', 'When moving from print position to print position, raise the printer head 0.2mm so it does not knock off the print (experimental).')
175
176 setting('plugin_config', '', str, 'hidden', 'hidden')
177 setting('object_center_x', -1, float, 'hidden', 'hidden')
178 setting('object_center_y', -1, float, 'hidden', 'hidden')
179
180 setting('start.gcode', """;Sliced at: {day} {date} {time}
181 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
182 ;Print time: {print_time}
183 ;Filament used: {filament_amount}m {filament_weight}g
184 ;Filament cost: {filament_cost}
185 G21        ;metric values
186 G90        ;absolute positioning
187 M107       ;start with the fan off
188
189 G28 X0 Y0  ;move X/Y to min endstops
190 G28 Z0     ;move Z to min endstops
191
192 G1 Z15.0 F{travel_speed} ;move the platform down 15mm
193
194 G92 E0                  ;zero the extruded length
195 G1 F200 E3              ;extrude 3mm of feed stock
196 G92 E0                  ;zero the extruded length again
197 G1 F{travel_speed}
198 M117 Printing...
199 """, str, 'alteration', 'alteration')
200 #######################################################################################
201 setting('end.gcode', """;End GCode
202 M104 S0                     ;extruder heater off
203 M140 S0                     ;heated bed heater off (if you have it)
204
205 G91                                    ;relative positioning
206 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
207 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
208 G28 X0 Y0                              ;move X/Y to min endstops, so the head is out of the way
209
210 M84                         ;steppers off
211 G90                         ;absolute positioning
212 """, str, 'alteration', 'alteration')
213 #######################################################################################
214 setting('support_start.gcode', '', str, 'alteration', 'alteration')
215 setting('support_end.gcode', '', str, 'alteration', 'alteration')
216 setting('cool_start.gcode', '', str, 'alteration', 'alteration')
217 setting('cool_end.gcode', '', str, 'alteration', 'alteration')
218 setting('replace.csv', '', str, 'alteration', 'alteration')
219 #######################################################################################
220 setting('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.
221 G92 E0
222
223 G91                                    ;relative positioning
224 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
225 G1 Z+0.5 E-5 F{travel_speed}           ;move Z up a bit and retract filament even more
226 G90                                    ;absolute positioning
227
228 G1 Z{clear_z} F{max_z_speed}
229 G92 E0
230 G1 X{object_center_x} Y{object_center_y} F{travel_speed}
231 G1 F200 E6
232 G92 E0
233 """, str, 'alteration', 'alteration')
234 #######################################################################################
235 setting('switchExtruder.gcode', """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
236 G92 E0
237 G1 E-36 F5000
238 G92 E0
239 T{extruder}
240 G1 X{new_x} Y{new_y} Z{new_z} F{travel_speed}
241 G1 E36 F5000
242 G92 E0
243 """, str, 'alteration', 'alteration')
244
245 setting('startMode', 'Simple', ['Simple', 'Normal'], 'preference', 'hidden')
246 setting('lastFile', os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'resources', 'example', 'UltimakerRobot_support.stl')), str, 'preference', 'hidden')
247 setting('machine_width', '205', float, 'preference', 'hidden').setLabel('Maximum width (mm)', 'Size of the machine in mm')
248 setting('machine_depth', '205', float, 'preference', 'hidden').setLabel('Maximum depth (mm)', 'Size of the machine in mm')
249 setting('machine_height', '200', float, 'preference', 'hidden').setLabel('Maximum height (mm)', 'Size of the machine in mm')
250 setting('machine_type', 'unknown', str, 'preference', 'hidden')
251 setting('machine_center_is_zero', 'False', bool, 'preference', 'hidden')
252 setting('ultimaker_extruder_upgrade', 'False', bool, 'preference', 'hidden')
253 setting('has_heated_bed', 'False', bool, 'preference', 'hidden').setLabel('Heated bed', 'If you have an heated bed, this enabled heated bed settings (requires restart)')
254 setting('reprap_name', 'RepRap', str, 'preference', 'hidden')
255 setting('extruder_amount', '1', ['1','2','3','4'], 'preference', 'hidden').setLabel('Extruder count', 'Amount of extruders in your machine.')
256 setting('extruder_offset_x1', '-21.6', float, 'preference', 'hidden').setLabel('Offset X', 'The offset of your secondary extruder compared to the primary.')
257 setting('extruder_offset_y1', '0.0', float, 'preference', 'hidden').setLabel('Offset Y', 'The offset of your secondary extruder compared to the primary.')
258 setting('extruder_offset_x2', '0.0', float, 'preference', 'hidden').setLabel('Offset X', 'The offset of your secondary extruder compared to the primary.')
259 setting('extruder_offset_y2', '0.0', float, 'preference', 'hidden').setLabel('Offset Y', 'The offset of your secondary extruder compared to the primary.')
260 setting('extruder_offset_x3', '0.0', float, 'preference', 'hidden').setLabel('Offset X', 'The offset of your secondary extruder compared to the primary.')
261 setting('extruder_offset_y3', '0.0', float, 'preference', 'hidden').setLabel('Offset Y', 'The offset of your secondary extruder compared to the primary.')
262 setting('filament_physical_density', '1300', float, 'preference', 'hidden').setRange(500.0, 3000.0).setLabel('Density (kg/m3)', 'Weight of the filament per m3. Around 1300 for PLA. And around 1040 for ABS. This value is used to estimate the weight if the filament used for the print.')
263 setting('steps_per_e', '0', float, 'preference', 'hidden').setLabel('E-Steps per 1mm filament', 'Amount of steps per mm filament extrusion')
264 setting('serial_port', 'AUTO', str, 'preference', 'hidden').setLabel('Serial port', 'Serial port to use for communication with the printer')
265 setting('serial_port_auto', '', str, 'preference', 'hidden')
266 setting('serial_baud', 'AUTO', str, 'preference', 'hidden').setLabel('Baudrate', 'Speed of the serial port communication\nNeeds to match your firmware settings\nCommon values are 250000, 115200, 57600')
267 setting('serial_baud_auto', '', int, 'preference', 'hidden')
268 setting('save_profile', 'False', bool, 'preference', 'hidden').setLabel('Save profile on slice', 'When slicing save the profile as [stl_file]_profile.ini next to the model.')
269 setting('filament_cost_kg', '0', float, 'preference', 'hidden').setLabel('Cost (price/kg)', 'Cost of your filament per kg, to estimate the cost of the final print.')
270 setting('filament_cost_meter', '0', float, 'preference', 'hidden').setLabel('Cost (price/m)', 'Cost of your filament per meter, to estimate the cost of the final print.')
271 setting('auto_detect_sd', 'True', bool, 'preference', 'hidden').setLabel('Auto detect SD card drive', 'Auto detect the SD card. You can disable this because on some systems external hard-drives or USB sticks are detected as SD card.')
272 setting('check_for_updates', 'True', bool, 'preference', 'hidden').setLabel('Check for updates', 'Check for newer versions of Cura on startup')
273 setting('submit_slice_information', 'False', bool, 'preference', 'hidden').setLabel('Send usage statistics', 'Submit anonymous usage information to improve next versions of Cura')
274
275 setting('extruder_head_size_min_x', '0.0', float, 'preference', 'hidden').setLabel('Head size towards X min (mm)', 'The head size when printing multiple objects, measured from the tip of the nozzle towards the outer part of the head. 75mm for an Ultimaker if the fan is on the left side.')
276 setting('extruder_head_size_min_y', '0.0', float, 'preference', 'hidden').setLabel('Head size towards Y min (mm)', 'The head size when printing multiple objects, measured from the tip of the nozzle towards the outer part of the head. 18mm for an Ultimaker if the fan is on the left side.')
277 setting('extruder_head_size_max_x', '0.0', float, 'preference', 'hidden').setLabel('Head size towards X max (mm)', 'The head size when printing multiple objects, measured from the tip of the nozzle towards the outer part of the head. 18mm for an Ultimaker if the fan is on the left side.')
278 setting('extruder_head_size_max_y', '0.0', float, 'preference', 'hidden').setLabel('Head size towards Y max (mm)', 'The head size when printing multiple objects, measured from the tip of the nozzle towards the outer part of the head. 35mm for an Ultimaker if the fan is on the left side.')
279 setting('extruder_head_size_height', '0.0', float, 'preference', 'hidden').setLabel('Printer gantry height (mm)', 'The height of the gantry holding up the printer head. If an object is higher then this then you cannot print multiple objects one for one. XXmm for an Ultimaker.')
280
281 setting('model_colour', '#7AB645', str, 'preference', 'hidden').setLabel('Model colour')
282 setting('model_colour2', '#CB3030', str, 'preference', 'hidden').setLabel('Model colour (2)')
283 setting('model_colour3', '#DDD93C', str, 'preference', 'hidden').setLabel('Model colour (3)')
284 setting('model_colour4', '#4550D3', str, 'preference', 'hidden').setLabel('Model colour (4)')
285
286 setting('window_maximized', 'True', bool, 'preference', 'hidden')
287 setting('window_pos_x', '-1', float, 'preference', 'hidden')
288 setting('window_pos_y', '-1', float, 'preference', 'hidden')
289 setting('window_width', '-1', float, 'preference', 'hidden')
290 setting('window_height', '-1', float, 'preference', 'hidden')
291 setting('window_normal_sash', '320', float, 'preference', 'hidden')
292
293 validators.warningAbove(settingsDictionary['layer_height'], lambda : (float(getProfileSetting('nozzle_size')) * 80.0 / 100.0), "Thicker layers then %.2fmm (80%% nozzle size) usually give bad results and are not recommended.")
294 validators.wallThicknessValidator(settingsDictionary['wall_thickness'])
295 validators.warningAbove(settingsDictionary['print_speed'], 150.0, "It is highly unlikely that your machine can achieve a printing speed above 150mm/s")
296 validators.printSpeedValidator(settingsDictionary['print_speed'])
297 validators.warningAbove(settingsDictionary['print_temperature'], 260.0, "Temperatures above 260C could damage your machine, be careful!")
298 validators.warningAbove(settingsDictionary['print_temperature2'], 260.0, "Temperatures above 260C could damage your machine, be careful!")
299 validators.warningAbove(settingsDictionary['print_temperature3'], 260.0, "Temperatures above 260C could damage your machine, be careful!")
300 validators.warningAbove(settingsDictionary['print_temperature4'], 260.0, "Temperatures above 260C could damage your machine, be careful!")
301 validators.warningAbove(settingsDictionary['filament_diameter'], 3.5, "Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm.")
302 validators.warningAbove(settingsDictionary['filament_diameter2'], 3.5, "Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm.")
303 validators.warningAbove(settingsDictionary['filament_diameter3'], 3.5, "Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm.")
304 validators.warningAbove(settingsDictionary['filament_diameter4'], 3.5, "Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm.")
305 validators.warningAbove(settingsDictionary['travel_speed'], 300.0, "It is highly unlikely that your machine can achieve a travel speed above 300mm/s")
306 validators.warningAbove(settingsDictionary['bottom_thickness'], lambda : (float(getProfileSetting('nozzle_size')) * 3.0 / 4.0), "A bottom layer of more then %.2fmm (3/4 nozzle size) usually give bad results and is not recommended.")
307
308 #Conditions for multiple extruders
309 settingsDictionary['print_temperature2'].addCondition(lambda : int(getPreference('extruder_amount')) > 1)
310 settingsDictionary['print_temperature3'].addCondition(lambda : int(getPreference('extruder_amount')) > 2)
311 settingsDictionary['print_temperature4'].addCondition(lambda : int(getPreference('extruder_amount')) > 3)
312 settingsDictionary['filament_diameter2'].addCondition(lambda : int(getPreference('extruder_amount')) > 1)
313 settingsDictionary['filament_diameter3'].addCondition(lambda : int(getPreference('extruder_amount')) > 2)
314 settingsDictionary['filament_diameter4'].addCondition(lambda : int(getPreference('extruder_amount')) > 3)
315 settingsDictionary['support_dual_extrusion'].addCondition(lambda : int(getPreference('extruder_amount')) > 1)
316 #Heated bed
317 settingsDictionary['print_bed_temperature'].addCondition(lambda : getPreference('has_heated_bed') == 'True')
318
319 #########################################################
320 ## Profile and preferences functions
321 #########################################################
322
323 def getSubCategoriesFor(category):
324         done = {}
325         ret = []
326         for s in settingsList:
327                 if s.getCategory() == category and not s.getSubCategory() in done:
328                         done[s.getSubCategory()] = True
329                         ret.append(s.getSubCategory())
330         return ret
331
332 def getSettingsForCategory(category, subCategory = None):
333         ret = []
334         for s in settingsList:
335                 if s.getCategory() == category and (subCategory is None or s.getSubCategory() == subCategory):
336                         ret.append(s)
337         return ret
338
339 ## Profile functions
340 def getBasePath():
341         if platform.system() == "Windows":
342                 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
343                 #If we have a frozen python install, we need to step out of the library.zip
344                 if hasattr(sys, 'frozen'):
345                         basePath = os.path.normpath(os.path.join(basePath, ".."))
346         else:
347                 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
348         if not os.path.isdir(basePath):
349                 os.makedirs(basePath)
350         return basePath
351
352 def getDefaultProfilePath():
353         return os.path.join(getBasePath(), 'current_profile.ini')
354
355 def loadProfile(filename):
356         #Read a configuration file as global config
357         profileParser = ConfigParser.ConfigParser()
358         try:
359                 profileParser.read(filename)
360         except ConfigParser.ParsingError:
361                 return
362         global settingsList
363         for set in settingsList:
364                 if set.isPreference():
365                         continue
366                 section = 'profile'
367                 if set.isAlteration():
368                         section = 'alterations'
369                 if profileParser.has_option(section, set.getName()):
370                         set.setValue(unicode(profileParser.get(section, set.getName()), 'utf-8', 'replace'))
371
372 def saveProfile(filename):
373         #Save the current profile to an ini file
374         profileParser = ConfigParser.ConfigParser()
375         profileParser.add_section('profile')
376         profileParser.add_section('alterations')
377         global settingsList
378         for set in settingsList:
379                 if set.isPreference():
380                         continue
381                 if set.isAlteration():
382                         profileParser.set('alterations', set.getName(), set.getValue().encode('utf-8'))
383                 else:
384                         profileParser.set('profile', set.getName(), set.getValue().encode('utf-8'))
385
386         profileParser.write(open(filename, 'w'))
387
388 def resetProfile():
389         #Read a configuration file as global config
390         global settingsList
391         for set in settingsList:
392                 if set.isPreference():
393                         continue
394                 set.setValue(set.getDefault())
395
396         if getPreference('machine_type') == 'ultimaker':
397                 putProfileSetting('nozzle_size', '0.4')
398                 if getPreference('ultimaker_extruder_upgrade') == 'True':
399                         putProfileSetting('retraction_enable', 'True')
400         else:
401                 putProfileSetting('nozzle_size', '0.5')
402
403 def loadProfileFromString(options):
404         options = base64.b64decode(options)
405         options = zlib.decompress(options)
406         (profileOpts, alt) = options.split('\f', 1)
407         global settingsDictionary
408         for option in profileOpts.split('\b'):
409                 if len(option) > 0:
410                         (key, value) = option.split('=', 1)
411                         if key in settingsDictionary:
412                                 if settingsDictionary[key].isProfile():
413                                         settingsDictionary[key].setValue(value)
414         for option in alt.split('\b'):
415                 if len(option) > 0:
416                         (key, value) = option.split('=', 1)
417                         if key in settingsDictionary:
418                                 if settingsDictionary[key].isAlteration():
419                                         settingsDictionary[key].setValue(value)
420
421 def getProfileString():
422         p = []
423         alt = []
424         global settingsList
425         for set in settingsList:
426                 if set.isProfile():
427                         if set.getName() in tempOverride:
428                                 p.append(set.getName() + "=" + tempOverride[set.getName()])
429                         else:
430                                 p.append(set.getName() + "=" + set.getValue())
431                 if set.isAlteration():
432                         if set.getName() in tempOverride:
433                                 alt.append(set.getName() + "=" + tempOverride[set.getName()])
434                         else:
435                                 alt.append(set.getName() + "=" + set.getValue())
436         ret = '\b'.join(p) + '\f' + '\b'.join(alt)
437         ret = base64.b64encode(zlib.compress(ret, 9))
438         return ret
439
440 def getGlobalPreferencesString():
441         p = []
442         global settingsList
443         for set in settingsList:
444                 if set.isPreference():
445                         p.append(set.getName() + "=" + set.getValue())
446         ret = '\b'.join(p)
447         ret = base64.b64encode(zlib.compress(ret, 9))
448         return ret
449
450
451 def getProfileSetting(name):
452         if name in tempOverride:
453                 return tempOverride[name]
454         global settingsDictionary
455         if name in settingsDictionary and settingsDictionary[name].isProfile():
456                 return settingsDictionary[name].getValue()
457         print 'Error: "%s" not found in profile settings' % (name)
458         return ''
459
460 def getProfileSettingFloat(name):
461         try:
462                 setting = getProfileSetting(name).replace(',', '.')
463                 return float(eval(setting, {}, {}))
464         except:
465                 return 0.0
466
467 def putProfileSetting(name, value):
468         #Check if we have a configuration file loaded, else load the default.
469         global settingsDictionary
470         if name in settingsDictionary and settingsDictionary[name].isProfile():
471                 settingsDictionary[name].setValue(value)
472
473 def isProfileSetting(name):
474         global settingsDictionary
475         if name in settingsDictionary and settingsDictionary[name].isProfile():
476                 return True
477         return False
478
479 ## Preferences functions
480 def getPreferencePath():
481         return os.path.join(getBasePath(), 'preferences.ini')
482
483 def getPreferenceFloat(name):
484         try:
485                 setting = getPreference(name).replace(',', '.')
486                 return float(eval(setting, {}, {}))
487         except:
488                 return 0.0
489
490 def getPreferenceColour(name):
491         colorString = getPreference(name)
492         return [float(int(colorString[1:3], 16)) / 255, float(int(colorString[3:5], 16)) / 255, float(int(colorString[5:7], 16)) / 255, 1.0]
493
494 def loadPreferences(filename):
495         #Read a configuration file as global config
496         profileParser = ConfigParser.ConfigParser()
497         try:
498                 profileParser.read(filename)
499         except ConfigParser.ParsingError:
500                 return
501         global settingsList
502         for set in settingsList:
503                 if set.isPreference():
504                         if profileParser.has_option('preference', set.getName()):
505                                 set.setValue(unicode(profileParser.get('preference', set.getName()), 'utf-8', 'replace'))
506
507 def savePreferences(filename):
508         #Save the current profile to an ini file
509         parser = ConfigParser.ConfigParser()
510         parser.add_section('preference')
511         global settingsList
512         for set in settingsList:
513                 if set.isPreference():
514                         parser.set('preference', set.getName(), set.getValue().encode('utf-8'))
515         parser.write(open(filename, 'w'))
516
517 def getPreference(name):
518         if name in tempOverride:
519                 return tempOverride[name]
520         global settingsDictionary
521         if name in settingsDictionary and settingsDictionary[name].isPreference():
522                 return settingsDictionary[name].getValue()
523         print 'Error: "%s" not found in profile settings' % (name)
524         return ''
525
526 def putPreference(name, value):
527         #Check if we have a configuration file loaded, else load the default.
528         global settingsDictionary
529         if name in settingsDictionary and settingsDictionary[name].isPreference():
530                 settingsDictionary[name].setValue(value)
531         savePreferences(getPreferencePath())
532
533 def isPreference(name):
534         global settingsDictionary
535         if name in settingsDictionary and settingsDictionary[name].isPreference():
536                 return True
537         return False
538
539 ## Temp overrides for multi-extruder slicing and the project planner.
540 tempOverride = {}
541 def setTempOverride(name, value):
542         tempOverride[name] = unicode(value).encode("utf-8")
543 def clearTempOverride(name):
544         del tempOverride[name]
545 def resetTempOverride():
546         tempOverride.clear()
547
548 #########################################################
549 ## Utility functions to calculate common profile values
550 #########################################################
551 def calculateEdgeWidth():
552         wallThickness = getProfileSettingFloat('wall_thickness')
553         nozzleSize = getProfileSettingFloat('nozzle_size')
554
555         if wallThickness < 0.01:
556                 return nozzleSize
557         if wallThickness < nozzleSize:
558                 return wallThickness
559
560         lineCount = int(wallThickness / nozzleSize + 0.0001)
561         lineWidth = wallThickness / lineCount
562         lineWidthAlt = wallThickness / (lineCount + 1)
563         if lineWidth > nozzleSize * 1.5:
564                 return lineWidthAlt
565         return lineWidth
566
567 def calculateLineCount():
568         wallThickness = getProfileSettingFloat('wall_thickness')
569         nozzleSize = getProfileSettingFloat('nozzle_size')
570         
571         if wallThickness < nozzleSize:
572                 return 1
573
574         lineCount = int(wallThickness / nozzleSize + 0.0001)
575         lineWidth = wallThickness / lineCount
576         lineWidthAlt = wallThickness / (lineCount + 1)
577         if lineWidth > nozzleSize * 1.5:
578                 return lineCount + 1
579         return lineCount
580
581 def calculateSolidLayerCount():
582         layerHeight = getProfileSettingFloat('layer_height')
583         solidThickness = getProfileSettingFloat('solid_layer_thickness')
584         if layerHeight == 0.0:
585                 return 1
586         return int(math.ceil(solidThickness / layerHeight - 0.0001))
587
588 def calculateObjectSizeOffsets():
589         size = 0.0
590
591         if getProfileSetting('platform_adhesion') == 'Brim':
592                 size += getProfileSettingFloat('brim_line_count') * calculateEdgeWidth()
593         elif getProfileSetting('platform_adhesion') == 'Raft':
594                 pass
595         else:
596                 if getProfileSettingFloat('skirt_line_count') > 0:
597                         size += getProfileSettingFloat('skirt_line_count') * calculateEdgeWidth() + getProfileSettingFloat('skirt_gap')
598
599         #if getProfileSetting('enable_raft') != 'False':
600         #       size += profile.getProfileSettingFloat('raft_margin') * 2
601         #if getProfileSetting('support') != 'None':
602         #       extraSizeMin = extraSizeMin + numpy.array([3.0, 0, 0])
603         #       extraSizeMax = extraSizeMax + numpy.array([3.0, 0, 0])
604         return [size, size]
605
606 def getMachineCenterCoords():
607         if getPreference('machine_center_is_zero') == 'True':
608                 return [0, 0]
609         return [getPreferenceFloat('machine_width') / 2, getPreferenceFloat('machine_depth') / 2]
610
611 #########################################################
612 ## Alteration file functions
613 #########################################################
614 def replaceTagMatch(m):
615         pre = m.group(1)
616         tag = m.group(2)
617         if tag == 'time':
618                 return pre + time.strftime('%H:%M:%S').encode('utf-8', 'replace')
619         if tag == 'date':
620                 return pre + time.strftime('%d %b %Y').encode('utf-8', 'replace')
621         if tag == 'day':
622                 return pre + time.strftime('%a').encode('utf-8', 'replace')
623         if tag == 'print_time':
624                 return pre + '#P_TIME#'
625         if tag == 'filament_amount':
626                 return pre + '#F_AMNT#'
627         if tag == 'filament_weight':
628                 return pre + '#F_WGHT#'
629         if tag == 'filament_cost':
630                 return pre + '#F_COST#'
631         if pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
632                 f = getProfileSettingFloat(tag) * 60
633         elif isProfileSetting(tag):
634                 f = getProfileSettingFloat(tag)
635         elif isPreference(tag):
636                 f = getProfileSettingFloat(tag)
637         else:
638                 return '%s?%s?' % (pre, tag)
639         if (f % 1) == 0:
640                 return pre + str(int(f))
641         return pre + str(f)
642
643 def replaceGCodeTags(filename, gcodeInt):
644         f = open(filename, 'r+')
645         data = f.read(2048)
646         data = data.replace('#P_TIME#', ('%5d:%02d' % (int(gcodeInt.totalMoveTimeMinute / 60), int(gcodeInt.totalMoveTimeMinute % 60)))[-8:])
647         data = data.replace('#F_AMNT#', ('%8.2f' % (gcodeInt.extrusionAmount / 1000))[-8:])
648         data = data.replace('#F_WGHT#', ('%8.2f' % (gcodeInt.calculateWeight() * 1000))[-8:])
649         cost = gcodeInt.calculateCost()
650         if cost is None:
651                 cost = 'Unknown'
652         data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
653         f.seek(0)
654         f.write(data)
655         f.close()
656
657 ### Get aleration raw contents. (Used internally in Cura)
658 def getAlterationFile(filename):
659         if filename in tempOverride:
660                 return tempOverride[filename]
661         global settingsDictionary
662         if filename in settingsDictionary and settingsDictionary[filename].isAlteration():
663                 return settingsDictionary[filename].getValue()
664         print 'Error: "%s" not found in profile settings' % (filename)
665         return ''
666
667 def setAlterationFile(name, value):
668         #Check if we have a configuration file loaded, else load the default.
669         global settingsDictionary
670         if name in settingsDictionary and settingsDictionary[name].isAlteration():
671                 settingsDictionary[name].setValue(value)
672         saveProfile(getDefaultProfilePath())
673
674 ### Get the alteration file for output. (Used by Skeinforge)
675 def getAlterationFileContents(filename, extruderCount = 1):
676         prefix = ''
677         postfix = ''
678         alterationContents = getAlterationFile(filename)
679         if filename == 'start.gcode':
680                 #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.
681                 #We also set our steps per E here, if configured.
682                 eSteps = getPreferenceFloat('steps_per_e')
683                 if eSteps > 0:
684                         prefix += 'M92 E%f\n' % (eSteps)
685                 temp = getProfileSettingFloat('print_temperature')
686                 bedTemp = 0
687                 if getPreference('has_heated_bed') == 'True':
688                         bedTemp = getProfileSettingFloat('print_bed_temperature')
689                 
690                 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
691                         prefix += 'M140 S%f\n' % (bedTemp)
692                 if temp > 0 and not '{print_temperature}' in alterationContents:
693                         if extruderCount > 0:
694                                 for n in xrange(1, extruderCount):
695                                         t = temp
696                                         if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
697                                                 t = getProfileSettingFloat('print_temperature%d' % (n+1))
698                                         prefix += 'M104 T%d S%f\n' % (n, t)
699                                 for n in xrange(0, extruderCount):
700                                         t = temp
701                                         if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
702                                                 t = getProfileSettingFloat('print_temperature%d' % (n+1))
703                                         prefix += 'M109 T%d S%f\n' % (n, t)
704                                 prefix += 'T0\n'
705                         else:
706                                 prefix += 'M109 S%f\n' % (temp)
707                 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
708                         prefix += 'M190 S%f\n' % (bedTemp)
709         elif filename == 'end.gcode':
710                 #Append the profile string to the end of the GCode, so we can load it from the GCode file later.
711                 postfix = ';CURA_PROFILE_STRING:%s\n' % (getProfileString())
712         elif filename == 'replace.csv':
713                 #Always remove the extruder on/off M codes. These are no longer needed in 5D printing.
714                 prefix = 'M101\nM103\n'
715         elif filename == 'support_start.gcode' or filename == 'support_end.gcode':
716                 #Add support start/end code 
717                 if getProfileSetting('support_dual_extrusion') == 'True' and int(getPreference('extruder_amount')) > 1:
718                         if filename == 'support_start.gcode':
719                                 setTempOverride('extruder', '1')
720                         else:
721                                 setTempOverride('extruder', '0')
722                         alterationContents = getAlterationFileContents('switchExtruder.gcode')
723                         clearTempOverride('extruder')
724                 else:
725                         alterationContents = ''
726         return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n'
727
728 ###### PLUGIN #####
729
730 def getPluginConfig():
731         try:
732                 return pickle.loads(getProfileSetting('plugin_config'))
733         except:
734                 return []
735
736 def setPluginConfig(config):
737         putProfileSetting('plugin_config', pickle.dumps(config))
738
739 def getPluginBasePaths():
740         ret = []
741         if platform.system() != "Windows":
742                 ret.append(os.path.expanduser('~/.cura/plugins/'))
743         if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
744                 ret.append(os.path.normpath(os.path.join(resources.resourceBasePath, "Cura/plugins")))
745         else:
746                 ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
747         return ret
748
749 def getPluginList():
750         ret = []
751         for basePath in getPluginBasePaths():
752                 for filename in glob.glob(os.path.join(basePath, '*.py')):
753                         filename = os.path.basename(filename)
754                         if filename.startswith('_'):
755                                 continue
756                         with open(os.path.join(basePath, filename), "r") as f:
757                                 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
758                                 for line in f:
759                                         line = line.strip()
760                                         if not line.startswith('#'):
761                                                 break
762                                         line = line[1:].split(':', 1)
763                                         if len(line) != 2:
764                                                 continue
765                                         if line[0].upper() == 'NAME':
766                                                 item['name'] = line[1].strip()
767                                         elif line[0].upper() == 'INFO':
768                                                 item['info'] = line[1].strip()
769                                         elif line[0].upper() == 'TYPE':
770                                                 item['type'] = line[1].strip()
771                                         elif line[0].upper() == 'DEPEND':
772                                                 pass
773                                         elif line[0].upper() == 'PARAM':
774                                                 m = re.match('([a-zA-Z][a-zA-Z0-9_]*)\(([a-zA-Z_]*)(?::([^\)]*))?\) +(.*)', line[1].strip())
775                                                 if m is not None:
776                                                         item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
777                                         else:
778                                                 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
779                                 if item['name'] is not None and item['type'] == 'postprocess':
780                                         ret.append(item)
781         return ret
782
783 def runPostProcessingPlugins(gcodefilename):
784         pluginConfigList = getPluginConfig()
785         pluginList = getPluginList()
786         
787         for pluginConfig in pluginConfigList:
788                 plugin = None
789                 for pluginTest in pluginList:
790                         if pluginTest['filename'] == pluginConfig['filename']:
791                                 plugin = pluginTest
792                 if plugin is None:
793                         continue
794                 
795                 pythonFile = None
796                 for basePath in getPluginBasePaths():
797                         testFilename = os.path.join(basePath, pluginConfig['filename'])
798                         if os.path.isfile(testFilename):
799                                 pythonFile = testFilename
800                 if pythonFile is None:
801                         continue
802                 
803                 locals = {'filename': gcodefilename}
804                 for param in plugin['params']:
805                         value = param['default']
806                         if param['name'] in pluginConfig['params']:
807                                 value = pluginConfig['params'][param['name']]
808                         
809                         if param['type'] == 'float':
810                                 try:
811                                         value = float(value)
812                                 except:
813                                         value = float(param['default'])
814                         
815                         locals[param['name']] = value
816                 try:
817                         execfile(pythonFile, locals)
818                 except:
819                         locationInfo = traceback.extract_tb(sys.exc_info()[2])[-1]
820                         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])
821         return None