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:
parent
084ca956fb
commit
90618723d4
7 changed files with 207 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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%");
|
||||
|
|
|
|||
Loading…
Reference in a new issue