Merge pull request #1 from foosel/devel

Update fork
This commit is contained in:
Salandora 2014-03-03 23:18:34 +01:00
commit d5e2bf546e
25 changed files with 870 additions and 158 deletions

View file

@ -20,7 +20,7 @@ Get connection settings
.. sourcecode:: http .. sourcecode:: http
GET /api/control/connection HTTP/1.1 GET /api/connection HTTP/1.1
Host: example.com Host: example.com
Content-Type: application/json Content-Type: application/json
X-Api-Key: abcdef... X-Api-Key: abcdef...
@ -77,7 +77,7 @@ Issue a connection command
.. sourcecode:: http .. sourcecode:: http
POST /api/control/connection HTTP/1.1 POST /api/connection HTTP/1.1
Host: example.com Host: example.com
Content-Type: application/json Content-Type: application/json
X-Api-Key: abcdef... X-Api-Key: abcdef...
@ -94,7 +94,7 @@ Issue a connection command
.. sourcecode:: http .. sourcecode:: http
POST /api/control/connection HTTP/1.1 POST /api/connection HTTP/1.1
Host: example.com Host: example.com
Content-Type: application/json Content-Type: application/json
X-Api-Key: abcdef... X-Api-Key: abcdef...
@ -114,6 +114,6 @@ Issue a connection command
Defaults to ``false`` if not set. Defaults to ``false`` if not set.
:json boolean autoconnect: ``connect`` command: Whether to attempt to automatically connect to the printer on server :json boolean autoconnect: ``connect`` command: Whether to attempt to automatically connect to the printer on server
startup. If not set no changes will be made to the current setting. startup. If not set no changes will be made to the current setting.
:statuscode 200: No error :statuscode 204: No error
:statuscode 400: If the selected `port` or `baudrate` for a ``connect`` command are not part of the available :statuscode 400: If the selected `port` or `baudrate` for a ``connect`` command are not part of the available
options. options.

View file

@ -12,4 +12,4 @@ API Documentation
connection.rst connection.rst
printer.rst printer.rst
job.rst job.rst
logs.rst

177
docs/api/logs.rst Normal file
View file

@ -0,0 +1,177 @@
.. _sec-api-logs:
*******************
Log file management
*******************
.. note::
All log file management operations require admin rights.
.. contents::
.. _sec-api-logs-list:
Retrieve a list of available log files
======================================
.. http:post:: /api/logs
Retrieve information regarding all log files currently available and regarding the disk space still available
in the system on the location the log files are being stored.
Returns a :ref:`Logfile Retrieve response <sec-api-logs-datamodel-retrieveresponse>`.
**Example Request**
.. sourcecode:: http
GET /api/logs HTTP/1.1
Host: example.com
X-Api-Key: abcdef...
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"files" : [
{
"date" : 1393158814,
"name" : "octoprint.log",
"size" : 43712,
"refs": {
"resource": "http://example.com/api/logs/octoprint.log",
"download": "http://example.com/downloads/logs/octoprint.log"
}
},
{
"date" : 1392628936,
"name" : "octoprint.log.2014-02-17",
"size" : 13205,
"refs": {
"resource": "http://example.com/api/logs/octoprint.log.2014-02-17",
"download": "http://example.com/downloads/logs/octoprint.log.2014-02-17"
}
},
{
"date" : 1393158814,
"name" : "serial.log",
"size" : 1798419,
"refs": {
"resource": "http://example.com/api/logs/serial.log",
"download": "http://example.com/downloads/logs/serial.log"
}
}
],
"free": 12237201408
}
:statuscode 200: No error
:statuscode 403: If the given API token did not have admin rights associated with it
.. _sec-api-logs-delete:
Delete a specific logfile
=========================
.. http:delete:: /api/logs/(path:filename)
Delete the selected log file with name `filename`.
Returns a :http:statuscode:`204` after successful deletion.
**Example Request**
.. sourcecode:: http
DELETE /api/logs/octoprint.log.2014-02-17 HTTP/1.1
Host: example.com
X-Api-Key: abcdef...
:param filename: The filename of the log file to delete
:statuscode 204: No error
:statuscode 403: If the given API token did not have admin rights associated with it
:statuscode 404: If the file was not found
.. _sec-api-logs-datamodel:
Datamodel
=========
.. _sec-api-logs-datamodel-retrieveresponse:
Logfile Retrieve Response
-------------------------
.. list-table::
:widths: 15 5 10 30
:header-rows: 1
* - Name
- Multiplicity
- Type
- Description
* - ``files``
- 0..*
- Array of :ref:`File information items <sec-api-logs-datamodel-fileinfo>`
- The list of requested files. Might be an empty list if no files are available
* - ``free``
- 1
- String
- The amount of disk space in bytes available in the local disk space (refers to OctoPrint's ``logs`` folder).
.. _sec-api-logs-datamodel-fileinfo:
File information
----------------
.. list-table::
:widths: 15 5 10 30
:header-rows: 1
* - Name
- Multiplicity
- Type
- Description
* - ``name``
- 1
- String
- The name of the file
* - ``size``
- 1
- Number
- The size of the file in bytes.
* - ``date``
- 1
- Unix timestamp
- The timestamp when this file was last modified.
* - ``refs``
- 1
- :ref:`sec-api-logs-datamodel-ref`
- References relevant to this file
.. _sec-api-logs-datamodel-ref:
References
----------
.. list-table::
:widths: 15 5 10 30
:header-rows: 1
* - Name
- Multiplicity
- Type
- Description
* - ``resource``
- 1
- URL
- The resource that represents the file (e.g. for deleting)
* - ``download``
- 1
- URL
- The download URL for the file

View file

@ -560,8 +560,6 @@ class MetadataAnalyzer:
def _work(self): def _work(self):
aborted = None aborted = None
while True: while True:
self._active.wait()
if aborted is not None: if aborted is not None:
filename = aborted filename = aborted
aborted = None aborted = None
@ -570,6 +568,8 @@ class MetadataAnalyzer:
(priority, filename) = self._queue.get() (priority, filename) = self._queue.get()
self._logger.debug("Processing file %s from queue (priority %d)" % (filename, priority)) self._logger.debug("Processing file %s from queue (priority %d)" % (filename, priority))
self._active.wait()
try: try:
self._analyzeGcode(filename) self._analyzeGcode(filename)
self._queue.task_done() self._queue.task_done()

View file

@ -37,6 +37,7 @@ class Printer():
self._gcodeManager.registerCallback(self) self._gcodeManager.registerCallback(self)
# state # state
# TODO do we really need to hold the temperature here?
self._temp = None self._temp = None
self._bedTemp = None self._bedTemp = None
self._targetTemp = None self._targetTemp = None
@ -355,10 +356,11 @@ class Printer():
"actual": temp[tool][0], "actual": temp[tool][0],
"target": temp[tool][1] "target": temp[tool][1]
} }
data["bed"] = { if bedTemp is not None and isinstance(bedTemp, tuple):
"actual": bedTemp[0], data["bed"] = {
"target": bedTemp[1] "actual": bedTemp[0],
} "target": bedTemp[1]
}
self._temps.append(data) self._temps.append(data)

View file

@ -2,6 +2,8 @@
__author__ = "Gina Häußge <osd@foosel.net>" __author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import flask
import tornado.wsgi
from sockjs.tornado import SockJSRouter from sockjs.tornado import SockJSRouter
from flask import Flask, render_template, send_from_directory, make_response from flask import Flask, render_template, send_from_directory, make_response
from flask.ext.login import LoginManager from flask.ext.login import LoginManager
@ -27,8 +29,8 @@ principals = Principal(app)
admin_permission = Permission(RoleNeed("admin")) admin_permission = Permission(RoleNeed("admin"))
user_permission = Permission(RoleNeed("user")) user_permission = Permission(RoleNeed("user"))
# only import the octoprint stuff down here, as it might depend on things defined above to be initialized already
from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection, admin_validator
from octoprint.printer import Printer, getConnectionOptions from octoprint.printer import Printer, getConnectionOptions
from octoprint.settings import settings from octoprint.settings import settings
import octoprint.gcodefiles as gcodefiles import octoprint.gcodefiles as gcodefiles
@ -174,9 +176,23 @@ class Server():
self._router = SockJSRouter(self._createSocketConnection, "/sockjs") self._router = SockJSRouter(self._createSocketConnection, "/sockjs")
def admin_access_validation(request):
"""
Creates a custom wsgi and Flask request context in order to be able to process user information
stored in the current session.
:param request: The Tornado request for which to create the environment and context
"""
wsgi_environ = tornado.wsgi.WSGIContainer.environ(request)
with app.request_context(wsgi_environ):
app.session_interface.open_session(app, flask.request)
loginManager.reload_user()
admin_validator(flask.request)
self._tornado_app = Application(self._router.urls + [ self._tornado_app = Application(self._router.urls + [
(r"/downloads/timelapse/([^/]*\.mpg)", LargeResponseHandler, {"path": settings().getBaseFolder("timelapse"), "as_attachment": True}), (r"/downloads/timelapse/([^/]*\.mpg)", LargeResponseHandler, {"path": settings().getBaseFolder("timelapse"), "as_attachment": True}),
(r"/downloads/files/local/([^/]*\.(gco|gcode))", LargeResponseHandler, {"path": settings().getBaseFolder("uploads"), "as_attachment": True}), (r"/downloads/files/local/([^/]*\.(gco|gcode))", LargeResponseHandler, {"path": settings().getBaseFolder("uploads"), "as_attachment": True}),
(r"/downloads/logs/([^/]*)", LargeResponseHandler, {"path": settings().getBaseFolder("logs"), "as_attachment": True, "access_validation": admin_access_validation}),
(r".*", FallbackHandler, {"fallback": WSGIContainer(app.wsgi_app)}) (r".*", FallbackHandler, {"fallback": WSGIContainer(app.wsgi_app)})
]) ])
self._server = HTTPServer(self._tornado_app) self._server = HTTPServer(self._tornado_app)
@ -190,6 +206,8 @@ class Server():
printer.connect(port, baudrate) printer.connect(port, baudrate)
try: try:
IOLoop.instance().start() IOLoop.instance().start()
except KeyboardInterrupt:
logger.info("Goodbye!")
except: except:
logger.fatal("Now that is embarrassing... Something really really went wrong here. Please report this including the stacktrace below in OctoPrint's bugtracker. Thanks!") logger.fatal("Now that is embarrassing... Something really really went wrong here. Please report this including the stacktrace below in OctoPrint's bugtracker. Thanks!")
logger.exception("Stacktrace follows:") logger.exception("Stacktrace follows:")

