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