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
GET /api/control/connection HTTP/1.1
GET /api/connection HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -77,7 +77,7 @@ Issue a connection command
.. sourcecode:: http
POST /api/control/connection HTTP/1.1
POST /api/connection HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -94,7 +94,7 @@ Issue a connection command
.. sourcecode:: http
POST /api/control/connection HTTP/1.1
POST /api/connection HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -114,6 +114,6 @@ Issue a connection command
Defaults to ``false`` if not set.
: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.
: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
options.

View file

@ -12,4 +12,4 @@ API Documentation
connection.rst
printer.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):
aborted = None
while True:
self._active.wait()
if aborted is not None:
filename = aborted
aborted = None
@ -570,6 +568,8 @@ class MetadataAnalyzer:
(priority, filename) = self._queue.get()
self._logger.debug("Processing file %s from queue (priority %d)" % (filename, priority))
self._active.wait()
try:
self._analyzeGcode(filename)
self._queue.task_done()

View file

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

View file

@ -2,6 +2,8 @@
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import flask
import tornado.wsgi
from sockjs.tornado import SockJSRouter
from flask import Flask, render_template, send_from_directory, make_response
from flask.ext.login import LoginManager
@ -27,8 +29,8 @@ principals = Principal(app)
admin_permission = Permission(RoleNeed("admin"))
user_permission = Permission(RoleNeed("user"))
from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection
# 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, admin_validator
from octoprint.printer import Printer, getConnectionOptions
from octoprint.settings import settings
import octoprint.gcodefiles as gcodefiles
@ -174,9 +176,23 @@ class Server():
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 + [
(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/logs/([^/]*)", LargeResponseHandler, {"path": settings().getBaseFolder("logs"), "as_attachment": True, "access_validation": admin_access_validation}),
(r".*", FallbackHandler, {"fallback": WSGIContainer(app.wsgi_app)})
])
self._server = HTTPServer(self._tornado_app)
@ -190,6 +206,8 @@ class Server():
printer.connect(port, baudrate)
try:
IOLoop.instance().start()
except KeyboardInterrupt:
logger.info("Goodbye!")
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.exception("Stacktrace follows:")

View file

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

View file

@ -11,7 +11,6 @@ import octoprint.util as util
from octoprint.filemanager.destinations import FileDestinations
from octoprint.settings import settings, valid_boolean_trues
from octoprint.server import printer, gcodeManager, eventManager, restricted_access, NO_CONTENT
from octoprint.server.util import urlForDownload
from octoprint.server.api import api
@ -66,7 +65,7 @@ def _getFileList(origin):
file.update({
"refs": {
"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
@ -169,7 +168,7 @@ def uploadGcodeFile(target):
"origin": FileDestinations.LOCAL,
"refs": {
"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:
gcodeManager.removeFile(filename)
# return an updated list of files
return readGcodeFiles()
return NO_CONTENT

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"]),
"alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]),
"sdSupport": s.getBoolean(["feature", "sdSupport"]),
"swallowOkAfterResend": s.getBoolean(["feature", "swallowOkAfterResend"])
"swallowOkAfterResend": s.getBoolean(["feature", "swallowOkAfterResend"]),
"repetierTargetTemp": s.getBoolean(["feature", "repetierTargetTemp"])
},
"serial": {
"port": connectionOptions["portPreference"],
@ -136,6 +137,7 @@ def setSettings():
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 "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 "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 tornado.web import StaticFileHandler, HTTPError
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 sockjs.tornado import SockJSConnection
@ -21,6 +25,7 @@ import octoprint.server
from octoprint.users import ApiUser
from octoprint.events import Events
def restricted_access(func, apiEnabled=True):
"""
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
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):
if "apikey" in request.values:
# Check Flask GET/POST arguments
if hasattr(request, "values") and "apikey" in request.values:
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():
return request.headers.get("X-Api-Key")
return None
@ -204,11 +230,15 @@ class LargeResponseHandler(StaticFileHandler):
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)
self._as_attachment = as_attachment
self._access_validation = access_validation
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)
abspath = os.path.abspath(os.path.join(self.root, path))
# os.path.abspath strips a trailing /
@ -273,6 +303,29 @@ class LargeResponseHandler(StaticFileHandler):
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
@ -333,7 +386,3 @@ def redirectToTornado(request, target):
redirectUrl += fragment
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,
"alwaysSendChecksum": False,
"sdSupport": True,
"swallowOkAfterResend": True
"swallowOkAfterResend": True,
"repetierTargetTemp": False
},
"folder": {
"uploads": None,
@ -143,7 +144,8 @@ default_settings = {
"forceChecksum": False,
"okWithLinenumber": False,
"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;
self.loginStateViewModel = loginStateViewModel;
@ -10,6 +10,7 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
self.gcodeFilesViewModel = gcodeFilesViewModel;
self.timelapseViewModel = timelapseViewModel;
self.gcodeViewModel = gcodeViewModel;
self.logViewModel = logViewModel;
self._socket = undefined;
self._autoReconnecting = false;
@ -38,7 +39,8 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
self._autoReconnectTrial = 0;
if ($("#offline_overlay").is(":visible")) {
$("#offline_overlay").hide();
$("#offline_overlay").hide();
self.logViewModel.requestData();
self.timelapseViewModel.requestData();
self.loginStateViewModel.requestData();
self.gcodeFilesViewModel.requestData();

View file

@ -1,5 +1,4 @@
$(function() {
//~~ Initialize view models
var loginStateViewModel = new LoginStateViewModel();
var usersViewModel = new UsersViewModel(loginStateViewModel);
@ -14,6 +13,7 @@ $(function() {
var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel);
var gcodeViewModel = new GcodeViewModel(loginStateViewModel, settingsViewModel);
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
var logViewModel = new LogViewModel(loginStateViewModel);
var dataUpdater = new DataUpdater(
loginStateViewModel,
@ -24,7 +24,8 @@ $(function() {
terminalViewModel,
gcodeFilesViewModel,
timelapseViewModel,
gcodeViewModel
gcodeViewModel,
logViewModel
);
// 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(printerStateViewModel, document.getElementById("state_accordion"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_accordion"));
@ -302,6 +309,7 @@ $(function() {
ko.applyBindings(navigationViewModel, document.getElementById("navbar"));
ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]);
ko.applyBindings(printerStateViewModel, document.getElementById("drop_overlay"));
ko.applyBindings(logViewModel, document.getElementById("logs"));
var timelapseElement = document.getElementById("timelapse");
if (timelapseElement) {
@ -316,6 +324,7 @@ $(function() {
gcodeFilesViewModel.requestData();
timelapseViewModel.requestData();
settingsViewModel.requestData();
logViewModel.requestData();
loginStateViewModel.subscribe(function(change, data) {
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_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.printer_movementSpeedX = ko.observable(undefined);
@ -19,8 +18,69 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.printer_movementSpeedE = ko.observable(undefined);
self.printer_invertAxes = 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_snapshotUrl = ko.observable(undefined);
@ -36,6 +96,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.feature_alwaysSendChecksum = ko.observable(undefined);
self.feature_sdSupport = ko.observable(undefined);
self.feature_swallowOkAfterResend = ko.observable(undefined);
self.feature_repetierTargetTemp = ko.observable(undefined);
self.serial_port = ko.observable();
self.serial_baudrate = ko.observable();
@ -145,6 +206,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.feature_alwaysSendChecksum(response.feature.alwaysSendChecksum);
self.feature_sdSupport(response.feature.sdSupport);
self.feature_swallowOkAfterResend(response.feature.swallowOkAfterResend);
self.feature_repetierTargetTemp(response.feature.repetierTargetTemp);
self.serial_port(response.serial.port);
self.serial_baudrate(response.serial.baudrate);
@ -207,7 +269,8 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
"waitForStart": self.feature_waitForStart(),
"alwaysSendChecksum": self.feature_alwaysSendChecksum(),
"sdSupport": self.feature_sdSupport(),
"swallowOkAfterResend": self.feature_swallowOkAfterResend()
"swallowOkAfterResend": self.feature_swallowOkAfterResend(),
"repetierTargetTemp": self.feature_repetierTargetTemp()
},
"serial": {
"port": self.serial_port(),

View file

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

View file

@ -21,6 +21,9 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
self.filters = self.settings.terminalFilters;
self.filterRegex = undefined;
self.cmdHistory = [];
self.cmdHistoryIdx = -1;
self.activeFilters = ko.observableArray([]);
self.activeFilters.subscribe(function(e) {
self.updateFilterRegex();
@ -30,25 +33,25 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
self.fromCurrentData = function(data) {
self._processStateData(data.state);
self._processCurrentLogData(data.logs);
}
};
self.fromHistoryData = function(data) {
self._processStateData(data.state);
self._processHistoryLogData(data.logHistory);
}
};
self._processCurrentLogData = function(data) {
if (!self.log)
self.log = []
self.log = self.log.concat(data)
self.log = self.log.slice(-300)
self.log = [];
self.log = self.log.concat(data);
self.log = self.log.slice(-300);
self.updateOutput();
}
};
self._processHistoryLogData = function(data) {
self.log = data;
self.updateOutput();
}
};
self._processStateData = function(data) {
self.isErrorOrClosed(data.flags.closedOrError);
@ -58,7 +61,7 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
self.isError(data.flags.error);
self.isReady(data.flags.ready);
self.isLoading(data.flags.loading);
}
};
self.updateFilterRegex = function() {
var filterRegexStr = self.activeFilters().join("|").trim();
@ -68,7 +71,7 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
self.filterRegex = new RegExp(filterRegexStr);
}
console.log("Terminal filter regex: " + filterRegexStr);
}
};
self.updateOutput = function() {
if (!self.log)
@ -86,10 +89,13 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
if (self.autoscrollEnabled()) {
container.scrollTop(container[0].scrollHeight - container.height())
}
}
};
self.sendCommand = function() {
var command = self.command();
if (!command) {
return;
}
var re = /^([gmt][0-9]+)(\s.*)?/;
var commandMatch = command.match(re);
@ -105,14 +111,45 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
contentType: "application/json; charset=UTF-8",
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.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) {
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_TIMELAPSEFILESPERPAGE = 10;
var CONFIG_LOGFILESPERPAGE = 10;
var CONFIG_USERSPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
@ -273,7 +274,7 @@
<!-- ko foreach: tools -->
<tr data-bind="template: { name: 'temprow-template' }"></tr>
<!-- /ko -->
<tr data-bind="template: { name: 'temprow-template', data: bedTemp }"></tr>
<tr data-bind="template: { name: 'temprow-template', data: bedTemp }, visible: hasBed"></tr>
</table>
<script type="text/html" id="temprow-template">
@ -516,7 +517,7 @@
</div>
<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>
</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/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/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/helpers.js') }}"></script>

View file

@ -20,6 +20,7 @@
<li class="nav-header">OctoPrint</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_logs" data-toggle="tab">Logs</a></li>
</ul>
<div class="tab-content span8">
@ -83,61 +84,83 @@
<div class="tab-pane" id="settings_printerParameters">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-movementSpeedX">Movement Speed X Axis</label>
<div class="controls">
<label class="control-label">Axis</label>
<div class="controls form-inline">
<label>X:</label>
<div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedX" id="settings-movementSpeedX">
<span class="add-on">mm/min</span>
</div>
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertX" id="settings-printerInvertX"> Invert control
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-movementSpeedY">Movement Speed Y Axis</label>
<div class="controls">
<div class="controls form-inline">
<label>Y:</label>
<div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedY" id="settings-movementSpeedY">
<span class="add-on">mm/min</span>
</div>
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertY" id="settings-printerInvertY"> Invert control
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-movementSpeedZ">Movement Speed Z Axis</label>
<div class="controls">
<div class="controls form-inline">
<label>Z:</label>
<div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedZ" id="settings-movementSpeedZ">
<span class="add-on">mm/min</span>
</div>
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertZ" id="settings-printerInvertZ"> Invert control
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-movementSpeedE">Movement Speed Extruder</label>
<div class="controls">
<div class="controls form-inline">
<label>E:</label>
<div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedE" id="settings-movementSpeedE">
<span class="add-on">mm/min</span>
</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">
<label class="control-label" for="settings-numExtruders">Number of Extruders</label>
<div class="controls">
<input type="number" class="input-mini text-right" min="1" max="5" data-bind="value: printer_numExtruders" id="settings-numExtruders">
</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>
</div>
<div class="tab-pane" id="settings_webcam">
@ -224,6 +247,13 @@
</label>
</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="controls">
<label class="checkbox">
@ -535,6 +565,54 @@
</div>
{% 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>

View file

@ -191,6 +191,24 @@ def silentRemove(file):
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):
if not "application/json" in request.headers["Content-Type"]:
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.filemanager.destinations import FileDestinations
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
try:
@ -136,10 +136,8 @@ class MachineCom(object):
self._baudrateDetectList = baudrateList()
self._baudrateDetectRetry = 0
self._temp = {}
self._targetTemp = {}
self._tempOffset = {}
self._bedTemp = 0
self._bedTargetTemp = 0
self._bedTemp = None
self._bedTempOffset = 0
self._commandQueue = queue.Queue()
self._currentZ = None
@ -147,18 +145,42 @@ class MachineCom(object):
self._heatupWaitTimeLost = 0.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:
# - 1: whole tool designator incl. optional toolNumber ("T", "Tn", "B")
# - 2: toolNumber, if given ("", "n", "")
# - 3: actual temperature
# - 4: whole target substring, if given (e.g. " / 22.0")
# - 5: target temperature
self._tempRegex = re.compile("(B|T(\d*)):\s*([-+]?\d*\.?\d*)(\s*\/?\s*([-+]?\d*\.?\d*))?")
self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"])
self._currentLine = 1
self._resendDelta = None
self._lastLines = deque([], 50)
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._regex_repetierTempBed = re.compile("TargetBed:(%s)" % positiveFloatPattern)
# multithreading locks
self._sendNextLock = threading.Lock()
@ -169,14 +191,6 @@ class MachineCom(object):
self.thread.daemon = True
self.thread.start()
# SD status data
self._sdAvailable = False
self._sdFileList = False
self._sdFiles = []
# print job
self._currentFile = None
def __del__(self):
self.close()
@ -341,6 +355,7 @@ class MachineCom(object):
if self._currentFile is not None:
payload = {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation()
}
eventManager().fire(Events.PRINT_FAILED, payload)
@ -367,16 +382,17 @@ class MachineCom(object):
if self._currentFile is None:
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:
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 wasPaused:
self.sendCommand("M26 S0")
@ -438,6 +454,7 @@ class MachineCom(object):
eventManager().fire(Events.PRINT_CANCELLED, {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation()
})
@ -454,6 +471,7 @@ class MachineCom(object):
eventManager().fire(Events.PRINT_RESUMED, {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation()
})
elif pause and self.isPrinting():
@ -463,6 +481,7 @@ class MachineCom(object):
eventManager().fire(Events.PRINT_PAUSED, {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation()
})
@ -521,7 +540,7 @@ class MachineCom(object):
def _parseTemperatures(self, line):
result = {}
maxToolNum = 0
for match in re.finditer(self._tempRegex, line):
for match in re.finditer(self._regex_temp, line):
tool = match.group(1)
toolNumber = int(match.group(2)) if match.group(2) and len(match.group(2)) > 0 else None
if toolNumber > maxToolNum:
@ -558,12 +577,24 @@ class MachineCom(object):
continue
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
if "B" in parsedTemps.keys():
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):
feedbackControls = settings().getFeedbackControls()
@ -588,6 +619,8 @@ class MachineCom(object):
startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"])
heatingUp = False
swallowOk = False
supportRepetierTargetTemp = settings().getBoolean(["feature", "repetierTargetTemp"])
while True:
try:
line = self._readline()
@ -600,7 +633,11 @@ class MachineCom(object):
##~~ 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 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
##~~ Temperature processing
@ -615,6 +652,33 @@ class MachineCom(object):
t = time.time()
self._heatupWaitTimeLost = t - self._heatupWaitStartTime
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
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)
elif 'SD printing byte' in line:
# 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._callback.mcProgress()
elif 'File opened' in line:
# 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)))
elif 'File selected' in line:
# 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:
# anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s"
self._printSection = "CUSTOM"
self._changeState(self.STATE_PRINTING)
line = "ok"
elif 'Done printing file' in line:
@ -665,8 +728,9 @@ class MachineCom(object):
self._changeState(self.STATE_OPERATIONAL)
eventManager().fire(Events.PRINT_DONE, {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation(),
"time": time.time() - self._currentFile.getStartTime()
"time": self.getPrintTime()
})
elif 'Done saving file' in line:
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"
# 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.
if re.match('Error:[0-9]\n', line):
if self._regex_minMaxError.match(line):
line = line.rstrip() + self._readline()
#Skip the communication errors, as those get corrected.
if 'checksum mismatch' in line \
@ -914,7 +978,7 @@ class MachineCom(object):
if ret == '':
#self._log("Recv: TIMEOUT")
return ''
self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
self._log("Recv: %s" % sanitizeAscii(ret))
return ret
def _sendNext(self):
@ -928,7 +992,7 @@ class MachineCom(object):
payload = {
"local": self._currentFile.getLocalFilename(),
"remote": self._currentFile.getRemoteFilename(),
"time": time.time() - self._currentFile.getStartTime()
"time": self.getPrintTime()
}
self._currentFile = None
@ -939,8 +1003,9 @@ class MachineCom(object):
else:
payload = {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation(),
"time": time.time() - self._currentFile.getStartTime()
"time": self.getPrintTime()
}
self._callback.mcPrintjobDone()
self._changeState(self.STATE_OPERATIONAL)
@ -993,7 +1058,7 @@ class MachineCom(object):
return
if not self.isStreaming():
gcode = re.search("^\s*([GM]\d+|T)", cmd)
gcode = self._regex_command.search(cmd)
if gcode:
gcode = gcode.group(1)
@ -1042,20 +1107,22 @@ class MachineCom(object):
self.close(True)
def _gcode_T(self, cmd):
toolMatch = re.search('T([0-9]+)', cmd)
toolMatch = self._regex_paramTInt.search(cmd)
if toolMatch:
self._currentExtruder = int(toolMatch.group(1))
return cmd
def _gcode_G0(self, cmd):
if 'Z' in cmd:
try:
z = float(re.search('Z([0-9\.]*)', cmd).group(1))
if self._currentZ != z:
self._currentZ = z
self._callback.mcZChange(z)
except ValueError:
pass
match = self._regex_paramZFloat.search(cmd)
if match:
try:
z = float(match.group(1))
if self._currentZ != z:
self._currentZ = z
self._callback.mcZChange(z)
except ValueError:
pass
return cmd
_gcode_G1 = _gcode_G0
@ -1066,22 +1133,32 @@ class MachineCom(object):
def _gcode_M104(self, cmd):
toolNum = self._currentExtruder
toolMatch = re.search('T([0-9]+)', cmd)
toolMatch = self._regex_paramTInt.search(cmd)
if toolMatch:
toolNum = int(toolMatch.group(1))
match = re.search('S([0-9]+)', cmd)
match = self._regex_paramSInt.search(cmd)
if match:
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:
pass
return cmd
def _gcode_M140(self, cmd):
match = re.search('S([0-9]+)', cmd)
match = self._regex_paramSInt.search(cmd)
if match:
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:
pass
return cmd
@ -1096,7 +1173,7 @@ class MachineCom(object):
def _gcode_M110(self, cmd):
newLineNumber = None
match = re.search("N([0-9]+)", cmd)
match = self._regex_paramNInt.search(cmd)
if match:
try:
newLineNumber = int(match.group(1))
@ -1232,16 +1309,19 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
def __init__(self, filename, offsetCallback):
PrintingFileInformation.__init__(self, filename)
self._filehandle = None
self._filesetMenuModehandle = None
self._lineCount = None
self._firstLine = None
self._currentTool = 0
self._offsetCallback = offsetCallback
self._tempCommandPattern = re.compile("M(104|109|140|190)")
self._tempCommandTemperaturePattern = re.compile("S([-+]?\d*\.?\d*)")
self._tempCommandToolPattern = re.compile("T(\d+)")
self._toolCommandPattern = re.compile("^T(\d+)")
self._regex_tempCommand = re.compile("M(104|109|140|190)")
self._regex_tempCommandTemperature = re.compile("S([-+]?\d*\.?\d*)")
self._regex_tempCommandTool = 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):
raise IOError("File %s does not exist" % self._filename)
@ -1295,14 +1375,14 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
line = line[0:line.find(";")]
line = line.strip()
if len(line) > 0:
toolMatch = self._toolCommandPattern.match(line)
toolMatch = self._regex_toolCommand.match(line)
if toolMatch is not None:
# track tool changes
self._currentTool = int(toolMatch.group(1))
else:
## apply offsets
if self._offsetCallback is not None:
tempMatch = self._tempCommandPattern.match(line)
tempMatch = self._regex_tempCommand.match(line)
if tempMatch is not None:
# if we have a temperature command, retrieve current offsets
tempOffset, bedTempOffset = self._offsetCallback()
@ -1310,7 +1390,7 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
# extruder temperature, determine which one and retrieve corresponding offset
toolNum = self._currentTool
toolNumMatch = self._tempCommandToolPattern.search(line)
toolNumMatch = self._regex_tempCommandTool.search(line)
if toolNumMatch is not None:
try:
toolNum = int(toolNumMatch.group(1))
@ -1327,7 +1407,7 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
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
tempValueMatch = self._tempCommandTemperaturePattern.search(line)
tempValueMatch = self._regex_tempCommandTemperature.search(line)
if tempValueMatch is not None:
try:
temp = float(tempValueMatch.group(1))

View file

@ -104,10 +104,14 @@ class VirtualPrinter():
for i in range(len(self.temp)):
allTemps.append((i, self.temp[i], self.targetTemp[i]))
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:
self.readList.append("ok %s B:%.2f /%.2f @:64\n" % (allTempsString, self.bedTemp, self.bedTargetTemp))
self.readList.append("ok %s @:64\n" % allTempsString)
else:
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: