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
6 import os, traceback, math, re, zlib, base64, time, sys
\r
7 if sys.version_info[0] < 3:
\r
10 import configparser as ConfigParser
\r
12 #########################################################
\r
13 ## Default settings when none are found.
\r
14 #########################################################
\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
25 'print_speed': '50',
\r
26 'print_temperature': '0',
\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
44 'fan_speed_max': '100',
\r
45 'model_scale': '1.0',
\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
63 'enable_skin': 'False',
\r
64 'enable_raft': 'False',
\r
65 'cool_min_feedrate': '5',
\r
66 'bridge_speed': '100',
\r
68 'raft_base_material_amount': '100',
\r
69 'raft_interface_material_amount': '100',
\r
70 'bottom_thickness': '0.3',
\r
72 'enable_dwindle': 'False',
\r
73 'dwindle_pent_up_volume': '0.4',
\r
74 'dwindle_slowdown_volume': '5.0',
\r
76 'add_start_end_gcode': 'True',
\r
77 'gcode_extension': 'gcode',
\r
78 'alternative_center': '',
\r
82 alterationDefault = {
\r
83 #######################################################################################
\r
84 'start.gcode': """;Sliced at: {day} {date} {time}
\r
85 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
\r
87 G90 ;absolute positioning
\r
88 M107 ;start with the fan off
\r
90 G28 X0 Y0 ;move X/Y to min endstops
\r
91 G28 Z0 ;move Z to min endstops
\r
93 ; if your prints start too high, try changing the Z0.0 below
\r
94 ; to Z1.0 - the number after the Z is the actual, physical
\r
95 ; height of the nozzle in mm. This can take some messing around
\r
96 ; with to get just right...
\r
97 G92 X0 Y0 Z0 E0 ;reset software position to front/left/z=0.0
\r
98 G1 Z15.0 F{max_z_speed} ;move the platform down 15mm
\r
99 G92 E0 ;zero the extruded length
\r
101 G1 F200 E5 ;extrude 5mm of feed stock
\r
102 G1 F200 E3.5 ;reverse feed stock by 1.5mm
\r
103 G92 E0 ;zero the extruded length again
\r
105 ;go to the middle of the platform, and move to Z=0 before starting the print.
\r
106 G1 X{machine_center_x} Y{machine_center_y} F{travel_speed}
\r
107 G1 Z0.0 F{max_z_speed}
\r
109 #######################################################################################
\r
110 'end.gcode': """;End GCode
\r
111 M104 S0 ;extruder heat off
\r
112 G91 ;relative positioning
\r
113 G1 Z+10 E-5 F{max_z_speed} ;move Z up a bit and retract filament by 5mm
\r
114 G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way
\r
116 G90 ;absolute positioning
\r
118 #######################################################################################
\r
119 'support_start.gcode': '',
\r
120 'support_end.gcode': '',
\r
121 'cool_start.gcode': '',
\r
122 'cool_end.gcode': '',
\r
124 #######################################################################################
\r
125 '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 G1 Z{clear_z} E-5 F{max_z_speed}
\r
129 G1 X{machine_center_x} Y{machine_center_y} F{travel_speed}
\r
132 G1 Z0 F{max_z_speed}
\r
134 #######################################################################################
\r
135 'switchExtruder.gcode': """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
\r
143 preferencesDefaultSettings = {
\r
144 'wizardDone': 'False',
\r
145 'startMode': 'Simple',
\r
147 'machine_width': '205',
\r
148 'machine_depth': '205',
\r
149 'machine_height': '200',
\r
150 'machine_type': 'unknown',
\r
151 'extruder_amount': '1',
\r
152 'extruder_offset_x1': '-22.0',
\r
153 'extruder_offset_y1': '0.0',
\r
154 'extruder_offset_x2': '0.0',
\r
155 'extruder_offset_y2': '0.0',
\r
156 'extruder_offset_x3': '0.0',
\r
157 'extruder_offset_y3': '0.0',
\r
158 'filament_density': '1300',
\r
159 'steps_per_e': '0',
\r
160 'serial_port': 'AUTO',
\r
161 'serial_baud': 'AUTO',
\r
162 'slicer': 'Cura (Skeinforge based)',
\r
163 'save_profile': 'False',
\r
164 'filament_cost_kg': '0',
\r
165 'filament_cost_meter': '0',
\r
167 'sdshortnames': 'True',
\r
169 'extruder_head_size_min_x': '70.0',
\r
170 'extruder_head_size_min_y': '18.0',
\r
171 'extruder_head_size_max_x': '18.0',
\r
172 'extruder_head_size_max_y': '35.0',
\r
175 #########################################################
\r
176 ## Profile and preferences functions
\r
177 #########################################################
\r
179 ## Profile functions
\r
180 def getDefaultProfilePath():
\r
181 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
\r
182 #If we have a frozen python install, we need to step out of the library.zip
\r
183 if hasattr(sys, 'frozen'):
\r
184 basePath = os.path.normpath(os.path.join(basePath, ".."))
\r
185 return os.path.normpath(os.path.join(basePath, "current_profile.ini"))
\r
187 def loadGlobalProfile(filename):
\r
188 #Read a configuration file as global config
\r
189 global globalProfileParser
\r
190 globalProfileParser = ConfigParser.ConfigParser()
\r
191 globalProfileParser.read(filename)
\r
193 def resetGlobalProfile():
\r
194 #Read a configuration file as global config
\r
195 global globalProfileParser
\r
196 globalProfileParser = ConfigParser.ConfigParser()
\r
198 def saveGlobalProfile(filename):
\r
199 #Save the current profile to an ini file
\r
200 globalProfileParser.write(open(filename, 'w'))
\r
202 def loadGlobalProfileFromString(options):
\r
203 global globalProfileParser
\r
204 globalProfileParser = ConfigParser.ConfigParser()
\r
205 globalProfileParser.add_section('profile')
\r
206 globalProfileParser.add_section('alterations')
\r
207 options = base64.b64decode(options)
\r
208 options = zlib.decompress(options)
\r
209 (profileOpts, alt) = options.split('\f', 1)
\r
210 for option in profileOpts.split('\b'):
\r
211 if len(option) > 0:
\r
212 (key, value) = option.split('=', 1)
\r
213 globalProfileParser.set('profile', key, value)
\r
214 for option in alt.split('\b'):
\r
215 if len(option) > 0:
\r
216 (key, value) = option.split('=', 1)
\r
217 globalProfileParser.set('alterations', key, value)
\r
219 def getGlobalProfileString():
\r
220 global globalProfileParser
\r
221 if not globals().has_key('globalProfileParser'):
\r
222 loadGlobalProfile(getDefaultProfilePath())
\r
227 if globalProfileParser.has_section('profile'):
\r
228 for key in globalProfileParser.options('profile'):
\r
229 if key in tempOverride:
\r
230 p.append(key + "=" + unicode(tempOverride[key]))
\r
231 tempDone.append(key)
\r
233 p.append(key + "=" + globalProfileParser.get('profile', key))
\r
234 if globalProfileParser.has_section('alterations'):
\r
235 for key in globalProfileParser.options('alterations'):
\r
236 if key in tempOverride:
\r
237 p.append(key + "=" + tempOverride[key])
\r
238 tempDone.append(key)
\r
240 alt.append(key + "=" + globalProfileParser.get('alterations', key))
\r
241 for key in tempOverride:
\r
242 if key not in tempDone:
\r
243 p.append(key + "=" + unicode(tempOverride[key]))
\r
244 ret = '\b'.join(p) + '\f' + '\b'.join(alt)
\r
245 ret = base64.b64encode(zlib.compress(ret, 9))
\r
248 def getProfileSetting(name):
\r
249 if name in tempOverride:
\r
250 return unicode(tempOverride[name])
\r
251 #Check if we have a configuration file loaded, else load the default.
\r
252 if not globals().has_key('globalProfileParser'):
\r
253 loadGlobalProfile(getDefaultProfilePath())
\r
254 if not globalProfileParser.has_option('profile', name):
\r
255 if name in profileDefaultSettings:
\r
256 default = profileDefaultSettings[name]
\r
258 print("Missing default setting for: '" + name + "'")
\r
259 profileDefaultSettings[name] = ''
\r
261 if not globalProfileParser.has_section('profile'):
\r
262 globalProfileParser.add_section('profile')
\r
263 globalProfileParser.set('profile', name, str(default))
\r
264 #print(name + " not found in profile, so using default: " + str(default))
\r
266 return globalProfileParser.get('profile', name)
\r
268 def getProfileSettingFloat(name):
\r
270 return float(eval(getProfileSetting(name), {}, {}))
\r
271 except (ValueError, SyntaxError, TypeError):
\r
274 def putProfileSetting(name, value):
\r
275 #Check if we have a configuration file loaded, else load the default.
\r
276 if not globals().has_key('globalProfileParser'):
\r
277 loadGlobalProfile(getDefaultProfilePath())
\r
278 if not globalProfileParser.has_section('profile'):
\r
279 globalProfileParser.add_section('profile')
\r
280 globalProfileParser.set('profile', name, str(value))
\r
282 def isProfileSetting(name):
\r
283 if name in profileDefaultSettings:
\r
287 ## Preferences functions
\r
288 global globalPreferenceParser
\r
289 globalPreferenceParser = None
\r
291 def getPreferencePath():
\r
292 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
\r
293 #If we have a frozen python install, we need to step out of the library.zip
\r
294 if hasattr(sys, 'frozen'):
\r
295 basePath = os.path.normpath(os.path.join(basePath, ".."))
\r
296 return os.path.normpath(os.path.join(basePath, "preferences.ini"))
\r
298 def getPreferenceFloat(name):
\r
300 return float(eval(getPreference(name), {}, {}))
\r
301 except (ValueError, SyntaxError, TypeError):
\r
304 def getPreference(name):
\r
305 if name in tempOverride:
\r
306 return unicode(tempOverride[name])
\r
307 global globalPreferenceParser
\r
308 if globalPreferenceParser == None:
\r
309 globalPreferenceParser = ConfigParser.ConfigParser()
\r
310 globalPreferenceParser.read(getPreferencePath())
\r
311 if not globalPreferenceParser.has_option('preference', name):
\r
312 if name in preferencesDefaultSettings:
\r
313 default = preferencesDefaultSettings[name]
\r
315 print("Missing default setting for: '" + name + "'")
\r
316 preferencesDefaultSettings[name] = ''
\r
318 if not globalPreferenceParser.has_section('preference'):
\r
319 globalPreferenceParser.add_section('preference')
\r
320 globalPreferenceParser.set('preference', name, str(default))
\r
321 #print(name + " not found in preferences, so using default: " + str(default))
\r
323 return unicode(globalPreferenceParser.get('preference', name), "utf-8")
\r
325 def putPreference(name, value):
\r
326 #Check if we have a configuration file loaded, else load the default.
\r
327 global globalPreferenceParser
\r
328 if globalPreferenceParser == None:
\r
329 globalPreferenceParser = ConfigParser.ConfigParser()
\r
330 globalPreferenceParser.read(getPreferencePath())
\r
331 if not globalPreferenceParser.has_section('preference'):
\r
332 globalPreferenceParser.add_section('preference')
\r
333 globalPreferenceParser.set('preference', name, unicode(value).encode("utf-8"))
\r
334 globalPreferenceParser.write(open(getPreferencePath(), 'w'))
\r
336 def isPreference(name):
\r
337 if name in preferencesDefaultSettings:
\r
341 ## Temp overrides for multi-extruder slicing and the project planner.
\r
343 def setTempOverride(name, value):
\r
344 tempOverride[name] = value
\r
345 def resetTempOverride():
\r
346 tempOverride.clear()
\r
348 #########################################################
\r
349 ## Utility functions to calculate common profile values
\r
350 #########################################################
\r
351 def calculateEdgeWidth():
\r
352 wallThickness = getProfileSettingFloat('wall_thickness')
\r
353 nozzleSize = getProfileSettingFloat('nozzle_size')
\r
355 if wallThickness < nozzleSize:
\r
356 return wallThickness
\r
358 lineCount = int(wallThickness / nozzleSize)
\r
359 lineWidth = wallThickness / lineCount
\r
360 lineWidthAlt = wallThickness / (lineCount + 1)
\r
361 if lineWidth > nozzleSize * 1.5:
\r
362 return lineWidthAlt
\r
365 def calculateLineCount():
\r
366 wallThickness = getProfileSettingFloat('wall_thickness')
\r
367 nozzleSize = getProfileSettingFloat('nozzle_size')
\r
369 if wallThickness < nozzleSize:
\r
372 lineCount = int(wallThickness / nozzleSize + 0.0001)
\r
373 lineWidth = wallThickness / lineCount
\r
374 lineWidthAlt = wallThickness / (lineCount + 1)
\r
375 if lineWidth > nozzleSize * 1.5:
\r
376 return lineCount + 1
\r
379 def calculateSolidLayerCount():
\r
380 layerHeight = getProfileSettingFloat('layer_height')
\r
381 solidThickness = getProfileSettingFloat('solid_layer_thickness')
\r
382 return int(math.ceil(solidThickness / layerHeight - 0.0001))
\r
384 #########################################################
\r
385 ## Alteration file functions
\r
386 #########################################################
\r
387 def replaceTagMatch(m):
\r
388 tag = m.group(0)[1:-1]
\r
390 return time.strftime('%H:%M:%S')
\r
392 return time.strftime('%d %b %Y')
\r
394 return time.strftime('%a')
\r
395 if tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
\r
396 f = getProfileSettingFloat(tag) * 60
\r
397 elif isProfileSetting(tag):
\r
398 f = getProfileSettingFloat(tag)
\r
399 elif isPreference(tag):
\r
400 f = getProfileSettingFloat(tag)
\r
407 ### Get aleration raw contents. (Used internally in Cura)
\r
408 def getAlterationFile(filename):
\r
409 #Check if we have a configuration file loaded, else load the default.
\r
410 if not globals().has_key('globalProfileParser'):
\r
411 loadGlobalProfile(getDefaultProfilePath())
\r
413 if not globalProfileParser.has_option('alterations', filename):
\r
414 if filename in alterationDefault:
\r
415 default = alterationDefault[filename]
\r
417 print("Missing default alteration for: '" + filename + "'")
\r
418 alterationDefault[filename] = ''
\r
420 if not globalProfileParser.has_section('alterations'):
\r
421 globalProfileParser.add_section('alterations')
\r
422 #print("Using default for: %s" % (filename))
\r
423 globalProfileParser.set('alterations', filename, default)
\r
424 return unicode(globalProfileParser.get('alterations', filename), "utf-8")
\r
426 def setAlterationFile(filename, value):
\r
427 #Check if we have a configuration file loaded, else load the default.
\r
428 if not globals().has_key('globalProfileParser'):
\r
429 loadGlobalProfile(getDefaultProfilePath())
\r
430 if not globalProfileParser.has_section('alterations'):
\r
431 globalProfileParser.add_section('alterations')
\r
432 globalProfileParser.set('alterations', filename, value.encode("utf-8"))
\r
433 saveGlobalProfile(getDefaultProfilePath())
\r
435 ### Get the alteration file for output. (Used by Skeinforge)
\r
436 def getAlterationFileContents(filename):
\r
439 alterationContents = getAlterationFile(filename)
\r
440 if filename == 'start.gcode':
\r
441 #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
442 #We also set our steps per E here, if configured.
\r
443 eSteps = getPreferenceFloat('steps_per_e')
\r
445 prefix += 'M92 E%f\n' % (eSteps)
\r
446 temp = getProfileSettingFloat('print_temperature')
\r
447 if temp > 0 and not '{print_temperature}' in alterationContents:
\r
448 prefix += 'M109 S%f\n' % (temp)
\r
449 elif filename == 'end.gcode':
\r
450 #Append the profile string to the end of the GCode, so we can load it from the GCode file later.
\r
451 postfix = ';CURA_PROFILE_STRING:%s\n' % (getGlobalProfileString())
\r
452 elif filename == 'replace.csv':
\r
453 #Always remove the extruder on/off M codes. These are no longer needed in 5D printing.
\r
454 prefix = 'M101\nM103\n'
\r
456 return unicode(prefix + re.sub("\{[^\}]*\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).encode('utf-8')
\r