From 8bc9663294c7253f0bea512e16011b573dc47688 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 12 Dec 2017 10:28:55 +0100 Subject: [PATCH] Removed old & unused code CL-541 --- .../NetworkClusterPrinterOutputDevice.py | 716 --------- .../NetworkPrinterOutputDevice.py | 1306 ----------------- .../NetworkPrinterOutputDevicePlugin.py | 357 ----- plugins/UM3NetworkPrinting/__init__.py | 4 +- 4 files changed, 2 insertions(+), 2381 deletions(-) delete mode 100644 plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py delete mode 100755 plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py delete mode 100644 plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py diff --git a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py deleted file mode 100644 index 853ef72f72..0000000000 --- a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py +++ /dev/null @@ -1,716 +0,0 @@ -import datetime -import getpass -import gzip -import json -import os -import os.path -import time - -from enum import Enum -from PyQt5.QtNetwork import QNetworkRequest, QHttpPart, QHttpMultiPart -from PyQt5.QtCore import QUrl, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject -from PyQt5.QtGui import QDesktopServices -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply -from UM.Application import Application -from UM.Logger import Logger -from UM.Message import Message -from UM.OutputDevice import OutputDeviceError -from UM.i18n import i18nCatalog -from UM.Qt.Duration import Duration, DurationFormat -from UM.PluginRegistry import PluginRegistry - -from . import NetworkPrinterOutputDevice - - -i18n_catalog = i18nCatalog("cura") - - -class OutputStage(Enum): - ready = 0 - uploading = 2 - - -class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinterOutputDevice): - printJobsChanged = pyqtSignal() - printersChanged = pyqtSignal() - selectedPrinterChanged = pyqtSignal() - - def __init__(self, key, address, properties, api_prefix): - super().__init__(key, address, properties, api_prefix) - # Store the address of the master. - self._master_address = address - name_property = properties.get(b"name", b"") - if name_property: - name = name_property.decode("utf-8") - else: - name = key - - self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated # The printer is always authenticated - - self.setName(name) - description = i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network") - self.setShortDescription(description) - self.setDescription(description) - - self._stage = OutputStage.ready - host_override = os.environ.get("CLUSTER_OVERRIDE_HOST", "") - if host_override: - Logger.log( - "w", - "Environment variable CLUSTER_OVERRIDE_HOST is set to [%s], cluster hosts are now set to this host", - host_override) - self._host = "http://" + host_override - else: - self._host = "http://" + address - - # is the same as in NetworkPrinterOutputDevicePlugin - self._cluster_api_version = "1" - self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/" - self._api_base_uri = self._host + self._cluster_api_prefix - - self._file_name = None - self._progress_message = None - self._request = None - self._reply = None - - # The main reason to keep the 'multipart' form data on the object - # is to prevent the Python GC from claiming it too early. - self._multipart = None - - self._print_view = None - self._request_job = [] - - self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml") - self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml") - - self._print_jobs = [] - self._print_job_by_printer_uuid = {} - self._print_job_by_uuid = {} # Print jobs by their own uuid - self._printers = [] - self._printers_dict = {} # by unique_name - - self._connected_printers_type_count = [] - self._automatic_printer = {"unique_name": "", "friendly_name": "Automatic"} # empty unique_name IS automatic selection - self._selected_printer = self._automatic_printer - - self._cluster_status_update_timer = QTimer() - self._cluster_status_update_timer.setInterval(5000) - self._cluster_status_update_timer.setSingleShot(False) - self._cluster_status_update_timer.timeout.connect(self._requestClusterStatus) - - self._can_pause = True - self._can_abort = True - self._can_pre_heat_bed = False - self._can_control_manually = False - self._cluster_size = int(properties.get(b"cluster_size", 0)) - - self._cleanupRequest() - - #These are texts that are to be translated for future features. - temporary_translation = i18n_catalog.i18n("This printer is not set up to host a group of connected Ultimaker 3 printers.") - temporary_translation2 = i18n_catalog.i18nc("Count is number of printers.", "This printer is the host for a group of {count} connected Ultimaker 3 printers.").format(count = 3) - temporary_translation3 = i18n_catalog.i18n("{printer_name} has finished printing '{job_name}'. Please collect the print and confirm clearing the build plate.") #When finished. - temporary_translation4 = i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.") #When configuration changed. - - ## No authentication, so requestAuthentication should do exactly nothing - @pyqtSlot() - def requestAuthentication(self, message_id = None, action_id = "Retry"): - pass # Cura Connect doesn't do any authorization - - def setAuthenticationState(self, auth_state): - self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated # The printer is always authenticated - - def _verifyAuthentication(self): - pass - - def _checkAuthentication(self): - Logger.log("d", "_checkAuthentication Cura Connect - nothing to be done") - - @pyqtProperty(QObject, notify=selectedPrinterChanged) - def controlItem(self): - # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time. - if not self._control_item: - self._createControlViewFromQML() - name = self._selected_printer.get("friendly_name") - if name == self._automatic_printer.get("friendly_name") or name == "": - return self._control_item - # Let cura use the default. - return None - - @pyqtSlot(int, result = str) - def getTimeCompleted(self, time_remaining): - current_time = time.time() - datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining) - return "{hour:02d}:{minute:02d}".format(hour = datetime_completed.hour, minute = datetime_completed.minute) - - @pyqtSlot(int, result = str) - def getDateCompleted(self, time_remaining): - current_time = time.time() - datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining) - return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper() - - @pyqtProperty(int, constant = True) - def clusterSize(self): - return self._cluster_size - - @pyqtProperty(str, notify=selectedPrinterChanged) - def name(self): - # Show the name of the selected printer. - # This is not the nicest way to do this, but changes to the Cura UI are required otherwise. - name = self._selected_printer.get("friendly_name") - if name != self._automatic_printer.get("friendly_name"): - return name - # Return name of cluster master. - return self._properties.get(b"name", b"").decode("utf-8") - - def connect(self): - super().connect() - self._cluster_status_update_timer.start() - - def close(self): - super().close() - self._cluster_status_update_timer.stop() - - def _setJobState(self, job_state): - if not self._selected_printer: - return - - selected_printer_uuid = self._printers_dict[self._selected_printer["unique_name"]]["uuid"] - if selected_printer_uuid not in self._print_job_by_printer_uuid: - return - - print_job_uuid = self._print_job_by_printer_uuid[selected_printer_uuid]["uuid"] - - url = QUrl(self._api_base_uri + "print_jobs/" + print_job_uuid + "/action") - put_request = QNetworkRequest(url) - put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - data = '{"action": "' + job_state + '"}' - self._manager.put(put_request, data.encode()) - - def _requestClusterStatus(self): - # TODO: Handle timeout. We probably want to know if the cluster is still reachable or not. - url = QUrl(self._api_base_uri + "printers/") - printers_request = QNetworkRequest(url) - self._addUserAgentHeader(printers_request) - self._manager.get(printers_request) - # See _finishedPrintersRequest() - - if self._printers: # if printers is not empty - url = QUrl(self._api_base_uri + "print_jobs/") - print_jobs_request = QNetworkRequest(url) - self._addUserAgentHeader(print_jobs_request) - self._manager.get(print_jobs_request) - # See _finishedPrintJobsRequest() - - def _finishedPrintJobsRequest(self, reply): - try: - json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received an invalid print job state message: Not valid JSON.") - return - self.setPrintJobs(json_data) - - def _finishedPrintersRequest(self, reply): - try: - json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received an invalid print job state message: Not valid JSON.") - return - self.setPrinters(json_data) - - def materialHotendChangedMessage(self, callback): - # When there is just one printer, the activate configuration option is enabled - if (self._cluster_size == 1): - super().materialHotendChangedMessage(callback = callback) - - def _startCameraStream(self): - ## Request new image - url = QUrl("http://" + self._printers_dict[self._selected_printer["unique_name"]]["ip_address"] + ":8080/?action=stream") - self._image_request = QNetworkRequest(url) - self._addUserAgentHeader(self._image_request) - self._image_reply = self._manager.get(self._image_request) - self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress) - - def spawnPrintView(self): - if self._print_view is None: - path = os.path.join(self._plugin_path, "PrintWindow.qml") - self._print_view = Application.getInstance().createQmlComponent(path, {"OutputDevice", self}) - if self._print_view is not None: - self._print_view.show() - - ## Store job info, show Print view for settings - def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): - self._selected_printer = self._automatic_printer # reset to default option - self._request_job = [nodes, file_name, filter_by_machine, file_handler, kwargs] - - if self._stage != OutputStage.ready: - if self._error_message: - self._error_message.hide() - self._error_message = Message( - i18n_catalog.i18nc("@info:status", - "Sending new jobs (temporarily) blocked, still sending the previous print job.")) - self._error_message.show() - return - - self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer - - if len(self._printers) > 1: - self.spawnPrintView() # Ask user how to print it. - elif len(self._printers) == 1: - # If there is only one printer, don't bother asking. - self.selectAutomaticPrinter() - self.sendPrintJob() - else: - # Cluster has no printers, warn the user of this. - if self._error_message: - self._error_message.hide() - self._error_message = Message( - i18n_catalog.i18nc("@info:status", - "Unable to send new print job: this 3D printer is not (yet) set up to host a group of connected Ultimaker 3 printers.")) - self._error_message.show() - - ## Actually send the print job, called from the dialog - # :param: require_printer_name: name of printer, or "" - @pyqtSlot() - def sendPrintJob(self): - nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job - require_printer_name = self._selected_printer["unique_name"] - - self._send_gcode_start = time.time() - Logger.log("d", "Sending print job [%s] to host..." % file_name) - - if self._stage != OutputStage.ready: - Logger.log("d", "Unable to send print job as the state is %s", self._stage) - raise OutputDeviceError.DeviceBusyError() - self._stage = OutputStage.uploading - - self._file_name = "%s.gcode.gz" % file_name - self._showProgressMessage() - - new_request = self._buildSendPrintJobHttpRequest(require_printer_name) - if new_request is None or self._stage != OutputStage.uploading: - return - self._request = new_request - self._reply = self._manager.post(self._request, self._multipart) - self._reply.uploadProgress.connect(self._onUploadProgress) - # See _finishedPostPrintJobRequest() - - def _buildSendPrintJobHttpRequest(self, require_printer_name): - api_url = QUrl(self._api_base_uri + "print_jobs/") - request = QNetworkRequest(api_url) - # Create multipart request and add the g-code. - self._multipart = QHttpMultiPart(QHttpMultiPart.FormDataType) - - # Add gcode - part = QHttpPart() - part.setHeader(QNetworkRequest.ContentDispositionHeader, - 'form-data; name="file"; filename="%s"' % self._file_name) - - gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") - compressed_gcode = self._compressGcode(gcode) - if compressed_gcode is None: - return None # User aborted print, so stop trying. - - part.setBody(compressed_gcode) - self._multipart.append(part) - - # require_printer_name "" means automatic - if require_printer_name: - self._multipart.append(self.__createKeyValueHttpPart("require_printer_name", require_printer_name)) - user_name = self.__get_username() - if user_name is None: - user_name = "unknown" - self._multipart.append(self.__createKeyValueHttpPart("owner", user_name)) - - self._addUserAgentHeader(request) - return request - - def _compressGcode(self, gcode): - self._compressing_print = True - batched_line = "" - max_chars_per_line = int(1024 * 1024 / 4) # 1 / 4 MB - - byte_array_file_data = b"" - - def _compressDataAndNotifyQt(data_to_append): - compressed_data = gzip.compress(data_to_append.encode("utf-8")) - self._progress_message.setProgress(-1) # Tickle the message so that it's clear that it's still being used. - QCoreApplication.processEvents() # Ensure that the GUI does not freeze. - # Pretend that this is a response, as zipping might take a bit of time. - self._last_response_time = time.time() - return compressed_data - - if gcode is None: - Logger.log("e", "Unable to find sliced gcode, returning empty.") - return byte_array_file_data - - for line in gcode: - if not self._compressing_print: - self._progress_message.hide() - return None # Stop trying to zip, abort was called. - batched_line += line - # if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file. - # Compressing line by line in this case is extremely slow, so we need to batch them. - if len(batched_line) < max_chars_per_line: - continue - byte_array_file_data += _compressDataAndNotifyQt(batched_line) - batched_line = "" - - # Also compress the leftovers. - if batched_line: - byte_array_file_data += _compressDataAndNotifyQt(batched_line) - - return byte_array_file_data - - def __createKeyValueHttpPart(self, key, value): - metadata_part = QHttpPart() - metadata_part.setHeader(QNetworkRequest.ContentTypeHeader, 'text/plain') - metadata_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="%s"' % (key)) - metadata_part.setBody(bytearray(value, "utf8")) - return metadata_part - - def __get_username(self): - try: - return getpass.getuser() - except: - Logger.log("d", "Could not get the system user name, returning 'unknown' instead.") - return None - - def _finishedPrintJobPostRequest(self, reply): - self._stage = OutputStage.ready - if self._progress_message: - self._progress_message.hide() - self._progress_message = None - self.writeFinished.emit(self) - - if reply.error(): - self._showRequestFailedMessage(reply) - self.writeError.emit(self) - else: - self._showRequestSucceededMessage() - self.writeSuccess.emit(self) - - self._cleanupRequest() - - def _showRequestFailedMessage(self, reply): - if reply is not None: - Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format( - cluster_name = self.getName(), - error_string = str(reply.errorString()), - error = str(reply.error()))) - error_message_template = i18n_catalog.i18nc("@info:status", "Unable to send print job to group {cluster_name}.") - message = Message(text=error_message_template.format( - cluster_name = self.getName())) - message.show() - - def _showRequestSucceededMessage(self): - confirmation_message_template = i18n_catalog.i18nc( - "@info:status", - "Sent {file_name} to group {cluster_name}." - ) - file_name = os.path.basename(self._file_name).split(".")[0] - message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name) - message = Message(text=message_text) - button_text = i18n_catalog.i18nc("@action:button", "Show print jobs") - button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.") - message.addAction("open_browser", button_text, "globe", button_tooltip) - message.actionTriggered.connect(self._onMessageActionTriggered) - message.show() - - def setPrintJobs(self, print_jobs): - #TODO: hack, last seen messes up the check, so drop it. - for job in print_jobs: - del job["last_seen"] - # Strip any extensions - job["name"] = self._removeGcodeExtension(job["name"]) - - if self._print_jobs != print_jobs: - old_print_jobs = self._print_jobs - self._print_jobs = print_jobs - - self._notifyFinishedPrintJobs(old_print_jobs, print_jobs) - self._notifyConfigurationChangeRequired(old_print_jobs, print_jobs) - - # Yes, this is a hacky way of doing it, but it's quick and the API doesn't give the print job per printer - # for some reason. ugh. - self._print_job_by_printer_uuid = {} - self._print_job_by_uuid = {} - for print_job in print_jobs: - if "printer_uuid" in print_job and print_job["printer_uuid"] is not None: - self._print_job_by_printer_uuid[print_job["printer_uuid"]] = print_job - self._print_job_by_uuid[print_job["uuid"]] = print_job - self.printJobsChanged.emit() - - def _removeGcodeExtension(self, name): - parts = name.split(".") - if parts[-1].upper() == "GZ": - parts = parts[:-1] - if parts[-1].upper() == "GCODE": - parts = parts[:-1] - return ".".join(parts) - - def _notifyFinishedPrintJobs(self, old_print_jobs, new_print_jobs): - """Notify the user when any of their print jobs have just completed. - - Arguments: - - old_print_jobs -- the previous list of print job status information as returned by the cluster REST API. - new_print_jobs -- the current list of print job status information as returned by the cluster REST API. - """ - if old_print_jobs is None: - return - - username = self.__get_username() - if username is None: - return - - our_old_print_jobs = self.__filterOurPrintJobs(old_print_jobs) - our_old_not_finished_print_jobs = [pj for pj in our_old_print_jobs if pj["status"] != "wait_cleanup"] - - our_new_print_jobs = self.__filterOurPrintJobs(new_print_jobs) - our_new_finished_print_jobs = [pj for pj in our_new_print_jobs if pj["status"] == "wait_cleanup"] - - old_not_finished_print_job_uuids = set([pj["uuid"] for pj in our_old_not_finished_print_jobs]) - - for print_job in our_new_finished_print_jobs: - if print_job["uuid"] in old_not_finished_print_job_uuids: - - printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"]) - if printer_name is None: - printer_name = i18n_catalog.i18nc("@label Printer name", "Unknown") - - message_text = (i18n_catalog.i18nc("@info:status", - "Printer '{printer_name}' has finished printing '{job_name}'.") - .format(printer_name=printer_name, job_name=print_job["name"])) - message = Message(text=message_text, title=i18n_catalog.i18nc("@info:status", "Print finished")) - Application.getInstance().showMessage(message) - Application.getInstance().showToastMessage( - i18n_catalog.i18nc("@info:status", "Print finished"), - message_text) - - def __filterOurPrintJobs(self, print_jobs): - username = self.__get_username() - return [print_job for print_job in print_jobs if print_job["owner"] == username] - - def _notifyConfigurationChangeRequired(self, old_print_jobs, new_print_jobs): - if old_print_jobs is None: - return - - old_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(old_print_jobs)) - new_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(new_print_jobs)) - old_change_required_print_job_uuids = set([pj["uuid"] for pj in old_change_required_print_jobs]) - - for print_job in new_change_required_print_jobs: - if print_job["uuid"] not in old_change_required_print_job_uuids: - - printer_name = self.__getPrinterNameFromUuid(print_job["assigned_to"]) - if printer_name is None: - # don't report on yet unknown printers - continue - - message_text = (i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.") - .format(printer_name=printer_name, job_name=print_job["name"])) - message = Message(text=message_text, title=i18n_catalog.i18nc("@label:status", "Action required")) - Application.getInstance().showMessage(message) - Application.getInstance().showToastMessage( - i18n_catalog.i18nc("@label:status", "Action required"), - message_text) - - def __filterConfigChangePrintJobs(self, print_jobs): - return filter(self.__isConfigurationChangeRequiredPrintJob, print_jobs) - - def __isConfigurationChangeRequiredPrintJob(self, print_job): - if print_job["status"] == "queued": - changes_required = print_job.get("configuration_changes_required", []) - return len(changes_required) != 0 - return False - - def __getPrinterNameFromUuid(self, printer_uuid): - for printer in self._printers: - if printer["uuid"] == printer_uuid: - return printer["friendly_name"] - return None - - def setPrinters(self, printers): - if self._printers != printers: - self._connected_printers_type_count = [] - printers_count = {} - self._printers = printers - self._printers_dict = dict((p["unique_name"], p) for p in printers) # for easy lookup by unique_name - - for printer in printers: - variant = printer["machine_variant"] - if variant in printers_count: - printers_count[variant] += 1 - else: - printers_count[variant] = 1 - for type in printers_count: - self._connected_printers_type_count.append({"machine_type": type, "count": printers_count[type]}) - self.printersChanged.emit() - - @pyqtProperty("QVariantList", notify=printersChanged) - def connectedPrintersTypeCount(self): - return self._connected_printers_type_count - - @pyqtProperty("QVariantList", notify=printersChanged) - def connectedPrinters(self): - return self._printers - - @pyqtProperty(int, notify=printJobsChanged) - def numJobsPrinting(self): - num_jobs_printing = 0 - for job in self._print_jobs: - if job["status"] in ["printing", "wait_cleanup", "sent_to_printer", "pre_print", "post_print"]: - num_jobs_printing += 1 - return num_jobs_printing - - @pyqtProperty(int, notify=printJobsChanged) - def numJobsQueued(self): - num_jobs_queued = 0 - for job in self._print_jobs: - if job["status"] == "queued": - num_jobs_queued += 1 - return num_jobs_queued - - @pyqtProperty("QVariantMap", notify=printJobsChanged) - def printJobsByUUID(self): - return self._print_job_by_uuid - - @pyqtProperty("QVariantMap", notify=printJobsChanged) - def printJobsByPrinterUUID(self): - return self._print_job_by_printer_uuid - - @pyqtProperty("QVariantList", notify=printJobsChanged) - def printJobs(self): - return self._print_jobs - - @pyqtProperty("QVariantList", notify=printersChanged) - def printers(self): - return [self._automatic_printer, ] + self._printers - - @pyqtSlot(str, str) - def selectPrinter(self, unique_name, friendly_name): - self.stopCamera() - self._selected_printer = {"unique_name": unique_name, "friendly_name": friendly_name} - Logger.log("d", "Selected printer: %s %s", friendly_name, unique_name) - # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time. - if unique_name == "": - self._address = self._master_address - else: - self._address = self._printers_dict[self._selected_printer["unique_name"]]["ip_address"] - - self.selectedPrinterChanged.emit() - - def _updateJobState(self, job_state): - name = self._selected_printer.get("friendly_name") - if name == "" or name == "Automatic": - # TODO: This is now a bit hacked; If no printer is selected, don't show job state. - if self._job_state != "": - self._job_state = "" - self.jobStateChanged.emit() - else: - if self._job_state != job_state: - self._job_state = job_state - self.jobStateChanged.emit() - - @pyqtSlot() - def selectAutomaticPrinter(self): - self.stopCamera() - self._selected_printer = self._automatic_printer - self.selectedPrinterChanged.emit() - - @pyqtProperty("QVariant", notify=selectedPrinterChanged) - def selectedPrinterName(self): - return self._selected_printer.get("unique_name", "") - - def getPrintJobsUrl(self): - return self._host + "/print_jobs" - - def getPrintersUrl(self): - return self._host + "/printers" - - def _showProgressMessage(self): - progress_message_template = i18n_catalog.i18nc("@info:progress", - "Sending {file_name} to group {cluster_name}") - file_name = os.path.basename(self._file_name).split(".")[0] - self._progress_message = Message(progress_message_template.format(file_name = file_name, cluster_name = self.getName()), 0, False, -1) - self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") - self._progress_message.actionTriggered.connect(self._onMessageActionTriggered) - self._progress_message.show() - - def _addUserAgentHeader(self, request): - request.setRawHeader(b"User-agent", b"CuraPrintClusterOutputDevice Plugin") - - def _cleanupRequest(self): - self._request = None - self._stage = OutputStage.ready - self._file_name = None - - def _onFinished(self, reply): - super()._onFinished(reply) - reply_url = reply.url().toString() - status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - if status_code == 500: - Logger.log("w", "Request to {url} returned a 500.".format(url = reply_url)) - return - if reply.error() == QNetworkReply.ContentOperationNotPermittedError: - # It was probably "/api/v1/materials" for legacy UM3 - return - if reply.error() == QNetworkReply.ContentNotFoundError: - # It was probably "/api/v1/print_job" for legacy UM3 - return - - if reply.operation() == QNetworkAccessManager.PostOperation: - if self._cluster_api_prefix + "print_jobs" in reply_url: - self._finishedPrintJobPostRequest(reply) - return - - # We need to do this check *after* we process the post operation! - # If the sending of g-code is cancelled by the user it will result in an error, but we do need to handle this. - if reply.error() != QNetworkReply.NoError: - Logger.log("e", "After requesting [%s] we got a network error [%s]. Not processing anything...", reply_url, reply.error()) - return - - elif reply.operation() == QNetworkAccessManager.GetOperation: - if self._cluster_api_prefix + "print_jobs" in reply_url: - self._finishedPrintJobsRequest(reply) - elif self._cluster_api_prefix + "printers" in reply_url: - self._finishedPrintersRequest(reply) - - @pyqtSlot() - def openPrintJobControlPanel(self): - Logger.log("d", "Opening print job control panel...") - QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl())) - - @pyqtSlot() - def openPrinterControlPanel(self): - Logger.log("d", "Opening printer control panel...") - QDesktopServices.openUrl(QUrl(self.getPrintersUrl())) - - def _onMessageActionTriggered(self, message, action): - if action == "open_browser": - QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl())) - - if action == "Abort": - Logger.log("d", "User aborted sending print to remote.") - self._progress_message.hide() - self._compressing_print = False - if self._reply: - self._reply.abort() - self._stage = OutputStage.ready - Application.getInstance().getController().setActiveStage("PrepareStage") - - @pyqtSlot(int, result=str) - def formatDuration(self, seconds): - return Duration(seconds).getDisplayString(DurationFormat.Format.Short) - - ## For cluster below - def _get_plugin_directory_name(self): - current_file_absolute_path = os.path.realpath(__file__) - directory_path = os.path.dirname(current_file_absolute_path) - _, directory_name = os.path.split(directory_path) - return directory_name - - @property - def _plugin_path(self): - return PluginRegistry.getInstance().getPluginPath(self._get_plugin_directory_name()) diff --git a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py deleted file mode 100755 index 3a48bab11b..0000000000 --- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py +++ /dev/null @@ -1,1306 +0,0 @@ -# Copyright (c) 2017 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from UM.i18n import i18nCatalog -from UM.Application import Application -from UM.Logger import Logger -from UM.Signal import signalemitter - -from UM.Message import Message - -import UM.Settings.ContainerRegistry -import UM.Version #To compare firmware version numbers. - -from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState -from cura.Settings.ContainerManager import ContainerManager -import cura.Settings.ExtruderManager - -from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply -from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot, QCoreApplication -from PyQt5.QtGui import QImage, QColor -from PyQt5.QtWidgets import QMessageBox - -import json -import os -import gzip - -from time import time - -from time import gmtime -from enum import IntEnum - -i18n_catalog = i18nCatalog("cura") - -class AuthState(IntEnum): - NotAuthenticated = 1 - AuthenticationRequested = 2 - Authenticated = 3 - AuthenticationDenied = 4 - -## Network connected (wifi / lan) printer that uses the Ultimaker API -@signalemitter -class NetworkPrinterOutputDevice(PrinterOutputDevice): - def __init__(self, key, address, properties, api_prefix): - super().__init__(key) - self._address = address - self._key = key - self._properties = properties # Properties dict as provided by zero conf - self._api_prefix = api_prefix - - self._gcode = None - self._print_finished = True # _print_finished == False means we're halfway in a print - self._write_finished = True # _write_finished == False means we're currently sending a G-code file - - self._use_gzip = True # Should we use g-zip compression before sending the data? - - # This holds the full JSON file that was received from the last request. - # The JSON looks like: - #{ - # "led": {"saturation": 0.0, "brightness": 100.0, "hue": 0.0}, - # "beep": {}, - # "network": { - # "wifi_networks": [], - # "ethernet": {"connected": true, "enabled": true}, - # "wifi": {"ssid": "xxxx", "connected": False, "enabled": False} - # }, - # "diagnostics": {}, - # "bed": {"temperature": {"target": 60.0, "current": 44.4}}, - # "heads": [{ - # "max_speed": {"z": 40.0, "y": 300.0, "x": 300.0}, - # "position": {"z": 20.0, "y": 6.0, "x": 180.0}, - # "fan": 0.0, - # "jerk": {"z": 0.4, "y": 20.0, "x": 20.0}, - # "extruders": [ - # { - # "feeder": {"max_speed": 45.0, "jerk": 5.0, "acceleration": 3000.0}, - # "active_material": {"guid": "xxxxxxx", "length_remaining": -1.0}, - # "hotend": {"temperature": {"target": 0.0, "current": 22.8}, "id": "AA 0.4"} - # }, - # { - # "feeder": {"max_speed": 45.0, "jerk": 5.0, "acceleration": 3000.0}, - # "active_material": {"guid": "xxxx", "length_remaining": -1.0}, - # "hotend": {"temperature": {"target": 0.0, "current": 22.8}, "id": "BB 0.4"} - # } - # ], - # "acceleration": 3000.0 - # }], - # "status": "printing" - #} - - self._json_printer_state = {} - - ## Todo: Hardcoded value now; we should probably read this from the machine file. - ## It's okay to leave this for now, as this plugin is um3 only (and has 2 extruders by definition) - self._num_extruders = 2 - - # These are reinitialised here (from PrinterOutputDevice) to match the new _num_extruders - self._hotend_temperatures = [0] * self._num_extruders - self._target_hotend_temperatures = [0] * self._num_extruders - - self._material_ids = [""] * self._num_extruders - self._hotend_ids = [""] * self._num_extruders - self._target_bed_temperature = 0 - self._processing_preheat_requests = True - - self._can_control_manually = False - - self.setPriority(3) # Make sure the output device gets selected above local file output - self.setName(key) - self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) - self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) - self.setIconName("print") - - self._manager = None - - self._post_request = None - self._post_reply = None - self._post_multi_part = None - self._post_part = None - - self._material_multi_part = None - self._material_part = None - - self._progress_message = None - self._error_message = None - self._connection_message = None - - self._update_timer = QTimer() - self._update_timer.setInterval(2000) # TODO; Add preference for update interval - self._update_timer.setSingleShot(False) - self._update_timer.timeout.connect(self._update) - - self._camera_timer = QTimer() - self._camera_timer.setInterval(500) # Todo: Add preference for camera update interval - self._camera_timer.setSingleShot(False) - self._camera_timer.timeout.connect(self._updateCamera) - - self._image_request = None - self._image_reply = None - - self._use_stream = True - self._stream_buffer = b"" - self._stream_buffer_start_index = -1 - - self._camera_image_id = 0 - - self._authentication_counter = 0 - self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min) - - self._authentication_timer = QTimer() - self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval - self._authentication_timer.setSingleShot(False) - self._authentication_timer.timeout.connect(self._onAuthenticationTimer) - self._authentication_request_active = False - - self._authentication_state = AuthState.NotAuthenticated - self._authentication_id = None - self._authentication_key = None - - self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Access to the printer requested. Please approve the request on the printer"), lifetime = 0, dismissable = False, progress = 0, title = i18n_catalog.i18nc("@info:title", "Connection status")) - self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""), title = i18n_catalog.i18nc("@info:title", "Connection Status")) - self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) - self._authentication_failed_message.actionTriggered.connect(self.requestAuthentication) - self._authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Access to the printer accepted"), title = i18n_catalog.i18nc("@info:title", "Connection Status")) - self._not_authenticated_message = Message(i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."), title = i18n_catalog.i18nc("@info:title", "Connection Status")) - self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"), None, i18n_catalog.i18nc("@info:tooltip", "Send access request to the printer")) - self._not_authenticated_message.actionTriggered.connect(self.requestAuthentication) - - self._camera_image = QImage() - - self._material_post_objects = {} - self._connection_state_before_timeout = None - - self._last_response_time = time() - self._last_request_time = None - self._response_timeout_time = 10 - self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. - self._recreate_network_manager_count = 1 - - self._send_gcode_start = time() # Time when the sending of the g-code started. - - self._last_command = "" - - self._compressing_print = False - self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") - printer_type = self._properties.get(b"machine", b"").decode("utf-8") - if printer_type.startswith("9511"): - self._updatePrinterType("ultimaker3_extended") - elif printer_type.startswith("9066"): - self._updatePrinterType("ultimaker3") - else: - self._updatePrinterType("unknown") - - Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) - - def _onNetworkAccesibleChanged(self, accessible): - Logger.log("d", "Network accessible state changed to: %s", accessible) - - ## Triggered when the output device manager changes devices. - # - # This is how we can detect that our device is no longer active now. - def _onOutputDevicesChanged(self): - if self.getId() not in Application.getInstance().getOutputDeviceManager().getOutputDeviceIds(): - self.stopCamera() - - def _onAuthenticationTimer(self): - self._authentication_counter += 1 - self._authentication_requested_message.setProgress(self._authentication_counter / self._max_authentication_counter * 100) - if self._authentication_counter > self._max_authentication_counter: - self._authentication_timer.stop() - Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._key) - self.setAuthenticationState(AuthState.AuthenticationDenied) - - def _onAuthenticationRequired(self, reply, authenticator): - if self._authentication_id is not None and self._authentication_key is not None: - Logger.log("d", "Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s", self._key, self._authentication_id, self._getSafeAuthKey()) - authenticator.setUser(self._authentication_id) - authenticator.setPassword(self._authentication_key) - else: - Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._key) - - def getProperties(self): - return self._properties - - @pyqtSlot(str, result = str) - def getProperty(self, key): - key = key.encode("utf-8") - if key in self._properties: - return self._properties.get(key, b"").decode("utf-8") - else: - return "" - - ## Get the unique key of this machine - # \return key String containing the key of the machine. - @pyqtSlot(result = str) - def getKey(self): - return self._key - - ## The IP address of the printer. - @pyqtProperty(str, constant = True) - def address(self): - return self._properties.get(b"address", b"").decode("utf-8") - - ## Name of the printer (as returned from the ZeroConf properties) - @pyqtProperty(str, constant = True) - def name(self): - return self._properties.get(b"name", b"").decode("utf-8") - - ## Firmware version (as returned from the ZeroConf properties) - @pyqtProperty(str, constant=True) - def firmwareVersion(self): - return self._properties.get(b"firmware_version", b"").decode("utf-8") - - ## IPadress of this printer - @pyqtProperty(str, constant=True) - def ipAddress(self): - return self._address - - ## Pre-heats the heated bed of the printer. - # - # \param temperature The temperature to heat the bed to, in degrees - # Celsius. - # \param duration How long the bed should stay warm, in seconds. - @pyqtSlot(float, float) - def preheatBed(self, temperature, duration): - temperature = round(temperature) #The API doesn't allow floating point. - duration = round(duration) - if UM.Version.Version(self.firmwareVersion) < UM.Version.Version("3.5.92"): #Real bed pre-heating support is implemented from 3.5.92 and up. - self.setTargetBedTemperature(temperature = temperature) #No firmware-side duration support then. - return - url = QUrl("http://" + self._address + self._api_prefix + "printer/bed/pre_heat") - if duration > 0: - data = """{"temperature": "%i", "timeout": "%i"}""" % (temperature, duration) - else: - data = """{"temperature": "%i"}""" % temperature - Logger.log("i", "Pre-heating bed to %i degrees.", temperature) - put_request = QNetworkRequest(url) - put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self._processing_preheat_requests = False - self._manager.put(put_request, data.encode()) - self._preheat_bed_timer.start(self._preheat_bed_timeout * 1000) #Times 1000 because it needs to be provided as milliseconds. - self.preheatBedRemainingTimeChanged.emit() - - ## Cancels pre-heating the heated bed of the printer. - # - # If the bed is not pre-heated, nothing happens. - @pyqtSlot() - def cancelPreheatBed(self): - Logger.log("i", "Cancelling pre-heating of the bed.") - self.preheatBed(temperature = 0, duration = 0) - self._preheat_bed_timer.stop() - self._preheat_bed_timer.setInterval(0) - self.preheatBedRemainingTimeChanged.emit() - - ## Changes the target bed temperature on the printer. - # - # /param temperature The new target temperature of the bed. - def _setTargetBedTemperature(self, temperature): - if not self._updateTargetBedTemperature(temperature): - return - - url = QUrl("http://" + self._address + self._api_prefix + "printer/bed/temperature/target") - data = str(temperature) - put_request = QNetworkRequest(url) - put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self._manager.put(put_request, data.encode()) - - ## Updates the target bed temperature from the printer, and emit a signal if it was changed. - # - # /param temperature The new target temperature of the bed. - # /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature - def _updateTargetBedTemperature(self, temperature): - if self._target_bed_temperature == temperature: - return False - self._target_bed_temperature = temperature - self.targetBedTemperatureChanged.emit() - return True - - ## Updates the target hotend temperature from the printer, and emit a signal if it was changed. - # - # /param index The index of the hotend. - # /param temperature The new target temperature of the hotend. - # /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature - def _updateTargetHotendTemperature(self, index, temperature): - if self._target_hotend_temperatures[index] == temperature: - return False - self._target_hotend_temperatures[index] = temperature - self.targetHotendTemperaturesChanged.emit() - return True - - def _stopCamera(self): - self._stream_buffer = b"" - self._stream_buffer_start_index = -1 - - if self._camera_timer.isActive(): - self._camera_timer.stop() - - if self._image_reply: - try: - # disconnect the signal - try: - self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress) - except Exception: - pass - # abort the request if it's not finished - if not self._image_reply.isFinished(): - self._image_reply.close() - except Exception as e: #RuntimeError - pass # It can happen that the wrapped c++ object is already deleted. - self._image_reply = None - self._image_request = None - - def _startCamera(self): - if self._use_stream: - self._startCameraStream() - else: - self._camera_timer.start() - - def _startCameraStream(self): - ## Request new image - url = QUrl("http://" + self._address + ":8080/?action=stream") - self._image_request = QNetworkRequest(url) - self._image_reply = self._manager.get(self._image_request) - self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress) - - def _updateCamera(self): - if not self._manager.networkAccessible(): - return - ## Request new image - url = QUrl("http://" + self._address + ":8080/?action=snapshot") - image_request = QNetworkRequest(url) - self._manager.get(image_request) - self._last_request_time = time() - - ## Set the authentication state. - # \param auth_state \type{AuthState} Enum value representing the new auth state - def setAuthenticationState(self, auth_state): - if auth_state == self._authentication_state: - return # Nothing to do here. - - Logger.log("d", "Attempting to update auth state from %s to %s for printer %s" % (self._authentication_state, auth_state, self._key)) - - if auth_state == AuthState.AuthenticationRequested: - Logger.log("d", "Authentication state changed to authentication requested.") - self.setAcceptsCommands(False) - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. Please approve the access request on the printer.")) - self._authentication_requested_message.show() - self._authentication_request_active = True - self._authentication_timer.start() # Start timer so auth will fail after a while. - elif auth_state == AuthState.Authenticated: - Logger.log("d", "Authentication state changed to authenticated") - self.setAcceptsCommands(True) - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network.")) - self._authentication_requested_message.hide() - if self._authentication_request_active: - self._authentication_succeeded_message.show() - - # Stop waiting for a response - self._authentication_timer.stop() - self._authentication_counter = 0 - - # Once we are authenticated we need to send all material profiles. - self.sendMaterialProfiles() - elif auth_state == AuthState.AuthenticationDenied: - self.setAcceptsCommands(False) - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. No access to control the printer.")) - self._authentication_requested_message.hide() - if self._authentication_request_active: - if self._authentication_timer.remainingTime() > 0: - Logger.log("d", "Authentication state changed to authentication denied before the request timeout.") - self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request was denied on the printer.")) - else: - Logger.log("d", "Authentication state changed to authentication denied due to a timeout") - self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request failed due to a timeout.")) - - self._authentication_failed_message.show() - self._authentication_request_active = False - - # Stop waiting for a response - self._authentication_timer.stop() - self._authentication_counter = 0 - - self._authentication_state = auth_state - self.authenticationStateChanged.emit() - - authenticationStateChanged = pyqtSignal() - - @pyqtProperty(int, notify = authenticationStateChanged) - def authenticationState(self): - return self._authentication_state - - @pyqtSlot() - def requestAuthentication(self, message_id = None, action_id = "Retry"): - if action_id == "Request" or action_id == "Retry": - Logger.log("d", "Requestion authentication for %s due to action %s" % (self._key, action_id)) - self._authentication_failed_message.hide() - self._not_authenticated_message.hide() - self.setAuthenticationState(AuthState.NotAuthenticated) - self._authentication_counter = 0 - self._authentication_requested_message.setProgress(0) - self._authentication_id = None - self._authentication_key = None - self._createNetworkManager() # Re-create network manager to force re-authentication. - - ## Request data from the connected device. - def _update(self): - if self._last_response_time: - time_since_last_response = time() - self._last_response_time - else: - time_since_last_response = 0 - if self._last_request_time: - time_since_last_request = time() - self._last_request_time - else: - time_since_last_request = float("inf") # An irrelevantly large number of seconds - - # Connection is in timeout, check if we need to re-start the connection. - # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. - # Re-creating the QNetworkManager seems to fix this issue. - if self._last_response_time and self._connection_state_before_timeout: - if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count: - self._recreate_network_manager_count += 1 - counter = 0 # Counter to prevent possible indefinite while loop. - # It can happen that we had a very long timeout (multiple times the recreate time). - # In that case we should jump through the point that the next update won't be right away. - while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time and counter < 10: - counter += 1 - self._recreate_network_manager_count += 1 - Logger.log("d", "Timeout lasted over %.0f seconds (%.1fs), re-checking connection.", self._recreate_network_manager_time, time_since_last_response) - self._createNetworkManager() - return - - # Check if we have an connection in the first place. - if not self._manager.networkAccessible(): - if not self._connection_state_before_timeout: - Logger.log("d", "The network connection seems to be disabled. Going into timeout mode") - self._connection_state_before_timeout = self._connection_state - self.setConnectionState(ConnectionState.error) - self._connection_message = Message(i18n_catalog.i18nc("@info:status", - "The connection with the network was lost."), - title = i18n_catalog.i18nc("@info:title", "Connection Status")) - self._connection_message.show() - - if self._progress_message: - self._progress_message.hide() - - # Check if we were uploading something. Abort if this is the case. - # Some operating systems handle this themselves, others give weird issues. - if self._post_reply: - Logger.log("d", "Stopping post upload because the connection was lost.") - self._finalizePostReply() - return - else: - if not self._connection_state_before_timeout: - self._recreate_network_manager_count = 1 - - # Check that we aren't in a timeout state - if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout: - if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time: - # Go into timeout state. - Logger.log("d", "We did not receive a response for %0.1f seconds, so it seems the printer is no longer accessible.", time_since_last_response) - self._connection_state_before_timeout = self._connection_state - self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost. Check your printer to see if it is connected."), - title = i18n_catalog.i18nc("@info:title", "Connection Status")) - self._connection_message.show() - - if self._progress_message: - self._progress_message.hide() - - # Check if we were uploading something. Abort if this is the case. - # Some operating systems handle this themselves, others give weird issues. - if self._post_reply: - Logger.log("d", "Stopping post upload because the connection was lost.") - self._finalizePostReply() - self.setConnectionState(ConnectionState.error) - return - - if self._authentication_state == AuthState.NotAuthenticated: - self._verifyAuthentication() # We don't know if we are authenticated; check if we have correct auth. - elif self._authentication_state == AuthState.AuthenticationRequested: - self._checkAuthentication() # We requested authentication at some point. Check if we got permission. - - ## Request 'general' printer data - url = QUrl("http://" + self._address + self._api_prefix + "printer") - printer_request = QNetworkRequest(url) - self._manager.get(printer_request) - - ## Request print_job data - url = QUrl("http://" + self._address + self._api_prefix + "print_job") - print_job_request = QNetworkRequest(url) - self._manager.get(print_job_request) - - self._last_request_time = time() - - def _finalizePostReply(self): - # Indicate uploading was finished (so another file can be send) - self._write_finished = True - - if self._post_reply is None: - return - - try: - try: - self._post_reply.uploadProgress.disconnect(self._onUploadProgress) - except TypeError: - pass # The disconnection can fail on mac in some cases. Ignore that. - - try: - self._post_reply.finished.disconnect(self._onUploadFinished) - except TypeError: - pass # The disconnection can fail on mac in some cases. Ignore that. - - self._post_reply.abort() - self._post_reply = None - except RuntimeError: - self._post_reply = None # It can happen that the wrapped c++ object is already deleted. - - def _createNetworkManager(self): - if self._manager: - self._manager.finished.disconnect(self._onFinished) - self._manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged) - self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired) - - self._manager = QNetworkAccessManager() - self._manager.finished.connect(self._onFinished) - self._manager.authenticationRequired.connect(self._onAuthenticationRequired) - self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes - - ## Convenience function that gets information from the received json data and converts it to the right internal - # values / variables - def _spliceJSONData(self): - # Check for hotend temperatures - for index in range(0, self._num_extruders): - temperatures = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["temperature"] - self._setHotendTemperature(index, temperatures["current"]) - self._updateTargetHotendTemperature(index, temperatures["target"]) - try: - material_id = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] - except KeyError: - material_id = "" - self._setMaterialId(index, material_id) - try: - hotend_id = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] - except KeyError: - hotend_id = "" - self._setHotendId(index, hotend_id) - - bed_temperatures = self._json_printer_state["bed"]["temperature"] - self._setBedTemperature(bed_temperatures["current"]) - self._updateTargetBedTemperature(bed_temperatures["target"]) - - head_x = self._json_printer_state["heads"][0]["position"]["x"] - head_y = self._json_printer_state["heads"][0]["position"]["y"] - head_z = self._json_printer_state["heads"][0]["position"]["z"] - self._updateHeadPosition(head_x, head_y, head_z) - self._updatePrinterState(self._json_printer_state["status"]) - - if self._processing_preheat_requests: - try: - is_preheating = self._json_printer_state["bed"]["pre_heat"]["active"] - except KeyError: #Old firmware doesn't support that. - pass #Don't update the pre-heat remaining time. - else: - if is_preheating: - try: - remaining_preheat_time = self._json_printer_state["bed"]["pre_heat"]["remaining"] - except KeyError: #Error in firmware. If "active" is supported, "remaining" should also be supported. - pass #Anyway, don't update. - else: - #Only update if time estimate is significantly off (>5000ms). - #Otherwise we get issues with latency causing the timer to count inconsistently. - if abs(self._preheat_bed_timer.remainingTime() - remaining_preheat_time * 1000) > 5000: - self._preheat_bed_timer.setInterval(remaining_preheat_time * 1000) - self._preheat_bed_timer.start() - self.preheatBedRemainingTimeChanged.emit() - else: #Not pre-heating. Must've cancelled. - if self._preheat_bed_timer.isActive(): - self._preheat_bed_timer.setInterval(0) - self._preheat_bed_timer.stop() - self.preheatBedRemainingTimeChanged.emit() - - def close(self): - Logger.log("d", "Closing connection of printer %s with ip %s", self._key, self._address) - self._updateJobState("") - self.setConnectionState(ConnectionState.closed) - if self._progress_message: - self._progress_message.hide() - - # Reset authentication state - self._authentication_requested_message.hide() - self.setAuthenticationState(AuthState.NotAuthenticated) - self._authentication_counter = 0 - self._authentication_timer.stop() - - self._authentication_requested_message.hide() - self._authentication_failed_message.hide() - self._authentication_succeeded_message.hide() - - # Reset stored material & hotend data. - self._material_ids = [""] * self._num_extruders - self._hotend_ids = [""] * self._num_extruders - - if self._error_message: - self._error_message.hide() - - # Reset timeout state - self._connection_state_before_timeout = None - self._last_response_time = time() - self._last_request_time = None - - # Stop update timers - self._update_timer.stop() - - self.stopCamera() - - ## Request the current scene to be sent to a network-connected printer. - # - # \param nodes A collection of scene nodes to send. This is ignored. - # \param file_name \type{string} A suggestion for a file name to write. - # This is ignored. - # \param filter_by_machine Whether to filter MIME types by machine. This - # is ignored. - # \param kwargs Keyword arguments. - def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): - - if self._printer_state not in ["idle", ""]: - self._error_message = Message( - i18n_catalog.i18nc("@info:status", "Unable to start a new print job, printer is busy. Current printer status is %s.") % self._printer_state, - title = i18n_catalog.i18nc("@info:title", "Printer Status")) - self._error_message.show() - return - elif self._authentication_state != AuthState.Authenticated: - self._not_authenticated_message.show() - Logger.log("d", "Attempting to perform an action without authentication for printer %s. Auth state is %s", self._key, self._authentication_state) - return - - Application.getInstance().getController().setActiveStage("MonitorStage") - self._print_finished = True - self.writeStarted.emit(self) - self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") - - print_information = Application.getInstance().getPrintInformation() - warnings = [] # There might be multiple things wrong. Keep a list of all the stuff we need to warn about. - - # Only check for mistakes if there is material length information. - if print_information.materialLengths: - # Check if PrintCores / materials are loaded at all. Any failure in these results in an Error. - for index in range(0, self._num_extruders): - if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0: - if self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] == "": - Logger.log("e", "No cartridge loaded in slot %s, unable to start print", index + 1) - self._error_message = Message( - i18n_catalog.i18nc("@info:status", "Unable to start a new print job. No Printcore loaded in slot {0}".format(index + 1)), - title = i18n_catalog.i18nc("@info:title", "Error")) - self._error_message.show() - return - if self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] == "": - Logger.log("e", "No material loaded in slot %s, unable to start print", index + 1) - self._error_message = Message( - i18n_catalog.i18nc("@info:status", - "Unable to start a new print job. No material loaded in slot {0}".format(index + 1)), - title = i18n_catalog.i18nc("@info:title", "Error")) - self._error_message.show() - return - - for index in range(0, self._num_extruders): - # Check if there is enough material. Any failure in these results in a warning. - material_length = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["length_remaining"] - if material_length != -1 and index < len(print_information.materialLengths) and print_information.materialLengths[index] > material_length: - Logger.log("w", "Printer reports that there is not enough material left for extruder %s. We need %s and the printer has %s", index + 1, print_information.materialLengths[index], material_length) - warnings.append(i18n_catalog.i18nc("@label", "Not enough material for spool {0}.").format(index+1)) - - # Check if the right cartridges are loaded. Any failure in these results in a warning. - extruder_manager = cura.Settings.ExtruderManager.ExtruderManager.getInstance() - if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0: - variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"}) - core_name = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] - if variant: - if variant.getName() != core_name: - Logger.log("w", "Extruder %s has a different Cartridge (%s) as Cura (%s)", index + 1, core_name, variant.getName()) - warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {0}, Printer: {1}) selected for extruder {2}".format(variant.getName(), core_name, index + 1))) - - material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"}) - if material: - remote_material_guid = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] - if material.getMetaDataEntry("GUID") != remote_material_guid: - Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, - remote_material_guid, - material.getMetaDataEntry("GUID")) - - remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material", GUID = remote_material_guid, read_only = True) - remote_material_name = "Unknown" - if remote_materials: - remote_material_name = remote_materials[0].getName() - warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1)) - - try: - is_offset_calibrated = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["offset"]["state"] == "valid" - except KeyError: # Older versions of the API don't expose the offset property, so we must asume that all is well. - is_offset_calibrated = True - - if not is_offset_calibrated: - warnings.append(i18n_catalog.i18nc("@label", "PrintCore {0} is not properly calibrated. XY calibration needs to be performed on the printer.").format(index + 1)) - else: - Logger.log("w", "There was no material usage found. No check to match used material with machine is done.") - - if warnings: - text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?") - informative_text = i18n_catalog.i18nc("@label", "There is a mismatch between the configuration or calibration of the printer and Cura. " - "For the best result, always slice for the PrintCores and materials that are inserted in your printer.") - detailed_text = "" - for warning in warnings: - detailed_text += warning + "\n" - - Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), - text, - informative_text, - detailed_text, - buttons=QMessageBox.Yes + QMessageBox.No, - icon=QMessageBox.Question, - callback=self._configurationMismatchMessageCallback - ) - return - - self.startPrint() - - def _configurationMismatchMessageCallback(self, button): - def delayedCallback(): - if button == QMessageBox.Yes: - self.startPrint() - else: - Application.getInstance().getController().setActiveStage("PrepareStage") - # For some unknown reason Cura on OSX will hang if we do the call back code - # immediately without first returning and leaving QML's event system. - QTimer.singleShot(100, delayedCallback) - - def isConnected(self): - return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error - - ## Start requesting data from printer - def connect(self): - # Don't allow to connect to a printer with a faulty connection state. - # For instance when switching printers but the printer is disconnected from the network - if self._connection_state == ConnectionState.error: - return - - if self.isConnected(): - self.close() # Close previous connection - - self._createNetworkManager() - - self._last_response_time = time() # Ensure we reset the time when trying to connect (again) - - self.setConnectionState(ConnectionState.connecting) - self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. - if not self._use_stream: - self._updateCamera() - Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) - - ## Check if this machine was authenticated before. - self._authentication_id = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_id", None) - self._authentication_key = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_key", None) - - if self._authentication_id is None and self._authentication_key is None: - Logger.log("d", "No authentication found in metadata.") - else: - Logger.log("d", "Loaded authentication id %s and key %s from the metadata entry for printer %s", self._authentication_id, self._getSafeAuthKey(), self._key) - - self._update_timer.start() - - ## Stop requesting data from printer - def disconnect(self): - Logger.log("d", "Connection with printer %s with ip %s stopped", self._key, self._address) - self.close() - - newImage = pyqtSignal() - - @pyqtProperty(QUrl, notify = newImage) - def cameraImage(self): - self._camera_image_id += 1 - # There is an image provider that is called "camera". In order to ensure that the image qml object, that - # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl - # as new (instead of relying on cached version and thus forces an update. - temp = "image://camera/" + str(self._camera_image_id) - return QUrl(temp, QUrl.TolerantMode) - - def getCameraImage(self): - return self._camera_image - - def _setJobState(self, job_state): - self._last_command = job_state - url = QUrl("http://" + self._address + self._api_prefix + "print_job/state") - put_request = QNetworkRequest(url) - put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - data = "{\"target\": \"%s\"}" % job_state - self._manager.put(put_request, data.encode()) - - ## Convenience function to get the username from the OS. - # The code was copied from the getpass module, as we try to use as little dependencies as possible. - def _getUserName(self): - for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): - user = os.environ.get(name) - if user: - return user - return "Unknown User" # Couldn't find out username. - - def _progressMessageActionTrigger(self, message_id = None, action_id = None): - if action_id == "Abort": - Logger.log("d", "User aborted sending print to remote.") - self._progress_message.hide() - self._compressing_print = False - self._write_finished = True # post_reply does not always exist, so make sure we unblock writing - if self._post_reply: - self._finalizePostReply() - Application.getInstance().getController().setActiveStage("PrepareStage") - - ## Attempt to start a new print. - # This function can fail to actually start a print due to not being authenticated or another print already - # being in progress. - def startPrint(self): - - # Check if we're already writing - if not self._write_finished: - self._error_message = Message( - i18n_catalog.i18nc("@info:status", - "Sending new jobs (temporarily) blocked, still sending the previous print job.")) - self._error_message.show() - return - - # Indicate we're starting a new write action, is set back to True at the end of this method - self._write_finished = False - - try: - self._send_gcode_start = time() - self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data")) - self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") - self._progress_message.actionTriggered.connect(self._progressMessageActionTrigger) - self._progress_message.show() - Logger.log("d", "Started sending g-code to remote printer.") - self._compressing_print = True - ## Mash the data into single string - - max_chars_per_line = 1024 * 1024 / 4 # 1 / 4 MB - - byte_array_file_data = b"" - batched_line = "" - - def _compress_data_and_notify_qt(data_to_append): - compressed_data = gzip.compress(data_to_append.encode("utf-8")) - self._progress_message.setProgress(-1) # Tickle the message so that it's clear that it's still being used. - QCoreApplication.processEvents() # Ensure that the GUI does not freeze. - # Pretend that this is a response, as zipping might take a bit of time. - self._last_response_time = time() - return compressed_data - - for line in self._gcode: - if not self._compressing_print: - self._progress_message.hide() - return # Stop trying to zip, abort was called. - - if self._use_gzip: - batched_line += line - # if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file. - # Compressing line by line in this case is extremely slow, so we need to batch them. - if len(batched_line) < max_chars_per_line: - continue - - byte_array_file_data += _compress_data_and_notify_qt(batched_line) - batched_line = "" - else: - byte_array_file_data += line.encode("utf-8") - - # don't miss the last batch if it's there - if self._use_gzip: - if batched_line: - byte_array_file_data += _compress_data_and_notify_qt(batched_line) - - if self._use_gzip: - file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName - else: - file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName - - self._compressing_print = False - ## Create multi_part request - self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) - - ## Create part (to be placed inside multipart) - self._post_part = QHttpPart() - self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, - "form-data; name=\"file\"; filename=\"%s\"" % file_name) - self._post_part.setBody(byte_array_file_data) - self._post_multi_part.append(self._post_part) - - url = QUrl("http://" + self._address + self._api_prefix + "print_job") - - ## Create the QT request - self._post_request = QNetworkRequest(url) - - ## Post request + data - self._post_reply = self._manager.post(self._post_request, self._post_multi_part) - self._post_reply.uploadProgress.connect(self._onUploadProgress) - self._post_reply.finished.connect(self._onUploadFinished) # used to unblock new write actions - - except IOError: - self._progress_message.hide() - self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to printer. Is another job still active?"), - title = i18n_catalog.i18nc("@info:title", "Warning")) - self._error_message.show() - except Exception as e: - self._progress_message.hide() - Logger.log("e", "An exception occurred in network connection: %s" % str(e)) - - ## Verify if we are authenticated to make requests. - def _verifyAuthentication(self): - url = QUrl("http://" + self._address + self._api_prefix + "auth/verify") - request = QNetworkRequest(url) - self._manager.get(request) - - ## Check if the authentication request was allowed by the printer. - def _checkAuthentication(self): - Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) - self._manager.get(QNetworkRequest(QUrl("http://" + self._address + self._api_prefix + "auth/check/" + str(self._authentication_id)))) - - ## Request a authentication key from the printer so we can be authenticated - def _requestAuthentication(self): - url = QUrl("http://" + self._address + self._api_prefix + "auth/request") - request = QNetworkRequest(url) - request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self._authentication_key = None - self._authentication_id = None - self._manager.post(request, json.dumps({"application": "Cura-" + Application.getInstance().getVersion(), "user": self._getUserName()}).encode()) - self.setAuthenticationState(AuthState.AuthenticationRequested) - - ## Send all material profiles to the printer. - def sendMaterialProfiles(self): - for container in UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material"): - try: - xml_data = container.serialize() - if xml_data == "" or xml_data is None: - continue - - names = ContainerManager.getInstance().getLinkedMaterials(container.getId()) - if names: - # There are other materials that share this GUID. - if not container.isReadOnly(): - continue # If it's not readonly, it's created by user, so skip it. - - material_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) - - material_part = QHttpPart() - file_name = "none.xml" - material_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\";filename=\"%s\"" % file_name) - material_part.setBody(xml_data.encode()) - material_multi_part.append(material_part) - url = QUrl("http://" + self._address + self._api_prefix + "materials") - material_post_request = QNetworkRequest(url) - reply = self._manager.post(material_post_request, material_multi_part) - - # Keep reference to material_part, material_multi_part and reply so the garbage collector won't touch them. - self._material_post_objects[id(reply)] = (material_part, material_multi_part, reply) - except NotImplementedError: - # If the material container is not the most "generic" one it can't be serialized an will raise a - # NotImplementedError. We can simply ignore these. - pass - - ## Handler for all requests that have finished. - def _onFinished(self, reply): - if reply.error() == QNetworkReply.TimeoutError: - Logger.log("w", "Received a timeout on a request to the printer") - self._connection_state_before_timeout = self._connection_state - # Check if we were uploading something. Abort if this is the case. - # Some operating systems handle this themselves, others give weird issues. - if self._post_reply: - self._finalizePostReply() - Logger.log("d", "Uploading of print failed after %s", time() - self._send_gcode_start) - self._progress_message.hide() - - self.setConnectionState(ConnectionState.error) - return - - if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. - Logger.log("d", "We got a response (%s) from the server after %0.1f of silence. Going back to previous state %s", reply.url().toString(), time() - self._last_response_time, self._connection_state_before_timeout) - - # Camera was active before timeout. Start it again - if self._camera_active: - self._startCamera() - - self.setConnectionState(self._connection_state_before_timeout) - self._connection_state_before_timeout = None - - if reply.error() == QNetworkReply.NoError: - self._last_response_time = time() - - status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - if not status_code: - if self._connection_state != ConnectionState.error: - Logger.log("d", "A reply from %s did not have status code.", reply.url().toString()) - # Received no or empty reply - return - reply_url = reply.url().toString() - - if reply.operation() == QNetworkAccessManager.GetOperation: - # "printer" is also in "printers", therefore _api_prefix is added. - if self._api_prefix + "printer" in reply_url: # Status update from printer. - if status_code == 200: - if self._connection_state == ConnectionState.connecting: - self.setConnectionState(ConnectionState.connected) - try: - self._json_printer_state = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received an invalid printer state message: Not valid JSON.") - return - self._spliceJSONData() - - # Hide connection error message if the connection was restored - if self._connection_message: - self._connection_message.hide() - self._connection_message = None - else: - Logger.log("w", "We got an unexpected status (%s) while requesting printer state", status_code) - pass # TODO: Handle errors - elif self._api_prefix + "print_job" in reply_url: # Status update from print_job: - if status_code == 200: - try: - json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received an invalid print job state message: Not valid JSON.") - return - progress = json_data["progress"] - ## If progress is 0 add a bit so another print can't be sent. - if progress == 0: - progress += 0.001 - elif progress == 1: - self._print_finished = True - else: - self._print_finished = False - self.setProgress(progress * 100) - - state = json_data["state"] - - # There is a short period after aborting or finishing a print where the printer - # reports a "none" state (but the printer is not ready to receive a print) - # If this happens before the print has reached progress == 1, the print has - # been aborted. - if state == "none" or state == "": - if self._last_command == "abort": - self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Aborting print...")) - state = "error" - else: - state = "printing" - if state == "wait_cleanup" and self._last_command == "abort": - # Keep showing the "aborted" error state until after the buildplate has been cleaned - self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Print aborted. Please check the printer")) - state = "error" - - # NB/TODO: the following two states are intentionally added for future proofing the i18n strings - # but are currently non-functional - if state == "!pausing": - self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Pausing print...")) - if state == "!resuming": - self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Resuming print...")) - - self._updateJobState(state) - self.setTimeElapsed(json_data["time_elapsed"]) - self.setTimeTotal(json_data["time_total"]) - self.setJobName(json_data["name"]) - elif status_code == 404: - self.setProgress(0) # No print job found, so there can't be progress or other data. - self._updateJobState("") - self.setErrorText("") - self.setTimeElapsed(0) - self.setTimeTotal(0) - self.setJobName("") - else: - Logger.log("w", "We got an unexpected status (%s) while requesting print job state", status_code) - elif "snapshot" in reply_url: # Status update from image: - if status_code == 200: - self._camera_image.loadFromData(reply.readAll()) - self.newImage.emit() - elif "auth/verify" in reply_url: # Answer when requesting authentication - if status_code == 401: - if self._authentication_state != AuthState.AuthenticationRequested: - # Only request a new authentication when we have not already done so. - Logger.log("i", "Not authenticated (Current auth state is %s). Attempting to request authentication for printer %s", self._authentication_state, self._key ) - self._requestAuthentication() - elif status_code == 403: - # If we already had an auth (eg; didn't request one), we only need a single 403 to see it as denied. - if self._authentication_state != AuthState.AuthenticationRequested: - Logger.log("d", "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s", self._authentication_state) - self.setAuthenticationState(AuthState.AuthenticationDenied) - elif status_code == 200: - self.setAuthenticationState(AuthState.Authenticated) - global_container_stack = Application.getInstance().getGlobalContainerStack() - - ## Save authentication details. - if global_container_stack: - if "network_authentication_key" in global_container_stack.getMetaData(): - global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key) - else: - global_container_stack.addMetaDataEntry("network_authentication_key", self._authentication_key) - if "network_authentication_id" in global_container_stack.getMetaData(): - global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id) - else: - global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id) - Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) - Application.getInstance().saveStack(global_container_stack) # Force save so we are sure the data is not lost. - else: - Logger.log("w", "Unable to save authentication for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) - - # Request 'system' printer data once, when we know we have authentication, so we know we can set the system time. - url = QUrl("http://" + self._address + self._api_prefix + "system") - system_data_request = QNetworkRequest(url) - self._manager.get(system_data_request) - - else: # Got a response that we didn't expect, so something went wrong. - Logger.log("e", "While trying to authenticate, we got an unexpected response: %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) - self.setAuthenticationState(AuthState.NotAuthenticated) - - elif "auth/check" in reply_url: # Check if we are authenticated (user can refuse this!) - try: - data = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.") - return - if data.get("message", "") == "authorized": - Logger.log("i", "Authentication was approved") - self._verifyAuthentication() # Ensure that the verification is really used and correct. - elif data.get("message", "") == "unauthorized": - Logger.log("i", "Authentication was denied.") - self.setAuthenticationState(AuthState.AuthenticationDenied) - else: - pass - - elif self._api_prefix + "system" in reply_url: - # Check if the printer has time, and if this has a valid system time. - try: - data = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.") - return - if "time" in data and "utc" in data["time"]: - try: - printer_time = gmtime(float(data["time"]["utc"])) - Logger.log("i", "Printer has system time of: %s", str(printer_time)) - except ValueError: - printer_time = None - if printer_time is not None and printer_time.tm_year < 1990: - # The system time is not valid, sync our current system time to it, so we at least have some reasonable time in the printer. - Logger.log("w", "Printer system time invalid, setting system time") - url = QUrl("http://" + self._address + self._api_prefix + "system/time/utc") - put_request = QNetworkRequest(url) - put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self._manager.put(put_request, str(time()).encode()) - - elif reply.operation() == QNetworkAccessManager.PostOperation: - if "/auth/request" in reply_url: - # We got a response to requesting authentication. - try: - data = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.") - return - global_container_stack = Application.getInstance().getGlobalContainerStack() - if global_container_stack: # Remove any old data. - Logger.log("d", "Removing old network authentication data for %s as a new one was requested.", self._key) - global_container_stack.removeMetaDataEntry("network_authentication_key") - global_container_stack.removeMetaDataEntry("network_authentication_id") - Application.getInstance().saveStack(global_container_stack) # Force saving so we don't keep wrong auth data. - - self._authentication_key = data["key"] - self._authentication_id = data["id"] - Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.", self._authentication_id, self._getSafeAuthKey()) - - # Check if the authentication is accepted. - self._checkAuthentication() - elif "materials" in reply_url: - # Remove cached post request items. - del self._material_post_objects[id(reply)] - elif "print_job" in reply_url: - self._onUploadFinished() # Make sure the upload flag is reset as reply.finished is not always triggered - try: - reply.uploadProgress.disconnect(self._onUploadProgress) - except: - pass - try: - reply.finished.disconnect(self._onUploadFinished) - except: - pass - Logger.log("d", "Uploading of print succeeded after %s", time() - self._send_gcode_start) - # Only reset the _post_reply if it was the same one. - if reply == self._post_reply: - self._post_reply = None - self._progress_message.hide() - - elif reply.operation() == QNetworkAccessManager.PutOperation: - if "printer/bed/pre_heat" in reply_url: #Pre-heat command has completed. Re-enable syncing pre-heating. - self._processing_preheat_requests = True - if status_code in [200, 201, 202, 204]: - pass # Request was successful! - else: - Logger.log("d", "Something went wrong when trying to update data of API (%s). Message: %s Statuscode: %s", reply_url, reply.readAll(), status_code) - else: - Logger.log("d", "NetworkPrinterOutputDevice got an unhandled operation %s", reply.operation()) - - def _onStreamDownloadProgress(self, bytes_received, bytes_total): - # An MJPG stream is (for our purpose) a stream of concatenated JPG images. - # JPG images start with the marker 0xFFD8, and end with 0xFFD9 - if self._image_reply is None: - return - self._stream_buffer += self._image_reply.readAll() - - if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger - Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...") - self._stopCamera() # resets stream buffer and start index - self._startCamera() - return - - if self._stream_buffer_start_index == -1: - self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8') - stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9') - # If this happens to be more than a single frame, then so be it; the JPG decoder will - # ignore the extra data. We do it like this in order not to get a buildup of frames - - if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1: - jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2] - self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:] - self._stream_buffer_start_index = -1 - - self._camera_image.loadFromData(jpg_data) - self.newImage.emit() - - def _onUploadProgress(self, bytes_sent, bytes_total): - if bytes_total > 0: - new_progress = bytes_sent / bytes_total * 100 - # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get - # timeout responses if this happens. - self._last_response_time = time() - if new_progress > self._progress_message.getProgress(): - self._progress_message.show() # Ensure that the message is visible. - self._progress_message.setProgress(bytes_sent / bytes_total * 100) - else: - self._progress_message.setProgress(0) - self._progress_message.hide() - - ## Allow new write actions (uploads) again when uploading is finished. - def _onUploadFinished(self): - self._write_finished = True - - ## Let the user decide if the hotends and/or material should be synced with the printer - def materialHotendChangedMessage(self, callback): - Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"), - i18n_catalog.i18nc("@label", - "Would you like to use your current printer configuration in Cura?"), - i18n_catalog.i18nc("@label", - "The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."), - buttons=QMessageBox.Yes + QMessageBox.No, - icon=QMessageBox.Question, - callback=callback - ) - - ## Convenience function to "blur" out all but the last 5 characters of the auth key. - # This can be used to debug print the key, without it compromising the security. - def _getSafeAuthKey(self): - if self._authentication_key is not None: - result = self._authentication_key[-5:] - result = "********" + result - return result - return self._authentication_key diff --git a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py deleted file mode 100644 index 46538f1af9..0000000000 --- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py +++ /dev/null @@ -1,357 +0,0 @@ -# Copyright (c) 2017 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import time -import json -from queue import Queue -from threading import Event, Thread - -from PyQt5.QtCore import QObject, pyqtSlot -from PyQt5.QtCore import QUrl -from PyQt5.QtGui import QDesktopServices -from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager -from UM.Application import Application -from UM.Logger import Logger -from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin -from UM.Preferences import Preferences -from UM.Signal import Signal, signalemitter -from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo # type: ignore - -from . import NetworkPrinterOutputDevice, NetworkClusterPrinterOutputDevice - - -## This plugin handles the connection detection & creation of output device objects for the UM3 printer. -# Zero-Conf is used to detect printers, which are saved in a dict. -# If we discover a printer that has the same key as the active machine instance a connection is made. -@signalemitter -class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin): - def __init__(self): - super().__init__() - self._zero_conf = None - self._browser = None - self._printers = {} - self._cluster_printers_seen = {} # do not forget a cluster printer when we have seen one, to not 'downgrade' from Connect to legacy printer - - self._api_version = "1" - self._api_prefix = "/api/v" + self._api_version + "/" - self._cluster_api_version = "1" - self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/" - - self._network_manager = QNetworkAccessManager() - self._network_manager.finished.connect(self._onNetworkRequestFinished) - - # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces - # authentication requests. - self._old_printers = [] - - # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. - self.addPrinterSignal.connect(self.addPrinter) - self.removePrinterSignal.connect(self.removePrinter) - Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections) - - # Get list of manual printers from preferences - self._preferences = Preferences.getInstance() - self._preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames - self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",") - - self._network_requests_buffer = {} # store api responses until data is complete - - # The zeroconf service changed requests are handled in a separate thread, so we can re-schedule the requests - # which fail to get detailed service info. - # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick - # them up and process them. - self._service_changed_request_queue = Queue() - self._service_changed_request_event = Event() - self._service_changed_request_thread = Thread(target = self._handleOnServiceChangedRequests, - daemon = True) - self._service_changed_request_thread.start() - - addPrinterSignal = Signal() - removePrinterSignal = Signal() - printerListChanged = Signal() - - ## Start looking for devices on network. - def start(self): - self.startDiscovery() - - def startDiscovery(self): - self.stop() - if self._browser: - self._browser.cancel() - self._browser = None - self._old_printers = [printer_name for printer_name in self._printers] - self._printers = {} - self.printerListChanged.emit() - # After network switching, one must make a new instance of Zeroconf - # On windows, the instance creation is very fast (unnoticable). Other platforms? - self._zero_conf = Zeroconf() - self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._appendServiceChangedRequest]) - - # Look for manual instances from preference - for address in self._manual_instances: - if address: - self.addManualPrinter(address) - - def addManualPrinter(self, address): - if address not in self._manual_instances: - self._manual_instances.append(address) - self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances)) - - instance_name = "manual:%s" % address - properties = { - b"name": address.encode("utf-8"), - b"address": address.encode("utf-8"), - b"manual": b"true", - b"incomplete": b"true" - } - - if instance_name not in self._printers: - # Add a preliminary printer instance - self.addPrinter(instance_name, address, properties) - - self.checkManualPrinter(address) - self.checkClusterPrinter(address) - - def removeManualPrinter(self, key, address = None): - if key in self._printers: - if not address: - address = self._printers[key].ipAddress - self.removePrinter(key) - - if address in self._manual_instances: - self._manual_instances.remove(address) - self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances)) - - def checkManualPrinter(self, address): - # Check if a printer exists at this address - # If a printer responds, it will replace the preliminary printer created above - # origin=manual is for tracking back the origin of the call - url = QUrl("http://" + address + self._api_prefix + "system?origin=manual_name") - name_request = QNetworkRequest(url) - self._network_manager.get(name_request) - - def checkClusterPrinter(self, address): - cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/?origin=check_cluster") - cluster_request = QNetworkRequest(cluster_url) - self._network_manager.get(cluster_request) - - ## Handler for all requests that have finished. - def _onNetworkRequestFinished(self, reply): - reply_url = reply.url().toString() - status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - - if reply.operation() == QNetworkAccessManager.GetOperation: - address = reply.url().host() - if "origin=manual_name" in reply_url: # Name returned from printer. - if status_code == 200: - - try: - system_info = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.JSONDecodeError: - Logger.log("e", "Printer returned invalid JSON.") - return - except UnicodeDecodeError: - Logger.log("e", "Printer returned incorrect UTF-8.") - return - - if address not in self._network_requests_buffer: - self._network_requests_buffer[address] = {} - self._network_requests_buffer[address]["system"] = system_info - elif "origin=check_cluster" in reply_url: - if address not in self._network_requests_buffer: - self._network_requests_buffer[address] = {} - if status_code == 200: - # We know it's a cluster printer - Logger.log("d", "Cluster printer detected: [%s]", reply.url()) - - try: - cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.JSONDecodeError: - Logger.log("e", "Printer returned invalid JSON.") - return - except UnicodeDecodeError: - Logger.log("e", "Printer returned incorrect UTF-8.") - return - - self._network_requests_buffer[address]["cluster"] = True - self._network_requests_buffer[address]["cluster_size"] = len(cluster_printers_list) - else: - Logger.log("d", "This url is not from a cluster printer: [%s]", reply.url()) - self._network_requests_buffer[address]["cluster"] = False - - # Both the system call and cluster call are finished - if (address in self._network_requests_buffer and - "system" in self._network_requests_buffer[address] and - "cluster" in self._network_requests_buffer[address]): - - instance_name = "manual:%s" % address - system_info = self._network_requests_buffer[address]["system"] - machine = "unknown" - if "variant" in system_info: - variant = system_info["variant"] - if variant == "Ultimaker 3": - machine = "9066" - elif variant == "Ultimaker 3 Extended": - machine = "9511" - - properties = { - b"name": system_info["name"].encode("utf-8"), - b"address": address.encode("utf-8"), - b"firmware_version": system_info["firmware"].encode("utf-8"), - b"manual": b"true", - b"machine": machine.encode("utf-8") - } - - if self._network_requests_buffer[address]["cluster"]: - properties[b"cluster_size"] = self._network_requests_buffer[address]["cluster_size"] - - if instance_name in self._printers: - # Only replace the printer if it is still in the list of (manual) printers - self.removePrinter(instance_name) - self.addPrinter(instance_name, address, properties) - - del self._network_requests_buffer[address] - - ## Stop looking for devices on network. - def stop(self): - if self._zero_conf is not None: - Logger.log("d", "zeroconf close...") - self._zero_conf.close() - - def getPrinters(self): - return self._printers - - def reCheckConnections(self): - active_machine = Application.getInstance().getGlobalContainerStack() - if not active_machine: - return - - for key in self._printers: - if key == active_machine.getMetaDataEntry("um_network_key"): - if not self._printers[key].isConnected(): - Logger.log("d", "Connecting [%s]..." % key) - self._printers[key].connect() - self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) - else: - if self._printers[key].isConnected(): - Logger.log("d", "Closing connection [%s]..." % key) - self._printers[key].close() - self._printers[key].connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged) - - ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. - def addPrinter(self, name, address, properties): - cluster_size = int(properties.get(b"cluster_size", -1)) - if cluster_size >= 0: - printer = NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice( - name, address, properties, self._api_prefix) - else: - printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix) - self._printers[printer.getKey()] = printer - self._cluster_printers_seen[printer.getKey()] = name # Cluster printers that may be temporary unreachable or is rebooted keep being stored here - global_container_stack = Application.getInstance().getGlobalContainerStack() - if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"): - if printer.getKey() not in self._old_printers: # Was the printer already connected, but a re-scan forced? - Logger.log("d", "addPrinter, connecting [%s]..." % printer.getKey()) - self._printers[printer.getKey()].connect() - printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) - self.printerListChanged.emit() - - def removePrinter(self, name): - printer = self._printers.pop(name, None) - if printer: - if printer.isConnected(): - printer.disconnect() - printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged) - Logger.log("d", "removePrinter, disconnecting [%s]..." % name) - self.printerListChanged.emit() - - ## Handler for when the connection state of one of the detected printers changes - def _onPrinterConnectionStateChanged(self, key): - if key not in self._printers: - return - if self._printers[key].isConnected(): - self.getOutputDeviceManager().addOutputDevice(self._printers[key]) - else: - self.getOutputDeviceManager().removeOutputDevice(key) - - ## Handler for zeroConf detection. - # Return True or False indicating if the process succeeded. - def _onServiceChanged(self, zeroconf, service_type, name, state_change): - if state_change == ServiceStateChange.Added: - Logger.log("d", "Bonjour service added: %s" % name) - - # First try getting info from zeroconf cache - info = ServiceInfo(service_type, name, properties = {}) - for record in zeroconf.cache.entries_with_name(name.lower()): - info.update_record(zeroconf, time.time(), record) - - for record in zeroconf.cache.entries_with_name(info.server): - info.update_record(zeroconf, time.time(), record) - if info.address: - break - - # Request more data if info is not complete - if not info.address: - Logger.log("d", "Trying to get address of %s", name) - info = zeroconf.get_service_info(service_type, name) - - if info: - type_of_device = info.properties.get(b"type", None) - if type_of_device: - if type_of_device == b"printer": - address = '.'.join(map(lambda n: str(n), info.address)) - self.addPrinterSignal.emit(str(name), address, info.properties) - else: - Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device ) - else: - Logger.log("w", "Could not get information about %s" % name) - return False - - elif state_change == ServiceStateChange.Removed: - Logger.log("d", "Bonjour service removed: %s" % name) - self.removePrinterSignal.emit(str(name)) - - return True - - ## Appends a service changed request so later the handling thread will pick it up and processes it. - def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change): - # append the request and set the event so the event handling thread can pick it up - item = (zeroconf, service_type, name, state_change) - self._service_changed_request_queue.put(item) - self._service_changed_request_event.set() - - def _handleOnServiceChangedRequests(self): - while True: - # wait for the event to be set - self._service_changed_request_event.wait(timeout = 5.0) - # stop if the application is shutting down - if Application.getInstance().isShuttingDown(): - return - - self._service_changed_request_event.clear() - - # handle all pending requests - reschedule_requests = [] # a list of requests that have failed so later they will get re-scheduled - while not self._service_changed_request_queue.empty(): - request = self._service_changed_request_queue.get() - zeroconf, service_type, name, state_change = request - try: - result = self._onServiceChanged(zeroconf, service_type, name, state_change) - if not result: - reschedule_requests.append(request) - except Exception: - Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled", - service_type, name) - reschedule_requests.append(request) - - # re-schedule the failed requests if any - if reschedule_requests: - for request in reschedule_requests: - self._service_changed_request_queue.put(request) - - @pyqtSlot() - def openControlPanel(self): - Logger.log("d", "Opening print jobs web UI...") - selected_device = self.getOutputDeviceManager().getActiveDevice() - if isinstance(selected_device, NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice): - QDesktopServices.openUrl(QUrl(selected_device.getPrintJobsUrl())) diff --git a/plugins/UM3NetworkPrinting/__init__.py b/plugins/UM3NetworkPrinting/__init__.py index 6dd86a16d2..b68086cb75 100644 --- a/plugins/UM3NetworkPrinting/__init__.py +++ b/plugins/UM3NetworkPrinting/__init__.py @@ -1,6 +1,6 @@ -# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from . import NetworkPrinterOutputDevicePlugin + from . import DiscoverUM3Action from UM.i18n import i18nCatalog catalog = i18nCatalog("cura")