Parse slicing progress from Cura and provide it on sock.js socket

UI for now displays it in the "Slicing" progress bar text.
This commit is contained in:
Gina Häußge 2014-10-20 18:58:04 +02:00
parent 084ca956fb
commit 90618723d4
7 changed files with 207 additions and 15 deletions

View file

@ -35,6 +35,9 @@ following message types are currently available for usage by 3rd party clients:
payload data model as ``current``, see :ref:`below <sec-api-push-datamodel-currentandhistory>`.
* ``event``: Events triggered within OctoPrint, such as e.g. ``PrintFailed`` or ``MovieRenderDone``. Payload is the event
type and payload, see :ref:`below <sec-api-push-datamodel-event>`. Sent when an event is triggered internally.
* ``slicingProgress``: Progress updates from an active slicing background job, payload contains information about the
model being sliced, the target file, the slicer being used and the progress as a percentage.
See :ref:`the payload data model <sec-api-push-datamodel-slicingprogress>`.
Clients must ignore any unknown messages.
@ -112,3 +115,41 @@ Datamodel
- 1
- Object
- Payload associated with the event
.. _sec-api-push-datamodel-slicingprogress:
``slicingProgress`` payload
---------------------------
.. list-table::
:widths: 15 5 10 30
:header-rows: 1
* - Name
- Multiplicity
- Type
- Description
* - ``slicer``
- 1
- String
- Name of the slicer used
* - ``source_location``
- 1
- String
- Location of the source file being sliced, at the moment either ``local`` or ``sdcard``
* - ``source_path``
- 1
- String
- Path of the source file being sliced (e.g. an STL file)
* - ``dest_location``
- 1
- String
- Location of the destination file being created, at the moment either ``local`` or ``sdcard``
* - ``dest_path``
- 1
- String
- Path of the destination file being sliced (e.g. a GCODE file)
* - ``progress``
- 1
- Number (Float)
- Percentage of slicing job already completed

View file

@ -105,9 +105,18 @@ class FileManager(object):
self._slicing_jobs = dict()
self._slicing_jobs_mutex = threading.Lock()
self._slicing_progress_callbacks = []
self._last_slicing_progress = None
for storage_type, storage_manager in self._storage_managers.items():
self._determine_analysis_backlog(storage_type, storage_manager)
def register_slicingprogress_callback(self, callback):
self._slicing_progress_callbacks.append(callback)
def unregister_slicingprogress_callback(self, callback):
self._slicing_progress_callbacks.remove(callback)
def _determine_analysis_backlog(self, storage_type, storage_manager):
self._logger.info("Adding backlog items from {storage_type} to analysis queue".format(**locals()))
for entry, path in storage_manager.analysis_backlog:
@ -197,7 +206,30 @@ class FileManager(object):
self._slicing_jobs[dest_location] = (slicer_name, absolute_source_path, temp_path)
args = (source_location, source_path, temp_path, dest_location, dest_path, start_time, callback, callback_args)
return self._slicing_manager.slice(slicer_name, absolute_source_path, temp_path, profile, stlProcessed, callback_args=args, overrides=overrides)
return self._slicing_manager.slice(
slicer_name,
absolute_source_path,
temp_path,
profile,
stlProcessed,
callback_args=args,
overrides=overrides,
on_progress=self.on_slicing_progress,
on_progress_args=(slicer_name, source_location, source_path, dest_location, dest_path))
def on_slicing_progress(self, slicer, source_location, source_path, dest_location, dest_path, _progress=None):
if not _progress:
return
progress_int = int(_progress * 100)
if self._last_slicing_progress == progress_int:
return
else:
self._last_slicing_progress = progress_int
for callback in self._slicing_progress_callbacks:
try: callback.sendSlicingProgress(slicer, source_location, source_path, dest_location, dest_path, progress_int)
except: pass
def file_exists(self, destination, path):
return self._storage(destination).file_exists(path)

View file