View file

@ -27,6 +27,7 @@ from . import files as api_files
from . import settings as api_settings from . import settings as api_settings
from . import timelapse as api_timelapse from . import timelapse as api_timelapse
from . import users as api_users from . import users as api_users
from . import log as api_logs
#~~ first run setup #~~ first run setup

View file

@ -11,7 +11,6 @@ import octoprint.util as util
from octoprint.filemanager.destinations import FileDestinations from octoprint.filemanager.destinations import FileDestinations
from octoprint.settings import settings, valid_boolean_trues from octoprint.settings import settings, valid_boolean_trues
from octoprint.server import printer, gcodeManager, eventManager, restricted_access, NO_CONTENT from octoprint.server import printer, gcodeManager, eventManager, restricted_access, NO_CONTENT
from octoprint.server.util import urlForDownload
from octoprint.server.api import api from octoprint.server.api import api
@ -66,7 +65,7 @@ def _getFileList(origin):
file.update({ file.update({
"refs": { "refs": {
"resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=file["name"], _external=True), "resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=file["name"], _external=True),
"download": urlForDownload(FileDestinations.LOCAL, file["name"]) "download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + file["name"]
} }
}) })
return files return files
@ -169,7 +168,7 @@ def uploadGcodeFile(target):
"origin": FileDestinations.LOCAL, "origin": FileDestinations.LOCAL,
"refs": { "refs": {
"resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=filename, _external=True), "resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=filename, _external=True),
"download": urlForDownload(FileDestinations.LOCAL, filename) "download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + filename
} }
} }
}) })
@ -269,6 +268,5 @@ def deleteGcodeFile(filename, target):
else: else:
gcodeManager.removeFile(filename) gcodeManager.removeFile(filename)
# return an updated list of files return NO_CONTENT
return readGcodeFiles()

View file

@ -0,0 +1,62 @@
# coding=utf-8
__author__ = "Marc Hannappel Salandora"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import os
from flask import request, jsonify, url_for, make_response
from werkzeug.utils import secure_filename
from octoprint.settings import settings
from octoprint.server import restricted_access, NO_CONTENT, admin_permission
from octoprint.server.util import redirectToTornado
from octoprint.server.api import api
from octoprint.util import getFreeBytes
@api.route("/logs", methods=["GET"])
@restricted_access
@admin_permission.require(403)
def getLogFiles():
files = _getLogFiles()
return jsonify(files=files, free=getFreeBytes(settings().getBaseFolder("logs")))
@api.route("/logs/<path:filename>", methods=["GET"])
@restricted_access
@admin_permission.require(403)
def downloadLog(filename):
return redirectToTornado(request, url_for("index") + "downloads/logs/" + filename)
@api.route("/logs/<path:filename>", methods=["DELETE"])
@restricted_access
@admin_permission.require(403)
def deleteLog(filename):
secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename))
if not os.path.exists(secure):
return make_response("File not found: %s" % filename, 404)
os.remove(secure)
return NO_CONTENT
def _getLogFiles():
files = []
basedir = settings().getBaseFolder("logs")
for osFile in os.listdir(basedir):
statResult = os.stat(os.path.join(basedir, osFile))
files.append({
"name": osFile,
"date": int(statResult.st_mtime),
"size": statResult.st_size,
"refs": {
"resource": url_for(".downloadLog", filename=osFile, _external=True),
"download": url_for("index", _external=True) + "downloads/logs/" + osFile
}
})
return files

View file

