Established settings mechanism

Allows reading and writing default serial port and baudrate (this also is available via the web interface) and setting the host and port on which the server should listen. Might allow persisting more options in the future.

The configuration file is stored in ~/.printerwebui/config.ini under Linux, in %APPDATA%/PrinterWebUI/config.ini under Windows and should be stored in ~/Library/Application Support/config.ini under MacOS X

Closes #1
master
Gina Häußge 2013-01-01 21:04:00 +01:00
parent a0cac835b5
commit a85ea69fb0
13 changed files with 262 additions and 815 deletions

View File

@ -24,6 +24,8 @@ installed using `pip`:
pip install -r requirements.txt
Printer WebUI currently only supports Python 2.7.
Usage
-----
@ -38,6 +40,27 @@ to only listen on the local interface on port 8080, the command line would be
python -m printer_webui.server --host=127.0.0.1 --port=8080
Alternatively, the host and port on which to bind can be defined via the configuration.
Configuration
-------------
The config-file for Printer WebUI is expected at `~/.printerwebui/config.ini` for Linux, at `%APPDATA%/PrinterWebUI/config.ini`
for Windows and at `~/Library/Application Support/config.ini` for MacOS X.
The following example config should explain the available options:
[serial]
# use the following option to define the default serial port, defaults to unset (= AUTO)
port = /dev/ttyACM0
# use the following option to define the default baudrate, defaults to unset (= AUTO)
baudrate = 115200
[server]
# use this option to define the host to which to bind the server, defaults to "0.0.0.0" (= all interfaces)
host = 0.0.0.0
# use this option to define the port to which to bind the server, defaults to 5000
port = 5000
Credits
-------

View File

@ -1,5 +1,6 @@
# coding=utf-8
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import time
import os
@ -8,23 +9,18 @@ import datetime
import printer_webui.util.comm as comm
from printer_webui.util import gcodeInterpreter
from printer_webui.util import profile
from printer_webui.settings import settings
def getConnectionOptions():
"""
Retrieves the available ports, baudrates, prefered port and baudrate for connecting to the printer.
"""
baudratePref = None
try:
baudratePref = int(profile.getPreference('serial_baud_auto'))
except ValueError:
pass
return {
"ports": comm.serialList(),
"baudrates": comm.baudrateList(),
"portPreference": profile.getPreference('serial_port_auto'),
"baudratePreference": baudratePref
"portPreference": settings().get("serial", "port"),
"baudratePreference": settings().getInt("serial", "baudrate")
}
def _getFormattedTimeDelta(d):
@ -198,7 +194,7 @@ class Printer():
if self.gcode.totalMoveTimeMinute:
formattedPrintTimeEstimation = _getFormattedTimeDelta(datetime.timedelta(minutes=self.gcode.totalMoveTimeMinute))
if self.gcode.extrusionAmount:
formattedFilament = "%.2fm %.2fg" % (self.gcode.extrusionAmount / 1000, self.gcode.calculateWeight() * 1000)
formattedFilament = "%.2fm" % (self.gcode.extrusionAmount / 1000)
formattedCurrentZ = None
if self.currentZ:

View File

