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