@ -59,7 +59,8 @@ def getSettings():
"waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]), "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]),
"alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]), "alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]),
"sdSupport": s.getBoolean(["feature", "sdSupport"]), "sdSupport": s.getBoolean(["feature", "sdSupport"]),
"swallowOkAfterResend": s.getBoolean(["feature", "swallowOkAfterResend"]) "swallowOkAfterResend": s.getBoolean(["feature", "swallowOkAfterResend"]),
"repetierTargetTemp": s.getBoolean(["feature", "repetierTargetTemp"])
}, },
"serial": { "serial": {
"port": connectionOptions["portPreference"], "port": connectionOptions["portPreference"],
@ -136,6 +137,7 @@ def setSettings():
if "alwaysSendChecksum" in data["feature"].keys(): s.setBoolean(["feature", "alwaysSendChecksum"], data["feature"]["alwaysSendChecksum"]) if "alwaysSendChecksum" in data["feature"].keys(): s.setBoolean(["feature", "alwaysSendChecksum"], data["feature"]["alwaysSendChecksum"])
if "sdSupport" in data["feature"].keys(): s.setBoolean(["feature", "sdSupport"], data["feature"]["sdSupport"]) if "sdSupport" in data["feature"].keys(): s.setBoolean(["feature", "sdSupport"], data["feature"]["sdSupport"])
if "swallowOkAfterResend" in data["feature"].keys(): s.setBoolean(["feature", "swallowOkAfterResend"], data["feature"]["swallowOkAfterResend"]) if "swallowOkAfterResend" in data["feature"].keys(): s.setBoolean(["feature", "swallowOkAfterResend"], data["feature"]["swallowOkAfterResend"])
if "repetierTargetTemp" in data["feature"].keys(): s.setBoolean(["feature", "repetierTargetTemp"], data["feature"]["repetierTargetTemp"])
if "serial" in data.keys(): if "serial" in data.keys():
if "autoconnect" in data["serial"].keys(): s.setBoolean(["serial", "autoconnect"], data["serial"]["autoconnect"]) if "autoconnect" in data["serial"].keys(): s.setBoolean(["serial", "autoconnect"], data["serial"]["autoconnect"])

View file

@ -1,7 +1,11 @@
# coding=utf-8
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
from flask.ext.principal import identity_changed, Identity from flask.ext.principal import identity_changed, Identity
from tornado.web import StaticFileHandler, HTTPError from tornado.web import StaticFileHandler, HTTPError
from flask import url_for, make_response, request, current_app from flask import url_for, make_response, request, current_app
from flask.ext.login import login_required, login_user from flask.ext.login import login_required, login_user, current_user
from werkzeug.utils import redirect from werkzeug.utils import redirect
from sockjs.tornado import SockJSConnection from sockjs.tornado import SockJSConnection
@ -21,6 +25,7 @@ import octoprint.server
from octoprint.users import ApiUser from octoprint.users import ApiUser
from octoprint.events import Events from octoprint.events import Events
def restricted_access(func, apiEnabled=True): def restricted_access(func, apiEnabled=True):
""" """
If you decorate a view with this, it will ensure that first setup has been If you decorate a view with this, it will ensure that first setup has been
@ -80,11 +85,32 @@ def api_access(func):
return decorated_view return decorated_view
def _getUserForApiKey(apikey):
if settings().get(["api", "enabled"]) and apikey is not None:
if apikey == settings().get(["api", "key"]):
# master key was used
return ApiUser()
else:
# user key might have been used
return octoprint.server.userManager.findUser(apikey=apikey)
else:
return None
def _getApiKey(request): def _getApiKey(request):
if "apikey" in request.values: # Check Flask GET/POST arguments
if hasattr(request, "values") and "apikey" in request.values:
return request.values["apikey"] return request.values["apikey"]
# Check Tornado GET/POST arguments
if hasattr(request, "arguments") and "apikey" in request.arguments \
and len(request.arguments["apikey"]) > 0 and len(request.arguments["apikey"].strip()) > 0:
return request.arguments["apikey"]
# Check Tornado and Flask headers
if "X-Api-Key" in request.headers.keys(): if "X-Api-Key" in request.headers.keys():
return request.headers.get("X-Api-Key") return request.headers.get("X-Api-Key")
return None return None
@ -204,11 +230,15 @@ class LargeResponseHandler(StaticFileHandler):
CHUNK_SIZE = 16 * 1024 CHUNK_SIZE = 16 * 1024
def initialize(self, path, default_filename=None, as_attachment=False): def initialize(self, path, default_filename=None, as_attachment=False, access_validation=None):
StaticFileHandler.initialize(self, path, default_filename) StaticFileHandler.initialize(self, path, default_filename)
self._as_attachment = as_attachment self._as_attachment = as_attachment
self._access_validation = access_validation
def get(self, path, include_body=True): def get(self, path, include_body=True):
if self._access_validation is not None:
self._access_validation(self.request)
path = self.parse_url_path(path) path = self.parse_url_path(path)
abspath = os.path.abspath(os.path.join(self.root, path)) abspath = os.path.abspath(os.path.join(self.root, path))
# os.path.abspath strips a trailing / # os.path.abspath strips a trailing /
@ -273,6 +303,29 @@ class LargeResponseHandler(StaticFileHandler):
self.set_header("Content-Disposition", "attachment") self.set_header("Content-Disposition", "attachment")
#~~ admin access validator for use with tornado
def admin_validator(request):
"""
Validates that the given request is made by an admin user, identified either by API key or existing Flask
session.
Must be executed in an existing Flask request context!
:param request: The Flask request object
"""
apikey = _getApiKey(request)
if settings().get(["api", "enabled"]) and apikey is not None:
user = _getUserForApiKey(apikey)
else:
user = current_user
if user is None or not user.is_authenticated() or not user.is_admin():
raise HTTPError(403)
#~~ reverse proxy compatible wsgi middleware #~~ reverse proxy compatible wsgi middleware
@ -333,7 +386,3 @@ def redirectToTornado(request, target):
redirectUrl += fragment redirectUrl += fragment
return redirect(redirectUrl) return redirect(redirectUrl)
def urlForDownload(origin, filename):
return url_for("index", _external=True) + "downloads/files/" + origin + "/" + filename

View file

@ -65,7 +65,8 @@ default_settings = {
"waitForStartOnConnect": False, "waitForStartOnConnect": False,
"alwaysSendChecksum": False, "alwaysSendChecksum": False,
"sdSupport": True, "sdSupport": True,
"swallowOkAfterResend": True "swallowOkAfterResend": True,
"repetierTargetTemp": False
}, },
"folder": { "folder": {
"uploads": None, "uploads": None,
@ -143,7 +144,8 @@ default_settings = {
"forceChecksum": False, "forceChecksum": False,
"okWithLinenumber": False, "okWithLinenumber": False,
"numExtruders": 1, "numExtruders": 1,
"includeCurrentToolInTemps": True "includeCurrentToolInTemps": True,
"hasBed": True
} }
} }
} }

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) { function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel, logViewModel) {
var self = this; var self = this;
self.loginStateViewModel = loginStateViewModel; self.loginStateViewModel = loginStateViewModel;
@ -10,6 +10,7 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
self.gcodeFilesViewModel = gcodeFilesViewModel; self.gcodeFilesViewModel = gcodeFilesViewModel;
self.timelapseViewModel = timelapseViewModel; self.timelapseViewModel = timelapseViewModel;
self.gcodeViewModel = gcodeViewModel; self.gcodeViewModel = gcodeViewModel;
self.logViewModel = logViewModel;
self._socket = undefined; self._socket = undefined;
self._autoReconnecting = false; self._autoReconnecting = false;
@ -38,7 +39,8 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
self._autoReconnectTrial = 0; self._autoReconnectTrial = 0;
if ($("#offline_overlay").is(":visible")) { if ($("#offline_overlay").is(":visible")) {
$("#offline_overlay").hide(); $("#offline_overlay").hide();
self.logViewModel.requestData();
self.timelapseViewModel.requestData(); self.timelapseViewModel.requestData();
self.loginStateViewModel.requestData(); self.loginStateViewModel.requestData();
self.gcodeFilesViewModel.requestData(); self.gcodeFilesViewModel.requestData();

View file

@ -1,5 +1,4 @@
$(function() { $(function() {
//~~ Initialize view models //~~ Initialize view models
var loginStateViewModel = new LoginStateViewModel(); var loginStateViewModel = new LoginStateViewModel();
var usersViewModel = new UsersViewModel(loginStateViewModel); var usersViewModel = new UsersViewModel(loginStateViewModel);
@ -14,6 +13,7 @@ $(function() {
var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel); var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel);
var gcodeViewModel = new GcodeViewModel(loginStateViewModel, settingsViewModel); var gcodeViewModel = new GcodeViewModel(loginStateViewModel, settingsViewModel);
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel); var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
var logViewModel = new LogViewModel(loginStateViewModel);
var dataUpdater = new DataUpdater( var dataUpdater = new DataUpdater(
loginStateViewModel, loginStateViewModel,
@ -24,7 +24,8 @@ $(function() {
terminalViewModel, terminalViewModel,
gcodeFilesViewModel, gcodeFilesViewModel,
timelapseViewModel, timelapseViewModel,
gcodeViewModel gcodeViewModel,
logViewModel
); );
// work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at // work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at
@ -287,6 +288,12 @@ $(function() {
} }
} }
ko.bindingHandlers.allowBindings = {
init: function (elem, valueAccessor) {
return { controlsDescendantBindings: !valueAccessor() };
}
};
ko.applyBindings(connectionViewModel, document.getElementById("connection_accordion")); ko.applyBindings(connectionViewModel, document.getElementById("connection_accordion"));
ko.applyBindings(printerStateViewModel, document.getElementById("state_accordion")); ko.applyBindings(printerStateViewModel, document.getElementById("state_accordion"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_accordion")); ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_accordion"));
@ -302,6 +309,7 @@ $(function() {
ko.applyBindings(navigationViewModel, document.getElementById("navbar")); ko.applyBindings(navigationViewModel, document.getElementById("navbar"));
ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]); ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]);
ko.applyBindings(printerStateViewModel, document.getElementById("drop_overlay")); ko.applyBindings(printerStateViewModel, document.getElementById("drop_overlay"));
ko.applyBindings(logViewModel, document.getElementById("logs"));
var timelapseElement = document.getElementById("timelapse"); var timelapseElement = document.getElementById("timelapse");
if (timelapseElement) { if (timelapseElement) {
@ -316,6 +324,7 @@ $(function() {
gcodeFilesViewModel.requestData(); gcodeFilesViewModel.requestData();
timelapseViewModel.requestData(); timelapseViewModel.requestData();
settingsViewModel.requestData(); settingsViewModel.requestData();
logViewModel.requestData();
loginStateViewModel.subscribe(function(change, data) { loginStateViewModel.subscribe(function(change, data) {
if ("login" == change) { if ("login" == change) {

View file

@ -0,0 +1,62 @@
function LogViewModel(loginStateViewModel) {
var self = this;
self.loginState = loginStateViewModel;
// initialize list helper
self.listHelper = new ItemListHelper(
"logFiles",
{
"name": function(a, b) {
// sorts ascending
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
return 0;
},
"modification": function(a, b) {
// sorts descending
if (a["date"] > b["date"]) return -1;
if (a["date"] < b["date"]) return 1;
return 0;
},
"size": function(a, b) {
// sorts descending
if (a["bytes"] > b["bytes"]) return -1;
if (a["bytes"] < b["bytes"]) return 1;
return 0;
}
},
{
},
"name",
[],
[],
CONFIG_LOGFILESPERPAGE
);
self.requestData = function() {
$.ajax({
url: API_BASEURL + "logs",
type: "GET",
dataType: "json",
success: self.fromResponse
});
};
self.fromResponse = function(response) {
var files = response.files;
if (files === undefined)
return;
self.listHelper.updateItems(files);
}
self.removeFile = function(filename) {
$.ajax({
url: API_BASEURL + "logs/" + filename,
type: "DELETE",
dataType: "json",
success: self.requestData
});
}
}

View file

@ -10,7 +10,6 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.appearance_name = ko.observable(undefined); self.appearance_name = ko.observable(undefined);
self.appearance_color = ko.observable(undefined); self.appearance_color = ko.observable(undefined);
/* I did attempt to allow arbitrary gradients but cross browser support via knockout or jquery was going to be horrible */
self.appearance_available_colors = ko.observable(["default", "red", "orange", "yellow", "green", "blue", "violet", "black"]); self.appearance_available_colors = ko.observable(["default", "red", "orange", "yellow", "green", "blue", "violet", "black"]);
self.printer_movementSpeedX = ko.observable(undefined); self.printer_movementSpeedX = ko.observable(undefined);
@ -19,8 +18,69 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.printer_movementSpeedE = ko.observable(undefined); self.printer_movementSpeedE = ko.observable(undefined);
self.printer_invertAxes = ko.observable(undefined); self.printer_invertAxes = ko.observable(undefined);
self.printer_numExtruders = ko.observable(undefined); self.printer_numExtruders = ko.observable(undefined);
self.printer_extruderOffsets = ko.observableArray([]);
self.printer_bedDimensions = ko.observable(undefined); self._printer_extruderOffsets = ko.observableArray([]);
self.printer_extruderOffsets = ko.computed({
read: function() {
var extruderOffsets = self._printer_extruderOffsets();
var result = [];
for (var i = 0; i < extruderOffsets.length; i++) {
result[i] = {
x: parseFloat(extruderOffsets[i].x()),
y: parseFloat(extruderOffsets[i].y())
}
}
return result;
},
write: function(value) {
var result = [];
if (value && Array.isArray(value)) {
for (var i = 0; i < value.length; i++) {
result[i] = {
x: ko.observable(value[i].x),
y: ko.observable(value[i].y)
}
}
}
self._printer_extruderOffsets(result);
},
owner: self
});
self.ko_printer_extruderOffsets = ko.computed(function() {
var extruderOffsets = self._printer_extruderOffsets();
var numExtruders = self.printer_numExtruders();
if (!numExtruders) {
numExtruders = 1;
}
if (numExtruders > extruderOffsets.length) {
for (var i = extruderOffsets.length; i < numExtruders; i++) {
extruderOffsets[i] = {
x: ko.observable(0),
y: ko.observable(0)
}
}
self._printer_extruderOffsets(extruderOffsets);
}
return extruderOffsets.slice(0, numExtruders);
});
self.printer_bedDimensionX = ko.observable(undefined);
self.printer_bedDimensionY = ko.observable(undefined);
self.printer_bedDimensions = ko.computed({
read: function () {
return {
x: parseFloat(self.printer_bedDimensionX()),
y: parseFloat(self.printer_bedDimensionY())
};
},
write: function(value) {
self.printer_bedDimensionX(value.x);
self.printer_bedDimensionY(value.y);
},
owner: self
});
self.webcam_streamUrl = ko.observable(undefined); self.webcam_streamUrl = ko.observable(undefined);
self.webcam_snapshotUrl = ko.observable(undefined); self.webcam_snapshotUrl = ko.observable(undefined);
@ -36,6 +96,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.feature_alwaysSendChecksum = ko.observable(undefined); self.feature_alwaysSendChecksum = ko.observable(undefined);
self.feature_sdSupport = ko.observable(undefined); self.feature_sdSupport = ko.observable(undefined);
self.feature_swallowOkAfterResend = ko.observable(undefined); self.feature_swallowOkAfterResend = ko.observable(undefined);
self.feature_repetierTargetTemp = ko.observable(undefined);
self.serial_port = ko.observable(); self.serial_port = ko.observable();
self.serial_baudrate = ko.observable(); self.serial_baudrate = ko.observable();
@ -145,6 +206,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.feature_alwaysSendChecksum(response.feature.alwaysSendChecksum); self.feature_alwaysSendChecksum(response.feature.alwaysSendChecksum);
self.feature_sdSupport(response.feature.sdSupport); self.feature_sdSupport(response.feature.sdSupport);
self.feature_swallowOkAfterResend(response.feature.swallowOkAfterResend); self.feature_swallowOkAfterResend(response.feature.swallowOkAfterResend);
self.feature_repetierTargetTemp(response.feature.repetierTargetTemp);
self.serial_port(response.serial.port); self.serial_port(response.serial.port);
self.serial_baudrate(response.serial.baudrate); self.serial_baudrate(response.serial.baudrate);
@ -207,7 +269,8 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
"waitForStart": self.feature_waitForStart(), "waitForStart": self.feature_waitForStart(),
"alwaysSendChecksum": self.feature_alwaysSendChecksum(), "alwaysSendChecksum": self.feature_alwaysSendChecksum(),
"sdSupport": self.feature_sdSupport(), "sdSupport": self.feature_sdSupport(),
"swallowOkAfterResend": self.feature_swallowOkAfterResend() "swallowOkAfterResend": self.feature_swallowOkAfterResend(),
"repetierTargetTemp": self.feature_repetierTargetTemp()
}, },
"serial": { "serial": {
"port": self.serial_port(), "port": self.serial_port(),

View file

@ -17,6 +17,7 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
}; };
self.tools = ko.observableArray([]); self.tools = ko.observableArray([]);
self.hasBed = ko.observable(true);
self.bedTemp = self._createToolEntry(); self.bedTemp = self._createToolEntry();
self.bedTemp["name"]("Bed"); self.bedTemp["name"]("Bed");
self.bedTemp["key"]("bed"); self.bedTemp["key"]("bed");
@ -33,14 +34,14 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
self.heaterOptions = ko.observable({}); self.heaterOptions = ko.observable({});
self.settingsViewModel.printer_numExtruders.subscribe(function(oldVal, newVal) { self._numExtrudersUpdated = function() {
var graphColors = ["red", "orange", "green", "brown", "purple"]; var graphColors = ["red", "orange", "green", "brown", "purple"];
var heaterOptions = {}; var heaterOptions = {};
var tools = self.tools(); var tools = self.tools();
// tools // tools
var numExtruders = self.settingsViewModel.printer_numExtruders(); var numExtruders = self.settingsViewModel.printer_numExtruders();
if (numExtruders > 1) { if (numExtruders && numExtruders > 1) {
// multiple extruders // multiple extruders
for (var extruder = 0; extruder < numExtruders; extruder++) { for (var extruder = 0; extruder < numExtruders; extruder++) {
var color = graphColors.shift(); var color = graphColors.shift();
@ -71,7 +72,8 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
// write back // write back
self.heaterOptions(heaterOptions); self.heaterOptions(heaterOptions);
self.tools(tools); self.tools(tools);
}); };
self.settingsViewModel.printer_numExtruders.subscribe(self._numExtrudersUpdated);
self.temperatures = []; self.temperatures = [];
self.plotOptions = { self.plotOptions = {
@ -106,19 +108,19 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
noColumns: 2, noColumns: 2,
backgroundOpacity: 0 backgroundOpacity: 0
} }
} };
self.fromCurrentData = function(data) { self.fromCurrentData = function(data) {
self._processStateData(data.state); self._processStateData(data.state);
self._processTemperatureUpdateData(data.temps); self._processTemperatureUpdateData(data.temps);
self._processOffsetData(data.offsets); self._processOffsetData(data.offsets);
} };
self.fromHistoryData = function(data) { self.fromHistoryData = function(data) {
self._processStateData(data.state); self._processStateData(data.state);
self._processTemperatureHistoryData(data.tempHistory); self._processTemperatureHistoryData(data.tempHistory);
self._processOffsetData(data.offsets); self._processOffsetData(data.offsets);
} };
self._processStateData = function(data) { self._processStateData = function(data) {
self.isErrorOrClosed(data.flags.closedOrError); self.isErrorOrClosed(data.flags.closedOrError);
@ -128,7 +130,7 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
self.isError(data.flags.error); self.isError(data.flags.error);
self.isReady(data.flags.ready); self.isReady(data.flags.ready);
self.isLoading(data.flags.loading); self.isLoading(data.flags.loading);
} };
self._processTemperatureUpdateData = function(data) { self._processTemperatureUpdateData = function(data) {
if (data.length == 0) if (data.length == 0)
@ -144,8 +146,13 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
} }
} }
self.bedTemp["actual"](lastData.bed.actual); if (lastData.hasOwnProperty("bed")) {
self.bedTemp["target"](lastData.bed.target); self.hasBed(true);
self.bedTemp["actual"](lastData.bed.actual);
self.bedTemp["target"](lastData.bed.target);
} else {
self.hasBed(false);
}
if (!CONFIG_TEMPERATURE_GRAPH) return; if (!CONFIG_TEMPERATURE_GRAPH) return;
@ -156,12 +163,12 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
}); });
self.updatePlot(); self.updatePlot();
} };
self._processTemperatureHistoryData = function(data) { self._processTemperatureHistoryData = function(data) {
self.temperatures = self._processTemperatureData(data); self.temperatures = self._processTemperatureData(data);
self.updatePlot(); self.updatePlot();
} };
self._processOffsetData = function(data) { self._processOffsetData = function(data) {
var tools = self.tools(); var tools = self.tools();
@ -174,7 +181,7 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
if (data.hasOwnProperty("bed")) { if (data.hasOwnProperty("bed")) {
self.bedTemp["offset"](data["bed"]); self.bedTemp["offset"](data["bed"]);
} }
} };
self._processTemperatureData = function(data, result) { self._processTemperatureData = function(data, result) {
var types = _.keys(self.heaterOptions()); var types = _.keys(self.heaterOptions());
@ -183,6 +190,7 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
if (!result) { if (!result) {
result = {}; result = {};
} }
_.each(types, function(type) { _.each(types, function(type) {
if (!result.hasOwnProperty(type)) { if (!result.hasOwnProperty(type)) {
result[type] = {actual: [], target: []}; result[type] = {actual: [], target: []};
@ -198,11 +206,13 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
if (!d[type]) return; if (!d[type]) return;
result[type].actual.push([time, d[type].actual]); result[type].actual.push([time, d[type].actual]);
result[type].target.push([time, d[type].target]); result[type].target.push([time, d[type].target]);
self.hasBed(self.hasBed() || (type == "bed"));
}) })
}); });
return result; return result;
} };
self.updatePlot = function() { self.updatePlot = function() {
var graph = $("#temperature-graph"); var graph = $("#temperature-graph");
@ -212,6 +222,10 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
if (!heaterOptions) return; if (!heaterOptions) return;
_.each(_.keys(heaterOptions), function(type) { _.each(_.keys(heaterOptions), function(type) {
if (type == "bed" && !self.hasBed()) {
return;
}
var actuals = []; var actuals = [];
var targets = []; var targets = [];
@ -237,7 +251,7 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
$.plot(graph, data, self.plotOptions); $.plot(graph, data, self.plotOptions);
} }
} };
self.setTarget = function(item) { self.setTarget = function(item) {
var value = item.newTarget(); var value = item.newTarget();

View file

@ -21,6 +21,9 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
self.filters = self.settings.terminalFilters; self.filters = self.settings.terminalFilters;
self.filterRegex = undefined; self.filterRegex = undefined;
self.cmdHistory = [];
self.cmdHistoryIdx = -1;
self.activeFilters = ko.observableArray([]); self.activeFilters = ko.observableArray([]);
self.activeFilters.subscribe(function(e) { self.activeFilters.subscribe(function(e) {
self.updateFilterRegex(); self.updateFilterRegex();
@ -30,25 +33,25 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
self.fromCurrentData = function(data) { self.fromCurrentData = function(data) {
self._processStateData(data.state); self._processStateData(data.state);
self._processCurrentLogData(data.logs); self._processCurrentLogData(data.logs);
} };
self.fromHistoryData = function(data) { self.fromHistoryData = function(data) {
self._processStateData(data.state); self._processStateData(data.state);
self._processHistoryLogData(data.logHistory); self._processHistoryLogData(data.logHistory);
} };
self._processCurrentLogData = function(data) { self._processCurrentLogData = function(data) {
if (!self.log) if (!self.log)
self.log = [] self.log = [];
self.log = self.log.concat(data) self.log = self.log.concat(data);
self.log = self.log.slice(-300) self.log = self.log.slice(-300);
self.updateOutput(); self.updateOutput();
} };
self._processHistoryLogData = function(data) { self._processHistoryLogData = function(data) {
self.log = data; self.log = data;
self.updateOutput(); self.updateOutput();
} };
self._processStateData = function(data) { self._processStateData = function(data) {
self.isErrorOrClosed(data.flags.closedOrError); self.isErrorOrClosed(data.flags.closedOrError);
@ -58,7 +61,7 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
self.isError(data.flags.error); self.isError(data.flags.error);
self.isReady(data.flags.ready); self.isReady(data.flags.ready);
self.isLoading(data.flags.loading); self.isLoading(data.flags.loading);
} };
self.updateFilterRegex = function() { self.updateFilterRegex = function() {
var filterRegexStr = self.activeFilters().join("|").trim(); var filterRegexStr = self.activeFilters().join("|").trim();
@ -68,7 +71,7 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
self.filterRegex = new RegExp(filterRegexStr); self.filterRegex = new RegExp(filterRegexStr);
} }
console.log("Terminal filter regex: " + filterRegexStr); console.log("Terminal filter regex: " + filterRegexStr);
} };
self.updateOutput = function() { self.updateOutput = function() {
if (!self.log) if (!self.log)
@ -86,10 +89,13 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
if (self.autoscrollEnabled()) { if (self.autoscrollEnabled()) {
container.scrollTop(container[0].scrollHeight - container.height()) container.scrollTop(container[0].scrollHeight - container.height())
} }
} };
self.sendCommand = function() { self.sendCommand = function() {
var command = self.command(); var command = self.command();
if (!command) {
return;
}
var re = /^([gmt][0-9]+)(\s.*)?/; var re = /^([gmt][0-9]+)(\s.*)?/;
var commandMatch = command.match(re); var commandMatch = command.match(re);
@ -105,14 +111,45 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
contentType: "application/json; charset=UTF-8", contentType: "application/json; charset=UTF-8",
data: JSON.stringify({"command": command}) data: JSON.stringify({"command": command})
}); });
self.cmdHistory.push(command);
self.cmdHistory.slice(-300); // just to set a sane limit to how many manually entered commands will be saved...
self.cmdHistoryIdx = self.cmdHistory.length;
self.command(""); self.command("");
} }
} };
self.handleEnter = function(event) { self.handleKeyDown = function(event) {
var keyCode = event.keyCode;
if (keyCode == 38 || keyCode == 40) {
if (keyCode == 38 && self.cmdHistory.length > 0 && self.cmdHistoryIdx > 0) {
self.cmdHistoryIdx--;
} else if (keyCode == 40 && self.cmdHistoryIdx < self.cmdHistory.length - 1) {
self.cmdHistoryIdx++;
}
if (self.cmdHistoryIdx >= 0 && self.cmdHistoryIdx < self.cmdHistory.length) {
self.command(self.cmdHistory[self.cmdHistoryIdx]);
}
// prevent the cursor from being moved to the beginning of the input field (this is actually the reason
// why we do the arrow key handling in the keydown event handler, keyup would be too late already to
// prevent this from happening, causing a jumpy cursor)
return false;
}
// do not prevent default action
return true;
};
self.handleKeyUp = function(event) {
if (event.keyCode == 13) { if (event.keyCode == 13) {
self.sendCommand(); self.sendCommand();
} }
}
// do not prevent default action
return true;
};
} }

View file

@ -279,6 +279,38 @@ table {
} }
} }
} }
// log files
&.settings_logs_name {
text-overflow: ellipsis;
text-align: left;
}
&.settings_logs_size {
text-align: right;
width: 70px;
}
&.settings_logs_date {
text-align: left;
width: 130px;
}
&.settings_logs_action {
text-align: center;
width: 70px;
a {
text-decoration: none;
color: #000;
&.disabled {
color: #ccc;
cursor: default;
}
}
}
} }
} }

