Allow adding slicing profiles for unconfigured slicers and properly reload slicing view model upon configuration

This should fix the problem where on a fresh setup it was impossible to upload slicing profiles for Cura before
the path to the binary was configured.

Also made the slicing dialog auto update available slicers when the settings are updated. The slicing button
in the file list is now only active if a slicer is available. The slicing dialog will only show upon upload of
an STL file if a slicer is available.

Closes #795
This commit is contained in:
Gina Häußge 2015-03-02 15:34:29 +01:00
parent 36a837cd77
commit befe2e7bee
7 changed files with 175 additions and 114 deletions

View file

@ -12,19 +12,28 @@ from octoprint.server import slicingManager
from octoprint.server.util.flask import restricted_access
from octoprint.server.api import api, NO_CONTENT
from octoprint.settings import settings as s
from octoprint.settings import settings as s, valid_boolean_trues
from octoprint.slicing import SlicerNotConfigured
@api.route("/slicing", methods=["GET"])
def slicingListAll():
default_slicer = s().get(["slicing", "defaultSlicer"])
if "configured" in request.values and request.values["configured"] in valid_boolean_trues:
slicers = slicingManager.configured_slicers
else:
slicers = slicingManager.registered_slicers
result = dict()
for slicer in slicingManager.registered_slicers:
for slicer in slicers:
slicer_impl = slicingManager.get_slicer(slicer, require_configured=False)
result[slicer] = dict(
key=slicer,
displayName=slicingManager.get_slicer(slicer).get_slicer_properties()["name"],
displayName=slicer_impl.get_slicer_properties()["name"],
default=default_slicer == slicer,
configured = slicer_impl.is_slicer_configured(),
profiles=_getSlicingProfilesData(slicer)
)
@ -35,7 +44,13 @@ def slicingListSlicerProfiles(slicer):
if not slicer in slicingManager.registered_slicers:
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
return jsonify(_getSlicingProfilesData(slicer))
configured = False
if "configured" in request.values and request.values["configured"] in valid_boolean_trues:
if not slicer in slicingManager.configured_slicers:
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
configured = True
return jsonify(_getSlicingProfilesData(slicer, require_configured=configured))
@api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["GET"])
def slicingGetSlicerProfile(slicer, name):
@ -57,7 +72,7 @@ def slicingAddSlicerProfile(slicer, name):
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
if not "application/json" in request.headers["Content-Type"]:
return None, None, make_response("Expected content-type JSON", 400)
return make_response("Expected content-type JSON", 400)
try:
json_data = request.json
@ -88,7 +103,7 @@ def slicingPatchSlicerProfile(slicer, name):
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
if not "application/json" in request.headers["Content-Type"]:
return None, None, make_response("Expected content-type JSON", 400)
return make_response("Expected content-type JSON", 400)
profile = slicingManager.load_profile(slicer, name)
if not profile:
@ -130,9 +145,10 @@ def slicingDelSlicerProfile(slicer, name):
slicingManager.delete_profile(slicer, name)
return NO_CONTENT
def _getSlicingProfilesData(slicer):
profiles = slicingManager.all_profiles(slicer)
if not profiles:
def _getSlicingProfilesData(slicer, require_configured=False):
try:
profiles = slicingManager.all_profiles(slicer, require_configured=require_configured)
except SlicerNotConfigured:
return dict()
result = dict()

View file

