OctoPrint/Cura/util/profile.py

648 lines
23 KiB
Python

from __future__ import absolute_import
from __future__ import division
#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.
import __init__
import os, traceback, math, re, zlib, base64, time, sys, platform, glob, string, stat
import cPickle as pickle
if sys.version_info[0] < 3:
import ConfigParser
else:
import configparser as ConfigParser
from util import version
#########################################################
## Default settings when none are found.
#########################################################
#Single place to store the defaults, so we have a consistent set of default settings.
profileDefaultSettings = {
'nozzle_size': '0.4',
'layer_height': '0.2',
'wall_thickness': '0.8',
'solid_layer_thickness': '0.6',
'fill_density': '20',
'skirt_line_count': '1',
'skirt_gap': '3.0',
'print_speed': '50',
'print_temperature': '220',
'print_bed_temperature': '70',
'support': 'None',
'filament_diameter': '2.89',
'filament_density': '1.00',
'retraction_min_travel': '5.0',
'retraction_enable': 'False',
'retraction_speed': '40.0',
'retraction_amount': '4.5',
'retraction_extra': '0.0',
'retract_on_jumps_only': 'True',
'travel_speed': '150',
'max_z_speed': '3.0',
'bottom_layer_speed': '20',
'cool_min_layer_time': '5',
'fan_enabled': 'True',
'fan_layer': '1',
'fan_speed': '100',
'fan_speed_max': '100',
'model_scale': '1.0',
'flip_x': 'False',
'flip_y': 'False',
'flip_z': 'False',
'swap_xz': 'False',
'swap_yz': 'False',
'model_rotate_base': '0',
'model_multiply_x': '1',
'model_multiply_y': '1',
'extra_base_wall_thickness': '0.0',
'sequence': 'Loops > Perimeter > Infill',
'force_first_layer_sequence': 'True',
'infill_type': 'Line',
'solid_top': 'True',
'fill_overlap': '15',
'support_rate': '50',
'support_distance': '0.5',
'support_dual_extrusion': 'False',
'joris': 'False',
'enable_skin': 'False',
'enable_raft': 'False',
'cool_min_feedrate': '10',
'bridge_speed': '100',
'raft_margin': '5',
'raft_base_material_amount': '100',
'raft_interface_material_amount': '100',
'bottom_thickness': '0.3',
'hop_on_move': 'False',
'plugin_config': '',
'add_start_end_gcode': 'True',
'gcode_extension': 'gcode',
'alternative_center': '',
'clear_z': '0.0',
'extruder': '0',
}
alterationDefault = {
#######################################################################################
'start.gcode': """;Sliced {filename} at: {day} {date} {time}
;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
;Print time: {print_time}
;Filament used: {filament_amount}m {filament_weight}g
;Filament cost: {filament_cost}
G21 ;metric values
G90 ;absolute positioning
M107 ;start with the fan off
G28 X0 Y0 ;move X/Y to min endstops
G28 Z0 ;move Z to min endstops
G92 X0 Y0 Z0 E0 ;reset software position to front/left/z=0.0
G1 Z15.0 F{max_z_speed} ;move the platform down 15mm
G92 E0 ;zero the extruded length
G1 F200 E3 ;extrude 3mm of feed stock
G92 E0 ;zero the extruded length again
G1 F{travel_speed}
""",
#######################################################################################
'end.gcode': """;End GCode
M104 S0 ;extruder heater off
M140 S0 ;heated bed heater off (if you have it)
G91 ;relative positioning
G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way
M84 ;steppers off
G90 ;absolute positioning
""",
#######################################################################################
'support_start.gcode': '',
'support_end.gcode': '',
'cool_start.gcode': '',
'cool_end.gcode': '',
'replace.csv': '',
#######################################################################################
'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.
G92 E0
G91 ;relative positioning
G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
G1 Z+0.5 E-5 F{travel_speed} ;move Z up a bit and retract filament even more
G90 ;absolute positioning
G1 Z{clear_z} F{max_z_speed}
G92 E0
G1 X{object_center_x} Y{object_center_x} F{travel_speed}
G1 F200 E6
G92 E0
""",
#######################################################################################
'switchExtruder.gcode': """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
G92 E0
G1 E-5 F5000
G92 E0
T{extruder}
G1 E5 F5000
G92 E0
""",
}
preferencesDefaultSettings = {
'startMode': 'Simple',
'lastFile': os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'example', 'UltimakerRobot_support.stl')),
'machine_width': '205',
'machine_depth': '205',
'machine_height': '200',
'machine_type': 'unknown',
'ultimaker_extruder_upgrade': 'False',
'has_heated_bed': 'False',
'extruder_amount': '1',
'extruder_offset_x1': '-22.0',
'extruder_offset_y1': '0.0',
'extruder_offset_x2': '0.0',
'extruder_offset_y2': '0.0',
'extruder_offset_x3': '0.0',
'extruder_offset_y3': '0.0',
'filament_density': '1300',
'steps_per_e': '0',
'serial_port': 'AUTO',
'serial_port_auto': '',
'serial_baud': 'AUTO',
'serial_baud_auto': '',
'slicer': 'Cura (Skeinforge based)',
'save_profile': 'False',
'filament_cost_kg': '0',
'filament_cost_meter': '0',
'sdpath': '',
'sdshortnames': 'True',
'extruder_head_size_min_x': '70.0',
'extruder_head_size_min_y': '18.0',
'extruder_head_size_max_x': '18.0',
'extruder_head_size_max_y': '35.0',
'extruder_head_size_height': '80.0',
'model_colour': '#72CB30',
'model_colour2': '#CB3030',
'model_colour3': '#DDD93C',
'model_colour4': '#4550D3',
}
#########################################################
## Profile and preferences functions
#########################################################
## Profile functions
def getDefaultProfilePath():
if platform.system() == "Windows":
basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
#If we have a frozen python install, we need to step out of the library.zip
if hasattr(sys, 'frozen'):
basePath = os.path.normpath(os.path.join(basePath, ".."))
else:
basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
if not os.path.isdir(basePath):
os.makedirs(basePath)
return os.path.join(basePath, 'current_profile.ini')
def loadGlobalProfile(filename):
#Read a configuration file as global config
global globalProfileParser
globalProfileParser = ConfigParser.ConfigParser()
globalProfileParser.read(filename)
def resetGlobalProfile():
#Read a configuration file as global config
global globalProfileParser
globalProfileParser = ConfigParser.ConfigParser()
if getPreference('machine_type') == 'ultimaker':
putProfileSetting('nozzle_size', '0.4')
if getPreference('ultimaker_extruder_upgrade') == 'True':
putProfileSetting('retraction_enable', 'True')
else:
putProfileSetting('nozzle_size', '0.5')
def saveGlobalProfile(filename):
#Save the current profile to an ini file
globalProfileParser.write(open(filename, 'w'))
def loadGlobalProfileFromString(options):
global globalProfileParser
globalProfileParser = ConfigParser.ConfigParser()
globalProfileParser.add_section('profile')
globalProfileParser.add_section('alterations')
options = base64.b64decode(options)
options = zlib.decompress(options)
(profileOpts, alt) = options.split('\f', 1)
for option in profileOpts.split('\b'):
if len(option) > 0:
(key, value) = option.split('=', 1)
globalProfileParser.set('profile', key, value)
for option in alt.split('\b'):
if len(option) > 0:
(key, value) = option.split('=', 1)
globalProfileParser.set('alterations', key, value)
def getGlobalProfileString():
global globalProfileParser
if not globals().has_key('globalProfileParser'):
loadGlobalProfile(getDefaultProfilePath())
p = []
alt = []
tempDone = []
if globalProfileParser.has_section('profile'):
for key in globalProfileParser.options('profile'):
if key in tempOverride:
p.append(key + "=" + tempOverride[key])
tempDone.append(key)
else:
p.append(key + "=" + globalProfileParser.get('profile', key))
if globalProfileParser.has_section('alterations'):
for key in globalProfileParser.options('alterations'):
if key in tempOverride:
p.append(key + "=" + tempOverride[key])
tempDone.append(key)
else:
alt.append(key + "=" + globalProfileParser.get('alterations', key))
for key in tempOverride:
if key not in tempDone:
p.append(key + "=" + tempOverride[key])
ret = '\b'.join(p) + '\f' + '\b'.join(alt)
ret = base64.b64encode(zlib.compress(ret, 9))
return ret
def getProfileSetting(name):
if name in tempOverride:
return unicode(tempOverride[name], "utf-8")
#Check if we have a configuration file loaded, else load the default.
if not globals().has_key('globalProfileParser'):
loadGlobalProfile(getDefaultProfilePath())
if not globalProfileParser.has_option('profile', name):
if name in profileDefaultSettings:
default = profileDefaultSettings[name]
else:
print("Missing default setting for: '" + name + "'")
profileDefaultSettings[name] = ''
default = ''
if not globalProfileParser.has_section('profile'):
globalProfileParser.add_section('profile')
globalProfileParser.set('profile', name, str(default))
#print(name + " not found in profile, so using default: " + str(default))
return default
return globalProfileParser.get('profile', name)
def getProfileSettingFloat(name):
try:
setting = getProfileSetting(name).replace(',', '.')
return float(eval(setting, {}, {}))
except (ValueError, SyntaxError, TypeError):
return 0.0
def putProfileSetting(name, value):
#Check if we have a configuration file loaded, else load the default.
if not globals().has_key('globalProfileParser'):
loadGlobalProfile(getDefaultProfilePath())
if not globalProfileParser.has_section('profile'):
globalProfileParser.add_section('profile')
globalProfileParser.set('profile', name, str(value))
def isProfileSetting(name):
if name in profileDefaultSettings:
return True
return False
## Preferences functions
global globalPreferenceParser
globalPreferenceParser = None
def getPreferencePath():
if platform.system() == "Windows":
basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
#If we have a frozen python install, we need to step out of the library.zip
if hasattr(sys, 'frozen'):
basePath = os.path.normpath(os.path.join(basePath, ".."))
else:
basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
if not os.path.isdir(basePath):
os.makedirs(basePath)
return os.path.join(basePath, 'preferences.ini')
def getPreferenceFloat(name):
try:
setting = getPreference(name).replace(',', '.')
return float(eval(setting, {}, {}))
except (ValueError, SyntaxError, TypeError):
return 0.0
def getPreferenceColour(name):
colorString = getPreference(name)
return [float(int(colorString[1:3], 16)) / 255, float(int(colorString[3:5], 16)) / 255, float(int(colorString[5:7], 16)) / 255, 1.0]
def getPreference(name):
if name in tempOverride:
return unicode(tempOverride[name])
global globalPreferenceParser
if globalPreferenceParser == None:
globalPreferenceParser = ConfigParser.ConfigParser()
globalPreferenceParser.read(getPreferencePath())
if not globalPreferenceParser.has_option('preference', name):
if name in preferencesDefaultSettings:
default = preferencesDefaultSettings[name]
else:
print("Missing default setting for: '" + name + "'")
preferencesDefaultSettings[name] = ''
default = ''
if not globalPreferenceParser.has_section('preference'):
globalPreferenceParser.add_section('preference')
globalPreferenceParser.set('preference', name, str(default))
#print(name + " not found in preferences, so using default: " + str(default))
return default
return unicode(globalPreferenceParser.get('preference', name), "utf-8")
def putPreference(name, value):
#Check if we have a configuration file loaded, else load the default.
global globalPreferenceParser
if globalPreferenceParser == None:
globalPreferenceParser = ConfigParser.ConfigParser()
globalPreferenceParser.read(getPreferencePath())
if not globalPreferenceParser.has_section('preference'):
globalPreferenceParser.add_section('preference')
globalPreferenceParser.set('preference', name, unicode(value).encode("utf-8"))
globalPreferenceParser.write(open(getPreferencePath(), 'w'))
def isPreference(name):
if name in preferencesDefaultSettings:
return True
return False
## Temp overrides for multi-extruder slicing and the project planner.
tempOverride = {}
def setTempOverride(name, value):
tempOverride[name] = unicode(value).encode("utf-8")
def clearTempOverride(name):
del tempOverride[name]
def resetTempOverride():
tempOverride.clear()
#########################################################
## Utility functions to calculate common profile values
#########################################################
def calculateEdgeWidth():
wallThickness = getProfileSettingFloat('wall_thickness')
nozzleSize = getProfileSettingFloat('nozzle_size')
if wallThickness < nozzleSize:
return wallThickness
lineCount = int(wallThickness / nozzleSize)
lineWidth = wallThickness / lineCount
lineWidthAlt = wallThickness / (lineCount + 1)
if lineWidth > nozzleSize * 1.5:
return lineWidthAlt
return lineWidth
def calculateLineCount():
wallThickness = getProfileSettingFloat('wall_thickness')
nozzleSize = getProfileSettingFloat('nozzle_size')
if wallThickness < nozzleSize:
return 1
lineCount = int(wallThickness / nozzleSize + 0.0001)
lineWidth = wallThickness / lineCount
lineWidthAlt = wallThickness / (lineCount + 1)
if lineWidth > nozzleSize * 1.5:
return lineCount + 1
return lineCount
def calculateSolidLayerCount():
layerHeight = getProfileSettingFloat('layer_height')
solidThickness = getProfileSettingFloat('solid_layer_thickness')
return int(math.ceil(solidThickness / layerHeight - 0.0001))
#########################################################
## Alteration file functions
#########################################################
def replaceTagMatch(m):
pre = m.group(1)
tag = m.group(2)
if tag == 'time':
return pre + time.strftime('%H:%M:%S')
if tag == 'date':
return pre + time.strftime('%d %b %Y')
if tag == 'day':
return pre + time.strftime('%a')
if tag == 'print_time':
return pre + '#P_TIME#'
if tag == 'filament_amount':
return pre + '#F_AMNT#'
if tag == 'filament_weight':
return pre + '#F_WGHT#'
if tag == 'filament_cost':
return pre + '#F_COST#'
if pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
f = getProfileSettingFloat(tag) * 60
elif isProfileSetting(tag):
f = getProfileSettingFloat(tag)
elif isPreference(tag):
f = getProfileSettingFloat(tag)
else:
return '%s?%s?' % (pre, tag)
if (f % 1) == 0:
return pre + str(int(f))
return pre + str(f)
def replaceGCodeTags(filename, gcodeInt):
f = open(filename, 'r+')
data = f.read(2048)
data = data.replace('#P_TIME#', ('%5d:%02d' % (int(gcodeInt.totalMoveTimeMinute / 60), int(gcodeInt.totalMoveTimeMinute % 60)))[-8:])
data = data.replace('#F_AMNT#', ('%8.2f' % (gcodeInt.extrusionAmount / 1000))[-8:])
data = data.replace('#F_WGHT#', ('%8.2f' % (gcodeInt.calculateWeight() * 1000))[-8:])
cost = gcodeInt.calculateCost()
if cost == False:
cost = 'Unknown'
data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
f.seek(0)
f.write(data)
f.close()
### Get aleration raw contents. (Used internally in Cura)
def getAlterationFile(filename):
#Check if we have a configuration file loaded, else load the default.
if not globals().has_key('globalProfileParser'):
loadGlobalProfile(getDefaultProfilePath())
if not globalProfileParser.has_option('alterations', filename):
if filename in alterationDefault:
default = alterationDefault[filename]
else:
print("Missing default alteration for: '" + filename + "'")
alterationDefault[filename] = ''
default = ''
if not globalProfileParser.has_section('alterations'):
globalProfileParser.add_section('alterations')
#print("Using default for: %s" % (filename))
globalProfileParser.set('alterations', filename, default)
return unicode(globalProfileParser.get('alterations', filename), "utf-8")
def setAlterationFile(filename, value):
#Check if we have a configuration file loaded, else load the default.
if not globals().has_key('globalProfileParser'):
loadGlobalProfile(getDefaultProfilePath())
if not globalProfileParser.has_section('alterations'):
globalProfileParser.add_section('alterations')
globalProfileParser.set('alterations', filename, value.encode("utf-8"))
saveGlobalProfile(getDefaultProfilePath())
### Get the alteration file for output. (Used by Skeinforge)
def getAlterationFileContents(filename):
prefix = ''
postfix = ''
alterationContents = getAlterationFile(filename)
if filename == 'start.gcode':
#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.
#We also set our steps per E here, if configured.
eSteps = getPreferenceFloat('steps_per_e')
if eSteps > 0:
prefix += 'M92 E%f\n' % (eSteps)
temp = getProfileSettingFloat('print_temperature')
bedTemp = 0
if getPreference('has_heated_bed') == 'True':
bedTemp = getProfileSettingFloat('print_bed_temperature')
if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
prefix += 'M140 S%f\n' % (bedTemp)
if temp > 0 and not '{print_temperature}' in alterationContents:
prefix += 'M109 S%f\n' % (temp)
if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
prefix += 'M190 S%f\n' % (bedTemp)
elif filename == 'end.gcode':
#Append the profile string to the end of the GCode, so we can load it from the GCode file later.
postfix = ';CURA_PROFILE_STRING:%s\n' % (getGlobalProfileString())
elif filename == 'replace.csv':
#Always remove the extruder on/off M codes. These are no longer needed in 5D printing.
prefix = 'M101\nM103\n'
elif filename == 'support_start.gcode' or filename == 'support_end.gcode':
#Add support start/end code
if getProfileSetting('support_dual_extrusion') == 'True' and int(getPreference('extruder_amount')) > 1:
if filename == 'support_start.gcode':
setTempOverride('extruder', '1')
else:
setTempOverride('extruder', '0')
alterationContents = getAlterationFileContents('switchExtruder.gcode')
clearTempOverride('extruder')
else:
alterationContents = ''
return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8')
###### PLUGIN #####
def getPluginConfig():
try:
return pickle.loads(getProfileSetting('plugin_config'))
except:
return []
def setPluginConfig(config):
putProfileSetting('plugin_config', pickle.dumps(config))
def getPluginBasePaths():
ret = []
if platform.system() != "Windows":
ret.append(os.path.expanduser('~/.cura/plugins/'))
ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
return ret
def getPluginList():
ret = []
for basePath in getPluginBasePaths():
for filename in glob.glob(os.path.join(basePath, '*.py')):
filename = os.path.basename(filename)
if filename.startswith('_'):
continue
with open(os.path.join(basePath, filename), "r") as f:
item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
for line in f:
line = line.strip()
if not line.startswith('#'):
break
line = line[1:].split(':', 1)
if len(line) != 2:
continue
if line[0].upper() == 'NAME':
item['name'] = line[1].strip()
elif line[0].upper() == 'INFO':
item['info'] = line[1].strip()
elif line[0].upper() == 'TYPE':
item['type'] = line[1].strip()
elif line[0].upper() == 'DEPEND':
pass
elif line[0].upper() == 'PARAM':
m = re.match('([a-zA-Z]*)\(([a-zA-Z_]*)(?:\:([^\)]*))?\) +(.*)', line[1].strip())
if m != None:
item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
else:
print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
if item['name'] != None and item['type'] == 'postprocess':
ret.append(item)
return ret
def runPostProcessingPlugins(gcodefilename):
pluginConfigList = getPluginConfig()
pluginList = getPluginList()
for pluginConfig in pluginConfigList:
plugin = None
for pluginTest in pluginList:
if pluginTest['filename'] == pluginConfig['filename']:
plugin = pluginTest
if plugin == None:
continue
pythonFile = None
for basePath in getPluginBasePaths():
testFilename = os.path.join(basePath, pluginConfig['filename'])
if os.path.isfile(testFilename):
pythonFile = testFilename
if pythonFile == None:
continue
locals = {'filename': gcodefilename}
for param in plugin['params']:
value = param['default']
if param['name'] in pluginConfig['params']:
value = pluginConfig['params'][param['name']]
if param['type'] == 'float':
try:
value = float(value)
except:
value = float(param['default'])
locals[param['name']] = value
try:
execfile(pythonFile, locals)
except:
locationInfo = traceback.extract_tb(sys.exc_info()[2])[-1]
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])
return None
def getSDcardDrives():
drives = ['']
if platform.system() == "Windows":
from ctypes import windll
bitmask = windll.kernel32.GetLogicalDrives()
for letter in string.uppercase:
if bitmask & 1:
drives.append(letter + ':/')
bitmask >>= 1
if platform.system() == "Darwin":
drives = []
for volume in glob.glob('/Volumes/*'):
if stat.S_ISLNK(os.lstat(volume).st_mode):
continue
drives.append(volume)
return drives