View file

@ -27,6 +27,7 @@
var CONFIG_GCODEFILESPERPAGE = 5; var CONFIG_GCODEFILESPERPAGE = 5;
var CONFIG_TIMELAPSEFILESPERPAGE = 10; var CONFIG_TIMELAPSEFILESPERPAGE = 10;
var CONFIG_LOGFILESPERPAGE = 10;
var CONFIG_USERSPERPAGE = 10; var CONFIG_USERSPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}"; var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %} var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
@ -273,7 +274,7 @@
<!-- ko foreach: tools --> <!-- ko foreach: tools -->
<tr data-bind="template: { name: 'temprow-template' }"></tr> <tr data-bind="template: { name: 'temprow-template' }"></tr>
<!-- /ko --> <!-- /ko -->
<tr data-bind="template: { name: 'temprow-template', data: bedTemp }"></tr> <tr data-bind="template: { name: 'temprow-template', data: bedTemp }, visible: hasBed"></tr>
</table> </table>
<script type="text/html" id="temprow-template"> <script type="text/html" id="temprow-template">
@ -516,7 +517,7 @@
</div> </div>
<div class="input-append" style="display: none;" data-bind="visible: loginState.isUser"> <div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { handleEnter(e); } }, enable: isOperational() && loginState.isUser()"> <input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { return handleKeyUp(e); }, keydown: function(d,e) { return handleKeyDown(e); } }, enable: isOperational() && loginState.isUser()">
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">Send</button> <button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">Send</button>
</div> </div>
</div> </div>
@ -644,6 +645,7 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/terminal.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/terminal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/timelapse.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/timelapse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/users.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/users.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/log.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/dataupdater.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/app/dataupdater.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/helpers.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/app/helpers.js') }}"></script>