@ -11,6 +11,8 @@ import octoprint.plugin
import octoprint.events
from octoprint.settings import settings
from .exceptions import *
class SlicingProfile(object):
def __init__(self, slicer, name, data, display_name=None, description=None):
@ -44,10 +46,6 @@ class TemporaryProfile(object):
pass
class SlicingCancelled(BaseException):
pass
class SlicingManager(object):
def __init__(self, profile_path, printer_profile_manager):
self._profile_path = profile_path
@ -71,8 +69,7 @@ class SlicingManager(object):
def _load_slicers(self):
plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SlicerPlugin)
for name, plugin in plugins.items():
if plugin.is_slicer_configured():
self._slicers[plugin.get_slicer_properties()["type"]] = plugin
self._slicers[plugin.get_slicer_properties()["type"]] = plugin
@property
def slicing_enabled(self):
@ -89,13 +86,15 @@ class SlicingManager(object):
@property
def default_slicer(self):
slicer_name = settings().get(["slicing", "defaultSlicer"])
if slicer_name in self.configured_slicers:
if slicer_name in self.registered_slicers:
return slicer_name
else:
return None
def get_slicer(self, slicer, require_configured=True):
return self._slicers[slicer] if slicer in self._slicers and (not require_configured or self._slicers[slicer].is_slicer_configured()) else None
if slicer in self._slicers and (not require_configured or self._slicers[slicer].is_slicer_configured()):
return self._slicers[slicer]
raise SlicerNotConfigured(slicer)
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, printer_profile_id=None, position=None):
if callback_args is None:
@ -106,11 +105,13 @@ class SlicingManager(object):
if not slicer_name in self.configured_slicers:
if not slicer_name in self.registered_slicers:
error = "No such slicer: {slicer_name}".format(**locals())
exc = UnknownSlicer(slicer_name)
else:
error = "Slicer not configured: {slicer_name}".format(**locals())
callback_kwargs.update(dict(_error=error))
exc = SlicerNotConfigured(slicer_name)
callback_kwargs.update(dict(_error=error, _exc=exc))
callback(*callback_args, **callback_kwargs)
return False, error
raise exc
slicer = self.get_slicer(slicer_name)
@ -154,23 +155,24 @@ class SlicingManager(object):
def cancel_slicing(self, slicer_name, source_path, dest_path):
if not slicer_name in self.registered_slicers:
return
raise UnknownSlicer(slicer_name)
slicer = self.get_slicer(slicer_name)
slicer.cancel_slicing(dest_path)
def load_profile(self, slicer, name):
def load_profile(self, slicer, name, require_configured=True):
if not slicer in self.registered_slicers:
return None
raise UnknownSlicer(slicer)
try:
path = self.get_profile_path(slicer, name, must_exist=True)
except IOError:
return None
return self._load_profile_from_path(slicer, path)
return self._load_profile_from_path(slicer, path, require_configured=require_configured)
def save_profile(self, slicer, name, profile, overrides=None, allow_overwrite=True, display_name=None, description=None):
if not slicer in self.registered_slicers:
return
raise UnknownSlicer(slicer)
if not isinstance(profile, SlicingProfile):
if isinstance(profile, dict):
@ -191,7 +193,7 @@ class SlicingManager(object):
def temporary_profile(self, slicer, name=None, overrides=None):
if not slicer in self.registered_slicers:
return None
raise UnknownSlicer(slicer)
profile = self._get_default_profile(slicer)
if name:
@ -207,16 +209,21 @@ class SlicingManager(object):
def delete_profile(self, slicer, name):
if not slicer in self.registered_slicers:
return None
raise UnknownSlicer(slicer)
if not name:
raise ValueError("name must be set")
path = self.get_profile_path(slicer, name)
if not os.path.exists(path) or not os.path.isfile(path):
return
os.remove(path)
def all_profiles(self, slicer):
def all_profiles(self, slicer, require_configured=False):
if not slicer in self.registered_slicers:
return None
raise UnknownSlicer(slicer)
if require_configured and not slicer in self.configured_slicers:
raise SlicerNotConfigured(slicer)
profiles = dict()
slicer_profile_path = self.get_slicer_profile_path(slicer)
@ -228,12 +235,12 @@ class SlicingManager(object):
path = os.path.join(slicer_profile_path, entry)
profile_name = entry[:-len(".profile")]
profiles[profile_name] = self._load_profile_from_path(slicer, path)
profiles[profile_name] = self._load_profile_from_path(slicer, path, require_configured=require_configured)
return profiles
def get_slicer_profile_path(self, slicer):
if not slicer in self.registered_slicers:
return None
raise UnknownSlicer(slicer)
path = os.path.join(self._profile_path, slicer)
if not os.path.exists(path):
@ -242,10 +249,10 @@ class SlicingManager(object):
def get_profile_path(self, slicer, name, must_exist=False):
if not slicer in self.registered_slicers:
return None
raise UnknownSlicer(slicer)
if not name:
return None
raise ValueError("name must be set")
name = self._sanitize(name)
@ -269,11 +276,11 @@ class SlicingManager(object):
sanitized_name = sanitized_name.replace(" ", "_")
return sanitized_name
def _load_profile_from_path(self, slicer, path):
return self.get_slicer(slicer).get_slicer_profile(path)
def _load_profile_from_path(self, slicer, path, require_configured=False):
return self.get_slicer(slicer, require_configured=require_configured).get_slicer_profile(path)
def _save_profile_to_path(self, slicer, path, profile, allow_overwrite=True, overrides=None):
self.get_slicer(slicer).save_slicer_profile(path, profile, allow_overwrite=allow_overwrite, overrides=overrides)
def _save_profile_to_path(self, slicer, path, profile, allow_overwrite=True, overrides=None, require_configured=False):
self.get_slicer(slicer, require_configured=require_configured).save_slicer_profile(path, profile, allow_overwrite=allow_overwrite, overrides=overrides)
def _get_default_profile(self, slicer):
default_profiles = settings().get(["slicing", "defaultProfiles"])

