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