View file

@ -20,6 +20,7 @@
<li class="nav-header">OctoPrint</li> <li class="nav-header">OctoPrint</li>
<li><a href="#settings_folder" data-toggle="tab">Folders</a></li> <li><a href="#settings_folder" data-toggle="tab">Folders</a></li>
<li><a href="#settings_appearance" data-toggle="tab">Appearance</a></li> <li><a href="#settings_appearance" data-toggle="tab">Appearance</a></li>
<li><a href="#settings_logs" data-toggle="tab">Logs</a></li>
</ul> </ul>
<div class="tab-content span8"> <div class="tab-content span8">
@ -83,61 +84,83 @@
<div class="tab-pane" id="settings_printerParameters"> <div class="tab-pane" id="settings_printerParameters">
<form class="form-horizontal"> <form class="form-horizontal">
<div class="control-group"> <div class="control-group">
<label class="control-label" for="settings-movementSpeedX">Movement Speed X Axis</label> <label class="control-label">Axis</label>
<div class="controls"> <div class="controls form-inline">
<label>X:</label>
<div class="input-append"> <div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedX" id="settings-movementSpeedX"> <input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedX" id="settings-movementSpeedX">
<span class="add-on">mm/min</span> <span class="add-on">mm/min</span>
</div> </div>
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertX" id="settings-printerInvertX"> Invert control
</label>
</div> </div>
</div> <div class="controls form-inline">
<div class="control-group"> <label>Y:</label>
<label class="control-label" for="settings-movementSpeedY">Movement Speed Y Axis</label>
<div class="controls">
<div class="input-append"> <div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedY" id="settings-movementSpeedY"> <input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedY" id="settings-movementSpeedY">
<span class="add-on">mm/min</span> <span class="add-on">mm/min</span>
</div> </div>
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertY" id="settings-printerInvertY"> Invert control
</label>
</div> </div>
</div> <div class="controls form-inline">
<div class="control-group"> <label>Z:</label>
<label class="control-label" for="settings-movementSpeedZ">Movement Speed Z Axis</label>
<div class="controls">
<div class="input-append"> <div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedZ" id="settings-movementSpeedZ"> <input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedZ" id="settings-movementSpeedZ">
<span class="add-on">mm/min</span> <span class="add-on">mm/min</span>
</div> </div>
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertZ" id="settings-printerInvertZ"> Invert control
</label>
</div> </div>
</div> <div class="controls form-inline">
<div class="control-group"> <label>E:</label>
<label class="control-label" for="settings-movementSpeedE">Movement Speed Extruder</label>
<div class="controls">
<div class="input-append"> <div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedE" id="settings-movementSpeedE"> <input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedE" id="settings-movementSpeedE">
<span class="add-on">mm/min</span> <span class="add-on">mm/min</span>
</div> </div>
</div> </div>
</div> </div>
<div class="control-group">
<div class="controls">
Invert controls
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertX" id="settings-printerInvertX"> X axis
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertY" id="settings-printerInvertY"> Y axis
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertZ" id="settings-printerInvertZ"> Z axis
</label>
</div>
</div>
<div class="control-group"> <div class="control-group">
<label class="control-label" for="settings-numExtruders">Number of Extruders</label> <label class="control-label" for="settings-numExtruders">Number of Extruders</label>
<div class="controls"> <div class="controls">
<input type="number" class="input-mini text-right" min="1" max="5" data-bind="value: printer_numExtruders" id="settings-numExtruders"> <input type="number" class="input-mini text-right" min="1" max="5" data-bind="value: printer_numExtruders" id="settings-numExtruders">
</div> </div>
</div> </div>
<div class="control-group">
<label class="control-label" for="settings-extruderOffsets">Extruder Offsets</label>
<!-- ko foreach: ko_printer_extruderOffsets -->
<div class="controls form-inline">
<label>X:</label>
<div class="input-append">
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: x">
<span class="add-on">mm</span>
</div>
<label>Y:</label>
<div class="input-append">
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: y">
<span class="add-on">mm</span>
</div>
</div>
<!-- /ko -->
</div>
<div class="control-group">
<label class="control-label" for="settings-bedSize">Bed Size</label>
<div class="controls form-inline">
<label>X:</label>
<div class="input-append">
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: printer_bedDimensionX" id="settings-bedX">
<span class="add-on">mm</span>
</div>
<label>Y:</label>
<div class="input-append">
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: printer_bedDimensionY" id="settings-bedY">
<span class="add-on">mm</span>
</div>
</div>
</div>
</form> </form>
</div> </div>
<div class="tab-pane" id="settings_webcam"> <div class="tab-pane" id="settings_webcam">
@ -224,6 +247,13 @@
</label> </label>
</div> </div>
</div> </div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_repetierTargetTemp" id="settings-featureRepetierTargetTemp"> Support <code>TargetExtr%n</code>/<code>TargetBed</code> target temperature format <span class="label">Repetier</span>
</label>
</div>
</div>
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<label class="checkbox"> <label class="checkbox">
@ -535,6 +565,54 @@
</div> </div>
{% endif %} {% endif %}
<div class="tab-pane" id="settings_logs" data-bind="allowBindings: false">
<div id="logs">
<h1>Logs</h1>
<div class="pull-right">
<small>
Sort by: <a href="#" data-bind="click: function() { listHelper.changeSorting('name'); }">Name (ascending)</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('modification'); }">Modification date (descending)</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('size'); }">Size (descending)</a>
</small>
</div>
<table class="table table-striped table-hover table-condensed table-hover" id="log_files">
<thead>
<tr>
<th class="settings_logs_name">Name</th>
<th class="settings_logs_size">Size</th>
<th class="settings_logs_date">Date</th>
<th class="settings_logs_action">Action</th>
</tr>
</thead>
<tbody data-bind="foreach: listHelper.paginatedItems">
<tr data-bind="attr: {title: name}">
<td class="settings_logs_name" data-bind="text: name"></td>
<td class="settings_logs_size" data-bind="text: formatSize(size)"></td>
<td class="settings_logs_date" data-bind="text: formatDate(date)"></td>
<td class="settings_logs_action">
<a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-download" data-bind="attr: {href: refs.download}"></a>
</td>
</tr>
</tbody>
</table>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: listHelper.currentPage() === 0}">
<a href="#" data-bind="click: listHelper.prevPage">«</a>
</li>
</ul>
<ul data-bind="foreach: listHelper.pages">
<li data-bind="css: { active: $data.number === $root.listHelper.currentPage(), disabled: $data.number === -1 }">
<a href="#" data-bind="text: $data.text, click: function() { $root.listHelper.changePage($data.number); }"></a>
</li>
</ul>
<ul>
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}">
<a href="#" data-bind="click: listHelper.nextPage">»</a>
</li>
</ul>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -191,6 +191,24 @@ def silentRemove(file):
pass pass
def sanitizeAscii(line):
return unicode(line, 'ascii', 'replace').encode('ascii', 'replace').rstrip()
def filterNonAscii(line):
"""
Returns True if the line contains non-ascii characters, false otherwise
@param line the line to test
"""
try:
unicode(line, 'ascii').encode('ascii')
return False
except ValueError:
return True
def getJsonCommandFromRequest(request, valid_commands): def getJsonCommandFromRequest(request, valid_commands):
if not "application/json" in request.headers["Content-Type"]: if not "application/json" in request.headers["Content-Type"]:
return None, None, make_response("Expected content-type JSON", 400) return None, None, make_response("Expected content-type JSON", 400)

