From a85ea69fb032d4387743dbb6f77585406af6993b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 1 Jan 2013 21:04:00 +0100 Subject: [PATCH] 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 --- README.md | 23 + printer_webui/printer.py | 16 +- printer_webui/server.py | 59 ++- printer_webui/settings.py | 114 +++++ printer_webui/static/js/ui.js | 55 ++- printer_webui/templates/index.html | 11 +- printer_webui/util/README | 11 +- printer_webui/util/comm.py | 26 +- printer_webui/util/gcodeInterpreter.py | 41 +- printer_webui/util/profile.py | 651 ------------------------- printer_webui/util/resources.py | 34 -- printer_webui/util/version.py | 33 -- requirements.txt | 3 +- 13 files changed, 262 insertions(+), 815 deletions(-) create mode 100644 printer_webui/settings.py delete mode 100644 printer_webui/util/profile.py delete mode 100644 printer_webui/util/resources.py delete mode 100644 printer_webui/util/version.py diff --git a/README.md b/README.md index 3e8ee30..6efecc3 100644 --- a/README.md +++ b/README.md @@ -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 ------- diff --git a/printer_webui/printer.py b/printer_webui/printer.py index 4877fe7..f3ed0b2 100644 --- a/printer_webui/printer.py +++ b/printer_webui/printer.py @@ -1,5 +1,6 @@ # coding=utf-8 __author__ = "Gina Häußge " +__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: diff --git a/printer_webui/server.py b/printer_webui/server.py index 9b0225b..c746e20 100644 --- a/printer_webui/server.py +++ b/printer_webui/server.py @@ -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) diff --git a/printer_webui/settings.py b/printer_webui/settings.py new file mode 100644 index 0000000..47a53c9 --- /dev/null +++ b/printer_webui/settings.py @@ -0,0 +1,114 @@ +# coding=utf-8 +__author__ = "Gina Häußge " +__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 + diff --git a/printer_webui/static/js/ui.js b/printer_webui/static/js/ui.js index eda2ce7..02cd6a8 100644 --- a/printer_webui/static/js/ui.js +++ b/printer_webui/static/js/ui.js @@ -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(); + } ); diff --git a/printer_webui/templates/index.html b/printer_webui/templates/index.html index 4cb5b87..465c970 100644 --- a/printer_webui/templates/index.html +++ b/printer_webui/templates/index.html @@ -38,10 +38,13 @@
- - - - + + + + +
diff --git a/printer_webui/util/README b/printer_webui/util/README index 1a8cde2..145ef86 100644 --- a/printer_webui/util/README +++ b/printer_webui/util/README @@ -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. \ No newline at end of file +* util3d => Cura.util.util3d \ No newline at end of file diff --git a/printer_webui/util/comm.py b/printer_webui/util/comm.py index 8a20c08..dd2b5ab 100644 --- a/printer_webui/util/comm.py +++ b/printer_webui/util/comm.py @@ -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 diff --git a/printer_webui/util/gcodeInterpreter.py b/printer_webui/util/gcodeInterpreter.py index 72cd592..8ba22ab 100644 --- a/printer_webui/util/gcodeInterpreter.py +++ b/printer_webui/util/gcodeInterpreter.py @@ -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: diff --git a/printer_webui/util/profile.py b/printer_webui/util/profile.py deleted file mode 100644 index 14c186e..0000000 --- a/printer_webui/util/profile.py +++ /dev/null @@ -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 diff --git a/printer_webui/util/resources.py b/printer_webui/util/resources.py deleted file mode 100644 index 4eb6e37..0000000 --- a/printer_webui/util/resources.py +++ /dev/null @@ -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) diff --git a/printer_webui/util/version.py b/printer_webui/util/version.py deleted file mode 100644 index 439b6b5..0000000 --- a/printer_webui/util/version.py +++ /dev/null @@ -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()) - diff --git a/requirements.txt b/requirements.txt index ddc2390..c5bf08f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file