View file

@ -0,0 +1,28 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
class SlicingException(BaseException):
pass
class SlicingCancelled(SlicingException):
pass
class SlicerException(SlicingException):
def __init__(self, slicer, *args, **kwargs):
super(SlicingException, self).__init__(*args, **kwargs)
self.slicer = slicer
class SlicerNotConfigured(SlicerException):
def __init__(self, slicer, *args, **kwargs):
super(SlicerException, self).__init__(slicer, *args, **kwargs)
self.message = "Slicer not configured: {slicer}".format(slicer=slicer)
class UnknownSlicer(SlicerException):
def __init__(self, slicer, *args, **kwargs):
super(SlicerException, self).__init__(slicer, *args, **kwargs)
self.message = "No such slicer: {slicer}".format(slicer=slicer)

View file

@ -167,25 +167,7 @@ function DataUpdater(allViewModels) {
log.debug("Got event " + type + " with payload: " + JSON.stringify(payload));
if (type == "UpdatedFiles") {
_.each(self.allViewModels, function (viewModel) {
if (viewModel.hasOwnProperty("onUpdatedFiles")) {
viewModel.onUpdatedFiles(payload);
}
});
} else if (type == "MetadataStatisticsUpdated") {
_.each(self.allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onMetadataStatisticsUpdated")) {
viewModel.onMetadataStatisticsUpdated(payload);
}
})
} else if (type == "MetadataAnalysisFinished") {
_.each(self.allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onMetadataAnalysisFinished")) {
viewModel.onMetadataAnalysisFinished(payload);
}
});
} else if (type == "MovieRendering") {
if (type == "MovieRendering") {
new PNotify({title: gettext("Rendering timelapse"), text: _.sprintf(gettext("Now rendering timelapse %(movie_basename)s"), payload)});
} else if (type == "MovieDone") {
new PNotify({title: gettext("Timelapse ready"), text: _.sprintf(gettext("New timelapse %(movie_basename)s is done rendering."), payload)});
@ -207,21 +189,10 @@ function DataUpdater(allViewModels) {
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), type: "success"});
_.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%");
@ -229,11 +200,6 @@ 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%");
@ -249,6 +215,26 @@ function DataUpdater(allViewModels) {
});
gcodeFilesViewModel.requestData(payload.remote, "sdcard");
}
var legacyEventHandlers = {
"UpdatedFiles": "onUpdatedFiles",
"MetadataStatisticsUpdated": "onMetadataStatisticsUpdated",
"MetadataAnalysisFinished": "onMetadataAnalysisFinished",
"SlicingDone": "onSlicingDone",
"SlicingCancelled": "onSlicingCancelled",
"SlicingFailed": "onSlicingFailed"
};
_.each(self.allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onEvent" + type)) {
viewModel["onEvent" + type](payload);
} else if (legacyEventHandlers.hasOwnProperty(type) && viewModel.hasOwnProperty(legacyEventHandlers[type])) {
// there might still be code that uses the old callbacks, make sure those still get called
// but log a warning
log.warn("View model " + viewModel.name + " is using legacy event handler " + legacyEventHandlers[type] + ", new handler is called " + legacyEventHandlers[type]);
viewModel[legacyEventHandlers[type]](payload);
}
});
break;
}
case "feedbackCommandOutput": {

View file

@ -189,7 +189,7 @@ $(function() {
self.sliceFile = function(file) {
if (!file) return;
self.slicing.show(file.origin, file.name);
self.slicing.show(file.origin, file.name, true);
};
self.initSdCard = function() {
@ -265,7 +265,7 @@ $(function() {
};
self.enableSlicing = function(data) {
return self.loginState.isUser() && !(self.isPrinting() || self.isPaused());
return self.loginState.isUser() && self.slicing.enableSlicingDialog();
};
self.enableAdditionalData = function(data) {
@ -379,7 +379,7 @@ $(function() {
}
function gcode_upload_fail(e, data) {
var error = "<p>" + gettext("Could not upload the file. Make sure that it is a GCODE file and has the extension \".gcode\" or \".gco\" or that it is an STL file with the extension \".stl\" and slicing support is enabled and configured.") + "</p>";
var error = "<p>" + gettext("Could not upload the file. Make sure that it is a GCODE file and has the extension \".gcode\" or \".gco\" or that it is an STL file with the extension \".stl\".") + "</p>";
error += pnotifyAdditionalInfo("<pre>" + data.jqXHR.responseText + "</pre>");
new PNotify({
title: "Upload failed",
@ -551,21 +551,21 @@ $(function() {
self.requestData();
};
self.onUpdatedFiles = function(payload) {
self.onEventUpdatedFiles = function(payload) {
if (payload.type == "gcode") {
self.requestData();
}
};
self.onSlicingDone = function(payload) {
self.onEventSlicingDone = function(payload) {
self.requestData();
};
self.onMetadataAnalysisFinished = function(payload) {
self.onEventMetadataAnalysisFinished = function(payload) {
self.requestData();
};
self.onMetadataStatisticsUpdated = function(payload) {
self.onEventMetadataStatisticsUpdated = function(payload) {
self.requestData();
};
}

View file

@ -21,6 +21,12 @@ $(function() {
self.profiles = ko.observableArray();
self.printerProfile = ko.observable();
self.configured_slicers = ko.computed(function() {
return _.filter(self.slicers(), function(slicer) {
return slicer.configured;
});
});
self.afterSlicingOptions = [
{"value": "none", "text": gettext("Do nothing")},
{"value": "select", "text": gettext("Select for printing")},
@ -28,7 +34,11 @@ $(function() {
];
self.afterSlicing = ko.observable("none");
self.show = function(target, file) {
self.show = function(target, file, force) {
if (!self.enableSlicingDialog() && !force) {
return;
}
self.requestData();
self.target = target;
self.file = file;
@ -43,6 +53,10 @@ $(function() {
self.profilesForSlicer(newValue);
});
self.enableSlicingDialog = ko.computed(function() {
return self.configured_slicers().length > 0;
});
self.enableSliceButton = ko.computed(function() {
return self.gcodeFilename() != undefined
&& self.gcodeFilename().trim() != ""
@ -75,13 +89,14 @@ $(function() {
name = slicer.key;
}
if (slicer.default) {
if (slicer.default && slicer.configured) {
selectedSlicer = slicer.key;
}
self.slicers.push({
key: slicer.key,
name: name
name: name,
configured: slicer.configured
});
});
@ -170,6 +185,10 @@ $(function() {
self.onStartup = function() {
self.requestData();
};
self.onEventSettingsUpdated = function(payload) {
self.requestData();
};
}
OCTOPRINT_VIEWMODELS.push([

View file

@ -4,42 +4,47 @@
<h3 data-bind="text: title"></h3>
</div>
<div class="modal-body">
<p>{{ _('Please configure which slicer and which slicing profile to use and name the GCode file to slice to below, or click "Cancel" if you do not wish to slice the file now.') }}</p>
<form class="form-horizontal">
<div class="control-group">
<label class="control-label">{{ _('Slicer') }}</label>
<div class="controls">
<select data-bind="options: slicers, optionsText: 'name', optionsValue: 'key', optionsCaption: '{{ _('Select a slicer...') }}', value: slicer, valueAllowUnset: true"></select>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Slicing Profile') }}</label>
<div class="controls">
<select data-bind="options: profiles, optionsText: 'name', optionsValue: 'key', optionsCaption: '{{ _('Select a slicing profile...') }}', value: profile, valueAllowUnset: true"></select>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Printer Profile') }}</label>
<div class="controls">
<select data-bind="options: printerProfiles.profiles.items, optionsText: 'name', optionsValue: 'id', value: printerProfile, optionsCaption: '{{ _('Select a printer profile...') }}'"></select>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('GCode Filename') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" data-bind="value: gcodeFilename">
<span class="add-on">.gco</span>
<div data-bind="visible: !enableSlicingDialog()">
<p>{{ _('Slicing is currently disabled since no slicer has been configured yet. Please configure a slicer under "Settings".') }}</p>
</div>
<div data-bind="visible: enableSlicingDialog()">
<p>{{ _('Please configure which slicer and which slicing profile to use and name the GCode file to slice to below, or click "Cancel" if you do not wish to slice the file now.') }}</p>
<form class="form-horizontal">
<div class="control-group">
<label class="control-label">{{ _('Slicer') }}</label>
<div class="controls">
<select data-bind="options: configured_slicers, optionsText: 'name', optionsValue: 'key', optionsCaption: '{{ _('Select a slicer...') }}', value: slicer, valueAllowUnset: true"></select>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('After slicing...') }}</label>
<div class="controls">
<select data-bind="options: afterSlicingOptions, optionsText: 'text', optionsValue: 'value', value: afterSlicing"></select>
<div class="control-group">
<label class="control-label">{{ _('Slicing Profile') }}</label>
<div class="controls">
<select data-bind="options: profiles, optionsText: 'name', optionsValue: 'key', optionsCaption: '{{ _('Select a slicing profile...') }}', value: profile, valueAllowUnset: true"></select>
</div>
</div>
</div>
</form>
<div class="control-group">
<label class="control-label">{{ _('Printer Profile') }}</label>
<div class="controls">
<select data-bind="options: printerProfiles.profiles.items, optionsText: 'name', optionsValue: 'id', value: printerProfile, optionsCaption: '{{ _('Select a printer profile...') }}'"></select>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('GCode Filename') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" data-bind="value: gcodeFilename">
<span class="add-on">.gco</span>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('After slicing...') }}</label>
<div class="controls">
<select data-bind="options: afterSlicingOptions, optionsText: 'text', optionsValue: 'value', value: afterSlicing"></select>
</div>
</div>
</form>
</div>
</div>
<div class="modal-footer">
<a href="#" class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Cancel') }}</a>