View file

@ -23,7 +23,7 @@ from octoprint.settings import settings
from octoprint.events import eventManager, Events from octoprint.events import eventManager, Events
from octoprint.filemanager.destinations import FileDestinations from octoprint.filemanager.destinations import FileDestinations
from octoprint.gcodefiles import isGcodeFileName from octoprint.gcodefiles import isGcodeFileName
from octoprint.util import getExceptionString, getNewTimeout from octoprint.util import getExceptionString, getNewTimeout, sanitizeAscii, filterNonAscii
from octoprint.util.virtual import VirtualPrinter from octoprint.util.virtual import VirtualPrinter
try: try:
@ -136,10 +136,8 @@ class MachineCom(object):
self._baudrateDetectList = baudrateList() self._baudrateDetectList = baudrateList()
self._baudrateDetectRetry = 0 self._baudrateDetectRetry = 0
self._temp = {} self._temp = {}
self._targetTemp = {}
self._tempOffset = {} self._tempOffset = {}
self._bedTemp = 0 self._bedTemp = None
self._bedTargetTemp = 0
self._bedTempOffset = 0 self._bedTempOffset = 0
self._commandQueue = queue.Queue() self._commandQueue = queue.Queue()
self._currentZ = None self._currentZ = None
@ -147,18 +145,42 @@ class MachineCom(object):
self._heatupWaitTimeLost = 0.0 self._heatupWaitTimeLost = 0.0
self._currentExtruder = 0 self._currentExtruder = 0
self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"])
self._currentLine = 1
self._resendDelta = None
self._lastLines = deque([], 50)
# SD status data
self._sdAvailable = False
self._sdFileList = False
self._sdFiles = []
# print job
self._currentFile = None
# regexes
floatPattern = "[-+]?[0-9]*\.?[0-9]+"
positiveFloatPattern = "[+]?[0-9]*\.?[0-9]+"
intPattern = "\d+"
self._regex_command = re.compile("^\s*([GM]\d+|T)")
self._regex_float = re.compile(floatPattern)
self._regex_paramZFloat = re.compile("Z(%s)" % floatPattern)
self._regex_paramSInt = re.compile("S(%s)" % intPattern)
self._regex_paramNInt = re.compile("N(%s)" % intPattern)
self._regex_paramTInt = re.compile("T(%s)" % intPattern)
self._regex_minMaxError = re.compile("Error:[0-9]\n")
self._regex_sdPrintingByte = re.compile("([0-9]*)/([0-9]*)")
self._regex_sdFileOpened = re.compile("File opened:\s*(.*?)\s+Size:\s*(%s)" % intPattern)
# Regex matching temperature entries in line. Groups will be as follows: # Regex matching temperature entries in line. Groups will be as follows:
# - 1: whole tool designator incl. optional toolNumber ("T", "Tn", "B") # - 1: whole tool designator incl. optional toolNumber ("T", "Tn", "B")
# - 2: toolNumber, if given ("", "n", "") # - 2: toolNumber, if given ("", "n", "")
# - 3: actual temperature # - 3: actual temperature
# - 4: whole target substring, if given (e.g. " / 22.0") # - 4: whole target substring, if given (e.g. " / 22.0")
# - 5: target temperature # - 5: target temperature
self._tempRegex = re.compile("(B|T(\d*)):\s*([-+]?\d*\.?\d*)(\s*\/?\s*([-+]?\d*\.?\d*))?") self._regex_temp = re.compile("(B|T(\d*)):\s*(%s)(\s*\/?\s*(%s))?" % (positiveFloatPattern, positiveFloatPattern))
self._regex_repetierTempExtr = re.compile("TargetExtr([0-9]+):(%s)" % positiveFloatPattern)
self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"]) self._regex_repetierTempBed = re.compile("TargetBed:(%s)" % positiveFloatPattern)
self._currentLine = 1
self._resendDelta = None
self._lastLines = deque([], 50)
# multithreading locks # multithreading locks
self._sendNextLock = threading.Lock() self._sendNextLock = threading.Lock()
@ -169,14 +191,6 @@ class MachineCom(object):
self.thread.daemon = True self.thread.daemon = True
self.thread.start() self.thread.start()
# SD status data
self._sdAvailable = False
self._sdFileList = False
self._sdFiles = []
# print job
self._currentFile = None
def __del__(self): def __del__(self):
self.close() self.close()
@ -341,6 +355,7 @@ class MachineCom(object):
if self._currentFile is not None: if self._currentFile is not None:
payload = { payload = {
"file": self._currentFile.getFilename(), "file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation() "origin": self._currentFile.getFileLocation()
} }
eventManager().fire(Events.PRINT_FAILED, payload) eventManager().fire(Events.PRINT_FAILED, payload)
@ -367,16 +382,17 @@ class MachineCom(object):
if self._currentFile is None: if self._currentFile is None:
raise ValueError("No file selected for printing") raise ValueError("No file selected for printing")
wasPaused = self.isPaused()
self._printSection = "CUSTOM"
self._changeState(self.STATE_PRINTING)
eventManager().fire(Events.PRINT_STARTED, {
"file": self._currentFile.getFilename(),
"origin": self._currentFile.getFileLocation()
})
try: try:
self._currentFile.start() self._currentFile.start()
wasPaused = self.isPaused()
self._changeState(self.STATE_PRINTING)
eventManager().fire(Events.PRINT_STARTED, {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation()
})
if self.isSdFileSelected(): if self.isSdFileSelected():
if wasPaused: if wasPaused:
self.sendCommand("M26 S0") self.sendCommand("M26 S0")
@ -438,6 +454,7 @@ class MachineCom(object):
eventManager().fire(Events.PRINT_CANCELLED, { eventManager().fire(Events.PRINT_CANCELLED, {
"file": self._currentFile.getFilename(), "file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation() "origin": self._currentFile.getFileLocation()
}) })
@ -454,6 +471,7 @@ class MachineCom(object):
eventManager().fire(Events.PRINT_RESUMED, { eventManager().fire(Events.PRINT_RESUMED, {
"file": self._currentFile.getFilename(), "file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation() "origin": self._currentFile.getFileLocation()
}) })
elif pause and self.isPrinting(): elif pause and self.isPrinting():
@ -463,6 +481,7 @@ class MachineCom(object):
eventManager().fire(Events.PRINT_PAUSED, { eventManager().fire(Events.PRINT_PAUSED, {
"file": self._currentFile.getFilename(), "file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation() "origin": self._currentFile.getFileLocation()
}) })
@ -521,7 +540,7 @@ class MachineCom(object):
def _parseTemperatures(self, line): def _parseTemperatures(self, line):
result = {} result = {}
maxToolNum = 0 maxToolNum = 0
for match in re.finditer(self._tempRegex, line): for match in re.finditer(self._regex_temp, line):
tool = match.group(1) tool = match.group(1)
toolNumber = int(match.group(2)) if match.group(2) and len(match.group(2)) > 0 else None toolNumber = int(match.group(2)) if match.group(2) and len(match.group(2)) > 0 else None
if toolNumber > maxToolNum: if toolNumber > maxToolNum:
@ -558,12 +577,24 @@ class MachineCom(object):
continue continue
toolNum, actual, target = parsedTemps[tool] toolNum, actual, target = parsedTemps[tool]
self._temp[toolNum] = (actual, target) if target is not None:
self._temp[toolNum] = (actual, target)
elif toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple):
(oldActual, oldTarget) = self._temp[toolNum]
self._temp[toolNum] = (actual, oldTarget)
else:
self._temp[toolNum] = (actual, None)
# bed temperature # bed temperature
if "B" in parsedTemps.keys(): if "B" in parsedTemps.keys():
toolNum, actual, target = parsedTemps["B"] toolNum, actual, target = parsedTemps["B"]
self._bedTemp = (actual, target) if target is not None:
self._bedTemp = (actual, target)
elif self._bedTemp is not None and isinstance(self._bedTemp, tuple):
(oldActual, oldTarget) = self._bedTemp
self._bedTemp = (actual, oldTarget)
else:
self._bedTemp = (actual, None)
def _monitor(self): def _monitor(self):
feedbackControls = settings().getFeedbackControls() feedbackControls = settings().getFeedbackControls()
@ -588,6 +619,8 @@ class MachineCom(object):
startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"]) startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"])
heatingUp = False heatingUp = False
swallowOk = False swallowOk = False
supportRepetierTargetTemp = settings().getBoolean(["feature", "repetierTargetTemp"])
while True: while True:
try: try:
line = self._readline() line = self._readline()
@ -600,7 +633,11 @@ class MachineCom(object):
##~~ SD file list ##~~ SD file list
# if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing
if self._sdFileList and isGcodeFileName(line.strip().lower()) and not 'End file list' in line: if self._sdFileList and isGcodeFileName(line.strip().lower()) and not 'End file list' in line:
self._sdFiles.append(line.strip().lower()) filename = line.strip().lower()
if filterNonAscii(filename):
self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename)
else:
self._sdFiles.append(filename)
continue continue
##~~ Temperature processing ##~~ Temperature processing
@ -615,6 +652,33 @@ class MachineCom(object):
t = time.time() t = time.time()
self._heatupWaitTimeLost = t - self._heatupWaitStartTime self._heatupWaitTimeLost = t - self._heatupWaitStartTime
self._heatupWaitStartTime = t self._heatupWaitStartTime = t
elif supportRepetierTargetTemp:
matchExtr = self._regex_repetierTempExtr.match(line)
matchBed = self._regex_repetierTempBed.match(line)
if matchExtr is not None:
toolNum = int(matchExtr.group(1))
try:
target = float(matchExtr.group(2))
if toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple):
(actual, oldTarget) = self._temp[toolNum]
self._temp[toolNum] = (actual, target)
else:
self._temp[toolNum] = (None, target)
self._callback.mcTempUpdate(self._temp, self._bedTemp)
except ValueError:
pass
elif matchBed is not None:
try:
target = float(matchBed.group(1))
if self._bedTemp is not None and isinstance(self._bedTemp, tuple):
(actual, oldTarget) = self._bedTemp
self._bedTemp = (actual, target)
else:
self._bedTemp = (None, target)
self._callback.mcTempUpdate(self._temp, self._bedTemp)
except ValueError:
pass
##~~ SD Card handling ##~~ SD Card handling
elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line: elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line:
@ -638,12 +702,12 @@ class MachineCom(object):
self._callback.mcSdFiles(self._sdFiles) self._callback.mcSdFiles(self._sdFiles)
elif 'SD printing byte' in line: elif 'SD printing byte' in line:
# answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d"
match = re.search("([0-9]*)/([0-9]*)", line) match = self._regex_sdPrintingByte.search("([0-9]*)/([0-9]*)", line)
self._currentFile.setFilepos(int(match.group(1))) self._currentFile.setFilepos(int(match.group(1)))
self._callback.mcProgress() self._callback.mcProgress()
elif 'File opened' in line: elif 'File opened' in line:
# answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d"
match = re.search("File opened:\s*(.*?)\s+Size:\s*([0-9]*)", line) match = self._regex_sdFileOpened.search(line)
self._currentFile = PrintingSdFileInformation(match.group(1), int(match.group(2))) self._currentFile = PrintingSdFileInformation(match.group(1), int(match.group(2)))
elif 'File selected' in line: elif 'File selected' in line:
# final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected"
@ -655,7 +719,6 @@ class MachineCom(object):
}) })
elif 'Writing to file' in line: elif 'Writing to file' in line:
# anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" # anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s"
self._printSection = "CUSTOM"
self._changeState(self.STATE_PRINTING) self._changeState(self.STATE_PRINTING)
line = "ok" line = "ok"
elif 'Done printing file' in line: elif 'Done printing file' in line:
@ -665,8 +728,9 @@ class MachineCom(object):
self._changeState(self.STATE_OPERATIONAL) self._changeState(self.STATE_OPERATIONAL)
eventManager().fire(Events.PRINT_DONE, { eventManager().fire(Events.PRINT_DONE, {
"file": self._currentFile.getFilename(), "file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation(), "origin": self._currentFile.getFileLocation(),
"time": time.time() - self._currentFile.getStartTime() "time": self.getPrintTime()
}) })
elif 'Done saving file' in line: elif 'Done saving file' in line:
self.refreshSdFiles() self.refreshSdFiles()
@ -884,7 +948,7 @@ class MachineCom(object):
# Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n" # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
# But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!" # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
# So we can have an extra newline in the most common case. Awesome work people. # So we can have an extra newline in the most common case. Awesome work people.
if re.match('Error:[0-9]\n', line): if self._regex_minMaxError.match(line):
line = line.rstrip() + self._readline() line = line.rstrip() + self._readline()
#Skip the communication errors, as those get corrected. #Skip the communication errors, as those get corrected.
if 'checksum mismatch' in line \ if 'checksum mismatch' in line \
@ -914,7 +978,7 @@ class MachineCom(object):
if ret == '': if ret == '':
#self._log("Recv: TIMEOUT") #self._log("Recv: TIMEOUT")
return '' return ''
self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip())) self._log("Recv: %s" % sanitizeAscii(ret))
return ret return ret
def _sendNext(self): def _sendNext(self):
@ -928,7 +992,7 @@ class MachineCom(object):
payload = { payload = {
"local": self._currentFile.getLocalFilename(), "local": self._currentFile.getLocalFilename(),
"remote": self._currentFile.getRemoteFilename(), "remote": self._currentFile.getRemoteFilename(),
"time": time.time() - self._currentFile.getStartTime() "time": self.getPrintTime()
} }
self._currentFile = None self._currentFile = None
@ -939,8 +1003,9 @@ class MachineCom(object):
else: else:
payload = { payload = {
"file": self._currentFile.getFilename(), "file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation(), "origin": self._currentFile.getFileLocation(),
"time": time.time() - self._currentFile.getStartTime() "time": self.getPrintTime()
} }
self._callback.mcPrintjobDone() self._callback.mcPrintjobDone()
self._changeState(self.STATE_OPERATIONAL) self._changeState(self.STATE_OPERATIONAL)
@ -993,7 +1058,7 @@ class MachineCom(object):
return return
if not self.isStreaming(): if not self.isStreaming():
gcode = re.search("^\s*([GM]\d+|T)", cmd) gcode = self._regex_command.search(cmd)
if gcode: if gcode:
gcode = gcode.group(1) gcode = gcode.group(1)
@ -1042,20 +1107,22 @@ class MachineCom(object):
self.close(True) self.close(True)
def _gcode_T(self, cmd): def _gcode_T(self, cmd):
toolMatch = re.search('T([0-9]+)', cmd) toolMatch = self._regex_paramTInt.search(cmd)
if toolMatch: if toolMatch:
self._currentExtruder = int(toolMatch.group(1)) self._currentExtruder = int(toolMatch.group(1))
return cmd return cmd
def _gcode_G0(self, cmd): def _gcode_G0(self, cmd):
if 'Z' in cmd: if 'Z' in cmd:
try: match = self._regex_paramZFloat.search(cmd)
z = float(re.search('Z([0-9\.]*)', cmd).group(1)) if match:
if self._currentZ != z: try:
self._currentZ = z z = float(match.group(1))
self._callback.mcZChange(z) if self._currentZ != z:
except ValueError: self._currentZ = z
pass self._callback.mcZChange(z)
except ValueError:
pass
return cmd return cmd
_gcode_G1 = _gcode_G0 _gcode_G1 = _gcode_G0
@ -1066,22 +1133,32 @@ class MachineCom(object):
def _gcode_M104(self, cmd): def _gcode_M104(self, cmd):
toolNum = self._currentExtruder toolNum = self._currentExtruder
toolMatch = re.search('T([0-9]+)', cmd) toolMatch = self._regex_paramTInt.search(cmd)
if toolMatch: if toolMatch:
toolNum = int(toolMatch.group(1)) toolNum = int(toolMatch.group(1))
match = re.search('S([0-9]+)', cmd) match = self._regex_paramSInt.search(cmd)
if match: if match:
try: try:
self._targetTemp[toolNum] = float(match.group(1)) target = float(match.group(1))
if toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple):
(actual, oldTarget) = self._temp[toolNum]
self._temp[toolNum] = (actual, target)
else:
self._temp[toolNum] = (None, target)
except ValueError: except ValueError:
pass pass
return cmd return cmd
def _gcode_M140(self, cmd): def _gcode_M140(self, cmd):
match = re.search('S([0-9]+)', cmd) match = self._regex_paramSInt.search(cmd)
if match: if match:
try: try:
self._bedTargetTemp = float(match.group(1)) target = float(match.group(1))
if self._bedTemp is not None and isinstance(self._bedTemp, tuple):
(actual, oldTarget) = self._bedTemp
self._bedTemp = (actual, target)
else:
self._bedTemp = (None, target)
except ValueError: except ValueError:
pass pass
return cmd return cmd
@ -1096,7 +1173,7 @@ class MachineCom(object):
def _gcode_M110(self, cmd): def _gcode_M110(self, cmd):
newLineNumber = None newLineNumber = None
match = re.search("N([0-9]+)", cmd) match = self._regex_paramNInt.search(cmd)
if match: if match:
try: try:
newLineNumber = int(match.group(1)) newLineNumber = int(match.group(1))
@ -1232,16 +1309,19 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
def __init__(self, filename, offsetCallback): def __init__(self, filename, offsetCallback):
PrintingFileInformation.__init__(self, filename) PrintingFileInformation.__init__(self, filename)
self._filehandle = None
self._filesetMenuModehandle = None self._filesetMenuModehandle = None
self._lineCount = None self._lineCount = None
self._firstLine = None self._firstLine = None
self._currentTool = 0 self._currentTool = 0
self._offsetCallback = offsetCallback self._offsetCallback = offsetCallback
self._tempCommandPattern = re.compile("M(104|109|140|190)") self._regex_tempCommand = re.compile("M(104|109|140|190)")
self._tempCommandTemperaturePattern = re.compile("S([-+]?\d*\.?\d*)") self._regex_tempCommandTemperature = re.compile("S([-+]?\d*\.?\d*)")
self._tempCommandToolPattern = re.compile("T(\d+)") self._regex_tempCommandTool = re.compile("T(\d+)")
self._toolCommandPattern = re.compile("^T(\d+)") self._regex_toolCommand = re.compile("^T(\d+)")
if not os.path.exists(self._filename) or not os.path.isfile(self._filename): if not os.path.exists(self._filename) or not os.path.isfile(self._filename):
raise IOError("File %s does not exist" % self._filename) raise IOError("File %s does not exist" % self._filename)
@ -1295,14 +1375,14 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
line = line[0:line.find(";")] line = line[0:line.find(";")]
line = line.strip() line = line.strip()
if len(line) > 0: if len(line) > 0:
toolMatch = self._toolCommandPattern.match(line) toolMatch = self._regex_toolCommand.match(line)
if toolMatch is not None: if toolMatch is not None:
# track tool changes # track tool changes
self._currentTool = int(toolMatch.group(1)) self._currentTool = int(toolMatch.group(1))
else: else:
## apply offsets ## apply offsets
if self._offsetCallback is not None: if self._offsetCallback is not None:
tempMatch = self._tempCommandPattern.match(line) tempMatch = self._regex_tempCommand.match(line)
if tempMatch is not None: if tempMatch is not None:
# if we have a temperature command, retrieve current offsets # if we have a temperature command, retrieve current offsets
tempOffset, bedTempOffset = self._offsetCallback() tempOffset, bedTempOffset = self._offsetCallback()
@ -1310,7 +1390,7 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
# extruder temperature, determine which one and retrieve corresponding offset # extruder temperature, determine which one and retrieve corresponding offset
toolNum = self._currentTool toolNum = self._currentTool
toolNumMatch = self._tempCommandToolPattern.search(line) toolNumMatch = self._regex_tempCommandTool.search(line)
if toolNumMatch is not None: if toolNumMatch is not None:
try: try:
toolNum = int(toolNumMatch.group(1)) toolNum = int(toolNumMatch.group(1))
@ -1327,7 +1407,7 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
if not offset == 0: if not offset == 0:
# if we have an offset != 0, we need to get the temperature to be set and apply the offset to it # if we have an offset != 0, we need to get the temperature to be set and apply the offset to it
tempValueMatch = self._tempCommandTemperaturePattern.search(line) tempValueMatch = self._regex_tempCommandTemperature.search(line)
if tempValueMatch is not None: if tempValueMatch is not None:
try: try:
temp = float(tempValueMatch.group(1)) temp = float(tempValueMatch.group(1))