@ -212,7 +212,7 @@ class SlicerPlugin(Plugin):
def save_slicer_profile(self, path, profile, allow_overwrite=True, overrides=None):
pass
def do_slice(self, model_path, machinecode_path=None, profile_path=None):
def do_slice(self, model_path, machinecode_path=None, profile_path=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
pass
def cancel_slicing(self, machinecode_path):

View file

@ -198,13 +198,19 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
self._save_profile(path, new_profile, allow_overwrite=allow_overwrite)
def do_slice(self, model_path, machinecode_path=None, profile_path=None):
def do_slice(self, model_path, machinecode_path=None, profile_path=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
if not profile_path:
profile_path = s.get(["default_profile"])
if not machinecode_path:
path, _ = os.path.splitext(model_path)
machinecode_path = path + ".gco"
if on_progress:
if not on_progress_args:
on_progress_args = ()
if not on_progress_kwargs:
on_progress_kwargs = dict()
engine_settings = self._convert_to_engine(profile_path)
executable = s.get(["cura_engine"])
@ -221,10 +227,75 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
command = " ".join(args)
self._logger.info("Running %r in %s" % (command, working_dir))
try:
p = sarge.run(command, cwd=working_dir, async=True)
with self._slicing_commands_mutex:
self._slicing_commands[machinecode_path] = p.commands[0]
p.wait()
p = sarge.run(command, cwd=working_dir, async=True, stdout=sarge.Capture(), stderr=sarge.Capture())
try:
with self._slicing_commands_mutex:
self._slicing_commands[machinecode_path] = p.commands[0]
if on_progress is not None:
# The Cura slicing process has three individual steps, each consisting of <layer_count> substeps:
#
# - inset
# - skin
# - export
#
# So each layer will be processed three times, once for each step, resulting in a total amount of
# substeps of 3 * <layer_count>.
#
# The CuraEngine reports the calculated layer count and the continuous progress on stderr.
# The layer count gets reported right at the beginning in a line of the format:
#
# Layer count: <layer_count>
#
# The individual progress per each of the three steps gets reported on stderr in a line of
# the format:
#
# Progress:<step>:<current_layer>:<layer_count>
#
# Thus, for determining the overall progress the following formula applies:
#
# progress = <step_factor> * <layer_count> + <current_layer> / <layer_count> * 3
#
# with <step_factor> being 0 for "inset", 1 for "skin" and 2 for "export".
import time
layer_count = None
line_seen = False
step_factor = dict(
inset=0,
skin=1,
export=2
)
while p.returncode is None:
line = p.stderr.readline(timeout=0.5)
if not line:
if line_seen:
break
else:
continue
line_seen = True
if line.startswith("Layer count:") and layer_count is None:
try:
layer_count = float(line[len("Layer count:"):].strip())
except:
pass
elif line.startswith("Progress:"):
split_line = line[len("Progress:"):].strip().split(":")
if len(split_line) == 3:
step, current_layer, _ = split_line
try:
current_layer = float(current_layer)
except:
pass
else:
if not step in step_factor:
continue
on_progress_kwargs["_progress"] = (step_factor[step] * layer_count + current_layer) / (layer_count * 3)
on_progress(*on_progress_args, **on_progress_kwargs)
finally:
p.close()
with self._cancelled_jobs_mutex:
if machinecode_path in self._cancelled_jobs:

View file

@ -47,6 +47,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection):
self._emit("connected", {"apikey": octoprint.server.UI_API_KEY, "version": octoprint.server.VERSION, "display_version": octoprint.server.DISPLAY_VERSION})
self._printer.registerCallback(self)
self._fileManager.register_slicingprogress_callback(self)
octoprint.timelapse.registerCallback(self)
self._eventManager.fire(Events.CLIENT_OPENED, {"remoteAddress": remoteAddress})
@ -58,6 +59,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection):
def on_close(self):
self._logger.info("Client connection closed")
self._printer.unregisterCallback(self)
self._fileManager.unregister_slicingprogress_callback(self)
octoprint.timelapse.unregisterCallback(self)
self._eventManager.fire(Events.CLIENT_CLOSED)
@ -100,6 +102,11 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection):
def sendTimelapseConfig(self, timelapseConfig):
self._emit("timelapse", timelapseConfig)
def sendSlicingProgress(self, slicer, source_location, source_path, dest_location, dest_path, progress):
self._emit("slicingProgress",
dict(slicer=slicer, source_location=source_location, source_path=source_path, dest_location=dest_location, dest_path=dest_path, progress=progress)
)
def addLog(self, data):
with self._logBacklogMutex:
self._logBacklog.append(data)

View file

