chiark / gitweb /
Added newer ultimaker firmware. Make linux version install the 115200 baud firmware...
[cura.git] / Cura / util / profile.py
1 from __future__ import absolute_import\r
2 from __future__ import division\r
3 #Init has to be imported first because it has code to workaround the python bug where relative imports don't work if the module is imported as a main module.\r
4 import __init__\r
5 \r
6 import os, traceback, math, re, zlib, base64, time, sys\r
7 if sys.version_info[0] < 3:\r
8         import ConfigParser\r
9 else:\r
10         import configparser as ConfigParser\r
11 \r
12 #########################################################\r
13 ## Default settings when none are found.\r
14 #########################################################\r
15 \r
16 #Single place to store the defaults, so we have a consistent set of default settings.\r
17 profileDefaultSettings = {\r
18         'nozzle_size': '0.4',\r
19         'layer_height': '0.2',\r
20         'wall_thickness': '0.8',\r
21         'solid_layer_thickness': '0.6',\r
22         'fill_density': '20',\r
23         'skirt_line_count': '1',\r
24         'skirt_gap': '3.0',\r
25         'print_speed': '50',\r
26         'print_temperature': '0',\r
27         'support': 'None',\r
28         'filament_diameter': '2.89',\r
29         'filament_density': '1.00',\r
30         'machine_center_x': '100',\r
31         'machine_center_y': '100',\r
32         'retraction_min_travel': '5.0',\r
33         'retraction_speed': '40.0',\r
34         'retraction_amount': '0.0',\r
35         'retraction_extra': '0.0',\r
36         'retract_on_jumps_only': 'True',\r
37         'travel_speed': '150',\r
38         'max_z_speed': '3.0',\r
39         'bottom_layer_speed': '20',\r
40         'cool_min_layer_time': '10',\r
41         'fan_enabled': 'True',\r
42         'fan_layer': '1',\r
43         'fan_speed': '100',\r
44         'model_scale': '1.0',\r
45         'flip_x': 'False',\r
46         'flip_y': 'False',\r
47         'flip_z': 'False',\r
48         'swap_xz': 'False',\r
49         'swap_yz': 'False',\r
50         'model_rotate_base': '0',\r
51         'model_multiply_x': '1',\r
52         'model_multiply_y': '1',\r
53         'extra_base_wall_thickness': '0.0',\r
54         'sequence': 'Loops > Perimeter > Infill',\r
55         'force_first_layer_sequence': 'True',\r
56         'infill_type': 'Line',\r
57         'solid_top': 'True',\r
58         'fill_overlap': '15',\r
59         'support_rate': '50',\r
60         'support_distance': '0.5',\r
61         'joris': 'False',\r
62         'enable_skin': 'False',\r
63         'enable_raft': 'False',\r
64         'cool_min_feedrate': '5',\r
65         'bridge_speed': '100',\r
66         'raft_margin': '5',\r
67         'raft_base_material_amount': '100',\r
68         'raft_interface_material_amount': '100',\r
69         'bottom_thickness': '0.3',\r
70         \r
71         'enable_dwindle': 'False',\r
72         'dwindle_pent_up_volume': '0.4',\r
73         'dwindle_slowdown_volume': '5.0',\r
74 \r
75         'add_start_end_gcode': 'True',\r
76         'gcode_extension': 'gcode',\r
77         'alternative_center': '',\r
78         'clear_z': '0.0',\r
79         'extruder': '0',\r
80 }\r
81 alterationDefault = {\r
82 #######################################################################################\r
83         'start.gcode': """;Sliced at: {day} {date} {time}\r
84 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}\r
85 G21        ;metric values\r
86 G90        ;absolute positioning\r
87 M107       ;start with the fan off\r
88 \r
89 G28 X0 Y0  ;move X/Y to min endstops\r
90 G28 Z0     ;move Z to min endstops\r
91 \r
92 ; if your prints start too high, try changing the Z0.0 below\r
93 ; to Z1.0 - the number after the Z is the actual, physical\r
94 ; height of the nozzle in mm. This can take some messing around\r
95 ; with to get just right...\r
96 G92 X0 Y0 Z0 E0         ;reset software position to front/left/z=0.0\r
97 G1 Z15.0 F{max_z_speed} ;move the platform down 15mm\r
98 G92 E0                  ;zero the extruded length\r
99 \r
100 G1 F200 E5              ;extrude 5mm of feed stock\r
101 G1 F200 E3.5            ;reverse feed stock by 1.5mm\r
102 G92 E0                  ;zero the extruded length again\r
103 \r
104 ;go to the middle of the platform, and move to Z=0 before starting the print.\r
105 G1 X{machine_center_x} Y{machine_center_y} F{travel_speed}\r
106 G1 Z0.0 F{max_z_speed}\r
107 """,\r
108 #######################################################################################\r
109         'end.gcode': """;End GCode\r
110 M104 S0                    ;extruder heat off\r
111 G91                        ;relative positioning\r
112 G1 Z+10 E-5 F{max_z_speed} ;move Z up a bit and retract filament by 5mm\r
113 G28 X0 Y0                  ;move X/Y to min endstops, so the head is out of the way\r
114 M84                        ;steppers off\r
115 G90                        ;absolute positioning\r
116 """,\r
117 #######################################################################################\r
118         'support_start.gcode': '',\r
119         'support_end.gcode': '',\r
120         'cool_start.gcode': '',\r
121         'cool_end.gcode': '',\r
122         'replace.csv': '',\r
123 #######################################################################################\r
124         'nextobject.gcode': """;Move to next object on the platform. clear_z is the minimal z height we need to make sure we do not hit any objects.\r
125 G92 E0\r
126 G1 Z{clear_z} E-5 F{max_z_speed}\r
127 G92 E0\r
128 G1 X{machine_center_x} Y{machine_center_y} F{travel_speed}\r
129 G1 F200 E5.5\r
130 G92 E0\r
131 G1 Z0 F{max_z_speed}\r
132 """,\r
133 #######################################################################################\r
134         'switchExtruder.gcode': """;Switch between the current extruder and the next extruder, when printing with multiple extruders.\r
135 G1 E-5 F5000\r
136 G92 E0\r
137 T{extruder}\r
138 G1 E5 F5000\r
139 G92 E0\r
140 """,\r
141 }\r
142 preferencesDefaultSettings = {\r
143         'wizardDone': 'False',\r
144         'startMode': 'Simple',\r
145         'lastFile': '',\r
146         'machine_width': '205',\r
147         'machine_depth': '205',\r
148         'machine_height': '200',\r
149         'machine_type': 'unknown',\r
150         'extruder_amount': '1',\r
151         'extruder_offset_x1': '-22.0',\r
152         'extruder_offset_y1': '0.0',\r
153         'extruder_offset_x2': '0.0',\r
154         'extruder_offset_y2': '0.0',\r
155         'extruder_offset_x3': '0.0',\r
156         'extruder_offset_y3': '0.0',\r
157         'filament_density': '1300',\r
158         'steps_per_e': '0',\r
159         'serial_port': 'AUTO',\r
160         'serial_baud': 'AUTO',\r
161         'slicer': 'Cura (Skeinforge based)',\r
162         'save_profile': 'False',\r
163         'filament_cost_kg': '0',\r
164         'filament_cost_meter': '0',\r
165         'sdpath': '',\r
166         'sdshortnames': 'True',\r
167         \r
168         'extruder_head_size_min_x': '70.0',\r
169         'extruder_head_size_min_y': '18.0',\r
170         'extruder_head_size_max_x': '18.0',\r
171         'extruder_head_size_max_y': '35.0',\r
172 }\r
173 \r
174 #########################################################\r
175 ## Profile and preferences functions\r
176 #########################################################\r
177 \r
178 ## Profile functions\r
179 def getDefaultProfilePath():\r
180         basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))\r
181         #If we have a frozen python install, we need to step out of the library.zip\r
182         if hasattr(sys, 'frozen'):\r
183                 basePath = os.path.normpath(os.path.join(basePath, ".."))\r
184         return os.path.normpath(os.path.join(basePath, "current_profile.ini"))\r
185 \r
186 def loadGlobalProfile(filename):\r
187         #Read a configuration file as global config\r
188         global globalProfileParser\r
189         globalProfileParser = ConfigParser.ConfigParser()\r
190         globalProfileParser.read(filename)\r
191 \r
192 def resetGlobalProfile():\r
193         #Read a configuration file as global config\r
194         global globalProfileParser\r
195         globalProfileParser = ConfigParser.ConfigParser()\r
196 \r
197 def saveGlobalProfile(filename):\r
198         #Save the current profile to an ini file\r
199         globalProfileParser.write(open(filename, 'w'))\r
200 \r
201 def loadGlobalProfileFromString(options):\r
202         global globalProfileParser\r
203         globalProfileParser = ConfigParser.ConfigParser()\r
204         globalProfileParser.add_section('profile')\r
205         globalProfileParser.add_section('alterations')\r
206         options = base64.b64decode(options)\r
207         options = zlib.decompress(options)\r
208         (profileOpts, alt) = options.split('\f', 1)\r
209         for option in profileOpts.split('\b'):\r
210                 if len(option) > 0:\r
211                         (key, value) = option.split('=', 1)\r
212                         globalProfileParser.set('profile', key, value)\r
213         for option in alt.split('\b'):\r
214                 if len(option) > 0:\r
215                         (key, value) = option.split('=', 1)\r
216                         globalProfileParser.set('alterations', key, value)\r
217 \r
218 def getGlobalProfileString():\r
219         global globalProfileParser\r
220         if not globals().has_key('globalProfileParser'):\r
221                 loadGlobalProfile(getDefaultProfilePath())\r
222         \r
223         p = []\r
224         alt = []\r
225         tempDone = []\r
226         if globalProfileParser.has_section('profile'):\r
227                 for key in globalProfileParser.options('profile'):\r
228                         if key in tempOverride:\r
229                                 p.append(key + "=" + unicode(tempOverride[key]))\r
230                                 tempDone.append(key)\r
231                         else:\r
232                                 p.append(key + "=" + globalProfileParser.get('profile', key))\r
233         if globalProfileParser.has_section('alterations'):\r
234                 for key in globalProfileParser.options('alterations'):\r
235                         if key in tempOverride:\r
236                                 p.append(key + "=" + tempOverride[key])\r
237                                 tempDone.append(key)\r
238                         else:\r
239                                 alt.append(key + "=" + globalProfileParser.get('alterations', key))\r
240         for key in tempOverride:\r
241                 if key not in tempDone:\r
242                         p.append(key + "=" + unicode(tempOverride[key]))\r
243         ret = '\b'.join(p) + '\f' + '\b'.join(alt)\r
244         ret = base64.b64encode(zlib.compress(ret, 9))\r
245         return ret\r
246 \r
247 def getProfileSetting(name):\r
248         if name in tempOverride:\r
249                 return unicode(tempOverride[name])\r
250         #Check if we have a configuration file loaded, else load the default.\r
251         if not globals().has_key('globalProfileParser'):\r
252                 loadGlobalProfile(getDefaultProfilePath())\r
253         if not globalProfileParser.has_option('profile', name):\r
254                 if name in profileDefaultSettings:\r
255                         default = profileDefaultSettings[name]\r
256                 else:\r
257                         print("Missing default setting for: '" + name + "'")\r
258                         profileDefaultSettings[name] = ''\r
259                         default = ''\r
260                 if not globalProfileParser.has_section('profile'):\r
261                         globalProfileParser.add_section('profile')\r
262                 globalProfileParser.set('profile', name, str(default))\r
263                 #print(name + " not found in profile, so using default: " + str(default))\r
264                 return default\r
265         return globalProfileParser.get('profile', name)\r
266 \r
267 def getProfileSettingFloat(name):\r
268         try:\r
269                 return float(eval(getProfileSetting(name), {}, {}))\r
270         except (ValueError, SyntaxError):\r
271                 return 0.0\r
272 \r
273 def putProfileSetting(name, value):\r
274         #Check if we have a configuration file loaded, else load the default.\r
275         if not globals().has_key('globalProfileParser'):\r
276                 loadGlobalProfile(getDefaultProfilePath())\r
277         if not globalProfileParser.has_section('profile'):\r
278                 globalProfileParser.add_section('profile')\r
279         globalProfileParser.set('profile', name, str(value))\r
280 \r
281 def isProfileSetting(name):\r
282         if name in profileDefaultSettings:\r
283                 return True\r
284         return False\r
285 \r
286 ## Preferences functions\r
287 global globalPreferenceParser\r
288 globalPreferenceParser = None\r
289 \r
290 def getPreferencePath():\r
291         basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))\r
292         #If we have a frozen python install, we need to step out of the library.zip\r
293         if hasattr(sys, 'frozen'):\r
294                 basePath = os.path.normpath(os.path.join(basePath, ".."))\r
295         return os.path.normpath(os.path.join(basePath, "preferences.ini"))\r
296 \r
297 def getPreferenceFloat(name):\r
298         try:\r
299                 return float(eval(getPreference(name), {}, {}))\r
300         except (ValueError, SyntaxError):\r
301                 return 0.0\r
302 \r
303 def getPreference(name):\r
304         if name in tempOverride:\r
305                 return unicode(tempOverride[name])\r
306         global globalPreferenceParser\r
307         if globalPreferenceParser == None:\r
308                 globalPreferenceParser = ConfigParser.ConfigParser()\r
309                 globalPreferenceParser.read(getPreferencePath())\r
310         if not globalPreferenceParser.has_option('preference', name):\r
311                 if name in preferencesDefaultSettings:\r
312                         default = preferencesDefaultSettings[name]\r
313                 else:\r
314                         print("Missing default setting for: '" + name + "'")\r
315                         preferencesDefaultSettings[name] = ''\r
316                         default = ''\r
317                 if not globalPreferenceParser.has_section('preference'):\r
318                         globalPreferenceParser.add_section('preference')\r
319                 globalPreferenceParser.set('preference', name, str(default))\r
320                 #print(name + " not found in preferences, so using default: " + str(default))\r
321                 return default\r
322         return unicode(globalPreferenceParser.get('preference', name), "utf-8")\r
323 \r
324 def putPreference(name, value):\r
325         #Check if we have a configuration file loaded, else load the default.\r
326         global globalPreferenceParser\r
327         if globalPreferenceParser == None:\r
328                 globalPreferenceParser = ConfigParser.ConfigParser()\r
329                 globalPreferenceParser.read(getPreferencePath())\r
330         if not globalPreferenceParser.has_section('preference'):\r
331                 globalPreferenceParser.add_section('preference')\r
332         globalPreferenceParser.set('preference', name, unicode(value).encode("utf-8"))\r
333         globalPreferenceParser.write(open(getPreferencePath(), 'w'))\r
334 \r
335 def isPreference(name):\r
336         if name in preferencesDefaultSettings:\r
337                 return True\r
338         return False\r
339 \r
340 ## Temp overrides for multi-extruder slicing and the project planner.\r
341 tempOverride = {}\r
342 def setTempOverride(name, value):\r
343         tempOverride[name] = value\r
344 def resetTempOverride():\r
345         tempOverride.clear()\r
346 \r
347 #########################################################\r
348 ## Utility functions to calculate common profile values\r
349 #########################################################\r
350 def calculateEdgeWidth():\r
351         wallThickness = getProfileSettingFloat('wall_thickness')\r
352         nozzleSize = getProfileSettingFloat('nozzle_size')\r
353         \r
354         if wallThickness < nozzleSize:\r
355                 return wallThickness\r
356 \r
357         lineCount = int(wallThickness / nozzleSize)\r
358         lineWidth = wallThickness / lineCount\r
359         lineWidthAlt = wallThickness / (lineCount + 1)\r
360         if lineWidth > nozzleSize * 1.5:\r
361                 return lineWidthAlt\r
362         return lineWidth\r
363 \r
364 def calculateLineCount():\r
365         wallThickness = getProfileSettingFloat('wall_thickness')\r
366         nozzleSize = getProfileSettingFloat('nozzle_size')\r
367         \r
368         if wallThickness < nozzleSize:\r
369                 return 1\r
370 \r
371         lineCount = int(wallThickness / nozzleSize + 0.0001)\r
372         lineWidth = wallThickness / lineCount\r
373         lineWidthAlt = wallThickness / (lineCount + 1)\r
374         if lineWidth > nozzleSize * 1.5:\r
375                 return lineCount + 1\r
376         return lineCount\r
377 \r
378 def calculateSolidLayerCount():\r
379         layerHeight = getProfileSettingFloat('layer_height')\r
380         solidThickness = getProfileSettingFloat('solid_layer_thickness')\r
381         return int(math.ceil(solidThickness / layerHeight - 0.0001))\r
382 \r
383 #########################################################\r
384 ## Alteration file functions\r
385 #########################################################\r
386 def replaceTagMatch(m):\r
387         tag = m.group(0)[1:-1]\r
388         if tag == 'time':\r
389                 return time.strftime('%H:%M:%S')\r
390         if tag == 'date':\r
391                 return time.strftime('%d %b %Y')\r
392         if tag == 'day':\r
393                 return time.strftime('%a')\r
394         if tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:\r
395                 f = getProfileSettingFloat(tag) * 60\r
396         elif isProfileSetting(tag):\r
397                 f = getProfileSettingFloat(tag)\r
398         elif isPreference(tag):\r
399                 f = getProfileSettingFloat(tag)\r
400         else:\r
401                 return tag\r
402         if (f % 1) == 0:\r
403                 return str(int(f))\r
404         return str(f)\r
405 \r
406 ### Get aleration raw contents. (Used internally in Cura)\r
407 def getAlterationFile(filename):\r
408         #Check if we have a configuration file loaded, else load the default.\r
409         if not globals().has_key('globalProfileParser'):\r
410                 loadGlobalProfile(getDefaultProfilePath())\r
411         \r
412         if not globalProfileParser.has_option('alterations', filename):\r
413                 if filename in alterationDefault:\r
414                         default = alterationDefault[filename]\r
415                 else:\r
416                         print("Missing default alteration for: '" + filename + "'")\r
417                         alterationDefault[filename] = ''\r
418                         default = ''\r
419                 if not globalProfileParser.has_section('alterations'):\r
420                         globalProfileParser.add_section('alterations')\r
421                 #print("Using default for: %s" % (filename))\r
422                 globalProfileParser.set('alterations', filename, default)\r
423         return unicode(globalProfileParser.get('alterations', filename), "utf-8")\r
424 \r
425 def setAlterationFile(filename, value):\r
426         #Check if we have a configuration file loaded, else load the default.\r
427         if not globals().has_key('globalProfileParser'):\r
428                 loadGlobalProfile(getDefaultProfilePath())\r
429         if not globalProfileParser.has_section('alterations'):\r
430                 globalProfileParser.add_section('alterations')\r
431         globalProfileParser.set('alterations', filename, value.encode("utf-8"))\r
432         saveGlobalProfile(getDefaultProfilePath())\r
433 \r
434 ### Get the alteration file for output. (Used by Skeinforge)\r
435 def getAlterationFileContents(filename):\r
436         prefix = ''\r
437         alterationContents = getAlterationFile(filename)\r
438         if filename == 'start.gcode':\r
439                 #For the start code, hack the temperature and the steps per E value into it. So the temperature is reached before the start code extrusion.\r
440                 #We also set our steps per E here, if configured.\r
441                 eSteps = getPreferenceFloat('steps_per_e')\r
442                 if eSteps > 0:\r
443                         prefix += 'M92 E%f\n' % (eSteps)\r
444                 temp = getProfileSettingFloat('print_temperature')\r
445                 if temp > 0 and not '{print_temperature}' in alterationContents:\r
446                         prefix += 'M109 S%f\n' % (temp)\r
447         elif filename == 'replace.csv':\r
448                 #Always remove the extruder on/off M codes. These are no longer needed in 5D printing.\r
449                 prefix = 'M101\nM103\n'\r
450         \r
451         return unicode(prefix + re.sub("\{[^\}]*\}", replaceTagMatch, alterationContents).rstrip() + '\n').encode('utf-8')\r
452 \r