chiark / gitweb /
Add more documentation, fix commandline slicing.
[cura.git] / Cura / util / profile.py
1 """
2 The profile module contains all the settings for Cura.
3 These settings can be globally accessed and modified.
4 """
5 from __future__ import division
6 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
7
8 import os, traceback, math, re, zlib, base64, time, sys, platform, glob, string, stat, types
9 import cPickle as pickle
10 import numpy
11 if sys.version_info[0] < 3:
12         import ConfigParser
13 else:
14         import configparser as ConfigParser
15
16 from Cura.util import resources
17 from Cura.util import version
18 from Cura.util import validators
19
20 #The settings dictionary contains a key/value reference to all possible settings. With the setting name as key.
21 settingsDictionary = {}
22 #The settings list is used to keep a full list of all the settings. This is needed to keep the settings in the proper order,
23 # as the dictionary will not contain insertion order.
24 settingsList = []
25
26 #Currently selected machine (by index) Cura support multiple machines in the same preferences and can switch between them.
27 # Each machine has it's own index and unique name.
28 _selectedMachineIndex = 0
29
30 class setting(object):
31         #A setting object contains a configuration setting. These are globally accessible trough the quick access functions
32         # and trough the settingsDictionary function.
33         # Settings can be:
34         # * profile settings (settings that effect the slicing process and the print result)
35         # * preferences (settings that effect how cura works and acts)
36         # * machine settings (settings that relate to the physical configuration of your machine)
37         # * alterations (bad name copied from Skeinforge. These are the start/end code pieces)
38         # Settings have validators that check if the value is valid, but do not prevent invalid values!
39         # Settings have conditions that enable/disable this setting depending on other settings. (Ex: Dual-extrusion)
40         def __init__(self, name, default, type, category, subcategory):
41                 self._name = name
42                 self._label = name
43                 self._tooltip = ''
44                 self._default = unicode(default)
45                 self._values = []
46                 self._type = type
47                 self._category = category
48                 self._subcategory = subcategory
49                 self._validators = []
50                 self._conditions = []
51
52                 if type is types.FloatType:
53                         validators.validFloat(self)
54                 elif type is types.IntType:
55                         validators.validInt(self)
56
57                 global settingsDictionary
58                 settingsDictionary[name] = self
59                 global settingsList
60                 settingsList.append(self)
61
62         def setLabel(self, label, tooltip = ''):
63                 self._label = label
64                 self._tooltip = tooltip
65                 return self
66
67         def setRange(self, minValue=None, maxValue=None):
68                 if len(self._validators) < 1:
69                         return
70                 self._validators[0].minValue = minValue
71                 self._validators[0].maxValue = maxValue
72                 return self
73
74         def getLabel(self):
75                 return _(self._label)
76
77         def getTooltip(self):
78                 return _(self._tooltip)
79
80         def getCategory(self):
81                 return self._category
82
83         def getSubCategory(self):
84                 return self._subcategory
85
86         def isPreference(self):
87                 return self._category == 'preference'
88
89         def isMachineSetting(self):
90                 return self._category == 'machine'
91
92         def isAlteration(self):
93                 return self._category == 'alteration'
94
95         def isProfile(self):
96                 return not self.isAlteration() and not self.isPreference() and not self.isMachineSetting()
97
98         def getName(self):
99                 return self._name
100
101         def getType(self):
102                 return self._type
103
104         def getValue(self, index = None):
105                 if index is None:
106                         index = self.getValueIndex()
107                 if index >= len(self._values):
108                         return self._default
109                 return self._values[index]
110
111         def getDefault(self):
112                 return self._default
113
114         def setValue(self, value, index = None):
115                 if index is None:
116                         index = self.getValueIndex()
117                 while index >= len(self._values):
118                         self._values.append(self._default)
119                 self._values[index] = unicode(value)
120
121         def getValueIndex(self):
122                 if self.isMachineSetting():
123                         global _selectedMachineIndex
124                         return _selectedMachineIndex
125                 return 0
126
127         def validate(self):
128                 result = validators.SUCCESS
129                 msgs = []
130                 for validator in self._validators:
131                         res, err = validator.validate()
132                         if res == validators.ERROR:
133                                 result = res
134                         elif res == validators.WARNING and result != validators.ERROR:
135                                 result = res
136                         if res != validators.SUCCESS:
137                                 msgs.append(err)
138                 return result, '\n'.join(msgs)
139
140         def addCondition(self, conditionFunction):
141                 self._conditions.append(conditionFunction)
142
143         def checkConditions(self):
144                 for condition in self._conditions:
145                         if not condition():
146                                 return False
147                 return True
148
149 #########################################################
150 ## Settings
151 #########################################################
152
153 #Define a fake _() function to fake the gettext tools in to generating strings for the profile settings.
154 def _(n):
155         return n
156
157 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 determine 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."))
158 setting('wall_thickness',            0.8, float, 'basic',    _('Quality')).setRange(0.0).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."))
159 setting('retraction_enable',        True, 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."))
160 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 multiple of the layer thickness makes sense. And keep it near your wall thickness to make an evenly strong part."))
161 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 affect the outside of the print and only adjusts how strong the part becomes."))
162 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."))
163 setting('print_speed',                50, float, 'basic',    _('Speed and 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."))
164 setting('print_temperature',         220, int,   'basic',    _('Speed and 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."))
165 setting('print_temperature2',          0, int,   'basic',    _('Speed and 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."))
166 setting('print_temperature3',          0, int,   'basic',    _('Speed and 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."))
167 setting('print_temperature4',          0, int,   'basic',    _('Speed and 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."))
168 setting('print_bed_temperature',      70, int,   'basic',    _('Speed and Temperature')).setRange(0,340).setLabel(_("Bed temperature (C)"), _("Temperature used for the heated printer bed. Set at 0 to pre-heat yourself."))
169 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."))
170 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)"))
171 setting('support_dual_extrusion',  'Both', [_('Both'), _('First extruder'), _('Second extruder')], 'basic', _('Support')).setLabel(_("Support dual extrusion"), _("Which extruder to use for support material, for break-away support you can use both extruders.\nBut if one of the materials is more expensive then the other you could select an extruder to use for support material. This causes more extruder switches.\nYou can also use the 2nd extruder for soluble support materials."))
172 setting('wipe_tower',              False, bool,  'basic',    _('Dual extrusion')).setLabel(_("Wipe&prime tower"), _("The wipe-tower is a tower printed on every layer when switching between nozzles.\nThe old nozzle is wiped off on the tower before the new nozzle is used to print the 2nd color."))
173 setting('wipe_tower_volume',          15, float, 'expert',   _('Dual extrusion')).setLabel(_("Wipe&prime tower volume per layer (mm3)"), _("The amount of material put in the wipe/prime tower.\nThis is done in volume because in general you want to extrude a\ncertain amount of volume to get the extruder going, independent on the layer height.\nThis means that with thinner layers, your tower gets bigger."))
174 setting('ooze_shield',             False, bool,  'basic',    _('Dual extrusion')).setLabel(_("Ooze shield"), _("The ooze shield is a 1 line thick shell around the object which stands a few mm from the object.\nThis shield catches any oozing from the unused nozzle in dual-extrusion."))
175 setting('filament_diameter',        2.85, 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."))
176 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."))
177 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."))
178 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."))
179 setting('filament_flow',            100., float, 'basic',    _('Filament')).setRange(5,300).setLabel(_("Flow (%)"), _("Flow compensation, the amount of material extruded is multiplied by this value"))
180 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."))
181 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 4.5mm seems to generate good results."))
182 setting('retraction_dual_amount',   16.5, float, 'advanced', _('Retraction')).setRange(0).setLabel(_("Dual extrusion switch amount (mm)"), _("Amount of retraction when switching nozzle with dual-extrusion, set at 0 for no retraction at all. A value of 16.0mm seems to generate good results."))
183 setting('retraction_min_travel',     1.5, float, 'expert',   _('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."))
184 setting('retraction_combing',       True, bool,  'expert',   _('Retraction')).setLabel(_("Enable combing"), _("Combing is the act of avoiding holes in the print for the head to travel over. If combing is disabled the printer head moves straight from the start point to the end point and it will always retract."))
185 setting('retraction_minimal_extrusion',0.02, float,'expert',  _('Retraction')).setRange(0).setLabel(_("Minimal extrusion before retracting (mm)"), _("The minimal amount of extrusion that needs to be done before retracting again if a retraction needs to happen before this minimal is reached the retraction is ignored.\nThis avoids retraction a lot on the same piece of filament which flattens the filament and causes grinding issues."))
186 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."))
187 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."))
188 #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."))
189 setting('overlap_dual',             0.15, float, 'advanced', _('Quality')).setLabel(_("Dual extrusion overlap (mm)"), _("Add a certain amount of overlapping extrusion on dual-extrusion prints. This bonds the different colors better together."))
190 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."))
191 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."))
192 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."))
193 setting('inset0_speed',              0.0, float, 'advanced', _('Speed')).setRange(0.0).setLabel(_("Outser shell speed (mm/s)"), _("Speed at which outer shell is printed. If set to 0 then the print speed is used. Printing the outer shell at a lower speed improves the final skin quality. However, having a large difference between the inner shell speed and the outer shell speed will effect quality in a negative way."))
194 setting('insetx_speed',              0.0, float, 'advanced', _('Speed')).setRange(0.0).setLabel(_("Innser shell speed (mm/s)"), _("Speed at which inner shells are printed. If set to 0 then the print speed is used. Printing the inner shell faster then the outer shell will reduce printing time. It is good to set this somewhere in between the outer shell speed and the infill/printing speed."))
195 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."))
196 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."))
197
198 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."))
199 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."))
200 setting('skirt_minimal_length',    150.0, float, 'expert', 'Skirt').setRange(0).setLabel(_("Minimal length (mm)"), _("The minimal length of the skirt, if this minimal length is not reached it will add more skirt lines to reach this minimal lenght.\nNote: If the line count is set to 0 this is ignored."))
201 #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."))
202 #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.'))
203 setting('fan_full_height',           0.5, float, 'expert',   _('Cool')).setRange(0).setLabel(_("Fan full on at height (mm)"), _("The height at which the fan is turned on completely. For the layers below this the fan speed is scaled linear with the fan off at layer 0."))
204 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."))
205 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%."))
206 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."))
207 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."))
208 #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."))
209 #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')
210 #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\''))
211 #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."))
212 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."))
213 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."))
214 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."))
215 setting('support_angle', 60, float, 'expert', _('Support')).setRange(0,90).setLabel(_("Overhang angle for support (deg)"), _("The minimal angle that overhangs need to have to get support. With 0deg being horizontal and 90deg being vertical."))
216 setting('support_fill_rate', 15, int, 'expert', _('Support')).setRange(0,100).setLabel(_("Fill amount (%)"), _("Amount of infill structure in the support material, less material gives weaker support which is easier to remove. 15% seems to be a good average."))
217 setting('support_xy_distance', 0.7, float, 'expert', _('Support')).setRange(0,10).setLabel(_("Distance X/Y (mm)"), _("Distance of the support material from the print, in the X/Y directions.\n0.7mm gives a nice distance from the print so the support does not stick to the print."))
218 setting('support_z_distance', 0.15, float, 'expert', _('Support')).setRange(0,10).setLabel(_("Distance Z (mm)"), _("Distance from the top/bottom of the support to the print. A small gap here makes it easier to remove the support but makes the print a bit uglier.\n0.15mm gives a good seperation of the support material."))
219 setting('spiralize', False, bool, 'expert', 'Spiralize').setLabel(_("Spiralize the outer contour"), _("Spiralize is smoothing out the Z move of the outer edge. This will create a steady Z increase over the whole print. This feature turns a solid object into a single walled print with a solid bottom."))
220 #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."))
221 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."))
222 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."))
223 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."))
224 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."))
225 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."))
226 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."))
227 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."))
228 #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)."))
229 setting('fix_horrible_union_all_type_a', True,  bool, 'expert', _('Fix horrible')).setLabel(_("Combine everything (Type-A)"), _("This expert option adds all parts of the model together. The result is usually that internal cavities disappear. Depending on the model this can be intended or not. Enabling this option is at your own risk. Type-A is depended on the model normals and tries to keep some internal holes intact. Type-B ignores all internal holes and only keeps the outside shape per layer."))
230 setting('fix_horrible_union_all_type_b', False, bool, 'expert', _('Fix horrible')).setLabel(_("Combine everything (Type-B)"), _("This expert option adds all parts of the model together. The result is usually that internal cavities disappear. Depending on the model this can be intended or not. Enabling this option is at your own risk. Type-A is depended on the model normals and tries to keep some internal holes intact. Type-B ignores all internal holes and only keeps the outside shape per layer."))
231 setting('fix_horrible_use_open_bits', False, bool, 'expert', _('Fix horrible')).setLabel(_("Keep open faces"), _("This expert option keeps all the open bits of the model intact. Normally Cura tries to stitch up small holes and remove everything with big holes, but this option keeps bits that are not properly part of anything and just goes with whatever it is left. This option is usually not what you want, but it might enable you to slice models otherwise failing to produce proper paths.\nAs with all \"Fix horrible\" options, results may vary and use at your own risk."))
232 setting('fix_horrible_extensive_stitching', False, bool, 'expert', _('Fix horrible')).setLabel(_("Extensive stitching"), _("Extrensive stitching tries to fix up open holes in the model by closing the hole with touching polygons. This algorthm is quite expensive and could introduce a lot of processing time.\nAs with all \"Fix horrible\" options, results may vary and use at your own risk."))
233
234 setting('plugin_config', '', str, 'hidden', 'hidden')
235 setting('object_center_x', -1, float, 'hidden', 'hidden')
236 setting('object_center_y', -1, float, 'hidden', 'hidden')
237
238 setting('start.gcode', """;Sliced at: {day} {date} {time}
239 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
240 ;Print time: {print_time}
241 ;Filament used: {filament_amount}m {filament_weight}g
242 ;Filament cost: {filament_cost}
243 ;M190 S{print_bed_temperature} ;Uncomment to add your own bed temperature line
244 ;M109 S{print_temperature} ;Uncomment to add your own temperature line
245 G21        ;metric values
246 G90        ;absolute positioning
247 M107       ;start with the fan off
248
249 G28 X0 Y0  ;move X/Y to min endstops
250 G28 Z0     ;move Z to min endstops
251
252 G1 Z15.0 F{travel_speed} ;move the platform down 15mm
253
254 G92 E0                  ;zero the extruded length
255 G1 F200 E3              ;extrude 3mm of feed stock
256 G92 E0                  ;zero the extruded length again
257 G1 F{travel_speed}
258 ;Put printing message on LCD screen
259 M117 Printing...
260 """, str, 'alteration', 'alteration')
261 #######################################################################################
262 setting('end.gcode', """;End GCode
263 M104 S0                     ;extruder heater off
264 M140 S0                     ;heated bed heater off (if you have it)
265
266 G91                                    ;relative positioning
267 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
268 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
269 G28 X0 Y0                              ;move X/Y to min endstops, so the head is out of the way
270
271 M84                         ;steppers off
272 G90                         ;absolute positioning
273 """, str, 'alteration', 'alteration')
274 #######################################################################################
275 setting('start2.gcode', """;Sliced at: {day} {date} {time}
276 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
277 ;Print time: {print_time}
278 ;Filament used: {filament_amount}m {filament_weight}g
279 ;Filament cost: {filament_cost}
280 ;M190 S{print_bed_temperature} ;Uncomment to add your own bed temperature line
281 ;M104 S{print_temperature} ;Uncomment to add your own temperature line
282 ;M109 T1 S{print_temperature2} ;Uncomment to add your own temperature line
283 ;M109 T0 S{print_temperature} ;Uncomment to add your own temperature line
284 G21        ;metric values
285 G90        ;absolute positioning
286 M107       ;start with the fan off
287
288 G28 X0 Y0  ;move X/Y to min endstops
289 G28 Z0     ;move Z to min endstops
290
291 G1 Z15.0 F{travel_speed} ;move the platform down 15mm
292
293 T1                      ;Switch to the 2nd extruder
294 G92 E0                  ;zero the extruded length
295 G1 F200 E10             ;extrude 10mm of feed stock
296 G92 E0                  ;zero the extruded length again
297 G1 F200 E-{retraction_dual_amount}
298
299 T0                      ;Switch to the first extruder
300 G92 E0                  ;zero the extruded length
301 G1 F200 E10             ;extrude 10mm of feed stock
302 G92 E0                  ;zero the extruded length again
303 G1 F{travel_speed}
304 ;Put printing message on LCD screen
305 M117 Printing...
306 """, str, 'alteration', 'alteration')
307 #######################################################################################
308 setting('end2.gcode', """;End GCode
309 M104 T0 S0                     ;extruder heater off
310 M104 T1 S0                     ;extruder heater off
311 M140 S0                     ;heated bed heater off (if you have it)
312
313 G91                                    ;relative positioning
314 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
315 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
316 G28 X0 Y0                              ;move X/Y to min endstops, so the head is out of the way
317
318 M84                         ;steppers off
319 G90                         ;absolute positioning
320 """, str, 'alteration', 'alteration')
321 #######################################################################################
322 setting('support_start.gcode', '', str, 'alteration', 'alteration')
323 setting('support_end.gcode', '', str, 'alteration', 'alteration')
324 setting('cool_start.gcode', '', str, 'alteration', 'alteration')
325 setting('cool_end.gcode', '', str, 'alteration', 'alteration')
326 setting('replace.csv', '', str, 'alteration', 'alteration')
327 #######################################################################################
328 setting('switchExtruder.gcode', """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
329 G92 E0
330 G1 E-36 F5000
331 G92 E0
332 T{extruder}
333 G1 X{new_x} Y{new_y} Z{new_z} F{travel_speed}
334 G1 E36 F5000
335 G92 E0
336 """, str, 'alteration', 'alteration')
337
338 setting('startMode', 'Simple', ['Simple', 'Normal'], 'preference', 'hidden')
339 setting('oneAtATime', 'True', bool, 'preference', 'hidden')
340 setting('lastFile', os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'resources', 'example', 'UltimakerRobot_support.stl')), str, 'preference', 'hidden')
341 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."))
342 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."))
343 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."))
344 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."))
345 setting('check_for_updates', 'True', bool, 'preference', 'hidden').setLabel(_("Check for updates"), _("Check for newer versions of Cura on startup"))
346 setting('submit_slice_information', 'False', bool, 'preference', 'hidden').setLabel(_("Send usage statistics"), _("Submit anonymous usage information to improve next versions of Cura"))
347 setting('youmagine_token', '', str, 'preference', 'hidden')
348 setting('filament_physical_density', '1240', float, 'preference', 'hidden').setRange(500.0, 3000.0).setLabel(_("Density (kg/m3)"), _("Weight of the filament per m3. Around 1240 for PLA. And around 1040 for ABS. This value is used to estimate the weight if the filament used for the print."))
349 setting('language', 'English', str, 'preference', 'hidden').setLabel(_('Language'), _('Change the language in which Cura runs. Switching language requires a restart of Cura'))
350 setting('active_machine', '0', int, 'preference', 'hidden')
351
352 setting('model_colour', '#FFC924', str, 'preference', 'hidden').setLabel(_('Model colour'), _('Display color for first extruder'))
353 setting('model_colour2', '#CB3030', str, 'preference', 'hidden').setLabel(_('Model colour (2)'), _('Display color for second extruder'))
354 setting('model_colour3', '#DDD93C', str, 'preference', 'hidden').setLabel(_('Model colour (3)'), _('Display color for third extruder'))
355 setting('model_colour4', '#4550D3', str, 'preference', 'hidden').setLabel(_('Model colour (4)'), _('Display color for forth extruder'))
356
357 setting('window_maximized', 'True', bool, 'preference', 'hidden')
358 setting('window_pos_x', '-1', float, 'preference', 'hidden')
359 setting('window_pos_y', '-1', float, 'preference', 'hidden')
360 setting('window_width', '-1', float, 'preference', 'hidden')
361 setting('window_height', '-1', float, 'preference', 'hidden')
362 setting('window_normal_sash', '320', float, 'preference', 'hidden')
363 setting('last_run_version', '', str, 'preference', 'hidden')
364
365 setting('machine_name', '', str, 'machine', 'hidden')
366 setting('machine_type', 'unknown', str, 'machine', 'hidden') #Ultimaker, Ultimaker2, RepRap
367 setting('machine_width', '205', float, 'machine', 'hidden').setLabel(_("Maximum width (mm)"), _("Size of the machine in mm"))
368 setting('machine_depth', '205', float, 'machine', 'hidden').setLabel(_("Maximum depth (mm)"), _("Size of the machine in mm"))
369 setting('machine_height', '200', float, 'machine', 'hidden').setLabel(_("Maximum height (mm)"), _("Size of the machine in mm"))
370 setting('machine_center_is_zero', 'False', bool, 'machine', 'hidden').setLabel(_("Machine center 0,0"), _("Machines firmware defines the center of the bed as 0,0 instead of the front left corner."))
371 setting('machine_shape', 'Square', ['Square','Circular'], 'machine', 'hidden').setLabel(_("Build area shape"), _("The shape of machine build area."))
372 setting('ultimaker_extruder_upgrade', 'False', bool, 'machine', 'hidden')
373 setting('has_heated_bed', 'False', bool, 'machine', 'hidden').setLabel(_("Heated bed"), _("If you have an heated bed, this enabled heated bed settings (requires restart)"))
374 setting('gcode_flavor', 'RepRap (Marlin/Sprinter)', ['RepRap (Marlin/Sprinter)', 'UltiGCode', 'MakerBot'], 'machine', 'hidden').setLabel(_("GCode Flavor"), _("Flavor of generated GCode.\nRepRap is normal 5D GCode which works on Marlin/Sprinter based firmwares.\nUltiGCode is a variation of the RepRap GCode which puts more settings in the machine instead of the slicer.\nMakerBot GCode has a few changes in the way GCode is generated, but still requires MakerWare to generate to X3G."))
375 setting('extruder_amount', '1', ['1','2','3','4'], 'machine', 'hidden').setLabel(_("Extruder count"), _("Amount of extruders in your machine."))
376 setting('extruder_offset_x1', '0.0', float, 'machine', 'hidden').setLabel(_("Offset X"), _("The offset of your secondary extruder compared to the primary."))
377 setting('extruder_offset_y1', '21.6', float, 'machine', 'hidden').setLabel(_("Offset Y"), _("The offset of your secondary extruder compared to the primary."))
378 setting('extruder_offset_x2', '0.0', float, 'machine', 'hidden').setLabel(_("Offset X"), _("The offset of your tertiary extruder compared to the primary."))
379 setting('extruder_offset_y2', '0.0', float, 'machine', 'hidden').setLabel(_("Offset Y"), _("The offset of your tertiary extruder compared to the primary."))
380 setting('extruder_offset_x3', '0.0', float, 'machine', 'hidden').setLabel(_("Offset X"), _("The offset of your forth extruder compared to the primary."))
381 setting('extruder_offset_y3', '0.0', float, 'machine', 'hidden').setLabel(_("Offset Y"), _("The offset of your forth extruder compared to the primary."))
382 setting('steps_per_e', '0', float, 'machine', 'hidden').setLabel(_("E-Steps per 1mm filament"), _("Amount of steps per mm filament extrusion. If set to 0 then this value is ignored and the value in your firmware is used."))
383 setting('serial_port', 'AUTO', str, 'machine', 'hidden').setLabel(_("Serial port"), _("Serial port to use for communication with the printer"))
384 setting('serial_port_auto', '', str, 'machine', 'hidden')
385 setting('serial_baud', 'AUTO', str, 'machine', 'hidden').setLabel(_("Baudrate"), _("Speed of the serial port communication\nNeeds to match your firmware settings\nCommon values are 250000, 115200, 57600"))
386 setting('serial_baud_auto', '', int, 'machine', 'hidden')
387
388 setting('extruder_head_size_min_x', '0.0', float, 'machine', '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."))
389 setting('extruder_head_size_min_y', '0.0', float, 'machine', '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."))
390 setting('extruder_head_size_max_x', '0.0', float, 'machine', '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."))
391 setting('extruder_head_size_max_y', '0.0', float, 'machine', '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."))
392 setting('extruder_head_size_height', '0.0', float, 'machine', '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. 60mm for an Ultimaker."))
393
394 validators.warningAbove(settingsDictionary['filament_flow'], 150, _("More flow than 150% is rare and usually not recommended."))
395 validators.warningBelow(settingsDictionary['filament_flow'], 50, _("Less flow than 50% is rare and usually not recommended."))
396 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."))
397 validators.wallThicknessValidator(settingsDictionary['wall_thickness'])
398 validators.warningAbove(settingsDictionary['print_speed'], 150.0, _("It is highly unlikely that your machine can achieve a printing speed above 150mm/s"))
399 validators.printSpeedValidator(settingsDictionary['print_speed'])
400 validators.warningAbove(settingsDictionary['print_temperature'], 260.0, _("Temperatures above 260C could damage your machine, be careful!"))
401 validators.warningAbove(settingsDictionary['print_temperature2'], 260.0, _("Temperatures above 260C could damage your machine, be careful!"))
402 validators.warningAbove(settingsDictionary['print_temperature3'], 260.0, _("Temperatures above 260C could damage your machine, be careful!"))
403 validators.warningAbove(settingsDictionary['print_temperature4'], 260.0, _("Temperatures above 260C could damage your machine, be careful!"))
404 validators.warningAbove(settingsDictionary['filament_diameter'], 3.5, _("Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm."))
405 validators.warningAbove(settingsDictionary['filament_diameter2'], 3.5, _("Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm."))
406 validators.warningAbove(settingsDictionary['filament_diameter3'], 3.5, _("Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm."))
407 validators.warningAbove(settingsDictionary['filament_diameter4'], 3.5, _("Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm."))
408 validators.warningAbove(settingsDictionary['travel_speed'], 300.0, _("It is highly unlikely that your machine can achieve a travel speed above 300mm/s"))
409 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."))
410
411 #Conditions for multiple extruders
412 settingsDictionary['print_temperature2'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
413 settingsDictionary['print_temperature3'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 2)
414 settingsDictionary['print_temperature4'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 3)
415 settingsDictionary['filament_diameter2'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
416 settingsDictionary['filament_diameter3'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 2)
417 settingsDictionary['filament_diameter4'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 3)
418 settingsDictionary['support_dual_extrusion'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
419 settingsDictionary['retraction_dual_amount'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
420 settingsDictionary['wipe_tower'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
421 settingsDictionary['wipe_tower_volume'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
422 settingsDictionary['ooze_shield'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
423 #Heated bed
424 settingsDictionary['print_bed_temperature'].addCondition(lambda : getMachineSetting('has_heated_bed') == 'True')
425
426 #UltiGCode uses less settings, as these settings are located inside the machine instead of gcode.
427 settingsDictionary['print_temperature'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
428 settingsDictionary['print_temperature2'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
429 settingsDictionary['print_temperature3'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
430 settingsDictionary['print_temperature4'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
431 settingsDictionary['filament_diameter'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
432 settingsDictionary['filament_diameter2'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
433 settingsDictionary['filament_diameter3'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
434 settingsDictionary['filament_diameter4'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
435 settingsDictionary['filament_flow'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
436 settingsDictionary['print_bed_temperature'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
437 settingsDictionary['retraction_speed'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
438 settingsDictionary['retraction_amount'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
439 settingsDictionary['retraction_dual_amount'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
440
441 #Remove fake defined _() because later the localization will define a global _()
442 del _
443
444 #########################################################
445 ## Profile and preferences functions
446 #########################################################
447
448 def getSubCategoriesFor(category):
449         done = {}
450         ret = []
451         for s in settingsList:
452                 if s.getCategory() == category and not s.getSubCategory() in done and s.checkConditions():
453                         done[s.getSubCategory()] = True
454                         ret.append(s.getSubCategory())
455         return ret
456
457 def getSettingsForCategory(category, subCategory = None):
458         ret = []
459         for s in settingsList:
460                 if s.getCategory() == category and (subCategory is None or s.getSubCategory() == subCategory) and s.checkConditions():
461                         ret.append(s)
462         return ret
463
464 ## Profile functions
465 def getBasePath():
466         if platform.system() == "Windows":
467                 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
468                 #If we have a frozen python install, we need to step out of the library.zip
469                 if hasattr(sys, 'frozen'):
470                         basePath = os.path.normpath(os.path.join(basePath, ".."))
471         else:
472                 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
473         if not os.path.isdir(basePath):
474                 os.makedirs(basePath)
475         return basePath
476
477 def getAlternativeBasePaths():
478         paths = []
479         basePath = os.path.normpath(os.path.join(getBasePath(), '..'))
480         for subPath in os.listdir(basePath):
481                 path = os.path.join(basePath, subPath)
482                 if os.path.isdir(path) and os.path.isfile(os.path.join(path, 'preferences.ini')) and path != getBasePath():
483                         paths.append(path)
484                 path = os.path.join(basePath, subPath, 'Cura')
485                 if os.path.isdir(path) and os.path.isfile(os.path.join(path, 'preferences.ini')) and path != getBasePath():
486                         paths.append(path)
487         return paths
488
489 def getDefaultProfilePath():
490         return os.path.join(getBasePath(), 'current_profile.ini')
491
492 def loadProfile(filename):
493         #Read a configuration file as global config
494         profileParser = ConfigParser.ConfigParser()
495         try:
496                 profileParser.read(filename)
497         except ConfigParser.ParsingError:
498                 return
499         global settingsList
500         for set in settingsList:
501                 if set.isPreference():
502                         continue
503                 section = 'profile'
504                 if set.isAlteration():
505                         section = 'alterations'
506                 if profileParser.has_option(section, set.getName()):
507                         set.setValue(unicode(profileParser.get(section, set.getName()), 'utf-8', 'replace'))
508
509 def saveProfile(filename):
510         #Save the current profile to an ini file
511         profileParser = ConfigParser.ConfigParser()
512         profileParser.add_section('profile')
513         profileParser.add_section('alterations')
514         global settingsList
515         for set in settingsList:
516                 if set.isPreference() or set.isMachineSetting():
517                         continue
518                 if set.isAlteration():
519                         profileParser.set('alterations', set.getName(), set.getValue().encode('utf-8'))
520                 else:
521                         profileParser.set('profile', set.getName(), set.getValue().encode('utf-8'))
522
523         profileParser.write(open(filename, 'w'))
524
525 def resetProfile():
526         #Read a configuration file as global config
527         global settingsList
528         for set in settingsList:
529                 if not set.isProfile():
530                         continue
531                 set.setValue(set.getDefault())
532
533         if getMachineSetting('machine_type') == 'ultimaker':
534                 putProfileSetting('nozzle_size', '0.4')
535                 if getMachineSetting('ultimaker_extruder_upgrade') == 'True':
536                         putProfileSetting('retraction_enable', 'True')
537         elif getMachineSetting('machine_type') == 'ultimaker2':
538                 putProfileSetting('nozzle_size', '0.4')
539                 putProfileSetting('retraction_enable', 'True')
540         else:
541                 putProfileSetting('nozzle_size', '0.5')
542                 putProfileSetting('retraction_enable', 'True')
543
544 def setProfileFromString(options):
545         options = base64.b64decode(options)
546         options = zlib.decompress(options)
547         (profileOpts, alt) = options.split('\f', 1)
548         global settingsDictionary
549         for option in profileOpts.split('\b'):
550                 if len(option) > 0:
551                         (key, value) = option.split('=', 1)
552                         if key in settingsDictionary:
553                                 if settingsDictionary[key].isProfile():
554                                         settingsDictionary[key].setValue(value)
555         for option in alt.split('\b'):
556                 if len(option) > 0:
557                         (key, value) = option.split('=', 1)
558                         if key in settingsDictionary:
559                                 if settingsDictionary[key].isAlteration():
560                                         settingsDictionary[key].setValue(value)
561
562 def getProfileString():
563         p = []
564         alt = []
565         global settingsList
566         for set in settingsList:
567                 if set.isProfile():
568                         if set.getName() in tempOverride:
569                                 p.append(set.getName() + "=" + tempOverride[set.getName()])
570                         else:
571                                 p.append(set.getName() + "=" + set.getValue())
572                 elif set.isAlteration():
573                         if set.getName() in tempOverride:
574                                 alt.append(set.getName() + "=" + tempOverride[set.getName()])
575                         else:
576                                 alt.append(set.getName() + "=" + set.getValue())
577         ret = '\b'.join(p) + '\f' + '\b'.join(alt)
578         ret = base64.b64encode(zlib.compress(ret, 9))
579         return ret
580
581 def insertNewlines(string, every=64): #This should be moved to a better place then profile.
582         lines = []
583         for i in xrange(0, len(string), every):
584                 lines.append(string[i:i+every])
585         return '\n'.join(lines)
586
587 def getPreferencesString():
588         p = []
589         global settingsList
590         for set in settingsList:
591                 if set.isPreference():
592                         p.append(set.getName() + "=" + set.getValue())
593         ret = '\b'.join(p)
594         ret = base64.b64encode(zlib.compress(ret, 9))
595         return ret
596
597
598 def getProfileSetting(name):
599         if name in tempOverride:
600                 return tempOverride[name]
601         global settingsDictionary
602         if name in settingsDictionary and settingsDictionary[name].isProfile():
603                 return settingsDictionary[name].getValue()
604         traceback.print_stack()
605         sys.stderr.write('Error: "%s" not found in profile settings\n' % (name))
606         return ''
607
608 def getProfileSettingFloat(name):
609         try:
610                 setting = getProfileSetting(name).replace(',', '.')
611                 return float(eval(setting, {}, {}))
612         except:
613                 return 0.0
614
615 def putProfileSetting(name, value):
616         #Check if we have a configuration file loaded, else load the default.
617         global settingsDictionary
618         if name in settingsDictionary and settingsDictionary[name].isProfile():
619                 settingsDictionary[name].setValue(value)
620
621 def isProfileSetting(name):
622         global settingsDictionary
623         if name in settingsDictionary and settingsDictionary[name].isProfile():
624                 return True
625         return False
626
627 ## Preferences functions
628 def getPreferencePath():
629         return os.path.join(getBasePath(), 'preferences.ini')
630
631 def getPreferenceFloat(name):
632         try:
633                 setting = getPreference(name).replace(',', '.')
634                 return float(eval(setting, {}, {}))
635         except:
636                 return 0.0
637
638 def getPreferenceColour(name):
639         colorString = getPreference(name)
640         return [float(int(colorString[1:3], 16)) / 255, float(int(colorString[3:5], 16)) / 255, float(int(colorString[5:7], 16)) / 255, 1.0]
641
642 def loadPreferences(filename):
643         global settingsList
644         #Read a configuration file as global config
645         profileParser = ConfigParser.ConfigParser()
646         try:
647                 profileParser.read(filename)
648         except ConfigParser.ParsingError:
649                 return
650
651         for set in settingsList:
652                 if set.isPreference():
653                         if profileParser.has_option('preference', set.getName()):
654                                 set.setValue(unicode(profileParser.get('preference', set.getName()), 'utf-8', 'replace'))
655
656         n = 0
657         while profileParser.has_section('machine_%d' % (n)):
658                 for set in settingsList:
659                         if set.isMachineSetting():
660                                 if profileParser.has_option('machine_%d' % (n), set.getName()):
661                                         set.setValue(unicode(profileParser.get('machine_%d' % (n), set.getName()), 'utf-8', 'replace'), n)
662                 n += 1
663
664         setActiveMachine(int(getPreferenceFloat('active_machine')))
665
666 def loadMachineSettings(filename):
667         global settingsList
668         #Read a configuration file as global config
669         profileParser = ConfigParser.ConfigParser()
670         try:
671                 profileParser.read(filename)
672         except ConfigParser.ParsingError:
673                 return
674
675         for set in settingsList:
676                 if set.isMachineSetting():
677                         if profileParser.has_option('machine', set.getName()):
678                                 set.setValue(unicode(profileParser.get('machine', set.getName()), 'utf-8', 'replace'))
679         checkAndUpdateMachineName()
680
681 def savePreferences(filename):
682         global settingsList
683         #Save the current profile to an ini file
684         parser = ConfigParser.ConfigParser()
685         parser.add_section('preference')
686
687         for set in settingsList:
688                 if set.isPreference():
689                         parser.set('preference', set.getName(), set.getValue().encode('utf-8'))
690
691         n = 0
692         while getMachineSetting('machine_name', n) != '':
693                 parser.add_section('machine_%d' % (n))
694                 for set in settingsList:
695                         if set.isMachineSetting():
696                                 parser.set('machine_%d' % (n), set.getName(), set.getValue(n).encode('utf-8'))
697                 n += 1
698         parser.write(open(filename, 'w'))
699
700 def getPreference(name):
701         if name in tempOverride:
702                 return tempOverride[name]
703         global settingsDictionary
704         if name in settingsDictionary and settingsDictionary[name].isPreference():
705                 return settingsDictionary[name].getValue()
706         traceback.print_stack()
707         sys.stderr.write('Error: "%s" not found in preferences\n' % (name))
708         return ''
709
710 def putPreference(name, value):
711         #Check if we have a configuration file loaded, else load the default.
712         global settingsDictionary
713         if name in settingsDictionary and settingsDictionary[name].isPreference():
714                 settingsDictionary[name].setValue(value)
715                 savePreferences(getPreferencePath())
716                 return
717         traceback.print_stack()
718         sys.stderr.write('Error: "%s" not found in preferences\n' % (name))
719
720 def isPreference(name):
721         global settingsDictionary
722         if name in settingsDictionary and settingsDictionary[name].isPreference():
723                 return True
724         return False
725
726 def getMachineSettingFloat(name, index = None):
727         try:
728                 setting = getMachineSetting(name, index).replace(',', '.')
729                 return float(eval(setting, {}, {}))
730         except:
731                 return 0.0
732
733 def getMachineSetting(name, index = None):
734         if name in tempOverride:
735                 return tempOverride[name]
736         global settingsDictionary
737         if name in settingsDictionary and settingsDictionary[name].isMachineSetting():
738                 return settingsDictionary[name].getValue(index)
739         traceback.print_stack()
740         sys.stderr.write('Error: "%s" not found in machine settings\n' % (name))
741         return ''
742
743 def putMachineSetting(name, value):
744         #Check if we have a configuration file loaded, else load the default.
745         global settingsDictionary
746         if name in settingsDictionary and settingsDictionary[name].isMachineSetting():
747                 settingsDictionary[name].setValue(value)
748         savePreferences(getPreferencePath())
749
750 def isMachineSetting(name):
751         global settingsDictionary
752         if name in settingsDictionary and settingsDictionary[name].isMachineSetting():
753                 return True
754         return False
755
756 def checkAndUpdateMachineName():
757         global _selectedMachineIndex
758         name = getMachineSetting('machine_name')
759         index = None
760         if name == '':
761                 name = getMachineSetting('machine_type')
762         for n in xrange(0, getMachineCount()):
763                 if n == _selectedMachineIndex:
764                         continue
765                 if index is None:
766                         if name == getMachineSetting('machine_name', n):
767                                 index = 1
768                 else:
769                         if '%s (%d)' % (name, index) == getMachineSetting('machine_name', n):
770                                 index += 1
771         if index is not None:
772                 name = '%s (%d)' % (name, index)
773         putMachineSetting('machine_name', name)
774         putPreference('active_machine', _selectedMachineIndex)
775
776 def getMachineCount():
777         n = 0
778         while getMachineSetting('machine_name', n) != '':
779                 n += 1
780         if n < 1:
781                 return 1
782         return n
783
784 def setActiveMachine(index):
785         global _selectedMachineIndex
786         _selectedMachineIndex = index
787         putPreference('active_machine', _selectedMachineIndex)
788
789 def removeMachine(index):
790         global _selectedMachineIndex
791         global settingsList
792         if getMachineCount() < 2:
793                 return
794         for n in xrange(index, getMachineCount()):
795                 for setting in settingsList:
796                         if setting.isMachineSetting():
797                                 setting.setValue(setting.getValue(n+1), n)
798
799         if _selectedMachineIndex >= index:
800                 setActiveMachine(getMachineCount() - 1)
801
802 ## Temp overrides for multi-extruder slicing and the project planner.
803 tempOverride = {}
804 def setTempOverride(name, value):
805         tempOverride[name] = unicode(value).encode("utf-8")
806 def clearTempOverride(name):
807         del tempOverride[name]
808 def resetTempOverride():
809         tempOverride.clear()
810
811 #########################################################
812 ## Utility functions to calculate common profile values
813 #########################################################
814 def calculateEdgeWidth():
815         wallThickness = getProfileSettingFloat('wall_thickness')
816         nozzleSize = getProfileSettingFloat('nozzle_size')
817
818         if getProfileSetting('spiralize') == 'True':
819                 return wallThickness
820
821         if wallThickness < 0.01:
822                 return nozzleSize
823         if wallThickness < nozzleSize:
824                 return wallThickness
825
826         lineCount = int(wallThickness / (nozzleSize - 0.0001))
827         if lineCount == 0:
828                 return nozzleSize
829         lineWidth = wallThickness / lineCount
830         lineWidthAlt = wallThickness / (lineCount + 1)
831         if lineWidth > nozzleSize * 1.5:
832                 return lineWidthAlt
833         return lineWidth
834
835 def calculateLineCount():
836         wallThickness = getProfileSettingFloat('wall_thickness')
837         nozzleSize = getProfileSettingFloat('nozzle_size')
838
839         if wallThickness < 0.01:
840                 return 0
841         if wallThickness < nozzleSize:
842                 return 1
843         if getProfileSetting('spiralize') == 'True':
844                 return 1
845
846         lineCount = int(wallThickness / (nozzleSize - 0.0001))
847         if lineCount < 1:
848                 lineCount = 1
849         lineWidth = wallThickness / lineCount
850         lineWidthAlt = wallThickness / (lineCount + 1)
851         if lineWidth > nozzleSize * 1.5:
852                 return lineCount + 1
853         return lineCount
854
855 def calculateSolidLayerCount():
856         layerHeight = getProfileSettingFloat('layer_height')
857         solidThickness = getProfileSettingFloat('solid_layer_thickness')
858         if layerHeight == 0.0:
859                 return 1
860         return int(math.ceil(solidThickness / (layerHeight - 0.0001)))
861
862 def calculateObjectSizeOffsets():
863         size = 0.0
864
865         if getProfileSetting('platform_adhesion') == 'Brim':
866                 size += getProfileSettingFloat('brim_line_count') * calculateEdgeWidth()
867         elif getProfileSetting('platform_adhesion') == 'Raft':
868                 pass
869         else:
870                 if getProfileSettingFloat('skirt_line_count') > 0:
871                         size += getProfileSettingFloat('skirt_line_count') * calculateEdgeWidth() + getProfileSettingFloat('skirt_gap')
872
873         #if getProfileSetting('enable_raft') != 'False':
874         #       size += profile.getProfileSettingFloat('raft_margin') * 2
875         #if getProfileSetting('support') != 'None':
876         #       extraSizeMin = extraSizeMin + numpy.array([3.0, 0, 0])
877         #       extraSizeMax = extraSizeMax + numpy.array([3.0, 0, 0])
878         return [size, size]
879
880 def getMachineCenterCoords():
881         if getMachineSetting('machine_center_is_zero') == 'True':
882                 return [0, 0]
883         return [getMachineSettingFloat('machine_width') / 2, getMachineSettingFloat('machine_depth') / 2]
884
885 #Returns a list of convex polygons, first polygon is the allowed area of the machine,
886 # the rest of the polygons are the dis-allowed areas of the machine.
887 def getMachineSizePolygons():
888         size = numpy.array([getMachineSettingFloat('machine_width'), getMachineSettingFloat('machine_depth'), getMachineSettingFloat('machine_height')], numpy.float32)
889         ret = []
890         if getMachineSetting('machine_shape') == 'Circular':
891                 # Circle platform for delta printers...
892                 circle = []
893                 steps = 32
894                 for n in xrange(0, steps):
895                         circle.append([math.cos(float(n)/steps*2*math.pi) * size[0]/2, math.sin(float(n)/steps*2*math.pi) * size[1]/2])
896                 ret.append(numpy.array(circle, numpy.float32))
897         else:
898                 ret.append(numpy.array([[-size[0]/2,-size[1]/2],[size[0]/2,-size[1]/2],[size[0]/2, size[1]/2], [-size[0]/2, size[1]/2]], numpy.float32))
899
900         if getMachineSetting('machine_type') == 'ultimaker2':
901                 #UM2 no-go zones
902                 w = 25
903                 h = 10
904                 ret.append(numpy.array([[-size[0]/2,-size[1]/2],[-size[0]/2+w+2,-size[1]/2], [-size[0]/2+w,-size[1]/2+h], [-size[0]/2,-size[1]/2+h]], numpy.float32))
905                 ret.append(numpy.array([[ size[0]/2-w-2,-size[1]/2],[ size[0]/2,-size[1]/2], [ size[0]/2,-size[1]/2+h],[ size[0]/2-w,-size[1]/2+h]], numpy.float32))
906                 ret.append(numpy.array([[-size[0]/2+w+2, size[1]/2],[-size[0]/2, size[1]/2], [-size[0]/2, size[1]/2-h],[-size[0]/2+w, size[1]/2-h]], numpy.float32))
907                 ret.append(numpy.array([[ size[0]/2, size[1]/2],[ size[0]/2-w-2, size[1]/2], [ size[0]/2-w, size[1]/2-h],[ size[0]/2, size[1]/2-h]], numpy.float32))
908         return ret
909
910 #returns the number of extruders minimal used. Normally this returns 1, but with dual-extrusion support material it returns 2
911 def minimalExtruderCount():
912         if int(getMachineSetting('extruder_amount')) < 2:
913                 return 1
914         if getProfileSetting('support') == 'None':
915                 return 1
916         if getProfileSetting('support_dual_extrusion') == 'Second extruder':
917                 return 2
918         return 1
919
920 #########################################################
921 ## Alteration file functions
922 #########################################################
923 def replaceTagMatch(m):
924         pre = m.group(1)
925         tag = m.group(2)
926         if tag == 'time':
927                 return pre + time.strftime('%H:%M:%S')
928         if tag == 'date':
929                 return pre + time.strftime('%d-%m-%Y')
930         if tag == 'day':
931                 return pre + ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][int(time.strftime('%w'))]
932         if tag == 'print_time':
933                 return pre + '#P_TIME#'
934         if tag == 'filament_amount':
935                 return pre + '#F_AMNT#'
936         if tag == 'filament_weight':
937                 return pre + '#F_WGHT#'
938         if tag == 'filament_cost':
939                 return pre + '#F_COST#'
940         if pre == 'F' and tag == 'max_z_speed':
941                 f = getProfileSettingFloat('travel_speed') * 60
942         if pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
943                 f = getProfileSettingFloat(tag) * 60
944         elif isProfileSetting(tag):
945                 f = getProfileSettingFloat(tag)
946         elif isPreference(tag):
947                 f = getProfileSettingFloat(tag)
948         else:
949                 return '%s?%s?' % (pre, tag)
950         if (f % 1) == 0:
951                 return pre + str(int(f))
952         return pre + str(f)
953
954 def replaceGCodeTags(filename, gcodeInt):
955         f = open(filename, 'r+')
956         data = f.read(2048)
957         data = data.replace('#P_TIME#', ('%5d:%02d' % (int(gcodeInt.totalMoveTimeMinute / 60), int(gcodeInt.totalMoveTimeMinute % 60)))[-8:])
958         data = data.replace('#F_AMNT#', ('%8.2f' % (gcodeInt.extrusionAmount / 1000))[-8:])
959         data = data.replace('#F_WGHT#', ('%8.2f' % (gcodeInt.calculateWeight() * 1000))[-8:])
960         cost = gcodeInt.calculateCost()
961         if cost is None:
962                 cost = 'Unknown'
963         data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
964         f.seek(0)
965         f.write(data)
966         f.close()
967
968 def replaceGCodeTagsFromSlicer(filename, slicerInt):
969         f = open(filename, 'r+')
970         data = f.read(2048)
971         data = data.replace('#P_TIME#', ('%8.2f' % (int(slicerInt._printTimeSeconds)))[-8:])
972         data = data.replace('#F_AMNT#', ('%8.2f' % (slicerInt._filamentMM[0]))[-8:])
973         data = data.replace('#F_WGHT#', ('%8.2f' % (float(slicerInt.getFilamentWeight()) * 1000))[-8:])
974         cost = slicerInt.getFilamentCost()
975         if cost is None:
976                 cost = 'Unknown'
977         data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
978         f.seek(0)
979         f.write(data)
980         f.close()
981
982 ### Get aleration raw contents. (Used internally in Cura)
983 def getAlterationFile(filename):
984         if filename in tempOverride:
985                 return tempOverride[filename]
986         global settingsDictionary
987         if filename in settingsDictionary and settingsDictionary[filename].isAlteration():
988                 return settingsDictionary[filename].getValue()
989         traceback.print_stack()
990         sys.stderr.write('Error: "%s" not found in alteration settings\n' % (filename))
991         return ''
992
993 def setAlterationFile(name, value):
994         #Check if we have a configuration file loaded, else load the default.
995         global settingsDictionary
996         if name in settingsDictionary and settingsDictionary[name].isAlteration():
997                 settingsDictionary[name].setValue(value)
998         saveProfile(getDefaultProfilePath())
999
1000 def isTagIn(tag, contents):
1001         contents = re.sub(';[^\n]*\n', '', contents)
1002         return tag in contents
1003
1004 ### Get the alteration file for output. (Used by Skeinforge)
1005 def getAlterationFileContents(filename, extruderCount = 1):
1006         prefix = ''
1007         postfix = ''
1008         alterationContents = getAlterationFile(filename)
1009         if getMachineSetting('gcode_flavor') == 'UltiGCode':
1010                 if filename == 'end.gcode':
1011                         return 'M25 ;Stop reading from this point on.\n;CURA_PROFILE_STRING:%s\n' % (getProfileString())
1012                 return ''
1013         if filename == 'start.gcode':
1014                 if extruderCount > 1:
1015                         alterationContents = getAlterationFile("start%d.gcode" % (extruderCount))
1016                 #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.
1017                 #We also set our steps per E here, if configured.
1018                 eSteps = getMachineSettingFloat('steps_per_e')
1019                 if eSteps > 0:
1020                         prefix += 'M92 E%f\n' % (eSteps)
1021                 temp = getProfileSettingFloat('print_temperature')
1022                 bedTemp = 0
1023                 if getMachineSetting('has_heated_bed') == 'True':
1024                         bedTemp = getProfileSettingFloat('print_bed_temperature')
1025
1026                 if bedTemp > 0 and not isTagIn('{print_bed_temperature}', alterationContents):
1027                         prefix += 'M140 S%f\n' % (bedTemp)
1028                 if temp > 0 and not isTagIn('{print_temperature}', alterationContents):
1029                         if extruderCount > 0:
1030                                 for n in xrange(1, extruderCount):
1031                                         t = temp
1032                                         if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
1033                                                 t = getProfileSettingFloat('print_temperature%d' % (n+1))
1034                                         prefix += 'M104 T%d S%f\n' % (n, t)
1035                                 for n in xrange(0, extruderCount):
1036                                         t = temp
1037                                         if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
1038                                                 t = getProfileSettingFloat('print_temperature%d' % (n+1))
1039                                         prefix += 'M109 T%d S%f\n' % (n, t)
1040                                 prefix += 'T0\n'
1041                         else:
1042                                 prefix += 'M109 S%f\n' % (temp)
1043                 if bedTemp > 0 and not isTagIn('{print_bed_temperature}', alterationContents):
1044                         prefix += 'M190 S%f\n' % (bedTemp)
1045         elif filename == 'end.gcode':
1046                 if extruderCount > 1:
1047                         alterationContents = getAlterationFile("end%d.gcode" % (extruderCount))
1048                 #Append the profile string to the end of the GCode, so we can load it from the GCode file later.
1049                 postfix = ';CURA_PROFILE_STRING:%s\n' % (getProfileString())
1050         return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n'
1051
1052 ###### PLUGIN #####
1053
1054 def getPluginConfig():
1055         try:
1056                 return pickle.loads(str(getProfileSetting('plugin_config')))
1057         except:
1058                 return []
1059
1060 def setPluginConfig(config):
1061         putProfileSetting('plugin_config', pickle.dumps(config))
1062
1063 def getPluginBasePaths():
1064         ret = []
1065         if platform.system() != "Windows":
1066                 ret.append(os.path.expanduser('~/.cura/plugins/'))
1067         if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
1068                 ret.append(os.path.normpath(os.path.join(resources.resourceBasePath, "Cura/plugins")))
1069         else:
1070                 ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
1071         return ret
1072
1073 def getPluginList():
1074         ret = []
1075         for basePath in getPluginBasePaths():
1076                 for filename in glob.glob(os.path.join(basePath, '*.py')):
1077                         filename = os.path.basename(filename)
1078                         if filename.startswith('_'):
1079                                 continue
1080                         with open(os.path.join(basePath, filename), "r") as f:
1081                                 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
1082                                 for line in f:
1083                                         line = line.strip()
1084                                         if not line.startswith('#'):
1085                                                 break
1086                                         line = line[1:].split(':', 1)
1087                                         if len(line) != 2:
1088                                                 continue
1089                                         if line[0].upper() == 'NAME':
1090                                                 item['name'] = line[1].strip()
1091                                         elif line[0].upper() == 'INFO':
1092                                                 item['info'] = line[1].strip()
1093                                         elif line[0].upper() == 'TYPE':
1094                                                 item['type'] = line[1].strip()
1095                                         elif line[0].upper() == 'DEPEND':
1096                                                 pass
1097                                         elif line[0].upper() == 'PARAM':
1098                                                 m = re.match('([a-zA-Z][a-zA-Z0-9_]*)\(([a-zA-Z_]*)(?::([^\)]*))?\) +(.*)', line[1].strip())
1099                                                 if m is not None:
1100                                                         item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
1101                                         else:
1102                                                 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
1103                                 if item['name'] is not None and item['type'] == 'postprocess':
1104                                         ret.append(item)
1105         return ret
1106
1107 def runPostProcessingPlugins(gcodefilename):
1108         pluginConfigList = getPluginConfig()
1109         pluginList = getPluginList()
1110
1111         for pluginConfig in pluginConfigList:
1112                 plugin = None
1113                 for pluginTest in pluginList:
1114                         if pluginTest['filename'] == pluginConfig['filename']:
1115                                 plugin = pluginTest
1116                 if plugin is None:
1117                         continue
1118
1119                 pythonFile = None
1120                 for basePath in getPluginBasePaths():
1121                         testFilename = os.path.join(basePath, pluginConfig['filename'])
1122                         if os.path.isfile(testFilename):
1123                                 pythonFile = testFilename
1124                 if pythonFile is None:
1125                         continue
1126
1127                 locals = {'filename': gcodefilename}
1128                 for param in plugin['params']:
1129                         value = param['default']
1130                         if param['name'] in pluginConfig['params']:
1131                                 value = pluginConfig['params'][param['name']]
1132
1133                         if param['type'] == 'float':
1134                                 try:
1135                                         value = float(value)
1136                                 except:
1137                                         value = float(param['default'])
1138
1139                         locals[param['name']] = value
1140                 try:
1141                         execfile(pythonFile, locals)
1142                 except:
1143                         locationInfo = traceback.extract_tb(sys.exc_info()[2])[-1]
1144                         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])
1145         return None