@ -56,6 +56,15 @@ class SlicingManager(object):
self._slicer_names = dict()
self._load_slicers()
self._progress_callbacks = []
self._last_progress_report = None
def register_progress_callback(self, callback):
self._progress_callbacks.append(callback)
def unregister_progress_callback(self, callback):
self._progress_callbacks.remove(callback)
def _load_slicers(self):
plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SlicerPlugin)
for name, plugin in plugins.items():
@ -81,7 +90,7 @@ class SlicingManager(object):
def get_slicer(self, slicer):
return self._slicers[slicer] if slicer in self._slicers else None
def slice(self, slicer_name, source_path, dest_path, profile_name, callback, callback_args=None, callback_kwargs=None, overrides=None):
def slice(self, slicer_name, source_path, dest_path, profile_name, callback, callback_args=None, callback_kwargs=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
if callback_args is None:
callback_args = ()
if callback_kwargs is None:
@ -97,8 +106,16 @@ class SlicingManager(object):
def slicer_worker(slicer, model_path, machinecode_path, profile_name, overrides, callback, callback_args, callback_kwargs):
try:
with self.temporary_profile(slicer.get_slicer_type(), name=profile_name, overrides=overrides) as profile_path:
ok, result = slicer.do_slice(model_path, machinecode_path=machinecode_path, profile_path=profile_path)
slicer_name = slicer.get_slicer_type()
with self.temporary_profile(slicer_name, name=profile_name, overrides=overrides) as profile_path:
ok, result = slicer.do_slice(
model_path,
machinecode_path=machinecode_path,
profile_path=profile_path,
on_progress=on_progress,
on_progress_args=on_progress_args,
on_progress_kwargs=on_progress_kwargs
)
if not ok:
callback_kwargs.update(dict(_error=result))

View file

@ -94,6 +94,9 @@ function DataUpdater(allViewModels) {
var data = e.data[prop];
var gcodeUploadProgress = $("#gcode_upload_progress");
var gcodeUploadProgressBar = $(".bar", gcodeUploadProgress);
switch (prop) {
case "connected": {
// update the current UI API key and send it with any request
@ -137,14 +140,21 @@ function DataUpdater(allViewModels) {
});
break;
}
case "slicingProgress": {
gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: Math.round(data["progress"])}));
_.each(self.allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onSlicingProgress")) {
viewModel.onSlicingProgress(data["slicer"], data["model_path"], data["machinecode_path"], data["progress"]);
}
});
break;
}
case "event": {
var type = data["type"];
var payload = data["payload"];
var html = "";
var gcodeUploadProgress = $("#gcode_upload_progress");
var gcodeUploadProgressBar = $(".bar", gcodeUploadProgress);
console.log("Got event " + type + " with payload: " + JSON.stringify(payload))
if (type == "UpdatedFiles") {
@ -171,18 +181,27 @@ function DataUpdater(allViewModels) {
} else if (type == "SlicingStarted") {
gcodeUploadProgress.addClass("progress-striped").addClass("active");
gcodeUploadProgressBar.css("width", "100%");
gcodeUploadProgressBar.text(gettext("Slicing ..."));
gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: 0}));
} else if (type == "SlicingDone") {
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
gcodeUploadProgressBar.css("width", "0%");
gcodeUploadProgressBar.text("");
new PNotify({title: gettext("Slicing done"), text: _.sprintf(gettext("Sliced %(stl)s to %(gcode)s, took %(time).2f seconds"), payload)});
_.each(self.allViewModels, function(viewModel) {
_.each(self.allViewModels, function (viewModel) {
if (viewModel.hasOwnProperty("onSlicingDone")) {
viewModel.onSlicingDone(payload);
}
});
} else if (type == "SlicingCancelled") {
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
gcodeUploadProgressBar.css("width", "0%");
gcodeUploadProgressBar.text("");
_.each(self.allViewModels, function (viewModel) {
if (viewModel.hasOwnProperty("onSlicingCancelled")) {
viewModel.onSlicingCancelled(payload);
}
});
} else if (type == "SlicingFailed") {
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
gcodeUploadProgressBar.css("width", "0%");
@ -190,6 +209,11 @@ function DataUpdater(allViewModels) {
html = _.sprintf(gettext("Could not slice %(stl)s to %(gcode)s: %(reason)s"), payload);
new PNotify({title: gettext("Slicing failed"), text: html, type: "error", hide: false});
_.each(self.allViewModels, function (viewModel) {
if (viewModel.hasOwnProperty("onSlicingFailed")) {
viewModel.onSlicingFailed(payload);
}
});
} else if (type == "TransferStarted") {
gcodeUploadProgress.addClass("progress-striped").addClass("active");
gcodeUploadProgressBar.css("width", "100%");