diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py deleted file mode 100644 index b0509df4..00000000 --- a/src/octoprint/plugins/cura/__init__.py +++ /dev/null @@ -1,421 +0,0 @@ -# coding=utf-8 -from __future__ import absolute_import - -__author__ = "Gina Häußge " -__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" -__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" - -import logging -import logging.handlers -import os -import flask -import math - -import octoprint.plugin -import octoprint.util -import octoprint.slicing -import octoprint.settings - -from .profile import Profile - -class CuraPlugin(octoprint.plugin.SlicerPlugin, - octoprint.plugin.SettingsPlugin, - octoprint.plugin.TemplatePlugin, - octoprint.plugin.AssetPlugin, - octoprint.plugin.BlueprintPlugin, - octoprint.plugin.StartupPlugin): - - def __init__(self): - self._logger = logging.getLogger("octoprint.plugins.cura") - self._cura_logger = logging.getLogger("octoprint.plugins.cura.engine") - - # setup job tracking across threads - import threading - self._slicing_commands = dict() - self._cancelled_jobs = [] - self._job_mutex = threading.Lock() - - ##~~ StartupPlugin API - - def on_startup(self, host, port): - # setup our custom logger - cura_logging_handler = logging.handlers.RotatingFileHandler(self._settings.getPluginLogfilePath(postfix="engine"), maxBytes=2*1024*1024) - cura_logging_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s")) - cura_logging_handler.setLevel(logging.DEBUG) - - self._cura_logger.addHandler(cura_logging_handler) - self._cura_logger.setLevel(logging.DEBUG if self._settings.getBoolean(["debug_logging"]) else logging.CRITICAL) - self._cura_logger.propagate = False - - ##~~ BlueprintPlugin API - - @octoprint.plugin.BlueprintPlugin.route("/import", methods=["POST"]) - def importCuraProfile(self): - import datetime - import tempfile - - from octoprint.server import slicingManager - - input_name = "file" - input_upload_name = input_name + "." + self._settings.globalGet(["server", "uploads", "nameSuffix"]) - input_upload_path = input_name + "." + self._settings.globalGet(["server", "uploads", "pathSuffix"]) - - if input_upload_name in flask.request.values and input_upload_path in flask.request.values: - filename = flask.request.values[input_upload_name] - try: - profile_dict = Profile.from_cura_ini(flask.request.values[input_upload_path]) - except Exception as e: - return flask.make_response("Something went wrong while converting imported profile: {message}".format(e.message), 500) - - elif input_name in flask.request.files: - temp_file = tempfile.NamedTemporaryFile("wb", delete=False) - try: - temp_file.close() - upload = flask.request.files[input_name] - upload.save(temp_file.name) - profile_dict = Profile.from_cura_ini(temp_file.name) - except Exception as e: - return flask.make_response("Something went wrong while converting imported profile: {message}".format(e.message), 500) - finally: - os.remove(temp_file) - - filename = upload.filename - - else: - return flask.make_response("No file included", 400) - - if profile_dict is None: - return flask.make_response("Could not convert Cura profile", 400) - - name, _ = os.path.splitext(filename) - - # default values for name, display name and description - profile_name = _sanitize_name(name) - profile_display_name = name - profile_description = "Imported from {filename} on {date}".format(filename=filename, date=octoprint.util.getFormattedDateTime(datetime.datetime.now())) - profile_allow_overwrite = False - - # overrides - if "name" in flask.request.values: - profile_name = flask.request.values["name"] - if "displayName" in flask.request.values: - profile_display_name = flask.request.values["displayName"] - if "description" in flask.request.values: - profile_description = flask.request.values["description"] - if "allowOverwrite" in flask.request.values: - from octoprint.server.api import valid_boolean_trues - profile_allow_overwrite = flask.request.values["allowOverwrite"] in valid_boolean_trues - - slicingManager.save_profile("cura", - profile_name, - profile_dict, - allow_overwrite=profile_allow_overwrite, - display_name=profile_display_name, - description=profile_description) - - result = dict( - resource=flask.url_for("api.slicingGetSlicerProfile", slicer="cura", name=profile_name, _external=True), - displayName=profile_display_name, - description=profile_description - ) - r = flask.make_response(flask.jsonify(result), 201) - r.headers["Location"] = result["resource"] - return r - - ##~~ AssetPlugin API - - def get_assets(self): - return { - "js": ["js/cura.js"], - "less": ["less/cura.less"], - "css": ["css/cura.css"] - } - - ##~~ SettingsPlugin API - - def on_settings_save(self, data): - old_debug_logging = self._settings.getBoolean(["debug_logging"]) - - super(CuraPlugin, self).on_settings_save(data) - - new_debug_logging = self._settings.getBoolean(["debug_logging"]) - if old_debug_logging != new_debug_logging: - if new_debug_logging: - self._cura_logger.setLevel(logging.DEBUG) - else: - self._cura_logger.setLevel(logging.CRITICAL) - - def get_settings_defaults(self): - return dict( - cura_engine=None, - default_profile=None, - debug_logging=False - ) - - ##~~ SlicerPlugin API - - def is_slicer_configured(self): - cura_engine = self._settings.get(["cura_engine"]) - if cura_engine is not None and os.path.exists(cura_engine): - return True - else: - self._logger.info("Path to CuraEngine has not been configured yet or does not exist (currently set to %r), Cura will not be selectable for slicing" % cura_engine) - - def get_slicer_properties(self): - return dict( - type="cura", - name="CuraEngine", - same_device=True, - progress_report=True - ) - - def get_slicer_default_profile(self): - path = self._settings.get(["default_profile"]) - if not path: - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "profiles", "default.profile.yaml") - return self.get_slicer_profile(path) - - def get_slicer_profile(self, path): - profile_dict = self._load_profile(path) - - display_name = None - description = None - if "_display_name" in profile_dict: - display_name = profile_dict["_display_name"] - del profile_dict["_display_name"] - if "_description" in profile_dict: - description = profile_dict["_description"] - del profile_dict["_description"] - - properties = self.get_slicer_properties() - return octoprint.slicing.SlicingProfile(properties["type"], "unknown", profile_dict, display_name=display_name, description=description) - - def save_slicer_profile(self, path, profile, allow_overwrite=True, overrides=None): - new_profile = Profile.merge_profile(profile.data, overrides=overrides) - - if profile.display_name is not None: - new_profile["_display_name"] = profile.display_name - if profile.description is not None: - new_profile["_description"] = profile.description - - self._save_profile(path, new_profile, allow_overwrite=allow_overwrite) - - def do_slice(self, model_path, printer_profile, machinecode_path=None, profile_path=None, position=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None): - try: - with self._job_mutex: - if not profile_path: - profile_path = self._settings.get(["default_profile"]) - if not machinecode_path: - path, _ = os.path.splitext(model_path) - machinecode_path = path + ".gco" - - if position and isinstance(position, dict) and "x" in position and "y" in position: - posX = position["x"] - posY = position["y"] - else: - posX = None - posY = None - - if on_progress: - if not on_progress_args: - on_progress_args = () - if not on_progress_kwargs: - on_progress_kwargs = dict() - - self._cura_logger.info("### Slicing %s to %s using profile stored at %s" % (model_path, machinecode_path, profile_path)) - - engine_settings = self._convert_to_engine(profile_path, printer_profile, posX, posY) - - executable = self._settings.get(["cura_engine"]) - if not executable: - return False, "Path to CuraEngine is not configured " - - working_dir, _ = os.path.split(executable) - args = ['"%s"' % executable, '-v', '-p'] - for k, v in engine_settings.items(): - args += ["-s", '"%s=%s"' % (k, str(v))] - args += ['-o', '"%s"' % machinecode_path, '"%s"' % model_path] - - import sarge - command = " ".join(args) - self._logger.info("Running %r in %s" % (command, working_dir)) - - p = sarge.run(command, cwd=working_dir, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) - p.wait_events() - self._slicing_commands[machinecode_path] = p.commands[0] - - try: - layer_count = None - step_factor = dict( - inset=0, - skin=1, - export=2 - ) - analysis = None - while p.returncode is None: - line = p.stderr.readline(timeout=0.5) - if not line: - p.commands[0].poll() - continue - - self._cura_logger.debug(line.strip()) - - if on_progress is not None: - # The Cura slicing process has three individual steps, each consisting of 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 * . - # - # 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: - # - # The individual progress per each of the three steps gets reported on stderr in a line of - # the format: - # - # Progress::: - # - # Thus, for determining the overall progress the following formula applies: - # - # progress = * + / * 3 - # - # with being 0 for "inset", 1 for "skin" and 2 for "export". - - 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) - - elif line.startswith("Print time:"): - try: - print_time = int(line[len("Print time:"):].strip()) - if analysis is None: - analysis = dict() - analysis["estimatedPrintTime"] = print_time - except: - pass - - elif line.startswith("Filament:") or line.startswith("Filament2:"): - if line.startswith("Filament:"): - filament_str = line[len("Filament:"):].strip() - tool_key = "tool0" - else: - filament_str = line[len("Filament2:"):].strip() - tool_key = "tool1" - - try: - filament = int(filament_str) - if analysis is None: - analysis = dict() - if not "filament" in analysis: - analysis["filament"] = dict() - if not tool_key in analysis["filament"]: - analysis["filament"][tool_key] = dict() - analysis["filament"][tool_key]["length"] = filament - if "filamentDiameter" in engine_settings: - radius_in_cm = float(int(engine_settings["filamentDiameter"]) / 10000.0) / 2.0 - filament_in_cm = filament / 10.0 - analysis["filament"][tool_key]["volume"] = filament_in_cm * math.pi * radius_in_cm * radius_in_cm - except: - pass - finally: - p.close() - - with self._job_mutex: - if machinecode_path in self._cancelled_jobs: - self._cura_logger.info("### Cancelled") - raise octoprint.slicing.SlicingCancelled() - - self._cura_logger.info("### Finished, returncode %d" % p.returncode) - if p.returncode == 0: - return True, dict(analysis=analysis) - else: - self._logger.warn("Could not slice via Cura, got return code %r" % p.returncode) - return False, "Got returncode %r" % p.returncode - - except octoprint.slicing.SlicingCancelled as e: - raise e - except: - self._logger.exception("Could not slice via Cura, got an unknown error") - return False, "Unknown error, please consult the log file" - - finally: - with self._job_mutex: - if machinecode_path in self._cancelled_jobs: - self._cancelled_jobs.remove(machinecode_path) - if machinecode_path in self._slicing_commands: - del self._slicing_commands[machinecode_path] - - self._cura_logger.info("-" * 40) - - def cancel_slicing(self, machinecode_path): - with self._job_mutex: - if machinecode_path in self._slicing_commands: - self._cancelled_jobs.append(machinecode_path) - command = self._slicing_commands[machinecode_path] - if command is not None: - command.terminate() - self._logger.info("Cancelled slicing of %s" % machinecode_path) - - def _load_profile(self, path): - import yaml - profile_dict = dict() - with open(path, "r") as f: - try: - profile_dict = yaml.safe_load(f) - except: - raise IOError("Couldn't read profile from {path}".format(path=path)) - return profile_dict - - def _save_profile(self, path, profile, allow_overwrite=True): - if not allow_overwrite and os.path.exists(path): - raise IOError("Cannot overwrite {path}".format(path=path)) - - import yaml - with open(path, "wb") as f: - yaml.safe_dump(profile, f, default_flow_style=False, indent=" ", allow_unicode=True) - - def _convert_to_engine(self, profile_path, printer_profile, posX, posY): - profile = Profile(self._load_profile(profile_path), printer_profile, posX, posY) - return profile.convert_to_engine() - -def _sanitize_name(name): - if name is None: - return None - - if "/" in name or "\\" in name: - raise ValueError("name must not contain / or \\") - - import string - valid_chars = "-_.() {ascii}{digits}".format(ascii=string.ascii_letters, digits=string.digits) - sanitized_name = ''.join(c for c in name if c in valid_chars) - sanitized_name = sanitized_name.replace(" ", "_") - return sanitized_name.lower() - -__plugin_name__ = "CuraEngine" -__plugin_version__ = "0.1" -__plugin_author__ = "Gina Häußge" -__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura" -__plugin_description__ = "Adds support for slicing via CuraEngine from within OctoPrint" -__plugin_license__ = "AGPLv3" -__plugin_implementations__ = [CuraPlugin()] \ No newline at end of file diff --git a/src/octoprint/plugins/svgtogcode/__init__.py b/src/octoprint/plugins/svgtogcode/__init__.py index d78706a0..972def38 100644 --- a/src/octoprint/plugins/svgtogcode/__init__.py +++ b/src/octoprint/plugins/svgtogcode/__init__.py @@ -287,6 +287,7 @@ class SvgToGcodePlugin(octoprint.plugin.SlicerPlugin, self._logger.info("Running %r in %s" % (command, working_dir)) try: p = sarge.run(command, cwd=working_dir, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) + p.wait_events() try: with self._slicing_commands_mutex: self._slicing_commands[machinecode_path] = p.commands[0]