From 12557d28d554cbcc2d19e49bf5351fbaab5fb234 Mon Sep 17 00:00:00 2001 From: Formerlurker Date: Tue, 27 Oct 2020 20:42:56 -0500 Subject: [PATCH] Implement features #49, #50, #51, #53, and #54. Solve issues #46, #45, #43, #39, #36. --- octoprint_arc_welder/__init__.py | 227 ++++++++++++++++-- octoprint_arc_welder/preprocessor.py | 195 ++++++++++++--- .../static/css/arc_welder.css | 4 + .../help/settings.print_after_processing.md | 1 + .../help/settings.select_after_processing.md | 1 + octoprint_arc_welder/static/js/arc_welder.js | 205 +++++++++++++--- .../templates/arc_welder_settings.jinja2 | 32 ++- .../templates/arc_welder_tab.jinja2 | 58 +++-- 8 files changed, 611 insertions(+), 112 deletions(-) create mode 100644 octoprint_arc_welder/static/docs/help/settings.print_after_processing.md create mode 100644 octoprint_arc_welder/static/docs/help/settings.select_after_processing.md diff --git a/octoprint_arc_welder/__init__.py b/octoprint_arc_welder/__init__.py index 8fdf58c..6fe947f 100644 --- a/octoprint_arc_welder/__init__.py +++ b/octoprint_arc_welder/__init__.py @@ -33,6 +33,7 @@ import threading from distutils.version import LooseVersion from six import string_types +from past.builtins import xrange from flask import request, jsonify import os import sys @@ -98,12 +99,25 @@ class ArcWelderPlugin( SOURCE_FILE_DELETE_MANUAL = "manual-only" SOURCE_FILE_DELETE_DISABLED = "disabled" + PRINT_AFTER_PROCESSING_BOTH = "both" + PRINT_AFTER_PROCESSING_MANUAL = "manual-only" + PRINT_AFTER_PROCESSING_AUTO = "auto-only" + PRINT_AFTER_PROCESSING_DISABLED = "disabled" + + SELECT_FILE_AFTER_PROCESSING_BOTH = "both" + SELECT_FILE_AFTER_PROCESSING_MANUAL = "manual-only" + SELECT_FILE_AFTER_PROCESSING_AUTO = "auto-only" + SELECT_FILE_AFTER_PROCESSING_DISABLED = "disabled" + def __init__(self): super(ArcWelderPlugin, self).__init__() self.preprocessing_job_guid = None self.preprocessing_job_source_file_path = "" self.preprocessing_job_target_file_name = "" self.is_cancelled = False + # Note, you cannot count the number of items left to process using the + # _processing_queue. Items put into this queue will be inserted into + # an internal dequeue by the preprocessor self._processing_queue = queue.Queue() self.settings_default = dict( use_octoprint_settings=True, @@ -120,7 +134,9 @@ def __init__(self): ), feature_settings=dict( file_processing=ArcWelderPlugin.FILE_PROCESSING_BOTH, - delete_source=ArcWelderPlugin.SOURCE_FILE_DELETE_DISABLED + delete_source=ArcWelderPlugin.SOURCE_FILE_DELETE_DISABLED, + select_after_processing=ArcWelderPlugin.SELECT_FILE_AFTER_PROCESSING_BOTH, + print_after_processing=ArcWelderPlugin.PRINT_AFTER_PROCESSING_DISABLED ), enabled=True, logging_configuration=dict( @@ -131,8 +147,12 @@ def __init__(self): version=__version__, git_version=__git_version__, ) - # start the preprocessor worker + # preprocessor worker self._preprocessor_worker = None + # track removed files to handle moves + self._recently_moved_files = [] + # a wait period for clearing recently moved files + self._recently_moved_file_wait_ms = 1000 def on_after_startup(self): logging_configurator.configure_loggers( @@ -228,8 +248,11 @@ def process_request(self): # Extract only the supported metadata from the added file additional_metadata = self.get_additional_metadata(metadata) # add the file and metadata to the processor queue - self.add_file_to_preprocessor_queue(path, additional_metadata, True) - return jsonify({"success": True}) + success = self.add_file_to_preprocessor_queue(path, additional_metadata, True) + if success: + return jsonify({"success": True}) + else: + return jsonify({"success": False}) return jsonify({"success": False, "message": "Arc Welder is Disabled."}) @octoprint.plugin.BlueprintPlugin.route("/restoreDefaultSettings", methods=["POST"]) @@ -282,7 +305,12 @@ def get_assets(self): css=["css/arc_welder.css"], ) - def _is_file_selected(self, path, origin): + def _select_file(self, path): + if path and len(path) > 1 and path[0] == '/': + path = path[1:] + self._printer.select_file(path, False) + + def _get_is_file_selected(self, path, origin): current_job = self._printer.get_current_job() current_file = current_job.get("file", {'path': "", "origin": ""}) current_file_path = current_file["path"] @@ -298,11 +326,10 @@ def _get_is_printing(self, path=None): return False # If the path parameter is provided, check for a locally printing file of the same path if path: - return self._is_file_selected(path, FileDestinations.LOCAL) + return self._get_is_file_selected(path, FileDestinations.LOCAL) + return True - return False # Properties - @property def _log_file_path(self): return self._settings.get_plugin_logfile_path() @@ -415,6 +442,39 @@ def _target_postfix(self): target_postfix = target_postfix.strip() return target_postfix + @property + def _select_file_after_automatic_processing(self): + return self._settings.get( + ["feature_settings", "select_after_processing"] + ) in [ + ArcWelderPlugin.SELECT_FILE_AFTER_PROCESSING_BOTH, ArcWelderPlugin.SELECT_FILE_AFTER_PROCESSING_AUTO + ] + + + @property + def _select_file_after_manual_processing(self): + return self._settings.get( + ["feature_settings", "select_after_processing"] + ) in [ + ArcWelderPlugin.SELECT_FILE_AFTER_PROCESSING_BOTH, ArcWelderPlugin.SELECT_FILE_AFTER_PROCESSING_MANUAL + ] + + @property + def _print_after_manual_processing(self): + return self._settings.get( + ["feature_settings", "print_after_processing"] + ) in [ + ArcWelderPlugin.PRINT_AFTER_PROCESSING_BOTH, ArcWelderPlugin.PRINT_AFTER_PROCESSING_MANUAL + ] + + @property + def _print_after_automatic_processing(self): + return self._settings.get( + ["feature_settings", "print_after_processing"] + ) in [ + ArcWelderPlugin.PRINT_AFTER_PROCESSING_BOTH, ArcWelderPlugin.PRINT_AFTER_PROCESSING_AUTO + ] + @property def _show_started_notification(self): return self._settings.get(["notification_settings", "show_started_notification"]) @@ -431,6 +491,18 @@ def _show_completed_notification(self): def _delete_source_after_processing(self): return self._settings.get(["delete_source_after_processing"]) + def _get_print_after_processing(self, is_manual): + if is_manual: + return self._print_after_manual_processing + else: + return self._print_after_automatic_processing + + def _get_select_file_after_processing(self, is_manual): + if is_manual: + return self._select_file_after_manual_processing + else: + return self._select_file_after_automatic_processing + def get_storage_path_and_name(self, storage_path, add_prefix_and_postfix): path, name = self._file_manager.split_path(FileDestinations.LOCAL, storage_path) if add_prefix_and_postfix: @@ -595,7 +667,10 @@ def copy_thumbnail(self, thumbnail_src, thumbnail_path, gcode_filename): return new_metadata return None - def preprocessing_started(self, path, preprocessor_args): + def preprocessing_started(self, task): + path = task["path"] + preprocessor_args = task["preprocessor_args"] + print_after_processing = task["print_after_processing"] new_path, new_name = self.get_storage_path_and_name( path, not self._overwrite_source_file ) @@ -616,6 +691,9 @@ def preprocessing_started(self, path, preprocessor_args): preprocessor_args["log_level"] ) + if print_after_processing: + logger.info("The queued file will be printed after processing is complete.") + if self._show_started_notification: # A bit of a hack. Need to rethink the start notification. if self._show_progress_bar: @@ -656,11 +734,23 @@ def preprocessing_progress(self, progress): } self._plugin_manager.send_plugin_message(self._identifier, data) time.sleep(0.01) - # return true if processing should continue. This is set by the cancelled blueprint plugin. + # return true if processing should continue. + # If self.is_cancelled is true (This is set by the cancelled blueprint plugin) + # or the we are currently printing, return false return not self.is_cancelled - def preprocessing_cancelled(self, path, preprocessor_args): - message = "Preprocessing has been cancelled for '{0}'.".format(path) + def preprocessing_cancelled(self, task, auto_cancelled): + + path = task["path"] + preprocessor_args = task["preprocessor_args"] + + if auto_cancelled: + message = "Cannot process while printing. Arc Welding has been cancelled for '{0}'. The file will be " \ + "processed once printing has completed. " + else: + message = "Preprocessing has been cancelled for '{0}'." + + message = message.format(path) data = { "message_type": "preprocessing-cancelled", "source_filename": self.preprocessing_job_source_file_path, @@ -670,7 +760,16 @@ def preprocessing_cancelled(self, path, preprocessor_args): } self._plugin_manager.send_plugin_message(self._identifier, data) - def preprocessing_success(self, results, path, preprocessor_args, additional_metadata, is_manual_request): + def preprocessing_success( + self, results, task + ): + # extract the task data + path = task["path"] + preprocessor_args = task["preprocessor_args"] + additional_metadata = task["additional_metadata"] + is_manual_request = task["is_manual_request"] + print_after_processing = task["print_after_processing"] + select_file_after_processing = task["select_file_after_processing"] # save the newly created file. This must be done before # exiting this callback because the target file isn't # guaranteed to exist later. @@ -688,7 +787,6 @@ def preprocessing_success(self, results, path, preprocessor_args, additional_met } self._plugin_manager.send_plugin_message(self._identifier, data) return - if ( ( (is_manual_request and self._delete_source_after_manual_processing) @@ -699,7 +797,7 @@ def preprocessing_success(self, results, path, preprocessor_args, additional_met ): if not self._get_is_printing(path): # if the file is selected, deselect it. - if self._is_file_selected(path, FileDestinations.LOCAL): + if self._get_is_file_selected(path, FileDestinations.LOCAL): self._printer.unselect_file() # delete the source file logger.info("Deleting source file at %s.", path) @@ -720,6 +818,21 @@ def preprocessing_success(self, results, path, preprocessor_args, additional_met } self._plugin_manager.send_plugin_message(self._identifier, data) + if select_file_after_processing or print_after_processing: + try: + self._select_file(new_path) + # make sure the file is selected + if ( + self._get_is_file_selected(new_path, FileDestinations.LOCAL) + and print_after_processing + and not self._get_is_printing() + ): + self._printer.start_print(tags=set('arc_welder')) + except (octoprint.printer.InvalidFileType, octoprint.printer.InvalidFileLocation): + # we don't care too much if OctoPrint can't select the file. There's nothing + # we can do about it anyway + pass + def preprocessing_completed(self): data = { "message_type": "preprocessing-complete" @@ -729,7 +842,7 @@ def preprocessing_completed(self): self.preprocessing_job_target_file_name = None self._plugin_manager.send_plugin_message(self._identifier, data) - def preprocessing_failed(self, message): + def preprocessing_failed(self, message, task): data = { "message_type": "preprocessing-failed", "source_filename": self.preprocessing_job_source_file_path, @@ -739,10 +852,58 @@ def preprocessing_failed(self, message): } self._plugin_manager.send_plugin_message(self._identifier, data) + def _clean_removed_files(self): + # get the current date and time so we can remove old files + cutoff_date_time = ( + datetime.datetime.now() - + datetime.timedelta(milliseconds=self._recently_moved_file_wait_ms) + ) + # iterate over the list in reverse, so we can remove elements + for i in xrange(len(self._recently_moved_files) - 1, -1, -1): + current_file = self._recently_moved_files[i] + if current_file["date_removed"] < cutoff_date_time: + del self._recently_moved_files[i] + + # first remove any old moved files + def _add_removed_file(self, file_name): + # first, clean up the removed file list + self._clean_removed_files() + # now, add the file + self._recently_moved_files.append({ + "file_name": file_name, + "date_removed": datetime.datetime.now() + }) + + # see if a file was recently removed to handle file move + def _is_file_moved(self, file_name): + # first, clean up the removed file list + self._clean_removed_files() + # now, iterate the list and see if the file_name exists + for item in self._recently_moved_files: + if item["file_name"] == file_name: + return True + return False + def on_event(self, event, payload): - # Need to use file added event to catch uploads and other non-upload methods of adding a file. - if event == Events.FILE_ADDED: + if event == Events.PRINT_STARTED: + printing_stopped = self._preprocessor_worker.prevent_printing_for_existing_jobs() + if printing_stopped: + self.send_notification_toast( + "warning", "Arc-Welder: Cannot Print", + "A print is running, but processing tasks are in the queue that are marked 'Print After " + "Processing'. To protect your printer, Arc Welder will not automatically print when processing " + "is completed.", + True, + key="auto_print_cancelled", close_keys=["auto_print_cancelled"] + ) + elif event == Events.FILE_ADDED: + # Need to use file added event to catch uploads and other non-upload methods of adding a file. + # skip the file if it was recently moved. This COULD cause problems due to potential + # race conditions, but in most cases it will not. + if self._is_file_moved(payload["name"]): + return + if not self._enabled or not self._auto_pre_processing_enabled: return # Note, 'target' is the key for FILE_UPLOADED, but 'storage' is the key for FILE_ADDED @@ -768,6 +929,9 @@ def on_event(self, event, payload): additional_metadata = self.get_additional_metadata(metadata) # Add this file to the processor queue. self.add_file_to_preprocessor_queue(path, additional_metadata, False) + elif event == Events.FILE_REMOVED: + # add the file to the removed list + self._add_removed_file(payload["name"]) def get_additional_metadata(self, metadata): # list of supported metadata @@ -782,7 +946,8 @@ def get_additional_metadata(self, metadata): def add_file_to_preprocessor_queue(self, path, additional_metadata, is_manual_request): # get the file by path # file = self._file_manager.get_file(FileDestinations.LOCAL, path) - if self._get_is_printing(): + is_printing = self._get_is_printing() + if is_printing: self.send_notification_toast( "warning", "Arc-Welder: Unable to Process", "Cannot preprocess gcode while a print is in progress because print quality may be affected. The " @@ -799,7 +964,26 @@ def add_file_to_preprocessor_queue(self, path, additional_metadata, is_manual_re path = '/' + path preprocessor_args = self.get_preprocessor_arguments(path_on_disk) - self._processing_queue.put((path, preprocessor_args, additional_metadata, is_manual_request)) + print_after_processing = not is_printing and self._get_print_after_processing(is_manual_request) + select_after_processing = self._get_select_file_after_processing(is_manual_request) + task = { + "path": path, + "preprocessor_args": preprocessor_args, + "additional_metadata": additional_metadata, + "is_manual_request": is_manual_request, + "print_after_processing": print_after_processing, + "select_file_after_processing": select_after_processing + } + results = self._preprocessor_worker.add_task(task) + if not results["success"]: + self.send_notification_toast( + "warning", "Arc-Welder: Unable To Queue File", + results["error_message"], + True, + key="unable_to_queue", close_keys=["unable_to_queue"] + ) + return False + return True def register_custom_routes(self, server_routes, *args, **kwargs): # version specific permission validator @@ -831,7 +1015,6 @@ def admin_permission_validator(flask_request): # ~~ software update hook - arc_welder_update_info = dict( displayName="Arc Welder: Anti-Stutter", # version check: github repository @@ -855,8 +1038,6 @@ def admin_permission_validator(flask_request): ], ) - - def get_release_info(self): # Starting with V1.5.0 prerelease branches are supported! if LooseVersion(octoprint.server.VERSION) < LooseVersion("1.5.0"): diff --git a/octoprint_arc_welder/preprocessor.py b/octoprint_arc_welder/preprocessor.py index 70f68f0..0c896ca 100644 --- a/octoprint_arc_welder/preprocessor.py +++ b/octoprint_arc_welder/preprocessor.py @@ -33,11 +33,13 @@ import shutil import os import PyArcWelder as converter # must import AFTER log, else this will fail to log and may crasy +from collections import deque try: import queue except ImportError: import Queue as queue + logging_configurator = log.LoggingConfigurator("arc_welder", "arc_welder.", "octoprint_arc_welder.") root_logger = logging_configurator.get_root_logger() # so that we can @@ -61,9 +63,13 @@ def __init__( super(PreProcessorWorker, self).__init__() self._source_file_path = os.path.join(data_folder, "source.gcode") self._target_file_path = os.path.join(data_folder, "target.gcode") - self._idle_sleep_seconds = 2.5 # wait at most 2.5 seconds for a rendering job from the queue - self._task_queue = task_queue + self._processing_file_path = None + self._idle_sleep_seconds = 2.5 # wait at most 2.5 seconds for a rendering job from the queue + # holds incoming tasks that need to be added to the _task_deque + self._incoming_task_queue = task_queue + self._task_deque = deque() self._is_printing_callback = is_printing_callback + self.print_after_processing = False self._start_callback = start_callback self._progress_callback = progress_callback self._cancel_callback = cancel_callback @@ -76,15 +82,78 @@ def __init__( self.r_lock = threading.RLock() def cancel_all(self): - while not self._task_queue.empty(): - path, processor_args = self._task_queue.get(False) - logger.info("Preprocessing of %s has been cancelled.", processor_args["path"]) - self._cancel_callback(path, processor_args) + with self.r_lock: + while not self._incoming_task_queue.empty(): + task = self._incoming_task_queue.get(False) + path = task["path"] + preprocessor_args = task["preprocessor_args"] + logger.info("Preprocessing of %s has been cancelled.", preprocessor_args["path"]) + self._cancel_callback(path, preprocessor_args) + while not len(self._task_deque) == 0: + task = self._task_deque.pop() + path = task["path"] + preprocessor_args = task["preprocessor_args"] + logger.info("Preprocessing of %s has been cancelled.", preprocessor_args["path"]) + self._cancel_callback(path, preprocessor_args) def is_processing(self): with self.r_lock: - is_processing = (not self._task_queue.empty()) or self._is_processing - return is_processing + return ( + not self._incoming_task_queue.empty() + or self._is_processing + or len(self._task_deque) != 0 + ) + + def add_task(self, new_task): + with self.r_lock: + results = { + "success": False, + "error_message": "" + } + + path = new_task["preprocessor_args"]["path"] + logger.info("Adding a new task to the processor queue at %s.", path) + # make sure the task isn't already being processed + if new_task["path"] == self._current_file_processing_path: + results["error_message"] = "This file is currently processing and cannot be added again until " \ + "processing completes. " + logger.info(results["error_message"]) + return results + for existing_task in self._task_deque: + if existing_task["path"] == new_task["path"]: + results["error_message"] = "This file is already queued for processing and cannot be added again." + logger.info(results["error_message"]) + return results + + if new_task["print_after_processing"]: + if self._is_printing_callback(): + new_task["print_after_processing"] = False + logger.info("The task was marked for printing after completion, but this has been cancelled " + "because a print is currently running") + else: + logger.info("This task will be printed after preprocessing is complete.") + self._task_deque.appendleft(new_task) + results["success"] = True + logger.info("The task was added successfully.") + return results + + # set print_after_processing to False for all tasks + def prevent_printing_for_existing_jobs(self): + with self.r_lock: + # first make sure any internal queue items are added to the queue + has_cancelled_print_after_processing = False + for task in self._task_deque: + if task["print_after_processing"]: + task["print_after_processing"] = False + has_cancelled_print_after_processing = True + path = task["preprocessor_args"]["path"] + logger.info("Print after processing has been cancelled for %s.", path) + # make sure the current task does not print after it is complete + if self.print_after_processing: + self.print_after_processing = False + has_cancelled_print_after_processing = True + logger.info("Print after processing has been cancelled for the file currently processing.") + return has_cancelled_print_after_processing def run(self): while True: @@ -92,49 +161,69 @@ def run(self): # see if there are any rendering tasks. time.sleep(self._idle_sleep_seconds) - if not self._task_queue.empty(): - # add an additional sleep in case this file was uploaded - # from cura to give the printer state a chance to catch up. - time.sleep(0.1) - if self._is_printing_callback(): + # see if we are printing + is_printing = self._is_printing_callback() + + if is_printing: + # if we are printing, do not process anything continue - path, processor_args, additional_metadata, is_manual_request = self._task_queue.get(False) + # We want this next step to be atomic + with self.r_lock: + # the _task_deque is not thread safe. only use within a lock + if len(self._task_deque) < 1: + continue + # get the task + try: + task = self._task_deque.pop() + except IndexError: + # no items, they could have been cleared or cancelled + continue + self.print_after_processing = task["print_after_processing"] + self._current_file_processing_path = task["path"] + self._processing_cancelled_while_printing = False + success = False try: - self._process(path, processor_args, additional_metadata, is_manual_request) + self._process(task) except Exception as e: logger.exception("An unhandled exception occurred while preprocessing the gcode file.") message = "An error occurred while preprocessing {0}. Check plugin_arc_welder.log for details.".\ - format(path) + format(task["path"]) self._failed_callback(message) finally: self._completed_callback() + with self.r_lock: + self._current_file_processing_path = None + self.print_after_processing = None + self._processing_cancelled_while_printing = False except queue.Empty: pass - def _process(self, path, processor_args, additional_metadata, is_manual_request): - self._start_callback(path, processor_args) + def _process(self, task): + self._start_callback(task) logger.info( - "Copying source gcode file at %s to %s for processing.", processor_args["path"], self._source_file_path + "Copying source gcode file at %s to %s for processing.", + task["preprocessor_args"]["path"], + self._source_file_path ) - if not os.path.exists(processor_args["path"]): + if not os.path.exists(task["preprocessor_args"]["path"]): message = "The source file path at '{0}' does not exist. It may have been moved or deleted". \ - format(processor_args["path"]) + format(task["preprocessor_args"]["path"]) self._failed_callback(message) return - shutil.copy(processor_args["path"], self._source_file_path) - source_filename = utilities.get_filename_from_path(processor_args["path"]) - # Add arguments to the processor_args dict - processor_args["on_progress_received"] = self._progress_received - processor_args["source_file_path"] = self._source_file_path - processor_args["target_file_path"] = self._target_file_path + shutil.copy(task["preprocessor_args"]["path"], self._source_file_path) + source_filename = utilities.get_filename_from_path(task["preprocessor_args"]["path"]) + # Add arguments to the preprocessor_args dict + task["preprocessor_args"]["on_progress_received"] = self._progress_received + task["preprocessor_args"]["source_file_path"] = self._source_file_path + task["preprocessor_args"]["target_file_path"] = self._target_file_path # Convert the file via the C++ extension logger.info( "Calling conversion routine on copied source gcode file to target at %s.", self._source_file_path ) try: - results = converter.ConvertFile(processor_args) + results = converter.ConvertFile(task["preprocessor_args"]) except Exception as e: # It would be better to catch only specific errors here, but we will log them. Any # unhandled errors that occur would shut down the worker thread until reboot. @@ -142,14 +231,14 @@ def _process(self, path, processor_args, additional_metadata, is_manual_request) # Log the exception logger.exception( - "An unexpected exception occurred while preprocessing %s.", processor_args["path"] + "An unexpected exception occurred while preprocessing %s.", task["preprocessor_args"]["path"] ) # create results that will be sent back to the client for notification of failure. results = { "cancelled": False, "success": False, "message": "An unexpected exception occurred while preprocessing the gcode file at {0}. Please see " - "plugin_arc_welder.log for more details.".format(processor_args["path"]) + "plugin_arc_welder.log for more details.".format(task["preprocessor_args"]["path"]) } # the progress payload will all be in bytes (str for python 2) format. # Make sure everything is in unicode (str for python3) because mixed encoding @@ -158,14 +247,38 @@ def _process(self, path, processor_args, additional_metadata, is_manual_request) encoded_results = utilities.dict_encode(results) encoded_results["source_filename"] = source_filename if encoded_results["cancelled"]: - logger.info("Preprocessing of %s has been cancelled.", processor_args["path"]) - self._cancel_callback(path, processor_args) + auto_cancelled = self._processing_cancelled_while_printing + self._processing_cancelled_while_printing = False + if auto_cancelled: + logger.info( + "Preprocessing of %s has been cancelled automatically because printing has started. Readding " + "task to the queue. " + , task["preprocessor_args"]["path"]) + with self.r_lock: + task["print_after_processing"] = self.print_after_processing + self._task_deque.appendleft(task) + else: + logger.info( + "Preprocessing of %s has been cancelled by the user." + , task["preprocessor_args"]["path"]) + self._cancel_callback(task, auto_cancelled) elif encoded_results["success"]: - logger.info("Preprocessing of %s completed.", processor_args["path"]) - # Save the produced gcode file - self._success_callback(encoded_results, path, processor_args, additional_metadata, is_manual_request) + logger.info("Preprocessing of %s completed.", task["preprocessor_args"]["path"]) + with self.r_lock: + # It is possible, but unlikely that this file was marked as print_after_processing, but + # a print started in the meanwhile + # this must be done within a lock since print_after_processing can be modified by another + # thread + if not self.print_after_processing: + task["print_after_processing"] = False + # Clear out info about the current job + self._current_file_processing_path = None + self.print_after_processing = None + self._success_callback( + encoded_results, task + ) else: - self._failed_callback(encoded_results["message"]) + self._failed_callback(encoded_results["message"], task) logger.info("Deleting temporary source.gcode file.") if os.path.isfile(self._source_file_path): @@ -174,14 +287,20 @@ def _process(self, path, processor_args, additional_metadata, is_manual_request) if os.path.isfile(self._target_file_path): os.unlink(self._target_file_path) - def _progress_received(self, progress): # the progress payload will all be in bytes (str for python 2) format. # Make sure everything is in unicode (str for python3) because mixed encoding # messes with things. encoded_progresss = utilities.dict_encode(progress) logger.verbose("Progress Received: %s", encoded_progresss) - return self._progress_callback(encoded_progresss) + progress_return = self._progress_callback(encoded_progresss) + + if self._is_printing_callback(): + self._processing_cancelled_while_printing = True + progress_return = False + + return progress_return + diff --git a/octoprint_arc_welder/static/css/arc_welder.css b/octoprint_arc_welder/static/css/arc_welder.css index a94fbc3..0ba68f0 100644 --- a/octoprint_arc_welder/static/css/arc_welder.css +++ b/octoprint_arc_welder/static/css/arc_welder.css @@ -58,3 +58,7 @@ div.ui-pnotify.arc-welder { text-overflow: ellipsis; overflow: hidden; } + +#tab_plugin_arc_welder_controls .no_underline:link { + text-decoration:none; +} \ No newline at end of file diff --git a/octoprint_arc_welder/static/docs/help/settings.print_after_processing.md b/octoprint_arc_welder/static/docs/help/settings.print_after_processing.md new file mode 100644 index 0000000..0343cfc --- /dev/null +++ b/octoprint_arc_welder/static/docs/help/settings.print_after_processing.md @@ -0,0 +1 @@ +When enabled, Arc Welder will automatically print the output file after processing is complete. This is only possible if you are not currently printing. \ No newline at end of file diff --git a/octoprint_arc_welder/static/docs/help/settings.select_after_processing.md b/octoprint_arc_welder/static/docs/help/settings.select_after_processing.md new file mode 100644 index 0000000..9f470f9 --- /dev/null +++ b/octoprint_arc_welder/static/docs/help/settings.select_after_processing.md @@ -0,0 +1 @@ +When enabled, Arc Welder will select the output file after processing is complete. This is only possible if you are not currently printing. \ No newline at end of file diff --git a/octoprint_arc_welder/static/js/arc_welder.js b/octoprint_arc_welder/static/js/arc_welder.js index 49413d0..9bef3e6 100644 --- a/octoprint_arc_welder/static/js/arc_welder.js +++ b/octoprint_arc_welder/static/js/arc_welder.js @@ -27,7 +27,89 @@ $(function () { // ArcWelder Global ArcWelder = {}; ArcWelder.PLUGIN_ID = "arc_welder"; + ArcWelder.toggleContentFunction = function ($elm, options, updateObservable) { + if (options.toggle_observable) { + //console.log("Toggling element."); + if (updateObservable) { + options.toggle_observable(!options.toggle_observable()); + //console.log("Observable updated - " + options.toggle_observable()) + } + if (options.toggle_observable()) { + if (options.class_showing) { + $elm.children('[class^="icon-"]').addClass(options.class_showing); + $elm.children('[class^="fa"]').addClass(options.class_showing); + } + if (options.class_hiding) { + $elm.children('[class^="icon-"]').removeClass(options.class_hiding); + $elm.children('[class^="fa"]').removeClass(options.class_hiding); + } + if (options.container) { + if (options.parent) { + $elm.parents(options.parent).find(options.container).stop().slideDown('fast', options.onComplete); + } else { + $(options.container).stop().slideDown('fast', options.onComplete); + } + } + } else { + if (options.class_hiding) { + $elm.children('[class^="icon-"]').addClass(options.class_hiding); + $elm.children('[class^="fa"]').addClass(options.class_hiding); + } + if (options.class_showing) { + $elm.children('[class^="icon-"]').removeClass(options.class_showing); + $elm.children('[class^="fa"]').removeClass(options.class_showing); + } + if (options.container) { + if (options.parent) { + $elm.parents(options.parent).find(options.container).stop().slideUp('fast', options.onComplete); + } else { + $(options.container).stop().slideUp('fast', options.onComplete); + } + } + } + } else { + if (options.class) { + $elm.children('[class^="icon-"]').toggleClass(options.class_hiding + ' ' + options.class_showing); + $elm.children('[class^="fa"]').toggleClass(options.class_hiding + ' ' + options.class_showing); + } + if (options.container) { + if (options.parent) { + $elm.parents(options.parent).find(options.container).stop().slideToggle('fast', options.onComplete); + } else { + $(options.container).stop().slideToggle('fast', options.onComplete); + } + } + } + + }; + ArcWelder.toggle = { + init: function (element, valueAccessor) { + var $elm = $(element), + options = $.extend({ + class_showing: null, + class_hiding: null, + container: null, + parent: null, + toggle_observable: null, + onComplete: function () { + $(document).trigger("slideCompleted"); + } + }, valueAccessor()); + + if (options.toggle_observable) { + ArcWelder.toggleContentFunction($elm, options, false); + } + + + $elm.on("click", function (e) { + e.preventDefault(); + ArcWelder.toggleContentFunction($elm, options, true); + + }); + } + }; + ko.bindingHandlers.arc_welder_toggle = ArcWelder.toggle; ArcWelder.setLocalStorage = function (name, value) { localStorage.setItem("arc_welder_" + name, value) }; @@ -152,6 +234,28 @@ $(function () { {name:"Disabled", value: ArcWelder.SOURCE_FILE_DELETE_DISABLED} ]; + ArcWelder.PRINT_AFTER_PROCESSING_BOTH = "both"; + ArcWelder.PRINT_AFTER_PROCESSING_AUTO = "auto-only"; + ArcWelder.PRINT_AFTER_PROCESSING_MANUAL = "manual-only"; + ArcWelder.PRINT_AFTER_PROCESSING_DISABLED = "disabled"; + ArcWelder.PRINT_AFTER_PROCESSING_OPTIONS = [ + {name:"Always Print After Processing", value: ArcWelder.PRINT_AFTER_PROCESSING_BOTH}, + {name:"Print After Automatic Processing", value: ArcWelder.PRINT_AFTER_PROCESSING_AUTO}, + {name:"Print After Manual Processing", value: ArcWelder.PRINT_AFTER_PROCESSING_MANUAL}, + {name:"Disabled", value: ArcWelder.PRINT_AFTER_PROCESSING_DISABLED} + ]; + + ArcWelder.SELECT_FILE_AFTER_PROCESSING_BOTH = "both"; + ArcWelder.SELECT_FILE_AFTER_PROCESSING_AUTO = "auto-only"; + ArcWelder.SELECT_FILE_AFTER_PROCESSING_MANUAL = "manual-only"; + ArcWelder.SELECT_FILE_AFTER_PROCESSING_DISABLED = "disabled"; + ArcWelder.SELECT_FILE_AFTER_PROCESSING_OPTIONS = [ + {name:"Always Select File After Processing", value: ArcWelder.SELECT_FILE_AFTER_PROCESSING_BOTH}, + {name:"Select File After Automatic Processing", value: ArcWelder.SELECT_FILE_AFTER_PROCESSING_AUTO}, + {name:"Select File After Manual Processing", value: ArcWelder.SELECT_FILE_AFTER_PROCESSING_MANUAL}, + {name:"Disabled", value: ArcWelder.SELECT_FILE_AFTER_PROCESSING_DISABLED} + ]; + ArcWelder.ArcWelderViewModel = function (parameters) { var self = this; // variable to hold the settings view model. @@ -181,7 +285,12 @@ $(function () { self.statistics.compression_percent = ko.observable().extend({arc_welder_numeric: 1}); self.statistics.source_filename = ko.observable(); self.statistics.target_filename = ko.observable(); - + var initial_run_configuration_visible = ArcWelder.getLocalStorage("run_configuration_visible") !== "false"; + self.run_configuration_visible = ko.observable(initial_run_configuration_visible); + self.run_configuration_visible.subscribe(function(newValue){ + var storage_value = newValue ? 'true' : 'false'; + ArcWelder.setLocalStorage("run_configuration_visible", storage_value); + }) self.statistics.segment_statistics_text = ko.observable(); self.current_files = null; @@ -216,19 +325,45 @@ $(function () { return file_processing_type === ArcWelder.FILE_PROCESSING_MANUAL || file_processing_type === ArcWelder.FILE_PROCESSING_BOTH; }); + // Auto Select + self.select_auto_processed_file = ko.pureComputed(function(){ + var auto_select_type = self.plugin_settings.feature_settings.select_after_processing(); + return auto_select_type === ArcWelder.SELECT_FILE_AFTER_PROCESSING_AUTO || + auto_select_type === ArcWelder.SELECT_FILE_AFTER_PROCESSING_BOTH; + }); + + self.select_manual_processed_file = ko.pureComputed(function(){ + var auto_select_type = self.plugin_settings.feature_settings.select_after_processing(); + return auto_select_type === ArcWelder.SELECT_FILE_AFTER_PROCESSING_MANUAL || + auto_select_type === ArcWelder.SELECT_FILE_AFTER_PROCESSING_BOTH; + }); + // Auto Print + self.print_auto_processed_file = ko.pureComputed(function(){ + var auto_select_type = self.plugin_settings.feature_settings.select_after_processing(); + return auto_select_type === ArcWelder.PRINT_AFTER_PROCESSING_AUTO || + auto_select_type === ArcWelder.PRINT_AFTER_PROCESSING_BOTH; + }); + + self.print_manual_processed_file = ko.pureComputed(function(){ + var auto_select_type = self.plugin_settings.feature_settings.select_after_processing(); + return auto_select_type === ArcWelder.PRINT_AFTER_PROCESSING_MANUAL || + auto_select_type === ArcWelder.PRINT_AFTER_PROCESSING_BOTH; + }); + + self.source_file_delete_description = ko.pureComputed(function(){ delete_setting = self.plugin_settings.feature_settings.delete_source(); switch(delete_setting) { case ArcWelder.SOURCE_FILE_DELETE_AUTO: - return "Only automatically processed source files will be deleted."; + return "After Automatic Processing Only"; case ArcWelder.SOURCE_FILE_DELETE_MANUAL: - return "Only manually processed source files will be deleted."; + return "After Manual Processing Only"; case ArcWelder.SOURCE_FILE_DELETE_BOTH: - return "The source file will be deleted."; + return "Always"; default: - return "The source file will not be deleted"; + return "Disabled"; } }); @@ -666,21 +801,22 @@ $(function () { if (is_printing) { title = "Cannot weld arcs during a print, this would impact performance."; + disable = true; } - else if (file.origin !== "local") + + if (file.origin !== "local") { disable = true; title = "Cannot weld arcs for files stored on your printer's SD card."; } - else { - - if (file.arc_welder) - { - is_welded = true; - title = "View Arc-Welder statistics for this file."; - } + if (file.arc_welder) + { + disable = false; + is_welded = true; + title = "View Arc-Welder statistics for this file."; } + // Create the button var $button = $('\ @@ -688,15 +824,16 @@ $(function () { \ \ '); - // Add an on click event if the button is not disabled - //if (!is_welded) - //{ - var data = {path: file.path, origin: file.origin}; + // Add an on click handler for the arc welder filemanager if it is not disabled + var data = {path: file.path, origin: file.origin}; + if (!disable) + { $button.click(data, function(e) { self.processButtonClicked(e); }); - //} + } + // Add the button to the file manager $(file_element).find("a.btn-mini").after($button); @@ -797,21 +934,23 @@ $(function () { } else { - var options = { - title: 'Arc Welder Error', - text: results.message, - type: 'error', - hide: false, - addclass: "arc-welder", - desktop: { - desktop: true - } - }; - PNotifyExtensions.displayPopupForKey( - options, - ArcWelder.PopupKey("process-error"), - ArcWelder.PopupKey(["process-error"]) - ); + if (results.message) { + var options = { + title: 'Arc Welder Error', + text: results.message, + type: 'error', + hide: false, + addclass: "arc-welder", + desktop: { + desktop: true + } + }; + PNotifyExtensions.displayPopupForKey( + options, + ArcWelder.PopupKey("process-error"), + ArcWelder.PopupKey(["process-error"]) + ); + } } }, error: function (XMLHttpRequest, textStatus, errorThrown) { diff --git a/octoprint_arc_welder/templates/arc_welder_settings.jinja2 b/octoprint_arc_welder/templates/arc_welder_settings.jinja2 index 657ce0d..1688f94 100644 --- a/octoprint_arc_welder/templates/arc_welder_settings.jinja2 +++ b/octoprint_arc_welder/templates/arc_welder_settings.jinja2 @@ -85,8 +85,9 @@
- +
+ +
+
+
+ +
+ + +
+
Source File Options diff --git a/octoprint_arc_welder/templates/arc_welder_tab.jinja2 b/octoprint_arc_welder/templates/arc_welder_tab.jinja2 index cd43058..ad8ad5b 100644 --- a/octoprint_arc_welder/templates/arc_welder_tab.jinja2 +++ b/octoprint_arc_welder/templates/arc_welder_tab.jinja2 @@ -24,8 +24,13 @@ # following email address: FormerLurker@pm.me ##################################################################################-->
-

Current Run Configuration

-
+

+ + Current Run Configuration + +

+
+
@@ -45,6 +50,17 @@ Automatic and Manual Processing Enabled
+
+
+ G90/G91 Influences Extruder: +
+
+ (using octoprint settings) + +
+
Resolution: @@ -63,7 +79,7 @@
- Output File: + Output File Name:
Gcode files will be overwritten after processing. @@ -88,18 +104,7 @@
- G90 Influences Extruder: -
-
- (using octoprint settings) - -
-
-
-
- Source File Deletion: + Delete Output File:
@@ -111,6 +116,28 @@
+
+
+ Select After Processing: +
+
+ After Automatic Processing Only + After Manual Processing Only + Always + Disabled +
+
+
+
+ Print After Processing: +
+
+ After Automatic Processing Only + After Manual Processing Only + Always + Disabled +
+
@@ -121,6 +148,7 @@
+