@ -5,30 +5,17 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp
from flask import Flask, request, render_template, jsonify, make_response
from werkzeug import secure_filename
from printer import Printer, getConnectionOptions
from printer_webui.printer import Printer, getConnectionOptions
from printer_webui.settings import settings
import sys
import os
import fnmatch
APPNAME="PrinterWebUI"
BASEURL="/ajax/"
SUCCESS={}
# taken from http://stackoverflow.com/questions/1084697/how-do-i-store-desktop-application-data-in-a-cross-platform-way-for-python
if sys.platform == "darwin":
from AppKit import NSSearchPathForDirectoriesInDomains
# http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html#//apple_ref/c/func/NSSearchPathForDirectoriesInDomains
# NSApplicationSupportDirectory = 14
# NSUserDomainMask = 1
# True for expanding the tilde into a fully qualified path
appdata = os.path.join(NSSearchPathForDirectoriesInDomains(14, 1, True)[0], APPNAME)
elif sys.platform == "win32":
appdata = os.path.join(os.environ["APPDATA"], APPNAME)
else:
appdata = os.path.expanduser(os.path.join("~", "." + APPNAME.lower()))
UPLOAD_FOLDER = appdata + os.sep + "uploads"
UPLOAD_FOLDER = os.path.join(settings().settings_dir, "uploads")
if not os.path.isdir(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
ALLOWED_EXTENSIONS = set(["gcode"])
@ -115,6 +102,10 @@ def connect():
port = request.values["port"]
if request.values.has_key("baudrate"):
baudrate = request.values["baudrate"]
if request.values.has_key("save"):
settings().set("serial", "port", port)
settings().set("serial", "baudrate", baudrate)
settings().save()
printer.connect(port=port, baudrate=baudrate)
return jsonify(state="Connecting")
@ -239,6 +230,29 @@ def deleteGcodeFile():
os.remove(secure)
return readGcodeFiles()
#~~ settings
@app.route(BASEURL + "settings", methods=["GET"])
def getSettings():
s = settings()
return jsonify({
"serial_port": s.get("serial", "port"),
"serial_baudrate": s.get("serial", "baudrate")
})
@app.route(BASEURL + "settings", methods=["POST"])
def setSettings():
s = settings()
if request.values.has_key("serial_port"):
s.set("serial", "port", request.values["serial_port"])
if request.values.has_key("serial_baudrate"):
s.set("serial", "baudrate", request.values["serial_baudrate"])
s.save()
return getSettings()
#~~ helper functions
def sizeof_fmt(num):
"""
Taken from http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
@ -252,6 +266,8 @@ def sizeof_fmt(num):
def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1] in ALLOWED_EXTENSIONS
#~~ startup code
def run(host = "0.0.0.0", port = 5000, debug = False):
app.debug = debug
app.run(host=host, port=port, use_reloader=False)
@ -259,13 +275,16 @@ def run(host = "0.0.0.0", port = 5000, debug = False):
def main():
from optparse import OptionParser
defaultHost = settings().get("server", "host")
defaultPort = settings().get("server", "port")
parser = OptionParser(usage="usage: %prog [options]")
parser.add_option("-d", "--debug", action="store_true", dest="debug",
help="Enable debug mode")
parser.add_option("--host", action="store", type="string", default="0.0.0.0", dest="host",
help="Specify the host on which to bind the server, defaults to 0.0.0.0 (all interfaces) if not set")
parser.add_option("--port", action="store", type="int", default=5000, dest="port",
help="Specify the port on which to bind the server, defaults to 5000 if not set")
parser.add_option("--host", action="store", type="string", default=defaultHost, dest="host",
help="Specify the host on which to bind the server, defaults to %s if not set" % (defaultHost))
parser.add_option("--port", action="store", type="int", default=defaultPort, dest="port",
help="Specify the port on which to bind the server, defaults to %s if not set" % (defaultPort))
(options, args) = parser.parse_args()
run(host=options.host, port=options.port, debug=options.debug)

114
printer_webui/settings.py Normal file
View File

@ -0,0 +1,114 @@
# coding=utf-8
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import ConfigParser
import sys
import os
APPNAME="PrinterWebUI"
instance = None
def settings():
global instance
if instance is None:
instance = Settings()
return instance
default_settings = {
"serial": {
"port": None,
"baudrate": None
},
"server": {
"host": "0.0.0.0",
"port": 5000
}
}
class Settings(object):
def __init__(self):
self.settings_dir = None
self._config = None
self._changes = None
self.init_settings_dir()
self.load()
def init_settings_dir(self):
# taken from http://stackoverflow.com/questions/1084697/how-do-i-store-desktop-application-data-in-a-cross-platform-way-for-python
if sys.platform == "darwin":
from AppKit import NSSearchPathForDirectoriesInDomains
# http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html#//apple_ref/c/func/NSSearchPathForDirectoriesInDomains
# NSApplicationSupportDirectory = 14
# NSUserDomainMask = 1
# True for expanding the tilde into a fully qualified path
self.settings_dir = os.path.join(NSSearchPathForDirectoriesInDomains(14, 1, True)[0], APPNAME)
elif sys.platform == "win32":
self.settings_dir = os.path.join(os.environ["APPDATA"], APPNAME)
else:
self.settings_dir = os.path.expanduser(os.path.join("~", "." + APPNAME.lower()))
def load(self):
self._config = ConfigParser.ConfigParser(allow_no_value=True)
self._config.read(os.path.join(self.settings_dir, "config.ini"))
def save(self, force=False):
if self._changes is None and not force:
return
for section in default_settings.keys():
if self._changes.has_key(section):
for key in self._changes[section].keys():
value = self._changes[section][key]
if not self._config.has_section(section):
self._config.add_section(section)
self._config.set(section, key, value)
with open(os.path.join(self.settings_dir, "config.ini"), "wb") as configFile:
self._config.write(configFile)
self._changes = None
def get(self, section, key):
if section not in default_settings.keys():
return None
value = None
if self._config.has_option(section, key):
value = self._config.get(section, key)
if value is None:
if default_settings.has_key(section) and default_settings[section].has_key(key):
return default_settings[section][key]
else:
return None
else:
return value
def getInt(self, section, key):
value = self.get(section, key)
if value is None:
return None
try:
return int(value)
except ValueError:
return None
def set(self, section, key, value):
if section not in default_settings.keys():
return None
if self._changes is None:
self._changes = {}
if self._changes.has_key(section):
sectionConfig = self._changes[section]
else:
sectionConfig = {}
sectionConfig[key] = value
self._changes[section] = sectionConfig

View File

@ -5,6 +5,7 @@ function ConnectionViewModel() {
self.baudrateOptions = ko.observableArray(undefined);
self.selectedPort = ko.observable(undefined);
self.selectedBaudrate = ko.observable(undefined);
self.saveSettings = ko.observable(undefined);
self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined);
@ -23,6 +24,17 @@ function ConnectionViewModel() {
self.previousIsOperational = undefined;
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "control/connectionOptions",
method: "GET",
dataType: "json",
success: function(response) {
self.fromResponse(response);
}
})
}
self.fromResponse = function(response) {
self.portOptions(response.ports);
self.baudrateOptions(response.baudrates);
@ -31,6 +43,8 @@ function ConnectionViewModel() {
self.selectedPort(response.portPreference);
if (!self.selectedBaudrate() && response.baudrates && response.baudrates.indexOf(response.baudratePreference) >= 0)
self.selectedBaudrate(response.baudratePreference);
self.saveSettings(false);
}
self.fromStateResponse = function(response) {
@ -58,13 +72,22 @@ function ConnectionViewModel() {
self.connect = function() {
if (self.isErrorOrClosed()) {
var data = {
"port": self.selectedPort(),
"baudrate": self.selectedBaudrate()
};
if (self.saveSettings())
data["save"] = true;
$.ajax({
url: AJAX_BASEURL + "control/connect",
type: "POST",
dataType: "json",
data: { "port": self.selectedPort(), "baudrate": self.selectedBaudrate() }
data: data
})
} else {
self.requestData();
$.ajax({
url: AJAX_BASEURL + "control/disconnect",
type: "POST",
@ -321,6 +344,17 @@ function GcodeFilesViewModel() {
self.files = ko.observableArray([]);
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "gcodefiles",
method: "GET",
dataType: "json",
success: function(response) {
self.fromResponse(response);
}
});
}
self.fromResponse = function(response) {
self.files(response.files);
}
@ -545,22 +579,9 @@ $(function() {
//~~ startup commands
dataUpdater.requestData();
$.ajax({
url: AJAX_BASEURL + "gcodefiles",
method: "GET",
dataType: "json",
success: function(response) {
self.gcodeFilesViewModel.fromResponse(response);
}
});
$.ajax({
url: AJAX_BASEURL + "control/connectionOptions",
method: "GET",
dataType: "json",
success: function(response) {
connectionViewModel.fromResponse(response);
}
})
connectionViewModel.requestData();
gcodeFilesViewModel.requestData();
}
);

View File

@ -38,10 +38,13 @@
</div>
<div class="accordion-body collapse in" id="connection">
<div class="accordion-inner">
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed">Serial Port</label>
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"></select>
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed">Baudrate</label>
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"></select>
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed">Serial Port</label>
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"></select>
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed">Baudrate</label>
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"></select>
<label class="checkbox">
<input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"> Save connection settings
</label>
<button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText()">Connect</button>
</div>
</div>

View File

@ -1,12 +1,7 @@
The code in this sub package originates from the Cura project (https://github.com/daid/Cura). It has been
slightly reorganized. The mapping to the original Cura source is the following:
The code in this sub package mostly originates from the Cura project (https://github.com/daid/Cura). It has been
slightly reorganized and adapted. The mapping to the original Cura source is the following:
* avr_isp.* => Cura.avr_isp.*
* comm => Cura.util.machineCom
* gcodeInterpreter => Cura.util.gcodeInterpreter
* profile => Cura.util.profile
* resources => Cura.util.resources
* util3d => Cura.util.util3d
* version => Cura.util.version
In the future "profile" and "version" are to be replaced by custom implementations.
* util3d => Cura.util.util3d

View File

@ -15,7 +15,7 @@ import serial
from printer_webui.util.avr_isp import stk500v2
from printer_webui.util.avr_isp import ispBase
from printer_webui.util import profile
from printer_webui.settings import settings
try:
import _winreg
@ -37,22 +37,21 @@ def serialList():
i+=1
except:
pass
baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/tty.usb*") + glob.glob("/dev/cu.*") + glob.glob("/dev/rfcomm*")
prev = profile.getPreference('serial_port_auto')
baselist = baselist + glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/cu.*") + glob.glob("/dev/rfcomm*")
prev = settings().get("serial", "port")
if prev in baselist:
baselist.remove(prev)
baselist.insert(0, prev)
if isDevVersion():
baselist.append('VIRTUAL')
baselist.append("VIRTUAL")
return baselist
def baudrateList():
ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
if profile.getPreference('serial_baud_auto') != '':
prev = int(profile.getPreference('serial_baud_auto'))
if prev in ret:
ret.remove(prev)
ret.insert(0, prev)
prev = settings().getInt("serial", "baudrate")
if prev in ret:
ret.remove(prev)
ret.insert(0, prev)
return ret
class VirtualPrinter():
@ -145,12 +144,13 @@ class MachineCom(object):
def __init__(self, port = None, baudrate = None, callbackObject = None):
if port == None:
port = profile.getPreference('serial_port')
port = settings().get("serial", "port")
if baudrate == None:
if profile.getPreference('serial_baud') == 'AUTO':
settingsBaudrate = settings().getInt("serial", "baudrate")
if settingsBaudrate is None:
baudrate = 0
else:
baudrate = int(profile.getPreference('serial_baud'))
baudrate = settingsBaudrate
if callbackObject == None:
callbackObject = MachineComPrintCallback()
@ -277,7 +277,6 @@ class MachineCom(object):
self._log("Connecting to: %s" % (p))
programmer.connect(p)
self._serial = programmer.leaveISP()
profile.putPreference('serial_port_auto', p)
break
except ispBase.IspError as (e):
self._log("Error while connecting to %s: %s" % (p, str(e)))
@ -378,7 +377,6 @@ class MachineCom(object):
else:
self._sendCommand("M999")
self._serial.timeout = 2
profile.putPreference('serial_baud_auto', self._serial.baudrate)
self._changeState(self.STATE_OPERATIONAL)
else:
self._testingBaudrate = False

View File

@ -6,7 +6,21 @@ import re
import os
from printer_webui.util import util3d
from printer_webui.util import profile
preferences = {
"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,
}
def getPreference(key, default=None):
if preferences.has_key(key):
return preferences[key]
else:
return default
class gcodePath(object):
def __init__(self, newType, pathType, layerThickness, startPoint):
@ -33,23 +47,6 @@ class gcode(object):
def loadList(self, l):
self._load(l)
def calculateWeight(self):
#Calculates the weight of the filament in kg
radius = float(profile.getProfileSetting('filament_diameter')) / 2
volumeM3 = (self.extrusionAmount * (math.pi * radius * radius)) / (1000*1000*1000)
return volumeM3 * profile.getPreferenceFloat('filament_density')
def calculateCost(self):
cost_kg = profile.getPreferenceFloat('filament_cost_kg')
cost_meter = profile.getPreferenceFloat('filament_cost_meter')
if cost_kg > 0.0 and cost_meter > 0.0:
return "%.2f / %.2f" % (self.calculateWeight() * cost_kg, self.extrusionAmount / 1000 * cost_meter)
elif cost_kg > 0.0:
return "%.2f" % (self.calculateWeight() * cost_kg)
elif cost_meter > 0.0:
return "%.2f" % (self.extrusionAmount / 1000 * cost_meter)
return False
def _load(self, gcodeFile):
filePos = 0
pos = util3d.Vector3()
@ -105,12 +102,12 @@ class gcode(object):
T = self.getCodeInt(line, 'T')
if T is not None:
if currentExtruder > 0:
posOffset.x -= profile.getPreferenceFloat('extruder_offset_x%d' % (currentExtruder))
posOffset.y -= profile.getPreferenceFloat('extruder_offset_y%d' % (currentExtruder))
posOffset.x -= getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
posOffset.y -= getPreference('extruder_offset_y%d' % (currentExtruder), 0.0)
currentExtruder = T
if currentExtruder > 0:
posOffset.x += profile.getPreferenceFloat('extruder_offset_x%d' % (currentExtruder))
posOffset.y += profile.getPreferenceFloat('extruder_offset_y%d' % (currentExtruder))
posOffset.x += getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
posOffset.y += getPreference('extruder_offset_y%d' % (currentExtruder), 0.0)
G = self.getCodeInt(line, 'G')
if G is not None:

View File

@ -1,651 +0,0 @@
from __future__ import absolute_import
from __future__ import division
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 printer_webui.util import resources
from printer_webui.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': '',
'object_center_x': '-1',
'object_center_y': '-1',
'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-15 F5000
G92 E0
T{extruder}
G1 E15 F5000
G92 E0
""",
}
preferencesDefaultSettings = {
'startMode': 'Simple',
'lastFile': os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'resources', '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 + 0.0001)
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').encode('utf-8', 'replace')
if tag == 'date':
return pre + time.strftime('%d %b %Y').encode('utf-8', 'replace')
if tag == 'day':
return pre + time.strftime('%a').encode('utf-8', 'replace')
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/'))
if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
ret.append(os.path.normpath(os.path.join(resources.resourceBasePath, "Cura/plugins")))
else:
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 is not 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 is None:
continue
pythonFile = None
for basePath in getPluginBasePaths():
testFilename = os.path.join(basePath, pluginConfig['filename'])
if os.path.isfile(testFilename):
pythonFile = testFilename
if pythonFile is 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

View File

@ -1,34 +0,0 @@
# coding=utf-8
from __future__ import absolute_import
import os
import sys
__all__ = ['getPathForResource', 'getPathForImage', 'getPathForMesh']
if sys.platform.startswith('darwin'):
if hasattr(sys, 'frozen'):
from Foundation import *
resourceBasePath = NSBundle.mainBundle().resourcePath()
else:
resourceBasePath = os.path.join(os.path.dirname(__file__), "../resources")
else:
if hasattr(sys, 'frozen'):
resourceBasePath = os.path.join(os.path.dirname(__file__), "../../resources")
else:
resourceBasePath = os.path.join(os.path.dirname(__file__), "../resources")
def getPathForResource(dir, subdir, resource_name):
assert os.path.isdir(dir), "{p} is not a directory".format(p=dir)
path = os.path.normpath(os.path.join(dir, subdir, resource_name))
assert os.path.isfile(path), "{p} is not a file.".format(p=path)
return path
def getPathForImage(name):
return getPathForResource(resourceBasePath, 'images', name)
def getPathForMesh(name):
return getPathForResource(resourceBasePath, 'meshes', name)
def getPathForFirmware(name):
return getPathForResource(resourceBasePath, 'firmware', name)

View File

@ -1,33 +0,0 @@
from __future__ import absolute_import
import os
import sys
from printer_webui.util import resources
def getVersion(getGitVersion = True):
gitPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../../.git"))
if hasattr(sys, 'frozen'):
versionFile = os.path.normpath(os.path.join(resources.resourceBasePath, "version"))
else:
versionFile = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../version"))
if os.path.exists(gitPath):
if not getGitVersion:
return "dev"
f = open(gitPath + "/refs/heads/master", "r")
version = f.readline()
f.close()
return version.strip()
if os.path.exists(versionFile):
f = open(versionFile, "r")
version = f.readline()
f.close()
return version.strip()
return "?"
def isDevVersion():
gitPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../../.git"))
return os.path.exists(gitPath)
if __name__ == '__main__':
print(getVersion())

View File

@ -1,4 +1,3 @@
flask>=0.9
numpy>=1.6.2
pyserial>=2.6
-e git+git://github.com/GreatFruitOmsk/Power.git#egg=Power
pyserial>=2.6