View file

@ -104,10 +104,14 @@ class VirtualPrinter():
for i in range(len(self.temp)): for i in range(len(self.temp)):
allTemps.append((i, self.temp[i], self.targetTemp[i])) allTemps.append((i, self.temp[i], self.targetTemp[i]))
allTempsString = " ".join(map(lambda x: "T%d:%.2f /%.2f" % x, allTemps)) allTempsString = " ".join(map(lambda x: "T%d:%.2f /%.2f" % x, allTemps))
if settings().getBoolean(["devel", "virtualPrinted", "includeCurrentToolInTemps"]):
self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f %s @:64\n" % (self.temp[self.currentExtruder], self.targetTemp[self.currentExtruder] + 1, self.bedTemp, self.bedTargetTemp, allTempsString)) if settings().getBoolean(["devel", "virtualPrinter", "hasBed"]):
allTempsString = "B:%.2f /%.2f %s" % (self.bedTemp, self.bedTargetTemp, allTempsString)
if settings().getBoolean(["devel", "virtualPrinter", "includeCurrentToolInTemps"]):
self.readList.append("ok T:%.2f /%.2f %s @:64\n" % (self.temp[self.currentExtruder], self.targetTemp[self.currentExtruder] + 1))
else: else:
self.readList.append("ok %s B:%.2f /%.2f @:64\n" % (allTempsString, self.bedTemp, self.bedTargetTemp)) self.readList.append("ok %s @:64\n" % allTempsString)
else: else:
self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp[0], self.targetTemp[0], self.bedTemp, self.bedTargetTemp)) self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp[0], self.targetTemp[0], self.bedTemp, self.bedTargetTemp))
elif 'M20' in data: elif 'M20' in data: