commit
d5e2bf546e
25 changed files with 870 additions and 158 deletions
|
|
@ -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.
|
||||
|
|
@ -12,4 +12,4 @@ API Documentation
|
|||
connection.rst
|
||||
printer.rst
|
||||
job.rst
|
||||
|
||||
logs.rst
|
||||
|
|
|
|||
177
docs/api/logs.rst
Normal file
177
docs/api/logs.rst
Normal 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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
62
src/octoprint/server/api/log.py
Normal file
62
src/octoprint/server/api/log.py
Normal 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
|
||||
|
||||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
62
src/octoprint/static/js/app/viewmodels/log.js
Normal file
62
src/octoprint/static/js/app/viewmodels/log.js
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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> | <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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue